@_linked/core 2.26.0 → 2.28.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.
Files changed (55) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +33 -0
  3. package/lib/esm/index.d.ts +9 -0
  4. package/lib/esm/index.js +8 -0
  5. package/lib/esm/index.js.map +1 -1
  6. package/lib/esm/interfaces/IDataset.d.ts +15 -0
  7. package/lib/esm/interfaces/IDataset.js.map +1 -1
  8. package/lib/esm/interfaces/IFileStore.d.ts +6 -0
  9. package/lib/esm/interfaces/IFileStore.js.map +1 -1
  10. package/lib/esm/live/LiveQueryStore.d.ts +240 -0
  11. package/lib/esm/live/LiveQueryStore.js +729 -0
  12. package/lib/esm/live/LiveQueryStore.js.map +1 -0
  13. package/lib/esm/live/changes.d.ts +30 -0
  14. package/lib/esm/live/changes.js +30 -0
  15. package/lib/esm/live/changes.js.map +1 -0
  16. package/lib/esm/live/keys.d.ts +54 -0
  17. package/lib/esm/live/keys.js +112 -0
  18. package/lib/esm/live/keys.js.map +1 -0
  19. package/lib/esm/live/matcher.d.ts +43 -0
  20. package/lib/esm/live/matcher.js +115 -0
  21. package/lib/esm/live/matcher.js.map +1 -0
  22. package/lib/esm/live/registry.d.ts +24 -0
  23. package/lib/esm/live/registry.js +41 -0
  24. package/lib/esm/live/registry.js.map +1 -0
  25. package/lib/esm/live.d.ts +12 -0
  26. package/lib/esm/live.js +15 -0
  27. package/lib/esm/live.js.map +1 -0
  28. package/lib/esm/queries/AskBuilder.d.ts +10 -0
  29. package/lib/esm/queries/AskBuilder.js +14 -0
  30. package/lib/esm/queries/AskBuilder.js.map +1 -1
  31. package/lib/esm/queries/CountBuilder.d.ts +10 -0
  32. package/lib/esm/queries/CountBuilder.js +14 -0
  33. package/lib/esm/queries/CountBuilder.js.map +1 -1
  34. package/lib/esm/queries/QueryBuilder.d.ts +10 -0
  35. package/lib/esm/queries/QueryBuilder.js +14 -0
  36. package/lib/esm/queries/QueryBuilder.js.map +1 -1
  37. package/lib/esm/queries/QueryContext.js +9 -2
  38. package/lib/esm/queries/QueryContext.js.map +1 -1
  39. package/lib/esm/queries/SelectQuery.d.ts +8 -8
  40. package/lib/esm/queries/SelectQuery.js.map +1 -1
  41. package/lib/esm/queries/queryDependencies.d.ts +58 -0
  42. package/lib/esm/queries/queryDependencies.js +494 -0
  43. package/lib/esm/queries/queryDependencies.js.map +1 -0
  44. package/lib/esm/queries/queryDispatch.d.ts +46 -0
  45. package/lib/esm/queries/queryDispatch.js +88 -3
  46. package/lib/esm/queries/queryDispatch.js.map +1 -1
  47. package/lib/esm/shapes/nodeShapeData.d.ts +16 -0
  48. package/lib/esm/shapes/nodeShapeData.js +41 -1
  49. package/lib/esm/shapes/nodeShapeData.js.map +1 -1
  50. package/lib/esm/sparql/irToAlgebra.js +1 -32
  51. package/lib/esm/sparql/irToAlgebra.js.map +1 -1
  52. package/lib/esm/utils/LinkedStorage.d.ts +6 -0
  53. package/lib/esm/utils/LinkedStorage.js +32 -6
  54. package/lib/esm/utils/LinkedStorage.js.map +1 -1
  55. package/package.json +6 -1
@@ -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