« get me outta code hell

data: roll paired "byRef" and "dynamic" properties into one - hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/src/data/things
diff options
context:
space:
mode:
author(quasar) nebula <qznebula@protonmail.com>2023-09-07 17:30:54 -0300
committer(quasar) nebula <qznebula@protonmail.com>2023-09-07 17:30:54 -0300
commitbbccaf51222cb4bed73466164496f5bc1030292c (patch)
treefc26c89f44b84140ba01b9edabedef10c73a637a /src/data/things
parentc18844784bd1c0ead7c49d0519727b7a92e23e13 (diff)
data: roll paired "byRef" and "dynamic" properties into one
Diffstat (limited to 'src/data/things')
-rw-r--r--src/data/things/album.js75
-rw-r--r--src/data/things/artist.js19
-rw-r--r--src/data/things/cacheable-object.js26
-rw-r--r--src/data/things/composite.js15
-rw-r--r--src/data/things/flash.js34
-rw-r--r--src/data/things/group.js27
-rw-r--r--src/data/things/homepage-layout.js60
-rw-r--r--src/data/things/thing.js147
-rw-r--r--src/data/things/track.js132
-rw-r--r--src/data/things/validators.js2
-rw-r--r--src/data/things/wiki-info.js15
11 files changed, 268 insertions, 284 deletions
diff --git a/src/data/things/album.js b/src/data/things/album.js
index 9cf58641..88308182 100644
--- a/src/data/things/album.js
+++ b/src/data/things/album.js
@@ -7,14 +7,12 @@ import Thing, {
   commentary,
   color,
   commentatorArtists,
-  contribsByRef,
   contribsPresent,
+  contributionList,
   directory,
-  dynamicContribs,
   fileExtension,
   flag,
   name,
-  resolvedReferenceList,
   referenceList,
   simpleDate,
   simpleString,
@@ -43,25 +41,31 @@ export class Album extends Thing {
       update: {validate: isDate},
 
       expose: {
-        dependencies: ['date', 'coverArtistContribsByRef'],
-        transform: (coverArtDate, {
-          coverArtistContribsByRef,
-          date,
-        }) =>
-          (!empty(coverArtistContribsByRef)
+        dependencies: ['date', 'coverArtistContribs'],
+        transform: (coverArtDate, {coverArtistContribs, date}) =>
+          (!empty(coverArtistContribs)
             ? coverArtDate ?? date ?? null
             : null),
       },
     },
 
-    artistContribsByRef: contribsByRef(),
-    coverArtistContribsByRef: contribsByRef(),
-    trackCoverArtistContribsByRef: contribsByRef(),
-    wallpaperArtistContribsByRef: contribsByRef(),
-    bannerArtistContribsByRef: contribsByRef(),
+    artistContribs: contributionList(),
+    coverArtistContribs: contributionList(),
+    trackCoverArtistContribs: contributionList(),
+    wallpaperArtistContribs: contributionList(),
+    bannerArtistContribs: contributionList(),
 
-    groupsByRef: referenceList(Group),
-    artTagsByRef: referenceList(ArtTag),
+    groups: referenceList({
+      class: Group,
+      find: find.group,
+      data: 'groupData',
+    }),
+
+    artTags: referenceList({
+      class: ArtTag,
+      find: find.artTag,
+      data: 'artTagData',
+    }),
 
     trackSections: {
       flags: {update: true, expose: true},
@@ -84,13 +88,12 @@ export class Album extends Thing {
             isDefaultTrackSection: section.isDefaultTrackSection ?? false,
 
             startIndex: (
-              startIndex += section.tracksByRef.length,
-              startIndex - section.tracksByRef.length
+              startIndex += section.tracks.length,
+              startIndex - section.tracks.length
             ),
 
-            tracksByRef: section.tracksByRef ?? [],
             tracks:
-              (trackData && section.tracksByRef
+              (trackData && section.tracks
                 ?.map(ref => find.track(ref, trackData, {mode: 'quiet'}))
                 .filter(Boolean)) ??
               [],
@@ -128,29 +131,11 @@ export class Album extends Thing {
 
     // Expose only
 
-    artistContribs: dynamicContribs('artistContribsByRef'),
-    coverArtistContribs: dynamicContribs('coverArtistContribsByRef'),
-    trackCoverArtistContribs: dynamicContribs('trackCoverArtistContribsByRef'),
-    wallpaperArtistContribs: dynamicContribs('wallpaperArtistContribsByRef'),
-    bannerArtistContribs: dynamicContribs('bannerArtistContribsByRef'),
-
     commentatorArtists: commentatorArtists(),
 
-    groups: resolvedReferenceList({
-      list: 'groupsByRef',
-      data: 'groupData',
-      find: find.group,
-    }),
-
-    artTags: resolvedReferenceList({
-      list: 'artTagsByRef',
-      data: 'artTagData',
-      find: find.artTag,
-    }),
-
-    hasCoverArt: contribsPresent('coverArtistContribsByRef'),
-    hasWallpaperArt: contribsPresent('wallpaperArtistContribsByRef'),
-    hasBannerArt: contribsPresent('bannerArtistContribsByRef'),
+    hasCoverArt: contribsPresent('coverArtistContribs'),
+    hasWallpaperArt: contribsPresent('wallpaperArtistContribs'),
+    hasBannerArt: contribsPresent('bannerArtistContribs'),
 
     tracks: {
       flags: {expose: true},
@@ -158,12 +143,12 @@ export class Album extends Thing {
       expose: {
         dependencies: ['trackSections', 'trackData'],
         compute: ({trackSections, trackData}) =>
-          trackSections && trackData
+          (trackSections && trackData
             ? trackSections
-                .flatMap((section) => section.tracksByRef ?? [])
-                .map((ref) => find.track(ref, trackData, {mode: 'quiet'}))
+                .flatMap(section => section.tracks ?? [])
+                .map(ref => find.track(ref, trackData, {mode: 'quiet'}))
                 .filter(Boolean)
-            : [],
+            : []),
       },
     },
   });
diff --git a/src/data/things/artist.js b/src/data/things/artist.js
index 2676591a..7a9dbd3c 100644
--- a/src/data/things/artist.js
+++ b/src/data/things/artist.js
@@ -33,7 +33,12 @@ export class Artist extends Thing {
     },
 
     isAlias: flag(),
-    aliasedArtistRef: singleReference(Artist),
+
+    aliasedArtist: singleReference({
+      class: Artist,
+      find: find.artist,
+      data: 'artistData',
+    }),
 
     // Update only
 
@@ -44,18 +49,6 @@ export class Artist extends Thing {
 
     // Expose only
 
-    aliasedArtist: {
-      flags: {expose: true},
-
-      expose: {
-        dependencies: ['artistData', 'aliasedArtistRef'],
-        compute: ({artistData, aliasedArtistRef}) =>
-          aliasedArtistRef && artistData
-            ? find.artist(aliasedArtistRef, artistData, {mode: 'quiet'})
-            : null,
-      },
-    },
-
     tracksAsArtist:
       Artist.filterByContrib('trackData', 'artistContribs'),
     tracksAsContributor:
diff --git a/src/data/things/cacheable-object.js b/src/data/things/cacheable-object.js
index 92a46d66..4bc3668d 100644
--- a/src/data/things/cacheable-object.js
+++ b/src/data/things/cacheable-object.js
@@ -86,16 +86,14 @@ export default class CacheableObject {
   #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.
-    */
+  // 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.
 
   constructor() {
     this.#defineProperties();
@@ -352,4 +350,12 @@ export default class CacheableObject {
       console.log(` - ${line}`);
     }
   }
+
+  static getUpdateValue(object, key) {
+    if (!Object.hasOwn(object, key)) {
+      return undefined;
+    }
+
+    return object.#propertyUpdateValues[key] ?? null;
+  }
 }
diff --git a/src/data/things/composite.js b/src/data/things/composite.js
index 29f5770c..96abf4af 100644
--- a/src/data/things/composite.js
+++ b/src/data/things/composite.js
@@ -1071,3 +1071,18 @@ export function raiseWithoutUpdateValue({
     },
   ]);
 }
+
+export function withUpdateValueAsDependency({
+  into = '#updateValue',
+} = {}) {
+  return {
+    annotation: `withUpdateValueAsDependency`,
+    flags: {expose: true, compose: true},
+
+    expose: {
+      mapContinuation: {into},
+      transform: (value, continuation) =>
+        continuation(value, {into: value}),
+    },
+  };
+}
diff --git a/src/data/things/flash.js b/src/data/things/flash.js
index 4e640dac..eb16d29e 100644
--- a/src/data/things/flash.js
+++ b/src/data/things/flash.js
@@ -9,13 +9,11 @@ import {
 } from '#validators';
 
 import Thing, {
-  dynamicContribs,
   color,
-  contribsByRef,
+  contributionList,
   fileExtension,
   name,
   referenceList,
-  resolvedReferenceList,
   simpleDate,
   simpleString,
   urls,
@@ -60,9 +58,13 @@ export class Flash extends Thing {
 
     coverArtFileExtension: fileExtension('jpg'),
 
-    contributorContribsByRef: contribsByRef(),
+    contributorContribs: contributionList(),
 
-    featuredTracksByRef: referenceList(Track),
+    featuredTracks: referenceList({
+      class: Track,
+      find: find.track,
+      data: 'trackData',
+    }),
 
     urls: urls(),
 
@@ -74,14 +76,6 @@ export class Flash extends Thing {
 
     // Expose only
 
-    contributorContribs: dynamicContribs('contributorContribsByRef'),
-
-    featuredTracks: resolvedReferenceList({
-      list: 'featuredTracksByRef',
-      data: 'trackData',
-      find: find.track,
-    }),
-
     act: {
       flags: {expose: true},
 
@@ -138,18 +132,14 @@ export class FlashAct extends Thing {
       }
     },
 
-    flashesByRef: referenceList(Flash),
+    flashes: referenceList({
+      class: Flash,
+      data: 'flashData',
+      find: find.flash,
+    }),
 
     // Update only
 
     flashData: wikiData(Flash),
-
-    // Expose only
-
-    flashes: resolvedReferenceList({
-      list: 'flashesByRef',
-      data: 'flashData',
-      find: find.flash,
-    }),
   })
 }
diff --git a/src/data/things/group.js b/src/data/things/group.js
index 873c6d88..f53fa48e 100644
--- a/src/data/things/group.js
+++ b/src/data/things/group.js
@@ -5,7 +5,6 @@ import Thing, {
   directory,
   name,
   referenceList,
-  resolvedReferenceList,
   simpleString,
   urls,
   wikiData,
@@ -24,7 +23,11 @@ export class Group extends Thing {
 
     urls: urls(),
 
-    featuredAlbumsByRef: referenceList(Album),
+    featuredAlbums: referenceList({
+      class: Album,
+      find: find.album,
+      data: 'albumData',
+    }),
 
     // Update only
 
@@ -33,12 +36,6 @@ export class Group extends Thing {
 
     // Expose only
 
-    featuredAlbums: resolvedReferenceList({
-      list: 'featuredAlbumsByRef',
-      data: 'albumData',
-      find: find.album,
-    }),
-
     descriptionShort: {
       flags: {expose: true},
 
@@ -89,18 +86,14 @@ export class GroupCategory extends Thing {
     name: name('Unnamed Group Category'),
     color: color(),
 
-    groupsByRef: referenceList(Group),
+    groups: referenceList({
+      class: Group,
+      find: find.group,
+      data: 'groupData',
+    }),
 
     // Update only
 
     groupData: wikiData(Group),
-
-    // Expose only
-
-    groups: resolvedReferenceList({
-      list: 'groupsByRef',
-      data: 'groupData',
-      find: find.group,
-    }),
   });
 }
diff --git a/src/data/things/homepage-layout.js b/src/data/things/homepage-layout.js
index ab6f4cff..b509c1e2 100644
--- a/src/data/things/homepage-layout.js
+++ b/src/data/things/homepage-layout.js
@@ -1,23 +1,29 @@
 import find from '#find';
 
 import {
+  compositeFrom,
+  exposeDependency,
+  withUpdateValueAsDependency,
+} from '#composite';
+
+import {
   is,
   isCountingNumber,
   isString,
   isStringNonEmpty,
+  oneOf,
   validateArrayItems,
   validateInstanceOf,
+  validateReference,
 } from '#validators';
 
 import Thing, {
   color,
   name,
   referenceList,
-  resolvedReference,
-  resolvedReferenceList,
   simpleString,
-  singleReference,
   wikiData,
+  withResolvedReference,
 } from './thing.js';
 
 export class HomepageLayout extends Thing {
@@ -101,8 +107,38 @@ export class HomepageLayoutAlbumsRow extends HomepageLayoutRow {
       },
     },
 
-    sourceGroupByRef: singleReference(Group),
-    sourceAlbumsByRef: referenceList(Album),
+    sourceGroup: compositeFrom(`HomepageLayoutAlbumsRow.sourceGroup`, [
+      {
+        transform: (value, continuation) =>
+          (value === 'new-releases' || value === 'new-additions'
+            ? value
+            : continuation(value)),
+      },
+
+      withUpdateValueAsDependency(),
+
+      withResolvedReference({
+        ref: '#updateValue',
+        data: 'groupData',
+        find: find.group,
+      }),
+
+      exposeDependency({
+        dependency: '#resolvedReference',
+        update: {
+          validate:
+            oneOf(
+              is('new-releases', 'new-additions'),
+              validateReference(Group[Thing.referenceType])),
+        },
+      }),
+    ]),
+
+    sourceAlbums: referenceList({
+      class: Album,
+      find: find.album,
+      data: 'albumData',
+    }),
 
     countAlbumsFromGroup: {
       flags: {update: true, expose: true},
@@ -113,19 +149,5 @@ export class HomepageLayoutAlbumsRow extends HomepageLayoutRow {
       flags: {update: true, expose: true},
       update: {validate: validateArrayItems(isString)},
     },
-
-    // Expose only
-
-    sourceGroup: resolvedReference({
-      ref: 'sourceGroupByRef',
-      data: 'groupData',
-      find: find.group,
-    }),
-
-    sourceAlbums: resolvedReferenceList({
-      list: 'sourceAlbumsByRef',
-      data: 'albumData',
-      find: find.album,
-    }),
   });
 }
diff --git a/src/data/things/thing.js b/src/data/things/thing.js
index 9d8b2ea2..91ad96af 100644
--- a/src/data/things/thing.js
+++ b/src/data/things/thing.js
@@ -11,8 +11,11 @@ import {filterMultipleArrays, getKebabCase} from '#wiki-data';
 import {
   compositeFrom,
   exitWithoutDependency,
+  exposeConstant,
   exposeDependency,
+  exposeDependencyOrContinue,
   raiseWithoutDependency,
+  withUpdateValueAsDependency,
 } from '#composite';
 
 import {
@@ -162,22 +165,31 @@ export function externalFunction() {
   };
 }
 
-// Super simple "contributions by reference" list, used for a variety of
-// properties (Artists, Cover Artists, etc). This is the property which is
-// externally provided, in the form:
+// Strong 'n sturdy contribution list, rolling a list of references (provided
+// as this property's update value) and the resolved results (as get exposed)
+// into one property. Update value will look something like this:
 //
-//     [
-//         {who: 'Artist Name', what: 'Viola'},
-//         {who: 'artist:john-cena', what: null},
-//         ...
-//     ]
+//   [
+//     {who: 'Artist Name', what: 'Viola'},
+//     {who: 'artist:john-cena', what: null},
+//     ...
+//   ]
 //
-// ...processed from YAML, spreadsheet, or any other kind of input.
-export function contribsByRef() {
-  return {
-    flags: {update: true, expose: true},
-    update: {validate: isContributionList},
-  };
+// ...typically as processed from YAML, spreadsheet, or elsewhere.
+// Exposes as the same, but with the "who" replaced with matches found in
+// artistData - which means this always depends on an `artistData` property
+// also existing on this object!
+//
+export function contributionList() {
+  return compositeFrom(`contributionList`, [
+    withUpdateValueAsDependency(),
+    withResolvedContribs({from: '#updateValue'}),
+    exposeDependencyOrContinue({dependency: '#resolvedContribs'}),
+    exposeConstant({
+      value: [],
+      update: {validate: isContributionList},
+    }),
+  ]);
 }
 
 // Artist commentary! Generally present on tracks and albums.
@@ -222,88 +234,77 @@ export function additionalFiles() {
 // 'artist' or 'track', but this utility keeps from having to hard-code the
 // string in multiple places by referencing the value saved on the class
 // instead.
-export function referenceList(thingClass) {
-  const {[Thing.referenceType]: referenceType} = thingClass;
-  if (!referenceType) {
-    throw new Error(`The passed constructor ${thingClass.name} doesn't define Thing.referenceType!`);
+export function referenceList({
+  class: thingClass,
+  data,
+  find,
+}) {
+  if (!thingClass) {
+    throw new TypeError(`Expected a Thing class`);
   }
 
-  return {
-    flags: {update: true, expose: true},
-    update: {validate: validateReferenceList(referenceType)},
-  };
-}
-
-// Corresponding function for a single reference.
-export function singleReference(thingClass) {
   const {[Thing.referenceType]: referenceType} = thingClass;
   if (!referenceType) {
     throw new Error(`The passed constructor ${thingClass.name} doesn't define Thing.referenceType!`);
   }
 
-  return {
-    flags: {update: true, expose: true},
-    update: {validate: validateReference(referenceType)},
-  };
-}
+  return compositeFrom(`referenceList`, [
+    withUpdateValueAsDependency(),
 
-// Corresponding dynamic property to referenceList, which takes the values
-// in the provided property and searches the specified wiki data for
-// matching actual Thing-subclass objects.
-export function resolvedReferenceList({list, data, find}) {
-  return compositeFrom(`resolvedReferenceList`, [
     withResolvedReferenceList({
-      list, data, find,
+      data, find,
+      list: '#updateValue',
       notFoundMode: 'filter',
     }),
 
-    exposeDependency({dependency: '#resolvedReferenceList'}),
+    exposeDependency({
+      dependency: '#resolvedReferenceList',
+      update: {
+        validate: validateReferenceList(referenceType),
+      },
+    }),
   ]);
 }
 
 // Corresponding function for a single reference.
-export function resolvedReference({ref, data, find}) {
-  return compositeFrom(`resolvedReference`, [
-    withResolvedReference({ref, data, find}),
-    exposeDependency({dependency: '#resolvedReference'}),
-  ]);
-}
+export function singleReference({
+  class: thingClass,
+  data,
+  find,
+}) {
+  if (!thingClass) {
+    throw new TypeError(`Expected a Thing class`);
+  }
 
-// Corresponding dynamic property to contribsByRef, which takes the values
-// in the provided property and searches the object's artistData for
-// matching actual Artist objects. The computed structure has the same form
-// as contribsByRef, but with Artist objects instead of string references:
-//
-//     [
-//         {who: (an Artist), what: 'Viola'},
-//         {who: (an Artist), what: null},
-//         ...
-//     ]
-//
-// Contributions whose "who" values don't match anything in artistData are
-// filtered out. (So if the list is all empty, chances are that either the
-// reference list is somehow messed up, or artistData isn't being provided
-// properly.)
-export function dynamicContribs(contribsByRefProperty) {
-  return compositeFrom(`dynamicContribs`, [
-    withResolvedContribs({from: contribsByRefProperty}),
-    exposeDependency({dependency: '#resolvedContribs'}),
+  const {[Thing.referenceType]: referenceType} = thingClass;
+  if (!referenceType) {
+    throw new Error(`The passed constructor ${thingClass.name} doesn't define Thing.referenceType!`);
+  }
+
+  return compositeFrom(`singleReference`, [
+    withUpdateValueAsDependency(),
+
+    withResolvedReference({ref: '#updateValue', data, find}),
+
+    exposeDependency({
+      dependency: '#resolvedReference',
+      update: {
+        validate: validateReference(referenceType),
+      },
+    }),
   ]);
 }
 
 // Nice 'n simple shorthand for an exposed-only flag which is true when any
 // contributions are present in the specified property.
-export function contribsPresent(contribsByRefProperty) {
+export function contribsPresent(contribsProperty) {
   return {
     flags: {expose: true},
     expose: {
-      dependencies: [contribsByRefProperty],
-      compute({
-        [contribsByRefProperty]: contribsByRef,
-      }) {
-        return !empty(contribsByRef);
-      },
-    }
+      dependencies: [contribsProperty],
+      compute: ({[contribsProperty]: contribs}) =>
+        !empty(contribs),
+    },
   };
 }
 
@@ -380,13 +381,13 @@ export function withResolvedContribs({
       mapDependencies: {from},
       compute: ({from}, continuation) =>
         continuation({
-          '#whoByRef': from.map(({who}) => who),
+          '#artistRefs': from.map(({who}) => who),
           '#what': from.map(({what}) => what),
         }),
     },
 
     withResolvedReferenceList({
-      list: '#whoByRef',
+      list: '#artistRefs',
       data: 'artistData',
       into: '#who',
       find: find.artist,
diff --git a/src/data/things/track.js b/src/data/things/track.js
index fcfd39c7..8263d399 100644
--- a/src/data/things/track.js
+++ b/src/data/things/track.js
@@ -3,7 +3,6 @@ import {inspect} from 'node:util';
 import {colors} from '#cli';
 import find from '#find';
 import {empty} from '#sugar';
-import {isColor, isDate, isDuration, isFileExtension} from '#validators';
 
 import {
   compositeFrom,
@@ -13,20 +12,28 @@ import {
   exposeDependencyOrContinue,
   exposeUpdateValueOrContinue,
   withResultOfAvailabilityCheck,
+  withUpdateValueAsDependency,
 } from '#composite';
 
+import {
+  isColor,
+  isContributionList,
+  isDate,
+  isDuration,
+  isFileExtension,
+} from '#validators';
+
+import CacheableObject from './cacheable-object.js';
+
 import Thing, {
   additionalFiles,
   commentary,
   commentatorArtists,
-  contribsByRef,
+  contributionList,
   directory,
-  dynamicContribs,
   flag,
   name,
   referenceList,
-  resolvedReference,
-  resolvedReferenceList,
   reverseReferenceList,
   simpleDate,
   singleReference,
@@ -55,13 +62,11 @@ export class Track extends Thing {
     urls: urls(),
     dateFirstReleased: simpleDate(),
 
-    artistContribsByRef: contribsByRef(),
-    contributorContribsByRef: contribsByRef(),
-    coverArtistContribsByRef: contribsByRef(),
-
-    referencedTracksByRef: referenceList(Track),
-    sampledTracksByRef: referenceList(Track),
-    artTagsByRef: referenceList(ArtTag),
+    artTags: referenceList({
+      class: ArtTag,
+      find: find.artTag,
+      data: 'artTagData',
+    }),
 
     color: compositeFrom(`Track.color`, [
       exposeUpdateValueOrContinue(),
@@ -134,9 +139,24 @@ export class Track extends Thing {
       }),
     ]),
 
-    originalReleaseTrackByRef: singleReference(Track),
+    originalReleaseTrack: singleReference({
+      class: Track,
+      find: find.track,
+      data: 'trackData',
+    }),
 
-    dataSourceAlbumByRef: singleReference(Album),
+    // Note - this is an internal property used only to help identify a track.
+    // It should not be assumed in general that the album and dataSourceAlbum match
+    // (i.e. a track may dynamically be moved from one album to another, at
+    // which point dataSourceAlbum refers to where it was originally from, and is
+    // not generally relevant information). It's also not guaranteed that
+    // dataSourceAlbum is available (depending on the Track creator to optionally
+    // provide this property's update value).
+    dataSourceAlbum: singleReference({
+      class: Album,
+      find: find.album,
+      data: 'albumData',
+    }),
 
     commentary: commentary(),
     lyrics: simpleString(),
@@ -161,19 +181,6 @@ export class Track extends Thing {
       exposeDependency({dependency: '#album'}),
     ]),
 
-    // Note - this is an internal property used only to help identify a track.
-    // It should not be assumed in general that the album and dataSourceAlbum match
-    // (i.e. a track may dynamically be moved from one album to another, at
-    // which point dataSourceAlbum refers to where it was originally from, and is
-    // not generally relevant information). It's also not guaranteed that
-    // dataSourceAlbum is available (depending on the Track creator to optionally
-    // provide dataSourceAlbumByRef).
-    dataSourceAlbum: resolvedReference({
-      ref: 'dataSourceAlbumByRef',
-      data: 'albumData',
-      find: find.album,
-    }),
-
     date: compositeFrom(`Track.date`, [
       exposeDependencyOrContinue({dependency: 'dateFirstReleased'}),
       withAlbumProperty({property: 'date'}),
@@ -192,11 +199,6 @@ export class Track extends Thing {
       exposeDependency({dependency: '#hasUniqueCoverArt'}),
     ]),
 
-    originalReleaseTrack: compositeFrom(`Track.originalReleaseTrack`, [
-      withOriginalRelease(),
-      exposeDependency({dependency: '#originalRelease'}),
-    ]),
-
     otherReleases: compositeFrom(`Track.otherReleases`, [
       exitWithoutDependency({dependency: 'trackData', mode: 'empty'}),
       withOriginalRelease({selfIfOriginal: true}),
@@ -224,26 +226,20 @@ export class Track extends Thing {
     artistContribs: compositeFrom(`Track.artistContribs`, [
       inheritFromOriginalRelease({property: 'artistContribs'}),
 
-      withResolvedContribs({
-        from: 'artistContribsByRef',
-        into: '#artistContribs',
-      }),
-
-      {
-        dependencies: ['#artistContribs'],
-        compute: ({'#artistContribs': contribsFromTrack}, continuation) =>
-          (empty(contribsFromTrack)
-            ? continuation()
-            : contribsFromTrack),
-      },
+      withUpdateValueAsDependency(),
+      withResolvedContribs({from: '#updateValue', into: '#artistContribs'}),
+      exposeDependencyOrContinue({dependency: '#artistContribs'}),
 
       withAlbumProperty({property: 'artistContribs'}),
-      exposeDependency({dependency: '#album.artistContribs'}),
+      exposeDependency({
+        dependency: '#album.artistContribs',
+        update: {validate: isContributionList},
+      }),
     ]),
 
     contributorContribs: compositeFrom(`Track.contributorContribs`, [
       inheritFromOriginalRelease({property: 'contributorContribs'}),
-      dynamicContribs('contributorContribsByRef'),
+      contributionList(),
     ]),
 
     // Cover artists aren't inherited from the original release, since it
@@ -258,47 +254,35 @@ export class Track extends Thing {
             : continuation()),
       },
 
-      withResolvedContribs({
-        from: 'coverArtistContribsByRef',
-        into: '#coverArtistContribs',
-      }),
-
-      {
-        dependencies: ['#coverArtistContribs'],
-        compute: ({'#coverArtistContribs': contribsFromTrack}, continuation) =>
-          (empty(contribsFromTrack)
-            ? continuation()
-            : contribsFromTrack),
-      },
+      withUpdateValueAsDependency(),
+      withResolvedContribs({from: '#updateValue', into: '#coverArtistContribs'}),
+      exposeDependencyOrContinue({dependency: '#coverArtistContribs'}),
 
       withAlbumProperty({property: 'trackCoverArtistContribs'}),
-      exposeDependency({dependency: '#album.trackCoverArtistContribs'}),
+      exposeDependency({
+        dependency: '#album.trackCoverArtistContribs',
+        update: {validate: isContributionList},
+      }),
     ]),
 
     referencedTracks: compositeFrom(`Track.referencedTracks`, [
       inheritFromOriginalRelease({property: 'referencedTracks'}),
-      resolvedReferenceList({
-        list: 'referencedTracksByRef',
-        data: 'trackData',
+      referenceList({
+        class: Track,
         find: find.track,
+        data: 'trackData',
       }),
     ]),
 
     sampledTracks: compositeFrom(`Track.sampledTracks`, [
       inheritFromOriginalRelease({property: 'sampledTracks'}),
-      resolvedReferenceList({
-        list: 'sampledTracksByRef',
-        data: 'trackData',
+      referenceList({
+        class: Track,
         find: find.track,
+        data: 'trackData',
       }),
     ]),
 
-    artTags: resolvedReferenceList({
-      list: 'artTagsByRef',
-      data: 'artTagData',
-      find: find.artTag,
-    }),
-
     // Specifically exclude re-releases from this list - while it's useful to
     // get from a re-release to the tracks it references, re-releases aren't
     // generally relevant from the perspective of the tracks being referenced.
@@ -327,7 +311,7 @@ export class Track extends Thing {
 
     parts.push(Thing.prototype[inspect.custom].apply(this));
 
-    if (this.originalReleaseTrackByRef) {
+    if (CacheableObject.getUpdateValue(this, 'originalReleaseTrack')) {
       parts.unshift(`${colors.yellow('[rerelease]')} `);
     }
 
@@ -564,7 +548,7 @@ function withOriginalRelease({
 } = {}) {
   return compositeFrom(`withOriginalRelease`, [
     withResolvedReference({
-      ref: 'originalReleaseTrackByRef',
+      ref: 'originalReleaseTrack',
       data: 'trackData',
       into: '#originalRelease',
       find: find.track,
@@ -607,7 +591,7 @@ function withHasUniqueCoverArt({
     },
 
     withResolvedContribs({
-      from: 'coverArtistContribsByRef',
+      from: 'coverArtistContribs',
       into: '#coverArtistContribs',
     }),
 
diff --git a/src/data/things/validators.js b/src/data/things/validators.js
index 4c8f683b..f0d1d9fd 100644
--- a/src/data/things/validators.js
+++ b/src/data/things/validators.js
@@ -308,7 +308,7 @@ export const isTrackSection = validateProperties({
   color: optional(isColor),
   dateOriginallyReleased: optional(isDate),
   isDefaultTrackSection: optional(isBoolean),
-  tracksByRef: optional(validateReferenceList('track')),
+  tracks: optional(validateReferenceList('track')),
 });
 
 export const isTrackSectionList = validateArrayItems(isTrackSection);
diff --git a/src/data/things/wiki-info.js b/src/data/things/wiki-info.js
index 416b6c4e..7c2de324 100644
--- a/src/data/things/wiki-info.js
+++ b/src/data/things/wiki-info.js
@@ -6,7 +6,6 @@ import Thing, {
   flag,
   name,
   referenceList,
-  resolvedReferenceList,
   simpleString,
   wikiData,
 } from './thing.js';
@@ -45,7 +44,11 @@ export class WikiInfo extends Thing {
       update: {validate: isURL},
     },
 
-    divideTrackListsByGroupsByRef: referenceList(Group),
+    divideTrackListsByGroups: referenceList({
+      class: Group,
+      find: find.group,
+      data: 'groupData',
+    }),
 
     // Feature toggles
     enableFlashesAndGames: flag(false),
@@ -57,13 +60,5 @@ export class WikiInfo extends Thing {
     // Update only
 
     groupData: wikiData(Group),
-
-    // Expose only
-
-    divideTrackListsByGroups: resolvedReferenceList({
-      list: 'divideTrackListsByGroupsByRef',
-      data: 'groupData',
-      find: find.group,
-    }),
   });
 }