« get me outta code hell

data: filter only requested deps, require requesting 'this' - hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
diff options
context:
space:
mode:
author(quasar) nebula <qznebula@protonmail.com>2023-08-22 13:02:19 -0300
committer(quasar) nebula <qznebula@protonmail.com>2023-09-05 21:02:49 -0300
commit75691866ed68b9261dd920b79d4ab214df3f049b (patch)
tree0a8d328279498631bdab9eaa2afedcee5574c7fb
parent93448ef747b681d3b87b050b555311c0172b83cc (diff)
data: filter only requested deps, require requesting 'this'
* Thing.composite.from() only provides the dependencies specified
  in each step and the base, and prevents '#'-prefixed keys from
  being specified on the main (composite) dependency list.

* CacheableObject no longer provides a "reflection" dependency to
  every compute/transform function, and now requires the property
  'this' to be specified instead of the constructor.instance
  symbol. (The static CacheableObject.instance, inherited by all
  subclasses, was also removed.)

* Also minor improvements to sugar.js data processing utility
  functions.
-rw-r--r--src/data/things/art-tag.js4
-rw-r--r--src/data/things/artist.js16
-rw-r--r--src/data/things/cacheable-object.js33
-rw-r--r--src/data/things/flash.js8
-rw-r--r--src/data/things/group.js13
-rw-r--r--src/data/things/thing.js48
-rw-r--r--src/data/things/track.js85
-rw-r--r--src/util/sugar.js24
8 files changed, 147 insertions, 84 deletions
diff --git a/src/data/things/art-tag.js b/src/data/things/art-tag.js
index c103c4d5..bb36e09e 100644
--- a/src/data/things/art-tag.js
+++ b/src/data/things/art-tag.js
@@ -37,8 +37,8 @@ export class ArtTag extends Thing {
       flags: {expose: true},
 
       expose: {
-        dependencies: ['albumData', 'trackData'],
-        compute: ({albumData, trackData, [ArtTag.instance]: artTag}) =>
+        dependencies: ['this', 'albumData', 'trackData'],
+        compute: ({this: artTag, albumData, trackData}) =>
           sortAlbumsTracksChronologically(
             [...albumData, ...trackData]
               .filter(({artTags}) => artTags.includes(artTag)),
diff --git a/src/data/things/artist.js b/src/data/things/artist.js
index 4f157bc6..bde84cfa 100644
--- a/src/data/things/artist.js
+++ b/src/data/things/artist.js
@@ -66,9 +66,9 @@ export class Artist extends Thing {
       flags: {expose: true},
 
       expose: {
-        dependencies: ['trackData'],
+        dependencies: ['this', 'trackData'],
 
-        compute: ({trackData, [Artist.instance]: artist}) =>
+        compute: ({this: artist, trackData}) =>
           trackData?.filter((track) =>
             [
               ...track.artistContribs ?? [],
@@ -82,9 +82,9 @@ export class Artist extends Thing {
       flags: {expose: true},
 
       expose: {
-        dependencies: ['trackData'],
+        dependencies: ['this', 'trackData'],
 
-        compute: ({trackData, [Artist.instance]: artist}) =>
+        compute: ({this: artist, trackData}) =>
           trackData?.filter(({commentatorArtists}) =>
             commentatorArtists.includes(artist)) ?? [],
       },
@@ -103,9 +103,9 @@ export class Artist extends Thing {
       flags: {expose: true},
 
       expose: {
-        dependencies: ['albumData'],
+        dependencies: [this, 'albumData'],
 
-        compute: ({albumData, [Artist.instance]: artist}) =>
+        compute: ({this: artist, albumData}) =>
           albumData?.filter(({commentatorArtists}) =>
             commentatorArtists.includes(artist)) ?? [],
       },
@@ -148,11 +148,11 @@ export class Artist extends Thing {
     flags: {expose: true},
 
     expose: {
-      dependencies: [thingDataProperty],
+      dependencies: ['this', thingDataProperty],
 
       compute: ({
+        this: artist,
         [thingDataProperty]: thingData,
-        [Artist.instance]: artist
       }) =>
         thingData?.filter(thing =>
           thing[contribsProperty]
diff --git a/src/data/things/cacheable-object.js b/src/data/things/cacheable-object.js
index ea705a61..24a6cf01 100644
--- a/src/data/things/cacheable-object.js
+++ b/src/data/things/cacheable-object.js
@@ -83,8 +83,6 @@ function inspect(value) {
 }
 
 export default class CacheableObject {
-  static instance = Symbol('CacheableObject `this` instance');
-
   #propertyUpdateValues = Object.create(null);
   #propertyUpdateCacheInvalidators = Object.create(null);
 
@@ -250,20 +248,27 @@ export default class CacheableObject {
 
     let getAllDependencies;
 
-    const dependencyKeys = expose.dependencies;
-    if (dependencyKeys?.length > 0) {
-      const reflectionEntry = [this.constructor.instance, this];
-      const dependencyGetters = dependencyKeys
-        .map(key => () => [key, this.#propertyUpdateValues[key]]);
+    if (expose.dependencies?.length > 0) {
+      const dependencyKeys = expose.dependencies.slice();
+      const shouldReflect = dependencyKeys.includes('this');
+
+      getAllDependencies = () => {
+        const dependencies = Object.create(null);
+
+        for (const key of dependencyKeys) {
+          dependencies[key] = this.#propertyUpdateValues[key];
+        }
 
-      getAllDependencies = () =>
-        Object.fromEntries(dependencyGetters
-          .map(f => f())
-          .concat([reflectionEntry]));
+        if (shouldReflect) {
+          dependencies.this = this;
+        }
+
+        return dependencies;
+      };
     } else {
-      const allDependencies = {[this.constructor.instance]: this};
-      Object.freeze(allDependencies);
-      getAllDependencies = () => allDependencies;
+      const dependencies = Object.create(null);
+      Object.freeze(dependencies);
+      getAllDependencies = () => dependencies;
     }
 
     if (flags.update) {
diff --git a/src/data/things/flash.js b/src/data/things/flash.js
index 6eb5234f..445fd07c 100644
--- a/src/data/things/flash.js
+++ b/src/data/things/flash.js
@@ -77,9 +77,9 @@ export class Flash extends Thing {
       flags: {expose: true},
 
       expose: {
-        dependencies: ['flashActData'],
+        dependencies: ['this', 'flashActData'],
 
-        compute: ({flashActData, [Flash.instance]: flash}) =>
+        compute: ({this: flash, flashActData}) =>
           flashActData.find((act) => act.flashes.includes(flash)) ?? null,
       },
     },
@@ -88,9 +88,9 @@ export class Flash extends Thing {
       flags: {expose: true},
 
       expose: {
-        dependencies: ['flashActData'],
+        dependencies: ['this', 'flashActData'],
 
-        compute: ({flashActData, [Flash.instance]: flash}) =>
+        compute: ({this: flash, flashActData}) =>
           flashActData.find((act) => act.flashes.includes(flash))?.color ?? null,
       },
     },
diff --git a/src/data/things/group.js b/src/data/things/group.js
index ba339b3e..f552b8f3 100644
--- a/src/data/things/group.js
+++ b/src/data/things/group.js
@@ -41,8 +41,8 @@ export class Group extends Thing {
       flags: {expose: true},
 
       expose: {
-        dependencies: ['albumData'],
-        compute: ({albumData, [Group.instance]: group}) =>
+        dependencies: ['this', 'albumData'],
+        compute: ({this: group, albumData}) =>
           albumData?.filter((album) => album.groups.includes(group)) ?? [],
       },
     },
@@ -51,9 +51,8 @@ export class Group extends Thing {
       flags: {expose: true},
 
       expose: {
-        dependencies: ['groupCategoryData'],
-
-        compute: ({groupCategoryData, [Group.instance]: group}) =>
+        dependencies: ['this', 'groupCategoryData'],
+        compute: ({this: group, groupCategoryData}) =>
           groupCategoryData.find((category) => category.groups.includes(group))
             ?.color,
       },
@@ -63,8 +62,8 @@ export class Group extends Thing {
       flags: {expose: true},
 
       expose: {
-        dependencies: ['groupCategoryData'],
-        compute: ({groupCategoryData, [Group.instance]: group}) =>
+        dependencies: ['this', 'groupCategoryData'],
+        compute: ({this: group, groupCategoryData}) =>
           groupCategoryData.find((category) => category.groups.includes(group)) ??
           null,
       },
diff --git a/src/data/things/thing.js b/src/data/things/thing.js
index 5d14b296..bc10e06b 100644
--- a/src/data/things/thing.js
+++ b/src/data/things/thing.js
@@ -5,7 +5,7 @@ import {inspect} from 'node:util';
 
 import {color} from '#cli';
 import find from '#find';
-import {empty, openAggregate} from '#sugar';
+import {empty, filterProperties, openAggregate} from '#sugar';
 import {getKebabCase} from '#wiki-data';
 
 import {
@@ -278,6 +278,7 @@ export default class Thing extends CacheableObject {
       flags: {expose: true},
       expose: {
         dependencies: [
+          'this',
           contribsByRefProperty,
           thingDataProperty,
           nullerProperty,
@@ -285,7 +286,7 @@ export default class Thing extends CacheableObject {
         ].filter(Boolean),
 
         compute({
-          [Thing.instance]: thing,
+          this: thing,
           [nullerProperty]: nuller,
           [contribsByRefProperty]: contribsByRef,
           [thingDataProperty]: thingData,
@@ -330,9 +331,9 @@ export default class Thing extends CacheableObject {
       flags: {expose: true},
 
       expose: {
-        dependencies: [thingDataProperty],
+        dependencies: ['this', thingDataProperty],
 
-        compute: ({[thingDataProperty]: thingData, [Thing.instance]: thing}) =>
+        compute: ({this: thing, [thingDataProperty]: thingData}) =>
           thingData?.filter(t => t[referencerRefListProperty].includes(thing)) ?? [],
       },
     }),
@@ -344,9 +345,9 @@ export default class Thing extends CacheableObject {
       flags: {expose: true},
 
       expose: {
-        dependencies: [thingDataProperty],
+        dependencies: ['this', thingDataProperty],
 
-        compute: ({[thingDataProperty]: thingData, [Thing.instance]: thing}) =>
+        compute: ({this: thing, [thingDataProperty]: thingData}) =>
           thingData?.filter((t) => t[referencerRefListProperty] === thing) ?? [],
       },
     }),
@@ -462,15 +463,19 @@ export default class Thing extends CacheableObject {
 
             if (step.expose.dependencies) {
               for (const dependency of step.expose.dependencies) {
+                if (typeof dependency === 'string' && dependency.startsWith('#')) continue;
                 exposeDependencies.add(dependency);
               }
             }
 
+            let fn, type;
             if (base.flags.update) {
               if (step.expose.transform) {
-                exposeFunctionOrder.push({type: 'transform', fn: step.expose.transform});
+                type = 'transform';
+                fn = step.expose.transform;
               } else {
-                exposeFunctionOrder.push({type: 'compute', fn: step.expose.compute});
+                type = 'compute';
+                fn = step.expose.compute;
               }
             } else {
               if (step.expose.transform && !step.expose.compute) {
@@ -478,8 +483,15 @@ export default class Thing extends CacheableObject {
                 break expose;
               }
 
-              exposeFunctionOrder.push({type: 'compute', fn: step.expose.compute});
+              type = 'compute';
+              fn = step.expose.compute;
             }
+
+            exposeFunctionOrder.push({
+              type,
+              fn,
+              ownDependencies: step.expose.dependencies,
+            });
           }
         });
       }
@@ -509,15 +521,20 @@ export default class Thing extends CacheableObject {
             const dependencies = {...initialDependencies};
             let valueSoFar = value;
 
-            for (const {type, fn} of exposeFunctionOrder) {
+            for (const {type, fn, ownDependencies} of exposeFunctionOrder) {
+              const filteredDependencies =
+                (ownDependencies
+                  ? filterProperties(dependencies, ownDependencies)
+                  : {})
+
               const result =
                 (type === 'transform'
-                  ? fn(valueSoFar, dependencies, (updatedValue, providedDependencies) => {
+                  ? fn(valueSoFar, filteredDependencies, (updatedValue, providedDependencies) => {
                       valueSoFar = updatedValue ?? null;
                       Object.assign(dependencies, providedDependencies ?? {});
                       return continuationSymbol;
                     })
-                  : fn(dependencies, providedDependencies => {
+                  : fn(filteredDependencies, providedDependencies => {
                       Object.assign(dependencies, providedDependencies ?? {});
                       return continuationSymbol;
                     }));
@@ -527,10 +544,13 @@ export default class Thing extends CacheableObject {
               }
             }
 
+            const filteredDependencies =
+              filterProperties(dependencies, base.expose.dependencies);
+
             if (base.expose.transform) {
-              return base.expose.transform(valueSoFar, dependencies);
+              return base.expose.transform(valueSoFar, filteredDependencies);
             } else {
-              return base.expose.compute(dependencies);
+              return base.expose.compute(filteredDependencies);
             }
           };
         } else {
diff --git a/src/data/things/track.js b/src/data/things/track.js
index 30c6fe58..551d9345 100644
--- a/src/data/things/track.js
+++ b/src/data/things/track.js
@@ -59,7 +59,8 @@ export class Track extends Thing {
         flags: {update: true, expose: true},
         update: {validate: isColor},
         expose: {
-          compute: ({album: {color}}) => color,
+          dependencies: ['#album.color'],
+          compute: ({'#album.color': color}) => color,
         },
       },
     ]),
@@ -75,18 +76,27 @@ export class Track extends Thing {
     // main artwork. (It does inherit `trackCoverArtFileExtension` if present
     // on the album.)
     coverArtFileExtension: Thing.composite.from([
-      Track.composite.withAlbumProperties(['trackCoverArtistContribsByRef', 'trackCoverArtFileExtension']),
+      Track.composite.withAlbumProperties([
+        'trackCoverArtistContribsByRef',
+        'trackCoverArtFileExtension',
+      ]),
 
       {
         flags: {update: true, expose: true},
         update: {validate: isFileExtension},
         expose: {
-          dependencies: ['coverArtistContribsByRef', 'disableUniqueCoverArt'],
+          dependencies: [
+            'coverArtistContribsByRef',
+            'disableUniqueCoverArt',
+            '#album.trackCoverArtistContribsByRef',
+            '#album.trackCoverArtFileExtension',
+          ],
 
           transform(coverArtFileExtension, {
             coverArtistContribsByRef,
             disableUniqueCoverArt,
-            album: {trackCoverArtistContribsByRef, trackCoverArtFileExtension},
+            '#album.trackCoverArtistContribsByRef': trackCoverArtistContribsByRef,
+            '#album.trackCoverArtFileExtension': trackCoverArtFileExtension,
           }) {
             if (disableUniqueCoverArt) return null;
             if (empty(coverArtistContribsByRef) && empty(trackCoverArtistContribsByRef)) return null;
@@ -101,18 +111,27 @@ export class Track extends Thing {
     // the track's own coverArtDate or its album's trackArtDate, so if neither
     // is specified, this value is null.
     coverArtDate: Thing.composite.from([
-      Track.composite.withAlbumProperties(['trackArtDate', 'trackCoverArtistContribsByRef']),
+      Track.composite.withAlbumProperties([
+        'trackArtDate',
+        'trackCoverArtistContribsByRef',
+      ]),
 
       {
         flags: {update: true, expose: true},
         update: {validate: isDate},
         expose: {
-          dependencies: ['coverArtistContribsByRef', 'disableUniqueCoverArt'],
+          dependencies: [
+            'coverArtistContribsByRef',
+            'disableUniqueCoverArt',
+            '#album.trackArtDate',
+            '#album.trackCoverArtistContribsByRef',
+          ],
 
           transform(coverArtDate, {
             coverArtistContribsByRef,
             disableUniqueCoverArt,
-            album: {trackArtDate, trackCoverArtistContribsByRef},
+            '#album.trackArtDate': trackArtDate,
+            '#album.trackCoverArtistContribsByRef': trackCoverArtistContribsByRef,
           }) {
             if (disableUniqueCoverArt) return null;
             if (empty(coverArtistContribsByRef) && empty(trackCoverArtistContribsByRef)) return null;
@@ -148,8 +167,8 @@ export class Track extends Thing {
       flags: {expose: true},
 
       expose: {
-        dependencies: ['albumData'],
-        compute: ({[Track.instance]: track, albumData}) =>
+        dependencies: ['this', 'albumData'],
+        compute: ({this: track, albumData}) =>
           albumData?.find((album) => album.tracks.includes(track)) ?? null,
       },
     },
@@ -182,7 +201,8 @@ export class Track extends Thing {
       {
         flags: {expose: true},
         expose: {
-          compute: ({album: {date}}) => date,
+          dependencies: ['#album.date'],
+          compute: ({'#album.date': date}) => date,
         },
       },
     ]),
@@ -200,11 +220,16 @@ export class Track extends Thing {
       {
         flags: {expose: true},
         expose: {
-          dependencies: ['coverArtistContribsByRef', 'disableUniqueCoverArt'],
+          dependencies: [
+            'coverArtistContribsByRef',
+            'disableUniqueCoverArt',
+            '#album.trackCoverArtistContribsByRef',
+          ],
+
           compute({
             coverArtistContribsByRef,
             disableUniqueCoverArt,
-            album: {trackCoverArtistContribsByRef},
+            '#album.trackCoverArtistContribsByRef': trackCoverArtistContribsByRef,
           }) {
             if (disableUniqueCoverArt) return false;
             if (!empty(coverArtistContribsByRef)) return true;
@@ -225,12 +250,12 @@ export class Track extends Thing {
       flags: {expose: true},
 
       expose: {
-        dependencies: ['originalReleaseTrackByRef', 'trackData'],
+        dependencies: ['this', 'originalReleaseTrackByRef', 'trackData'],
 
         compute: ({
+          this: t1,
           originalReleaseTrackByRef: t1origRef,
           trackData,
-          [Track.instance]: t1,
         }) => {
           if (!trackData) {
             return [];
@@ -252,15 +277,16 @@ export class Track extends Thing {
     artistContribs: Thing.composite.from([
       Track.composite.inheritFromOriginalRelease('artistContribs'),
 
-      Thing.composite.withDynamicContribs('artistContribsByRef', 'artistContribs'),
+      Thing.composite.withDynamicContribs('artistContribsByRef', '#artistContribs'),
       Track.composite.withAlbumProperties(['artistContribs']),
 
       {
         flags: {expose: true},
         expose: {
+          dependencies: ['#artistContribs', '#album.artistContribs'],
           compute: ({
-            artistContribs: contribsFromTrack,
-            album: {artistContribs: contribsFromAlbum},
+            '#artistContribs': contribsFromTrack,
+            '#album.artistContribs': contribsFromAlbum,
           }) =>
             (empty(contribsFromTrack)
               ? contribsFromAlbum
@@ -290,14 +316,15 @@ export class Track extends Thing {
       },
 
       Track.composite.withAlbumProperties(['trackCoverArtistContribs']),
-      Thing.composite.withDynamicContribs('coverArtistContribsByRef', 'coverArtistContribs'),
+      Thing.composite.withDynamicContribs('coverArtistContribsByRef', '#coverArtistContribs'),
 
       {
         flags: {expose: true},
         expose: {
+          dependencies: ['#coverArtistContribs', '#album.trackCoverArtistContribs'],
           compute: ({
-            coverArtistContribs: contribsFromTrack,
-            album: {trackCoverArtistContribs: contribsFromAlbum},
+            '#coverArtistContribs': contribsFromTrack,
+            '#album.trackCoverArtistContribs': contribsFromAlbum,
           }) =>
             (empty(contribsFromTrack)
               ? contribsFromAlbum
@@ -328,9 +355,9 @@ export class Track extends Thing {
       flags: {expose: true},
 
       expose: {
-        dependencies: ['trackData'],
+        dependencies: ['this', 'trackData'],
 
-        compute: ({trackData, [Track.instance]: track}) =>
+        compute: ({this: track, trackData}) =>
           trackData
             ? trackData
                 .filter((t) => !t.originalReleaseTrack)
@@ -344,9 +371,9 @@ export class Track extends Thing {
       flags: {expose: true},
 
       expose: {
-        dependencies: ['trackData'],
+        dependencies: ['this', 'trackData'],
 
-        compute: ({trackData, [Track.instance]: track}) =>
+        compute: ({this: track, trackData}) =>
           trackData
             ? trackData
                 .filter((t) => !t.originalReleaseTrack)
@@ -389,20 +416,20 @@ export class Track extends Thing {
       flags: {expose: true, compose: true},
 
       expose: {
-        dependencies: ['albumData'],
+        dependencies: ['this', 'albumData'],
 
-        compute({albumData, [Track.instance]: track}, continuation) {
+        compute({this: track, albumData}, continuation) {
           const album = albumData?.find((album) => album.tracks.includes(track));
+          const newDependencies = {};
 
-          const filteredAlbum = Object.create(null);
           for (const property of albumProperties) {
-            filteredAlbum[property] =
+            newDependencies['#album.' + property] =
               (album
                 ? album[property]
                 : null);
           }
 
-          return continuation({album: filteredAlbum});
+          return continuation(newDependencies);
         },
       },
     }),
diff --git a/src/util/sugar.js b/src/util/sugar.js
index 5b1f3193..1ba3f3ae 100644
--- a/src/util/sugar.js
+++ b/src/util/sugar.js
@@ -168,12 +168,24 @@ export function setIntersection(set1, set2) {
   return intersection;
 }
 
-export function filterProperties(obj, properties) {
-  const set = new Set(properties);
-  return Object.fromEntries(
-    Object
-      .entries(obj)
-      .filter(([key]) => set.has(key)));
+export function filterProperties(object, properties) {
+  if (typeof object !== 'object' || object === null) {
+    throw new TypeError(`Expected object to be an object, got ${object}`);
+  }
+
+  if (!Array.isArray(properties)) {
+    throw new TypeError(`Expected properties to be an array, got ${properties}`);
+  }
+
+  const filteredObject = {};
+
+  for (const property of properties) {
+    if (Object.hasOwn(object, property)) {
+      filteredObject[property] = object[property];
+    }
+  }
+
+  return filteredObject;
 }
 
 export function queue(array, max = 50) {