diff options
Diffstat (limited to 'src/data/cacheable-object.js')
| -rw-r--r-- | src/data/cacheable-object.js | 162 |
1 files changed, 99 insertions, 63 deletions
diff --git a/src/data/cacheable-object.js b/src/data/cacheable-object.js index a089e325..9c655823 100644 --- a/src/data/cacheable-object.js +++ b/src/data/cacheable-object.js @@ -12,28 +12,15 @@ export default class CacheableObject { static propertyDependants = Symbol.for('CacheableObject.propertyDependants'); static cacheValid = Symbol.for('CacheableObject.cacheValid'); + static cachedValue = Symbol.for('CacheableObject.cachedValue'); static updateValue = Symbol.for('CacheableObject.updateValues'); constructor({seal = true} = {}) { - 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)) { - const {flags, update} = propertyDescriptors[property]; - if (!flags.update) continue; - - if ( - typeof update === 'object' && - update !== null && - 'default' in update - ) { - this[property] = update?.default; - } else { - this[property] = null; - } - } + this[CacheableObject.updateValue] = + Object.create(this[CacheableObject.updateValue]); if (seal) { Object.seal(this); @@ -49,9 +36,31 @@ export default class CacheableObject { throw new Error(`Expected constructor ${this.name} to provide CacheableObject.propertyDescriptors`); } + const propertyDescriptors = this[CacheableObject.propertyDescriptors]; + + // Finalize prototype update value + + this.prototype[CacheableObject.updateValue] = + Object.create( + Object.getPrototypeOf(this.prototype)[CacheableObject.updateValue] ?? + null); + + for (const property of Reflect.ownKeys(propertyDescriptors)) { + const {flags, update} = propertyDescriptors[property]; + if (!flags.update) continue; + + if (typeof update === 'object' && update !== null && 'default' in update) { + validatePropertyValue(property, null, update.default, update); + this.prototype[CacheableObject.updateValue][property] = update.default; + } else { + this.prototype[CacheableObject.updateValue][property] = null; + } + } + + // Finalize prototype property descriptors + this[CacheableObject.propertyDependants] = Object.create(null); - const propertyDescriptors = this[CacheableObject.propertyDescriptors]; for (const property of Reflect.ownKeys(propertyDescriptors)) { const {flags, update, expose} = propertyDescriptors[property]; @@ -73,17 +82,7 @@ export default class CacheableObject { } 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}); - } + validatePropertyValue(property, oldValue, newValue, update); } this[CacheableObject.updateValue][property] = newValue; @@ -121,18 +120,14 @@ export default class CacheableObject { const dependencies = Object.create(null); for (const key of expose.dependencies ?? []) { - switch (key) { - case 'this': - dependencies.this = this; - break; - - case 'thisProperty': - dependencies.thisProperty = property; - break; - - default: - dependencies[key] = this[CacheableObject.updateValue][key]; - break; + if (key === 'this') { + dependencies.this = this; + } else if (key === 'thisProperty') { + dependencies.thisProperty = property; + } else if (key.startsWith('_')) { + dependencies[key] = this[CacheableObject.updateValue][key.slice(1)]; + } else { + dependencies[key] = this[key]; } } @@ -151,27 +146,11 @@ export default class CacheableObject { if (flags.expose) recordAsDependant: { const dependantsMap = this[CacheableObject.propertyDependants]; - if (flags.update && expose?.transform) { - if (dependantsMap[property]) { - dependantsMap[property].push(property); + for (const dependency of dependenciesOf(property, propertyDescriptors)) { + if (dependantsMap[dependency]) { + dependantsMap[dependency].push(property); } else { - dependantsMap[property] = [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]; - } - } + dependantsMap[dependency] = [property]; } } } @@ -187,7 +166,7 @@ export default class CacheableObject { } static hasPropertyDescriptor(property) { - return Object.hasOwn(this[CacheableObject.propertyDescriptors], property); + return property in this[CacheableObject.propertyDescriptors]; } static cacheAllExposedProperties(obj) { @@ -243,13 +222,13 @@ export class CacheableObjectPropertyValueError extends Error { try { inspectOldValue = inspect(oldValue); - } catch (error) { + } catch { inspectOldValue = colors.red(`(couldn't inspect)`); } try { inspectNewValue = inspect(newValue); - } catch (error) { + } catch { inspectNewValue = colors.red(`(couldn't inspect)`); } @@ -260,3 +239,60 @@ export class CacheableObjectPropertyValueError extends Error { this.property = property; } } + +// good ol' module-scope utility functions + +function validatePropertyValue(property, oldValue, newValue, update) { + 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}); + } +} + +function* dependenciesOf(property, propertyDescriptors, cycle = []) { + const descriptor = propertyDescriptors[property]; + + if (descriptor?.flags?.update && descriptor?.expose?.transform) { + yield property; + } + + const dependencies = descriptor?.expose?.dependencies; + if (!dependencies) return; + + for (const dependency of dependencies) { + if (dependency === 'this') continue; + if (dependency === 'thisProperty') continue; + + if (dependency.startsWith('_')) { + yield dependency.slice(1); + continue; + } + + if (dependency === property) { + throw new Error( + `property ${dependency} directly depends on its own computed value`); + } + + if (cycle.includes(dependency)) { + const subcycle = cycle.slice(cycle.indexOf(dependency)); + const supercycle = cycle.slice(0, cycle.indexOf(dependency)); + throw new Error( + `property ${dependency} indirectly depends on its own computed value\n` + + ` via: ` + subcycle.map(p => p + ' -> ').join('') + property + ' -> ' + dependency + + (supercycle.length + ? '\n in: ' + supercycle.join(' -> ') + : '')); + } + + cycle.push(property); + yield* dependenciesOf(dependency, propertyDescriptors, cycle); + cycle.pop(); + } +} |