« get me outta code hell

hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/src/data/cacheable-object.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/data/cacheable-object.js')
-rw-r--r--src/data/cacheable-object.js162
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();
+  }
+}