diff options
author | (quasar) nebula <qznebula@protonmail.com> | 2025-01-12 18:56:56 -0400 |
---|---|---|
committer | (quasar) nebula <qznebula@protonmail.com> | 2025-01-13 16:43:08 -0400 |
commit | eb12a115268671ad324aa437d91c170e5843f4bb (patch) | |
tree | f3f103e90362f4b5b8c01f15e1b66ad8275cee5a | |
parent | 8e5e35be0411246cbbc2b2bdc4a93ca6b36b1337 (diff) |
cacheable-object: remove 900,000 functions
-rw-r--r-- | src/data/cacheable-object.js | 313 | ||||
-rw-r--r-- | src/data/thing.js | 11 | ||||
-rw-r--r-- | src/data/things/index.js | 13 | ||||
-rw-r--r-- | src/find.js | 4 | ||||
-rw-r--r-- | src/listing-spec.js | 21 | ||||
-rw-r--r-- | src/util/search-spec.js | 6 |
6 files changed, 169 insertions, 199 deletions
diff --git a/src/data/cacheable-object.js b/src/data/cacheable-object.js index 774e695e..443dbc53 100644 --- a/src/data/cacheable-object.js +++ b/src/data/cacheable-object.js @@ -84,53 +84,21 @@ function inspect(value) { export default class CacheableObject { static propertyDescriptors = Symbol.for('CacheableObject.propertyDescriptors'); + static constructorFinalized = Symbol.for('CacheableObject.constructorFinalized'); + static propertyDependants = Symbol.for('CacheableObject.propertyDependants'); - #propertyUpdateValues = Object.create(null); - #propertyUpdateCacheInvalidators = Object.create(null); - - // Note the constructor doesn't take an initial data source. Due to a quirk - // of JavaScript, private members can't be accessed before the superclass's - // constructor is finished processing - so if we call the overridden - // update() function from inside this constructor, it will error when - // writing to private members. Pretty bad! - // - // That means initial data must be provided by following up with update() - // after constructing the new instance of the Thing (sub)class. + static cacheValid = Symbol.for('CacheableObject.cacheValid'); + static updateValue = Symbol.for('CacheableObject.updateValues'); constructor() { - this.#defineProperties(); - this.#initializeUpdatingPropertyValues(); - - if (CacheableObject.DEBUG_SLOW_TRACK_INVALID_PROPERTIES) { - return new Proxy(this, { - get: (obj, key) => { - if (!Object.hasOwn(obj, key)) { - if (key !== 'constructor') { - CacheableObject._invalidAccesses.add(`(${obj.constructor.name}).${key}`); - } - } - return obj[key]; - }, - }); - } - } - - #withEachPropertyDescriptor(callback) { - const {[CacheableObject.propertyDescriptors]: propertyDescriptors} = - this.constructor; + this[CacheableObject.updateValue] = Object.create(null); + this[CacheableObject.cachedValue] = Object.create(null); + this[CacheableObject.cacheValid] = Object.create(null); + const propertyDescriptors = this.constructor[CacheableObject.propertyDescriptors]; for (const property of Reflect.ownKeys(propertyDescriptors)) { - callback(property, propertyDescriptors[property]); - } - } - - #initializeUpdatingPropertyValues() { - this.#withEachPropertyDescriptor((property, descriptor) => { - const {flags, update} = descriptor; - - if (!flags.update) { - return; - } + const {flags, update} = propertyDescriptors[property]; + if (!flags.update) continue; if ( typeof update === 'object' && @@ -141,188 +109,157 @@ export default class CacheableObject { } else { this[property] = null; } - }); + } } - #defineProperties() { - if (!this.constructor[CacheableObject.propertyDescriptors]) { - throw new Error(`Expected constructor ${this.constructor.name} to provide CacheableObject.propertyDescriptors`); + static finalizeCacheableObjectPrototype() { + if (this[CacheableObject.constructorFinalized]) { + throw new Error(`Constructor ${this.name} already finalized`); } - this.#withEachPropertyDescriptor((property, descriptor) => { - const {flags} = descriptor; + if (!this[CacheableObject.propertyDescriptors]) { + throw new Error(`Expected constructor ${this.name} to provide CacheableObject.propertyDescriptors`); + } + + this[CacheableObject.constructorFinalized] = true; + this[CacheableObject.propertyDependants] = Object.create(null); + + const propertyDescriptors = this[CacheableObject.propertyDescriptors]; + for (const property of Reflect.ownKeys(propertyDescriptors)) { + const {flags, update, expose} = propertyDescriptors[property]; const definition = { configurable: false, enumerable: flags.expose, }; - if (flags.update) { - definition.set = this.#getUpdateObjectDefinitionSetterFunction(property); - } - - if (flags.expose) { - definition.get = this.#getExposeObjectDefinitionGetterFunction(property); - } + if (flags.update) setSetter: { + definition.set = function(newValue) { + if (newValue === undefined) { + throw new TypeError(`Properties cannot be set to undefined`); + } - Object.defineProperty(this, property, definition); - }); + const oldValue = this[CacheableObject.updateValue][property]; - Object.seal(this); - } + if (newValue === oldValue) { + return; + } - #getUpdateObjectDefinitionSetterFunction(property) { - const {update} = this.#getPropertyDescriptor(property); - const validate = update?.validate; + if (newValue !== null && update?.validate) { + try { + const result = update.validate(newValue); + if (result === undefined) { + throw new TypeError(`Validate function returned undefined`); + } else if (result !== true) { + throw new TypeError(`Validation failed for value ${newValue}`); + } + } catch (caughtError) { + throw new CacheableObjectPropertyValueError( + property, oldValue, newValue, {cause: caughtError}); + } + } - return (newValue) => { - const oldValue = this.#propertyUpdateValues[property]; + this[CacheableObject.updateValue][property] = newValue; - if (newValue === undefined) { - throw new TypeError(`Properties cannot be set to undefined`); + const dependants = this.constructor[CacheableObject.propertyDependants][property]; + if (dependants) { + for (const dependant of dependants) { + this[CacheableObject.cacheValid][dependant] = false; + } + } + }; } - if (newValue === oldValue) { - return; - } + if (flags.expose) setGetter: { + if (flags.update && !expose?.transform) { + definition.get = function() { + return this[CacheableObject.updateValue][property]; + }; - if (newValue !== null && validate) { - try { - const result = validate(newValue); - if (result === undefined) { - throw new TypeError(`Validate function returned undefined`); - } else if (result !== true) { - throw new TypeError(`Validation failed for value ${newValue}`); - } - } catch (caughtError) { - throw new CacheableObjectPropertyValueError( - property, oldValue, newValue, {cause: caughtError}); + break setGetter; } - } - this.#propertyUpdateValues[property] = newValue; - this.#invalidateCachesDependentUpon(property); - }; - } - - #getPropertyDescriptor(property) { - return this.constructor[CacheableObject.propertyDescriptors][property]; - } + if (flags.update && expose?.compute) { + throw new Error(`Updating property ${property} has compute function, should be formatted as transform`); + } - #invalidateCachesDependentUpon(property) { - const invalidators = this.#propertyUpdateCacheInvalidators[property]; - if (!invalidators) { - return; - } + if (!flags.update && !expose?.compute) { + throw new Error(`Exposed property ${property} does not update and is missing compute function`); + } - for (const invalidate of invalidators) { - invalidate(); - } - } + definition.get = function() { + if (this[CacheableObject.cacheValid][property]) { + return this[CacheableObject.cachedValue][property]; + } - #getExposeObjectDefinitionGetterFunction(property) { - const {flags} = this.#getPropertyDescriptor(property); - const compute = this.#getExposeComputeFunction(property); - - if (compute) { - let cachedValue; - const checkCacheValid = this.#getExposeCheckCacheValidFunction(property); - return () => { - if (checkCacheValid()) { - return cachedValue; - } else { - return (cachedValue = compute()); - } - }; - } else if (!flags.update && !compute) { - throw new Error(`Exposed property ${property} does not update and is missing compute function`); - } else { - return () => this.#propertyUpdateValues[property]; - } - } + this[CacheableObject.cacheValid][property] = true; - #getExposeComputeFunction(property) { - const {flags, expose} = this.#getPropertyDescriptor(property); + const dependencies = Object.create(null); + for (const key of expose.dependencies ?? []) { + switch (key) { + case 'this': + dependencies.this = this; + break; - const compute = expose?.compute; - const transform = expose?.transform; + case 'thisProperty': + dependencies.thisProperty = property; + break; - if (flags.update && !transform) { - return null; - } else if (flags.update && compute) { - throw new Error(`Updating property ${property} has compute function, should be formatted as transform`); - } else if (!flags.update && !compute) { - throw new Error(`Exposed property ${property} does not update and is missing compute function`); - } + default: + dependencies[key] = this[CacheableObject.updateValue][key]; + break; + } + } - let getAllDependencies; + const value = + (flags.update + ? expose.transform(this[CacheableObject.updateValue][property], dependencies) + : expose.compute(dependencies)); - if (expose.dependencies?.length > 0) { - const dependencyKeys = expose.dependencies.slice(); - const shouldReflectObject = dependencyKeys.includes('this'); - const shouldReflectProperty = dependencyKeys.includes('thisProperty'); + this[CacheableObject.cachedValue][property] = value; - getAllDependencies = () => { - const dependencies = Object.create(null); + return value; + }; + } - for (const key of dependencyKeys) { - dependencies[key] = this.#propertyUpdateValues[key]; - } + if (flags.expose) recordAsDependant: { + const dependantsMap = this[CacheableObject.propertyDependants]; - if (shouldReflectObject) { - dependencies.this = this; + if (flags.update && expose?.transform) { + if (dependantsMap[property]) { + dependantsMap[property].push(property); + } else { + dependantsMap[property] = [property]; + } } - if (shouldReflectProperty) { - dependencies.thisProperty = property; + for (const dependency of expose?.dependencies ?? []) { + switch (dependency) { + case 'this': + case 'thisProperty': + continue; + + default: { + if (dependantsMap[dependency]) { + dependantsMap[dependency].push(property); + } else { + dependantsMap[dependency] = [property]; + } + } + } } + } - return dependencies; - }; - } else { - const dependencies = Object.create(null); - Object.freeze(dependencies); - getAllDependencies = () => dependencies; - } - - if (flags.update) { - return () => transform(this.#propertyUpdateValues[property], getAllDependencies()); - } else { - return () => compute(getAllDependencies()); + Object.defineProperty(this.prototype, property, definition); } } - #getExposeCheckCacheValidFunction(property) { - const {flags, expose} = this.#getPropertyDescriptor(property); - - let valid = false; - - const invalidate = () => { - valid = false; - }; - - const dependencyKeys = new Set(expose?.dependencies); - - if (flags.update) { - dependencyKeys.add(property); - } - - for (const key of dependencyKeys) { - if (this.#propertyUpdateCacheInvalidators[key]) { - this.#propertyUpdateCacheInvalidators[key].push(invalidate); - } else { - this.#propertyUpdateCacheInvalidators[key] = [invalidate]; - } - } + static getPropertyDescriptor(property) { + return this[CacheableObject.propertyDescriptors][property]; + } - return () => { - if (!valid) { - valid = true; - return false; - } else { - return true; - } - }; + static hasPropertyDescriptor(property) { + return Object.hasOwn(this[CacheableObject.propertyDescriptors], property); } static cacheAllExposedProperties(obj) { @@ -368,11 +305,11 @@ export default class CacheableObject { } static getUpdateValue(object, key) { - if (!Object.hasOwn(object, key)) { + if (!object.constructor.hasPropertyDescriptor(key)) { return undefined; } - return object.#propertyUpdateValues[key] ?? null; + return object[CacheableObject.updateValue][key] ?? null; } static clone(object) { @@ -384,7 +321,7 @@ export default class CacheableObject { } static copyUpdateValuesOnto(source, target) { - Object.assign(target, source.#propertyUpdateValues); + Object.assign(target, source[CacheableObject.updateValue]); } } diff --git a/src/data/thing.js b/src/data/thing.js index 4c3ba3e4..c51c5fe5 100644 --- a/src/data/thing.js +++ b/src/data/thing.js @@ -28,14 +28,13 @@ export default class Thing extends CacheableObject { // Symbol.for('Thing.isThingConstructor') in constructor static [Symbol.for('Thing.isThingConstructor')] = NaN; - static [CacheableObject.propertyDescriptors] = { + constructor() { + super(); + // To detect: // Object.hasOwn(object, Symbol.for('Thing.isThing')) - [Symbol.for('Thing.isThing')]: { - flags: {expose: true}, - expose: {compute: () => NaN}, - }, - }; + this[Symbol.for('Thing.isThing')] = NaN; + } static [Symbol.for('Thing.selectAll')] = _wikiData => []; diff --git a/src/data/things/index.js b/src/data/things/index.js index f18e283a..9f033c23 100644 --- a/src/data/things/index.js +++ b/src/data/things/index.js @@ -177,6 +177,16 @@ function evaluateSerializeDescriptors() { }); } +function finalizeCacheableObjectPrototypes() { + return descriptorAggregateHelper({ + message: `Errors finalizing Thing class prototypes`, + + op(constructor) { + constructor.finalizeCacheableObjectPrototype(); + }, + }); +} + if (!errorDuplicateClassNames()) process.exit(1); @@ -188,6 +198,9 @@ if (!evaluatePropertyDescriptors()) if (!evaluateSerializeDescriptors()) process.exit(1); +if (!finalizeCacheableObjectPrototypes()) + process.exit(1); + Object.assign(allClasses, {Thing}); export default allClasses; diff --git a/src/find.js b/src/find.js index c7813e32..e590bc4f 100644 --- a/src/find.js +++ b/src/find.js @@ -34,7 +34,7 @@ export function processAvailableMatchesByName(data, { include = _thing => true, getMatchableNames = thing => - (Object.hasOwn(thing, 'name') + (thing.constructor.hasPropertyDescriptor('name') ? [thing.name] : []), @@ -72,7 +72,7 @@ export function processAvailableMatchesByDirectory(data, { include = _thing => true, getMatchableDirectories = thing => - (Object.hasOwn(thing, 'directory') + (thing.constructor.hasPropertyDescriptor('directory') ? [thing.directory] : [null]), diff --git a/src/listing-spec.js b/src/listing-spec.js index bfea397c..749f009a 100644 --- a/src/listing-spec.js +++ b/src/listing-spec.js @@ -238,6 +238,27 @@ listingSpec.push({ groupUnderOther: true, }); +// Dunkass mock. Listings should be Things! In the fuuuuture! +class Listing { + static properties = {}; + + constructor() { + Object.assign(this, this.constructor.properties); + } + + static hasPropertyDescriptor(key) { + return Object.hasOwn(this.properties, key); + } +} + +for (const [index, listing] of listingSpec.entries()) { + class ListingSubclass extends Listing { + static properties = listing; + } + + listingSpec.splice(index, 1, new ListingSubclass); +} + { const errors = []; diff --git a/src/util/search-spec.js b/src/util/search-spec.js index bc24e1a1..3d05c021 100644 --- a/src/util/search-spec.js +++ b/src/util/search-spec.js @@ -134,14 +134,14 @@ export const searchSpec = { thing.color; fields.artTags = - (Object.hasOwn(thing, 'artTags') + (thing.constructor.hasPropertyDescriptor('artTags') ? thing.artTags.map(artTag => artTag.nameShort) : []); fields.additionalNames = - (Object.hasOwn(thing, 'additionalNames') + (thing.constructor.hasPropertyDescriptor('additionalNames') ? thing.additionalNames.map(entry => entry.name) - : Object.hasOwn(thing, 'aliasNames') + : thing.constructor.hasPropertyDescriptor('aliasNames') ? thing.aliasNames : []); |