From ba1cf3fe611661c85ef4a7150c924f99e1e94ba3 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 24 Aug 2023 21:10:46 -0300 Subject: data: bug fixes & Thing.composite.from.debug mode --- src/data/things/thing.js | 272 +++++++++++++++++++++++++++++++++++++++-------- src/data/things/track.js | 3 +- 2 files changed, 229 insertions(+), 46 deletions(-) diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 1bca6c38..555e443d 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -508,7 +508,9 @@ export default class Thing extends CacheableObject { // } // // Performing an early exit is as simple as returning some other value, - // instead of the continuation. + // instead of the continuation. You may also use `continuation.exit(value)` + // to perform the exact same kind of early exit - it's just a different + // syntax that might fit in better in certain longer compositions. // // It may be fine to simply provide new dependencies under a hard-coded // name, such as '#excitingProperty' above, but if you're writing a utility @@ -715,6 +717,17 @@ export default class Thing extends CacheableObject { // still dynamically provided dependencies!) // from(firstArg, secondArg) { + const debug = fn => { + if (Thing.composite.from.debug === true) { + const result = fn(); + if (Array.isArray(result)) { + console.log(`[composite]`, ...result); + } else { + console.log(`[composite]`, result); + } + } + }; + let annotation, composition; if (typeof firstArg === 'string') { [annotation, composition] = [firstArg, secondArg]; @@ -738,6 +751,13 @@ export default class Thing extends CacheableObject { const exposeSteps = []; const exposeDependencies = new Set(base.expose?.dependencies); + if (base.expose?.mapDependencies) { + for (const dependency of Object.values(base.expose.mapDependencies)) { + if (typeof dependency === 'string' && dependency.startsWith('#')) continue; + exposeDependencies.add(dependency); + } + } + for (let i = 0; i < steps.length; i++) { const step = steps[i]; const message = @@ -767,6 +787,13 @@ export default class Thing extends CacheableObject { } } + if (step.expose.mapDependencies) { + for (const dependency of Object.values(step.expose.mapDependencies)) { + if (typeof dependency === 'string' && dependency.startsWith('#')) continue; + exposeDependencies.add(dependency); + } + } + let fn, type; if (base.flags.update) { if (step.expose.transform) { @@ -813,8 +840,8 @@ export default class Thing extends CacheableObject { const expose = constructedDescriptor.expose = {}; expose.dependencies = Array.from(exposeDependencies); - const continuationSymbol = Symbol(); - const noTransformSymbol = Symbol(); + const continuationSymbol = Symbol('continuation symbol'); + const noTransformSymbol = Symbol('no-transform symbol'); function _filterDependencies(dependencies, step) { const filteredDependencies = @@ -849,48 +876,153 @@ export default class Thing extends CacheableObject { return assignDependencies; } + function _prepareContinuation(transform, step) { + const continuationStorage = { + returnedWith: null, + providedDependencies: null, + providedValue: null, + }; + + const continuation = + (transform + ? (providedValue, providedDependencies = null) => { + continuationStorage.returnedWith = 'continuation'; + continuationStorage.providedDependencies = providedDependencies; + continuationStorage.providedValue = providedValue; + return continuationSymbol; + } + : (providedDependencies = null) => { + continuationStorage.returnedWith = 'continuation'; + continuationStorage.providedDependencies = providedDependencies; + return continuationSymbol; + }); + + continuation.exit = (providedValue) => { + continuationStorage.returnedWith = 'exit'; + continuationStorage.providedValue = providedValue; + return continuationSymbol; + }; + + if (base.flags.compose) { + continuation.raise = + (transform + ? (providedValue, providedDependencies = null) => { + continuationStorage.returnedWith = 'raise'; + continuationStorage.providedDependencies = providedDependencies; + continuationStorage.providedValue = providedValue; + return continuationSymbol; + } + : (providedDependencies = null) => { + continuationStorage.returnedWith = 'raise'; + continuationStorage.providedDependencies = providedDependencies; + return continuationSymbol; + }); + } + + return {continuation, continuationStorage}; + } + function _computeOrTransform(value, initialDependencies) { const dependencies = {...initialDependencies}; - let valueSoFar = value; + let valueSoFar = value; // Set only for {update: true} compositions + let exportDependencies = null; // Set only for {compose: true} compositions + + debug(() => color.bright(`begin composition (annotation: ${annotation})`)); + + for (let i = 0; i < exposeSteps.length; i++) { + const step = exposeSteps[i]; + debug(() => [`step #${i+1}:`, step]); + + const transform = + valueSoFar !== noTransformSymbol && + step.transform; - for (const step of exposeSteps) { const filteredDependencies = _filterDependencies(dependencies, step); + const {continuation, continuationStorage} = _prepareContinuation(transform, step); - let assignDependencies = null; + if (transform) { + debug(() => `step #${i+1} - transform with dependencies: ${inspect(filteredDependencies, {depth: 0})}`); + } else { + debug(() => `step #${i+1} - compute with dependencies: ${inspect(filteredDependencies, {depth: 0})}`); + } const result = - (valueSoFar !== noTransformSymbol && step.transform - ? step.transform( - valueSoFar, filteredDependencies, - (updatedValue, providedDependencies) => { - valueSoFar = updatedValue ?? null; - assignDependencies = providedDependencies; - return continuationSymbol; - }) - : step.compute( - filteredDependencies, - (providedDependencies) => { - assignDependencies = providedDependencies; - return continuationSymbol; - })); + (transform + ? step.transform(valueSoFar, filteredDependencies, continuation) + : step.compute(filteredDependencies, continuation)); if (result !== continuationSymbol) { + if (base.flags.compose) { + throw new TypeError(`Use continuation.exit() or continuation.raise() in {compose: true} compositions`); + } + + debug(() => `step #${i+1} - early-exit (inferred)`); + debug(() => `early-exit: ${inspect(result, {compact: true})}`); + debug(() => color.bright(`end composition (annotation: ${annotation})`)); + return result; } - Object.assign(dependencies, _assignDependencies(assignDependencies, step)); + if (continuationStorage.returnedWith === 'exit') { + debug(() => `step #${i+1} - result: early-exit (explicit)`); + debug(() => `early-exit: ${inspect(continuationStorage.providedValue, {compact: true})}`); + debug(() => color.bright(`end composition (annotation: ${annotation})`)); + + return continuationSymbol.providedValue; + } + + if (continuationStorage.returnedWith === 'raise') { + if (transform) { + valueSoFar = continuationStorage.providedValue; + } + + exportDependencies = _assignDependencies(continuationStorage.providedDependencies, step); + + debug(() => `step #${i+1} - result: raise`); + + break; + } + + if (continuationStorage.returnedWith === 'continuation') { + if (transform) { + valueSoFar = continuationStorage.providedValue; + } + + debug(() => `step #${i+1} - result: continuation`); + + if (continuationStorage.providedDependencies) { + const assignDependencies = _assignDependencies(continuationStorage.providedDependencies, step); + Object.assign(dependencies, assignDependencies); + + debug(() => [`assign dependencies:`, assignDependencies]); + } + } } + if (exportDependencies) { + debug(() => [`raise dependencies:`, exportDependencies]); + debug(() => color.bright(`end composition (annotation: ${annotation})`)); + return continuationIfApplicable(exportDependencies); + } + + debug(() => `completed all steps, reached base`); + const filteredDependencies = _filterDependencies(dependencies, base.expose); // Note: base.flags.compose is not compatible with base.flags.update. if (base.expose.transform) { - return base.expose.transform(valueSoFar, filteredDependencies); + debug(() => `base - transform with dependencies: ${inspect(filteredDependencies, {depth: 0})}`); + + const result = base.expose.transform(valueSoFar, filteredDependencies); + + debug(() => `base - non-compose (final) result: ${inspect(result, {compact: true})}`); + + return result; } else if (base.flags.compose) { - const continuation = continuationIfApplicable; + const {continuation, continuationStorage} = _prepareContinuation(transform, base.expose); - let exportDependencies; + debug(() => `base - compute with dependencies: ${inspect(filteredDependencies, {depth: 0})}`); const result = base.expose.compute(filteredDependencies, providedDependencies => { @@ -899,12 +1031,39 @@ export default class Thing extends CacheableObject { }); if (result !== continuationSymbol) { - return result; + throw new TypeError(`Use continuation.exit() or continuation.raise() in {compose: true} composition`); + } + + if (continuationStorage.returnedWith === 'continuation') { + throw new TypeError(`Use continuation.raise() in base of {compose: true} composition`); + } + + if (continuationStorage.returnedWith === 'exit') { + debug(() => `base - result: early-exit (explicit)`); + debug(() => `early-exit: ${inspect(continuationStorage.providedValue, {compact: true})}`); + debug(() => color.bright(`end composition (annotation: ${annotation})`)); + + return continuationStorage.providedValue; } - return continuation(_assignDependencies(exportDependencies, base.expose)); + if (continuationStorage.returnedWith === 'raise') { + exportDependencies = _assignDependencies(continuationStorage.providedDependencies, base.expose); + + debug(() => `base - result: raise`); + debug(() => `raise dependencies: ${inspect(exportDependencies, {compact: true})}`); + debug(() => color.bright(`end composition (annotation: ${annotation})`)); + + return continuationIfApplicable(exportDependencies); + } } else { - return base.expose.compute(filteredDependencies); + debug(() => `base - compute with dependencies: ${inspect(filteredDependencies, {depth: 0})}`); + + const result = base.expose.compute(filteredDependencies); + + debug(() => `base - non-compose (final) result: ${inspect(result, {compact: true})}`); + debug(() => color.bright(`end composition (annotation: ${annotation})`)); + + return result; } } @@ -953,7 +1112,7 @@ export default class Thing extends CacheableObject { : null); } - return continuation(exports); + return continuation.raise(exports); } }, }; @@ -985,34 +1144,57 @@ export default class Thing extends CacheableObject { // Otherwise, the data object is provided on the output dependency; // or null, if the reference doesn't match anything or itself was null // to begin with. - withResolvedReference({ + withResolvedReference: ({ ref, data, to, find: findFunction, earlyExitIfNotFound = false, - }) { - return { - annotation: `Thing.composite.withResolvedReference`, - flags: {expose: true, compose: true}, + }) => + Thing.composite.from(`Thing.composite.withResolvedReference`, [ + { + flags: {expose: true, compose: true}, + expose: { + mapDependencies: {ref}, + mapContinuation: {to}, + + compute: ({ref}, continuation) => + (ref + ? continuation() + : continuation.raise({to: null})), + }, + }, - expose: { - options: {findFunction, earlyExitIfNotFound}, - mapDependencies: {ref, data}, - mapContinuation: {to}, + { + flags: {expose: true, compose: true}, + expose: { + mapDependencies: {data}, + + compute: ({data}, continuation) => + (data === null + ? continuation.exit(null) + : continuation()), + }, + }, - compute({ref, data, findFunction, earlyExitIfNotFound}, continuation) { - if (!ref) return continuation({to: null}); + { + flags: {expose: true, compose: true}, + expose: { + options: {findFunction, earlyExitIfNotFound}, + mapDependencies: {ref, data}, + mapContinuation: {match: to}, - if (data === null) return null; + compute({ref, data, '#options': {findFunction, earlyExitIfNotFound}}, continuation) { + const match = findFunction(ref, data, {mode: 'quiet'}); - const match = findFunction(ref, data, {mode: 'quiet'}); - if (match === null && earlyExitIfNotFound) return null; + if (match === null && earlyExitIfNotFound) { + return continuation.exit(null); + } - return continuation({to: match}); + return continuation({match}); + }, }, }, - }; - } + ]), }; } diff --git a/src/data/things/track.js b/src/data/things/track.js index 2a3148ef..23b6da56 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -475,8 +475,9 @@ export class Track extends Thing { expose: { dependencies: ['this', 'albumData'], + options: {properties, prefix}, - compute({this: track, albumData}, continuation) { + compute({this: track, albumData, '#options': {properties, prefix}}, continuation) { const album = albumData?.find((album) => album.tracks.includes(track)); const newDependencies = {}; -- cgit 1.3.0-6-gf8a5