@feltdb/core 0.6.9 → 0.6.11
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-lifecycle.js +112 -0
- package/dist/cli/application.js +10 -0
- package/dist/cli/commands.js +407 -119
- package/dist/cli/development-session.js +54 -0
- package/dist/cli/index.js +1 -1
- package/dist/cli/workspace-integration.js +146 -20
- package/dist/collection.d.ts +110 -2
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +228 -3
- package/dist/create/cli.js +4 -2
- package/dist/create/create.js +42 -6
- package/dist/create/frameworks.js +18 -0
- 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/create/workspace-initialization.js +4 -5
- package/dist/db.d.ts +63 -1
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +145 -4
- package/dist/development-runtime-bridge.d.ts +60 -0
- package/dist/development-runtime-bridge.d.ts.map +1 -0
- package/dist/development-runtime-bridge.js +171 -0
- 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 +80 -0
- package/dist/index-core.d.ts.map +1 -0
- package/dist/index-core.js +74 -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.d.ts +2 -1
- package/dist/studio/app.d.ts.map +1 -1
- package/dist/studio/components/StateExplorer.d.ts +2 -1
- package/dist/studio/components/StateExplorer.d.ts.map +1 -1
- package/dist/studio/components/index.js +1 -1
- package/dist/studio/{components-9kDSWiGL.js → components-C5p2TfIU.js} +25 -14
- package/dist/studio/index.js +234 -160
- 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-DospFFYE.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/development-node.d.ts +16 -0
- package/dist/workspace/development-node.d.ts.map +1 -1
- package/dist/workspace/development-node.js +85 -1
- 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 +12 -2
- package/dist/workspace/index.d.ts.map +1 -1
- package/dist/workspace/index.js +7 -1
- 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 +88 -0
- package/dist/workspace/investigation-lifecycle-manager.d.ts.map +1 -0
- package/dist/workspace/investigation-lifecycle-manager.js +259 -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 +238 -0
- package/dist/workspace/runtime-investigation.d.ts +5 -1
- package/dist/workspace/runtime-investigation.d.ts.map +1 -1
- package/dist/workspace/runtime-investigation.js +50 -21
- package/dist/workspace/runtime-observation.d.ts +82 -0
- package/dist/workspace/runtime-observation.d.ts.map +1 -0
- package/dist/workspace/runtime-observation.js +170 -0
- package/dist/workspace/runtime-observer.d.ts +57 -0
- package/dist/workspace/runtime-observer.d.ts.map +1 -0
- package/dist/workspace/runtime-observer.js +272 -0
- package/dist/workspace/workspace-connection.d.ts.map +1 -1
- package/dist/workspace/workspace-connection.js +14 -5
- package/dist/workspace/workspace-identity.d.ts +1 -1
- package/dist/workspace/workspace-identity.d.ts.map +1 -1
- package/dist/workspace/workspace-identity.js +4 -4
- package/dist/workspace/workspace-types.d.ts +208 -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/file-db.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { validateOperationAdmissionInput, operationsConflict, generateOperationId, validateTransitionInput, isValidTransition, isTerminalStatus, isIdempotentTerminalTransition, } from './operation-admission.js';
|
|
2
2
|
import * as nodeFs from 'fs';
|
|
3
3
|
import * as nodePath from 'path';
|
|
4
|
+
import { decideTransaction, embeddedResult, EmbeddedTransactionRejected, } from './embedded-transaction.js';
|
|
4
5
|
// Synchronous versions for sync API (will throw if called in browser)
|
|
5
6
|
function getFsSync() {
|
|
6
7
|
if (typeof nodeFs.existsSync === 'function')
|
|
@@ -16,6 +17,7 @@ export class FileJsDb {
|
|
|
16
17
|
constructor(path) {
|
|
17
18
|
this.peers = new Set();
|
|
18
19
|
this.lockFd = null;
|
|
20
|
+
this.applied = new Set();
|
|
19
21
|
const nodePath = getPathSync();
|
|
20
22
|
const nodeFs = getFsSync();
|
|
21
23
|
// Normalize and resolve the path to prevent directory traversal
|
|
@@ -38,6 +40,9 @@ export class FileJsDb {
|
|
|
38
40
|
// Load or initialize state
|
|
39
41
|
this.state = this.loadState();
|
|
40
42
|
this.restorePeers();
|
|
43
|
+
for (const transactionId of this.state.applied_transactions ?? []) {
|
|
44
|
+
this.applied.add(transactionId);
|
|
45
|
+
}
|
|
41
46
|
}
|
|
42
47
|
loadState() {
|
|
43
48
|
const nodeFs = getFsSync();
|
|
@@ -58,7 +63,8 @@ export class FileJsDb {
|
|
|
58
63
|
rows: snapshot.rows,
|
|
59
64
|
events: snapshot.events,
|
|
60
65
|
peers: snapshot.meta.peers ?? [],
|
|
61
|
-
sequences: snapshot.meta.sequences ?? {}
|
|
66
|
+
sequences: snapshot.meta.sequences ?? {},
|
|
67
|
+
applied_transactions: snapshot.applied_transactions ?? []
|
|
62
68
|
};
|
|
63
69
|
}
|
|
64
70
|
catch (error) {
|
|
@@ -76,7 +82,8 @@ export class FileJsDb {
|
|
|
76
82
|
rows: {},
|
|
77
83
|
events: [],
|
|
78
84
|
peers: [],
|
|
79
|
-
sequences: {}
|
|
85
|
+
sequences: {},
|
|
86
|
+
applied_transactions: []
|
|
80
87
|
};
|
|
81
88
|
}
|
|
82
89
|
restorePeers() {
|
|
@@ -155,6 +162,7 @@ export class FileJsDb {
|
|
|
155
162
|
}
|
|
156
163
|
saveState() {
|
|
157
164
|
const nodeFs = getFsSync();
|
|
165
|
+
const nodePathModule = getPathSync();
|
|
158
166
|
try {
|
|
159
167
|
// Update peers in state
|
|
160
168
|
this.state.peers = [...this.peers];
|
|
@@ -169,14 +177,39 @@ export class FileJsDb {
|
|
|
169
177
|
sequences: this.state.sequences
|
|
170
178
|
},
|
|
171
179
|
rows: this.state.rows,
|
|
172
|
-
events: this.state.events
|
|
180
|
+
events: this.state.events,
|
|
181
|
+
applied_transactions: [...this.applied]
|
|
173
182
|
};
|
|
174
183
|
const content = JSON.stringify(snapshot, null, 2);
|
|
175
184
|
// Write to temporary file
|
|
176
185
|
const tempPath = this.statePath + '.tmp';
|
|
177
|
-
nodeFs.
|
|
186
|
+
const handle = nodeFs.openSync(tempPath, 'w');
|
|
187
|
+
try {
|
|
188
|
+
nodeFs.writeFileSync(handle, content, 'utf-8');
|
|
189
|
+
// Force the contents to disk before the rename publishes them.
|
|
190
|
+
// A rename is a metadata operation: without this, a crash can leave
|
|
191
|
+
// the new name pointing at a file whose contents never landed.
|
|
192
|
+
nodeFs.fsyncSync(handle);
|
|
193
|
+
}
|
|
194
|
+
finally {
|
|
195
|
+
nodeFs.closeSync(handle);
|
|
196
|
+
}
|
|
178
197
|
// Atomic rename commits the new state
|
|
179
198
|
nodeFs.renameSync(tempPath, this.statePath);
|
|
199
|
+
// Persist the rename itself, so the commit survives power loss.
|
|
200
|
+
try {
|
|
201
|
+
const directory = nodeFs.openSync(nodePathModule.dirname(this.statePath), 'r');
|
|
202
|
+
try {
|
|
203
|
+
nodeFs.fsyncSync(directory);
|
|
204
|
+
}
|
|
205
|
+
finally {
|
|
206
|
+
nodeFs.closeSync(directory);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
// Directory fsync is not available on every platform. The rename is
|
|
211
|
+
// still atomic; only the durability of the rename itself is weaker.
|
|
212
|
+
}
|
|
180
213
|
}
|
|
181
214
|
catch (error) {
|
|
182
215
|
throw new Error(`Failed to save FeltDB state: ${error}`);
|
|
@@ -196,6 +229,71 @@ export class FileJsDb {
|
|
|
196
229
|
id: `${this.origin}:${timestamp}:${this.state.sequence}`
|
|
197
230
|
});
|
|
198
231
|
}
|
|
232
|
+
/**
|
|
233
|
+
* Commit several operations atomically and durably.
|
|
234
|
+
*
|
|
235
|
+
* The snapshot is published by writing a temporary file, fsyncing it, and
|
|
236
|
+
* renaming it over the previous one. The rename is atomic, so a reader sees
|
|
237
|
+
* either the whole transaction or none of it, and a crash at any point
|
|
238
|
+
* leaves one of those two states on disk — never a mixture.
|
|
239
|
+
*
|
|
240
|
+
* Order matters here. The next state is built and persisted *before* it is
|
|
241
|
+
* adopted in memory, so a failed write cannot leave memory ahead of disk.
|
|
242
|
+
*/
|
|
243
|
+
async commit_transaction(request) {
|
|
244
|
+
const before = this.state.sequence;
|
|
245
|
+
const plan = decideTransaction(request, {
|
|
246
|
+
exists: key => key in this.state.rows,
|
|
247
|
+
hasApplied: transactionId => this.applied.has(transactionId),
|
|
248
|
+
});
|
|
249
|
+
if (plan.kind === 'duplicate') {
|
|
250
|
+
return embeddedResult(request, true, before, this.state.sequence);
|
|
251
|
+
}
|
|
252
|
+
if (plan.kind === 'reject') {
|
|
253
|
+
throw new EmbeddedTransactionRejected(plan.reason);
|
|
254
|
+
}
|
|
255
|
+
// Build the post-transaction state without touching the live one.
|
|
256
|
+
const rows = { ...this.state.rows };
|
|
257
|
+
const events = [...this.state.events];
|
|
258
|
+
let sequence = this.state.sequence;
|
|
259
|
+
const timestamp = Date.now();
|
|
260
|
+
for (const mutation of plan.mutations) {
|
|
261
|
+
sequence += 1;
|
|
262
|
+
if (mutation.value === undefined) {
|
|
263
|
+
delete rows[mutation.key];
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
rows[mutation.key] = mutation.value;
|
|
267
|
+
}
|
|
268
|
+
events.push({
|
|
269
|
+
sequence,
|
|
270
|
+
collection: mutation.collection,
|
|
271
|
+
key: mutation.key,
|
|
272
|
+
type: mutation.value === undefined ? 'delete' : 'put',
|
|
273
|
+
value: mutation.value,
|
|
274
|
+
timestamp,
|
|
275
|
+
origin: this.origin,
|
|
276
|
+
id: `${this.origin}:${timestamp}:${sequence}`,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
const previous = this.state;
|
|
280
|
+
this.state = { ...this.state, rows, events, sequence };
|
|
281
|
+
// The transaction id has to be part of the snapshot being written, not
|
|
282
|
+
// added after it. Recording it afterwards would leave a committed
|
|
283
|
+
// transaction whose identity did not survive a restart, and redelivery
|
|
284
|
+
// would then apply it a second time.
|
|
285
|
+
this.applied.add(request.transactionId);
|
|
286
|
+
try {
|
|
287
|
+
this.saveState();
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
// The commit did not become durable, so it must not be visible either.
|
|
291
|
+
this.state = previous;
|
|
292
|
+
this.applied.delete(request.transactionId);
|
|
293
|
+
throw error;
|
|
294
|
+
}
|
|
295
|
+
return embeddedResult(request, false, before, sequence);
|
|
296
|
+
}
|
|
199
297
|
insert(key, value) {
|
|
200
298
|
try {
|
|
201
299
|
const parsed = JSON.parse(value);
|
|
@@ -283,6 +381,32 @@ export class FileJsDb {
|
|
|
283
381
|
get_sequence() {
|
|
284
382
|
return this.state.sequence;
|
|
285
383
|
}
|
|
384
|
+
/**
|
|
385
|
+
* The file runtime declines revision validation, and this is the honest answer
|
|
386
|
+
* rather than a missing feature.
|
|
387
|
+
*
|
|
388
|
+
* A shared directory has no serialization authority over its writers.
|
|
389
|
+
* `insert`, `update`, `delete`, and `commit_transaction` do not take the
|
|
390
|
+
* cross-process lock, and publication is a whole-snapshot rename, so two
|
|
391
|
+
* processes writing the same directory lose data. `state.sequence` therefore
|
|
392
|
+
* cannot distinguish "one writer committed 50" from "two writers committed 50
|
|
393
|
+
* each and one snapshot won" — it produces a plausible but false answer,
|
|
394
|
+
* which is worse than producing none.
|
|
395
|
+
*
|
|
396
|
+
* Measured in tools/cli/test/file-runtime-concurrency.test.mjs and analysed in
|
|
397
|
+
* docs/architecture/freshness-revision-investigation.md. Making this runtime
|
|
398
|
+
* revision-capable requires fixing lock discipline and publication together;
|
|
399
|
+
* advertising `refresh` is correct until then, not a placeholder.
|
|
400
|
+
*/
|
|
401
|
+
freshness() {
|
|
402
|
+
return {
|
|
403
|
+
domain: 'multi-process-file',
|
|
404
|
+
validation: 'refresh',
|
|
405
|
+
invalidation: false,
|
|
406
|
+
reason: 'a shared directory has no serialization authority over its writers, '
|
|
407
|
+
+ 'so state.sequence cannot be an authoritative revision',
|
|
408
|
+
};
|
|
409
|
+
}
|
|
286
410
|
audit_events() {
|
|
287
411
|
return [...this.state.events];
|
|
288
412
|
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The FeltDB freshness capability.
|
|
3
|
+
*
|
|
4
|
+
* FeltDB does not require every runtime to produce a revision number, because
|
|
5
|
+
* not every runtime has a serialization boundary that could make one
|
|
6
|
+
* authoritative. `get_sequence()` looked like that number and is not: it is a
|
|
7
|
+
* hardcoded `0` in the authority runtime and counts only this handle's writes
|
|
8
|
+
* elsewhere, so a cache built on it would look correct with one writer and
|
|
9
|
+
* serve stale data the moment a second one exists.
|
|
10
|
+
*
|
|
11
|
+
* The contract instead makes freshness a capability a runtime advertises, and
|
|
12
|
+
* holds it to one rule:
|
|
13
|
+
*
|
|
14
|
+
* A runtime may advertise revision-based cache validation only if its
|
|
15
|
+
* revision is authoritative for the consistency domain it represents.
|
|
16
|
+
*
|
|
17
|
+
* A runtime that cannot say that says so, and callers fall back to explicit
|
|
18
|
+
* refresh. Declining is a correct answer here; fabricating a scalar is not.
|
|
19
|
+
*
|
|
20
|
+
* See docs/architecture/collection-freshness.md for the decision and
|
|
21
|
+
* docs/architecture/freshness-revision-investigation.md for the evidence.
|
|
22
|
+
*/
|
|
23
|
+
import type { JsDb } from './feltdb.js';
|
|
24
|
+
/**
|
|
25
|
+
* The consistency domain a runtime is operating in.
|
|
26
|
+
*
|
|
27
|
+
* This is a property of the deployment, not of the storage engine: the same
|
|
28
|
+
* runtime moves between domains when replication is switched on.
|
|
29
|
+
*/
|
|
30
|
+
export type ConsistencyDomain =
|
|
31
|
+
/** One process, one handle graph. Nothing writes this state from outside. */
|
|
32
|
+
'single-process'
|
|
33
|
+
/** A single server serializes every commit behind one lock. */
|
|
34
|
+
| 'single-authority'
|
|
35
|
+
/** One storage engine serializes competing local writers (IndexedDB across tabs). */
|
|
36
|
+
| 'local-first'
|
|
37
|
+
/** A shared directory with no serialization authority over its writers. */
|
|
38
|
+
| 'multi-process-file'
|
|
39
|
+
/** Causally ordered replicas. There is no global sequencer, by design. */
|
|
40
|
+
| 'replicated'
|
|
41
|
+
/** The runtime did not declare one. Treated as the least trustworthy case. */
|
|
42
|
+
| 'undeclared';
|
|
43
|
+
/**
|
|
44
|
+
* A committed-state revision.
|
|
45
|
+
*
|
|
46
|
+
* `value` is meaningful **only** against another revision carrying the same
|
|
47
|
+
* `scope`. That restriction is the whole point: it is what stops a scalar from
|
|
48
|
+
* being compared across replicas, where the authoritative order is partial and
|
|
49
|
+
* two replicas can each reach "revision 7" holding different state.
|
|
50
|
+
*/
|
|
51
|
+
export interface Revision {
|
|
52
|
+
value: number;
|
|
53
|
+
scope: string;
|
|
54
|
+
}
|
|
55
|
+
export interface FreshnessCapability {
|
|
56
|
+
domain: ConsistencyDomain;
|
|
57
|
+
/**
|
|
58
|
+
* How a cache holder is permitted to decide its copy is current.
|
|
59
|
+
*
|
|
60
|
+
* - `revision`: ask for the current revision and compare it against the one
|
|
61
|
+
* the cache was built from. Cheap, and it answers "am I stale?".
|
|
62
|
+
* - `refresh`: re-read the state. It is the only honest answer when no
|
|
63
|
+
* authoritative revision exists.
|
|
64
|
+
*/
|
|
65
|
+
validation: 'revision' | 'refresh';
|
|
66
|
+
/**
|
|
67
|
+
* Whether the runtime pushes a change signal.
|
|
68
|
+
*
|
|
69
|
+
* Invalidation tells a cache that it *might* be stale. It is never proof
|
|
70
|
+
* that a cache *is* current, and it never upgrades `validation`.
|
|
71
|
+
*/
|
|
72
|
+
invalidation: boolean;
|
|
73
|
+
/** Comparison scope for revisions. Present exactly when validation is `revision`. */
|
|
74
|
+
revisionScope?: string;
|
|
75
|
+
/** Why revision validation is not offered. Present exactly when validation is `refresh`. */
|
|
76
|
+
reason?: string;
|
|
77
|
+
}
|
|
78
|
+
export declare class FreshnessContractViolation extends Error {
|
|
79
|
+
constructor(message: string);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Check a capability against the rule before anyone acts on it.
|
|
83
|
+
*
|
|
84
|
+
* A runtime that claims revision validation without being able to produce a
|
|
85
|
+
* revision is worse than one that claims nothing, so this throws rather than
|
|
86
|
+
* degrading quietly. The failure belongs to whoever wrote the runtime.
|
|
87
|
+
*/
|
|
88
|
+
export declare function assertFreshnessCapability(capability: FreshnessCapability, db: Pick<JsDb, 'revision'>): FreshnessCapability;
|
|
89
|
+
/** Ask a runtime what freshness it can honestly support, and hold it to the rule. */
|
|
90
|
+
export declare function describeFreshness(db: Pick<JsDb, 'freshness' | 'revision'>): Promise<FreshnessCapability>;
|
|
91
|
+
/**
|
|
92
|
+
* Read a revision, but only from a runtime that advertised one.
|
|
93
|
+
*
|
|
94
|
+
* The capability is consulted on every read rather than cached, because a
|
|
95
|
+
* runtime's domain can change underneath a caller: switching on replication
|
|
96
|
+
* revokes revision authority, and a cache that had already captured the
|
|
97
|
+
* capability would keep trusting a number that stopped meaning anything.
|
|
98
|
+
*
|
|
99
|
+
* This is the accessor callers should use. Calling `db.revision()` directly
|
|
100
|
+
* skips the check that makes the answer trustworthy.
|
|
101
|
+
*/
|
|
102
|
+
export declare function readRevision(db: Pick<JsDb, 'freshness' | 'revision'>): Promise<Revision>;
|
|
103
|
+
/**
|
|
104
|
+
* Compare two revisions from the same scope.
|
|
105
|
+
*
|
|
106
|
+
* Throws on a scope mismatch instead of returning a comparison. Two revisions
|
|
107
|
+
* from different scopes are not ordered, and silently picking an answer is
|
|
108
|
+
* exactly the mistake a scalar revision invites once replication exists.
|
|
109
|
+
*/
|
|
110
|
+
export declare function isNewer(candidate: Revision, baseline: Revision): boolean;
|
|
111
|
+
/**
|
|
112
|
+
* Whether a cache built at `cached` is still current against `current`.
|
|
113
|
+
*
|
|
114
|
+
* Deliberately not "is it newer": equal revisions mean current, and anything
|
|
115
|
+
* else — including a revision that moved backwards, which should not happen —
|
|
116
|
+
* means refetch.
|
|
117
|
+
*/
|
|
118
|
+
export declare function isCacheCurrent(cached: Revision, current: Revision): boolean;
|
|
119
|
+
//# sourceMappingURL=freshness.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"freshness.d.ts","sourceRoot":"","sources":["../src/freshness.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAExC;;;;;GAKG;AACH,MAAM,MAAM,iBAAiB;AAC3B,6EAA6E;AAC3E,gBAAgB;AAClB,+DAA+D;GAC7D,kBAAkB;AACpB,qFAAqF;GACnF,aAAa;AACf,2EAA2E;GACzE,oBAAoB;AACtB,0EAA0E;GACxE,YAAY;AACd,8EAA8E;GAC5E,YAAY,CAAC;AAEjB;;;;;;;GAOG;AACH,MAAM,WAAW,QAAQ;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,iBAAiB,CAAC;IAE1B;;;;;;;OAOG;IACH,UAAU,EAAE,UAAU,GAAG,SAAS,CAAC;IAEnC;;;;;OAKG;IACH,YAAY,EAAE,OAAO,CAAC;IAEtB,qFAAqF;IACrF,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB,4FAA4F;IAC5F,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAUD,qBAAa,0BAA2B,SAAQ,KAAK;gBACvC,OAAO,EAAE,MAAM;CAI5B;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CACvC,UAAU,EAAE,mBAAmB,EAC/B,EAAE,EAAE,IAAI,CAAC,IAAI,EAAE,UAAU,CAAC,GACzB,mBAAmB,CA4BrB;AAED,qFAAqF;AACrF,wBAAsB,iBAAiB,CAAC,EAAE,EAAE,IAAI,CAAC,IAAI,EAAE,WAAW,GAAG,UAAU,CAAC,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAK9G;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,YAAY,CAAC,EAAE,EAAE,IAAI,CAAC,IAAI,EAAE,WAAW,GAAG,UAAU,CAAC,GAAG,OAAO,CAAC,QAAQ,CAAC,CAc9F;AAED;;;;;;GAMG;AACH,wBAAgB,OAAO,CAAC,SAAS,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAOxE;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,QAAQ,GAAG,OAAO,CAG3E"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The FeltDB freshness capability.
|
|
3
|
+
*
|
|
4
|
+
* FeltDB does not require every runtime to produce a revision number, because
|
|
5
|
+
* not every runtime has a serialization boundary that could make one
|
|
6
|
+
* authoritative. `get_sequence()` looked like that number and is not: it is a
|
|
7
|
+
* hardcoded `0` in the authority runtime and counts only this handle's writes
|
|
8
|
+
* elsewhere, so a cache built on it would look correct with one writer and
|
|
9
|
+
* serve stale data the moment a second one exists.
|
|
10
|
+
*
|
|
11
|
+
* The contract instead makes freshness a capability a runtime advertises, and
|
|
12
|
+
* holds it to one rule:
|
|
13
|
+
*
|
|
14
|
+
* A runtime may advertise revision-based cache validation only if its
|
|
15
|
+
* revision is authoritative for the consistency domain it represents.
|
|
16
|
+
*
|
|
17
|
+
* A runtime that cannot say that says so, and callers fall back to explicit
|
|
18
|
+
* refresh. Declining is a correct answer here; fabricating a scalar is not.
|
|
19
|
+
*
|
|
20
|
+
* See docs/architecture/collection-freshness.md for the decision and
|
|
21
|
+
* docs/architecture/freshness-revision-investigation.md for the evidence.
|
|
22
|
+
*/
|
|
23
|
+
/** A runtime that has not declared a capability gets the safe answer, not a guess. */
|
|
24
|
+
const UNDECLARED = Object.freeze({
|
|
25
|
+
domain: 'undeclared',
|
|
26
|
+
validation: 'refresh',
|
|
27
|
+
invalidation: false,
|
|
28
|
+
reason: 'the runtime does not declare a freshness capability',
|
|
29
|
+
});
|
|
30
|
+
export class FreshnessContractViolation extends Error {
|
|
31
|
+
constructor(message) {
|
|
32
|
+
super(message);
|
|
33
|
+
this.name = 'FreshnessContractViolation';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Check a capability against the rule before anyone acts on it.
|
|
38
|
+
*
|
|
39
|
+
* A runtime that claims revision validation without being able to produce a
|
|
40
|
+
* revision is worse than one that claims nothing, so this throws rather than
|
|
41
|
+
* degrading quietly. The failure belongs to whoever wrote the runtime.
|
|
42
|
+
*/
|
|
43
|
+
export function assertFreshnessCapability(capability, db) {
|
|
44
|
+
const hasRevision = typeof db.revision === 'function';
|
|
45
|
+
if (capability.validation === 'revision') {
|
|
46
|
+
if (!hasRevision) {
|
|
47
|
+
throw new FreshnessContractViolation('a runtime advertising revision validation must implement revision()');
|
|
48
|
+
}
|
|
49
|
+
if (!capability.revisionScope) {
|
|
50
|
+
throw new FreshnessContractViolation('a runtime advertising revision validation must name the scope its revisions are comparable within');
|
|
51
|
+
}
|
|
52
|
+
if (capability.domain === 'replicated' || capability.domain === 'multi-process-file') {
|
|
53
|
+
throw new FreshnessContractViolation(`the ${capability.domain} domain has no serialization boundary that can make a scalar revision authoritative`);
|
|
54
|
+
}
|
|
55
|
+
return capability;
|
|
56
|
+
}
|
|
57
|
+
if (!capability.reason) {
|
|
58
|
+
throw new FreshnessContractViolation('a runtime that cannot validate by revision must say why');
|
|
59
|
+
}
|
|
60
|
+
return capability;
|
|
61
|
+
}
|
|
62
|
+
/** Ask a runtime what freshness it can honestly support, and hold it to the rule. */
|
|
63
|
+
export async function describeFreshness(db) {
|
|
64
|
+
if (typeof db.freshness !== 'function') {
|
|
65
|
+
return assertFreshnessCapability(UNDECLARED, db);
|
|
66
|
+
}
|
|
67
|
+
return assertFreshnessCapability(await db.freshness(), db);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Read a revision, but only from a runtime that advertised one.
|
|
71
|
+
*
|
|
72
|
+
* The capability is consulted on every read rather than cached, because a
|
|
73
|
+
* runtime's domain can change underneath a caller: switching on replication
|
|
74
|
+
* revokes revision authority, and a cache that had already captured the
|
|
75
|
+
* capability would keep trusting a number that stopped meaning anything.
|
|
76
|
+
*
|
|
77
|
+
* This is the accessor callers should use. Calling `db.revision()` directly
|
|
78
|
+
* skips the check that makes the answer trustworthy.
|
|
79
|
+
*/
|
|
80
|
+
export async function readRevision(db) {
|
|
81
|
+
const capability = await describeFreshness(db);
|
|
82
|
+
if (capability.validation !== 'revision') {
|
|
83
|
+
throw new FreshnessContractViolation(`this runtime does not offer revision validation: ${capability.reason}`);
|
|
84
|
+
}
|
|
85
|
+
const revision = await db.revision();
|
|
86
|
+
if (revision.scope !== capability.revisionScope) {
|
|
87
|
+
throw new FreshnessContractViolation(`revision scope ${revision.scope} does not match the advertised scope ${capability.revisionScope}`);
|
|
88
|
+
}
|
|
89
|
+
return revision;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Compare two revisions from the same scope.
|
|
93
|
+
*
|
|
94
|
+
* Throws on a scope mismatch instead of returning a comparison. Two revisions
|
|
95
|
+
* from different scopes are not ordered, and silently picking an answer is
|
|
96
|
+
* exactly the mistake a scalar revision invites once replication exists.
|
|
97
|
+
*/
|
|
98
|
+
export function isNewer(candidate, baseline) {
|
|
99
|
+
if (candidate.scope !== baseline.scope) {
|
|
100
|
+
throw new FreshnessContractViolation(`revisions from different scopes are not comparable: ${baseline.scope} vs ${candidate.scope}`);
|
|
101
|
+
}
|
|
102
|
+
return candidate.value > baseline.value;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Whether a cache built at `cached` is still current against `current`.
|
|
106
|
+
*
|
|
107
|
+
* Deliberately not "is it newer": equal revisions mean current, and anything
|
|
108
|
+
* else — including a revision that moved backwards, which should not happen —
|
|
109
|
+
* means refetch.
|
|
110
|
+
*/
|
|
111
|
+
export function isCacheCurrent(cached, current) {
|
|
112
|
+
if (cached.scope !== current.scope)
|
|
113
|
+
return false;
|
|
114
|
+
return cached.value === current.value;
|
|
115
|
+
}
|
package/dist/http-db.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { JsDb } from './feltdb.js';
|
|
2
|
+
import type { FreshnessCapability, Revision } from './freshness.js';
|
|
2
3
|
interface JsResult {
|
|
3
4
|
success: boolean;
|
|
4
5
|
data?: string;
|
|
@@ -19,6 +20,27 @@ export declare class HttpJsDb implements JsDb {
|
|
|
19
20
|
query(collection: string): Promise<JsResult>;
|
|
20
21
|
insert(key: string, value: string): Promise<JsResult>;
|
|
21
22
|
update(key: string, value: string): Promise<JsResult>;
|
|
23
|
+
/**
|
|
24
|
+
* Commit a multi-operation transaction through the authority.
|
|
25
|
+
*
|
|
26
|
+
* The whole transaction travels as one request and is committed as one
|
|
27
|
+
* durable record on the server. A refusal means nothing was applied.
|
|
28
|
+
*/
|
|
29
|
+
commit_transaction(request: {
|
|
30
|
+
transactionId: string;
|
|
31
|
+
operations: Array<{
|
|
32
|
+
collection: string;
|
|
33
|
+
id: string;
|
|
34
|
+
value?: Record<string, unknown>;
|
|
35
|
+
expectedVersion?: number;
|
|
36
|
+
}>;
|
|
37
|
+
}): Promise<{
|
|
38
|
+
transactionId: string;
|
|
39
|
+
duplicate: boolean;
|
|
40
|
+
operations: number;
|
|
41
|
+
stateBefore: number;
|
|
42
|
+
stateAfter: number;
|
|
43
|
+
}>;
|
|
22
44
|
cas(params: {
|
|
23
45
|
key: string;
|
|
24
46
|
expectedVersion: number;
|
|
@@ -57,7 +79,33 @@ export declare class HttpJsDb implements JsDb {
|
|
|
57
79
|
get_pending_for_peer(): JsResult;
|
|
58
80
|
acknowledge_peer_operations(): JsResult;
|
|
59
81
|
instance_id(): string;
|
|
82
|
+
/**
|
|
83
|
+
* `get_sequence()` is a hardcoded `0` here, and that is why it could never be
|
|
84
|
+
* the freshness contract: the client has no local counter worth reporting.
|
|
85
|
+
*
|
|
86
|
+
* The authority does have one. A single process advances `inner.sequence`
|
|
87
|
+
* inside the same critical section that writes the durable transaction
|
|
88
|
+
* record, so concurrent clients are serialized by the server and the revision
|
|
89
|
+
* is totally ordered. `GET /revision` reads it without performing a write.
|
|
90
|
+
*/
|
|
60
91
|
get_sequence(): number;
|
|
92
|
+
/**
|
|
93
|
+
* The scope is the store's own instance id, reported by the server, not this
|
|
94
|
+
* client's URL.
|
|
95
|
+
*
|
|
96
|
+
* That distinction matters behind a load balancer or after a failover: the
|
|
97
|
+
* URL would stay the same across two different stores and make their counters
|
|
98
|
+
* look comparable. The store's identity changes, so the mismatch surfaces as
|
|
99
|
+
* a refusal in `readRevision` instead of a silently wrong comparison.
|
|
100
|
+
*
|
|
101
|
+
* It is remembered after the first read so the common path is one request,
|
|
102
|
+
* and a later disagreement is exactly the signal worth raising.
|
|
103
|
+
*/
|
|
104
|
+
private advertisedScope?;
|
|
105
|
+
freshness(): Promise<FreshnessCapability>;
|
|
106
|
+
revision(): Promise<Revision>;
|
|
107
|
+
private body;
|
|
108
|
+
private absorb;
|
|
61
109
|
}
|
|
62
110
|
export {};
|
|
63
111
|
//# sourceMappingURL=http-db.d.ts.map
|
package/dist/http-db.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http-db.d.ts","sourceRoot":"","sources":["../src/http-db.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"http-db.d.ts","sourceRoot":"","sources":["../src/http-db.ts"],"names":[],"mappings":"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"}
|
package/dist/http-db.js
CHANGED
|
@@ -82,6 +82,25 @@ export class HttpJsDb {
|
|
|
82
82
|
return { success: false, error: String(error) };
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Commit a multi-operation transaction through the authority.
|
|
87
|
+
*
|
|
88
|
+
* The whole transaction travels as one request and is committed as one
|
|
89
|
+
* durable record on the server. A refusal means nothing was applied.
|
|
90
|
+
*/
|
|
91
|
+
async commit_transaction(request) {
|
|
92
|
+
const response = await fetch(`${this.url}/transactions`, {
|
|
93
|
+
method: 'POST',
|
|
94
|
+
headers: this.headers(true),
|
|
95
|
+
body: JSON.stringify(request),
|
|
96
|
+
});
|
|
97
|
+
if (!response.ok) {
|
|
98
|
+
const failure = await this.failure(response);
|
|
99
|
+
throw new Error(failure.error || `transaction failed with HTTP ${response.status}`);
|
|
100
|
+
}
|
|
101
|
+
const body = await response.json();
|
|
102
|
+
return body;
|
|
103
|
+
}
|
|
85
104
|
async cas(params) {
|
|
86
105
|
try {
|
|
87
106
|
const [collection, id] = this.splitKey(params.key);
|
|
@@ -220,7 +239,16 @@ export class HttpJsDb {
|
|
|
220
239
|
if (controller.signal.aborted)
|
|
221
240
|
return;
|
|
222
241
|
}
|
|
223
|
-
|
|
242
|
+
if (controller.signal.aborted)
|
|
243
|
+
return;
|
|
244
|
+
// The retry timer must not keep a Node process alive on its own. An
|
|
245
|
+
// authority that has shut down would otherwise stop its clients from
|
|
246
|
+
// ever exiting, because this loop reconnects forever.
|
|
247
|
+
await new Promise(resolve => {
|
|
248
|
+
const timer = setTimeout(resolve, 1000);
|
|
249
|
+
timer?.unref?.();
|
|
250
|
+
controller.signal.addEventListener('abort', () => { clearTimeout(timer); resolve(); }, { once: true });
|
|
251
|
+
});
|
|
224
252
|
}
|
|
225
253
|
}
|
|
226
254
|
async failure(response) {
|
|
@@ -242,5 +270,53 @@ export class HttpJsDb {
|
|
|
242
270
|
get_pending_for_peer() { return { success: false, error: 'Operation transport is server-managed' }; }
|
|
243
271
|
acknowledge_peer_operations() { return { success: false, error: 'Operation transport is server-managed' }; }
|
|
244
272
|
instance_id() { return `remote:${this.url}`; }
|
|
273
|
+
/**
|
|
274
|
+
* `get_sequence()` is a hardcoded `0` here, and that is why it could never be
|
|
275
|
+
* the freshness contract: the client has no local counter worth reporting.
|
|
276
|
+
*
|
|
277
|
+
* The authority does have one. A single process advances `inner.sequence`
|
|
278
|
+
* inside the same critical section that writes the durable transaction
|
|
279
|
+
* record, so concurrent clients are serialized by the server and the revision
|
|
280
|
+
* is totally ordered. `GET /revision` reads it without performing a write.
|
|
281
|
+
*/
|
|
245
282
|
get_sequence() { return 0; }
|
|
283
|
+
async freshness() {
|
|
284
|
+
if (this.advertisedScope === undefined) {
|
|
285
|
+
// An authority that predates `/revision` cannot validate a cache by
|
|
286
|
+
// revision, and saying so is a real answer rather than a failure: the
|
|
287
|
+
// caller falls back to refresh, which is always correct. Only a 404
|
|
288
|
+
// degrades — a network error or a 500 is not evidence about what the
|
|
289
|
+
// server supports, so it is raised rather than reported as a capability.
|
|
290
|
+
const probe = await fetch(`${this.url}/revision`, { headers: this.headers() });
|
|
291
|
+
if (probe.status === 404) {
|
|
292
|
+
return {
|
|
293
|
+
domain: 'single-authority',
|
|
294
|
+
validation: 'refresh',
|
|
295
|
+
invalidation: true,
|
|
296
|
+
reason: 'this authority does not serve GET /revision, so it cannot report a committed revision',
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
this.absorb(await this.body(probe));
|
|
300
|
+
}
|
|
301
|
+
return {
|
|
302
|
+
domain: 'single-authority',
|
|
303
|
+
validation: 'revision',
|
|
304
|
+
invalidation: true,
|
|
305
|
+
revisionScope: this.advertisedScope,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
async revision() {
|
|
309
|
+
const body = this.absorb(await this.body(await fetch(`${this.url}/revision`, { headers: this.headers() })));
|
|
310
|
+
return { value: body.revision, scope: body.scope };
|
|
311
|
+
}
|
|
312
|
+
async body(response) {
|
|
313
|
+
if (!response.ok) {
|
|
314
|
+
throw new Error(`revision request failed: ${response.status} ${response.statusText}`);
|
|
315
|
+
}
|
|
316
|
+
return await response.json();
|
|
317
|
+
}
|
|
318
|
+
absorb(body) {
|
|
319
|
+
this.advertisedScope ?? (this.advertisedScope = body.scope);
|
|
320
|
+
return body;
|
|
321
|
+
}
|
|
246
322
|
}
|
package/dist/index-backend.d.ts
CHANGED
|
@@ -19,6 +19,14 @@ export declare class IndexBackend implements IIndexBackend {
|
|
|
19
19
|
initializeWasm(): Promise<void>;
|
|
20
20
|
createIndex(config: IndexConfig): void;
|
|
21
21
|
updateRecord(recordId: string, oldValue: Record<string, any> | null, newValue: Record<string, any>): void;
|
|
22
|
+
/**
|
|
23
|
+
* Discard indexed entries, keeping definitions. Reports whether it worked.
|
|
24
|
+
*
|
|
25
|
+
* The WASM backend exposes no reset, so a caller that needs the index to
|
|
26
|
+
* reflect a specific snapshot must treat `false` as "do not trust the index"
|
|
27
|
+
* rather than assuming the reset happened.
|
|
28
|
+
*/
|
|
29
|
+
resetEntries(): boolean;
|
|
22
30
|
queryIndex(name: string, query: IndexQuery): string[];
|
|
23
31
|
queryCompoundIndex(name: string, values: Record<string, any>): string[];
|
|
24
32
|
listIndexes(): IndexConfig[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index-backend.d.ts","sourceRoot":"","sources":["../src/index-backend.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEhE,MAAM,WAAW,aAAa;IAC5B,WAAW,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,CAAC;IACvC,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,EAAE,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,CAAC;IAC1G,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,GAAG,MAAM,EAAE,CAAC;IACtD,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,EAAE,CAAC;IACxE,WAAW,IAAI,WAAW,EAAE,CAAC;CAC9B;AAyBD,qBAAa,YAAa,YAAW,aAAa;IAChD,OAAO,CAAC,SAAS,CAA+B;IAChD,OAAO,CAAC,WAAW,CAAa;IAChC,OAAO,CAAC,OAAO,CAAkB;;IAM3B,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC;IAcrC,WAAW,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI;IAYtC,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,EAAE,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI;IAezG,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,GAAG,MAAM,EAAE;IAYrD,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,EAAE;IAgBvE,WAAW,IAAI,WAAW,EAAE;CAW7B"}
|
|
1
|
+
{"version":3,"file":"index-backend.d.ts","sourceRoot":"","sources":["../src/index-backend.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEhE,MAAM,WAAW,aAAa;IAC5B,WAAW,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,CAAC;IACvC,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,EAAE,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,CAAC;IAC1G,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,GAAG,MAAM,EAAE,CAAC;IACtD,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,EAAE,CAAC;IACxE,WAAW,IAAI,WAAW,EAAE,CAAC;CAC9B;AAyBD,qBAAa,YAAa,YAAW,aAAa;IAChD,OAAO,CAAC,SAAS,CAA+B;IAChD,OAAO,CAAC,WAAW,CAAa;IAChC,OAAO,CAAC,OAAO,CAAkB;;IAM3B,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC;IAcrC,WAAW,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI;IAYtC,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,EAAE,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI;IAezG;;;;;;OAMG;IACH,YAAY,IAAI,OAAO;IAMvB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,GAAG,MAAM,EAAE;IAYrD,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,EAAE;IAgBvE,WAAW,IAAI,WAAW,EAAE;CAW7B"}
|
package/dist/index-backend.js
CHANGED
|
@@ -72,6 +72,19 @@ export class IndexBackend {
|
|
|
72
72
|
}
|
|
73
73
|
this.tsBackend?.updateRecord(recordId, oldValue, newValue);
|
|
74
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Discard indexed entries, keeping definitions. Reports whether it worked.
|
|
77
|
+
*
|
|
78
|
+
* The WASM backend exposes no reset, so a caller that needs the index to
|
|
79
|
+
* reflect a specific snapshot must treat `false` as "do not trust the index"
|
|
80
|
+
* rather than assuming the reset happened.
|
|
81
|
+
*/
|
|
82
|
+
resetEntries() {
|
|
83
|
+
if (this.useWasm && this.wasmBackend)
|
|
84
|
+
return false;
|
|
85
|
+
this.tsBackend?.resetEntries();
|
|
86
|
+
return this.tsBackend !== null;
|
|
87
|
+
}
|
|
75
88
|
queryIndex(name, query) {
|
|
76
89
|
if (this.useWasm && this.wasmBackend) {
|
|
77
90
|
try {
|