@_linked/core 2.25.0 → 2.27.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/CHANGELOG.md +49 -0
- package/README.md +27 -0
- package/lib/esm/index.d.ts +9 -0
- package/lib/esm/index.js +8 -0
- package/lib/esm/index.js.map +1 -1
- package/lib/esm/interfaces/IDataset.d.ts +15 -0
- package/lib/esm/interfaces/IDataset.js.map +1 -1
- package/lib/esm/live/LiveQueryStore.d.ts +240 -0
- package/lib/esm/live/LiveQueryStore.js +729 -0
- package/lib/esm/live/LiveQueryStore.js.map +1 -0
- package/lib/esm/live/changes.d.ts +30 -0
- package/lib/esm/live/changes.js +30 -0
- package/lib/esm/live/changes.js.map +1 -0
- package/lib/esm/live/keys.d.ts +54 -0
- package/lib/esm/live/keys.js +112 -0
- package/lib/esm/live/keys.js.map +1 -0
- package/lib/esm/live/matcher.d.ts +43 -0
- package/lib/esm/live/matcher.js +115 -0
- package/lib/esm/live/matcher.js.map +1 -0
- package/lib/esm/live/registry.d.ts +24 -0
- package/lib/esm/live/registry.js +41 -0
- package/lib/esm/live/registry.js.map +1 -0
- package/lib/esm/live.d.ts +12 -0
- package/lib/esm/live.js +15 -0
- package/lib/esm/live.js.map +1 -0
- package/lib/esm/paths/normalizePropertyPath.js +3 -2
- package/lib/esm/paths/normalizePropertyPath.js.map +1 -1
- package/lib/esm/queries/AskBuilder.d.ts +10 -0
- package/lib/esm/queries/AskBuilder.js +14 -0
- package/lib/esm/queries/AskBuilder.js.map +1 -1
- package/lib/esm/queries/CountBuilder.d.ts +10 -0
- package/lib/esm/queries/CountBuilder.js +14 -0
- package/lib/esm/queries/CountBuilder.js.map +1 -1
- package/lib/esm/queries/QueryBuilder.d.ts +10 -0
- package/lib/esm/queries/QueryBuilder.js +14 -0
- package/lib/esm/queries/QueryBuilder.js.map +1 -1
- package/lib/esm/queries/QueryContext.js +9 -2
- package/lib/esm/queries/QueryContext.js.map +1 -1
- package/lib/esm/queries/queryDependencies.d.ts +58 -0
- package/lib/esm/queries/queryDependencies.js +494 -0
- package/lib/esm/queries/queryDependencies.js.map +1 -0
- package/lib/esm/queries/queryDispatch.d.ts +46 -0
- package/lib/esm/queries/queryDispatch.js +88 -3
- package/lib/esm/queries/queryDispatch.js.map +1 -1
- package/lib/esm/shapes/nodeShapeData.d.ts +16 -0
- package/lib/esm/shapes/nodeShapeData.js +41 -1
- package/lib/esm/shapes/nodeShapeData.js.map +1 -1
- package/lib/esm/shapes/relationShape.d.ts +35 -0
- package/lib/esm/shapes/relationShape.js +88 -0
- package/lib/esm/shapes/relationShape.js.map +1 -0
- package/lib/esm/shapes/validation.js +8 -4
- package/lib/esm/shapes/validation.js.map +1 -1
- package/lib/esm/sparql/irToAlgebra.js +5 -40
- package/lib/esm/sparql/irToAlgebra.js.map +1 -1
- package/lib/esm/utils/LinkedStorage.d.ts +6 -0
- package/lib/esm/utils/LinkedStorage.js +32 -6
- package/lib/esm/utils/LinkedStorage.js.map +1 -1
- package/lib/esm/utils/ShapeClass.d.ts +26 -0
- package/lib/esm/utils/ShapeClass.js +128 -0
- package/lib/esm/utils/ShapeClass.js.map +1 -1
- package/package.json +6 -2
|
@@ -0,0 +1,729 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
/*
|
|
11
|
+
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
12
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
13
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
14
|
+
*/
|
|
15
|
+
import { LinkedStorage } from '../utils/LinkedStorage.js';
|
|
16
|
+
import { subscribeQueryContext, UnresolvedContextError } from '../queries/QueryContext.js';
|
|
17
|
+
import { resolveContextId } from '../queries/ContextRef.js';
|
|
18
|
+
import { subscribeQueryDispatch } from '../queries/queryDispatch.js';
|
|
19
|
+
import { mutationEffects, queryDependencies, } from '../queries/queryDependencies.js';
|
|
20
|
+
import { getShapeClass } from '../utils/ShapeClass.js';
|
|
21
|
+
import { effectsHash, normalizeChange } from './changes.js';
|
|
22
|
+
import { selectInstances } from './matcher.js';
|
|
23
|
+
import { kindOf, paramsKey, splitQuery, stableStringify, stripSubjects, } from './keys.js';
|
|
24
|
+
import { setLiveQueryStore, peekLiveQueryStore } from './registry.js';
|
|
25
|
+
const IDLE = Object.freeze({ status: 'pending', notFound: false, refreshing: false });
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Store
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
/**
|
|
30
|
+
* Holds every live query in the runtime: templates (what is read) and
|
|
31
|
+
* instances (what it is applied to, and the data). Framework-free; UI layers
|
|
32
|
+
* subscribe to instances. One store per runtime, registered on `globalThis`
|
|
33
|
+
* (see `registry.ts`).
|
|
34
|
+
*/
|
|
35
|
+
export class LiveQueryStore {
|
|
36
|
+
constructor() {
|
|
37
|
+
this.options = { gcMs: 30000, echoMs: 50 };
|
|
38
|
+
/** @internal */
|
|
39
|
+
this._templates = new Map();
|
|
40
|
+
/** @internal Indexes read by the change matcher. */
|
|
41
|
+
this.templatesByProp = new Map();
|
|
42
|
+
/** @internal */
|
|
43
|
+
this.templatesByShape = new Map();
|
|
44
|
+
/** @internal */
|
|
45
|
+
this.instancesById = new Map();
|
|
46
|
+
this._disposers = [];
|
|
47
|
+
this._feeds = new Map();
|
|
48
|
+
this._recentChanges = new Map();
|
|
49
|
+
this._pendingRefetch = new Set();
|
|
50
|
+
this._flushScheduled = false;
|
|
51
|
+
this._disposers.push(subscribeQueryContext((name) => this._onContextChange(name)));
|
|
52
|
+
this._disposers.push(subscribeQueryDispatch((e) => this._onDispatch(e)));
|
|
53
|
+
this._disposers.push(LinkedStorage.onRoutingChanged(() => this._onRoutingChanged()));
|
|
54
|
+
this._scanDatasets();
|
|
55
|
+
}
|
|
56
|
+
// -------------------------------------------------------------------------
|
|
57
|
+
// Templates
|
|
58
|
+
// -------------------------------------------------------------------------
|
|
59
|
+
/** Register (or find) the template of `query`. Options merge into an existing template. */
|
|
60
|
+
template(query, opts = {}) {
|
|
61
|
+
return this._template(splitQuery(query).templateJson, query, opts);
|
|
62
|
+
}
|
|
63
|
+
_template(templateJson, query, opts) {
|
|
64
|
+
const key = stableStringify(templateJson);
|
|
65
|
+
let t = this._templates.get(key);
|
|
66
|
+
if (!t) {
|
|
67
|
+
t = {
|
|
68
|
+
key,
|
|
69
|
+
kind: kindOf(query),
|
|
70
|
+
json: templateJson,
|
|
71
|
+
builder: stripSubjects(query),
|
|
72
|
+
shapeIri: typeof templateJson.shape === 'string' ? templateJson.shape : undefined,
|
|
73
|
+
reactive: true,
|
|
74
|
+
pinned: false,
|
|
75
|
+
instances: new Map(),
|
|
76
|
+
};
|
|
77
|
+
this._templates.set(key, t);
|
|
78
|
+
}
|
|
79
|
+
if (opts.name !== undefined)
|
|
80
|
+
t.name = opts.name;
|
|
81
|
+
if (opts.reactive !== undefined)
|
|
82
|
+
t.reactive = opts.reactive;
|
|
83
|
+
if (opts.pinned)
|
|
84
|
+
t.pinned = true;
|
|
85
|
+
return t;
|
|
86
|
+
}
|
|
87
|
+
/** The registered templates (for database tuning, devtools). */
|
|
88
|
+
templates() {
|
|
89
|
+
return [...this._templates.values()].map((t) => ({ key: t.key, kind: t.kind, name: t.name, json: t.json }));
|
|
90
|
+
}
|
|
91
|
+
/** Compute the dependencies of every template now (they are otherwise computed on first use). */
|
|
92
|
+
prepare() {
|
|
93
|
+
for (const t of this._templates.values())
|
|
94
|
+
this.depsOf(t);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* The watch set of a template, computed once. When a query context used in
|
|
98
|
+
* `where` is not set yet, lowering throws; the template then gets provisional
|
|
99
|
+
* dependencies (its shape only) and is recomputed on its next use, which the
|
|
100
|
+
* context change triggers.
|
|
101
|
+
*/
|
|
102
|
+
depsOf(template) {
|
|
103
|
+
if (template.deps && !template.depsProvisional)
|
|
104
|
+
return template.deps;
|
|
105
|
+
try {
|
|
106
|
+
template.deps = queryDependencies(template.builder);
|
|
107
|
+
template.depsProvisional = false;
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
if (!(err instanceof UnresolvedContextError))
|
|
111
|
+
throw err;
|
|
112
|
+
if (template.deps)
|
|
113
|
+
return template.deps; // still provisional
|
|
114
|
+
template.deps = {
|
|
115
|
+
narrow: new Set(),
|
|
116
|
+
filter: new Set(),
|
|
117
|
+
hidden: new Set(),
|
|
118
|
+
shapes: new Set(template.shapeIri ? [template.shapeIri] : []),
|
|
119
|
+
unbound: false,
|
|
120
|
+
};
|
|
121
|
+
template.depsProvisional = true;
|
|
122
|
+
}
|
|
123
|
+
this._index(template);
|
|
124
|
+
return template.deps;
|
|
125
|
+
}
|
|
126
|
+
_index(t) {
|
|
127
|
+
const deps = t.deps;
|
|
128
|
+
for (const set of [deps.narrow, deps.filter, deps.hidden]) {
|
|
129
|
+
for (const p of set)
|
|
130
|
+
addToIndex(this.templatesByProp, p, t);
|
|
131
|
+
}
|
|
132
|
+
for (const s of deps.shapes)
|
|
133
|
+
addToIndex(this.templatesByShape, s, t);
|
|
134
|
+
}
|
|
135
|
+
_unindexTemplate(t) {
|
|
136
|
+
for (const set of this.templatesByProp.values())
|
|
137
|
+
set.delete(t);
|
|
138
|
+
for (const set of this.templatesByShape.values())
|
|
139
|
+
set.delete(t);
|
|
140
|
+
}
|
|
141
|
+
// -------------------------------------------------------------------------
|
|
142
|
+
// Instances and handles
|
|
143
|
+
// -------------------------------------------------------------------------
|
|
144
|
+
/** @internal Get or create the instance of `template` for `params`, executing `builder`. */
|
|
145
|
+
instance(template, params, builder) {
|
|
146
|
+
const pk = paramsKey(params);
|
|
147
|
+
let inst = template.instances.get(pk);
|
|
148
|
+
if (!inst) {
|
|
149
|
+
this.depsOf(template); // index the template before its first instance exists
|
|
150
|
+
inst = {
|
|
151
|
+
key: `${template.key}|${pk}`,
|
|
152
|
+
template,
|
|
153
|
+
params,
|
|
154
|
+
builder,
|
|
155
|
+
state: IDLE,
|
|
156
|
+
seq: 0,
|
|
157
|
+
inflight: false,
|
|
158
|
+
staleWhileInflight: false,
|
|
159
|
+
ids: new Set(),
|
|
160
|
+
listeners: new Set(),
|
|
161
|
+
dropped: false,
|
|
162
|
+
};
|
|
163
|
+
template.instances.set(pk, inst);
|
|
164
|
+
}
|
|
165
|
+
return inst;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Subscribe to `query`. The first listener on an instance starts its fetch;
|
|
169
|
+
* identical queries share one instance and one request. A handle created
|
|
170
|
+
* without a listener holds nothing: its instance is released after the grace
|
|
171
|
+
* period unless something subscribes.
|
|
172
|
+
*/
|
|
173
|
+
subscribe(query, listener, opts = {}) {
|
|
174
|
+
const { templateJson, params } = splitQuery(query);
|
|
175
|
+
const template = this._template(templateJson, query, opts);
|
|
176
|
+
const inst = this.instance(template, params, query);
|
|
177
|
+
if (inst.listeners.size === 0)
|
|
178
|
+
this._scheduleGc(inst);
|
|
179
|
+
const handle = new LiveQueryHandle(this, inst);
|
|
180
|
+
if (listener)
|
|
181
|
+
handle.subscribe(listener);
|
|
182
|
+
return handle;
|
|
183
|
+
}
|
|
184
|
+
/** @internal The live instance for a possibly dropped one (same template, params and builder). */
|
|
185
|
+
_resolve(inst) {
|
|
186
|
+
var _a;
|
|
187
|
+
if (!inst.dropped)
|
|
188
|
+
return inst;
|
|
189
|
+
if (inst.movedTo)
|
|
190
|
+
return this._resolve(inst.movedTo);
|
|
191
|
+
const t = (_a = this._templates.get(inst.template.key)) !== null && _a !== void 0 ? _a : this._template(inst.template.json, inst.builder, {});
|
|
192
|
+
return this.instance(t, inst.params, inst.builder);
|
|
193
|
+
}
|
|
194
|
+
/** @internal */
|
|
195
|
+
_attach(inst, listener) {
|
|
196
|
+
this._cancelGc(inst);
|
|
197
|
+
inst.listeners.add(listener);
|
|
198
|
+
if (inst.state.status === 'pending' && !inst.inflight) {
|
|
199
|
+
void this._fetch(inst);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/** @internal */
|
|
203
|
+
_detach(inst, listener) {
|
|
204
|
+
inst.listeners.delete(listener);
|
|
205
|
+
if (inst.listeners.size === 0 && !inst.dropped)
|
|
206
|
+
this._scheduleGc(inst);
|
|
207
|
+
}
|
|
208
|
+
/** Refetch an instance, keeping its data on screen. Resolves when that request settles. */
|
|
209
|
+
refresh(inst) {
|
|
210
|
+
return this._fetch(inst);
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Local edit of an instance's data. Never touches the store. A patched
|
|
214
|
+
* instance counts as loaded (`success`), so `await live` resolves with it.
|
|
215
|
+
*/
|
|
216
|
+
patch(inst, partial) {
|
|
217
|
+
const current = inst.state.data;
|
|
218
|
+
const mergeable = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
219
|
+
const next = typeof partial === 'function'
|
|
220
|
+
? partial(current)
|
|
221
|
+
: mergeable(current) && mergeable(partial)
|
|
222
|
+
? Object.assign(Object.assign({}, current), partial) : partial;
|
|
223
|
+
const status = inst.state.status === 'error' ? 'error' : 'success';
|
|
224
|
+
this._setState(inst, Object.assign(Object.assign({}, inst.state), { status, data: next, notFound: next === null }));
|
|
225
|
+
this._collectIds(inst, next);
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Refetch every instance matching `target`: a shape class or shape IRI
|
|
229
|
+
* (everything that scans or traverses it, as after a delete), a node
|
|
230
|
+
* reference (`{id}`), a query (its template), or a template. Never folded
|
|
231
|
+
* with other change events.
|
|
232
|
+
*/
|
|
233
|
+
invalidate(target) {
|
|
234
|
+
var _a, _b;
|
|
235
|
+
if (typeof target === 'function' || typeof target === 'string') {
|
|
236
|
+
const shape = typeof target === 'string' ? target : (_a = target.shape) === null || _a === void 0 ? void 0 : _a.id;
|
|
237
|
+
if (!shape)
|
|
238
|
+
throw new Error('invalidate(): the shape class has no registered shape.');
|
|
239
|
+
this.publish({ effects: { op: 'delete', shape, props: new Set(), ids: undefined, membership: new Set([shape]) } }, 'manual');
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
const instances = new Set();
|
|
243
|
+
if (isTemplate(target)) {
|
|
244
|
+
for (const i of target.instances.values())
|
|
245
|
+
instances.add(i);
|
|
246
|
+
}
|
|
247
|
+
else if (typeof target.__queryKind === 'string') {
|
|
248
|
+
const t = this._templates.get(stableStringify(splitQuery(target).templateJson));
|
|
249
|
+
if (t)
|
|
250
|
+
for (const i of t.instances.values())
|
|
251
|
+
instances.add(i);
|
|
252
|
+
}
|
|
253
|
+
else if (typeof target.id === 'string') {
|
|
254
|
+
for (const i of (_b = this.instancesById.get(target.id)) !== null && _b !== void 0 ? _b : [])
|
|
255
|
+
instances.add(i);
|
|
256
|
+
}
|
|
257
|
+
this._refetchAll(instances);
|
|
258
|
+
}
|
|
259
|
+
/** @internal Refetch a set of instances, folding in-flight ones into one follow-up request. */
|
|
260
|
+
_refetchAll(instances) {
|
|
261
|
+
for (const inst of instances) {
|
|
262
|
+
if (inst.listeners.size === 0 || inst.dropped)
|
|
263
|
+
continue; // nobody is watching; it refetches when subscribed again
|
|
264
|
+
if (inst.inflight)
|
|
265
|
+
inst.staleWhileInflight = true;
|
|
266
|
+
else
|
|
267
|
+
void this._fetch(inst);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
// -------------------------------------------------------------------------
|
|
271
|
+
// Change sources
|
|
272
|
+
// -------------------------------------------------------------------------
|
|
273
|
+
/**
|
|
274
|
+
* Tell the store that data changed. Every source ends here: local mutations
|
|
275
|
+
* (automatically, origin `local`), dataset change feeds (`feed`), application
|
|
276
|
+
* code via `publishChange()` (`app`) and `invalidate()` (`manual`).
|
|
277
|
+
*
|
|
278
|
+
* A local change and its remote confirmation carry identical effects; when
|
|
279
|
+
* they arrive within `options.echoMs` of each other they refetch once. Two
|
|
280
|
+
* events of the same origin are never folded — two writes to the same node
|
|
281
|
+
* in quick succession are two changes — and manual invalidations never are.
|
|
282
|
+
*/
|
|
283
|
+
publish(event, origin = 'app') {
|
|
284
|
+
const effects = normalizeChange(event);
|
|
285
|
+
if (origin !== 'manual') {
|
|
286
|
+
const hash = effectsHash(effects);
|
|
287
|
+
const now = Date.now();
|
|
288
|
+
const prev = this._recentChanges.get(hash);
|
|
289
|
+
if (prev && prev.origin !== 'manual' && prev.origin !== origin && now - prev.at < this.options.echoMs) {
|
|
290
|
+
return; // the echo of a change already applied
|
|
291
|
+
}
|
|
292
|
+
this._recentChanges.set(hash, { at: now, origin });
|
|
293
|
+
if (this._recentChanges.size > 256) {
|
|
294
|
+
// Bounded: anything older than the echo window can no longer fold with a newcomer.
|
|
295
|
+
for (const [h, r] of this._recentChanges)
|
|
296
|
+
if (now - r.at >= this.options.echoMs)
|
|
297
|
+
this._recentChanges.delete(h);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
for (const inst of selectInstances(this, effects))
|
|
301
|
+
this._pendingRefetch.add(inst);
|
|
302
|
+
this._scheduleFlush();
|
|
303
|
+
}
|
|
304
|
+
_scheduleFlush() {
|
|
305
|
+
if (this._flushScheduled)
|
|
306
|
+
return;
|
|
307
|
+
this._flushScheduled = true;
|
|
308
|
+
queueMicrotask(() => {
|
|
309
|
+
this._flushScheduled = false;
|
|
310
|
+
const batch = this._pendingRefetch;
|
|
311
|
+
this._pendingRefetch = new Set();
|
|
312
|
+
this._refetchAll(batch);
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Local mutations. Select and ask events are ignored: the store's own fetches
|
|
317
|
+
* run through the same dispatch and must not feed back into it.
|
|
318
|
+
*/
|
|
319
|
+
_onDispatch(e) {
|
|
320
|
+
var _a, _b;
|
|
321
|
+
if (e.kind !== 'create' && e.kind !== 'update' && e.kind !== 'delete')
|
|
322
|
+
return;
|
|
323
|
+
const query = e.query;
|
|
324
|
+
const dataset = (_a = e.target) !== null && _a !== void 0 ? _a : LinkedStorage.getDatasetForShapeClass((((_b = query.shape) === null || _b === void 0 ? void 0 : _b.id) ? getShapeClass(query.shape.id) : undefined));
|
|
325
|
+
if (dataset === null || dataset === void 0 ? void 0 : dataset.authoritativeChanges)
|
|
326
|
+
return; // the dataset's own feed will report it
|
|
327
|
+
e.result.then((result) => {
|
|
328
|
+
let effects;
|
|
329
|
+
try {
|
|
330
|
+
effects = mutationEffects(e.query, result);
|
|
331
|
+
}
|
|
332
|
+
catch (err) {
|
|
333
|
+
console.error('[linked] could not derive the effects of a mutation; live queries were not refetched', err);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
this.publish({ effects }, 'local');
|
|
337
|
+
}, () => { });
|
|
338
|
+
}
|
|
339
|
+
/** Subscribe to the change feeds of the datasets currently routed; drop feeds of datasets that left. */
|
|
340
|
+
_scanDatasets() {
|
|
341
|
+
const current = new Set(LinkedStorage.getDatasets());
|
|
342
|
+
for (const [dataset, off] of [...this._feeds]) {
|
|
343
|
+
if (!current.has(dataset)) {
|
|
344
|
+
off();
|
|
345
|
+
this._feeds.delete(dataset);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
for (const dataset of current) {
|
|
349
|
+
if (typeof dataset.subscribeChanges !== 'function' || this._feeds.has(dataset))
|
|
350
|
+
continue;
|
|
351
|
+
const off = dataset.subscribeChanges((event) => {
|
|
352
|
+
try {
|
|
353
|
+
this.publish(event, 'feed');
|
|
354
|
+
}
|
|
355
|
+
catch (err) {
|
|
356
|
+
console.error('[linked] a dataset change event could not be applied', err);
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
this._feeds.set(dataset, off);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Storage changed (a dataset was set, pinned or unpinned): what is cached may
|
|
364
|
+
* come from a store that no longer answers, so instances nobody watches are
|
|
365
|
+
* dropped and watched ones fetch again. Also refreshes the feed subscriptions.
|
|
366
|
+
*/
|
|
367
|
+
_onRoutingChanged() {
|
|
368
|
+
this._scanDatasets();
|
|
369
|
+
this._recentChanges.clear();
|
|
370
|
+
for (const t of [...this._templates.values()]) {
|
|
371
|
+
for (const inst of [...t.instances.values()]) {
|
|
372
|
+
if (inst.listeners.size === 0)
|
|
373
|
+
this._dropInstance(inst);
|
|
374
|
+
else
|
|
375
|
+
this._refetchAll([inst]);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
/** Drop every template and instance (tests). Subscriptions to sources stay. */
|
|
380
|
+
reset() {
|
|
381
|
+
for (const t of [...this._templates.values()]) {
|
|
382
|
+
for (const i of [...t.instances.values()])
|
|
383
|
+
this._dropInstance(i, true);
|
|
384
|
+
}
|
|
385
|
+
this._templates.clear();
|
|
386
|
+
this.templatesByProp.clear();
|
|
387
|
+
this.templatesByShape.clear();
|
|
388
|
+
this.instancesById.clear();
|
|
389
|
+
this._recentChanges.clear();
|
|
390
|
+
this._pendingRefetch.clear();
|
|
391
|
+
}
|
|
392
|
+
/** @internal */
|
|
393
|
+
dispose() {
|
|
394
|
+
this.reset();
|
|
395
|
+
for (const off of this._disposers)
|
|
396
|
+
off();
|
|
397
|
+
for (const off of this._feeds.values())
|
|
398
|
+
off();
|
|
399
|
+
this._feeds.clear();
|
|
400
|
+
}
|
|
401
|
+
// -------------------------------------------------------------------------
|
|
402
|
+
// Fetching
|
|
403
|
+
// -------------------------------------------------------------------------
|
|
404
|
+
_fetch(inst) {
|
|
405
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
406
|
+
const unresolvedSubject = !!inst.params.contextName && !inst.params.subject;
|
|
407
|
+
if (!LinkedStorage.isInitialised() || unresolvedSubject) {
|
|
408
|
+
// Nothing to request yet. Stay quiet if that is already what listeners know.
|
|
409
|
+
if (inst.state.status !== 'pending' || inst.state.refreshing) {
|
|
410
|
+
this._setState(inst, Object.assign(Object.assign({}, inst.state), { status: 'pending', refreshing: false }));
|
|
411
|
+
}
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
if (inst.template.depsProvisional)
|
|
415
|
+
this.depsOf(inst.template); // a context may have landed
|
|
416
|
+
const seq = ++inst.seq;
|
|
417
|
+
inst.inflight = true;
|
|
418
|
+
const hasData = inst.state.data !== undefined;
|
|
419
|
+
this._setState(inst, Object.assign(Object.assign({}, inst.state), { status: hasData ? inst.state.status : 'loading', refreshing: hasData }));
|
|
420
|
+
try {
|
|
421
|
+
// `exec()` is the same path `await query` takes: dispatch, count/ask
|
|
422
|
+
// contracts, error wrapping. The store adds nothing of its own to it.
|
|
423
|
+
const data = yield inst.builder.exec();
|
|
424
|
+
if (seq !== inst.seq)
|
|
425
|
+
return;
|
|
426
|
+
this._applyResult(inst, data);
|
|
427
|
+
}
|
|
428
|
+
catch (err) {
|
|
429
|
+
if (seq !== inst.seq)
|
|
430
|
+
return;
|
|
431
|
+
this._setState(inst, Object.assign(Object.assign({}, inst.state), { status: 'error', error: err instanceof Error ? err : new Error(String(err)), refreshing: false }));
|
|
432
|
+
}
|
|
433
|
+
finally {
|
|
434
|
+
if (seq === inst.seq) {
|
|
435
|
+
inst.inflight = false;
|
|
436
|
+
if (inst.staleWhileInflight) {
|
|
437
|
+
inst.staleWhileInflight = false;
|
|
438
|
+
void this._fetch(inst);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
_applyResult(inst, data) {
|
|
445
|
+
const notFound = data === null && inst.template.kind === 'select' && !!(inst.params.subject || inst.params.one);
|
|
446
|
+
if (inst.state.status === 'success' && deepEqual(inst.state.data, data)) {
|
|
447
|
+
// Structural sharing: same data, keep the reference and stay quiet.
|
|
448
|
+
if (inst.state.refreshing)
|
|
449
|
+
this._setState(inst, Object.assign(Object.assign({}, inst.state), { refreshing: false, error: undefined }));
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
this._collectIds(inst, data);
|
|
453
|
+
this._setState(inst, { status: 'success', data, error: undefined, notFound, refreshing: false });
|
|
454
|
+
}
|
|
455
|
+
_collectIds(inst, data) {
|
|
456
|
+
var _a;
|
|
457
|
+
const ids = new Set();
|
|
458
|
+
if (inst.params.subject)
|
|
459
|
+
ids.add(inst.params.subject);
|
|
460
|
+
for (const s of (_a = inst.params.subjects) !== null && _a !== void 0 ? _a : [])
|
|
461
|
+
ids.add(s);
|
|
462
|
+
collectIds(data, ids);
|
|
463
|
+
for (const old of inst.ids)
|
|
464
|
+
if (!ids.has(old))
|
|
465
|
+
removeFromIndex(this.instancesById, old, inst);
|
|
466
|
+
for (const id of ids)
|
|
467
|
+
if (!inst.ids.has(id))
|
|
468
|
+
addToIndex(this.instancesById, id, inst);
|
|
469
|
+
inst.ids = ids;
|
|
470
|
+
}
|
|
471
|
+
_setState(inst, state) {
|
|
472
|
+
inst.state = state;
|
|
473
|
+
for (const l of [...inst.listeners]) {
|
|
474
|
+
try {
|
|
475
|
+
l(state);
|
|
476
|
+
}
|
|
477
|
+
catch (err) {
|
|
478
|
+
console.error('[linked] live query listener failed', err);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
// -------------------------------------------------------------------------
|
|
483
|
+
// Query context
|
|
484
|
+
// -------------------------------------------------------------------------
|
|
485
|
+
/**
|
|
486
|
+
* A query context changed. An instance whose *subject* is that context is
|
|
487
|
+
* re-keyed to the new id (the builder resolves the context lazily, so it is
|
|
488
|
+
* reused); an instance that references it in `where` just fetches again.
|
|
489
|
+
*/
|
|
490
|
+
_onContextChange(name) {
|
|
491
|
+
var _a;
|
|
492
|
+
const resolved = resolveContextId(name, false);
|
|
493
|
+
for (const t of [...this._templates.values()]) {
|
|
494
|
+
for (const inst of [...t.instances.values()]) {
|
|
495
|
+
if ((_a = inst.params.contextNames) === null || _a === void 0 ? void 0 : _a.includes(name)) {
|
|
496
|
+
if (t.depsProvisional) {
|
|
497
|
+
this._unindexTemplate(t);
|
|
498
|
+
this.depsOf(t);
|
|
499
|
+
}
|
|
500
|
+
if (inst.listeners.size > 0)
|
|
501
|
+
this._refetchAll([inst]);
|
|
502
|
+
}
|
|
503
|
+
if (inst.params.contextName !== name)
|
|
504
|
+
continue;
|
|
505
|
+
const params = Object.assign({}, inst.params);
|
|
506
|
+
if (resolved)
|
|
507
|
+
params.subject = resolved;
|
|
508
|
+
else
|
|
509
|
+
delete params.subject;
|
|
510
|
+
if (paramsKey(params) === paramsKey(inst.params))
|
|
511
|
+
continue;
|
|
512
|
+
// Move the listeners to the instance for the new subject and drop the old one.
|
|
513
|
+
const next = this.instance(t, params, inst.builder);
|
|
514
|
+
this._cancelGc(next);
|
|
515
|
+
const listeners = [...inst.listeners];
|
|
516
|
+
inst.listeners.clear();
|
|
517
|
+
inst.seq++; // any in-flight response for the old subject is now stale
|
|
518
|
+
inst.movedTo = next;
|
|
519
|
+
this._dropInstance(inst, true);
|
|
520
|
+
for (const l of listeners)
|
|
521
|
+
next.listeners.add(l);
|
|
522
|
+
if (next.listeners.size > 0) {
|
|
523
|
+
if (!resolved)
|
|
524
|
+
this._setState(next, Object.assign({}, IDLE));
|
|
525
|
+
else if (next.state.status === 'pending' && !next.inflight)
|
|
526
|
+
void this._fetch(next);
|
|
527
|
+
else
|
|
528
|
+
this._setState(next, next.state); // publish the cached state to the moved listeners
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
this._scheduleGc(next);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
// -------------------------------------------------------------------------
|
|
537
|
+
// GC
|
|
538
|
+
// -------------------------------------------------------------------------
|
|
539
|
+
/** Remove an instance from its template (and the template when it is empty and not pinned). */
|
|
540
|
+
_dropInstance(inst, keepTemplate = false) {
|
|
541
|
+
this._cancelGc(inst);
|
|
542
|
+
inst.dropped = true;
|
|
543
|
+
const t = inst.template;
|
|
544
|
+
if (t.instances.get(paramsKey(inst.params)) === inst)
|
|
545
|
+
t.instances.delete(paramsKey(inst.params));
|
|
546
|
+
for (const id of inst.ids)
|
|
547
|
+
removeFromIndex(this.instancesById, id, inst);
|
|
548
|
+
if (!keepTemplate && t.instances.size === 0 && !t.pinned) {
|
|
549
|
+
this._templates.delete(t.key);
|
|
550
|
+
this._unindexTemplate(t);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
_scheduleGc(inst) {
|
|
554
|
+
var _a, _b;
|
|
555
|
+
this._cancelGc(inst);
|
|
556
|
+
// `unref` so a pending grace period never keeps a Node process (or a test
|
|
557
|
+
// runner) alive.
|
|
558
|
+
const timer = setTimeout(() => {
|
|
559
|
+
inst.gcTimer = undefined;
|
|
560
|
+
if (inst.listeners.size > 0)
|
|
561
|
+
return;
|
|
562
|
+
this._dropInstance(inst);
|
|
563
|
+
}, this.options.gcMs);
|
|
564
|
+
(_b = (_a = timer).unref) === null || _b === void 0 ? void 0 : _b.call(_a);
|
|
565
|
+
inst.gcTimer = timer;
|
|
566
|
+
}
|
|
567
|
+
_cancelGc(inst) {
|
|
568
|
+
if (inst.gcTimer) {
|
|
569
|
+
clearTimeout(inst.gcTimer);
|
|
570
|
+
inst.gcTimer = undefined;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
// ---------------------------------------------------------------------------
|
|
575
|
+
// Handle
|
|
576
|
+
// ---------------------------------------------------------------------------
|
|
577
|
+
class LiveQueryHandle {
|
|
578
|
+
constructor(store, inst) {
|
|
579
|
+
this.store = store;
|
|
580
|
+
this._mine = new Set();
|
|
581
|
+
this._inst = inst;
|
|
582
|
+
}
|
|
583
|
+
/** The current instance, re-resolved if the store dropped the one this handle held. */
|
|
584
|
+
get inst() {
|
|
585
|
+
if (this._inst.dropped)
|
|
586
|
+
this._inst = this.store._resolve(this._inst);
|
|
587
|
+
return this._inst;
|
|
588
|
+
}
|
|
589
|
+
get state() {
|
|
590
|
+
return this.inst.state;
|
|
591
|
+
}
|
|
592
|
+
get key() {
|
|
593
|
+
return this.inst.key;
|
|
594
|
+
}
|
|
595
|
+
subscribe(listener) {
|
|
596
|
+
const l = listener;
|
|
597
|
+
const inst = this.inst;
|
|
598
|
+
this._mine.add(l);
|
|
599
|
+
this.store._attach(inst, l);
|
|
600
|
+
return () => {
|
|
601
|
+
if (this._mine.delete(l))
|
|
602
|
+
this.store._detach(inst, l);
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
refresh() {
|
|
606
|
+
return this.store.refresh(this.inst);
|
|
607
|
+
}
|
|
608
|
+
patch(partial) {
|
|
609
|
+
this.store.patch(this.inst, partial);
|
|
610
|
+
}
|
|
611
|
+
close() {
|
|
612
|
+
const inst = this._inst;
|
|
613
|
+
for (const l of this._mine)
|
|
614
|
+
this.store._detach(inst, l);
|
|
615
|
+
this._mine.clear();
|
|
616
|
+
}
|
|
617
|
+
then(onfulfilled, onrejected) {
|
|
618
|
+
return this._firstResult().then(onfulfilled, onrejected);
|
|
619
|
+
}
|
|
620
|
+
/** Resolves with the first successful data; the subscription it needs is released afterwards. */
|
|
621
|
+
_firstResult() {
|
|
622
|
+
const s = this.state;
|
|
623
|
+
if (s.status === 'success')
|
|
624
|
+
return Promise.resolve(s.data);
|
|
625
|
+
if (s.status === 'error')
|
|
626
|
+
return Promise.reject(s.error);
|
|
627
|
+
return new Promise((resolve, reject) => {
|
|
628
|
+
const off = this.subscribe((st) => {
|
|
629
|
+
if (st.status === 'success') {
|
|
630
|
+
off();
|
|
631
|
+
resolve(st.data);
|
|
632
|
+
}
|
|
633
|
+
else if (st.status === 'error') {
|
|
634
|
+
off();
|
|
635
|
+
reject(st.error);
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
// ---------------------------------------------------------------------------
|
|
642
|
+
// Helpers
|
|
643
|
+
// ---------------------------------------------------------------------------
|
|
644
|
+
function addToIndex(index, key, value) {
|
|
645
|
+
let set = index.get(key);
|
|
646
|
+
if (!set)
|
|
647
|
+
index.set(key, (set = new Set()));
|
|
648
|
+
set.add(value);
|
|
649
|
+
}
|
|
650
|
+
function removeFromIndex(index, key, value) {
|
|
651
|
+
const set = index.get(key);
|
|
652
|
+
if (!set)
|
|
653
|
+
return;
|
|
654
|
+
set.delete(value);
|
|
655
|
+
if (set.size === 0)
|
|
656
|
+
index.delete(key);
|
|
657
|
+
}
|
|
658
|
+
function isTemplate(v) {
|
|
659
|
+
return !!v && typeof v === 'object' && 'instances' in v && 'builder' in v;
|
|
660
|
+
}
|
|
661
|
+
/** Every string value under an `id` key, at any depth. */
|
|
662
|
+
export function collectIds(value, into) {
|
|
663
|
+
if (Array.isArray(value)) {
|
|
664
|
+
for (const v of value)
|
|
665
|
+
collectIds(v, into);
|
|
666
|
+
}
|
|
667
|
+
else if (value && typeof value === 'object' && !(value instanceof Date)) {
|
|
668
|
+
for (const [k, v] of Object.entries(value)) {
|
|
669
|
+
if (k === 'id' && typeof v === 'string')
|
|
670
|
+
into.add(v);
|
|
671
|
+
else if (v && typeof v === 'object')
|
|
672
|
+
collectIds(v, into);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
return into;
|
|
676
|
+
}
|
|
677
|
+
/** Deep equality over the plain-data results the store holds (objects, arrays, dates, primitives). */
|
|
678
|
+
export function deepEqual(a, b) {
|
|
679
|
+
if (a === b)
|
|
680
|
+
return true;
|
|
681
|
+
if (a instanceof Date || b instanceof Date) {
|
|
682
|
+
return a instanceof Date && b instanceof Date && a.getTime() === b.getTime();
|
|
683
|
+
}
|
|
684
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
685
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length)
|
|
686
|
+
return false;
|
|
687
|
+
return a.every((v, i) => deepEqual(v, b[i]));
|
|
688
|
+
}
|
|
689
|
+
if (a && b && typeof a === 'object' && typeof b === 'object') {
|
|
690
|
+
const ka = Object.keys(a);
|
|
691
|
+
const kb = Object.keys(b);
|
|
692
|
+
if (ka.length !== kb.length)
|
|
693
|
+
return false;
|
|
694
|
+
return ka.every((k) => deepEqual(a[k], b[k]));
|
|
695
|
+
}
|
|
696
|
+
return false;
|
|
697
|
+
}
|
|
698
|
+
// ---------------------------------------------------------------------------
|
|
699
|
+
// Registration
|
|
700
|
+
// ---------------------------------------------------------------------------
|
|
701
|
+
/** The runtime's live-query store (registered on `globalThis` when this module loads). */
|
|
702
|
+
export function getLiveQueryStore() {
|
|
703
|
+
let store = peekLiveQueryStore();
|
|
704
|
+
if (!store) {
|
|
705
|
+
store = new LiveQueryStore();
|
|
706
|
+
setLiveQueryStore(store);
|
|
707
|
+
}
|
|
708
|
+
return store;
|
|
709
|
+
}
|
|
710
|
+
/** Publish a change from application code (your own transport, a server push). */
|
|
711
|
+
export function publishChange(event) {
|
|
712
|
+
getLiveQueryStore().publish(event, 'app');
|
|
713
|
+
}
|
|
714
|
+
/** Refetch every live query that `target` can have affected. See {@link LiveQueryStore.invalidate}. */
|
|
715
|
+
export function invalidate(target) {
|
|
716
|
+
getLiveQueryStore().invalidate(target);
|
|
717
|
+
}
|
|
718
|
+
/** Replace the store with a fresh one (tests). */
|
|
719
|
+
export function resetLiveQueryStore() {
|
|
720
|
+
var _a;
|
|
721
|
+
(_a = peekLiveQueryStore()) === null || _a === void 0 ? void 0 : _a.dispose();
|
|
722
|
+
const store = new LiveQueryStore();
|
|
723
|
+
setLiveQueryStore(store);
|
|
724
|
+
return store;
|
|
725
|
+
}
|
|
726
|
+
// Register on load: importing this module is what makes `.live()` work.
|
|
727
|
+
if (!peekLiveQueryStore())
|
|
728
|
+
setLiveQueryStore(new LiveQueryStore());
|
|
729
|
+
//# sourceMappingURL=LiveQueryStore.js.map
|