@bjornpagen/bumbledb 0.14.0 → 0.17.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 (108) hide show
  1. package/COOKBOOK.md +58 -62
  2. package/README.md +82 -56
  3. package/dist/capacity.d.ts +24 -136
  4. package/dist/capacity.d.ts.map +1 -1
  5. package/dist/capacity.js +18 -40
  6. package/dist/capacity.js.map +1 -1
  7. package/dist/closed.d.ts +0 -156
  8. package/dist/closed.d.ts.map +1 -1
  9. package/dist/closed.js +0 -104
  10. package/dist/closed.js.map +1 -1
  11. package/dist/db.d.ts +93 -290
  12. package/dist/db.d.ts.map +1 -1
  13. package/dist/db.js +713 -556
  14. package/dist/db.js.map +1 -1
  15. package/dist/face.d.ts +0 -133
  16. package/dist/face.d.ts.map +1 -1
  17. package/dist/face.js +0 -33
  18. package/dist/face.js.map +1 -1
  19. package/dist/fields.d.ts +1 -145
  20. package/dist/fields.d.ts.map +1 -1
  21. package/dist/fields.js +2 -91
  22. package/dist/fields.js.map +1 -1
  23. package/dist/index.d.ts +13 -23
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +11 -20
  26. package/dist/index.js.map +1 -1
  27. package/dist/law.d.ts +111 -93
  28. package/dist/law.d.ts.map +1 -1
  29. package/dist/law.js +23 -27
  30. package/dist/law.js.map +1 -1
  31. package/dist/lower.d.ts +9 -35
  32. package/dist/lower.d.ts.map +1 -1
  33. package/dist/lower.js +8 -53
  34. package/dist/lower.js.map +1 -1
  35. package/dist/marshal.d.ts +0 -65
  36. package/dist/marshal.d.ts.map +1 -1
  37. package/dist/marshal.js +0 -72
  38. package/dist/marshal.js.map +1 -1
  39. package/dist/native.d.ts +97 -390
  40. package/dist/native.d.ts.map +1 -1
  41. package/dist/native.js +39 -61
  42. package/dist/native.js.map +1 -1
  43. package/dist/query/atom.d.ts +10 -276
  44. package/dist/query/atom.d.ts.map +1 -1
  45. package/dist/query/atom.js +1 -96
  46. package/dist/query/atom.js.map +1 -1
  47. package/dist/query/find.d.ts +10 -76
  48. package/dist/query/find.d.ts.map +1 -1
  49. package/dist/query/find.js +0 -30
  50. package/dist/query/find.js.map +1 -1
  51. package/dist/query/lower.d.ts +64 -146
  52. package/dist/query/lower.d.ts.map +1 -1
  53. package/dist/query/lower.js +19 -256
  54. package/dist/query/lower.js.map +1 -1
  55. package/dist/query/parse-ir.d.ts +0 -7
  56. package/dist/query/parse-ir.d.ts.map +1 -1
  57. package/dist/query/parse-ir.js +1 -13
  58. package/dist/query/parse-ir.js.map +1 -1
  59. package/dist/query/run.d.ts +0 -36
  60. package/dist/query/run.d.ts.map +1 -1
  61. package/dist/query/run.js +0 -44
  62. package/dist/query/run.js.map +1 -1
  63. package/dist/query/scope.d.ts +24 -180
  64. package/dist/query/scope.d.ts.map +1 -1
  65. package/dist/query/scope.js +2 -66
  66. package/dist/query/scope.js.map +1 -1
  67. package/dist/relation.d.ts +2 -50
  68. package/dist/relation.d.ts.map +1 -1
  69. package/dist/relation.js +2 -37
  70. package/dist/relation.js.map +1 -1
  71. package/dist/schema.d.ts +13 -63
  72. package/dist/schema.d.ts.map +1 -1
  73. package/dist/schema.js +118 -92
  74. package/dist/schema.js.map +1 -1
  75. package/dist/spec.d.ts +1 -140
  76. package/dist/spec.d.ts.map +1 -1
  77. package/dist/spec.js +1 -68
  78. package/dist/spec.js.map +1 -1
  79. package/dist/statements.d.ts +6 -137
  80. package/dist/statements.d.ts.map +1 -1
  81. package/dist/statements.js +16 -119
  82. package/dist/statements.js.map +1 -1
  83. package/package.json +3 -3
  84. package/src/capacity.ts +26 -140
  85. package/src/closed.ts +5 -206
  86. package/src/db.ts +997 -854
  87. package/src/face.ts +0 -142
  88. package/src/fields.ts +4 -172
  89. package/src/index.ts +32 -35
  90. package/src/law.ts +201 -129
  91. package/src/lower.ts +8 -53
  92. package/src/marshal.ts +1 -85
  93. package/src/native.ts +192 -413
  94. package/src/query/atom.ts +26 -313
  95. package/src/query/find.ts +24 -110
  96. package/src/query/lower.ts +132 -377
  97. package/src/query/parse-ir.ts +1 -14
  98. package/src/query/run.ts +0 -45
  99. package/src/query/scope.ts +25 -186
  100. package/src/relation.ts +2 -66
  101. package/src/schema.ts +143 -122
  102. package/src/spec.ts +1 -160
  103. package/src/statements.ts +22 -174
  104. package/dist/exhume.d.ts +0 -143
  105. package/dist/exhume.d.ts.map +0 -1
  106. package/dist/exhume.js +0 -166
  107. package/dist/exhume.js.map +0 -1
  108. package/src/exhume.ts +0 -267
package/dist/db.js CHANGED
@@ -2,18 +2,16 @@
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, and read through scoped snapshots — all typed by
6
- * the schema's relations record.
5
+ * keyed to statements, and read through a synchronous instance callback —
6
+ * all typed by the schema's relations record.
7
7
  *
8
- * LIFETIMES ARE DISPOSABLES, never `close()` (ruled 2026-07-23, R12): no
9
- * value this module returns carries a close spelling — release is
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.
8
+ * A store read is one callback: `db.read((instance, witness) => …)`. The
9
+ * instance is invalid the moment the callback returns; the witness is a
10
+ * cloneable token and may escape. There is no handle-shaped read and no
11
+ * `using snap = db.read`. Builder, owned instance, and witness
12
+ * implement `Symbol.dispose`. Prepared plans are plain values whose
13
+ * engine-side half is reclaimed by a GC finalizer — reclamation only,
14
+ * never correctness.
17
15
  *
18
16
  * PROCESS MODEL: one process, one exclusive-lock handle per store. The
19
17
  * `Db` value owns the LMDB environment's exclusive lock until process
@@ -24,22 +22,56 @@
24
22
  * policy.
25
23
  *
26
24
  * REJECTION IS DATA: a rejected commit is a domain outcome (it becomes the
27
- * LLM repair prompt downstream), returned as a {@link WriteResult} carrying
28
- * {@link Violation} values. Genuine failures — I/O, used-after-scope,
29
- * marshal shape, a moved generation on {@link Db.writeFrom} — throw
30
- * `@superbuilders/errors` wrapped errors instead.
25
+ * LLM repair prompt downstream), returned as a {@link WriteOutcome}
26
+ * carrying {@link Violation} values. A moved generation on
27
+ * {@link Db.writeFrom} is the `{ tag: "moved" }` arm, not an exception.
28
+ * Genuine failures — I/O, used-after-scope, spent handle, marshal shape —
29
+ * throw `@superbuilders/errors` wrapped errors instead.
31
30
  */
32
31
  import * as path from "node:path";
33
32
  import * as errors from "@superbuilders/errors";
34
33
  import { isClosedMember, sealedFieldsOf } from "#closed.ts";
35
- import { exhumeStore } from "#exhume.ts";
36
34
  import { rosterOf } from "#fields.ts";
37
35
  import { lower } from "#lower.ts";
38
- import { factOf, handleOf, isFreshField, keyRowOf, recordOf, rowOf } from "#marshal.ts";
39
- import { bridged, native } from "#native.ts";
36
+ import { cellOf, factOf, handleOf, isFreshField, keyRowOf, recordOf, rowOf } from "#marshal.ts";
37
+ import { bridged, bridgedAsync, errorFromThrow, native } from "#native.ts";
40
38
  import { lowerQuery } from "#query/lower.ts";
41
39
  import { decodeAnswers, wireParams } from "#query/run.ts";
42
40
  import { isStatement } from "#statements.ts";
41
+ /**
42
+ * The flat projector: every fact's cells land in ONE row-major
43
+ * `FactValue` array (length rows×arity) — no JS array per fact exists
44
+ * anywhere between the caller's objects and the native crossing
45
+ * (proposals/one-representation/20, V1) — and the row count is counted
46
+ * while projecting (the {@link FlatCollection} law: the stated count is
47
+ * what the bridge verifies against `rows × arity`, exactly, for every
48
+ * arity). The per-cell judgment is `cellOf` — the one cell judge `rowOf`
49
+ * also speaks (closed handle→id, well-formedness, interval shape) — and
50
+ * the missing-field refusal is `rowOf`'s, byte for byte; only the output
51
+ * form differs (flat, never per-row).
52
+ */
53
+ function rowsOf(relation, facts) {
54
+ const data = relation.data;
55
+ const cells = [];
56
+ let rows = 0n;
57
+ for (const fact of facts) {
58
+ rows += 1n;
59
+ const record = recordOf(fact);
60
+ for (const declared of data.fields) {
61
+ const value = record[declared.name];
62
+ if (value === undefined) {
63
+ throw errors.new(`relation ${data.name}: fact is missing field ${declared.name}`);
64
+ }
65
+ cells.push(cellOf(`relation ${data.name} field ${declared.name}`, declared.field, value));
66
+ }
67
+ }
68
+ return { rows, cells };
69
+ }
70
+ function mutateCollection(relation, facts, apply) {
71
+ const flat = rowsOf(relation, facts);
72
+ const report = apply(flat.rows, flat.cells);
73
+ return Object.freeze({ submitted: report.submitted, changed: report.changed });
74
+ }
43
75
  function freshRangeOf(wire) {
44
76
  if (wire.empty) {
45
77
  return Object.freeze({
@@ -74,70 +106,81 @@ function freshRangeOf(wire) {
74
106
  }
75
107
  });
76
108
  }
77
- /**
78
- * The runtime discriminant of {@link Abandon} values — a property probe is
79
- * how `write`/`writeFrom` distinguish "abort without committing" from an
80
- * ordinary callback result, never a guess about the host's own value shapes.
81
- */
82
109
  const abandonMark = Symbol("bumbledb.abandon");
83
- /**
84
- * Wraps a payload in the {@link Abandon} sentinel — the one way a write
85
- * callback declines to commit: `return abandon(payload)` aborts the delta
86
- * (nothing is committed, not even an empty commit) and the write resolves
87
- * to `{ ok: false, abandoned: payload }`, from `write` and `writeFrom`
88
- * alike (R10).
89
- */
90
110
  function abandon(payload) {
91
111
  return Object.freeze({ [abandonMark]: true, payload });
92
112
  }
93
- /**
94
- * Narrows a write callback result to the abandon sentinel. The probe is
95
- * the private {@link abandonMark} symbol only {@link abandon} sets, and
96
- * `R`'s `Abandon` arm is the only way a sentinel can flow out of the
97
- * callback — so the narrowed payload type is sound by construction.
98
- */
99
113
  function isAbandon(value) {
100
114
  return typeof value === "object" && value !== null && abandonMark in value;
101
115
  }
102
- /**
103
- * The abandon outcome's trusted admission seam: the value's shape is the
104
- * checkable half (the sentinel mark only {@link abandon} mints, and the
105
- * outcome carrying that sentinel's own payload), and the sentinel's
106
- * existence IS the proof `R` carries an `Abandon` arm — so the outcome is
107
- * admitted at the conditional {@link AbandonedArm} face the type tier
108
- * cannot resolve over an open `R`.
109
- */
110
116
  function isAbandonedOutcome(outcome, sentinel) {
111
117
  return isAbandon(sentinel) && outcome.abandoned === sentinel.payload;
112
118
  }
113
- /** Builds the abandoned write outcome from the callback's own sentinel (the R10 arm's one mint). */
114
119
  function abandonedOutcome(sentinel) {
115
- const outcome = Object.freeze({ ok: false, abandoned: sentinel.payload });
120
+ const outcome = Object.freeze({ tag: "abandoned", abandoned: sentinel.payload });
116
121
  if (!isAbandonedOutcome(outcome, sentinel)) {
117
122
  throw errors.new("bumbledb abandon outcome construction incomplete");
118
123
  }
119
124
  return outcome;
120
125
  }
121
- /**
122
- * The module-private inference slot of {@link Prepared}: an optional symbol
123
- * property (never set at runtime) that keeps the prepared value's `Row` and
124
- * `Params` type arguments load-bearing, so `execute` infers the typed rows
125
- * and the typed params object from the value alone — the query module's
126
- * `inferred` pattern, local to this module. A type-level carrier only:
127
- * values stay bare, nothing is asserted.
128
- */
126
+ const witnessTypes = Symbol("bumbledb.witness.types");
129
127
  const preparedTypes = Symbol("bumbledb.prepared.types");
130
- /**
131
- * Mirrors the engine's materialized statement order
132
- * (`SchemaDescriptor::materialized_statements`, pinned by the fingerprint):
133
- * one auto-key per fresh field (relation declaration order, then field
134
- * order), one closed auto-key per closed relation (declaration order),
135
- * then the declared statements in declaration order — a `mirrors`
136
- * statement occupying TWO adjacent slots (the engine lowers `==` to two
137
- * containments, `source <= target` first), both owned by the one SDK
138
- * value. This positional match is how statement ids resolve back to SDK
139
- * statement values without the engine ever learning a wire format.
140
- */
128
+ function decodeOffendingFact(member, relation, fact) {
129
+ const declared = sealedFieldsOf(member);
130
+ const decoded = {};
131
+ for (const cell of fact.fields) {
132
+ const cited = declared.find(function byName(candidate) {
133
+ return candidate.name === cell.name;
134
+ });
135
+ const roster = rosterOf(cited?.field);
136
+ decoded[cell.name] =
137
+ roster !== undefined
138
+ ? handleOf(`violation fact ${fact.relation} field ${cell.name}`, roster, cell.value)
139
+ : cell.value;
140
+ }
141
+ return Object.freeze({ relation, fact: Object.freeze(decoded) });
142
+ }
143
+ function violationFromEntry(entry, wire, facts) {
144
+ const canonical = wire.canonical;
145
+ if (entry.kind === "functionality") {
146
+ if (!("statement" in entry)) {
147
+ return Object.freeze({ kind: "functionality", statement: undefined, canonical, facts });
148
+ }
149
+ return Object.freeze({ kind: "functionality", statement: entry.statement, canonical, facts });
150
+ }
151
+ if (entry.kind === "capacity") {
152
+ if (wire.kind !== "capacity") {
153
+ throw errors.new(`bumbledb violation ${wire.statementId} is a capacity slot without a measure`);
154
+ }
155
+ return Object.freeze({
156
+ kind: "capacity",
157
+ statement: entry.statement,
158
+ canonical,
159
+ measure: wire.measure,
160
+ facts
161
+ });
162
+ }
163
+ if (wire.kind !== "containment") {
164
+ throw errors.new(`bumbledb violation ${wire.statementId} is a containment slot without a direction`);
165
+ }
166
+ if (entry.kind === "mirrors") {
167
+ return Object.freeze({
168
+ kind: "containment",
169
+ statement: entry.statement,
170
+ canonical,
171
+ direction: wire.direction,
172
+ orientation: entry.orientation,
173
+ facts
174
+ });
175
+ }
176
+ return Object.freeze({
177
+ kind: "containment",
178
+ statement: entry.statement,
179
+ canonical,
180
+ direction: wire.direction,
181
+ facts
182
+ });
183
+ }
141
184
  function materializedEntries(theory) {
142
185
  const entries = impliedKeyEntries(theory);
143
186
  for (const statement of theory.statements) {
@@ -145,13 +188,6 @@ function materializedEntries(theory) {
145
188
  }
146
189
  return entries;
147
190
  }
148
- /**
149
- * The engine-materialized implied keys, in the engine's pinned order: one
150
- * auto-key per fresh field (relation declaration order, then field order),
151
- * then one closed auto-key `R(id) -> R` per closed relation (declaration
152
- * order). These slots carry no SDK statement value — the engine owns them
153
- * (`schema()` rejects an explicit duplicate).
154
- */
155
191
  function impliedKeyEntries(theory) {
156
192
  const entries = [];
157
193
  for (const member of Object.values(theory.relations)) {
@@ -179,12 +215,6 @@ function impliedKeyEntries(theory) {
179
215
  }
180
216
  return entries;
181
217
  }
182
- /**
183
- * One declared statement's materialized slots: a key or capacity statement
184
- * occupies one, a `mirrors` occupies two adjacent slots (the engine lowers
185
- * `==` to two containments, `source <= target` first), both owned by the
186
- * one SDK value.
187
- */
188
218
  function declaredEntries(statement) {
189
219
  const data = statement.data;
190
220
  switch (data.kind) {
@@ -221,26 +251,9 @@ function declaredEntries(statement) {
221
251
  function isThenable(value) {
222
252
  return typeof value === "object" && value !== null && "then" in value && typeof value.then === "function";
223
253
  }
224
- /**
225
- * Narrows a keyed-get middle argument to a statement value (vs a key
226
- * object) through the statement module's admission brand — a
227
- * REPRESENTATION, never a shape probe: fact cell shapes are structurally
228
- * OPEN (an interval value carrying an excess `kind` property is a legal
229
- * cell), so no property probe could ever be sound here, but no host-built
230
- * key object can spell the module-private brand symbol.
231
- */
232
254
  function isStatementValue(value) {
233
255
  return isStatement(value);
234
256
  }
235
- /**
236
- * THE one selector dispatch of the `get` overload pair (primary-key vs
237
- * key-statement, `docs/architecture/70-api.md` § the freeze): judges the
238
- * middle argument once and hands the narrowed pieces to the chosen
239
- * continuation. `Db.get` and the read scope's `get` both dispatch through
240
- * here, so the two mismatch refusals speak with one voice and the symmetry
241
- * rule (`db.get(...) === db.read(snap => snap.get(...))`) holds by
242
- * construction.
243
- */
244
257
  function selectKeyRead(keyOrStatement, declaredKey, byStatement, byPrimary) {
245
258
  if (declaredKey !== undefined) {
246
259
  if (!isStatementValue(keyOrStatement)) {
@@ -253,16 +266,6 @@ function selectKeyRead(keyOrStatement, declaredKey, byStatement, byPrimary) {
253
266
  }
254
267
  return byPrimary(keyOrStatement);
255
268
  }
256
- /**
257
- * Builds the id-resolution tables from the manifest, verifying the SDK's
258
- * positional mirror against the engine's reported order — any drift
259
- * (count, kind, id, or membership) is a construction-time failure, never a
260
- * silent misattribution of a violation to the wrong statement value. The
261
- * declaration-ordinal law the query lowering leans on is verified in the
262
- * same walks: relation ids and sealed field ids both equal declaration
263
- * order, so a constructed `Tables` IS the proof and `prepare` inherits it
264
- * structurally — never a silently misaddressed query.
265
- */
266
269
  function tablesOf(theory, manifest) {
267
270
  const entries = materializedEntries(theory);
268
271
  if (entries.length !== manifest.statements.length) {
@@ -312,16 +315,39 @@ function tablesOf(theory, manifest) {
312
315
  });
313
316
  return Object.freeze({ relations, statements: Object.freeze(entries) });
314
317
  }
315
- /** The private lifetime records of this module's read scopes. */
316
- const scopeStates = new WeakMap();
317
- /** The private engine halves of this module's prepared values. */
318
+ function tablesFromTheory(theory) {
319
+ const entries = materializedEntries(theory);
320
+ const relations = new Map();
321
+ Object.keys(theory.relations).forEach(function byOrdinal(name, ordinal) {
322
+ const member = theory.relations[name];
323
+ if (member === undefined) {
324
+ throw errors.new(`bumbledb theory has no relation ${name}`);
325
+ }
326
+ const fieldIds = new Map();
327
+ sealedFieldsOf(member).forEach(function byField(declared, fieldOrdinal) {
328
+ fieldIds.set(declared.name, fieldOrdinal);
329
+ });
330
+ let primaryKey;
331
+ entries.forEach(function firstOwnedKey(entry, index) {
332
+ if (primaryKey === undefined && entry.kind === "functionality" && entry.owner === name) {
333
+ primaryKey = Object.freeze({ statementId: index, projection: entry.projection });
334
+ }
335
+ });
336
+ relations.set(name, Object.freeze({ id: ordinal, member, fieldIds, primaryKey }));
337
+ });
338
+ return Object.freeze({ relations, statements: Object.freeze(entries) });
339
+ }
340
+ const instanceStates = new WeakMap();
341
+ const witnessStates = new WeakMap();
342
+ const witnessReclaimer = new FinalizationRegistry(function reclaimWitness(handle) {
343
+ const closed = errors.trySync(function closeWitness() {
344
+ native.witnessClose(handle);
345
+ });
346
+ if (closed.error) {
347
+ return;
348
+ }
349
+ });
318
350
  const preparedPlans = new WeakMap();
319
- /**
320
- * Reclaims the engine-side plan of a garbage-collected {@link Prepared}
321
- * value. RECLAMATION ONLY, never correctness: a plan the collector never
322
- * visits is idle engine memory until process exit, and a failure to close
323
- * is swallowed (there is no one left to care — the owning value is gone).
324
- */
325
351
  const planReclaimer = new FinalizationRegistry(function reclaimPlan(handle) {
326
352
  const closed = errors.trySync(function closePlan() {
327
353
  native.preparedClose(handle);
@@ -330,130 +356,219 @@ const planReclaimer = new FinalizationRegistry(function reclaimPlan(handle) {
330
356
  return;
331
357
  }
332
358
  });
333
- /**
334
- * The typed generation-moved refusal `writeFrom` throws when a
335
- * state-changing commit landed since the witness snapshot: retry is host
336
- * policy. Match with `errors.is`.
337
- */
338
- const ErrGenerationMoved = errors.new("bumbledb generationMoved: a state-changing commit landed since the witness snapshot");
339
- /**
340
- * Constructs one open `Db` over an already-admitted handle: builds the
341
- * id-resolution tables once and closes over them — the `Db` owns handle
342
- * and tables and nothing else. Handle lifetime is the process's: the store
343
- * cache holds the environment handle until the exit hook closes it.
344
- */
345
- function openDb(handle, theory, manifest) {
346
- const tables = tablesOf(theory, manifest);
347
- /** This store's identity token: read scopes and prepared values carry it, so cross-store use is a typed refusal. */
348
- const owner = Object.freeze({});
349
- function isMemberName(name) {
350
- return tables.relations.has(name);
351
- }
352
- function resolveOrdinary(relation) {
353
- const entry = tables.relations.get(relation.name);
354
- if (entry === undefined || entry.member !== relation) {
355
- throw errors.new(`relation ${relation.name} is not a member of schema ${theory.name}`);
359
+ const ErrAsyncCallback = errors.new("bumbledb asyncCallback: a read or write callback returned a thenable — the callback is synchronous");
360
+ const ErrSpentHandle = errors.new("bumbledb spentHandle: a consumed builder, instance, or witness was used");
361
+ const ErrUseAfterScope = errors.new("bumbledb useAfterScope: a stashed read instance or write transaction was used after its callback returned");
362
+ const ErrForeignPrepared = errors.new("bumbledb foreignPrepared: a prepared query met a foreign instance");
363
+ const ErrForeignWitness = errors.new("bumbledb foreignWitness: a witness met a foreign store");
364
+ function catalogMethods(theory, tables, owner, assertLive, ops) {
365
+ function planOf(prepared) {
366
+ const plan = preparedPlans.get(prepared);
367
+ if (plan === undefined) {
368
+ throw errors.wrap(ErrForeignPrepared, "bumbledb execute target is not a prepared value of this SDK");
356
369
  }
357
- if (isClosedMember(relation)) {
358
- throw errors.new(`relation ${relation.name} is closed — its extension is schema data (axioms), never scanned or written`);
370
+ if (plan.owner !== owner) {
371
+ throw errors.wrap(ErrForeignPrepared, `bumbledb prepared value was prepared by a different store than this one (schema ${theory.name})`);
359
372
  }
360
- return entry;
373
+ return plan;
361
374
  }
362
- function offendingFactOf(fact) {
363
- const entry = tables.relations.get(fact.relation);
364
- if (entry === undefined || !isMemberName(fact.relation)) {
365
- throw errors.new(`bumbledb violation cites unknown relation ${fact.relation}`);
366
- }
367
- const declared = sealedFieldsOf(entry.member);
368
- const decoded = {};
369
- for (const cell of fact.fields) {
370
- const cited = declared.find(function byName(candidate) {
371
- return candidate.name === cell.name;
372
- });
373
- const roster = rosterOf(cited?.field);
374
- decoded[cell.name] =
375
- roster !== undefined
376
- ? handleOf(`violation fact ${fact.relation} field ${cell.name}`, roster, cell.value)
377
- : cell.value;
378
- }
379
- return Object.freeze({ relation: fact.relation, fact: Object.freeze(decoded) });
375
+ function contains(relation, fact) {
376
+ assertLive();
377
+ const entry = ordinaryEntry(tables, theory, relation);
378
+ return bridged("bumbledb instance contains", function readContains() {
379
+ return ops.contains(entry.id, rowOf(relation.data, recordOf(fact)));
380
+ });
380
381
  }
381
- function violationOf(wire) {
382
- const entry = tables.statements[wire.statementId];
383
- if (entry === undefined) {
384
- throw errors.new(`bumbledb violation cites unknown statement id ${wire.statementId}`);
385
- }
386
- const facts = Object.freeze(wire.facts.map(offendingFactOf));
387
- const canonical = wire.canonical;
388
- if (entry.kind === "functionality") {
389
- if (!("statement" in entry)) {
390
- return Object.freeze({ kind: "functionality", statement: undefined, canonical, facts });
391
- }
392
- return Object.freeze({ kind: "functionality", statement: entry.statement, canonical, facts });
393
- }
394
- if (entry.kind === "capacity") {
395
- if (wire.kind !== "capacity") {
396
- throw errors.new(`bumbledb violation ${wire.statementId} is a capacity slot without a measure`);
382
+ function get(relation, keyOrStatement, declaredKey) {
383
+ assertLive();
384
+ const entry = ordinaryEntry(tables, theory, relation);
385
+ return selectKeyRead(keyOrStatement, declaredKey, function byStatement(statement, key) {
386
+ const selected = declaredKeyOf(tables, theory, relation, statement);
387
+ const row = bridged("bumbledb instance get", function readGet() {
388
+ return ops.get(entry.id, selected.statementId, keyRowOf(relation.data, selected.projection, recordOf(key)));
389
+ });
390
+ return row === null ? undefined : factOf(relation, row);
391
+ }, function byPrimary(key) {
392
+ const primaryKey = entry.primaryKey;
393
+ if (primaryKey === undefined) {
394
+ throw errors.new(`relation ${relation.name} has no candidate key — keyed get requires a fresh field or a declared key statement`);
397
395
  }
398
- return Object.freeze({
399
- kind: "capacity",
400
- statement: entry.statement,
401
- canonical,
402
- measure: wire.measure,
403
- facts
396
+ const row = bridged("bumbledb instance get", function readGet() {
397
+ return ops.get(entry.id, primaryKey.statementId, keyRowOf(relation.data, primaryKey.projection, recordOf(key)));
404
398
  });
399
+ return row === null ? undefined : factOf(relation, row);
400
+ });
401
+ }
402
+ function scan(relation) {
403
+ assertLive();
404
+ const entry = ordinaryEntry(tables, theory, relation);
405
+ const rows = bridged("bumbledb instance scan", function readScan() {
406
+ return ops.scan(entry.id);
407
+ });
408
+ return rows.map(function decodeRow(row) {
409
+ return factOf(relation, row);
410
+ });
411
+ }
412
+ function count(relation) {
413
+ assertLive();
414
+ const entry = ordinaryEntry(tables, theory, relation);
415
+ return bridged("bumbledb instance count", function readCount() {
416
+ return ops.count(entry.id);
417
+ });
418
+ }
419
+ function execute(prepared, params) {
420
+ assertLive();
421
+ const plan = planOf(prepared);
422
+ const wire = wireParams(plan.params, recordOf(params));
423
+ const rows = bridged("execute bumbledb prepared query", function callExecute() {
424
+ return ops.execute(plan.handle, wire);
425
+ });
426
+ return decodeAnswers(plan.finds, rows);
427
+ }
428
+ function prepare(q) {
429
+ assertLive();
430
+ if (q.schema !== theory) {
431
+ 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`);
405
432
  }
406
- if (wire.kind !== "containment") {
407
- throw errors.new(`bumbledb violation ${wire.statementId} is a containment slot without a direction`);
433
+ const queryIr = lowerQuery(q);
434
+ const outcome = bridged("prepare bumbledb query", function callPrepare() {
435
+ return ops.prepare(queryIr);
436
+ });
437
+ if (!outcome.ok) {
438
+ throwPrepareRefusal(outcome.message);
408
439
  }
409
- if (entry.kind === "mirrors") {
410
- return Object.freeze({
411
- kind: "containment",
412
- statement: entry.statement,
413
- canonical,
414
- direction: wire.direction,
415
- orientation: entry.orientation,
416
- facts
417
- });
440
+ const prepared = Object.freeze({});
441
+ preparedPlans.set(prepared, Object.freeze({
442
+ handle: outcome.prepared,
443
+ owner,
444
+ params: q.data.params,
445
+ finds: q.data.finds
446
+ }));
447
+ planReclaimer.register(prepared, outcome.prepared);
448
+ return prepared;
449
+ }
450
+ return { scan, count, get, contains, execute, prepare };
451
+ }
452
+ function ordinaryEntry(tables, theory, relation) {
453
+ const entry = tables.relations.get(relation.name);
454
+ if (entry === undefined || entry.member !== relation) {
455
+ throw errors.new(`relation ${relation.name} is not a member of schema ${theory.name}`);
456
+ }
457
+ if (isClosedMember(relation)) {
458
+ throw errors.new(`relation ${relation.name} is closed — its extension is schema data (axioms), never scanned or written`);
459
+ }
460
+ return entry;
461
+ }
462
+ function declaredKeyOf(tables, theory, relation, statement) {
463
+ const statementId = tables.statements.findIndex(function byIdentity(candidate) {
464
+ return "statement" in candidate && candidate.statement === statement;
465
+ });
466
+ const entry = tables.statements[statementId];
467
+ if (entry === undefined) {
468
+ throw errors.new(`keyed get statement is not a declared statement of schema ${theory.name} — statement identity is the membership rule`);
469
+ }
470
+ if (entry.kind !== "functionality") {
471
+ throw errors.new("keyed get takes a key() statement — containments and capacity statements key nothing");
472
+ }
473
+ if (entry.owner !== relation.name) {
474
+ throw errors.new(`keyed get statement keys ${entry.owner}, not ${relation.name} — the statement must be a declared key of the relation it reads`);
475
+ }
476
+ return Object.freeze({ statementId, projection: entry.projection });
477
+ }
478
+ function overlayMethods(theory, tables, assertLive, reads) {
479
+ function contains(relation, fact) {
480
+ assertLive();
481
+ const entry = ordinaryEntry(tables, theory, relation);
482
+ return reads.contains(entry.id, rowOf(relation.data, recordOf(fact)));
483
+ }
484
+ function readThroughKey(relation, entry, selected, key) {
485
+ const row = reads.get(entry.id, selected.statementId, keyRowOf(relation.data, selected.projection, key));
486
+ if (row === null) {
487
+ return undefined;
418
488
  }
419
- return Object.freeze({
420
- kind: "containment",
421
- statement: entry.statement,
422
- canonical,
423
- direction: wire.direction,
424
- facts
425
- });
489
+ return factOf(relation, row);
426
490
  }
427
- /**
428
- * Resolves a key-statement-selected read: the statement must be the
429
- * IDENTICAL `key()` value this schema declared (identity is the
430
- * membership rule) and must key `relation` — its materialized statement
431
- * id comes from the positional mirror, so the engine point-reads through
432
- * exactly the declared projection.
433
- */
434
- function declaredKeyOf(relation, statement) {
435
- const statementId = tables.statements.findIndex(function byIdentity(candidate) {
436
- return "statement" in candidate && candidate.statement === statement;
491
+ function get(relation, keyOrStatement, declaredKey) {
492
+ assertLive();
493
+ const entry = ordinaryEntry(tables, theory, relation);
494
+ return selectKeyRead(keyOrStatement, declaredKey, function byStatement(statement, key) {
495
+ return readThroughKey(relation, entry, declaredKeyOf(tables, theory, relation, statement), recordOf(key));
496
+ }, function byPrimary(key) {
497
+ const primaryKey = entry.primaryKey;
498
+ if (primaryKey === undefined) {
499
+ throw errors.new(`relation ${relation.name} has no candidate key — keyed get requires a fresh field or a declared key statement`);
500
+ }
501
+ return readThroughKey(relation, entry, primaryKey, recordOf(key));
437
502
  });
438
- const entry = tables.statements[statementId];
439
- if (entry === undefined) {
440
- throw errors.new(`keyed get statement is not a declared statement of schema ${theory.name} — statement identity is the membership rule`);
503
+ }
504
+ return { contains, get };
505
+ }
506
+ function createReadInstance(nativeHandle, theory, tables, owner) {
507
+ const state = { handle: nativeHandle, live: true, owner };
508
+ function assertLive() {
509
+ if (!state.live) {
510
+ throw errors.wrap(ErrUseAfterScope, "bumbledb read instance is invalidated — its owning callback already returned");
441
511
  }
442
- if (entry.kind !== "functionality") {
443
- throw errors.new("keyed get takes a key() statement — containments and capacity statements key nothing");
512
+ }
513
+ const methods = catalogMethods(theory, tables, owner, assertLive, {
514
+ scan(relationId) {
515
+ return native.instanceScan(state.handle, relationId);
516
+ },
517
+ count(relationId) {
518
+ return native.instanceCount(state.handle, relationId);
519
+ },
520
+ contains(relationId, values) {
521
+ return native.instanceContains(state.handle, relationId, values);
522
+ },
523
+ get(relationId, statementId, keyValues) {
524
+ return native.instanceGet(state.handle, relationId, statementId, keyValues);
525
+ },
526
+ prepare(query) {
527
+ return native.instancePrepare(state.handle, query);
528
+ },
529
+ execute(prepared, params) {
530
+ return native.preparedExecute(prepared, state.handle, params);
444
531
  }
445
- if (entry.owner !== relation.name) {
446
- throw errors.new(`keyed get statement keys ${entry.owner}, not ${relation.name} — the statement must be a declared key of the relation it reads`);
532
+ });
533
+ const instance = Object.freeze({
534
+ get generation() {
535
+ assertLive();
536
+ return bridged("bumbledb instance generation", function readGeneration() {
537
+ return native.instanceGeneration(state.handle);
538
+ });
539
+ },
540
+ ...methods
541
+ });
542
+ instanceStates.set(instance, state);
543
+ return instance;
544
+ }
545
+ function openDb(handle, theory, manifest) {
546
+ const tables = tablesOf(theory, manifest);
547
+ /** This store's identity token: read scopes and prepared values carry it, so cross-store use is a typed refusal. */
548
+ const owner = Object.freeze({});
549
+ function isMemberName(name) {
550
+ return tables.relations.has(name);
551
+ }
552
+ function violationOf(wire) {
553
+ const entry = tables.statements[wire.statementId];
554
+ if (entry === undefined) {
555
+ throw errors.new(`bumbledb violation cites unknown statement id ${wire.statementId}`);
447
556
  }
448
- return Object.freeze({ statementId, projection: entry.projection });
557
+ const facts = Object.freeze(wire.facts.map(function offending(fact) {
558
+ const rel = tables.relations.get(fact.relation);
559
+ if (rel === undefined || !isMemberName(fact.relation)) {
560
+ throw errors.new(`bumbledb violation cites unknown relation ${fact.relation}`);
561
+ }
562
+ return decodeOffendingFact(rel.member, fact.relation, fact);
563
+ }));
564
+ return violationFromEntry(entry, wire, facts);
449
565
  }
450
566
  function pointReadsOf(assertLive, reads) {
451
567
  function contains(relation, fact) {
452
568
  assertLive();
453
- const entry = resolveOrdinary(relation);
569
+ const entry = ordinaryEntry(tables, theory, relation);
454
570
  return reads.contains(entry.id, rowOf(relation.data, recordOf(fact)));
455
571
  }
456
- /** One keyed point read through an already-resolved key, decoded to a fact (`undefined` on a miss). */
457
572
  function readThroughKey(relation, entry, selected, key) {
458
573
  const row = reads.get(entry.id, selected.statementId, keyRowOf(relation.data, selected.projection, key));
459
574
  if (row === null) {
@@ -463,9 +578,9 @@ function openDb(handle, theory, manifest) {
463
578
  }
464
579
  function get(relation, keyOrStatement, declaredKey) {
465
580
  assertLive();
466
- const entry = resolveOrdinary(relation);
581
+ const entry = ordinaryEntry(tables, theory, relation);
467
582
  return selectKeyRead(keyOrStatement, declaredKey, function byStatement(statement, key) {
468
- return readThroughKey(relation, entry, declaredKeyOf(relation, statement), recordOf(key));
583
+ return readThroughKey(relation, entry, declaredKeyOf(tables, theory, relation, statement), recordOf(key));
469
584
  }, function byPrimary(key) {
470
585
  const primaryKey = entry.primaryKey;
471
586
  if (primaryKey === undefined) {
@@ -476,172 +591,62 @@ function openDb(handle, theory, manifest) {
476
591
  }
477
592
  return { contains, get };
478
593
  }
479
- /**
480
- * Resolves a prepared value's private plan, refusing foreign objects
481
- * and prepared values of other stores as typed errors.
482
- */
483
- function planOf(prepared) {
484
- const plan = preparedPlans.get(prepared);
485
- if (plan === undefined) {
486
- throw errors.new("bumbledb execute target is not a prepared value of this SDK");
487
- }
488
- if (plan.owner !== owner) {
489
- throw errors.new(`bumbledb prepared value was prepared by a different store than this one (schema ${theory.name})`);
490
- }
491
- return plan;
594
+ function pinPrepared(preparedHandle, q) {
595
+ const prepared = Object.freeze({});
596
+ preparedPlans.set(prepared, Object.freeze({
597
+ handle: preparedHandle,
598
+ owner,
599
+ params: q.data.params,
600
+ finds: q.data.finds
601
+ }));
602
+ planReclaimer.register(prepared, preparedHandle);
603
+ return prepared;
492
604
  }
493
- /**
494
- * Builds one {@link ReadScope} over a live scope state. Every verb
495
- * asserts liveness first: the owning call flips `state.live` the moment
496
- * its callback returns (or the scope's own `Symbol.dispose` does, for a
497
- * `using`-acquired scope), so a leaked scope is a typed refusal forever
498
- * after.
499
- */
500
- function makeScope(state) {
501
- function assertLive() {
502
- if (!state.live) {
503
- throw errors.new("bumbledb read scope is invalidated — its owning read callback already returned");
504
- }
505
- }
506
- const reads = pointReadsOf(assertLive, {
507
- contains(relationId, row) {
508
- return bridged("bumbledb snapshot contains", function readContains() {
509
- return native.snapshotContains(state.handle, relationId, row);
510
- });
511
- },
512
- get(relationId, statementId, key) {
513
- return bridged("bumbledb snapshot get", function readGet() {
514
- return native.snapshotGet(state.handle, relationId, statementId, key);
605
+ function makeWitness(nativeHandle) {
606
+ const state = { handle: nativeHandle, spent: false, owner };
607
+ const witness = Object.freeze({
608
+ [Symbol.dispose]() {
609
+ if (state.spent) {
610
+ return;
611
+ }
612
+ state.spent = true;
613
+ bridged("close bumbledb witness", function closeWitness() {
614
+ native.witnessClose(nativeHandle);
515
615
  });
516
616
  }
517
617
  });
518
- function scan(relation) {
519
- assertLive();
520
- const entry = resolveOrdinary(relation);
521
- const rows = bridged("bumbledb snapshot scan", function readScan() {
522
- return native.snapshotScan(state.handle, entry.id);
523
- });
524
- return rows.map(function decodeRow(row) {
525
- return factOf(relation, row);
526
- });
527
- }
528
- function execute(prepared, params) {
529
- assertLive();
530
- const plan = planOf(prepared);
531
- const wire = wireParams(plan.params, recordOf(params));
532
- const rows = bridged("execute bumbledb prepared query", function callExecute() {
533
- return native.preparedExecute(plan.handle, state.handle, wire);
534
- });
535
- return decodeAnswers(plan.finds, rows);
536
- }
537
- /** The R12 teardown: invalidate, then close — idempotent through the state's close latch. */
538
- function dispose() {
539
- state.live = false;
540
- closeScopeState(state);
541
- }
542
- const scope = Object.freeze({
543
- generation: state.generation,
544
- scan,
545
- get: reads.get,
546
- contains: reads.contains,
547
- execute,
548
- [Symbol.dispose]: dispose
549
- });
550
- scopeStates.set(scope, state);
551
- return scope;
552
- }
553
- /**
554
- * Live-handle accounting (diagnostic law, prod EINVAL 2026-07-17): every
555
- * snapshot open/close is counted so a write-begin failure can report how
556
- * many read handles were live at the fault — a leaked scope is invisible
557
- * until the exact moment it matters, so the failure carries the census.
558
- */
559
- let liveSnapshots = 0;
560
- /**
561
- * Opens one snapshot and its scope state (live until the owner flips
562
- * it). The witnessed generation rides the snapshot open itself — one
563
- * crossing carries both (finding 016), so the fault-pairing close
564
- * branch a second `dbGeneration` call needed is structurally gone.
565
- */
566
- function openScopeState() {
567
- const opened = bridged("open bumbledb snapshot", function openSnapshot() {
568
- return native.dbSnapshot(handle);
569
- });
570
- liveSnapshots += 1;
571
- return { handle: opened.snapshot, generation: opened.generation, live: true, closed: false, owner };
572
- }
573
- /**
574
- * Closes a scope's snapshot after the owner invalidated it — a LATCH:
575
- * the snapshot closes exactly once, whichever of the owning call and
576
- * the scope's own `Symbol.dispose` gets there first, so an early
577
- * in-callback disposal never double-closes (and never double-counts
578
- * the census).
579
- */
580
- function closeScopeState(state) {
581
- if (state.closed) {
582
- return;
583
- }
584
- state.closed = true;
585
- bridged("close bumbledb snapshot", function closeSnapshot() {
586
- native.snapshotClose(state.handle);
587
- });
588
- liveSnapshots -= 1;
589
- }
590
- function read(fn) {
591
- const state = openScopeState();
592
- const scope = makeScope(state);
593
- if (fn === undefined) {
594
- /**
595
- * The `using` acquisition (R12): the caller owns the lifetime —
596
- * `using snap = db.read()` — and the scope's `Symbol.dispose`
597
- * is the deterministic release, scope-shaped in the language's
598
- * own syntax.
599
- */
600
- return scope;
601
- }
602
- const result = errors.trySync(function runRead() {
603
- return fn(scope);
604
- });
605
- state.live = false;
606
- closeScopeState(state);
607
- if (result.error) {
608
- throw errors.wrap(result.error, "bumbledb read");
609
- }
610
- return result.data;
618
+ witnessStates.set(witness, state);
619
+ witnessReclaimer.register(witness, nativeHandle);
620
+ return witness;
611
621
  }
612
- function scan(relation) {
613
- return read(function scanInScope(snap) {
614
- return snap.scan(relation);
615
- });
622
+ function makeInstance(nativeHandle) {
623
+ return createReadInstance(nativeHandle, theory, tables, owner);
616
624
  }
617
- function get(relation, keyOrStatement, declaredKey) {
618
- return read(function getInScope(snap) {
619
- return selectKeyRead(keyOrStatement, declaredKey, function byStatement(statement, key) {
620
- return snap.get(relation, statement, key);
621
- }, function byPrimary(key) {
622
- return snap.get(relation, key);
625
+ function read(body) {
626
+ let captured;
627
+ const result = bridged("bumbledb read", function runRead() {
628
+ return native.dbRead(handle, function onRead(nativeInstance, nativeWitness) {
629
+ const instance = makeInstance(nativeInstance);
630
+ const witness = makeWitness(nativeWitness);
631
+ const value = body(instance, witness);
632
+ const state = instanceStates.get(instance);
633
+ if (state !== undefined) {
634
+ state.live = false;
635
+ }
636
+ if (isThenable(value)) {
637
+ throw errors.wrap(ErrAsyncCallback, "bumbledb read callback returned a thenable");
638
+ }
639
+ captured = value;
640
+ return value;
623
641
  });
624
642
  });
643
+ return (captured ?? result);
625
644
  }
626
- function contains(relation, fact) {
627
- return read(function containsInScope(snap) {
628
- return snap.contains(relation, fact);
629
- });
630
- }
631
- function execute(prepared, params) {
632
- return read(function executeInScope(snap) {
633
- return snap.execute(prepared, params);
634
- });
635
- }
636
- /**
637
- * Builds one {@link Tx} over a transaction-handle thunk: `write` and
638
- * `writeFrom` pass an already-begun handle.
639
- */
640
645
  function makeTx(resolveTx) {
641
646
  const txState = { spent: false };
642
647
  function assertLive() {
643
648
  if (txState.spent) {
644
- throw errors.new("bumbledb write transaction is spent");
649
+ throw errors.wrap(ErrUseAfterScope, "bumbledb write transaction is spent");
645
650
  }
646
651
  }
647
652
  const reads = pointReadsOf(assertLive, {
@@ -660,33 +665,27 @@ function openDb(handle, theory, manifest) {
660
665
  });
661
666
  function insert(relation, facts) {
662
667
  assertLive();
663
- const rows = [];
664
- for (const fact of facts) {
665
- rows.push(rowOf(relation.data, recordOf(fact)));
666
- }
667
- const entry = resolveOrdinary(relation);
668
+ const entry = ordinaryEntry(tables, theory, relation);
668
669
  const txHandle = resolveTx();
669
- const report = bridged("bumbledb tx insert", function record() {
670
- return native.txInsert(txHandle, entry.id, rows);
670
+ return mutateCollection(relation, facts, function applyCells(rows, cells) {
671
+ return bridged("bumbledb tx insert", function record() {
672
+ return native.txInsert(txHandle, entry.id, rows, cells);
673
+ });
671
674
  });
672
- return Object.freeze({ submitted: report.submitted, changed: report.changed });
673
675
  }
674
676
  function remove(relation, facts) {
675
677
  assertLive();
676
- const rows = [];
677
- for (const fact of facts) {
678
- rows.push(rowOf(relation.data, recordOf(fact)));
679
- }
680
- const entry = resolveOrdinary(relation);
678
+ const entry = ordinaryEntry(tables, theory, relation);
681
679
  const txHandle = resolveTx();
680
+ const flat = rowsOf(relation, facts);
682
681
  const report = bridged("bumbledb tx delete", function record() {
683
- return native.txDelete(txHandle, entry.id, rows);
682
+ return native.txDelete(txHandle, entry.id, flat.rows, flat.cells);
684
683
  });
685
684
  return Object.freeze({ submitted: report.submitted, changed: report.changed });
686
685
  }
687
686
  function reserve(relation, field, count) {
688
687
  assertLive();
689
- const entry = resolveOrdinary(relation);
688
+ const entry = ordinaryEntry(tables, theory, relation);
690
689
  const declared = relation.data.fields.find(function byName(candidate) {
691
690
  return candidate.name === field;
692
691
  });
@@ -715,104 +714,80 @@ function openDb(handle, theory, manifest) {
715
714
  }
716
715
  return { tx, spend };
717
716
  }
718
- function runDelta(txHandle, fn) {
719
- const made = makeTx(function resolveTx() {
720
- return txHandle;
721
- });
722
- const built = errors.trySync(function buildDelta() {
723
- return fn(made.tx);
724
- });
725
- made.spend();
726
- if (built.error) {
727
- bridged("abort bumbledb write transaction", function abort() {
728
- native.txAbort(txHandle);
729
- });
730
- throw errors.wrap(built.error, "build write delta");
731
- }
732
- if (isThenable(built.data)) {
733
- /**
734
- * An `async` callback TYPECHECKS (Promise<void> is assignable where
735
- * a `void` return is expected) but its body runs after the tx is
736
- * spent: committing here would be a silent EMPTY commit reported
737
- * ok while the callback's real inserts throw "spent" as unhandled
738
- * rejections. Refused typed instead — abort, nothing committed
739
- * (the same one-writer law as the thrown-callback path).
740
- */
741
- bridged("abort bumbledb write transaction", function abort() {
742
- native.txAbort(txHandle);
743
- });
744
- throw errors.new("bumbledb write callback returned a thenable — the delta build is synchronous; an async callback is refused, nothing was committed");
745
- }
746
- if (isAbandon(built.data)) {
747
- /**
748
- * The caller's explicit decline to commit (R10): the sentinel's
749
- * contract is unconditional — roll back, nothing committed, not
750
- * even an empty commit; commit is unreachable for a sentinel
751
- * result.
752
- */
753
- bridged("abort bumbledb write transaction", function abort() {
754
- native.txAbort(txHandle);
717
+ function mapNativeWrite(nativeOutcome, built) {
718
+ if (nativeOutcome.tag === "moved") {
719
+ return Object.freeze({
720
+ tag: "moved",
721
+ witnessed: nativeOutcome.witnessed,
722
+ current: nativeOutcome.current
755
723
  });
756
- return abandonedOutcome(built.data);
757
724
  }
758
- const committed = errors.trySync(function commitDelta() {
759
- return bridged("commit bumbledb write transaction", function commit() {
760
- return native.txCommit(txHandle);
761
- });
762
- });
763
- if (committed.error) {
764
- /**
765
- * A THROWN commit (engine I/O failure, bridge fault) must never
766
- * leave the write transaction live: LMDB holds one writer per
767
- * environment, and a leaked handle turns every later begin into
768
- * EINVAL for the process's lifetime. The abort is best-effort —
769
- * the native side may already have consumed the handle.
770
- */
771
- const aborted = errors.trySync(function abortAfterFailedCommit() {
772
- native.txAbort(txHandle);
725
+ if (nativeOutcome.tag === "rejected") {
726
+ return Object.freeze({
727
+ tag: "rejected",
728
+ violations: Object.freeze(nativeOutcome.violations.map(violationOf))
773
729
  });
774
- if (aborted.error) {
775
- }
776
- throw errors.wrap(committed.error, "commit bumbledb write transaction");
777
730
  }
778
- const outcome = committed.data;
779
- if (outcome.ok) {
780
- return Object.freeze({ ok: true, generation: outcome.generation });
731
+ if (nativeOutcome.tag === "abandoned") {
732
+ if (built === undefined || !isAbandon(built)) {
733
+ throw errors.new("bumbledb write abandoned without an abandon sentinel");
734
+ }
735
+ return abandonedOutcome(built);
781
736
  }
782
737
  return Object.freeze({
783
- ok: false,
784
- violations: Object.freeze(outcome.violations.map(violationOf))
738
+ tag: "accepted",
739
+ value: Object.freeze({
740
+ value: built,
741
+ generation: nativeOutcome.generation
742
+ })
785
743
  });
786
744
  }
787
- function write(fn) {
788
- const txHandle = bridged(`begin bumbledb write transaction (live snapshots: ${liveSnapshots})`, function begin() {
789
- return native.dbWriteBegin(handle);
790
- });
791
- return runDelta(txHandle, fn);
792
- }
793
- /**
794
- * One-shot write from a live snapshot this store owns. Begins immediately;
795
- * a moved generation throws {@link ErrGenerationMoved} and `fn` never
796
- * runs. The snapshot stays owned by the caller's read callback.
797
- */
798
- function writeFrom(snap, fn) {
799
- const snapState = scopeStates.get(snap);
800
- if (snapState === undefined) {
801
- throw errors.new("bumbledb writeFrom witness is not a read scope of this SDK");
802
- }
803
- if (snapState.owner !== owner) {
804
- throw errors.new(`bumbledb writeFrom snapshot belongs to a different store (schema ${theory.name})`);
805
- }
806
- if (!snapState.live) {
807
- throw errors.new("bumbledb writeFrom snapshot is invalidated — its owning read callback already returned");
808
- }
809
- const witnessed = bridged("begin witnessed bumbledb write transaction", function begin() {
810
- return native.dbWriteFrom(handle, snapState.handle);
745
+ function runWrite(invoke, fn) {
746
+ let built;
747
+ const nativeOutcome = bridged("bumbledb write", function callWrite() {
748
+ return invoke(function onWrite(txHandle) {
749
+ const made = makeTx(function resolveTx() {
750
+ return txHandle;
751
+ });
752
+ const result = errors.trySync(function buildDelta() {
753
+ return fn(made.tx);
754
+ });
755
+ made.spend();
756
+ if (result.error) {
757
+ throw errors.wrap(result.error, "build write delta");
758
+ }
759
+ if (isThenable(result.data)) {
760
+ throw errors.wrap(ErrAsyncCallback, "bumbledb write callback returned a thenable");
761
+ }
762
+ built = result.data;
763
+ return !isAbandon(result.data);
764
+ });
811
765
  });
812
- if (!witnessed.ok) {
813
- throw errors.wrap(ErrGenerationMoved, `writeFrom: generation moved (witnessed ${witnessed.witnessed} current ${witnessed.current}) against schema ${theory.name}`);
766
+ return mapNativeWrite(nativeOutcome, built);
767
+ }
768
+ function write(fn) {
769
+ const outcome = runWrite(function invoke(callback) {
770
+ return native.dbWrite(handle, callback);
771
+ }, fn);
772
+ if (outcome.tag === "moved") {
773
+ throw errors.new("bumbledb write reported moved — unconditional writes cannot move");
774
+ }
775
+ return outcome;
776
+ }
777
+ function writeFrom(witness, fn) {
778
+ const state = witnessStates.get(witness);
779
+ if (state === undefined) {
780
+ throw errors.wrap(ErrForeignWitness, "bumbledb writeFrom witness is not a witness of this SDK");
781
+ }
782
+ if (state.owner !== owner) {
783
+ throw errors.wrap(ErrForeignWitness, `bumbledb writeFrom witness belongs to a different store (schema ${theory.name})`);
784
+ }
785
+ if (state.spent) {
786
+ throw errors.wrap(ErrSpentHandle, "bumbledb writeFrom witness has been disposed");
814
787
  }
815
- return runDelta(witnessed.tx, fn);
788
+ return runWrite(function invoke(callback) {
789
+ return native.dbWriteFrom(handle, state.handle, callback);
790
+ }, fn);
816
791
  }
817
792
  function prepare(q) {
818
793
  if (q.schema !== theory) {
@@ -823,114 +798,296 @@ function openDb(handle, theory, manifest) {
823
798
  return native.dbPrepare(handle, queryIr);
824
799
  });
825
800
  if (!outcome.ok) {
826
- throw errors.new(`bumbledb ${outcome.kind} (prepare): ${outcome.message}`);
801
+ throwPrepareRefusal(outcome.message);
827
802
  }
828
- const preparedHandle = outcome.prepared;
829
- const prepared = Object.freeze({});
830
- preparedPlans.set(prepared, Object.freeze({
831
- handle: preparedHandle,
832
- owner,
833
- params: q.data.params,
834
- finds: q.data.finds
835
- }));
836
- planReclaimer.register(prepared, preparedHandle);
837
- return prepared;
803
+ return pinPrepared(outcome.prepared, q);
838
804
  }
839
805
  return Object.freeze({
840
806
  schema: theory,
841
807
  read,
842
- scan,
843
- get,
844
- contains,
845
- execute,
846
808
  write,
847
809
  writeFrom,
848
810
  prepare
849
811
  });
850
812
  }
851
- /**
852
- * The engine twin of the schema-level class wall, as a matchable value
853
- * (`errors.is`): the shared lowering rejected a spec whose statement pairs
854
- * faces with disagreeing newtype labels — the faces of a dependency agree
855
- * on their newtype, or neither carries one. UNREACHABLE through the typed
856
- * builder (the SDK computes every label from the laws, so its lowered
857
- * specs cohere by construction); a raw spec handed to the bridge is the
858
- * one road here, and the runtime referee that proves the engine judges
859
- * what the types claim.
860
- */
861
813
  const ErrNewtypeMismatch = errors.new("bumbledb newtypeMismatch: a statement pairs faces whose newtypes disagree — the faces of a dependency agree on their newtype, or neither carries one");
862
- /**
863
- * The one admission path both verbs share: lower the theory, run one
864
- * bridge call, and wrap the domain refusals — `schemaError` (spec
865
- * resolution + schema validation, every issue in one message),
866
- * `newtypeMismatch` (the coherence wall, {@link ErrNewtypeMismatch}),
867
- * and `fingerprintMismatch` (a different theory cannot open the store)
868
- * — into typed errors carrying the engine's message intact.
869
- * Environment failures (a second live writer on the same path, IO)
870
- * throw from the bridge; the engine's `EnvironmentLocked` message is
871
- * "another live handle holds this environment's lock".
872
- */
873
- function admit(verb, storePath, theory) {
814
+ const ErrSchemaError = errors.new("bumbledb schemaError: the declaration failed validation");
815
+ const ErrFingerprintMismatch = errors.new("bumbledb fingerprintMismatch: the store's schema does not match this theory");
816
+ const ErrIrError = errors.new("bumbledb irError: the query failed validation");
817
+ function throwOpenRefusal(verb, canonical, kind, message) {
818
+ const detail = `${verb} ${canonical}: ${message}`;
819
+ if (kind === "newtypeMismatch") {
820
+ throw errors.wrap(ErrNewtypeMismatch, detail);
821
+ }
822
+ if (kind === "schemaError") {
823
+ throw errors.wrap(ErrSchemaError, detail);
824
+ }
825
+ throw errors.wrap(ErrFingerprintMismatch, detail);
826
+ }
827
+ function throwPrepareRefusal(message) {
828
+ throw errors.wrap(ErrIrError, `prepare: ${message}`);
829
+ }
830
+ function openFromHandle(dbHandle, theory) {
831
+ const manifest = bridged("fetch bumbledb manifest", function fetchManifest() {
832
+ return native.dbManifest(dbHandle);
833
+ });
834
+ return openDb(dbHandle, theory, manifest);
835
+ }
836
+ async function createStore(storePath, theory) {
874
837
  const canonical = path.resolve(storePath);
875
838
  const spec = lower(theory);
876
- const opened = bridged(`${verb} bumbledb store at ${canonical}`, function callBridge() {
877
- if (verb === "create") {
878
- return native.dbCreate(canonical, spec);
839
+ const created = await bridgedAsync(`create bumbledb store at ${canonical}`, function callBridge() {
840
+ return native.dbCreate(canonical, spec);
841
+ });
842
+ if (created.tag === "schemaError" || created.tag === "newtypeMismatch") {
843
+ throwOpenRefusal("create", canonical, created.tag, created.message);
844
+ }
845
+ if (created.tag === "rejected") {
846
+ return Object.freeze({
847
+ tag: "rejected",
848
+ violations: Object.freeze(created.violations.map(function mapWire(wire) {
849
+ return mapViolationWithoutStore(theory, wire);
850
+ }))
851
+ });
852
+ }
853
+ return Object.freeze({ tag: "accepted", value: openFromHandle(created.db, theory) });
854
+ }
855
+ function mapViolationWithoutStore(theory, wire) {
856
+ const entries = materializedEntries(theory);
857
+ const entry = entries[wire.statementId];
858
+ if (entry === undefined) {
859
+ throw errors.new(`bumbledb violation cites unknown statement id ${wire.statementId}`);
860
+ }
861
+ const facts = Object.freeze(wire.facts.map(function offending(fact) {
862
+ const member = theory.relations[fact.relation];
863
+ if (member === undefined || !(fact.relation in theory.relations)) {
864
+ throw errors.new(`bumbledb violation cites unknown relation ${fact.relation}`);
879
865
  }
866
+ return decodeOffendingFact(member, fact.relation, fact);
867
+ }));
868
+ return violationFromEntry(entry, wire, facts);
869
+ }
870
+ async function openStore(storePath, theory) {
871
+ const canonical = path.resolve(storePath);
872
+ const spec = lower(theory);
873
+ const opened = await bridgedAsync(`open bumbledb store at ${canonical}`, function callBridge() {
880
874
  return native.dbOpen(canonical, spec);
881
875
  });
882
876
  if (!opened.ok) {
883
- if (opened.kind === "newtypeMismatch") {
884
- throw errors.wrap(ErrNewtypeMismatch, `${verb} ${canonical}: ${opened.message}`);
877
+ throwOpenRefusal("open", canonical, opened.kind, opened.message);
878
+ }
879
+ return openFromHandle(opened.db, theory);
880
+ }
881
+ const ownedRecords = new WeakMap();
882
+ const builderRecords = new WeakMap();
883
+ const ownedReclaimer = new FinalizationRegistry(function reclaimOwned(handle) {
884
+ const closed = errors.trySync(function closeOwned() {
885
+ native.ownedInstanceClose(handle);
886
+ });
887
+ if (closed.error) {
888
+ return;
889
+ }
890
+ });
891
+ const builderReclaimer = new FinalizationRegistry(function reclaimBuilder(handle) {
892
+ const closed = errors.trySync(function closeBuilder() {
893
+ native.instanceBuilderClose(handle);
894
+ });
895
+ if (closed.error) {
896
+ return;
897
+ }
898
+ });
899
+ function wrapOwned(nativeHandle, theory) {
900
+ const owner = Object.freeze({});
901
+ const rec = { handle: nativeHandle, theory, spent: false, owner };
902
+ const tables = tablesFromTheory(theory);
903
+ function assertLive() {
904
+ if (rec.spent) {
905
+ throw errors.wrap(ErrSpentHandle, "bumbledb owned instance has been disposed");
885
906
  }
886
- throw errors.new(`bumbledb ${opened.kind} (${verb} ${canonical}): ${opened.message}`);
887
907
  }
888
- const manifest = bridged("fetch bumbledb manifest", function fetchManifest() {
889
- return native.dbManifest(opened.db);
908
+ const methods = catalogMethods(theory, tables, owner, assertLive, {
909
+ scan(relationId) {
910
+ return native.ownedScan(nativeHandle, relationId);
911
+ },
912
+ count(relationId) {
913
+ return native.ownedCount(nativeHandle, relationId);
914
+ },
915
+ contains(relationId, values) {
916
+ return native.ownedContains(nativeHandle, relationId, values);
917
+ },
918
+ get(relationId, statementId, keyValues) {
919
+ return native.ownedGet(nativeHandle, relationId, statementId, keyValues);
920
+ },
921
+ prepare(query) {
922
+ return native.ownedPrepare(nativeHandle, query);
923
+ },
924
+ execute(prepared, params) {
925
+ return native.ownedExecute(prepared, nativeHandle, params);
926
+ }
927
+ });
928
+ const instance = Object.freeze({
929
+ ...methods,
930
+ [Symbol.dispose]() {
931
+ if (rec.spent) {
932
+ return;
933
+ }
934
+ try {
935
+ native.ownedInstanceClose(nativeHandle);
936
+ }
937
+ catch (caught) {
938
+ const error = errorFromThrow(caught);
939
+ if (/leased for publish/.test(error.message)) {
940
+ throw errors.wrap(ErrSpentHandle, "bumbledb owned instance is leased for publish");
941
+ }
942
+ throw errors.wrap(error, "close bumbledb owned instance");
943
+ }
944
+ rec.spent = true;
945
+ ownedReclaimer.unregister(instance);
946
+ }
947
+ });
948
+ ownedRecords.set(instance, rec);
949
+ ownedReclaimer.register(instance, nativeHandle, instance);
950
+ return instance;
951
+ }
952
+ function wrapBuilder(nativeHandle, theory) {
953
+ const rec = { handle: nativeHandle, theory, spent: false };
954
+ const tables = tablesFromTheory(theory);
955
+ function assertLive() {
956
+ if (rec.spent) {
957
+ throw errors.wrap(ErrSpentHandle, "bumbledb instance builder has been spent");
958
+ }
959
+ }
960
+ const overlay = overlayMethods(theory, tables, assertLive, {
961
+ contains(relationId, row) {
962
+ return bridged("bumbledb builder contains", function readContains() {
963
+ return native.instanceBuilderContains(nativeHandle, relationId, row);
964
+ });
965
+ },
966
+ get(relationId, statementId, key) {
967
+ return bridged("bumbledb builder get", function readGet() {
968
+ return native.instanceBuilderGet(nativeHandle, relationId, statementId, key);
969
+ });
970
+ }
971
+ });
972
+ const builder = Object.freeze({
973
+ load(relation, facts) {
974
+ assertLive();
975
+ const entry = ordinaryEntry(tables, theory, relation);
976
+ return mutateCollection(relation, facts, function applyCells(rows, cells) {
977
+ return bridged("bumbledb builder load", function loadCells() {
978
+ return native.instanceBuilderLoad(nativeHandle, entry.id, rows, cells);
979
+ });
980
+ });
981
+ },
982
+ delete(relation, facts) {
983
+ assertLive();
984
+ const entry = ordinaryEntry(tables, theory, relation);
985
+ const flat = rowsOf(relation, facts);
986
+ const report = bridged("bumbledb builder delete", function remove() {
987
+ return native.instanceBuilderDelete(nativeHandle, entry.id, flat.rows, flat.cells);
988
+ });
989
+ return Object.freeze({ submitted: report.submitted, changed: report.changed });
990
+ },
991
+ reserve(relation, field, count) {
992
+ assertLive();
993
+ const entry = ordinaryEntry(tables, theory, relation);
994
+ const declared = relation.data.fields.find(function byName(candidate) {
995
+ return candidate.name === field;
996
+ });
997
+ if (declared === undefined || !isFreshField(declared.field)) {
998
+ throw errors.new(`relation ${relation.name}: field ${field} is not a fresh cell`);
999
+ }
1000
+ const fieldId = entry.fieldIds.get(field);
1001
+ if (fieldId === undefined) {
1002
+ throw errors.new(`bumbledb manifest drift: relation ${relation.name} has no field id for ${field}`);
1003
+ }
1004
+ const range = bridged("bumbledb builder reserve", function mint() {
1005
+ return native.instanceBuilderReserve(nativeHandle, entry.id, fieldId, count);
1006
+ });
1007
+ return freshRangeOf(range);
1008
+ },
1009
+ contains: overlay.contains,
1010
+ get: overlay.get,
1011
+ async admit() {
1012
+ if (rec.spent) {
1013
+ throw errors.wrap(ErrSpentHandle, "bumbledb instance builder has been spent");
1014
+ }
1015
+ rec.spent = true;
1016
+ builderReclaimer.unregister(builder);
1017
+ let outcome;
1018
+ try {
1019
+ outcome = await native.instanceBuilderAdmit(nativeHandle);
1020
+ }
1021
+ catch (caught) {
1022
+ throw errors.wrap(errorFromThrow(caught), "admit bumbledb instance");
1023
+ }
1024
+ if (outcome.tag === "rejected") {
1025
+ return Object.freeze({
1026
+ tag: "rejected",
1027
+ violations: Object.freeze(outcome.violations.map(function mapWire(wire) {
1028
+ return mapViolationWithoutStore(theory, wire);
1029
+ }))
1030
+ });
1031
+ }
1032
+ return Object.freeze({
1033
+ tag: "accepted",
1034
+ value: wrapOwned(outcome.value, theory)
1035
+ });
1036
+ },
1037
+ [Symbol.dispose]() {
1038
+ if (rec.spent) {
1039
+ return;
1040
+ }
1041
+ rec.spent = true;
1042
+ builderReclaimer.unregister(builder);
1043
+ bridged("close bumbledb instance builder", function closeBuilder() {
1044
+ native.instanceBuilderClose(nativeHandle);
1045
+ });
1046
+ }
890
1047
  });
891
- return openDb(opened.db, theory, manifest);
1048
+ builderRecords.set(builder, rec);
1049
+ builderReclaimer.register(builder, nativeHandle, builder);
1050
+ return builder;
892
1051
  }
1052
+ const InstanceBuilder = Object.freeze({
1053
+ create(theory) {
1054
+ const spec = lower(theory);
1055
+ const handle = bridged("create bumbledb instance builder", function make() {
1056
+ return native.instanceBuilderNew(spec);
1057
+ });
1058
+ return wrapBuilder(handle, theory);
1059
+ }
1060
+ });
893
1061
  /**
894
1062
  * The store lifecycle — `Db.create(path, schema)` / `Db.open(path, schema)`.
895
1063
  * Create refuses an already-initialized directory; open verifies format
896
- * version, store kind, and the schema fingerprint. A second live handle
897
- * on the same path is the engine's `EnvironmentLocked`. There is no close
898
- * anywhere: the process owns the environment until GC/exit (durability is
899
- * the engine's per-commit fsync). One store kind exists: durable —
900
- * resume = reopen in a fresh process, or hold the `Db` this process opened.
1064
+ * version and the schema fingerprint. A second live handle on the same
1065
+ * path is the engine's `EnvironmentLocked`. There is no close anywhere:
1066
+ * the process owns the environment until GC/exit (durability is the
1067
+ * engine's per-commit fsync). Resume = reopen in a fresh process, or
1068
+ * hold the `Db` this process opened.
901
1069
  */
902
1070
  const Db = Object.freeze({
903
- /** Creates a fresh durable store at `path` from the schema. */
904
- async create(path, theory) {
905
- return admit("create", path, theory);
1071
+ async create(storePath, theory) {
1072
+ return createStore(storePath, theory);
906
1073
  },
907
- /**
908
- * Opens an existing durable store at `path` with the same theory.
909
- * A fingerprint-matching open also BACK-FILLS the store's persisted
910
- * schema descriptor when it is absent (self-describing stores, engine
911
- * 50-storage.md § the `_meta` block), so a legacy store becomes
912
- * exhumable after one ordinary open — adoption is automatic, never a
913
- * separate verb. A second open of a still-live path is
914
- * `EnvironmentLocked`.
915
- */
916
- async open(path, theory) {
917
- return admit("open", path, theory);
1074
+ async open(storePath, theory) {
1075
+ return openStore(storePath, theory);
918
1076
  },
919
- /**
920
- * Opens a store READ-ONLY from its own persisted descriptor — the SDK's
921
- * one schema-independent read path (no theory, no fingerprint check; the
922
- * store rebirth tool's entry). Lives beside `open`/`create` so the path
923
- * law stays in one place: the same `node:path.resolve` canonicalization,
924
- * applied here. The value is NOT cached and is a DISPOSABLE lifetime
925
- * (R12) — `using exhumed = await Db.exhume(path)` releases the engine
926
- * handle and the store's exclusive lock at scope exit, so a same-path
927
- * reopen never waits on GC. A store not yet adopted rejects with the
928
- * typed `ErrExhumeNoDescriptor` (the remedy: one fingerprint-matching
929
- * `Db.open` under the creating schema back-fills the descriptor).
930
- */
931
- async exhume(storePath) {
932
- return exhumeStore(path.resolve(storePath));
1077
+ async fromInstance(storePath, instance) {
1078
+ const rec = ownedRecords.get(instance);
1079
+ if (rec === undefined) {
1080
+ throw errors.wrap(ErrSpentHandle, "bumbledb fromInstance target is not an owned instance of this SDK");
1081
+ }
1082
+ if (rec.spent) {
1083
+ throw errors.wrap(ErrSpentHandle, "bumbledb fromInstance target has been disposed");
1084
+ }
1085
+ const canonical = path.resolve(storePath);
1086
+ const dbHandle = await bridgedAsync(`publish bumbledb instance at ${canonical}`, function publish() {
1087
+ return native.dbFromInstance(canonical, rec.handle);
1088
+ });
1089
+ return openFromHandle(dbHandle, rec.theory);
933
1090
  }
934
1091
  });
935
- export { abandon, Db, ErrGenerationMoved, ErrNewtypeMismatch };
1092
+ export { abandon, Db, ErrAsyncCallback, ErrFingerprintMismatch, ErrForeignPrepared, ErrForeignWitness, ErrIrError, ErrNewtypeMismatch, ErrSchemaError, ErrSpentHandle, ErrUseAfterScope, InstanceBuilder };
936
1093
  //# sourceMappingURL=db.js.map