From aefb53631314295e1574847683bbfa5fec98d536 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 19 Nov 2025 22:34:17 -0400 Subject: data, infra: better CacheableObject prototype inheritence Basically, properties (as written in source code) are only registered once EVER, instead of "also" on subclasses, which was previously the case - we were literally copying over the descriptors and then re-evaluating as though they were new. That's the goal, but to make it happen, we had to redo how default property values are initialized. Things no longer self- prepare by making a "storage" for all their cached values with null, or defaults, filled in - instead they create a new object which inherits those defaults from their prototype. This object respectively has the prototype of the superclass' prototype's defaults, and so on. A nice effect is that validating defaults against update.validate specs now happens during prototype finalization, instead of every time any instance is instantiated. We also had to do the same kind of inheritence shenanigans with the full propertyDescriptors specs themselves. The real-world performance boost for this is probably more or less zero RIGHT NOW, because Thing subclassing is used crazy sparingly (and there's still work to make it really nice). But if [[Prototype]] chains don't really take memory, then possibly this cleans up a lot of extremely similar objects where every new Thing property multiplies memory usage across all instances of that thing, regardless how few actually set a value. Maybe! Who knows what V8 really optimizes, but maybe! --- src/data/cacheable-object.js | 69 ++++++++++++++++++++++---------------- src/data/things/homepage-layout.js | 6 ---- src/data/things/index.js | 8 ++--- 3 files changed, 45 insertions(+), 38 deletions(-) (limited to 'src/data') diff --git a/src/data/cacheable-object.js b/src/data/cacheable-object.js index 3f70af30..e0703259 100644 --- a/src/data/cacheable-object.js +++ b/src/data/cacheable-object.js @@ -16,25 +16,11 @@ export default class CacheableObject { 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); @@ -50,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]; @@ -74,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; @@ -261,3 +259,18 @@ 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}); + } +} diff --git a/src/data/things/homepage-layout.js b/src/data/things/homepage-layout.js index 2456ca95..d00a2f4b 100644 --- a/src/data/things/homepage-layout.js +++ b/src/data/things/homepage-layout.js @@ -244,8 +244,6 @@ export class HomepageLayoutActionsRow extends HomepageLayoutRow { static [Thing.friendlyName] = `Homepage Actions Row`; static [Thing.getPropertyDescriptors] = (opts) => ({ - ...HomepageLayoutRow[Thing.getPropertyDescriptors](opts), - // Update & expose actionLinks: { @@ -278,8 +276,6 @@ export class HomepageLayoutAlbumCarouselRow extends HomepageLayoutRow { static [Thing.friendlyName] = `Homepage Album Carousel Row`; static [Thing.getPropertyDescriptors] = (opts, {Album} = opts) => ({ - ...HomepageLayoutRow[Thing.getPropertyDescriptors](opts), - // Update & expose albums: referenceList({ @@ -312,8 +308,6 @@ export class HomepageLayoutAlbumGridRow extends HomepageLayoutRow { static [Thing.friendlyName] = `Homepage Album Grid Row`; static [Thing.getPropertyDescriptors] = (opts, {Album, Group} = opts) => ({ - ...HomepageLayoutRow[Thing.getPropertyDescriptors](opts), - // Update & expose sourceGroup: [ diff --git a/src/data/things/index.js b/src/data/things/index.js index 41301575..676453ce 100644 --- a/src/data/things/index.js +++ b/src/data/things/index.js @@ -181,10 +181,10 @@ function evaluatePropertyDescriptors() { } } - constructor[CacheableObject.propertyDescriptors] = { - ...constructor[CacheableObject.propertyDescriptors] ?? {}, - ...results, - }; + constructor[CacheableObject.propertyDescriptors] = + Object.create(constructor[CacheableObject.propertyDescriptors] ?? null); + + Object.assign(constructor[CacheableObject.propertyDescriptors], results); }, showFailedClasses(failedClasses) { -- cgit 1.3.0-6-gf8a5