@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.
Files changed (122) hide show
  1. package/dist/cli/application-lifecycle.js +112 -0
  2. package/dist/cli/application.js +10 -0
  3. package/dist/cli/commands.js +407 -119
  4. package/dist/cli/development-session.js +54 -0
  5. package/dist/cli/index.js +1 -1
  6. package/dist/cli/workspace-integration.js +146 -20
  7. package/dist/collection.d.ts +110 -2
  8. package/dist/collection.d.ts.map +1 -1
  9. package/dist/collection.js +228 -3
  10. package/dist/create/cli.js +4 -2
  11. package/dist/create/create.js +42 -6
  12. package/dist/create/frameworks.js +18 -0
  13. package/dist/create/package-versions.js +1 -1
  14. package/dist/create/server-source/crates/feltdb/src/bin/feltdb_node.rs +408 -178
  15. package/dist/create/server-source/crates/feltdb/src/lib.rs +49 -5
  16. package/dist/create/server-source/crates/feltdb/src/multi_operation_transaction.rs +659 -0
  17. package/dist/create/server-source/crates/feltdb/src/p1_application_atomicity.rs +285 -0
  18. package/dist/create/server-source/crates/feltdb/src/p1_atomicity_acceptance.rs +582 -0
  19. package/dist/create/server-source/crates/feltdb-server/src/main.rs +141 -0
  20. package/dist/create/workspace-initialization.js +4 -5
  21. package/dist/db.d.ts +63 -1
  22. package/dist/db.d.ts.map +1 -1
  23. package/dist/db.js +145 -4
  24. package/dist/development-runtime-bridge.d.ts +60 -0
  25. package/dist/development-runtime-bridge.d.ts.map +1 -0
  26. package/dist/development-runtime-bridge.js +171 -0
  27. package/dist/embedded-transaction.d.ts +77 -0
  28. package/dist/embedded-transaction.d.ts.map +1 -0
  29. package/dist/embedded-transaction.js +102 -0
  30. package/dist/feltdb.d.ts +49 -1
  31. package/dist/feltdb.d.ts.map +1 -1
  32. package/dist/file-db.d.ts +33 -0
  33. package/dist/file-db.d.ts.map +1 -1
  34. package/dist/file-db.js +128 -4
  35. package/dist/freshness.d.ts +119 -0
  36. package/dist/freshness.d.ts.map +1 -0
  37. package/dist/freshness.js +115 -0
  38. package/dist/http-db.d.ts +48 -0
  39. package/dist/http-db.d.ts.map +1 -1
  40. package/dist/http-db.js +77 -1
  41. package/dist/index-backend.d.ts +8 -0
  42. package/dist/index-backend.d.ts.map +1 -1
  43. package/dist/index-backend.js +13 -0
  44. package/dist/index-core.d.ts +80 -0
  45. package/dist/index-core.d.ts.map +1 -0
  46. package/dist/index-core.js +74 -0
  47. package/dist/index-manager.d.ts +11 -0
  48. package/dist/index-manager.d.ts.map +1 -1
  49. package/dist/index-manager.js +30 -13
  50. package/dist/index-types.d.ts +17 -0
  51. package/dist/index-types.d.ts.map +1 -1
  52. package/dist/index.browser.d.ts +14 -0
  53. package/dist/index.browser.d.ts.map +1 -0
  54. package/dist/index.browser.js +13 -0
  55. package/dist/index.d.ts +8 -59
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +11 -54
  58. package/dist/indexeddb-db.d.ts +32 -0
  59. package/dist/indexeddb-db.d.ts.map +1 -1
  60. package/dist/indexeddb-db.js +151 -1
  61. package/dist/memory-db.d.ts +26 -0
  62. package/dist/memory-db.d.ts.map +1 -1
  63. package/dist/memory-db.js +86 -0
  64. package/dist/runtime-registry.d.ts +32 -0
  65. package/dist/runtime-registry.d.ts.map +1 -0
  66. package/dist/runtime-registry.js +38 -0
  67. package/dist/studio/app.d.ts +2 -1
  68. package/dist/studio/app.d.ts.map +1 -1
  69. package/dist/studio/components/StateExplorer.d.ts +2 -1
  70. package/dist/studio/components/StateExplorer.d.ts.map +1 -1
  71. package/dist/studio/components/index.js +1 -1
  72. package/dist/studio/{components-9kDSWiGL.js → components-C5p2TfIU.js} +25 -14
  73. package/dist/studio/index.js +234 -160
  74. package/dist/studio-app/assets/{feltdb_wasm-B4wq4mqp.js → feltdb_wasm-CJv3wHzi.js} +1 -1
  75. package/dist/studio-app/assets/feltdb_wasm_bg-C8TG8r2n.wasm +0 -0
  76. package/dist/studio-app/assets/index-DospFFYE.js +28 -0
  77. package/dist/studio-app/index.html +1 -1
  78. package/dist/transaction.d.ts +98 -0
  79. package/dist/transaction.d.ts.map +1 -0
  80. package/dist/transaction.js +86 -0
  81. package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
  82. package/dist/workspace/browser.d.ts +22 -0
  83. package/dist/workspace/browser.d.ts.map +1 -0
  84. package/dist/workspace/browser.js +17 -0
  85. package/dist/workspace/development-node.d.ts +16 -0
  86. package/dist/workspace/development-node.d.ts.map +1 -1
  87. package/dist/workspace/development-node.js +85 -1
  88. package/dist/workspace/git-identity.d.ts +35 -0
  89. package/dist/workspace/git-identity.d.ts.map +1 -0
  90. package/dist/workspace/git-identity.js +81 -0
  91. package/dist/workspace/index.d.ts +12 -2
  92. package/dist/workspace/index.d.ts.map +1 -1
  93. package/dist/workspace/index.js +7 -1
  94. package/dist/workspace/investigation-analysis.d.ts +47 -0
  95. package/dist/workspace/investigation-analysis.d.ts.map +1 -0
  96. package/dist/workspace/investigation-analysis.js +181 -0
  97. package/dist/workspace/investigation-lifecycle-manager.d.ts +88 -0
  98. package/dist/workspace/investigation-lifecycle-manager.d.ts.map +1 -0
  99. package/dist/workspace/investigation-lifecycle-manager.js +259 -0
  100. package/dist/workspace/investigation-supervisor.d.ts +100 -0
  101. package/dist/workspace/investigation-supervisor.d.ts.map +1 -0
  102. package/dist/workspace/investigation-supervisor.js +238 -0
  103. package/dist/workspace/runtime-investigation.d.ts +5 -1
  104. package/dist/workspace/runtime-investigation.d.ts.map +1 -1
  105. package/dist/workspace/runtime-investigation.js +50 -21
  106. package/dist/workspace/runtime-observation.d.ts +82 -0
  107. package/dist/workspace/runtime-observation.d.ts.map +1 -0
  108. package/dist/workspace/runtime-observation.js +170 -0
  109. package/dist/workspace/runtime-observer.d.ts +57 -0
  110. package/dist/workspace/runtime-observer.d.ts.map +1 -0
  111. package/dist/workspace/runtime-observer.js +272 -0
  112. package/dist/workspace/workspace-connection.d.ts.map +1 -1
  113. package/dist/workspace/workspace-connection.js +14 -5
  114. package/dist/workspace/workspace-identity.d.ts +1 -1
  115. package/dist/workspace/workspace-identity.d.ts.map +1 -1
  116. package/dist/workspace/workspace-identity.js +4 -4
  117. package/dist/workspace/workspace-types.d.ts +208 -0
  118. package/dist/workspace/workspace-types.d.ts.map +1 -1
  119. package/package.json +12 -4
  120. package/dist/create/server-source/crates/feltdb/src/phase_1c_three_node.rs +0 -523
  121. package/dist/studio-app/assets/feltdb_wasm_bg-Ceyi7l21.wasm +0 -0
  122. 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.writeFileSync(tempPath, content, 'utf-8');
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
@@ -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;AAExC,UAAU,QAAQ;IAChB,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,UAAU,kBAAkB;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;CACf;AAED,iFAAiF;AACjF,qBAAa,QAAS,YAAW,IAAI;IACnC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;IAC7B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;gBAEnB,OAAO,EAAE,kBAAkB;IAKvC,OAAO,CAAC,OAAO;IAQf,OAAO,CAAC,QAAQ;IAQV,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IAenC,KAAK,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IAa5C,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IAgBrD,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IAerD,GAAG,CAAC,MAAM,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,eAAe,EAAE,MAAM,CAAC;QAAC,aAAa,CAAC,EAAE,MAAM,CAAC;QAAC,eAAe,CAAC,EAAE,MAAM,CAAC;QAAC,gBAAgB,CAAC,EAAE,OAAO,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,cAAc,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IA8CvP,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IAa5C,mEAAmE;IAC7D,OAAO,CAAC,UAAU,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC;IAOtE,qEAAqE;IAC/D,MAAM,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,SAAK,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;IAQzE,UAAU,CAAC,UAAU,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC;IAMxD,YAAY,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;IAMxF,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAOxD,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC;IAQxD,iBAAiB,CAAC,QAAQ,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,GAAG,MAAM,IAAI;YAMvD,aAAa;YA+Bb,OAAO;IASrB,sBAAsB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IAC7D,UAAU,IAAI,QAAQ;IACtB,SAAS,IAAI,QAAQ;IACrB,QAAQ,IAAI,QAAQ;IACpB,aAAa,IAAI,QAAQ;IACzB,gBAAgB,IAAI,QAAQ;IAC5B,oBAAoB,IAAI,QAAQ;IAChC,2BAA2B,IAAI,QAAQ;IACvC,WAAW,IAAI,MAAM;IACrB,YAAY,IAAI,MAAM;CACvB"}
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
- await new Promise(resolve => setTimeout(resolve, 1000));
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
  }
@@ -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"}
@@ -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 {