@bjornpagen/bumbledb 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/COOKBOOK.md +155 -136
  2. package/README.md +3 -7
  3. package/dist/capacity.d.ts +14 -1
  4. package/dist/capacity.d.ts.map +1 -1
  5. package/dist/capacity.js.map +1 -1
  6. package/dist/db.d.ts +77 -109
  7. package/dist/db.d.ts.map +1 -1
  8. package/dist/db.js +121 -339
  9. package/dist/db.js.map +1 -1
  10. package/dist/index.d.ts +11 -16
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +7 -10
  13. package/dist/index.js.map +1 -1
  14. package/dist/lower.d.ts.map +1 -1
  15. package/dist/lower.js +8 -1
  16. package/dist/lower.js.map +1 -1
  17. package/dist/native.d.ts +69 -50
  18. package/dist/native.d.ts.map +1 -1
  19. package/dist/native.js.map +1 -1
  20. package/dist/query/atom.d.ts +162 -122
  21. package/dist/query/atom.d.ts.map +1 -1
  22. package/dist/query/atom.js +26 -22
  23. package/dist/query/atom.js.map +1 -1
  24. package/dist/query/find.d.ts +18 -35
  25. package/dist/query/find.d.ts.map +1 -1
  26. package/dist/query/find.js +13 -32
  27. package/dist/query/find.js.map +1 -1
  28. package/dist/query/lower.d.ts +113 -122
  29. package/dist/query/lower.d.ts.map +1 -1
  30. package/dist/query/lower.js +336 -260
  31. package/dist/query/lower.js.map +1 -1
  32. package/dist/query/parse-ir.d.ts +12 -0
  33. package/dist/query/parse-ir.d.ts.map +1 -0
  34. package/dist/query/parse-ir.js +71 -0
  35. package/dist/query/parse-ir.js.map +1 -0
  36. package/dist/query/run.d.ts +2 -2
  37. package/dist/query/run.d.ts.map +1 -1
  38. package/dist/query/run.js +2 -13
  39. package/dist/query/run.js.map +1 -1
  40. package/dist/query/scope.d.ts +4 -16
  41. package/dist/query/scope.d.ts.map +1 -1
  42. package/dist/query/scope.js +1 -6
  43. package/dist/query/scope.js.map +1 -1
  44. package/dist/schema.js +2 -2
  45. package/dist/schema.js.map +1 -1
  46. package/dist/statements.d.ts +15 -9
  47. package/dist/statements.d.ts.map +1 -1
  48. package/dist/statements.js +14 -9
  49. package/dist/statements.js.map +1 -1
  50. package/package.json +2 -2
  51. package/src/capacity.ts +24 -1
  52. package/src/db.ts +182 -443
  53. package/src/index.ts +9 -23
  54. package/src/lower.ts +8 -1
  55. package/src/native.ts +69 -42
  56. package/src/query/atom.ts +255 -165
  57. package/src/query/find.ts +39 -80
  58. package/src/query/lower.ts +578 -432
  59. package/src/query/parse-ir.ts +82 -0
  60. package/src/query/run.ts +2 -14
  61. package/src/query/scope.ts +3 -21
  62. package/src/schema.ts +2 -2
  63. package/src/statements.ts +33 -17
  64. package/dist/order.d.ts +0 -87
  65. package/dist/order.d.ts.map +0 -1
  66. package/dist/order.js +0 -153
  67. package/dist/order.js.map +0 -1
  68. package/dist/query/predicate.d.ts +0 -91
  69. package/dist/query/predicate.d.ts.map +0 -1
  70. package/dist/query/predicate.js +0 -156
  71. package/dist/query/predicate.js.map +0 -1
  72. package/src/order.ts +0 -234
  73. package/src/query/predicate.ts +0 -269
package/dist/db.js CHANGED
@@ -2,35 +2,32 @@
2
2
  * `Db` — the living half of the SDK (PRD-07): open/create a store from a
3
3
  * `Schema`, write typed facts through delta transactions with race-free
4
4
  * final-state point reads, receive rejections as typed violation VALUES
5
- * keyed to statements, read through scoped snapshots, and run the witnessed
6
- * read-compute-write loop — all typed by the schema's relations record.
5
+ * keyed to statements, and read through scoped snapshots all typed by
6
+ * the schema's relations record.
7
7
  *
8
8
  * LIFETIMES ARE DISPOSABLES, never `close()` (ruled 2026-07-23, R12): no
9
9
  * value this module returns carries a close spelling — release is
10
- * deterministic and scope-shaped in the language's own syntax. `Db` values
11
- * are CACHED per canonical path for the life of the process (a best-effort
12
- * exit hook closes the cached environments; correctness never depends on it
13
- * the engine fsyncs every commit, so a process that dies without the hook
14
- * loses nothing that was committed). Snapshots are scope-shaped both ways:
15
- * `read(fn)` opens one before `fn` and closes it after unconditionally
16
- * (the {@link ReadScope} handed to `fn` is invalidated the moment `fn`
17
- * returns), and `using snap = db.read()` hands the caller the lifetime,
18
- * released by the scope's own `Symbol.dispose` at scope exit. Prepared
19
- * plans are plain values whose engine-side half is reclaimed by a GC
20
- * finalizer — reclamation only, never correctness.
10
+ * deterministic and scope-shaped in the language's own syntax. Snapshots
11
+ * are scope-shaped both ways: `read(fn)` opens one before `fn` and closes
12
+ * it after unconditionally (the {@link ReadScope} handed to `fn` is
13
+ * invalidated the moment `fn` returns), and `using snap = db.read()` hands
14
+ * the caller the lifetime, released by the scope's own `Symbol.dispose`
15
+ * at scope exit. Prepared plans are plain values whose engine-side half
16
+ * is reclaimed by a GC finalizer reclamation only, never correctness.
21
17
  *
22
18
  * PROCESS MODEL: one process, one exclusive-lock handle per store. The
23
- * cached `Db` value owns the LMDB environment's exclusive lock until
24
- * process exit; a second engine-level open of the same store (an aliased
25
- * path spelling, or another process) is refused by the engine. The
26
- * run-store process model (PRD-16) depends on this being true: resume =
27
- * reopen, which is either this process's cached value or a fresh process's
28
- * open.
19
+ * `Db` value owns the LMDB environment's exclusive lock until process
20
+ * exit (or until GC reclaims the native handle); a second engine-level
21
+ * open of the same store is refused by the engine (`EnvironmentLocked`),
22
+ * matching Rust. Resume = reopen in a fresh process, or hold the one `Db`
23
+ * this process already opened. The host owns composition; retry is host
24
+ * policy.
29
25
  *
30
26
  * REJECTION IS DATA: a rejected commit is a domain outcome (it becomes the
31
27
  * LLM repair prompt downstream), returned as a {@link WriteResult} carrying
32
28
  * {@link Violation} values. Genuine failures — I/O, used-after-scope,
33
- * marshal shape throw `@superbuilders/errors` wrapped errors instead.
29
+ * marshal shape, a moved generation on {@link Db.writeFrom} — throw
30
+ * `@superbuilders/errors` wrapped errors instead.
34
31
  */
35
32
  import * as path from "node:path";
36
33
  import * as errors from "@superbuilders/errors";
@@ -45,7 +42,7 @@ import { decodeAnswers, wireParams } from "#query/run.ts";
45
42
  import { isStatement } from "#statements.ts";
46
43
  /**
47
44
  * The runtime discriminant of {@link Abandon} values — a property probe is
48
- * how `writeWitnessed` distinguishes "abort without committing" from an
45
+ * how `write`/`writeFrom` distinguish "abort without committing" from an
49
46
  * ordinary callback result, never a guess about the host's own value shapes.
50
47
  */
51
48
  const abandonMark = Symbol("bumbledb.abandon");
@@ -53,7 +50,7 @@ const abandonMark = Symbol("bumbledb.abandon");
53
50
  * Wraps a payload in the {@link Abandon} sentinel — the one way a write
54
51
  * callback declines to commit: `return abandon(payload)` aborts the delta
55
52
  * (nothing is committed, not even an empty commit) and the write resolves
56
- * to `{ ok: false, abandoned: payload }`, from `write` and `writeWitnessed`
53
+ * to `{ ok: false, abandoned: payload }`, from `write` and `writeFrom`
57
54
  * alike (R10).
58
55
  */
59
56
  function abandon(payload) {
@@ -131,8 +128,8 @@ function impliedKeyEntries(theory) {
131
128
  if (isFreshField(declared.field)) {
132
129
  entries.push({
133
130
  kind: "functionality",
134
- statement: undefined,
135
- key: { owner: member.name, projection: [declared.name] }
131
+ owner: member.name,
132
+ projection: [declared.name]
136
133
  });
137
134
  }
138
135
  }
@@ -141,8 +138,8 @@ function impliedKeyEntries(theory) {
141
138
  if (isClosedMember(member)) {
142
139
  entries.push({
143
140
  kind: "functionality",
144
- statement: undefined,
145
- key: { owner: member.name, projection: ["id"] }
141
+ owner: member.name,
142
+ projection: ["id"]
146
143
  });
147
144
  }
148
145
  }
@@ -162,21 +159,22 @@ function declaredEntries(statement) {
162
159
  {
163
160
  kind: "functionality",
164
161
  statement,
165
- key: { owner: data.owner.name, projection: data.projection }
162
+ owner: data.owner.name,
163
+ projection: data.projection
166
164
  }
167
165
  ];
168
166
  }
169
167
  case "containment": {
170
- if (data.bidirectional) {
171
- return [
172
- { kind: "containment", statement, key: undefined, reversed: false },
173
- { kind: "containment", statement, key: undefined, reversed: true }
174
- ];
175
- }
176
- return [{ kind: "containment", statement, key: undefined }];
168
+ return [{ kind: "containment", statement }];
169
+ }
170
+ case "mirrors": {
171
+ return [
172
+ { kind: "mirrors", statement, orientation: "written" },
173
+ { kind: "mirrors", statement, orientation: "mirrored" }
174
+ ];
177
175
  }
178
176
  case "capacity": {
179
- return [{ kind: "capacity", statement, key: undefined }];
177
+ return [{ kind: "capacity", statement }];
180
178
  }
181
179
  }
182
180
  }
@@ -221,16 +219,6 @@ function selectKeyRead(keyOrStatement, declaredKey, byStatement, byPrimary) {
221
219
  }
222
220
  return byPrimary(keyOrStatement);
223
221
  }
224
- /** Maps a slot's reversal flag to the violation's `orientation` payload. */
225
- function orientationOf(reversed) {
226
- if (reversed === undefined) {
227
- return undefined;
228
- }
229
- if (reversed) {
230
- return "mirrored";
231
- }
232
- return "written";
233
- }
234
222
  /**
235
223
  * Builds the id-resolution tables from the manifest, verifying the SDK's
236
224
  * positional mirror against the engine's reported order — any drift
@@ -248,9 +236,13 @@ function tablesOf(theory, manifest) {
248
236
  }
249
237
  manifest.statements.forEach(function verifySlot(statement, index) {
250
238
  const entry = entries[index];
251
- if (entry === undefined || statement.id !== index || entry.kind !== statement.kind) {
239
+ if (entry === undefined || statement.id !== index) {
252
240
  throw errors.new(`bumbledb manifest drift: statement ${statement.id} is ${statement.kind}, the SDK mirror at ${index} expected ${entry?.kind}`);
253
241
  }
242
+ const engineKind = entry.kind === "mirrors" ? "containment" : entry.kind;
243
+ if (engineKind !== statement.kind) {
244
+ throw errors.new(`bumbledb manifest drift: statement ${statement.id} is ${statement.kind}, the SDK mirror at ${index} expected ${engineKind}`);
245
+ }
254
246
  });
255
247
  const relations = new Map();
256
248
  for (const relation of manifest.relations) {
@@ -269,8 +261,8 @@ function tablesOf(theory, manifest) {
269
261
  });
270
262
  let primaryKey;
271
263
  entries.forEach(function firstOwnedKey(entry, index) {
272
- if (primaryKey === undefined && entry.key !== undefined && entry.key.owner === relation.name) {
273
- primaryKey = Object.freeze({ statementId: index, projection: entry.key.projection });
264
+ if (primaryKey === undefined && entry.kind === "functionality" && entry.owner === relation.name) {
265
+ primaryKey = Object.freeze({ statementId: index, projection: entry.projection });
274
266
  }
275
267
  });
276
268
  relations.set(relation.name, Object.freeze({ id: relation.id, member, fieldIds, primaryKey }));
@@ -305,34 +297,11 @@ const planReclaimer = new FinalizationRegistry(function reclaimPlan(handle) {
305
297
  }
306
298
  });
307
299
  /**
308
- * The internal retry signal a lazily-witnessed transaction throws when the
309
- * engine reports a moved generation at begin: `writeWitnessed` catches it
310
- * by identity (through cause chains, via `errors.is`) and reruns the whole
311
- * callback on a fresh snapshot. It never escapes the SDK.
312
- */
313
- const generationMovedSignal = errors.new("bumbledb witnessed generation moved");
314
- /**
315
- * The witnessed loop's attempt cap — a generous power of two. Benign
316
- * self-inflicted contention (the host's own commits landing between an
317
- * attempt's snapshot and its witnessed begin) converges in a handful of
318
- * retries because each rerun reads a FRESHER snapshot; a workload that moves
319
- * the generation on EVERY one of this many consecutive attempts is not
320
- * converging and never will (see {@link ErrWitnessedLivelock}).
300
+ * The typed generation-moved refusal `writeFrom` throws when a
301
+ * state-changing commit landed since the witness snapshot: retry is host
302
+ * policy. Match with `errors.is`.
321
303
  */
322
- const WITNESSED_ATTEMPT_CAP = 64;
323
- /**
324
- * The typed livelock refusal `writeWitnessed` throws past
325
- * {@link WITNESSED_ATTEMPT_CAP} attempts: every attempt found the generation
326
- * moved, which is only sustainable when the callback ITSELF (even
327
- * indirectly) issues an interleaved plain `db.write` before its first tx
328
- * verb on every attempt — each rerun then re-moves the generation it is
329
- * about to witness, forever. That is host-policy pathology, not engine
330
- * judgment (the engine ships the error, never a loop), so it THROWS rather
331
- * than returning a result arm. Match with `errors.is`; the remedy is to
332
- * move the interleaved write out of the callback (or make it first-attempt
333
- * only — the delta belongs on `tx`, premise reads on `snap`).
334
- */
335
- const ErrWitnessedLivelock = errors.new("bumbledb writeWitnessed livelock: the generation moved on every attempt — the callback itself commits an interleaved write each try, so no snapshot can ever stay current");
304
+ const ErrGenerationMoved = errors.new("bumbledb generationMoved: a state-changing commit landed since the witness snapshot");
336
305
  /**
337
306
  * Fills one insert's omitted fresh cells through the engine's
338
307
  * alloc-then-insert dyn lane (there is no insert-with-omitted-fields wire
@@ -410,15 +379,32 @@ function openDb(handle, theory, manifest) {
410
379
  if (entry === undefined) {
411
380
  throw errors.new(`bumbledb violation cites unknown statement id ${wire.statementId}`);
412
381
  }
413
- return Object.freeze({
414
- statement: entry.statement,
415
- kind: wire.kind,
416
- canonical: wire.canonical,
417
- direction: wire.direction,
418
- orientation: orientationOf(entry.reversed),
419
- measure: wire.measure,
420
- facts: Object.freeze(wire.facts.map(offendingFactOf))
421
- });
382
+ const facts = Object.freeze(wire.facts.map(offendingFactOf));
383
+ const statement = entry.statement;
384
+ const canonical = wire.canonical;
385
+ if (entry.kind === "functionality") {
386
+ return Object.freeze({ kind: "functionality", statement, canonical, facts });
387
+ }
388
+ if (entry.kind === "capacity") {
389
+ if (wire.kind !== "capacity") {
390
+ throw errors.new(`bumbledb violation ${wire.statementId} is a capacity slot without a measure`);
391
+ }
392
+ return Object.freeze({ kind: "capacity", statement, canonical, measure: wire.measure, facts });
393
+ }
394
+ if (wire.kind !== "containment") {
395
+ throw errors.new(`bumbledb violation ${wire.statementId} is a containment slot without a direction`);
396
+ }
397
+ if (entry.kind === "mirrors") {
398
+ return Object.freeze({
399
+ kind: "containment",
400
+ statement,
401
+ canonical,
402
+ direction: wire.direction,
403
+ orientation: entry.orientation,
404
+ facts
405
+ });
406
+ }
407
+ return Object.freeze({ kind: "containment", statement, canonical, direction: wire.direction, facts });
422
408
  }
423
409
  /**
424
410
  * Resolves a key-statement-selected read: the statement must be the
@@ -435,13 +421,13 @@ function openDb(handle, theory, manifest) {
435
421
  if (entry === undefined) {
436
422
  throw errors.new(`keyed get statement is not a declared statement of schema ${theory.name} — statement identity is the membership rule`);
437
423
  }
438
- if (entry.kind !== "functionality" || entry.key === undefined) {
424
+ if (entry.kind !== "functionality") {
439
425
  throw errors.new("keyed get takes a key() statement — containments and capacity statements key nothing");
440
426
  }
441
- if (entry.key.owner !== relation.name) {
442
- throw errors.new(`keyed get statement keys ${entry.key.owner}, not ${relation.name} — the statement must be a declared key of the relation it reads`);
427
+ if (entry.owner !== relation.name) {
428
+ throw errors.new(`keyed get statement keys ${entry.owner}, not ${relation.name} — the statement must be a declared key of the relation it reads`);
443
429
  }
444
- return Object.freeze({ statementId, projection: entry.key.projection });
430
+ return Object.freeze({ statementId, projection: entry.projection });
445
431
  }
446
432
  function pointReadsOf(assertLive, reads) {
447
433
  function contains(relation, fact) {
@@ -530,14 +516,6 @@ function openDb(handle, theory, manifest) {
530
516
  });
531
517
  return decodeAnswers(plan.finds, rows);
532
518
  }
533
- function explain(prepared, params) {
534
- assertLive();
535
- const plan = planOf(prepared);
536
- const wire = wireParams(plan.params, recordOf(params));
537
- return bridged("explain bumbledb prepared query", function callExplain() {
538
- return native.preparedExplain(plan.handle, state.handle, wire);
539
- });
540
- }
541
519
  /** The R12 teardown: invalidate, then close — idempotent through the state's close latch. */
542
520
  function dispose() {
543
521
  state.live = false;
@@ -549,7 +527,6 @@ function openDb(handle, theory, manifest) {
549
527
  get: reads.get,
550
528
  contains: reads.contains,
551
529
  execute,
552
- explain,
553
530
  [Symbol.dispose]: dispose
554
531
  });
555
532
  scopeStates.set(scope, state);
@@ -638,17 +615,9 @@ function openDb(handle, theory, manifest) {
638
615
  return snap.execute(prepared, params);
639
616
  });
640
617
  }
641
- function explain(prepared, params) {
642
- return read(function explainInScope(snap) {
643
- return snap.explain(prepared, params);
644
- });
645
- }
646
618
  /**
647
- * Builds one {@link Tx} over a transaction-handle thunk: `write` passes
648
- * an already-begun handle; `writeWitnessed` passes a LAZY thunk that
649
- * begins the witnessed transaction on the first delta verb (so premise
650
- * reads and the host's own interleaved writes can precede it) and
651
- * throws {@link generationMovedSignal} when the witness is stale.
619
+ * Builds one {@link Tx} over a transaction-handle thunk: `write` and
620
+ * `writeFrom` pass an already-begun handle.
652
621
  */
653
622
  function makeTx(resolveTx) {
654
623
  const txState = { spent: false };
@@ -784,181 +753,42 @@ function openDb(handle, theory, manifest) {
784
753
  return runDelta(txHandle, fn);
785
754
  }
786
755
  /**
787
- * Commits an already-begun witnessed transaction and closes the
788
- * attempt's snapshot: the committed generation, or the engine's
789
- * complete violation set as data.
756
+ * One-shot write from a live snapshot this store owns. Begins immediately;
757
+ * a moved generation throws {@link ErrGenerationMoved} and `fn` never
758
+ * runs. The snapshot stays owned by the caller's read callback.
790
759
  */
791
- function commitWitnessed(state, txHandle) {
792
- const committed = errors.trySync(function commitWitnessedDelta() {
793
- return bridged("commit bumbledb witnessed write transaction", function commit() {
794
- return native.txCommit(txHandle);
795
- });
796
- });
797
- if (committed.error) {
798
- /** Same one-writer law as `runDelta`: a thrown commit aborts before rethrowing. */
799
- const aborted = errors.trySync(function abortAfterFailedCommit() {
800
- native.txAbort(txHandle);
801
- });
802
- if (aborted.error) {
803
- }
804
- closeScopeState(state);
805
- throw errors.wrap(committed.error, "commit bumbledb witnessed write transaction");
760
+ function writeFrom(snap, fn) {
761
+ const snapState = scopeStates.get(snap);
762
+ if (snapState === undefined) {
763
+ throw errors.new("bumbledb writeFrom witness is not a read scope of this SDK");
806
764
  }
807
- const outcome = committed.data;
808
- closeScopeState(state);
809
- if (outcome.ok) {
810
- return Object.freeze({ ok: true, generation: outcome.generation });
765
+ if (snapState.owner !== owner) {
766
+ throw errors.new(`bumbledb writeFrom snapshot belongs to a different store (schema ${theory.name})`);
811
767
  }
812
- return Object.freeze({
813
- ok: false,
814
- violations: Object.freeze(outcome.violations.map(violationOf))
815
- });
816
- }
817
- /**
818
- * One attempt of the witnessed loop: fresh snapshot, the callback over
819
- * its scope and a LAZILY-begun witnessed transaction (the first delta
820
- * verb begins it, so premise reads and the host's own interleaved
821
- * writes can precede the witness check), then the witnessed commit —
822
- * or the abandon abort, which never issues a commit. Returns
823
- * `undefined` exactly when the generation moved and the whole callback
824
- * must rerun on a fresh snapshot.
825
- */
826
- function witnessedAttempt(fn) {
827
- const state = openScopeState();
828
- const scope = makeScope(state);
829
- const pending = { tx: undefined };
830
- function beginWitnessed() {
831
- const witnessed = bridged("begin witnessed bumbledb write transaction", function begin() {
832
- return native.dbWriteFrom(handle, state.handle);
833
- });
834
- if (!witnessed.ok) {
835
- return undefined;
836
- }
837
- return witnessed.tx;
768
+ if (!snapState.live) {
769
+ throw errors.new("bumbledb writeFrom snapshot is invalidated — its owning read callback already returned");
838
770
  }
839
- const made = makeTx(function resolveWitnessedTx() {
840
- if (pending.tx === undefined) {
841
- const begun = beginWitnessed();
842
- if (begun === undefined) {
843
- throw generationMovedSignal;
844
- }
845
- pending.tx = begun;
846
- }
847
- return pending.tx;
771
+ const witnessed = bridged("begin witnessed bumbledb write transaction", function begin() {
772
+ return native.dbWriteFrom(handle, snapState.handle);
848
773
  });
849
- const built = errors.trySync(function computeWitnessed() {
850
- return fn(scope, made.tx);
851
- });
852
- made.spend();
853
- state.live = false;
854
- /**
855
- * Aborts the pending transaction if one was begun. A faulted abort
856
- * still closes the attempt's snapshot BEFORE rethrowing — every
857
- * openScopeState is paired with closeScopeState on every exit, or a
858
- * reader slot and its snapshot worker leak for the process's lifetime.
859
- */
860
- function abortPending() {
861
- const txHandle = pending.tx;
862
- if (txHandle === undefined) {
863
- return;
864
- }
865
- const aborted = errors.trySync(function abort() {
866
- native.txAbort(txHandle);
867
- });
868
- if (aborted.error) {
869
- closeScopeState(state);
870
- throw errors.wrap(aborted.error, "abort bumbledb witnessed write transaction");
871
- }
872
- }
873
- if (built.error) {
874
- abortPending();
875
- closeScopeState(state);
876
- if (errors.is(built.error, generationMovedSignal)) {
877
- return undefined;
878
- }
879
- throw errors.wrap(built.error, "build witnessed write delta");
880
- }
881
- if (isThenable(built.data)) {
882
- /** The same async-callback refusal as `runDelta` — a thenable means the real delta build races the commit; nothing is committed. */
883
- abortPending();
884
- closeScopeState(state);
885
- throw errors.new("bumbledb writeWitnessed callback returned a thenable — the delta build is synchronous; an async callback is refused, nothing was committed");
886
- }
887
- if (isAbandon(built.data)) {
888
- abortPending();
889
- closeScopeState(state);
890
- return abandonedOutcome(built.data);
891
- }
892
- const late = errors.trySync(function resolveCommitTx() {
893
- if (pending.tx === undefined) {
894
- return beginWitnessed();
895
- }
896
- return pending.tx;
897
- });
898
- if (late.error) {
899
- /** A faulted late begin must not leak the attempt's snapshot either. */
900
- closeScopeState(state);
901
- throw late.error;
902
- }
903
- const txHandle = late.data;
904
- if (txHandle === undefined) {
905
- closeScopeState(state);
906
- return undefined;
774
+ if (!witnessed.ok) {
775
+ throw errors.wrap(ErrGenerationMoved, `writeFrom: generation moved (witnessed ${witnessed.witnessed} current ${witnessed.current}) against schema ${theory.name}`);
907
776
  }
908
- return commitWitnessed(state, txHandle);
909
- }
910
- /**
911
- * The witnessed retry loop. What it retries: the benign race — the
912
- * host's OWN interleaved commit landing between an attempt's snapshot
913
- * and its witnessed begin (every writer shares this handle, so a move
914
- * is always self-inflicted) — by rerunning the WHOLE callback on a
915
- * fresh snapshot, which converges because each rerun witnesses a
916
- * strictly newer generation. What it refuses: the pathology where the
917
- * callback itself (even indirectly) issues a plain `db.write` before
918
- * its first tx verb, moving the generation on EVERY attempt — an
919
- * unbounded loop would spin forever with no diagnostic, so past
920
- * {@link WITNESSED_ATTEMPT_CAP} attempts the loop throws the typed
921
- * {@link ErrWitnessedLivelock} instead (the engine's ruling: the
922
- * error, never a loop — retry is host policy, and this is the host
923
- * policy's own honesty bound).
924
- */
925
- function writeWitnessed(fn) {
926
- for (let attempts = 0; attempts < WITNESSED_ATTEMPT_CAP; attempts += 1) {
927
- const attempt = witnessedAttempt(fn);
928
- if (attempt !== undefined) {
929
- return attempt;
930
- }
931
- }
932
- throw errors.wrap(ErrWitnessedLivelock, `writeWitnessed livelock: the generation moved on all ${WITNESSED_ATTEMPT_CAP} attempts against schema ${theory.name}`);
777
+ return runDelta(witnessed.tx, fn);
933
778
  }
934
779
  function prepare(q) {
935
780
  if (q.schema !== theory) {
936
781
  throw errors.new(`query was built against schema ${q.schema.name}, not the identical schema value this store opened with — schema identity is the membership rule`);
937
782
  }
938
- const program = lowerQuery(q);
939
- const outcome = bridged("prepare bumbledb program", function callPrepare() {
940
- return native.dbPrepare(handle, program);
783
+ const queryIr = lowerQuery(q);
784
+ const outcome = bridged("prepare bumbledb query", function callPrepare() {
785
+ return native.dbPrepare(handle, queryIr);
941
786
  });
942
787
  if (!outcome.ok) {
943
788
  throw errors.new(`bumbledb ${outcome.kind} (prepare): ${outcome.message}`);
944
789
  }
945
790
  const preparedHandle = outcome.prepared;
946
- function staleness(snap) {
947
- const snapState = scopeStates.get(snap);
948
- if (snapState === undefined) {
949
- throw errors.new("bumbledb staleness witness is not a read scope of this SDK");
950
- }
951
- if (snapState.owner !== owner) {
952
- throw errors.new(`bumbledb read scope belongs to a different store than this prepared value (schema ${theory.name})`);
953
- }
954
- if (!snapState.live) {
955
- throw errors.new("bumbledb read scope is invalidated — its owning read callback already returned");
956
- }
957
- return bridged("read bumbledb prepared staleness", function callStaleness() {
958
- return native.preparedStaleness(preparedHandle, snapState.handle);
959
- });
960
- }
961
- const prepared = Object.freeze({ staleness });
791
+ const prepared = Object.freeze({});
962
792
  preparedPlans.set(prepared, Object.freeze({
963
793
  handle: preparedHandle,
964
794
  owner,
@@ -975,44 +805,11 @@ function openDb(handle, theory, manifest) {
975
805
  get,
976
806
  contains,
977
807
  execute,
978
- explain,
979
808
  write,
980
- writeWitnessed,
809
+ writeFrom,
981
810
  prepare
982
811
  });
983
812
  }
984
- /**
985
- * The per-process store cache, keyed by canonical path
986
- * (`node:path.resolve` — absolute and normalized). Symlink aliasing is
987
- * deliberately not resolved here: an aliased spelling misses the cache and
988
- * reaches the engine, whose exclusive lock refuses a second live handle on
989
- * the same store — the backstop that keeps "one store, one handle" true.
990
- */
991
- const openStores = new Map();
992
- /**
993
- * The in-process fingerprint check and its typing proof in one probe:
994
- * theory identity (`===`) implies `Rels` identity, because a cache entry's
995
- * `db` was constructed from that very theory value — so a hit narrows the
996
- * entry's `db` to `Db<Rels>` with no assertion anywhere.
997
- */
998
- function holdsTheory(entry, theory) {
999
- return entry.theory === theory;
1000
- }
1001
- /**
1002
- * The best-effort exit hook: closes every cached environment so LMDB
1003
- * releases its locks tidily on a clean exit. CORRECTNESS NEVER RESTS HERE —
1004
- * the engine fsyncs every commit, so a process killed before (or during)
1005
- * this hook loses nothing that was committed.
1006
- */
1007
- process.once("exit", function closeCachedStores() {
1008
- for (const cached of openStores.values()) {
1009
- const closed = errors.trySync(function closeEnvironment() {
1010
- native.dbClose(cached.handle);
1011
- });
1012
- if (closed.error) {
1013
- }
1014
- }
1015
- });
1016
813
  /**
1017
814
  * The engine twin of the schema-level class wall, as a matchable value
1018
815
  * (`errors.is`): the shared lowering rejected a spec whose statement pairs
@@ -1043,30 +840,19 @@ function refuseShadowedChanged(theory) {
1043
840
  }
1044
841
  }
1045
842
  /**
1046
- * The one admission path both verbs share: canonical-path cache lookup
1047
- * first (a hit returns the SAME `Db` value for the identical theory, a
1048
- * typed fingerprint error for a different one, and a typed refusal for
1049
- * `create` the store a cache entry proves initialized is exactly what
1050
- * create refuses). On a miss: lower the theory, run one bridge call, and
1051
- * wrap the domain refusals `schemaError` (spec resolution + schema
1052
- * validation, every issue in one message), `newtypeMismatch` (the
1053
- * coherence wall, {@link ErrNewtypeMismatch}), and `fingerprintMismatch`
1054
- * (a different theory cannot open the store) — into typed errors carrying
1055
- * the engine's message intact.
843
+ * The one admission path both verbs share: lower the theory, run one
844
+ * bridge call, and wrap the domain refusals — `schemaError` (spec
845
+ * resolution + schema validation, every issue in one message),
846
+ * `newtypeMismatch` (the coherence wall, {@link ErrNewtypeMismatch}),
847
+ * and `fingerprintMismatch` (a different theory cannot open the store)
848
+ * into typed errors carrying the engine's message intact.
849
+ * Environment failures (a second live writer on the same path, IO)
850
+ * throw from the bridge; the engine's `EnvironmentLocked` message is
851
+ * "another live handle holds this environment's lock".
1056
852
  */
1057
853
  function admit(verb, storePath, theory) {
1058
854
  refuseShadowedChanged(theory);
1059
855
  const canonical = path.resolve(storePath);
1060
- const cached = openStores.get(canonical);
1061
- if (cached !== undefined) {
1062
- if (verb === "create") {
1063
- throw errors.new(`create bumbledb store at ${canonical}: the store is already open in this process — create refuses an already-initialized directory`);
1064
- }
1065
- if (!holdsTheory(cached, theory)) {
1066
- throw errors.new(`bumbledb fingerprintMismatch (open ${canonical}): the cached store was opened with schema ${cached.theory.name}, not this theory value — schema identity is the membership rule`);
1067
- }
1068
- return cached.db;
1069
- }
1070
856
  const spec = lower(theory);
1071
857
  const opened = bridged(`${verb} bumbledb store at ${canonical}`, function callBridge() {
1072
858
  if (verb === "create") {
@@ -1083,34 +869,30 @@ function admit(verb, storePath, theory) {
1083
869
  const manifest = bridged("fetch bumbledb manifest", function fetchManifest() {
1084
870
  return native.dbManifest(opened.db);
1085
871
  });
1086
- const db = openDb(opened.db, theory, manifest);
1087
- openStores.set(canonical, Object.freeze({ theory, db, handle: opened.db }));
1088
- return db;
872
+ return openDb(opened.db, theory, manifest);
1089
873
  }
1090
874
  /**
1091
875
  * The store lifecycle — `Db.create(path, schema)` / `Db.open(path, schema)`.
1092
876
  * Create refuses an already-initialized directory; open verifies format
1093
- * version, store kind, and the schema fingerprint. Both return values
1094
- * CACHED per canonical path: a second open of the same path with the
1095
- * identical theory value returns the SAME `Db`, and a different theory on
1096
- * a cached path is a typed fingerprint error. There is no close anywhere:
1097
- * the process owns every cached environment until exit (a best-effort exit
1098
- * hook closes them; durability is the engine's per-commit fsync). One
1099
- * store kind exists: durable — resume = reopen, meaning this process's
1100
- * cached value or a fresh process's open.
877
+ * version, store kind, and the schema fingerprint. A second live handle
878
+ * on the same path is the engine's `EnvironmentLocked`. There is no close
879
+ * anywhere: the process owns the environment until GC/exit (durability is
880
+ * the engine's per-commit fsync). One store kind exists: durable
881
+ * resume = reopen in a fresh process, or hold the `Db` this process opened.
1101
882
  */
1102
883
  const Db = Object.freeze({
1103
- /** Creates a fresh durable store at `path` from the schema; the value is cached for every later open. */
884
+ /** Creates a fresh durable store at `path` from the schema. */
1104
885
  async create(path, theory) {
1105
886
  return admit("create", path, theory);
1106
887
  },
1107
888
  /**
1108
- * Opens an existing durable store at `path` with the same theory — the
1109
- * cached value when this process already holds it. A fingerprint-matching
1110
- * open also BACK-FILLS the store's persisted schema descriptor when it is
1111
- * absent (self-describing stores, engine 50-storage.md § the `_meta`
1112
- * block), so a legacy store becomes exhumable after one ordinary open —
1113
- * adoption is automatic, never a separate verb.
889
+ * Opens an existing durable store at `path` with the same theory.
890
+ * A fingerprint-matching open also BACK-FILLS the store's persisted
891
+ * schema descriptor when it is absent (self-describing stores, engine
892
+ * 50-storage.md § the `_meta` block), so a legacy store becomes
893
+ * exhumable after one ordinary open — adoption is automatic, never a
894
+ * separate verb. A second open of a still-live path is
895
+ * `EnvironmentLocked`.
1114
896
  */
1115
897
  async open(path, theory) {
1116
898
  return admit("open", path, theory);
@@ -1131,5 +913,5 @@ const Db = Object.freeze({
1131
913
  return exhumeStore(path.resolve(storePath));
1132
914
  }
1133
915
  });
1134
- export { abandon, Db, ErrNewtypeMismatch, ErrWitnessedLivelock, WITNESSED_ATTEMPT_CAP };
916
+ export { abandon, Db, ErrGenerationMoved, ErrNewtypeMismatch };
1135
917
  //# sourceMappingURL=db.js.map