@feltdb/core 0.6.8 → 0.6.10
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/cli/application.js +55 -88
- package/dist/cli/commands.js +206 -95
- package/dist/cli/index.js +1 -1
- package/dist/collection.d.ts +110 -2
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +228 -3
- package/dist/create/package-versions.js +1 -1
- package/dist/create/server-source/crates/feltdb/src/bin/feltdb_node.rs +408 -178
- package/dist/create/server-source/crates/feltdb/src/lib.rs +49 -5
- package/dist/create/server-source/crates/feltdb/src/multi_operation_transaction.rs +659 -0
- package/dist/create/server-source/crates/feltdb/src/p1_application_atomicity.rs +285 -0
- package/dist/create/server-source/crates/feltdb/src/p1_atomicity_acceptance.rs +582 -0
- package/dist/create/server-source/crates/feltdb-server/src/main.rs +141 -0
- package/dist/db.d.ts +59 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +94 -2
- package/dist/embedded-transaction.d.ts +77 -0
- package/dist/embedded-transaction.d.ts.map +1 -0
- package/dist/embedded-transaction.js +102 -0
- package/dist/feltdb.d.ts +49 -1
- package/dist/feltdb.d.ts.map +1 -1
- package/dist/file-db.d.ts +33 -0
- package/dist/file-db.d.ts.map +1 -1
- package/dist/file-db.js +128 -4
- package/dist/freshness.d.ts +119 -0
- package/dist/freshness.d.ts.map +1 -0
- package/dist/freshness.js +115 -0
- package/dist/http-db.d.ts +48 -0
- package/dist/http-db.d.ts.map +1 -1
- package/dist/http-db.js +77 -1
- package/dist/index-backend.d.ts +8 -0
- package/dist/index-backend.d.ts.map +1 -1
- package/dist/index-backend.js +13 -0
- package/dist/index-core.d.ts +78 -0
- package/dist/index-core.d.ts.map +1 -0
- package/dist/index-core.js +72 -0
- package/dist/index-manager.d.ts +11 -0
- package/dist/index-manager.d.ts.map +1 -1
- package/dist/index-manager.js +30 -13
- package/dist/index-types.d.ts +17 -0
- package/dist/index-types.d.ts.map +1 -1
- package/dist/index.browser.d.ts +14 -0
- package/dist/index.browser.d.ts.map +1 -0
- package/dist/index.browser.js +13 -0
- package/dist/index.d.ts +8 -59
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -54
- package/dist/indexeddb-db.d.ts +32 -0
- package/dist/indexeddb-db.d.ts.map +1 -1
- package/dist/indexeddb-db.js +151 -1
- package/dist/memory-db.d.ts +26 -0
- package/dist/memory-db.d.ts.map +1 -1
- package/dist/memory-db.js +86 -0
- package/dist/runtime-registry.d.ts +32 -0
- package/dist/runtime-registry.d.ts.map +1 -0
- package/dist/runtime-registry.js +38 -0
- package/dist/studio-app/assets/{feltdb_wasm-B4wq4mqp.js → feltdb_wasm-CJv3wHzi.js} +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-C8TG8r2n.wasm +0 -0
- package/dist/studio-app/assets/index-D3rT0SJi.js +28 -0
- package/dist/studio-app/index.html +1 -1
- package/dist/transaction.d.ts +98 -0
- package/dist/transaction.d.ts.map +1 -0
- package/dist/transaction.js +86 -0
- package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
- package/dist/workspace/browser.d.ts +22 -0
- package/dist/workspace/browser.d.ts.map +1 -0
- package/dist/workspace/browser.js +17 -0
- package/dist/workspace/git-identity.d.ts +35 -0
- package/dist/workspace/git-identity.d.ts.map +1 -0
- package/dist/workspace/git-identity.js +81 -0
- package/dist/workspace/index.d.ts +11 -1
- package/dist/workspace/index.d.ts.map +1 -1
- package/dist/workspace/index.js +6 -0
- package/dist/workspace/investigation-analysis.d.ts +47 -0
- package/dist/workspace/investigation-analysis.d.ts.map +1 -0
- package/dist/workspace/investigation-analysis.js +181 -0
- package/dist/workspace/investigation-lifecycle-manager.d.ts +86 -0
- package/dist/workspace/investigation-lifecycle-manager.d.ts.map +1 -0
- package/dist/workspace/investigation-lifecycle-manager.js +229 -0
- package/dist/workspace/investigation-supervisor.d.ts +100 -0
- package/dist/workspace/investigation-supervisor.d.ts.map +1 -0
- package/dist/workspace/investigation-supervisor.js +236 -0
- package/dist/workspace/runtime-investigation.d.ts +1 -1
- package/dist/workspace/runtime-investigation.d.ts.map +1 -1
- package/dist/workspace/runtime-investigation.js +46 -21
- package/dist/workspace/runtime-observation.d.ts +79 -0
- package/dist/workspace/runtime-observation.d.ts.map +1 -0
- package/dist/workspace/runtime-observation.js +152 -0
- package/dist/workspace/runtime-observer.d.ts +51 -0
- package/dist/workspace/runtime-observer.d.ts.map +1 -0
- package/dist/workspace/runtime-observer.js +268 -0
- package/dist/workspace/workspace-connection.d.ts.map +1 -1
- package/dist/workspace/workspace-connection.js +14 -5
- package/dist/workspace/workspace-types.d.ts +113 -0
- package/dist/workspace/workspace-types.d.ts.map +1 -1
- package/package.json +12 -4
- package/dist/create/server-source/crates/feltdb/src/phase_1c_three_node.rs +0 -523
- package/dist/studio-app/assets/feltdb_wasm_bg-Ceyi7l21.wasm +0 -0
- package/dist/studio-app/assets/index-BF45M757.js +0 -28
package/dist/collection.d.ts
CHANGED
|
@@ -8,9 +8,27 @@
|
|
|
8
8
|
* for efficient, real-time state propagation.
|
|
9
9
|
*/
|
|
10
10
|
import type { JsDb } from './feltdb.js';
|
|
11
|
-
import type
|
|
11
|
+
import { type FreshnessCapability, type Revision } from './freshness.js';
|
|
12
|
+
import type { IndexConfig, CollectionQueryPlan } from './index-types.js';
|
|
12
13
|
export type Predicate<T> = (item: T) => boolean;
|
|
13
14
|
export type Subscriber<T> = (items: T[]) => void;
|
|
15
|
+
/**
|
|
16
|
+
* How a refresh decided whether the local cache could be reused.
|
|
17
|
+
*
|
|
18
|
+
* Reporting only, and deliberately explicit about *why* the runtime was
|
|
19
|
+
* queried, so a benchmark or a caller can tell a validated cache hit from a
|
|
20
|
+
* fallback that happens to be fast.
|
|
21
|
+
*/
|
|
22
|
+
export interface FreshnessCheck {
|
|
23
|
+
/** `revision` when a revision decided it; `refresh` when the runtime was asked for the data. */
|
|
24
|
+
validation: 'revision' | 'refresh';
|
|
25
|
+
/** True only when the runtime was not queried for the collection at all. */
|
|
26
|
+
servedFromCache: boolean;
|
|
27
|
+
/** The revision the cache now corresponds to, when there is one. */
|
|
28
|
+
revision?: Revision;
|
|
29
|
+
/** Why a revision was not used. */
|
|
30
|
+
reason?: string;
|
|
31
|
+
}
|
|
14
32
|
/**
|
|
15
33
|
* Result of an atomic version-checked update operation.
|
|
16
34
|
* Represents either a successful commit or a version conflict.
|
|
@@ -49,6 +67,26 @@ export declare class Collection<T> {
|
|
|
49
67
|
private indexBackend;
|
|
50
68
|
private indexStore;
|
|
51
69
|
private indexesLoaded;
|
|
70
|
+
/** Incremented whenever `cache` is replaced, so the index can tell it is stale. */
|
|
71
|
+
private cacheGeneration;
|
|
72
|
+
/** The cache generation the index entries currently reflect, or -1 for none. */
|
|
73
|
+
private indexedGeneration;
|
|
74
|
+
private lastPlan;
|
|
75
|
+
/** Cache records addressed by id, valid for `indexedGeneration`. */
|
|
76
|
+
private recordsById;
|
|
77
|
+
/** Raw payload the cache was parsed from, used to detect that nothing changed. */
|
|
78
|
+
private lastRawSnapshot;
|
|
79
|
+
/**
|
|
80
|
+
* The runtime revision the cache was built from, when the runtime offers one.
|
|
81
|
+
*
|
|
82
|
+
* Captured *before* the query that produced the cache, never after. A write
|
|
83
|
+
* landing between the two then makes this older than the data it labels,
|
|
84
|
+
* which costs one unnecessary refresh. Capturing it after would make it
|
|
85
|
+
* newer than the data, and the cache would look current forever.
|
|
86
|
+
*/
|
|
87
|
+
private cachedRevision;
|
|
88
|
+
/** How the last refresh decided the cache was usable. Reporting only. */
|
|
89
|
+
private lastFreshness;
|
|
52
90
|
constructor(db: JsDb, name: string, predicate?: Predicate<T>, parent?: Collection<T>, loadIndexes?: boolean);
|
|
53
91
|
/**
|
|
54
92
|
* Load indexes that were persisted from a previous session.
|
|
@@ -59,8 +97,46 @@ export declare class Collection<T> {
|
|
|
59
97
|
* Returns cached results (live-updated).
|
|
60
98
|
*/
|
|
61
99
|
all(): Promise<T[]>;
|
|
62
|
-
/**
|
|
100
|
+
/**
|
|
101
|
+
* Find records whose fields match the supplied query.
|
|
102
|
+
*
|
|
103
|
+
* When a hash index covers one of the queried fields, the index supplies the
|
|
104
|
+
* candidate records and the remaining fields are checked on those candidates
|
|
105
|
+
* only. Otherwise every record is examined.
|
|
106
|
+
*
|
|
107
|
+
* The result is identical either way. The index is a projection of the same
|
|
108
|
+
* cached array a scan would walk, rebuilt whenever that array is replaced,
|
|
109
|
+
* so an indexed lookup cannot return a record a scan would miss or resolve
|
|
110
|
+
* an id the cache no longer holds.
|
|
111
|
+
*
|
|
112
|
+
* `lastQueryPlan()` reports which path ran.
|
|
113
|
+
*/
|
|
63
114
|
find(query?: Partial<T>): Promise<T[]>;
|
|
115
|
+
/**
|
|
116
|
+
* How the most recent `find` was answered.
|
|
117
|
+
*
|
|
118
|
+
* Exposed so an application — and the benchmark — can confirm that the index
|
|
119
|
+
* was actually consulted rather than inferring it from timing.
|
|
120
|
+
*/
|
|
121
|
+
lastQueryPlan(): CollectionQueryPlan | null;
|
|
122
|
+
/** A hash index covering one of the queried fields, if there is one. */
|
|
123
|
+
private indexFor;
|
|
124
|
+
/**
|
|
125
|
+
* Candidate records from an index, or null when the index cannot be trusted.
|
|
126
|
+
*
|
|
127
|
+
* Returning null makes the caller fall back to a scan, which is always
|
|
128
|
+
* correct. That is the safe direction: a wrong answer is far worse than a
|
|
129
|
+
* slow one.
|
|
130
|
+
*/
|
|
131
|
+
private indexCandidates;
|
|
132
|
+
/**
|
|
133
|
+
* Rebuild the index from the current cache when the cache has moved on.
|
|
134
|
+
*
|
|
135
|
+
* The index is only ever a projection of `this.cache`. Deriving it from the
|
|
136
|
+
* same array a scan walks is what makes the two paths agree by construction,
|
|
137
|
+
* rather than by hoping every write path remembered to update the index.
|
|
138
|
+
*/
|
|
139
|
+
private ensureIndexReflectsCache;
|
|
64
140
|
/**
|
|
65
141
|
* Create an index on this collection for faster queries.
|
|
66
142
|
* @example
|
|
@@ -159,6 +235,38 @@ export declare class Collection<T> {
|
|
|
159
235
|
subscribe(subscriber: Subscriber<T>, _pollInterval?: number): () => void;
|
|
160
236
|
/** Release runtime subscriptions owned by this live collection. */
|
|
161
237
|
close(): void;
|
|
238
|
+
/**
|
|
239
|
+
* What this collection's runtime can tell it about staleness.
|
|
240
|
+
*
|
|
241
|
+
* This is a *validation mechanism*, not a guarantee that the cache is fresh.
|
|
242
|
+
* `validation: 'revision'` means a cheap staleness check exists; it says
|
|
243
|
+
* nothing about whether this collection's cache currently passes it.
|
|
244
|
+
*
|
|
245
|
+
* @example
|
|
246
|
+
* const capability = await people.freshness();
|
|
247
|
+
* // 'revision' -> a cheap staleness check exists
|
|
248
|
+
* // 'refresh' -> re-read is the only honest answer, and `reason` says why
|
|
249
|
+
*/
|
|
250
|
+
freshness(): Promise<FreshnessCapability>;
|
|
251
|
+
/**
|
|
252
|
+
* How the last refresh decided the cache was usable.
|
|
253
|
+
*
|
|
254
|
+
* `servedFromCache` is true only when the runtime was never asked for this
|
|
255
|
+
* collection's data. Reporting only; it exists so a benchmark can tell a
|
|
256
|
+
* validated cache hit from a fallback that merely happened to be fast.
|
|
257
|
+
*/
|
|
258
|
+
lastFreshnessCheck(): FreshnessCheck | null;
|
|
259
|
+
/**
|
|
260
|
+
* Read the runtime's current revision, or explain why there isn't one.
|
|
261
|
+
*
|
|
262
|
+
* The capability is re-checked on every call rather than remembered, because
|
|
263
|
+
* a deployment can change underneath a live collection: switching on
|
|
264
|
+
* replication revokes revision authority, and a collection holding a cached
|
|
265
|
+
* capability would keep trusting a number that stopped meaning anything.
|
|
266
|
+
* Any refusal — no capability, a topology change, a transport failure —
|
|
267
|
+
* lands here as a reason, and the caller falls back to a full refresh.
|
|
268
|
+
*/
|
|
269
|
+
private currentRevision;
|
|
162
270
|
/**
|
|
163
271
|
* Refresh data from the database and notify subscribers.
|
|
164
272
|
*/
|
package/dist/collection.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"collection.d.ts","sourceRoot":"","sources":["../src/collection.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"collection.d.ts","sourceRoot":"","sources":["../src/collection.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAIL,KAAK,mBAAmB,EACxB,KAAK,QAAQ,EACd,MAAM,gBAAgB,CAAC;AAIxB,OAAO,KAAK,EAAE,WAAW,EAAc,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAErF,MAAM,MAAM,SAAS,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,KAAK,OAAO,CAAC;AAChD,MAAM,MAAM,UAAU,CAAC,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,EAAE,KAAK,IAAI,CAAC;AAEjD;;;;;;GAMG;AACH,MAAM,WAAW,cAAc;IAC7B,gGAAgG;IAChG,UAAU,EAAE,UAAU,GAAG,SAAS,CAAC;IACnC,4EAA4E;IAC5E,eAAe,EAAE,OAAO,CAAC;IACzB,oEAAoE;IACpE,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,mCAAmC;IACnC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;GAGG;AACH,MAAM,WAAW,qBAAqB,CAAC,CAAC;IACtC,mCAAmC;IACnC,OAAO,EAAE,OAAO,CAAC;IACjB,iEAAiE;IACjE,IAAI,CAAC,EAAE,CAAC,GAAG;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;IACjC,+DAA+D;IAC/D,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,wDAAwD;IACxD,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;;GAKG;AACH,qBAAa,UAAU,CAAC,CAAC;IACvB,OAAO,CAAC,EAAE,CAAO;IACjB,OAAO,CAAC,IAAI,CAAS;IACrB,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,SAAS,CAA6B;IAC9C,OAAO,CAAC,KAAK,CAAW;IACxB,OAAO,CAAC,WAAW,CAAiC;IACpD,OAAO,CAAC,MAAM,CAA8B;IAC5C,OAAO,CAAC,QAAQ,CAAuB;IACvC,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,oBAAoB,CAA8B;IAC1D,OAAO,CAAC,gBAAgB,CAA6B;IACrD,OAAO,CAAC,kBAAkB,CAA6B;IACvD,OAAO,CAAC,YAAY,CAAoC;IACxD,OAAO,CAAC,UAAU,CAAa;IAC/B,OAAO,CAAC,aAAa,CAAS;IAC9B,mFAAmF;IACnF,OAAO,CAAC,eAAe,CAAK;IAC5B,gFAAgF;IAChF,OAAO,CAAC,iBAAiB,CAAM;IAC/B,OAAO,CAAC,QAAQ,CAAoC;IACpD,oEAAoE;IACpE,OAAO,CAAC,WAAW,CAA+B;IAClD,kFAAkF;IAClF,OAAO,CAAC,eAAe,CAAuB;IAC9C;;;;;;;OAOG;IACH,OAAO,CAAC,cAAc,CAAyB;IAC/C,yEAAyE;IACzE,OAAO,CAAC,aAAa,CAA+B;gBAExC,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,EAAE,UAAU,CAAC,CAAC,CAAC,EAAE,WAAW,UAAO;IAoBxG;;OAEG;YACW,oBAAoB;IAalC;;;OAGG;IACG,GAAG,IAAI,OAAO,CAAC,CAAC,EAAE,CAAC;IAKzB;;;;;;;;;;;;;OAaG;IACG,IAAI,CAAC,KAAK,GAAE,OAAO,CAAC,CAAC,CAAM,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC;IAsChD;;;;;OAKG;IACH,aAAa,IAAI,mBAAmB,GAAG,IAAI;IAI3C,wEAAwE;IACxE,OAAO,CAAC,QAAQ;IAWhB;;;;;;OAMG;IACH,OAAO,CAAC,eAAe;IAiBvB;;;;;;OAMG;IACH,OAAO,CAAC,wBAAwB;IA0BhC;;;;;OAKG;IACH,WAAW,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI;IAWtC;;OAEG;IACH,WAAW,IAAI,WAAW,EAAE;IAI5B;;;OAGG;IACG,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC;IAYrC;;OAEG;IACG,eAAe,IAAI,OAAO,CAAC,OAAO,CAAC;IAazC;;OAEG;IACG,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;IAYjD;;;OAGG;IACH,KAAK,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC;IAO7C;;;OAGG;IACG,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IA4BrE;;OAEG;IACG,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IA8BrE;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA4BG;IACG,eAAe,CACnB,EAAE,EAAE,MAAM,GAAG,MAAM,EACnB,eAAe,EAAE,MAAM,EACvB,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EACnB,aAAa,CAAC,EAAE,MAAM,EACtB,eAAe,CAAC,EAAE,MAAM,EACxB,gBAAgB,CAAC,EAAE,OAAO,EAC1B,YAAY,CAAC,EAAE,CAAC,GACf,OAAO,CAAC,qBAAqB,CAAC,CAAC,CAAC,CAAC;IAmEpC;;OAEG;IACG,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA2BhD;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC;IAK9B;;OAEG;IACG,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAKnD;;;;;;OAMG;IACG,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC;QAAE,QAAQ,EAAE,OAAO,CAAC;QAAC,KAAK,EAAE,CAAC,CAAA;KAAE,CAAC;IAmClG;;;OAGG;IACH,SAAS,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC,CAAC,EAAE,aAAa,CAAC,EAAE,MAAM,GAAG,MAAM,IAAI;IA6CxE,mEAAmE;IACnE,KAAK,IAAI,IAAI;IASb;;;;;;;;;;;OAWG;IACH,SAAS,IAAI,OAAO,CAAC,mBAAmB,CAAC;IAIzC;;;;;;OAMG;IACH,kBAAkB,IAAI,cAAc,GAAG,IAAI;IAI3C;;;;;;;;;OASG;YACW,eAAe;IAQ7B;;OAEG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CA2E/B;AAED;;GAEG;AACH,qBAAa,YAAY,CAAC,MAAM,EAAE,KAAK;IACrC,OAAO,CAAC,QAAQ,CAAO;IACvB,OAAO,CAAC,OAAO,CAAO;IACtB,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,UAAU,CAAoC;gBAGpD,QAAQ,EAAE,IAAI,EACd,OAAO,EAAE,IAAI,EACb,eAAe,EAAE,MAAM,EACvB,UAAU,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,MAAM,GAAG,MAAM;IAQ/C;;OAEG;IACG,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC;CAoBxD"}
|
package/dist/collection.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* Collections now use reactive dependency graphs instead of polling
|
|
8
8
|
* for efficient, real-time state propagation.
|
|
9
9
|
*/
|
|
10
|
+
import { describeFreshness, isCacheCurrent, readRevision, } from './freshness.js';
|
|
10
11
|
import { getReactiveDependencyGraph } from './reactive-graph.js';
|
|
11
12
|
import { IndexBackend } from './index-backend.js';
|
|
12
13
|
import { IndexStore } from './index-store.js';
|
|
@@ -29,6 +30,26 @@ export class Collection {
|
|
|
29
30
|
this.runtimeUnsubscribe = null;
|
|
30
31
|
this.indexBackend = new IndexBackend();
|
|
31
32
|
this.indexesLoaded = false;
|
|
33
|
+
/** Incremented whenever `cache` is replaced, so the index can tell it is stale. */
|
|
34
|
+
this.cacheGeneration = 0;
|
|
35
|
+
/** The cache generation the index entries currently reflect, or -1 for none. */
|
|
36
|
+
this.indexedGeneration = -1;
|
|
37
|
+
this.lastPlan = null;
|
|
38
|
+
/** Cache records addressed by id, valid for `indexedGeneration`. */
|
|
39
|
+
this.recordsById = null;
|
|
40
|
+
/** Raw payload the cache was parsed from, used to detect that nothing changed. */
|
|
41
|
+
this.lastRawSnapshot = null;
|
|
42
|
+
/**
|
|
43
|
+
* The runtime revision the cache was built from, when the runtime offers one.
|
|
44
|
+
*
|
|
45
|
+
* Captured *before* the query that produced the cache, never after. A write
|
|
46
|
+
* landing between the two then makes this older than the data it labels,
|
|
47
|
+
* which costs one unnecessary refresh. Capturing it after would make it
|
|
48
|
+
* newer than the data, and the cache would look current forever.
|
|
49
|
+
*/
|
|
50
|
+
this.cachedRevision = null;
|
|
51
|
+
/** How the last refresh decided the cache was usable. Reporting only. */
|
|
52
|
+
this.lastFreshness = null;
|
|
32
53
|
this.db = db;
|
|
33
54
|
this.name = name;
|
|
34
55
|
this.collectionId = `${name}-${Math.random().toString(36).substr(2, 9)}`;
|
|
@@ -71,11 +92,132 @@ export class Collection {
|
|
|
71
92
|
await this.refresh();
|
|
72
93
|
return [...this.cache];
|
|
73
94
|
}
|
|
74
|
-
/**
|
|
95
|
+
/**
|
|
96
|
+
* Find records whose fields match the supplied query.
|
|
97
|
+
*
|
|
98
|
+
* When a hash index covers one of the queried fields, the index supplies the
|
|
99
|
+
* candidate records and the remaining fields are checked on those candidates
|
|
100
|
+
* only. Otherwise every record is examined.
|
|
101
|
+
*
|
|
102
|
+
* The result is identical either way. The index is a projection of the same
|
|
103
|
+
* cached array a scan would walk, rebuilt whenever that array is replaced,
|
|
104
|
+
* so an indexed lookup cannot return a record a scan would miss or resolve
|
|
105
|
+
* an id the cache no longer holds.
|
|
106
|
+
*
|
|
107
|
+
* `lastQueryPlan()` reports which path ran.
|
|
108
|
+
*/
|
|
75
109
|
async find(query = {}) {
|
|
76
110
|
const items = await this.all();
|
|
77
111
|
const entries = Object.entries(query);
|
|
78
|
-
|
|
112
|
+
if (entries.length === 0) {
|
|
113
|
+
this.lastPlan = { strategy: 'scan', reason: 'no query fields', candidatesExamined: items.length, recordsReturned: items.length };
|
|
114
|
+
return items;
|
|
115
|
+
}
|
|
116
|
+
const matches = (item) => entries.every(([field, expected]) => item[field] === expected);
|
|
117
|
+
const usable = this.indexFor(entries);
|
|
118
|
+
if (usable) {
|
|
119
|
+
const candidates = this.indexCandidates(usable.indexName, usable.field, usable.value);
|
|
120
|
+
if (candidates) {
|
|
121
|
+
const found = candidates.filter(matches);
|
|
122
|
+
this.lastPlan = {
|
|
123
|
+
strategy: 'index',
|
|
124
|
+
indexName: usable.indexName,
|
|
125
|
+
field: usable.field,
|
|
126
|
+
candidatesExamined: candidates.length,
|
|
127
|
+
recordsReturned: found.length,
|
|
128
|
+
};
|
|
129
|
+
return found;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const found = items.filter(matches);
|
|
133
|
+
this.lastPlan = {
|
|
134
|
+
strategy: 'scan',
|
|
135
|
+
reason: usable ? 'index could not be trusted for this snapshot' : 'no index covers the queried fields',
|
|
136
|
+
candidatesExamined: items.length,
|
|
137
|
+
recordsReturned: found.length,
|
|
138
|
+
};
|
|
139
|
+
return found;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* How the most recent `find` was answered.
|
|
143
|
+
*
|
|
144
|
+
* Exposed so an application — and the benchmark — can confirm that the index
|
|
145
|
+
* was actually consulted rather than inferring it from timing.
|
|
146
|
+
*/
|
|
147
|
+
lastQueryPlan() {
|
|
148
|
+
return this.lastPlan;
|
|
149
|
+
}
|
|
150
|
+
/** A hash index covering one of the queried fields, if there is one. */
|
|
151
|
+
indexFor(entries) {
|
|
152
|
+
for (const config of this.indexBackend.listIndexes()) {
|
|
153
|
+
// Only equality on a single-field hash index is planned today. Sorted,
|
|
154
|
+
// compound, and text indexes are maintained but not yet used for reads.
|
|
155
|
+
if (config.type !== 'hash' || !config.field)
|
|
156
|
+
continue;
|
|
157
|
+
const entry = entries.find(([field]) => field === config.field);
|
|
158
|
+
if (entry)
|
|
159
|
+
return { indexName: config.name, field: config.field, value: entry[1] };
|
|
160
|
+
}
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Candidate records from an index, or null when the index cannot be trusted.
|
|
165
|
+
*
|
|
166
|
+
* Returning null makes the caller fall back to a scan, which is always
|
|
167
|
+
* correct. That is the safe direction: a wrong answer is far worse than a
|
|
168
|
+
* slow one.
|
|
169
|
+
*/
|
|
170
|
+
indexCandidates(indexName, field, value) {
|
|
171
|
+
if (!this.ensureIndexReflectsCache())
|
|
172
|
+
return null;
|
|
173
|
+
let ids;
|
|
174
|
+
try {
|
|
175
|
+
ids = this.indexBackend.queryIndex(indexName, { field, operator: 'eq', value });
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
const records = [];
|
|
181
|
+
for (const id of ids) {
|
|
182
|
+
const record = this.recordsById?.get(id);
|
|
183
|
+
if (record === undefined)
|
|
184
|
+
return null; // the index disagrees with the cache
|
|
185
|
+
records.push(record);
|
|
186
|
+
}
|
|
187
|
+
return records;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Rebuild the index from the current cache when the cache has moved on.
|
|
191
|
+
*
|
|
192
|
+
* The index is only ever a projection of `this.cache`. Deriving it from the
|
|
193
|
+
* same array a scan walks is what makes the two paths agree by construction,
|
|
194
|
+
* rather than by hoping every write path remembered to update the index.
|
|
195
|
+
*/
|
|
196
|
+
ensureIndexReflectsCache() {
|
|
197
|
+
if (this.indexedGeneration === this.cacheGeneration)
|
|
198
|
+
return this.recordsById !== null;
|
|
199
|
+
if (!this.indexBackend.resetEntries()) {
|
|
200
|
+
this.recordsById = null;
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
const byId = new Map();
|
|
204
|
+
for (const item of this.cache) {
|
|
205
|
+
const recordId = item?.id;
|
|
206
|
+
// A record without a stable id cannot be resolved from an index result,
|
|
207
|
+
// so the whole snapshot is treated as unindexable rather than partially
|
|
208
|
+
// indexed.
|
|
209
|
+
if (recordId === undefined || recordId === null) {
|
|
210
|
+
this.recordsById = null;
|
|
211
|
+
this.indexedGeneration = this.cacheGeneration;
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
const key = String(recordId);
|
|
215
|
+
byId.set(key, item);
|
|
216
|
+
this.indexBackend.updateRecord(key, null, item);
|
|
217
|
+
}
|
|
218
|
+
this.recordsById = byId;
|
|
219
|
+
this.indexedGeneration = this.cacheGeneration;
|
|
220
|
+
return true;
|
|
79
221
|
}
|
|
80
222
|
/**
|
|
81
223
|
* Create an index on this collection for faster queries.
|
|
@@ -424,13 +566,93 @@ export class Collection {
|
|
|
424
566
|
unsubscribe();
|
|
425
567
|
this.unsubscribeFunctions.clear();
|
|
426
568
|
}
|
|
569
|
+
/**
|
|
570
|
+
* What this collection's runtime can tell it about staleness.
|
|
571
|
+
*
|
|
572
|
+
* This is a *validation mechanism*, not a guarantee that the cache is fresh.
|
|
573
|
+
* `validation: 'revision'` means a cheap staleness check exists; it says
|
|
574
|
+
* nothing about whether this collection's cache currently passes it.
|
|
575
|
+
*
|
|
576
|
+
* @example
|
|
577
|
+
* const capability = await people.freshness();
|
|
578
|
+
* // 'revision' -> a cheap staleness check exists
|
|
579
|
+
* // 'refresh' -> re-read is the only honest answer, and `reason` says why
|
|
580
|
+
*/
|
|
581
|
+
freshness() {
|
|
582
|
+
return describeFreshness(this.db);
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* How the last refresh decided the cache was usable.
|
|
586
|
+
*
|
|
587
|
+
* `servedFromCache` is true only when the runtime was never asked for this
|
|
588
|
+
* collection's data. Reporting only; it exists so a benchmark can tell a
|
|
589
|
+
* validated cache hit from a fallback that merely happened to be fast.
|
|
590
|
+
*/
|
|
591
|
+
lastFreshnessCheck() {
|
|
592
|
+
return this.lastFreshness;
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Read the runtime's current revision, or explain why there isn't one.
|
|
596
|
+
*
|
|
597
|
+
* The capability is re-checked on every call rather than remembered, because
|
|
598
|
+
* a deployment can change underneath a live collection: switching on
|
|
599
|
+
* replication revokes revision authority, and a collection holding a cached
|
|
600
|
+
* capability would keep trusting a number that stopped meaning anything.
|
|
601
|
+
* Any refusal — no capability, a topology change, a transport failure —
|
|
602
|
+
* lands here as a reason, and the caller falls back to a full refresh.
|
|
603
|
+
*/
|
|
604
|
+
async currentRevision() {
|
|
605
|
+
try {
|
|
606
|
+
return { revision: await readRevision(this.db) };
|
|
607
|
+
}
|
|
608
|
+
catch (error) {
|
|
609
|
+
return { reason: error instanceof Error ? error.message : String(error) };
|
|
610
|
+
}
|
|
611
|
+
}
|
|
427
612
|
/**
|
|
428
613
|
* Refresh data from the database and notify subscribers.
|
|
429
614
|
*/
|
|
430
615
|
async refresh() {
|
|
431
616
|
try {
|
|
617
|
+
// The revision is read *before* the query, so it can only ever be older
|
|
618
|
+
// than the data it ends up labelling. The reverse ordering would let a
|
|
619
|
+
// write that lands between the query and the revision read produce a
|
|
620
|
+
// cache tagged with a revision it does not contain, and that cache would
|
|
621
|
+
// then validate as current forever.
|
|
622
|
+
const observed = await this.currentRevision();
|
|
623
|
+
if ('revision' in observed) {
|
|
624
|
+
if (this.isInitialized && this.cachedRevision
|
|
625
|
+
&& isCacheCurrent(this.cachedRevision, observed.revision)) {
|
|
626
|
+
// The runtime is not queried at all. This is the only path in this
|
|
627
|
+
// method that skips the round trip, and it is reachable only when
|
|
628
|
+
// the runtime advertised an authoritative revision for its domain.
|
|
629
|
+
this.lastFreshness = { validation: 'revision', servedFromCache: true, revision: observed.revision };
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
else {
|
|
634
|
+
// No usable revision: the existing behaviour, unchanged.
|
|
635
|
+
this.cachedRevision = null;
|
|
636
|
+
}
|
|
432
637
|
const result = await this.db.query(this.name);
|
|
638
|
+
this.lastFreshness = 'revision' in observed
|
|
639
|
+
? { validation: 'revision', servedFromCache: false, revision: observed.revision }
|
|
640
|
+
: { validation: 'refresh', servedFromCache: false, reason: observed.reason };
|
|
433
641
|
if (result.success && result.data) {
|
|
642
|
+
// Record the revision only once the data it labels is actually in
|
|
643
|
+
// hand. A failed query must not leave the cache tagged as current.
|
|
644
|
+
this.cachedRevision = 'revision' in observed ? observed.revision : null;
|
|
645
|
+
// The runtime returned exactly what it returned last time, so the data
|
|
646
|
+
// has not changed. Re-parsing it would produce an identical array,
|
|
647
|
+
// discard the cache, and invalidate any index derived from it — paying
|
|
648
|
+
// twice to learn nothing.
|
|
649
|
+
//
|
|
650
|
+
// This remains useful for runtimes with no revision, and as a second
|
|
651
|
+
// line for those that have one: it is what keeps the index valid when
|
|
652
|
+
// some *other* collection's write moved a store-wide revision.
|
|
653
|
+
if (this.isInitialized && result.data === this.lastRawSnapshot) {
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
434
656
|
try {
|
|
435
657
|
let items = JSON.parse(result.data);
|
|
436
658
|
// Apply predicate if this is a derived collection
|
|
@@ -444,8 +666,11 @@ export class Collection {
|
|
|
444
666
|
}
|
|
445
667
|
});
|
|
446
668
|
}
|
|
447
|
-
// Update cache with all results
|
|
669
|
+
// Update cache with all results. The index is a projection of this
|
|
670
|
+
// array, so replacing it invalidates the index entries.
|
|
448
671
|
this.cache = items;
|
|
672
|
+
this.cacheGeneration += 1;
|
|
673
|
+
this.lastRawSnapshot = result.data;
|
|
449
674
|
this.isInitialized = true;
|
|
450
675
|
}
|
|
451
676
|
catch (err) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
// One release train keeps generated applications installable. The repository
|
|
2
2
|
// validation script checks these values against every workspace manifest.
|
|
3
|
-
export const FELTDB_PACKAGE_VERSION = '0.6.
|
|
3
|
+
export const FELTDB_PACKAGE_VERSION = '0.6.10';
|
|
4
4
|
export const feltdbPackageRange = `^${FELTDB_PACKAGE_VERSION}`;
|