« 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
diff options
context:
space:
mode:
Diffstat (limited to 'src/data')
-rw-r--r--src/data/things/composite.js32
-rw-r--r--src/data/things/thing.js36
-rw-r--r--src/data/things/track.js670
3 files changed, 379 insertions, 359 deletions
diff --git a/src/data/things/composite.js b/src/data/things/composite.js
index 21cf365f..bcc52a2a 100644
--- a/src/data/things/composite.js
+++ b/src/data/things/composite.js
@@ -808,7 +808,7 @@ function _export(mapping) {
   const mappingEntries = Object.entries(mapping);
 
   return {
-    annotation: `Thing.composite.export`,
+    annotation: `export`,
     flags: {expose: true, compose: true},
 
     expose: {
@@ -853,7 +853,7 @@ export function exposeDependency(dependency, {
   update = false,
 } = {}) {
   return {
-    annotation: `Thing.composite.exposeDependency`,
+    annotation: `exposeDependency`,
     flags: {expose: true, update: !!update},
 
     expose: {
@@ -878,7 +878,7 @@ export function exposeConstant(value, {
   update = false,
 } = {}) {
   return {
-    annotation: `Thing.composite.exposeConstant`,
+    annotation: `exposeConstant`,
     flags: {expose: true, update: !!update},
 
     expose: {
@@ -934,7 +934,7 @@ export function withResultOfAvailabilityCheck({
 
   if (fromDependency) {
     return {
-      annotation: `Thing.composite.withResultOfAvailabilityCheck.fromDependency`,
+      annotation: `withResultOfAvailabilityCheck.fromDependency`,
       flags: {expose: true, compose: true},
       expose: {
         mapDependencies: {from: fromDependency},
@@ -946,7 +946,7 @@ export function withResultOfAvailabilityCheck({
     };
   } else {
     return {
-      annotation: `Thing.composite.withResultOfAvailabilityCheck.fromUpdateValue`,
+      annotation: `withResultOfAvailabilityCheck.fromUpdateValue`,
       flags: {expose: true, compose: true},
       expose: {
         mapContinuation: {to},
@@ -963,7 +963,7 @@ export function withResultOfAvailabilityCheck({
 export function exposeDependencyOrContinue(dependency, {
   mode = 'null',
 } = {}) {
-  return compositeFrom(`Thing.composite.exposeDependencyOrContinue`, [
+  return compositeFrom(`exposeDependencyOrContinue`, [
     withResultOfAvailabilityCheck({
       fromDependency: dependency,
       mode,
@@ -991,7 +991,7 @@ export function exposeDependencyOrContinue(dependency, {
 export function exposeUpdateValueOrContinue({
   mode = 'null',
 } = {}) {
-  return compositeFrom(`Thing.composite.exposeUpdateValueOrContinue`, [
+  return compositeFrom(`exposeUpdateValueOrContinue`, [
     withResultOfAvailabilityCheck({
       fromUpdateValue: true,
       mode,
@@ -1019,7 +1019,7 @@ export function earlyExitIfAvailabilityCheckFailed({
   availability = '#availability',
   value = null,
 } = {}) {
-  return compositeFrom(`Thing.composite.earlyExitIfAvailabilityCheckFailed`, [
+  return compositeFrom(`earlyExitIfAvailabilityCheckFailed`, [
     {
       mapDependencies: {availability},
       compute: ({availability}, continuation) =>
@@ -1042,7 +1042,7 @@ export function earlyExitWithoutDependency(dependency, {
   mode = 'null',
   value = null,
 } = {}) {
-  return compositeFrom(`Thing.composite.earlyExitWithoutDependency`, [
+  return compositeFrom(`earlyExitWithoutDependency`, [
     withResultOfAvailabilityCheck({fromDependency: dependency, mode}),
     earlyExitIfAvailabilityCheckFailed({value}),
   ]);
@@ -1054,7 +1054,7 @@ export function earlyExitWithoutUpdateValue({
   mode = 'null',
   value = null,
 } = {}) {
-  return compositeFrom(`Thing.composite.earlyExitWithoutDependency`, [
+  return compositeFrom(`earlyExitWithoutDependency`, [
     withResultOfAvailabilityCheck({fromUpdateValue: true, mode}),
     earlyExitIfAvailabilityCheckFailed({value}),
   ]);
@@ -1067,7 +1067,7 @@ export function raiseWithoutDependency(dependency, {
   map = {},
   raise = {},
 } = {}) {
-  return compositeFrom(`Thing.composite.raiseWithoutDependency`, [
+  return compositeFrom(`raiseWithoutDependency`, [
     withResultOfAvailabilityCheck({fromDependency: dependency, mode}),
 
     {
@@ -1094,7 +1094,7 @@ export function raiseWithoutUpdateValue({
   map = {},
   raise = {},
 } = {}) {
-  return compositeFrom(`Thing.composite.raiseWithoutUpdateValue`, [
+  return compositeFrom(`raiseWithoutUpdateValue`, [
     withResultOfAvailabilityCheck({fromUpdateValue: true, mode}),
 
     {
@@ -1121,8 +1121,8 @@ export function raiseWithoutUpdateValue({
 // means mapping the "who" reference of each contribution to an artist
 // object, and filtering out those whose "who" doesn't match any artist.
 export function withResolvedContribs({from, to}) {
-  return Thing.composite.from(`Thing.composite.withResolvedContribs`, [
-    Thing.composite.raiseWithoutDependency(from, {
+  return compositeFrom(`withResolvedContribs`, [
+    raiseWithoutDependency(from, {
       mode: 'empty',
       map: {to},
       raise: {to: []},
@@ -1172,7 +1172,7 @@ export function withResolvedReference({
   to = '#resolvedReference',
   earlyExitIfNotFound = false,
 }) {
-  return compositeFrom(`Thing.composite.withResolvedReference`, [
+  return compositeFrom(`withResolvedReference`, [
     raiseWithoutDependency(ref, {map: {to}, raise: {to: null}}),
     earlyExitWithoutDependency(data),
 
@@ -1210,7 +1210,7 @@ export function withResolvedReferenceList({
     throw new TypeError(`Expected notFoundMode to be filter, exit, or null`);
   }
 
-  return compositeFrom(`Thing.composite.withResolvedReferenceList`, [
+  return compositeFrom(`withResolvedReferenceList`, [
     earlyExitWithoutDependency(data, {value: []}),
 
     raiseWithoutDependency(list, {
diff --git a/src/data/things/thing.js b/src/data/things/thing.js
index 968dd102..0716931a 100644
--- a/src/data/things/thing.js
+++ b/src/data/things/thing.js
@@ -9,6 +9,15 @@ import {empty} from '#sugar';
 import {getKebabCase} from '#wiki-data';
 
 import {
+  from as compositeFrom,
+  exposeDependency,
+  withReverseReferenceList,
+  withResolvedContribs,
+  withResolvedReference,
+  withResolvedReferenceList,
+} from '#composite';
+
+import {
   isAdditionalFileList,
   isBoolean,
   isCommentary,
@@ -27,7 +36,6 @@ import {
 } from '#validators';
 
 import CacheableObject from './cacheable-object.js';
-import * as composite from './composite.js';
 
 export default class Thing extends CacheableObject {
   static referenceType = Symbol('Thing.referenceType');
@@ -194,20 +202,20 @@ export default class Thing extends CacheableObject {
     // in the provided property and searches the specified wiki data for
     // matching actual Thing-subclass objects.
     resolvedReferenceList({list, data, find}) {
-      return Thing.composite.from(`Thing.common.resolvedReferenceList`, [
-        Thing.composite.withResolvedReferenceList({
+      return compositeFrom(`Thing.common.resolvedReferenceList`, [
+        withResolvedReferenceList({
           list, data, find,
           notFoundMode: 'filter',
         }),
-        Thing.composite.exposeDependency('#resolvedReferenceList'),
+        exposeDependency('#resolvedReferenceList'),
       ]);
     },
 
     // Corresponding function for a single reference.
     resolvedReference({ref, data, find}) {
-      return Thing.composite.from(`Thing.common.resolvedReference`, [
-        Thing.composite.withResolvedReference({ref, data, find}),
-        Thing.composite.exposeDependency('#resolvedReference'),
+      return compositeFrom(`Thing.common.resolvedReference`, [
+        withResolvedReference({ref, data, find}),
+        exposeDependency('#resolvedReference'),
       ]);
     },
 
@@ -227,13 +235,13 @@ export default class Thing extends CacheableObject {
     // reference list is somehow messed up, or artistData isn't being provided
     // properly.)
     dynamicContribs(contribsByRefProperty) {
-      return Thing.composite.from(`Thing.common.dynamicContribs`, [
-        Thing.composite.withResolvedContribs({
+      return compositeFrom(`Thing.common.dynamicContribs`, [
+        withResolvedContribs({
           from: contribsByRefProperty,
           to: '#contribs',
         }),
 
-        Thing.composite.exposeDependency('#contribs'),
+        exposeDependency('#contribs'),
       ]);
     },
 
@@ -257,9 +265,9 @@ export default class Thing extends CacheableObject {
     // property. Naturally, the passed ref list property is of the things in the
     // wiki data provided, not the requesting Thing itself.
     reverseReferenceList({data, list}) {
-      return Thing.composite.from(`Thing.common.reverseReferenceList`, [
-        Thing.composite.withReverseReferenceList({data, list}),
-        Thing.composite.exposeDependency('#reverseReferenceList'),
+      return compositeFrom(`Thing.common.reverseReferenceList`, [
+        withReverseReferenceList({data, list}),
+        exposeDependency('#reverseReferenceList'),
       ]);
     },
 
@@ -323,6 +331,4 @@ export default class Thing extends CacheableObject {
 
     return `${thing.constructor[Thing.referenceType]}:${thing.directory}`;
   }
-
-  static composite = composite;
 }
diff --git a/src/data/things/track.js b/src/data/things/track.js
index 7d7e8a68..c5e6ff34 100644
--- a/src/data/things/track.js
+++ b/src/data/things/track.js
@@ -4,6 +4,19 @@ import {color} from '#cli';
 import find from '#find';
 import {empty} from '#sugar';
 
+import {
+  from as compositeFrom,
+  earlyExitWithoutDependency,
+  exposeConstant,
+  exposeDependency,
+  exposeDependencyOrContinue,
+  exposeUpdateValueOrContinue,
+  withResolvedContribs,
+  withResolvedReference,
+  withResultOfAvailabilityCheck,
+  withReverseReferenceList,
+} from '#composite';
+
 import Thing from './thing.js';
 
 export class Track extends Thing {
@@ -43,9 +56,9 @@ export class Track extends Thing {
     sampledTracksByRef: Thing.common.referenceList(Track),
     artTagsByRef: Thing.common.referenceList(ArtTag),
 
-    color: Thing.composite.from(`Track.color`, [
-      Thing.composite.exposeUpdateValueOrContinue(),
-      Track.composite.withContainingTrackSection({earlyExitIfNotFound: false}),
+    color: compositeFrom(`Track.color`, [
+      exposeUpdateValueOrContinue(),
+      withContainingTrackSection({earlyExitIfNotFound: false}),
 
       {
         dependencies: ['#trackSection'],
@@ -59,8 +72,8 @@ export class Track extends Thing {
             : continuation()),
       },
 
-      Track.composite.withAlbumProperty('color'),
-      Thing.composite.exposeDependency('#album.color', {
+      withAlbumProperty('color'),
+      exposeDependency('#album.color', {
         update: {validate: isColor},
       }),
     ]),
@@ -75,21 +88,21 @@ export class Track extends Thing {
     // track's unique cover artwork, if any, and does not inherit the extension
     // of the album's main artwork. It does inherit trackCoverArtFileExtension,
     // if present on the album.
-    coverArtFileExtension: Thing.composite.from(`Track.coverArtFileExtension`, [
+    coverArtFileExtension: compositeFrom(`Track.coverArtFileExtension`, [
       // No cover art file extension if the track doesn't have unique artwork
       // in the first place.
-      Track.composite.withHasUniqueCoverArt(),
-      Thing.composite.earlyExitWithoutDependency('#hasUniqueCoverArt', {mode: 'falsy'}),
+      withHasUniqueCoverArt(),
+      earlyExitWithoutDependency('#hasUniqueCoverArt', {mode: 'falsy'}),
 
       // Expose custom coverArtFileExtension update value first.
-      Thing.composite.exposeUpdateValueOrContinue(),
+      exposeUpdateValueOrContinue(),
 
       // Expose album's trackCoverArtFileExtension if no update value set.
-      Track.composite.withAlbumProperty('trackCoverArtFileExtension'),
-      Thing.composite.exposeDependencyOrContinue('#album.trackCoverArtFileExtension'),
+      withAlbumProperty('trackCoverArtFileExtension'),
+      exposeDependencyOrContinue('#album.trackCoverArtFileExtension'),
 
       // Fallback to 'jpg'.
-      Thing.composite.exposeConstant('jpg', {
+      exposeConstant('jpg', {
         update: {validate: isFileExtension},
       }),
     ]),
@@ -98,14 +111,14 @@ export class Track extends Thing {
     // only the track's own unique cover artwork, if any. This exposes only as
     // the track's own coverArtDate or its album's trackArtDate, so if neither
     // is specified, this value is null.
-    coverArtDate: Thing.composite.from(`Track.coverArtDate`, [
-      Track.composite.withHasUniqueCoverArt(),
-      Thing.composite.earlyExitWithoutDependency('#hasUniqueCoverArt', {mode: 'falsy'}),
+    coverArtDate: compositeFrom(`Track.coverArtDate`, [
+      withHasUniqueCoverArt(),
+      earlyExitWithoutDependency('#hasUniqueCoverArt', {mode: 'falsy'}),
 
-      Thing.composite.exposeUpdateValueOrContinue(),
+      exposeUpdateValueOrContinue(),
 
-      Track.composite.withAlbumProperty('trackArtDate'),
-      Thing.composite.exposeDependency('#album.trackArtDate', {
+      withAlbumProperty('trackArtDate'),
+      exposeDependency('#album.trackArtDate', {
         update: {validate: isDate},
       }),
     ]),
@@ -132,9 +145,9 @@ export class Track extends Thing {
 
     commentatorArtists: Thing.common.commentatorArtists(),
 
-    album: Thing.composite.from(`Track.album`, [
-      Track.composite.withAlbum(),
-      Thing.composite.exposeDependency('#album'),
+    album: compositeFrom(`Track.album`, [
+      withAlbum(),
+      exposeDependency('#album'),
     ]),
 
     // Note - this is an internal property used only to help identify a track.
@@ -150,10 +163,10 @@ export class Track extends Thing {
       find: find.album,
     }),
 
-    date: Thing.composite.from(`Track.date`, [
-      Thing.composite.exposeDependencyOrContinue('dateFirstReleased'),
-      Track.composite.withAlbumProperty('date'),
-      Thing.composite.exposeDependency('#album.date'),
+    date: compositeFrom(`Track.date`, [
+      exposeDependencyOrContinue('dateFirstReleased'),
+      withAlbumProperty('date'),
+      exposeDependency('#album.date'),
     ]),
 
     // Whether or not the track has "unique" cover artwork - a cover which is
@@ -163,19 +176,19 @@ export class Track extends Thing {
     // or a placeholder. (This property is named hasUniqueCoverArt instead of
     // the usual hasCoverArt to emphasize that it does not inherit from the
     // album.)
-    hasUniqueCoverArt: Thing.composite.from(`Track.hasUniqueCoverArt`, [
-      Track.composite.withHasUniqueCoverArt(),
-      Thing.composite.exposeDependency('#hasUniqueCoverArt'),
+    hasUniqueCoverArt: compositeFrom(`Track.hasUniqueCoverArt`, [
+      withHasUniqueCoverArt(),
+      exposeDependency('#hasUniqueCoverArt'),
     ]),
 
-    originalReleaseTrack: Thing.composite.from(`Track.originalReleaseTrack`, [
-      Track.composite.withOriginalRelease(),
-      Thing.composite.exposeDependency('#originalRelease'),
+    originalReleaseTrack: compositeFrom(`Track.originalReleaseTrack`, [
+      withOriginalRelease(),
+      exposeDependency('#originalRelease'),
     ]),
 
-    otherReleases: Thing.composite.from(`Track.otherReleases`, [
-      Thing.composite.earlyExitWithoutDependency('trackData', {mode: 'empty'}),
-      Track.composite.withOriginalRelease({selfIfOriginal: true}),
+    otherReleases: compositeFrom(`Track.otherReleases`, [
+      earlyExitWithoutDependency('trackData', {mode: 'empty'}),
+      withOriginalRelease({selfIfOriginal: true}),
 
       {
         flags: {expose: true},
@@ -197,10 +210,10 @@ export class Track extends Thing {
       },
     ]),
 
-    artistContribs: Thing.composite.from(`Track.artistContribs`, [
-      Track.composite.inheritFromOriginalRelease({property: 'artistContribs'}),
+    artistContribs: compositeFrom(`Track.artistContribs`, [
+      inheritFromOriginalRelease({property: 'artistContribs'}),
 
-      Thing.composite.withResolvedContribs({
+      withResolvedContribs({
         from: 'artistContribsByRef',
         to: '#artistContribs',
       }),
@@ -213,19 +226,19 @@ export class Track extends Thing {
             : contribsFromTrack),
       },
 
-      Track.composite.withAlbumProperty('artistContribs'),
-      Thing.composite.exposeDependency('#album.artistContribs'),
+      withAlbumProperty('artistContribs'),
+      exposeDependency('#album.artistContribs'),
     ]),
 
-    contributorContribs: Thing.composite.from(`Track.contributorContribs`, [
-      Track.composite.inheritFromOriginalRelease({property: 'contributorContribs'}),
+    contributorContribs: compositeFrom(`Track.contributorContribs`, [
+      inheritFromOriginalRelease({property: 'contributorContribs'}),
       Thing.common.dynamicContribs('contributorContribsByRef'),
     ]),
 
     // Cover artists aren't inherited from the original release, since it
     // typically varies by release and isn't defined by the musical qualities
     // of the track.
-    coverArtistContribs: Thing.composite.from(`Track.coverArtistContribs`, [
+    coverArtistContribs: compositeFrom(`Track.coverArtistContribs`, [
       {
         dependencies: ['disableUniqueCoverArt'],
         compute: ({disableUniqueCoverArt}, continuation) =>
@@ -234,7 +247,7 @@ export class Track extends Thing {
             : continuation()),
       },
 
-      Thing.composite.withResolvedContribs({
+      withResolvedContribs({
         from: 'coverArtistContribsByRef',
         to: '#coverArtistContribs',
       }),
@@ -247,12 +260,12 @@ export class Track extends Thing {
             : contribsFromTrack),
       },
 
-      Track.composite.withAlbumProperty('trackCoverArtistContribs'),
-      Thing.composite.exposeDependency('#album.trackCoverArtistContribs'),
+      withAlbumProperty('trackCoverArtistContribs'),
+      exposeDependency('#album.trackCoverArtistContribs'),
     ]),
 
-    referencedTracks: Thing.composite.from(`Track.referencedTracks`, [
-      Track.composite.inheritFromOriginalRelease({property: 'referencedTracks'}),
+    referencedTracks: compositeFrom(`Track.referencedTracks`, [
+      inheritFromOriginalRelease({property: 'referencedTracks'}),
       Thing.common.resolvedReferenceList({
         list: 'referencedTracksByRef',
         data: 'trackData',
@@ -260,8 +273,8 @@ export class Track extends Thing {
       }),
     ]),
 
-    sampledTracks: Thing.composite.from(`Track.sampledTracks`, [
-      Track.composite.inheritFromOriginalRelease({property: 'sampledTracks'}),
+    sampledTracks: compositeFrom(`Track.sampledTracks`, [
+      inheritFromOriginalRelease({property: 'sampledTracks'}),
       Thing.common.resolvedReferenceList({
         list: 'sampledTracksByRef',
         data: 'trackData',
@@ -283,10 +296,10 @@ export class Track extends Thing {
     // counting the number of times a track has been referenced, for use in
     // the "Tracks - by Times Referenced" listing page (or other data
     // processing).
-    referencedByTracks: Track.composite.trackReverseReferenceList('referencedTracks'),
+    referencedByTracks: trackReverseReferenceList('referencedTracks'),
 
     // For the same reasoning, exclude re-releases from sampled tracks too.
-    sampledByTracks: Track.composite.trackReverseReferenceList('sampledTracks'),
+    sampledByTracks: trackReverseReferenceList('sampledTracks'),
 
     featuredInFlashes: Thing.common.reverseReferenceList({
       data: 'flashData',
@@ -294,309 +307,310 @@ export class Track extends Thing {
     }),
   });
 
-  static composite = {
-    // Early exits with a value inherited from the original release, if
-    // this track is a rerelease, and otherwise continues with no further
-    // dependencies provided. If allowOverride is true, then the continuation
-    // will also be called if the original release exposed the requested
-    // property as null.
-    inheritFromOriginalRelease({
-      property: originalProperty,
-      allowOverride = false,
-    }) {
-      return Thing.composite.from(`Track.composite.inheritFromOriginalRelease`, [
-        Track.composite.withOriginalRelease(),
-
-        {
-          dependencies: ['#originalRelease'],
-          compute({'#originalRelease': originalRelease}, continuation) {
-            if (!originalRelease) return continuation.raise();
-
-            const value = originalRelease[originalProperty];
-            if (allowOverride && value === null) return continuation.raise();
-
-            return continuation.exit(value);
-          },
-        },
-      ]);
-    },
+  [inspect.custom](depth) {
+    const parts = [];
 
-    // Gets the track's album. Unless earlyExitIfNotFound is overridden false,
-    // this will early exit with null in two cases - albumData being missing,
-    // or not including an album whose .tracks array includes this track.
-    withAlbum({to = '#album', earlyExitIfNotFound = true} = {}) {
-      return Thing.composite.from(`Track.composite.withAlbum`, [
-        Thing.composite.withResultOfAvailabilityCheck({
-          fromDependency: 'albumData',
-          mode: 'empty',
-          to: '#albumDataAvailability',
-        }),
+    parts.push(Thing.prototype[inspect.custom].apply(this));
 
-        {
-          dependencies: ['#albumDataAvailability'],
-          options: {earlyExitIfNotFound},
-          mapContinuation: {to},
+    if (this.originalReleaseTrackByRef) {
+      parts.unshift(`${color.yellow('[rerelease]')} `);
+    }
 
-          compute: ({
-            '#albumDataAvailability': albumDataAvailability,
-            '#options': {earlyExitIfNotFound},
-          }, continuation) =>
-            (albumDataAvailability
-              ? continuation()
-              : (earlyExitIfNotFound
-                  ? continuation.exit(null)
-                  : continuation.raise({to: null}))),
-        },
+    let album;
+    if (depth >= 0 && (album = this.album ?? this.dataSourceAlbum)) {
+      const albumName = album.name;
+      const albumIndex = album.tracks.indexOf(this);
+      const trackNum =
+        (albumIndex === -1
+          ? '#?'
+          : `#${albumIndex + 1}`);
+      parts.push(` (${color.yellow(trackNum)} in ${color.green(albumName)})`);
+    }
 
-        {
-          dependencies: ['this', 'albumData'],
-          compute: ({this: track, albumData}, continuation) =>
-            continuation({
-              '#album':
-                albumData.find(album => album.tracks.includes(track)),
-            }),
-        },
+    return parts.join('');
+  }
+}
 
-        {
-          dependencies: ['#album'],
-          options: {earlyExitIfNotFound},
-          mapContinuation: {to},
-          compute: ({
-            '#album': album,
-            '#options': {earlyExitIfNotFound},
-          }, continuation) =>
-            (album
-              ? continuation.raise({to: album})
-              : (earlyExitIfNotFound
-                  ? continuation.exit(null)
-                  : continuation.raise({to: album}))),
-        },
-      ]);
+// Early exits with a value inherited from the original release, if
+// this track is a rerelease, and otherwise continues with no further
+// dependencies provided. If allowOverride is true, then the continuation
+// will also be called if the original release exposed the requested
+// property as null.
+function inheritFromOriginalRelease({
+  property: originalProperty,
+  allowOverride = false,
+}) {
+  return compositeFrom(`inheritFromOriginalRelease`, [
+    withOriginalRelease(),
+
+    {
+      dependencies: ['#originalRelease'],
+      compute({'#originalRelease': originalRelease}, continuation) {
+        if (!originalRelease) return continuation.raise();
+
+        const value = originalRelease[originalProperty];
+        if (allowOverride && value === null) return continuation.raise();
+
+        return continuation.exit(value);
+      },
     },
+  ]);
+}
 
-    // Gets a single property from this track's album, providing it as the same
-    // property name prefixed with '#album.' (by default). If the track's album
-    // isn't available, and earlyExitIfNotFound hasn't been set, the property
-    // will be provided as null.
-    withAlbumProperty(property, {
-      to = '#album.' + property,
-      earlyExitIfNotFound = false,
-    } = {}) {
-      return Thing.composite.from(`Track.composite.withAlbumProperty`, [
-        Track.composite.withAlbum({earlyExitIfNotFound}),
-
-        {
-          dependencies: ['#album'],
-          options: {property},
-          mapContinuation: {to},
+// Gets the track's album. Unless earlyExitIfNotFound is overridden false,
+// this will early exit with null in two cases - albumData being missing,
+// or not including an album whose .tracks array includes this track.
+function withAlbum({
+  to = '#album',
+  earlyExitIfNotFound = true,
+} = {}) {
+  return compositeFrom(`withAlbum`, [
+    withResultOfAvailabilityCheck({
+      fromDependency: 'albumData',
+      mode: 'empty',
+      to: '#albumDataAvailability',
+    }),
 
-          compute: ({
-            '#album': album,
-            '#options': {property},
-          }, continuation) =>
-            (album
-              ? continuation.raise({to: album[property]})
-              : continuation.raise({to: null})),
-        },
-      ]);
+    {
+      dependencies: ['#albumDataAvailability'],
+      options: {earlyExitIfNotFound},
+      mapContinuation: {to},
+
+      compute: ({
+        '#albumDataAvailability': albumDataAvailability,
+        '#options': {earlyExitIfNotFound},
+      }, continuation) =>
+        (albumDataAvailability
+          ? continuation()
+          : (earlyExitIfNotFound
+              ? continuation.exit(null)
+              : continuation.raise({to: null}))),
     },
 
-    // Gets the listed properties from this track's album, providing them as
-    // dependencies (by default) with '#album.' prefixed before each property
-    // name. If the track's album isn't available, and earlyExitIfNotFound
-    // hasn't been set, the same dependency names will be provided as null.
-    withAlbumProperties({
-      properties,
-      prefix = '#album',
-      earlyExitIfNotFound = false,
-    }) {
-      return Thing.composite.from(`Track.composite.withAlbumProperties`, [
-        Track.composite.withAlbum({earlyExitIfNotFound}),
-
-        {
-          dependencies: ['#album'],
-          options: {properties, prefix},
-
-          compute({
-            '#album': album,
-            '#options': {properties, prefix},
-          }, continuation) {
-            const raise = {};
-
-            if (album) {
-              for (const property of properties) {
-                raise[prefix + '.' + property] = album[property];
-              }
-            } else {
-              for (const property of properties) {
-                raise[prefix + '.' + property] = null;
-              }
-            }
-
-            return continuation.raise(raise);
-          },
-        },
-      ]);
+    {
+      dependencies: ['this', 'albumData'],
+      compute: ({this: track, albumData}, continuation) =>
+        continuation({
+          '#album':
+            albumData.find(album => album.tracks.includes(track)),
+        }),
     },
 
-    // Gets the track section containing this track from its album's track list.
-    // Unless earlyExitIfNotFound is overridden false, this will early exit if
-    // the album can't be found or if none of its trackSections includes the
-    // track for some reason.
-    withContainingTrackSection({
-      to = '#trackSection',
-      earlyExitIfNotFound = true,
-    } = {}) {
-      return Thing.composite.from(`Track.composite.withContainingTrackSection`, [
-        Track.composite.withAlbumProperty('trackSections', {earlyExitIfNotFound}),
-
-        {
-          dependencies: ['this', '#album.trackSections'],
-          mapContinuation: {to},
-
-          compute({
-            this: track,
-            '#album.trackSections': trackSections,
-          }, continuation) {
-            if (!trackSections) {
-              return continuation.raise({to: null});
-            }
-
-            const trackSection =
-              trackSections.find(({tracks}) => tracks.includes(track));
-
-            if (trackSection) {
-              return continuation.raise({to: trackSection});
-            } else if (earlyExitIfNotFound) {
-              return continuation.exit(null);
-            } else {
-              return continuation.raise({to: null});
-            }
-          },
-        },
-      ]);
+    {
+      dependencies: ['#album'],
+      options: {earlyExitIfNotFound},
+      mapContinuation: {to},
+      compute: ({
+        '#album': album,
+        '#options': {earlyExitIfNotFound},
+      }, continuation) =>
+        (album
+          ? continuation.raise({to: album})
+          : (earlyExitIfNotFound
+              ? continuation.exit(null)
+              : continuation.raise({to: album}))),
     },
+  ]);
+}
 
-    // Just includes the original release of this track as a dependency.
-    // If this track isn't a rerelease, then it'll provide null, unless the
-    // {selfIfOriginal} option is set, in which case it'll provide this track
-    // itself. Note that this will early exit if the original release is
-    // specified by reference and that reference doesn't resolve to anything.
-    // Outputs to '#originalRelease' by default.
-    withOriginalRelease({
-      to = '#originalRelease',
-      selfIfOriginal = false,
-    } = {}) {
-      return Thing.composite.from(`Track.composite.withOriginalRelease`, [
-        Thing.composite.withResolvedReference({
-          ref: 'originalReleaseTrackByRef',
-          data: 'trackData',
-          to: '#originalRelease',
-          find: find.track,
-          earlyExitIfNotFound: true,
-        }),
-
-        {
-          dependencies: ['this', '#originalRelease'],
-          options: {selfIfOriginal},
-          mapContinuation: {to},
-          compute: ({
-            this: track,
-            '#originalRelease': originalRelease,
-            '#options': {selfIfOriginal},
-          }, continuation) =>
-            continuation.raise({
-              to:
-                (originalRelease ??
-                  (selfIfOriginal
-                    ? track
-                    : null)),
-            }),
-        },
-      ]);
+// Gets a single property from this track's album, providing it as the same
+// property name prefixed with '#album.' (by default). If the track's album
+// isn't available, and earlyExitIfNotFound hasn't been set, the property
+// will be provided as null.
+function withAlbumProperty(property, {
+  to = '#album.' + property,
+  earlyExitIfNotFound = false,
+} = {}) {
+  return compositeFrom(`withAlbumProperty`, [
+    withAlbum({earlyExitIfNotFound}),
+
+    {
+      dependencies: ['#album'],
+      options: {property},
+      mapContinuation: {to},
+
+      compute: ({
+        '#album': album,
+        '#options': {property},
+      }, continuation) =>
+        (album
+          ? continuation.raise({to: album[property]})
+          : continuation.raise({to: null})),
     },
+  ]);
+}
 
-    // The algorithm for checking if a track has unique cover art is used in a
-    // couple places, so it's defined in full as a compositional step.
-    withHasUniqueCoverArt({
-      to = '#hasUniqueCoverArt',
-    } = {}) {
-      return Thing.composite.from(`Track.composite.withHasUniqueCoverArt`, [
-        {
-          dependencies: ['disableUniqueCoverArt'],
-          mapContinuation: {to},
-          compute: ({disableUniqueCoverArt}, continuation) =>
-            (disableUniqueCoverArt
-              ? continuation.raise({to: false})
-              : continuation()),
-        },
+// Gets the listed properties from this track's album, providing them as
+// dependencies (by default) with '#album.' prefixed before each property
+// name. If the track's album isn't available, and earlyExitIfNotFound
+// hasn't been set, the same dependency names will be provided as null.
+function withAlbumProperties({
+  properties,
+  prefix = '#album',
+  earlyExitIfNotFound = false,
+}) {
+  return compositeFrom(`withAlbumProperties`, [
+    withAlbum({earlyExitIfNotFound}),
+
+    {
+      dependencies: ['#album'],
+      options: {properties, prefix},
+
+      compute({
+        '#album': album,
+        '#options': {properties, prefix},
+      }, continuation) {
+        const raise = {};
+
+        if (album) {
+          for (const property of properties) {
+            raise[prefix + '.' + property] = album[property];
+          }
+        } else {
+          for (const property of properties) {
+            raise[prefix + '.' + property] = null;
+          }
+        }
+
+        return continuation.raise(raise);
+      },
+    },
+  ]);
+}
 
-        Thing.composite.withResolvedContribs({
-          from: 'coverArtistContribsByRef',
-          to: '#coverArtistContribs',
-        }),
+// Gets the track section containing this track from its album's track list.
+// Unless earlyExitIfNotFound is overridden false, this will early exit if
+// the album can't be found or if none of its trackSections includes the
+// track for some reason.
+function withContainingTrackSection({
+  to = '#trackSection',
+  earlyExitIfNotFound = true,
+} = {}) {
+  return compositeFrom(`withContainingTrackSection`, [
+    withAlbumProperty('trackSections', {earlyExitIfNotFound}),
+
+    {
+      dependencies: ['this', '#album.trackSections'],
+      mapContinuation: {to},
+
+      compute({
+        this: track,
+        '#album.trackSections': trackSections,
+      }, continuation) {
+        if (!trackSections) {
+          return continuation.raise({to: null});
+        }
+
+        const trackSection =
+          trackSections.find(({tracks}) => tracks.includes(track));
+
+        if (trackSection) {
+          return continuation.raise({to: trackSection});
+        } else if (earlyExitIfNotFound) {
+          return continuation.exit(null);
+        } else {
+          return continuation.raise({to: null});
+        }
+      },
+    },
+  ]);
+}
 
-        {
-          dependencies: ['#coverArtistContribs'],
-          mapContinuation: {to},
-          compute: ({'#coverArtistContribs': contribsFromTrack}, continuation) =>
-            (empty(contribsFromTrack)
-              ? continuation()
-              : continuation.raise({to: true})),
-        },
+// Just includes the original release of this track as a dependency.
+// If this track isn't a rerelease, then it'll provide null, unless the
+// {selfIfOriginal} option is set, in which case it'll provide this track
+// itself. Note that this will early exit if the original release is
+// specified by reference and that reference doesn't resolve to anything.
+// Outputs to '#originalRelease' by default.
+function withOriginalRelease({
+  to = '#originalRelease',
+  selfIfOriginal = false,
+} = {}) {
+  return compositeFrom(`withOriginalRelease`, [
+    withResolvedReference({
+      ref: 'originalReleaseTrackByRef',
+      data: 'trackData',
+      to: '#originalRelease',
+      find: find.track,
+      earlyExitIfNotFound: true,
+    }),
 
-        Track.composite.withAlbumProperty('trackCoverArtistContribs'),
+    {
+      dependencies: ['this', '#originalRelease'],
+      options: {selfIfOriginal},
+      mapContinuation: {to},
+      compute: ({
+        this: track,
+        '#originalRelease': originalRelease,
+        '#options': {selfIfOriginal},
+      }, continuation) =>
+        continuation.raise({
+          to:
+            (originalRelease ??
+              (selfIfOriginal
+                ? track
+                : null)),
+        }),
+    },
+  ]);
+}
 
-        {
-          dependencies: ['#album.trackCoverArtistContribs'],
-          mapContinuation: {to},
-          compute: ({'#album.trackCoverArtistContribs': contribsFromAlbum}, continuation) =>
-            (empty(contribsFromAlbum)
-              ? continuation.raise({to: false})
-              : continuation.raise({to: true})),
-        },
-      ]);
+// The algorithm for checking if a track has unique cover art is used in a
+// couple places, so it's defined in full as a compositional step.
+function withHasUniqueCoverArt({
+  to = '#hasUniqueCoverArt',
+} = {}) {
+  return compositeFrom(`withHasUniqueCoverArt`, [
+    {
+      dependencies: ['disableUniqueCoverArt'],
+      mapContinuation: {to},
+      compute: ({disableUniqueCoverArt}, continuation) =>
+        (disableUniqueCoverArt
+          ? continuation.raise({to: false})
+          : continuation()),
     },
 
-    trackReverseReferenceList(refListProperty) {
-      return Thing.composite.from(`Track.composite.trackReverseReferenceList`, [
-        Thing.composite.withReverseReferenceList({
-          data: 'trackData',
-          list: refListProperty,
-        }),
+    withResolvedContribs({
+      from: 'coverArtistContribsByRef',
+      to: '#coverArtistContribs',
+    }),
 
-        {
-          flags: {expose: true},
-          expose: {
-            dependencies: ['#reverseReferenceList'],
-            compute: ({'#reverseReferenceList': reverseReferenceList}) =>
-              reverseReferenceList.filter(track => !track.originalReleaseTrack),
-          },
-        },
-      ]);
+    {
+      dependencies: ['#coverArtistContribs'],
+      mapContinuation: {to},
+      compute: ({'#coverArtistContribs': contribsFromTrack}, continuation) =>
+        (empty(contribsFromTrack)
+          ? continuation()
+          : continuation.raise({to: true})),
     },
-  };
-
-  [inspect.custom](depth) {
-    const parts = [];
 
-    parts.push(Thing.prototype[inspect.custom].apply(this));
+    withAlbumProperty('trackCoverArtistContribs'),
 
-    if (this.originalReleaseTrackByRef) {
-      parts.unshift(`${color.yellow('[rerelease]')} `);
-    }
+    {
+      dependencies: ['#album.trackCoverArtistContribs'],
+      mapContinuation: {to},
+      compute: ({'#album.trackCoverArtistContribs': contribsFromAlbum}, continuation) =>
+        (empty(contribsFromAlbum)
+          ? continuation.raise({to: false})
+          : continuation.raise({to: true})),
+    },
+  ]);
+}
 
-    let album;
-    if (depth >= 0 && (album = this.album ?? this.dataSourceAlbum)) {
-      const albumName = album.name;
-      const albumIndex = album.tracks.indexOf(this);
-      const trackNum =
-        (albumIndex === -1
-          ? '#?'
-          : `#${albumIndex + 1}`);
-      parts.push(` (${color.yellow(trackNum)} in ${color.green(albumName)})`);
-    }
+function trackReverseReferenceList(refListProperty) {
+  return compositeFrom(`trackReverseReferenceList`, [
+    withReverseReferenceList({
+      data: 'trackData',
+      list: refListProperty,
+    }),
 
-    return parts.join('');
-  }
+    {
+      flags: {expose: true},
+      expose: {
+        dependencies: ['#reverseReferenceList'],
+        compute: ({'#reverseReferenceList': reverseReferenceList}) =>
+          reverseReferenceList.filter(track => !track.originalReleaseTrack),
+      },
+    },
+  ]);
 }