@uwdata/mosaic-core 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/mosaic-core.js +1895 -11407
- package/dist/mosaic-core.min.js +4 -13
- package/package.json +4 -4
- package/src/Catalog.js +37 -19
- package/src/Coordinator.js +72 -51
- package/src/DataTileIndexer.js +49 -50
- package/src/FilterGroup.js +3 -6
- package/src/MosaicClient.js +5 -2
- package/src/Param.js +67 -21
- package/src/QueryConsolidator.js +238 -0
- package/src/QueryManager.js +133 -0
- package/src/Selection.js +236 -33
- package/src/{clients → connectors}/rest.js +1 -1
- package/src/{clients → connectors}/socket.js +1 -1
- package/src/{clients → connectors}/wasm.js +1 -1
- package/src/index.js +7 -4
- package/src/util/AsyncDispatch.js +180 -0
- package/src/util/cache.js +58 -0
- package/src/util/priority-queue.js +85 -0
- package/src/util/query-result.js +8 -0
- package/src/util/synchronizer.js +47 -0
- package/src/QueryCache.js +0 -65
- package/src/util/skip-client.js +0 -3
- package/src/util/sql-from.js +0 -22
package/src/Selection.js
CHANGED
|
@@ -1,82 +1,285 @@
|
|
|
1
1
|
import { or } from '@uwdata/mosaic-sql';
|
|
2
2
|
import { Param } from './Param.js';
|
|
3
|
-
import { skipClient } from './util/skip-client.js';
|
|
4
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Test if a value is a Selection instance.
|
|
6
|
+
* @param {*} x The value to test.
|
|
7
|
+
* @returns {boolean} True if the input is a Selection, false otherwise.
|
|
8
|
+
*/
|
|
5
9
|
export function isSelection(x) {
|
|
6
10
|
return x instanceof Selection;
|
|
7
11
|
}
|
|
8
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Represents a dynamic set of query filter predicates.
|
|
15
|
+
*/
|
|
9
16
|
export class Selection extends Param {
|
|
10
17
|
|
|
11
|
-
|
|
12
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Create a new Selection instance with an
|
|
20
|
+
* intersect (conjunction) resolution strategy.
|
|
21
|
+
* @param {object} [options] The selection options.
|
|
22
|
+
* @param {boolean} [options.cross=false] Boolean flag indicating
|
|
23
|
+
* cross-filtered resolution. If true, selection clauses will not
|
|
24
|
+
* be applied to the clients they are associated with.
|
|
25
|
+
* @returns {Selection} The new Selection instance.
|
|
26
|
+
*/
|
|
27
|
+
static intersect({ cross = false } = {}) {
|
|
28
|
+
return new Selection(new SelectionResolver({ cross }));
|
|
13
29
|
}
|
|
14
30
|
|
|
15
|
-
|
|
16
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Create a new Selection instance with a
|
|
33
|
+
* union (disjunction) resolution strategy.
|
|
34
|
+
* @param {object} [options] The selection options.
|
|
35
|
+
* @param {boolean} [options.cross=false] Boolean flag indicating
|
|
36
|
+
* cross-filtered resolution. If true, selection clauses will not
|
|
37
|
+
* be applied to the clients they are associated with.
|
|
38
|
+
* @returns {Selection} The new Selection instance.
|
|
39
|
+
*/
|
|
40
|
+
static union({ cross = false } = {}) {
|
|
41
|
+
return new Selection(new SelectionResolver({ cross, union: true }));
|
|
17
42
|
}
|
|
18
43
|
|
|
19
|
-
|
|
20
|
-
|
|
44
|
+
/**
|
|
45
|
+
* Create a new Selection instance with a singular resolution strategy
|
|
46
|
+
* that keeps only the most recent selection clause.
|
|
47
|
+
* @param {object} [options] The selection options.
|
|
48
|
+
* @param {boolean} [options.cross=false] Boolean flag indicating
|
|
49
|
+
* cross-filtered resolution. If true, selection clauses will not
|
|
50
|
+
* be applied to the clients they are associated with.
|
|
51
|
+
* @returns {Selection} The new Selection instance.
|
|
52
|
+
*/
|
|
53
|
+
static single({ cross = false } = {}) {
|
|
54
|
+
return new Selection(new SelectionResolver({ cross, single: true }));
|
|
21
55
|
}
|
|
22
56
|
|
|
23
|
-
|
|
24
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Create a new Selection instance with a
|
|
59
|
+
* cross-filtered intersect resolution strategy.
|
|
60
|
+
* @returns {Selection} The new Selection instance.
|
|
61
|
+
*/
|
|
62
|
+
static crossfilter() {
|
|
63
|
+
return new Selection(new SelectionResolver({ cross: true }));
|
|
25
64
|
}
|
|
26
65
|
|
|
27
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Create a new Selection instance.
|
|
68
|
+
* @param {SelectionResolver} resolver The selection resolution
|
|
69
|
+
* strategy to apply.
|
|
70
|
+
*/
|
|
71
|
+
constructor(resolver = new SelectionResolver()) {
|
|
28
72
|
super([]);
|
|
29
|
-
this.
|
|
30
|
-
this.
|
|
31
|
-
this.cross = !!cross;
|
|
32
|
-
this.single = !!single;
|
|
73
|
+
this._resolved = this._value;
|
|
74
|
+
this._resolver = resolver;
|
|
33
75
|
}
|
|
34
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Create a cloned copy of this Selection instance.
|
|
79
|
+
* @returns {this} A clone of this selection.
|
|
80
|
+
*/
|
|
35
81
|
clone() {
|
|
36
|
-
const s = new Selection();
|
|
37
|
-
s.
|
|
38
|
-
s.union = this.union;
|
|
39
|
-
s.cross = this.cross;
|
|
40
|
-
s._value = this._value;
|
|
82
|
+
const s = new Selection(this._resolver);
|
|
83
|
+
s._value = s._resolved = this._value;
|
|
41
84
|
return s;
|
|
42
85
|
}
|
|
43
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Create a clone of this Selection with clauses corresponding
|
|
89
|
+
* to provided source removed.
|
|
90
|
+
* @param {*} source The clause source to remove.
|
|
91
|
+
* @returns {this} A cloned and updated Selection.
|
|
92
|
+
*/
|
|
93
|
+
remove(source) {
|
|
94
|
+
const s = this.clone();
|
|
95
|
+
s._value = s._resolved = s._resolver.resolve(this._resolved, { source });
|
|
96
|
+
s._value.active = { source };
|
|
97
|
+
return s;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The current active (most recently updated) selection clause.
|
|
102
|
+
*/
|
|
103
|
+
get active() {
|
|
104
|
+
return this.clauses.active;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The value corresponding to the current active selection clause.
|
|
109
|
+
* This method ensures compatibility where a normal Param is expected.
|
|
110
|
+
*/
|
|
44
111
|
get value() {
|
|
45
|
-
// return value of
|
|
46
|
-
|
|
47
|
-
return clauses[clauses.length - 1]?.value;
|
|
112
|
+
// return value of the active clause
|
|
113
|
+
return this.active?.value;
|
|
48
114
|
}
|
|
49
115
|
|
|
116
|
+
/**
|
|
117
|
+
* The current array of selection clauses.
|
|
118
|
+
*/
|
|
50
119
|
get clauses() {
|
|
51
120
|
return super.value;
|
|
52
121
|
}
|
|
53
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Indicate if this selection has a single resolution strategy.
|
|
125
|
+
*/
|
|
126
|
+
get single() {
|
|
127
|
+
return this._resolver.single;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Emit an activate event with the given selection clause.
|
|
132
|
+
* @param {*} clause The clause repesenting the potential activation.
|
|
133
|
+
*/
|
|
54
134
|
activate(clause) {
|
|
55
135
|
this.emit('activate', clause);
|
|
56
136
|
}
|
|
57
137
|
|
|
138
|
+
/**
|
|
139
|
+
* Update the selection with a new selection clause.
|
|
140
|
+
* @param {*} clause The selection clause to add.
|
|
141
|
+
* @returns {this} This Selection instance.
|
|
142
|
+
*/
|
|
58
143
|
update(clause) {
|
|
144
|
+
// we maintain an up-to-date list of all resolved clauses
|
|
145
|
+
// this ensures consistent clause state across unemitted event values
|
|
146
|
+
this._resolved = this._resolver.resolve(this._resolved, clause, true);
|
|
147
|
+
this._resolved.active = clause;
|
|
148
|
+
return super.update(this._resolved);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Upon value-typed updates, sets the current clause list to the
|
|
153
|
+
* input value and returns the active clause value.
|
|
154
|
+
* @param {string} type The event type.
|
|
155
|
+
* @param {*} value The input event value.
|
|
156
|
+
* @returns {*} For value-typed events, returns the active clause
|
|
157
|
+
* values. Otherwise returns the input event value as-is.
|
|
158
|
+
*/
|
|
159
|
+
willEmit(type, value) {
|
|
160
|
+
if (type === 'value') {
|
|
161
|
+
this._value = value;
|
|
162
|
+
return this.value;
|
|
163
|
+
}
|
|
164
|
+
return value;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Upon value-typed updates, returns a dispatch queue filter function.
|
|
169
|
+
* The return value depends on the selection resolution strategy.
|
|
170
|
+
* @param {string} type The event type.
|
|
171
|
+
* @param {*} value The input event value.
|
|
172
|
+
* @returns {*} For value-typed events, returns a dispatch queue filter
|
|
173
|
+
* function. Otherwise returns null.
|
|
174
|
+
*/
|
|
175
|
+
emitQueueFilter(type, value) {
|
|
176
|
+
return type === 'value'
|
|
177
|
+
? this._resolver.queueFilter(value)
|
|
178
|
+
: null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Indicates if a selection clause should not be applied to a given client.
|
|
183
|
+
* The return value depends on the selection resolution strategy.
|
|
184
|
+
* @param {*} client The selection clause.
|
|
185
|
+
* @param {*} clause The client to test.
|
|
186
|
+
* @returns True if the client should be skipped, false otherwise.
|
|
187
|
+
*/
|
|
188
|
+
skip(client, clause) {
|
|
189
|
+
return this._resolver.skip(client, clause);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Return a selection query predicate for the given client.
|
|
194
|
+
* @param {*} client The client whose data may be filtered.
|
|
195
|
+
* @returns {*} The query predicate for filtering client data,
|
|
196
|
+
* based on the current state of this selection.
|
|
197
|
+
*/
|
|
198
|
+
predicate(client) {
|
|
199
|
+
const { clauses } = this;
|
|
200
|
+
return this._resolver.predicate(clauses, clauses.active, client);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Implements selection clause resolution strategies.
|
|
206
|
+
*/
|
|
207
|
+
export class SelectionResolver {
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Create a new selection resolved instance.
|
|
211
|
+
* @param {object} [options] The resolution strategy options.
|
|
212
|
+
* @param {boolean} [options.union=false] Boolean flag to indicate a union strategy.
|
|
213
|
+
* If false, an intersection strategy is used.
|
|
214
|
+
* @param {boolean} [options.cross=false] Boolean flag to indicate cross-filtering.
|
|
215
|
+
* @param {boolean} [options.single=false] Boolean flag to indicate single clauses only.
|
|
216
|
+
*/
|
|
217
|
+
constructor({ union, cross, single } = {}) {
|
|
218
|
+
this.union = !!union;
|
|
219
|
+
this.cross = !!cross;
|
|
220
|
+
this.single = !!single;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Resolve a list of selection clauses according to the resolution strategy.
|
|
225
|
+
* @param {*[]} clauseList An array of selection clauses.
|
|
226
|
+
* @param {*} clause A new selection clause to add.
|
|
227
|
+
* @returns {*[]} An updated array of selection clauses.
|
|
228
|
+
*/
|
|
229
|
+
resolve(clauseList, clause, reset = false) {
|
|
59
230
|
const { source, predicate } = clause;
|
|
60
|
-
|
|
61
|
-
const clauses = this.single ? [] :
|
|
231
|
+
const filtered = clauseList.filter(c => source !== c.source);
|
|
232
|
+
const clauses = this.single ? [] : filtered;
|
|
233
|
+
if (this.single && reset) filtered.forEach(c => c.source?.reset?.());
|
|
62
234
|
if (predicate) clauses.push(clause);
|
|
63
|
-
return
|
|
235
|
+
return clauses;
|
|
64
236
|
}
|
|
65
237
|
|
|
66
|
-
|
|
67
|
-
|
|
238
|
+
/**
|
|
239
|
+
* Indicates if a selection clause should not be applied to a given client.
|
|
240
|
+
* The return value depends on the resolution strategy.
|
|
241
|
+
* @param {*} client The selection clause.
|
|
242
|
+
* @param {*} clause The client to test.
|
|
243
|
+
* @returns True if the client should be skipped, false otherwise.
|
|
244
|
+
*/
|
|
245
|
+
skip(client, clause) {
|
|
246
|
+
return this.cross && clause?.clients?.has(client);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Return a selection query predicate for the given client.
|
|
251
|
+
* @param {*[]} clauseList An array of selection clauses.
|
|
252
|
+
* @param {*} active The current active selection clause.
|
|
253
|
+
* @param {*} client The client whose data may be filtered.
|
|
254
|
+
* @returns {*} The query predicate for filtering client data,
|
|
255
|
+
* based on the current state of this selection.
|
|
256
|
+
*/
|
|
257
|
+
predicate(clauseList, active, client) {
|
|
258
|
+
const { union } = this;
|
|
68
259
|
|
|
69
260
|
// do nothing if cross-filtering and client is currently active
|
|
70
|
-
if (
|
|
261
|
+
if (this.skip(client, active)) return undefined;
|
|
71
262
|
|
|
72
263
|
// remove client-specific predicates if cross-filtering
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
).map(s => s.predicate);
|
|
264
|
+
const predicates = clauseList
|
|
265
|
+
.filter(clause => !this.skip(client, clause))
|
|
266
|
+
.map(clause => clause.predicate);
|
|
77
267
|
|
|
78
268
|
// return appropriate conjunction or disjunction
|
|
79
269
|
// an array of predicates is implicitly conjunctive
|
|
80
|
-
return union &&
|
|
270
|
+
return union && predicates.length > 1 ? or(predicates) : predicates;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Returns a filter function for queued selection updates.
|
|
275
|
+
* @param {*} value The new event value that will be enqueued.
|
|
276
|
+
* @returns {(value: *) => boolean|null} A dispatch queue filter
|
|
277
|
+
* function, or null if all unemitted event values should be filtered.
|
|
278
|
+
*/
|
|
279
|
+
queueFilter(value) {
|
|
280
|
+
if (this.cross) {
|
|
281
|
+
const source = value.active?.source;
|
|
282
|
+
return clauses => clauses.active?.source !== source;
|
|
283
|
+
}
|
|
81
284
|
}
|
|
82
285
|
}
|
package/src/index.js
CHANGED
|
@@ -2,9 +2,12 @@ export { MosaicClient } from './MosaicClient.js';
|
|
|
2
2
|
export { Coordinator, coordinator } from './Coordinator.js';
|
|
3
3
|
export { Selection, isSelection } from './Selection.js';
|
|
4
4
|
export { Param, isParam } from './Param.js';
|
|
5
|
+
export { Priority } from './QueryManager.js';
|
|
6
|
+
|
|
7
|
+
export { restConnector } from './connectors/rest.js';
|
|
8
|
+
export { socketConnector } from './connectors/socket.js';
|
|
9
|
+
export { wasmConnector } from './connectors/wasm.js';
|
|
10
|
+
|
|
5
11
|
export { distinct } from './util/distinct.js';
|
|
6
|
-
export {
|
|
12
|
+
export { synchronizer } from './util/synchronizer.js';
|
|
7
13
|
export { throttle } from './util/throttle.js';
|
|
8
|
-
export { restClient } from './clients/rest.js';
|
|
9
|
-
export { socketClient } from './clients/socket.js';
|
|
10
|
-
export { wasmClient } from './clients/wasm.js';
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event dispatcher supporting asynchronous updates.
|
|
3
|
+
* If an event handler callback returns a Promise, this dispatcher will
|
|
4
|
+
* wait for all such Promises to settle before dispatching future events
|
|
5
|
+
* of the same type.
|
|
6
|
+
*/
|
|
7
|
+
export class AsyncDispatch {
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Create a new asynchronous dispatcher instance.
|
|
11
|
+
*/
|
|
12
|
+
constructor() {
|
|
13
|
+
this._callbacks = new Map;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Add an event listener callback for the provided event type.
|
|
18
|
+
* @param {string} type The event type.
|
|
19
|
+
* @param {(value: *) => Promise?} callback The event handler
|
|
20
|
+
* callback function to add. If the callback has already been
|
|
21
|
+
* added for the event type, this method has no effect.
|
|
22
|
+
*/
|
|
23
|
+
addEventListener(type, callback) {
|
|
24
|
+
if (!this._callbacks.has(type)) {
|
|
25
|
+
this._callbacks.set(type, {
|
|
26
|
+
callbacks: new Set,
|
|
27
|
+
pending: null,
|
|
28
|
+
queue: new DispatchQueue()
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
const entry = this._callbacks.get(type);
|
|
32
|
+
entry.callbacks.add(callback);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Remove an event listener callback for the provided event type.
|
|
37
|
+
* @param {string} type The event type.
|
|
38
|
+
* @param {(value: *) => Promise?} callback The event handler
|
|
39
|
+
* callback function to remove.
|
|
40
|
+
*/
|
|
41
|
+
removeEventListener(type, callback) {
|
|
42
|
+
const entry = this._callbacks.get(type);
|
|
43
|
+
if (entry) {
|
|
44
|
+
entry.callbacks.delete(callback);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Lifecycle method that returns the event value to emit.
|
|
50
|
+
* This default implementation simply returns the input value as-is.
|
|
51
|
+
* Subclasses may override this method to implement custom transformations
|
|
52
|
+
* prior to emitting an event value to all listeners.
|
|
53
|
+
* @param {string} type The event type.
|
|
54
|
+
* @param {*} value The event value.
|
|
55
|
+
* @returns The (possibly transformed) event value to emit.
|
|
56
|
+
*/
|
|
57
|
+
willEmit(type, value) {
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Lifecycle method that returns a filter function for updating the
|
|
63
|
+
* queue of unemitted event values prior to enqueueing a new value.
|
|
64
|
+
* This default implementation simply returns null, indicating that
|
|
65
|
+
* any other unemitted event values should be dropped (that is, all
|
|
66
|
+
* queued events are filtered)
|
|
67
|
+
* @param {*} value The new event value that will be enqueued.
|
|
68
|
+
* @returns {(value: *) => boolean|null} A dispatch queue filter
|
|
69
|
+
* function, or null if all unemitted event values should be filtered.
|
|
70
|
+
*/
|
|
71
|
+
emitQueueFilter() {
|
|
72
|
+
// removes all pending items
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Cancel all unemitted event values for the given event type.
|
|
78
|
+
* @param {string} type The event type.
|
|
79
|
+
*/
|
|
80
|
+
cancel(type) {
|
|
81
|
+
const entry = this._callbacks.get(type);
|
|
82
|
+
entry?.queue.clear();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Emit an event value to listeners for the given event type.
|
|
87
|
+
* If a previous emit has not yet resolved, the event value
|
|
88
|
+
* will be queued to be emitted later.
|
|
89
|
+
* The actual event value given to listeners will be the result
|
|
90
|
+
* of passing the input value through the emitValue() method.
|
|
91
|
+
* @param {string} type The event type.
|
|
92
|
+
* @param {*} value The event value.
|
|
93
|
+
*/
|
|
94
|
+
emit(type, value) {
|
|
95
|
+
const entry = this._callbacks.get(type) || {};
|
|
96
|
+
if (entry.pending) {
|
|
97
|
+
entry.queue.enqueue(value, this.emitQueueFilter(type, value));
|
|
98
|
+
} else {
|
|
99
|
+
const event = this.willEmit(type, value);
|
|
100
|
+
const { callbacks, queue } = entry;
|
|
101
|
+
if (callbacks?.size) {
|
|
102
|
+
const promise = Promise
|
|
103
|
+
.allSettled(Array.from(callbacks, callback => callback(event)))
|
|
104
|
+
.then(() => {
|
|
105
|
+
entry.pending = null;
|
|
106
|
+
if (!queue.isEmpty()) {
|
|
107
|
+
this.emit(type, queue.dequeue());
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
entry.pending = promise;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Queue for managing unemitted event values.
|
|
118
|
+
*/
|
|
119
|
+
export class DispatchQueue {
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Create a new dispatch queue instance.
|
|
123
|
+
*/
|
|
124
|
+
constructor() {
|
|
125
|
+
this.clear();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Clear the queue state of all event values.
|
|
130
|
+
*/
|
|
131
|
+
clear() {
|
|
132
|
+
this.next = null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Indicate if the queue is empty.
|
|
137
|
+
* @returns {boolean} True if queue is empty, false otherwise.
|
|
138
|
+
*/
|
|
139
|
+
isEmpty() {
|
|
140
|
+
return !this.next;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Add a new value to the queue, and optionally filter the
|
|
145
|
+
* current queue content in response.
|
|
146
|
+
* @param {*} value The value to add.
|
|
147
|
+
* @param {(value: *) => boolean} [filter] An optional filter
|
|
148
|
+
* function to apply to existing queue content. If unspecified
|
|
149
|
+
* or falsy, all previously queued values are removed. Otherwise,
|
|
150
|
+
* the provided function is applied to all queue entries. The
|
|
151
|
+
* entry is retained if the filter function returns a truthy value,
|
|
152
|
+
* otherwise the entry is removed.
|
|
153
|
+
*/
|
|
154
|
+
enqueue(value, filter) {
|
|
155
|
+
const tail = { value };
|
|
156
|
+
if (filter && this.next) {
|
|
157
|
+
let curr = this;
|
|
158
|
+
while (curr.next) {
|
|
159
|
+
if (filter(curr.next.value)) {
|
|
160
|
+
curr = curr.next;
|
|
161
|
+
} else {
|
|
162
|
+
curr.next = curr.next.next;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
curr.next = tail;
|
|
166
|
+
} else {
|
|
167
|
+
this.next = tail;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Remove and return the next queued event value.
|
|
173
|
+
* @returns {*} The next event value in the queue.
|
|
174
|
+
*/
|
|
175
|
+
dequeue() {
|
|
176
|
+
const { next } = this;
|
|
177
|
+
this.next = next?.next;
|
|
178
|
+
return next?.value;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const requestIdle = typeof requestIdleCallback !== 'undefined'
|
|
2
|
+
? requestIdleCallback
|
|
3
|
+
: setTimeout;
|
|
4
|
+
|
|
5
|
+
export const voidCache = () => ({
|
|
6
|
+
get: () => undefined,
|
|
7
|
+
set: (key, value) => value,
|
|
8
|
+
clear: () => {}
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export function lruCache({
|
|
12
|
+
max = 1000, // max entries
|
|
13
|
+
ttl = 3 * 60 * 60 * 1000 // time-to-live, default 3 hours
|
|
14
|
+
} = {}) {
|
|
15
|
+
let cache = new Map;
|
|
16
|
+
|
|
17
|
+
function evict() {
|
|
18
|
+
const expire = performance.now() - ttl;
|
|
19
|
+
let lruKey = null;
|
|
20
|
+
let lruLast = Infinity;
|
|
21
|
+
|
|
22
|
+
for (const [key, value] of cache) {
|
|
23
|
+
const { last } = value;
|
|
24
|
+
|
|
25
|
+
// least recently used entry seen so far
|
|
26
|
+
if (last < lruLast) {
|
|
27
|
+
lruKey = key;
|
|
28
|
+
lruLast = last;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// remove if time since last access exceeds ttl
|
|
32
|
+
if (expire > last) {
|
|
33
|
+
cache.delete(key);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// remove lru entry
|
|
38
|
+
if (lruKey) {
|
|
39
|
+
cache.delete(lruKey);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
get(key) {
|
|
45
|
+
const entry = cache.get(key);
|
|
46
|
+
if (entry) {
|
|
47
|
+
entry.last = performance.now();
|
|
48
|
+
return entry.value;
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
set(key, value) {
|
|
52
|
+
cache.set(key, { last: performance.now(), value });
|
|
53
|
+
if (cache.size > max) requestIdle(evict);
|
|
54
|
+
return value;
|
|
55
|
+
},
|
|
56
|
+
clear() { cache = new Map; }
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create a new priority queue instance.
|
|
3
|
+
* @param {number} ranks An integer number of rank-order priority levels.
|
|
4
|
+
* @returns A priority queue instance.
|
|
5
|
+
*/
|
|
6
|
+
export function priorityQueue(ranks) {
|
|
7
|
+
// one list for each integer priority level
|
|
8
|
+
const queue = Array.from(
|
|
9
|
+
{ length: ranks },
|
|
10
|
+
() => ({ head: null, tail: null })
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
/**
|
|
15
|
+
* Indicate if the queue is empty.
|
|
16
|
+
* @returns [boolean] true if empty, false otherwise.
|
|
17
|
+
*/
|
|
18
|
+
isEmpty() {
|
|
19
|
+
return queue.every(list => !list.head);
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Insert an item into the queue with a given priority rank.
|
|
24
|
+
* @param {*} item The item to add.
|
|
25
|
+
* @param {number} rank The integer priority rank.
|
|
26
|
+
* Priority ranks are integers starting at zero.
|
|
27
|
+
* Lower ranks indicate higher priority.
|
|
28
|
+
*/
|
|
29
|
+
insert(item, rank) {
|
|
30
|
+
const list = queue[rank];
|
|
31
|
+
if (!list) {
|
|
32
|
+
throw new Error(`Invalid queue priority rank: ${rank}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const node = { item, next: null };
|
|
36
|
+
if (list.head === null) {
|
|
37
|
+
list.head = list.tail = node;
|
|
38
|
+
} else {
|
|
39
|
+
list.tail = (list.tail.next = node);
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Remove a set of items from the queue, regardless of priority rank.
|
|
45
|
+
* If a provided item is not in the queue it will be ignored.
|
|
46
|
+
* @param {(item: *) => boolean} test A predicate function to test
|
|
47
|
+
* if an item should be removed (true to drop, false to keep).
|
|
48
|
+
*/
|
|
49
|
+
remove(test) {
|
|
50
|
+
for (const list of queue) {
|
|
51
|
+
let { head, tail } = list;
|
|
52
|
+
for (let prev = null, curr = head; curr; prev = curr, curr = curr.next) {
|
|
53
|
+
if (test(curr.item)) {
|
|
54
|
+
if (curr === head) {
|
|
55
|
+
head = curr.next;
|
|
56
|
+
} else {
|
|
57
|
+
prev.next = curr.next;
|
|
58
|
+
}
|
|
59
|
+
if (curr === tail) tail = prev || head;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
list.head = head;
|
|
63
|
+
list.tail = tail;
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Remove and return the next highest priority item.
|
|
69
|
+
* @returns {*} The next item in the queue,
|
|
70
|
+
* or undefined if this queue is empty.
|
|
71
|
+
*/
|
|
72
|
+
next() {
|
|
73
|
+
for (const list of queue) {
|
|
74
|
+
const { head } = list;
|
|
75
|
+
if (head !== null) {
|
|
76
|
+
list.head = head.next;
|
|
77
|
+
if (list.tail === head) {
|
|
78
|
+
list.tail = null;
|
|
79
|
+
}
|
|
80
|
+
return head.item;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
}
|