1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
|
import * as fr from './find-reverse.js';
import {sortByDate} from '#sort';
import {stitchArrays} from '#sugar';
function checkUnique(value) {
if (value.length === 0) {
return null;
} else if (value.length === 1) {
return value[0];
} else {
throw new Error(
`Requested unique referencing thing, ` +
`but ${value.length} reference this`);
}
}
function reverseHelper(spec) {
const cache = new WeakMap();
return (thing, data, {
unique = false,
} = {}) => {
// Check for an existing cache record which corresponds to this data.
// If it exists, query it for the requested thing, and return that;
// if it doesn't, create it and put it where it needs to be.
if (cache.has(data)) {
const value = cache.get(data).get(thing) ?? [];
if (unique) {
return checkUnique(value);
} else {
return value;
}
}
const cacheRecord = new WeakMap();
cache.set(data, cacheRecord);
// Get the referencing and referenced things. This is the meat of how
// one reverse spec is different from another. If the spec includes a
// 'tidy' step, use that to finalize the referencing things, the way
// they'll be recorded as results.
const interstitialReferencingThings =
(spec.bindTo === 'wikiData'
? spec.referencing(data)
: data.flatMap(thing => spec.referencing(thing)));
const referencedThings =
interstitialReferencingThings.map(thing => spec.referenced(thing));
const referencingThings =
(spec.tidy
? interstitialReferencingThings.map(thing => spec.tidy(thing))
: interstitialReferencingThings);
// Actually fill in the cache record. Since we're building up a *reverse*
// reference list, track connections in terms of the referenced thing.
// Also gather all referenced things into a set, for sorting purposes.
const allReferencedThings = new Set();
stitchArrays({
referencingThing: referencingThings,
referencedThings: referencedThings,
}).forEach(({referencingThing, referencedThings}) => {
for (const referencedThing of referencedThings) {
if (cacheRecord.has(referencedThing)) {
cacheRecord.get(referencedThing).push(referencingThing);
} else {
cacheRecord.set(referencedThing, [referencingThing]);
allReferencedThings.add(referencedThing);
}
}
});
// Sort the entries in the cache records, too, just by date. The rest of
// sorting should be handled externally - either preceding the reverse
// call (changing the data input) or following (sorting the output).
for (const referencedThing of allReferencedThings) {
if (cacheRecord.has(referencedThing)) {
const referencingThings = cacheRecord.get(referencedThing);
sortByDate(referencingThings);
}
}
// Then just pluck out the requested thing from the now-filled
// cache record!
const value = cacheRecord.get(thing) ?? [];
if (unique) {
return checkUnique(value);
} else {
return value;
}
};
}
const hardcodedReverseSpecs = {
// Artworks aren't Thing objects.
// This spec operates on albums and tracks alike!
artworksWhichReference: {
bindTo: 'wikiData',
referencing: ({albumData, trackData}) =>
[...albumData, ...trackData]
.flatMap(referencingThing =>
referencingThing.referencedArtworks
.map(({thing: referencedThing, ...referenceDetails}) => ({
referencingThing,
referencedThing,
referenceDetails,
}))),
referenced: ({referencedThing}) => [referencedThing],
tidy: ({referencingThing, referenceDetails}) =>
({thing: referencingThing, ...referenceDetails}),
},
};
const findReverseHelperConfig = {
word: `reverse`,
constructorKey: Symbol.for('Thing.reverseSpecs'),
hardcodedSpecs: hardcodedReverseSpecs,
postprocessSpec: postprocessReverseSpec,
};
export function postprocessReverseSpec(spec, {thingConstructor}) {
const newSpec = {...spec};
void thingConstructor;
return newSpec;
}
export function getAllReverseSpecs() {
return fr.getAllSpecs(findReverseHelperConfig);
}
export function findReverseSpec(key) {
return fr.findSpec(key, findReverseHelperConfig);
}
export default fr.tokenProxy({
findSpec: findReverseSpec,
prepareBehavior: reverseHelper,
});
export function bindReverse(wikiData, opts) {
return fr.bind(wikiData, opts, {
getAllSpecs: getAllReverseSpecs,
prepareBehavior: reverseHelper,
});
}
|