From: Lady Date: Sat, 23 Jul 2022 23:13:27 +0000 (-0700) Subject: Add LazyLoader class X-Git-Tag: 0.1.2^0 X-Git-Url: https://git.ladys.computer/Pisces/commitdiff_plain/2edbddced262b90f8422e77fe8ec61a9f8c82e68?ds=sidebyside;hp=e8d0d3ab4f8a844e15873345c0564929c80fdb74 Add LazyLoader class --- diff --git a/object.js b/object.js index 578f84f..a34423e 100644 --- a/object.js +++ b/object.js @@ -10,6 +10,64 @@ import { bind, call } from "./function.js"; import { toPrimitive, type } from "./value.js"; +/** + * An object whose properties are lazy‐loaded from the methods on the + * own properties of the provided object. + * + * This is useful when you are looking to reference properties on + * objects which, due to module dependency graphs, cannot be guaranteed + * to have been initialized yet. + * + * The resulting properties will have the same attributes (regarding + * configurability, enumerability, and writability) as the + * corresponding properties on the methods object. If a property is + * marked as writable, the method will never be called if it is set + * before it is gotten. By necessity, the resulting properties are all + * configurable before they are accessed for the first time. + * + * Methods will be called with the resulting object as their this + * value. + * + * LazyLoader objects have the same prototype as the passed methods + * object. + */ +export class LazyLoader extends null { + /** Constructs a new LazyLoader object. */ + constructor(loadMethods) { + const result = objectCreate(getPrototype(loadMethods)); + const methodKeys = getOwnPropertyKeys(loadMethods); + for (let index = 0; index < methodKeys.length; ++index) { + const methodKey = methodKeys[index]; + const { configurable, enumerable, writable } = + getOwnPropertyDescriptor(loadMethods, methodKey); + defineOwnProperty(result, methodKey, { + configurable: true, + enumerable, + get: () => { + const value = call(loadMethods[methodKey], result, []); + defineOwnProperty(result, methodKey, { + configurable, + enumerable, + value, + writable, + }); + return value; + }, + set: writable + ? ($) => + defineOwnProperty(result, methodKey, { + configurable, + enumerable, + value: $, + writable, + }) + : void {}, + }); + } + return result; + } +} + /** * A property descriptor object. * diff --git a/object.test.js b/object.test.js index da890d3..2b6cbb8 100644 --- a/object.test.js +++ b/object.test.js @@ -22,12 +22,251 @@ import { defineOwnProperties, deleteOwnProperty, frozenCopy, + LazyLoader, PropertyDescriptor, setPropertyValue, toObject, toPropertyKey, } from "./object.js"; +describe("LazyLoader", () => { + const symbol = Symbol(); + const prototype = {}; + const etaoinMethod = spy(() => "success"); + const shrdluMethod = spy(() => "success"); + const cmfwypMethod = spy(() => "success"); + const vbgkqjMethod = spy(() => "success"); + const methodsObject = Object.create( + prototype, + { + etaoin: { + configurable: false, + enumerable: true, + value: etaoinMethod, + writable: false, + }, + shrdlu: { + configurable: true, + enumerable: false, + value: shrdluMethod, + writable: false, + }, + cmfwyp: { + configurable: true, + enumerable: false, + get() { + return cmfwypMethod; + }, + }, + vbgkqj: { + configurable: false, + enumerable: true, + get() { + return vbgkqjMethod; + }, + set(_) {}, + }, + xzfiflffffi: { configurable: true, enumerable: false, set(_) {} }, + [symbol]: { + configurable: true, + enumerable: false, + value: "failure", + writable: true, + }, + }, + ); + + it("[[Construct]] creates a new object which inherits from the correct prototype", () => { + assertStrictEquals( + Object.getPrototypeOf(new LazyLoader(methodsObject)), + prototype, + ); + }); + + it("[[Construct]] creates a new object with the desired properties", () => { + assertEquals( + Reflect.ownKeys(new LazyLoader(methodsObject)), + ["etaoin", "shrdlu", "cmfwyp", "vbgkqj", "xzfiflffffi", symbol], + ); + }); + + it("[[Construct]] creates a new object with configurable properties", () => { + assertEquals( + Object.fromEntries( + function* (ll) { + for (const key of Reflect.ownKeys(ll)) { + yield [ + key, + Object.getOwnPropertyDescriptor(ll, key).configurable, + ]; + } + }(new LazyLoader(methodsObject)), + ), + { + etaoin: true, + shrdlu: true, + cmfwyp: true, + vbgkqj: true, + xzfiflffffi: true, + [symbol]: true, + }, + ); + }); + + it("[[Construct]] creates a new object with the correct enumerability", () => { + assertEquals( + Object.fromEntries( + function* (ll) { + for (const key of Reflect.ownKeys(ll)) { + yield [ + key, + Object.getOwnPropertyDescriptor(ll, key).enumerable, + ]; + } + }(new LazyLoader(methodsObject)), + ), + { + etaoin: true, + shrdlu: false, + cmfwyp: false, + vbgkqj: true, + xzfiflffffi: false, + [symbol]: false, + }, + ); + }); + + it("[[Construct]] creates a new object with defined getters", () => { + assertEquals( + Object.fromEntries( + function* (ll) { + for (const key of Reflect.ownKeys(ll)) { + yield [ + key, + Object.getOwnPropertyDescriptor(ll, key).get !== void {}, + ]; + } + }(new LazyLoader(methodsObject)), + ), + { + etaoin: true, + shrdlu: true, + cmfwyp: true, + vbgkqj: true, + xzfiflffffi: true, + [symbol]: true, + }, + ); + }); + + it("[[Construct]] creates a new object with defined setters for writable properties only", () => { + assertEquals( + Object.fromEntries( + function* (ll) { + for (const key of Reflect.ownKeys(ll)) { + yield [ + key, + Object.getOwnPropertyDescriptor(ll, key).set !== void {}, + ]; + } + }(new LazyLoader(methodsObject)), + ), + { + etaoin: false, + shrdlu: false, + cmfwyp: false, + vbgkqj: false, + xzfiflffffi: false, + [symbol]: true, + }, + ); + }); + + describe("[[Construct]] creates a new object with correct getter behaviour", () => { + const ll = new LazyLoader(methodsObject); + ll.etaoin; + assertEquals( + Object.getOwnPropertyDescriptor(ll, "etaoin"), + { + configurable: false, + enumerable: true, + value: "success", + writable: false, + }, + ); + assertSpyCalls(etaoinMethod, 1); + assertSpyCall(etaoinMethod, 0, { + args: [], + self: ll, + returned: "success", + }); + ll.shrdlu; + assertEquals( + Object.getOwnPropertyDescriptor(ll, "shrdlu"), + { + configurable: true, + enumerable: false, + value: "success", + writable: false, + }, + ); + assertSpyCalls(shrdluMethod, 1); + assertSpyCall(shrdluMethod, 0, { + args: [], + self: ll, + returned: "success", + }); + ll.cmfwyp; + assertEquals( + Object.getOwnPropertyDescriptor(ll, "cmfwyp"), + { + configurable: true, + enumerable: false, + value: "success", + writable: false, + }, + ); + assertSpyCalls(cmfwypMethod, 1); + assertSpyCall(cmfwypMethod, 0, { + args: [], + self: ll, + returned: "success", + }); + ll.vbgkqj; + assertEquals( + Object.getOwnPropertyDescriptor(ll, "vbgkqj"), + { + configurable: false, + enumerable: true, + value: "success", + writable: false, + }, + ); + assertSpyCalls(vbgkqjMethod, 1); + assertSpyCall(vbgkqjMethod, 0, { + args: [], + self: ll, + returned: "success", + }); + assertThrows(() => ll.xzfiflffffi); + assertThrows(() => ll[symbol]); + }); + + describe("[[Construct]] creates a new object with correct setter behaviour", () => { + const ll = new LazyLoader(methodsObject); + ll[symbol] = "success"; + assertEquals( + Object.getOwnPropertyDescriptor(ll, symbol), + { + configurable: true, + enumerable: false, + value: "success", + writable: true, + }, + ); + }); +}); + describe("PropertyDescriptor", () => { it("[[Construct]] creates a new PropertyDescriptor", () => { assertStrictEquals(