@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/src/db.ts 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,33 +22,38 @@
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
 
33
32
  import * as path from "node:path"
34
33
  import * as errors from "@superbuilders/errors"
35
34
  import { isClosedMember, sealedFieldsOf } from "#closed.ts"
36
- import type { Exhumed } from "#exhume.ts"
37
- import { exhumeStore } from "#exhume.ts"
38
35
  import { rosterOf } from "#fields.ts"
39
36
  import { lower } from "#lower.ts"
40
- import { factOf, handleOf, isFreshField, type KeyFact, keyRowOf, recordOf, rowOf } from "#marshal.ts"
37
+ import { cellOf, factOf, handleOf, isFreshField, type KeyFact, keyRowOf, recordOf, rowOf } from "#marshal.ts"
41
38
 
42
39
  import type {
40
+ AdmitResult,
41
+ BuilderHandle,
43
42
  DbHandle,
44
43
  FactValue,
44
+ InstanceHandle,
45
45
  Manifest,
46
+ NativeWriteOutcome,
47
+ OwnedHandle,
46
48
  PreparedHandle,
47
- SnapshotHandle,
48
49
  TxHandle,
49
50
  WireFreshRange,
51
+ WireMutationReport,
50
52
  Violation as WireViolation,
51
- ViolationFact as WireViolationFact
53
+ ViolationFact as WireViolationFact,
54
+ WitnessHandle
52
55
  } from "#native.ts"
53
- import { bridged, native } from "#native.ts"
56
+ import { bridged, bridgedAsync, errorFromThrow, native } from "#native.ts"
54
57
  import type { FindColumn } from "#query/atom.ts"
55
58
  import type { Query } from "#query/lower.ts"
56
59
  import { lowerQuery } from "#query/lower.ts"
@@ -60,26 +63,60 @@ import type { AnyRelation, Fact, FreshKeys } from "#relation.ts"
60
63
  import type { AnySchema, Schema, SchemaRelation, SchemaRelations } from "#schema.ts"
61
64
  import { isStatement, type KeyStatement, type Statement } from "#statements.ts"
62
65
 
63
- /**
64
- * The ordinary (writable, scannable) relations of a schema's record — the
65
- * only values the runtime methods accept: closed relations lack the
66
- * relation shape entirely, so passing one is a type error.
67
- */
68
66
  type MemberRelation<Rels extends SchemaRelations> = Extract<Rels[keyof Rels], AnyRelation>
69
67
 
70
- /**
71
- * Facts consumed vs facts that changed the in-memory final-state view.
72
- * The length-1 report is `{ submitted: 1n, changed: 0n | 1n }`.
73
- */
74
68
  interface MutationReport {
75
69
  readonly submitted: bigint
76
70
  readonly changed: bigint
77
71
  }
78
72
 
73
+ type CollectionWrite<R extends AnyRelation> = Iterable<Fact<R>>
74
+
75
+ interface FlatCollection {
76
+ readonly rows: bigint
77
+ readonly cells: readonly FactValue[]
78
+ }
79
+
79
80
  /**
80
- * Half-open fresh-id range from one `reserve`. Empty cannot yield a
81
- * minted id — `start` exists only on the nonempty arm.
81
+ * The flat projector: every fact's cells land in ONE row-major
82
+ * `FactValue` array (length rows×arity) — no JS array per fact exists
83
+ * anywhere between the caller's objects and the native crossing
84
+ * (proposals/one-representation/20, V1) — and the row count is counted
85
+ * while projecting (the {@link FlatCollection} law: the stated count is
86
+ * what the bridge verifies against `rows × arity`, exactly, for every
87
+ * arity). The per-cell judgment is `cellOf` — the one cell judge `rowOf`
88
+ * also speaks (closed handle→id, well-formedness, interval shape) — and
89
+ * the missing-field refusal is `rowOf`'s, byte for byte; only the output
90
+ * form differs (flat, never per-row).
82
91
  */
92
+ function rowsOf<R extends AnyRelation>(relation: R, facts: Iterable<Fact<R>>): FlatCollection {
93
+ const data = relation.data
94
+ const cells: FactValue[] = []
95
+ let rows = 0n
96
+ for (const fact of facts) {
97
+ rows += 1n
98
+ const record = recordOf(fact)
99
+ for (const declared of data.fields) {
100
+ const value = record[declared.name]
101
+ if (value === undefined) {
102
+ throw errors.new(`relation ${data.name}: fact is missing field ${declared.name}`)
103
+ }
104
+ cells.push(cellOf(`relation ${data.name} field ${declared.name}`, declared.field, value))
105
+ }
106
+ }
107
+ return { rows, cells }
108
+ }
109
+
110
+ function mutateCollection<R extends AnyRelation>(
111
+ relation: R,
112
+ facts: CollectionWrite<R>,
113
+ apply: (rows: bigint, cells: readonly FactValue[]) => WireMutationReport
114
+ ): MutationReport {
115
+ const flat = rowsOf(relation, facts)
116
+ const report = apply(flat.rows, flat.cells)
117
+ return Object.freeze({ submitted: report.submitted, changed: report.changed })
118
+ }
119
+
83
120
  type FreshRange =
84
121
  | {
85
122
  readonly empty: true
@@ -131,80 +168,36 @@ function freshRangeOf(wire: WireFreshRange): FreshRange {
131
168
  })
132
169
  }
133
170
 
134
- /**
135
- * The key object of a key-statement-selected `get`: exactly the selected
136
- * `key()` statement's projection fields, each at the relation's own BARE
137
- * structural value type — the {@link KeyFact} rule generalized from the
138
- * primary key to ANY declared key statement.
139
- */
140
171
  type DeclaredKeyFact<R extends AnyRelation, Projection extends readonly string[]> = {
141
172
  readonly [K in Projection[number] & keyof Fact<R>]: Fact<R>[K]
142
173
  }
143
174
 
144
- /**
145
- * One offending fact of a violation: the cited relation's name (a member
146
- * of the schema's record) and the fact decoded to a named natural-value
147
- * object — partial exactly as the engine cites it. Closed-referencing
148
- * cells arrive as handle NAMES (the marshal bijection's read half), so the
149
- * record and the violation's `canonical` string — which the engine already
150
- * renders with handle names — agree on the one spelling.
151
- */
152
175
  interface OffendingFact<Rels extends SchemaRelations> {
153
176
  readonly relation: keyof Rels & string
154
177
  readonly fact: Readonly<Record<string, FactValue>>
155
178
  }
156
179
 
157
- /**
158
- * Shared body of every violation arm: the engine's canonical rendering and
159
- * the cited facts. `statement` is NOT here — its presence is the
160
- * discriminant. Implied auto-keys have no SDK spelling (`statement` is
161
- * the value `undefined`); every declared form carries the IDENTICAL
162
- * statement value the schema declared (consumers `===`-match it).
163
- */
164
180
  type ViolationBody<Rels extends SchemaRelations> = {
165
181
  readonly canonical: string
166
182
  readonly facts: readonly OffendingFact<Rels>[]
167
183
  }
168
184
 
169
- /**
170
- * A functionality violation of an engine-materialized fresh-implied or
171
- * closed auto-key. These slots have no declared spelling (`schema()`
172
- * rejects an explicit duplicate); `statement` is present and `undefined`.
173
- */
174
185
  type ImpliedKeyViolation<Rels extends SchemaRelations> = ViolationBody<Rels> & {
175
186
  readonly kind: "functionality"
176
187
  readonly statement: undefined
177
188
  }
178
189
 
179
- /**
180
- * A functionality violation of a declared `key()` statement. `statement`
181
- * is the IDENTICAL SDK value the schema declared.
182
- */
183
190
  type DeclaredKeyViolation<Rels extends SchemaRelations> = ViolationBody<Rels> & {
184
191
  readonly kind: "functionality"
185
192
  readonly statement: Statement
186
193
  }
187
194
 
188
- /**
189
- * A containment violation of a declared `contained()` statement (no
190
- * `orientation` — that property exists exactly on {@link MirrorViolation}).
191
- */
192
195
  type ContainmentViolation<Rels extends SchemaRelations> = ViolationBody<Rels> & {
193
196
  readonly kind: "containment"
194
197
  readonly statement: Statement
195
198
  readonly direction: "sourceUnsatisfied" | "targetRequired"
196
199
  }
197
200
 
198
- /**
199
- * A containment violation of one slot of a declared `mirrors()` statement.
200
- * BOTH materialized slots render as the one `==` utterance in the written
201
- * orientation (identical `canonical` strings; the engine's `render.rs`
202
- * never emits a bare `<=` for a mirrored pair). `direction` is relative
203
- * to the violated SLOT's own orientation, so it alone cannot say which
204
- * side of the `==` was violated: `written` is the `source <= target` slot
205
- * as the statement was spelled, `mirrored` the engine-materialized
206
- * `target <= source` partner.
207
- */
208
201
  type MirrorViolation<Rels extends SchemaRelations> = ViolationBody<Rels> & {
209
202
  readonly kind: "containment"
210
203
  readonly statement: Statement
@@ -212,25 +205,12 @@ type MirrorViolation<Rels extends SchemaRelations> = ViolationBody<Rels> & {
212
205
  readonly orientation: "written" | "mirrored"
213
206
  }
214
207
 
215
- /**
216
- * A capacity violation of a declared `capacity()` statement. `measure` is
217
- * the engine's witnessed group total — u128-wide, crossing whole as
218
- * bigint (C3: truncation is unrepresentable).
219
- */
220
208
  type CapacityViolation<Rels extends SchemaRelations> = ViolationBody<Rels> & {
221
209
  readonly kind: "capacity"
222
210
  readonly statement: Statement
223
211
  readonly measure: bigint
224
212
  }
225
213
 
226
- /**
227
- * One violated statement of a rejected commit, as a typed value. The
228
- * arms are a true discriminant: `statement === undefined` is exactly the
229
- * implied-auto-key arm; every declared form carries `Statement` (not
230
- * `Statement | undefined`, not an omit-optional). `canonical` is the
231
- * ENGINE's rendering. `direction` / `measure` pass through from the
232
- * engine VERBATIM.
233
- */
234
214
  type Violation<Rels extends SchemaRelations> =
235
215
  | ImpliedKeyViolation<Rels>
236
216
  | DeclaredKeyViolation<Rels>
@@ -245,42 +225,37 @@ type Violation<Rels extends SchemaRelations> =
245
225
  * `never` and the arm vanishes from the sum. The outcome is in the type;
246
226
  * a dead arm is never handled.
247
227
  */
248
- type AbandonedArm<R> = R extends Abandon<infer P> ? { readonly ok: false; readonly abandoned: P } : never
228
+ type AbandonedArm<R> = R extends Abandon<infer P> ? { readonly tag: "abandoned"; readonly abandoned: P } : never
249
229
 
250
- /**
251
- * A write's domain outcome, one sum for BOTH verbs (ruled 2026-07-23,
252
- * R10): the committed generation; the COMPLETE violation set (every
253
- * violated statement cited once, per direction for a containment, in
254
- * materialized statement order); or the callback's own abandon payload —
255
- * commit-vs-abandon is in the type, so a caller's explicit decline to
256
- * commit can never be silently discarded. Narrows on `.ok`, then (when the
257
- * callback can abandon) on `"violations" in result`.
258
- */
259
- type WriteResult<Rels extends SchemaRelations, R = void> =
260
- | { readonly ok: true; readonly generation: bigint }
261
- | { readonly ok: false; readonly violations: readonly Violation<Rels>[] }
230
+ type SyncResult<R> = R extends PromiseLike<unknown> ? never : R
231
+
232
+ interface Committed<T> {
233
+ readonly value: T
234
+ readonly generation: bigint
235
+ }
236
+
237
+ type Admission<Rels extends SchemaRelations, T> =
238
+ | { readonly tag: "accepted"; readonly value: T }
239
+ | { readonly tag: "rejected"; readonly violations: readonly Violation<Rels>[] }
240
+
241
+ type WriteOutcome<Rels extends SchemaRelations, R> =
242
+ | { readonly tag: "accepted"; readonly value: Committed<Exclude<R, Abandon<unknown>>> }
243
+ | { readonly tag: "rejected"; readonly violations: readonly Violation<Rels>[] }
262
244
  | AbandonedArm<R>
263
245
 
264
- /**
265
- * The delta-building callback of a write: runs synchronously against the
266
- * live transaction. Returning {@link abandon}`(payload)` rolls the
267
- * transaction back (R10) — the result type carries the payload arm exactly
268
- * then.
269
- */
270
- type DeltaBuild<Rels extends SchemaRelations, R = void> = (tx: Tx<Rels>) => R
246
+ type WriteFromOutcome<Rels extends SchemaRelations, R> =
247
+ | WriteOutcome<Rels, R>
248
+ | { readonly tag: "moved"; readonly witnessed: bigint; readonly current: bigint }
249
+
250
+ type DeltaBuild<Rels extends SchemaRelations, R = void> = (tx: WriteTx<Rels>) => R
271
251
 
272
- /**
273
- * The runtime discriminant of {@link Abandon} values — a property probe is
274
- * how `write`/`writeFrom` distinguish "abort without committing" from an
275
- * ordinary callback result, never a guess about the host's own value shapes.
276
- */
277
252
  const abandonMark: unique symbol = Symbol("bumbledb.abandon")
278
253
 
279
254
  /**
280
255
  * The abandon sentinel {@link abandon} builds: returning one from a `write`
281
256
  * or `writeFrom` callback rolls the transaction back WITHOUT
282
257
  * committing (no empty commit is ever issued) and surfaces the payload as
283
- * `{ ok: false, abandoned: payload }` (ruled 2026-07-23, R10 — the
258
+ * `{ tag: "abandoned", abandoned: payload }` (ruled 2026-07-23, R10 — the
284
259
  * sentinel's contract is unconditional, whichever write verb received it).
285
260
  */
286
261
  interface Abandon<P> {
@@ -288,98 +263,43 @@ interface Abandon<P> {
288
263
  readonly payload: P
289
264
  }
290
265
 
291
- /**
292
- * Wraps a payload in the {@link Abandon} sentinel — the one way a write
293
- * callback declines to commit: `return abandon(payload)` aborts the delta
294
- * (nothing is committed, not even an empty commit) and the write resolves
295
- * to `{ ok: false, abandoned: payload }`, from `write` and `writeFrom`
296
- * alike (R10).
297
- */
298
266
  function abandon<P>(payload: P): Abandon<P> {
299
267
  return Object.freeze({ [abandonMark]: true as const, payload })
300
268
  }
301
269
 
302
- /**
303
- * The abandon payload type a write callback's return type implies: the
304
- * payload of its `Abandon` arm, `never` when the callback can never
305
- * abandon (the `abandoned` outcome is then statically unreachable and
306
- * {@link AbandonedArm} erases it from the sum).
307
- */
308
270
  type AbandonedPayload<R> = R extends Abandon<infer P> ? P : never
309
271
 
310
- /**
311
- * Narrows a write callback result to the abandon sentinel. The probe is
312
- * the private {@link abandonMark} symbol only {@link abandon} sets, and
313
- * `R`'s `Abandon` arm is the only way a sentinel can flow out of the
314
- * callback — so the narrowed payload type is sound by construction.
315
- */
316
272
  function isAbandon<R>(value: R): value is R & Abandon<AbandonedPayload<R>> {
317
273
  return typeof value === "object" && value !== null && abandonMark in value
318
274
  }
319
275
 
320
- /**
321
- * The abandon outcome's trusted admission seam: the value's shape is the
322
- * checkable half (the sentinel mark only {@link abandon} mints, and the
323
- * outcome carrying that sentinel's own payload), and the sentinel's
324
- * existence IS the proof `R` carries an `Abandon` arm — so the outcome is
325
- * admitted at the conditional {@link AbandonedArm} face the type tier
326
- * cannot resolve over an open `R`.
327
- */
328
276
  function isAbandonedOutcome<Rels extends SchemaRelations, R>(
329
- outcome: { readonly ok: false; readonly abandoned: AbandonedPayload<R> },
277
+ outcome: { readonly tag: "abandoned"; readonly abandoned: AbandonedPayload<R> },
330
278
  sentinel: Abandon<AbandonedPayload<R>>
331
- ): outcome is { readonly ok: false; readonly abandoned: AbandonedPayload<R> } & WriteResult<Rels, R> {
279
+ ): outcome is { readonly tag: "abandoned"; readonly abandoned: AbandonedPayload<R> } & WriteOutcome<Rels, R> {
332
280
  return isAbandon(sentinel) && outcome.abandoned === sentinel.payload
333
281
  }
334
282
 
335
- /** Builds the abandoned write outcome from the callback's own sentinel (the R10 arm's one mint). */
336
283
  function abandonedOutcome<Rels extends SchemaRelations, R>(
337
284
  sentinel: Abandon<AbandonedPayload<R>>
338
- ): WriteResult<Rels, R> {
339
- const outcome = Object.freeze({ ok: false as const, abandoned: sentinel.payload })
285
+ ): WriteOutcome<Rels, R> {
286
+ const outcome = Object.freeze({ tag: "abandoned" as const, abandoned: sentinel.payload })
340
287
  if (!isAbandonedOutcome<Rels, R>(outcome, sentinel)) {
341
288
  throw errors.new("bumbledb abandon outcome construction incomplete")
342
289
  }
343
290
  return outcome
344
291
  }
345
292
 
346
- /**
347
- * One live write transaction: the submitted delta with the engine's
348
- * FINAL-STATE point-read view (base + pending delta — the exact state the
349
- * commit judgment judges, so check-then-act is race-free by construction).
350
- * Spent when its owning `write`/`writeFrom` call resolves the attempt;
351
- * any later use throws.
352
- */
353
- interface Tx<Rels extends SchemaRelations> {
354
- /**
355
- * Records a collection of inserts. Singleton is `[fact]`. Empty is
356
- * lawful. Returns how many facts were consumed and how many changed
357
- * the in-memory final-state view. Every fact is complete — omitted
358
- * fresh cells are a type error; mint first with {@link Tx.reserve}.
359
- */
360
- insert<R extends MemberRelation<Rels>>(relation: R, facts: Iterable<Fact<R>>): MutationReport
361
- /**
362
- * Records a collection of deletes. Singleton is `[fact]`. Returns
363
- * how many facts were consumed and how many changed the view.
364
- */
293
+ interface WriteTx<Rels extends SchemaRelations> {
294
+ insert<R extends MemberRelation<Rels>>(relation: R, facts: CollectionWrite<R>): MutationReport
295
+
365
296
  delete<R extends MemberRelation<Rels>>(relation: R, facts: Iterable<Fact<R>>): MutationReport
366
- /**
367
- * Mints `count` consecutive fresh values for a `.fresh` field.
368
- * `count === 0n` is empty and does not yield a start.
369
- */
297
+
370
298
  reserve<R extends MemberRelation<Rels>>(relation: R, field: FreshKeys<R> & string, count: bigint): FreshRange
371
- /** Final-state membership of one complete fact. */
299
+
372
300
  contains<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean
373
- /**
374
- * Final-state point lookup through the relation's primary key (the
375
- * {@link KeyFact} rule); `undefined` on a miss.
376
- */
301
+
377
302
  get<R extends MemberRelation<Rels>>(relation: R, key: KeyFact<R>): Fact<R> | undefined
378
- /**
379
- * Final-state point lookup through a DECLARED `key()` statement of this
380
- * schema — the key object is typed by the statement's own projection;
381
- * `undefined` on a miss.
382
- */
383
303
  get<R extends MemberRelation<Rels>, const P extends readonly string[]>(
384
304
  relation: R,
385
305
  keyStatement: KeyStatement<R, P>,
@@ -387,67 +307,40 @@ interface Tx<Rels extends SchemaRelations> {
387
307
  ): Fact<R> | undefined
388
308
  }
389
309
 
390
- /**
391
- * The read view one `db.read(fn)` call scopes — or one `using snap =
392
- * db.read()` acquisition owns (ruled 2026-07-23, R12: lifetimes are
393
- * disposables, never `close()`). The callback form invalidates the value
394
- * when `fn` returns; the `using` form releases it at scope exit through
395
- * `Symbol.dispose` — either way the release is deterministic and
396
- * scope-shaped, every later verb call throws a typed used-after-scope
397
- * error, and the underlying snapshot (with its LMDB reader slot) is
398
- * already closed.
399
- */
400
- interface ReadScope<Rels extends SchemaRelations> extends Disposable {
401
- /**
402
- * The committed generation this scope witnessed — carried by the
403
- * snapshot open itself (one crossing, inside the snapshot's own
404
- * transaction), so it is atomic with the snapshot by construction.
405
- */
310
+ const witnessTypes: unique symbol = Symbol("bumbledb.witness.types")
311
+
312
+ interface Witness<Rels extends SchemaRelations> extends Disposable {
313
+ readonly [witnessTypes]?: Rels
314
+ }
315
+
316
+ interface ReadInstance<Rels extends SchemaRelations> {
406
317
  readonly generation: bigint
407
- /** Full-relation export in row-id order, decoded to bare structural facts. */
318
+
408
319
  scan<R extends MemberRelation<Rels>>(relation: R): Fact<R>[]
409
- /**
410
- * Committed-state point lookup through the relation's primary key
411
- * (the {@link KeyFact} rule); `undefined` on a miss.
412
- */
320
+
321
+ count<R extends MemberRelation<Rels>>(relation: R): bigint
322
+
413
323
  get<R extends MemberRelation<Rels>>(relation: R, key: KeyFact<R>): Fact<R> | undefined
414
- /**
415
- * Committed-state point lookup through a DECLARED `key()` statement of
416
- * this schema — the key object is typed by the statement's own
417
- * projection; `undefined` on a miss.
418
- */
324
+
419
325
  get<R extends MemberRelation<Rels>, const P extends readonly string[]>(
420
326
  relation: R,
421
327
  keyStatement: KeyStatement<R, P>,
422
328
  key: DeclaredKeyFact<R, P>
423
329
  ): Fact<R> | undefined
424
- /** Committed-state membership of one complete fact. */
330
+
425
331
  contains<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean
426
- /**
427
- * Executes a prepared query against this scope's snapshot with the
428
- * typed params object; returns the answer SET as plain rows with
429
- * bare structural values (no order — the host sorts). This is the ONE
430
- * execution spelling ({@link Prepared} carries no `execute`).
431
- */
332
+
432
333
  execute<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Row[]
334
+ prepare<Row, Params extends ParamsRecord>(q: Query<Rels, Row, Params>): Prepared<Rels, Row, Params>
433
335
  }
434
336
 
435
- /**
436
- * The module-private inference slot of {@link Prepared}: an optional symbol
437
- * property (never set at runtime) that keeps the prepared value's `Row` and
438
- * `Params` type arguments load-bearing, so `execute` infers the typed rows
439
- * and the typed params object from the value alone — the query module's
440
- * `inferred` pattern, local to this module. A type-level carrier only:
441
- * values stay bare, nothing is asserted.
442
- */
443
337
  const preparedTypes: unique symbol = Symbol("bumbledb.prepared.types")
444
338
 
445
339
  /**
446
340
  * One prepared query as a plain VALUE: explicit visible compilation
447
341
  * (`db.prepare(q)` lowers, pins the plan, and surfaces every engine roster
448
342
  * refusal), no lifecycle. Execution happens ONLY through
449
- * `snap.execute(prepared, params)` / `db.execute(prepared, params)` — the
450
- * symmetry rule's one spelling. The engine-side plan is reclaimed by a GC
343
+ * `instance.execute(prepared, params)`. The engine-side plan is reclaimed by a GC
451
344
  * finalizer when this value becomes unreachable (reclamation only, never
452
345
  * correctness — an unreclaimed plan is idle memory, and process exit frees
453
346
  * everything).
@@ -456,64 +349,21 @@ interface Prepared<Rels extends SchemaRelations, Row, Params extends ParamsRecor
456
349
  readonly [preparedTypes]?: { readonly rels: Rels; readonly row: Row; readonly params: Params }
457
350
  }
458
351
 
459
- /**
460
- * An open store. There is no close: read through `read`/the read sugar,
461
- * write through `write`/`writeFrom`, and let the process own the
462
- * environment's lifetime (the engine fsyncs every commit, so durability
463
- * never waits on a close). A second `open`/`create` of the same path
464
- * while this handle lives is the engine's `EnvironmentLocked`.
465
- */
466
352
  interface Db<Rels extends SchemaRelations> {
467
- /** The theory this store was opened with (fingerprint-verified by the engine). */
468
353
  readonly schema: Schema<Rels>
469
- /**
470
- * One scoped snapshot read: opens an MVCC snapshot, runs `fn`
471
- * SYNCHRONOUSLY against it, and closes the snapshot unconditionally
472
- * before returning `fn`'s result. The {@link ReadScope} is invalidated
473
- * when `fn` returns — a used-after-scope call throws a typed error.
474
- */
475
- read<T>(fn: (snap: ReadScope<Rels>) => T): T
476
- /**
477
- * The `using` acquisition (ruled 2026-07-23, R12): `using snap =
478
- * db.read()` — the caller owns the scope's lifetime, and the scope's
479
- * `Symbol.dispose` releases the snapshot deterministically at scope
480
- * exit, in the language's own syntax. Lifetimes are disposables, never
481
- * `close()`.
482
- */
483
- read(): ReadScope<Rels>
484
- /** `db.scan(r)` === `db.read(snap => snap.scan(r))` — the symmetry rule. */
485
- scan<R extends MemberRelation<Rels>>(relation: R): Fact<R>[]
486
- /** `db.get(r, k)` === `db.read(snap => snap.get(r, k))` — the symmetry rule. */
487
- get<R extends MemberRelation<Rels>>(relation: R, key: KeyFact<R>): Fact<R> | undefined
488
- /** `db.get(r, s, k)` === `db.read(snap => snap.get(r, s, k))` — the symmetry rule, keyed form. */
489
- get<R extends MemberRelation<Rels>, const P extends readonly string[]>(
490
- relation: R,
491
- keyStatement: KeyStatement<R, P>,
492
- key: DeclaredKeyFact<R, P>
493
- ): Fact<R> | undefined
494
- /** `db.contains(r, f)` === `db.read(snap => snap.contains(r, f))` — the symmetry rule. */
495
- contains<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean
496
- /** `db.execute(p, params)` === `db.read(snap => snap.execute(p, params))` — the symmetry rule. */
497
- execute<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Row[]
354
+
355
+ read<R>(body: (instance: ReadInstance<Rels>, witness: Witness<Rels>) => SyncResult<R>): SyncResult<R>
498
356
  /**
499
357
  * One delta transaction: builds the delta synchronously through `fn`,
500
358
  * commits, and returns the domain outcome. A throw from `fn` aborts
501
359
  * the delta (LMDB untouched) and rethrows wrapped. `fn` may decline to
502
- * commit by returning {@link abandon}`(payload)` (ruled 2026-07-23,
503
- * R10): the transaction rolls back — nothing is committed, not even an
504
- * empty commit — and the outcome is `{ ok: false, abandoned: payload }`,
505
- * an arm the result type carries exactly when the callback can abandon.
506
- */
507
- write<R = void>(fn: DeltaBuild<Rels, R>): WriteResult<Rels, R>
508
- /**
509
- * One-shot witnessed write from a live read snapshot: begins a
510
- * transaction that commits only if no state-changing commit landed
511
- * since `snap` was taken. Must run inside the read callback that owns
512
- * `snap`. A moved generation is the typed {@link ErrGenerationMoved}
513
- * — retry is host policy; this method never loops. `fn` may decline to
514
- * commit by returning {@link abandon}`(payload)`.
360
+ * commit by returning {@link abandon}`(payload)`: the transaction rolls
361
+ * back — nothing is committed, not even an empty commit — and the
362
+ * outcome is `{ tag: "abandoned", abandoned: payload }`.
515
363
  */
516
- writeFrom<R>(snap: ReadScope<Rels>, fn: DeltaBuild<Rels, R>): WriteResult<Rels, R>
364
+ write<R>(fn: (tx: WriteTx<Rels>) => SyncResult<R>): WriteOutcome<Rels, SyncResult<R>>
365
+
366
+ writeFrom<R>(witness: Witness<Rels>, fn: (tx: WriteTx<Rels>) => SyncResult<R>): WriteFromOutcome<Rels, SyncResult<R>>
517
367
  /**
518
368
  * Prepares a query value built against THIS schema (identity is the
519
369
  * membership rule): lowers it to the engine IR, pins the plan, and
@@ -524,7 +374,6 @@ interface Db<Rels extends SchemaRelations> {
524
374
  prepare<Row, Params extends ParamsRecord>(q: Query<Rels, Row, Params>): Prepared<Rels, Row, Params>
525
375
  }
526
376
 
527
- /** One relation's runtime tables: engine id, the identical schema member, field ids, primary key. */
528
377
  interface RelationEntry {
529
378
  readonly id: number
530
379
  readonly member: SchemaRelation
@@ -532,19 +381,11 @@ interface RelationEntry {
532
381
  readonly primaryKey: PrimaryKey | undefined
533
382
  }
534
383
 
535
- /** One relation's primary candidate key: its materialized statement id and projection. */
536
384
  interface PrimaryKey {
537
385
  readonly statementId: number
538
386
  readonly projection: readonly string[]
539
387
  }
540
388
 
541
- /**
542
- * One materialized-statement slot as the SDK mirrors it. Implied auto-keys
543
- * omit `statement` (the engine owns those slots); every declared form
544
- * carries the SDK value that lowered to it. Functionality forms also
545
- * carry the key's owner and projection (what keyed point reads resolve
546
- * through).
547
- */
548
389
  type ImpliedKeyEntry = {
549
390
  readonly kind: "functionality"
550
391
  readonly owner: string
@@ -565,17 +406,72 @@ type StatementEntry =
565
406
  | { readonly kind: "mirrors"; readonly statement: Statement; readonly orientation: "written" | "mirrored" }
566
407
  | { readonly kind: "capacity"; readonly statement: Statement }
567
408
 
568
- /**
569
- * Mirrors the engine's materialized statement order
570
- * (`SchemaDescriptor::materialized_statements`, pinned by the fingerprint):
571
- * one auto-key per fresh field (relation declaration order, then field
572
- * order), one closed auto-key per closed relation (declaration order),
573
- * then the declared statements in declaration order — a `mirrors`
574
- * statement occupying TWO adjacent slots (the engine lowers `==` to two
575
- * containments, `source <= target` first), both owned by the one SDK
576
- * value. This positional match is how statement ids resolve back to SDK
577
- * statement values without the engine ever learning a wire format.
578
- */
409
+ function decodeOffendingFact<Rels extends SchemaRelations>(
410
+ member: SchemaRelation,
411
+ relation: keyof Rels & string,
412
+ fact: WireViolationFact
413
+ ): OffendingFact<Rels> {
414
+ const declared = sealedFieldsOf(member)
415
+ const decoded: Record<string, FactValue> = {}
416
+ for (const cell of fact.fields) {
417
+ const cited = declared.find(function byName(candidate) {
418
+ return candidate.name === cell.name
419
+ })
420
+ const roster = rosterOf(cited?.field)
421
+ decoded[cell.name] =
422
+ roster !== undefined
423
+ ? handleOf(`violation fact ${fact.relation} field ${cell.name}`, roster, cell.value)
424
+ : cell.value
425
+ }
426
+ return Object.freeze({ relation, fact: Object.freeze(decoded) })
427
+ }
428
+
429
+ function violationFromEntry<Rels extends SchemaRelations>(
430
+ entry: StatementEntry,
431
+ wire: WireViolation,
432
+ facts: readonly OffendingFact<Rels>[]
433
+ ): Violation<Rels> {
434
+ const canonical = wire.canonical
435
+ if (entry.kind === "functionality") {
436
+ if (!("statement" in entry)) {
437
+ return Object.freeze({ kind: "functionality", statement: undefined, canonical, facts })
438
+ }
439
+ return Object.freeze({ kind: "functionality", statement: entry.statement, canonical, facts })
440
+ }
441
+ if (entry.kind === "capacity") {
442
+ if (wire.kind !== "capacity") {
443
+ throw errors.new(`bumbledb violation ${wire.statementId} is a capacity slot without a measure`)
444
+ }
445
+ return Object.freeze({
446
+ kind: "capacity",
447
+ statement: entry.statement,
448
+ canonical,
449
+ measure: wire.measure,
450
+ facts
451
+ })
452
+ }
453
+ if (wire.kind !== "containment") {
454
+ throw errors.new(`bumbledb violation ${wire.statementId} is a containment slot without a direction`)
455
+ }
456
+ if (entry.kind === "mirrors") {
457
+ return Object.freeze({
458
+ kind: "containment",
459
+ statement: entry.statement,
460
+ canonical,
461
+ direction: wire.direction,
462
+ orientation: entry.orientation,
463
+ facts
464
+ })
465
+ }
466
+ return Object.freeze({
467
+ kind: "containment",
468
+ statement: entry.statement,
469
+ canonical,
470
+ direction: wire.direction,
471
+ facts
472
+ })
473
+ }
474
+
579
475
  function materializedEntries(theory: AnySchema): StatementEntry[] {
580
476
  const entries = impliedKeyEntries(theory)
581
477
  for (const statement of theory.statements) {
@@ -584,13 +480,6 @@ function materializedEntries(theory: AnySchema): StatementEntry[] {
584
480
  return entries
585
481
  }
586
482
 
587
- /**
588
- * The engine-materialized implied keys, in the engine's pinned order: one
589
- * auto-key per fresh field (relation declaration order, then field order),
590
- * then one closed auto-key `R(id) -> R` per closed relation (declaration
591
- * order). These slots carry no SDK statement value — the engine owns them
592
- * (`schema()` rejects an explicit duplicate).
593
- */
594
483
  function impliedKeyEntries(theory: AnySchema): StatementEntry[] {
595
484
  const entries: StatementEntry[] = []
596
485
  for (const member of Object.values(theory.relations)) {
@@ -619,12 +508,6 @@ function impliedKeyEntries(theory: AnySchema): StatementEntry[] {
619
508
  return entries
620
509
  }
621
510
 
622
- /**
623
- * One declared statement's materialized slots: a key or capacity statement
624
- * occupies one, a `mirrors` occupies two adjacent slots (the engine lowers
625
- * `==` to two containments, `source <= target` first), both owned by the
626
- * one SDK value.
627
- */
628
511
  function declaredEntries(statement: Statement): StatementEntry[] {
629
512
  const data = statement.data
630
513
  switch (data.kind) {
@@ -663,29 +546,12 @@ function isThenable(value: unknown): boolean {
663
546
  return typeof value === "object" && value !== null && "then" in value && typeof value.then === "function"
664
547
  }
665
548
 
666
- /**
667
- * Narrows a keyed-get middle argument to a statement value (vs a key
668
- * object) through the statement module's admission brand — a
669
- * REPRESENTATION, never a shape probe: fact cell shapes are structurally
670
- * OPEN (an interval value carrying an excess `kind` property is a legal
671
- * cell), so no property probe could ever be sound here, but no host-built
672
- * key object can spell the module-private brand symbol.
673
- */
674
549
  function isStatementValue<R extends AnyRelation, P extends readonly string[]>(
675
550
  value: KeyFact<R> | KeyStatement<R, P>
676
551
  ): value is KeyStatement<R, P> {
677
552
  return isStatement(value)
678
553
  }
679
554
 
680
- /**
681
- * THE one selector dispatch of the `get` overload pair (primary-key vs
682
- * key-statement, `docs/architecture/70-api.md` § the freeze): judges the
683
- * middle argument once and hands the narrowed pieces to the chosen
684
- * continuation. `Db.get` and the read scope's `get` both dispatch through
685
- * here, so the two mismatch refusals speak with one voice and the symmetry
686
- * rule (`db.get(...) === db.read(snap => snap.get(...))`) holds by
687
- * construction.
688
- */
689
555
  function selectKeyRead<R extends AnyRelation, P extends readonly string[], T>(
690
556
  keyOrStatement: KeyFact<R> | KeyStatement<R, P>,
691
557
  declaredKey: DeclaredKeyFact<R, P> | undefined,
@@ -704,22 +570,11 @@ function selectKeyRead<R extends AnyRelation, P extends readonly string[], T>(
704
570
  return byPrimary(keyOrStatement)
705
571
  }
706
572
 
707
- /** The id-resolution tables one open builds: relation entries by name, statement slots by id. */
708
573
  interface Tables {
709
574
  readonly relations: ReadonlyMap<string, RelationEntry>
710
575
  readonly statements: readonly StatementEntry[]
711
576
  }
712
577
 
713
- /**
714
- * Builds the id-resolution tables from the manifest, verifying the SDK's
715
- * positional mirror against the engine's reported order — any drift
716
- * (count, kind, id, or membership) is a construction-time failure, never a
717
- * silent misattribution of a violation to the wrong statement value. The
718
- * declaration-ordinal law the query lowering leans on is verified in the
719
- * same walks: relation ids and sealed field ids both equal declaration
720
- * order, so a constructed `Tables` IS the proof and `prepare` inherits it
721
- * structurally — never a silently misaddressed query.
722
- */
723
578
  function tablesOf(theory: AnySchema, manifest: Manifest): Tables {
724
579
  const entries = materializedEntries(theory)
725
580
  if (entries.length !== manifest.statements.length) {
@@ -780,39 +635,59 @@ function tablesOf(theory: AnySchema, manifest: Manifest): Tables {
780
635
  return Object.freeze({ relations, statements: Object.freeze(entries) })
781
636
  }
782
637
 
783
- /** The point-read half a transaction and a read scope share, over their own handle. */
638
+ function tablesFromTheory(theory: AnySchema): Tables {
639
+ const entries = materializedEntries(theory)
640
+ const relations = new Map<string, RelationEntry>()
641
+ Object.keys(theory.relations).forEach(function byOrdinal(name, ordinal) {
642
+ const member = theory.relations[name]
643
+ if (member === undefined) {
644
+ throw errors.new(`bumbledb theory has no relation ${name}`)
645
+ }
646
+ const fieldIds = new Map<string, number>()
647
+ sealedFieldsOf(member).forEach(function byField(declared, fieldOrdinal) {
648
+ fieldIds.set(declared.name, fieldOrdinal)
649
+ })
650
+ let primaryKey: PrimaryKey | undefined
651
+ entries.forEach(function firstOwnedKey(entry, index) {
652
+ if (primaryKey === undefined && entry.kind === "functionality" && entry.owner === name) {
653
+ primaryKey = Object.freeze({ statementId: index, projection: entry.projection })
654
+ }
655
+ })
656
+ relations.set(name, Object.freeze({ id: ordinal, member, fieldIds, primaryKey }))
657
+ })
658
+ return Object.freeze({ relations, statements: Object.freeze(entries) })
659
+ }
660
+
784
661
  interface PointReads {
785
662
  contains(relationId: number, row: readonly FactValue[]): boolean
786
663
  get(relationId: number, statementId: number, key: readonly FactValue[]): FactValue[] | null
787
664
  }
788
665
 
789
- /**
790
- * One read scope's PRIVATE lifetime record: its live snapshot handle, the
791
- * generation it witnessed (carried by the snapshot open itself — one
792
- * crossing, finding 016), its liveness flag (flipped when the owning
793
- * `read` callback returns, or by the scope's own
794
- * `Symbol.dispose`), its close latch (`closed` — the snapshot closes
795
- * exactly once, whichever of the owner and the dispose gets there first),
796
- * and its owning store's identity token. Held in {@link scopeStates} —
797
- * the snapshot handle is never a public value.
798
- */
799
- interface ScopeState {
800
- readonly handle: SnapshotHandle
801
- readonly generation: bigint
666
+ interface InstanceState {
667
+ readonly handle: InstanceHandle
802
668
  live: boolean
803
- closed: boolean
804
669
  readonly owner: object
805
670
  }
806
671
 
807
- /** The private lifetime records of this module's read scopes. */
808
- const scopeStates = new WeakMap<object, ScopeState>()
672
+ const instanceStates = new WeakMap<object, InstanceState>()
673
+
674
+ interface WitnessState {
675
+ readonly handle: WitnessHandle
676
+ spent: boolean
677
+ readonly owner: object
678
+ }
679
+
680
+ const witnessStates = new WeakMap<object, WitnessState>()
681
+
682
+ const witnessReclaimer = new FinalizationRegistry<WitnessHandle>(function reclaimWitness(handle) {
683
+ const closed = errors.trySync(function closeWitness() {
684
+ native.witnessClose(handle)
685
+ })
686
+ if (closed.error) {
687
+ return
688
+ }
689
+ })
809
690
 
810
- /**
811
- * One prepared value's PRIVATE engine half: the pinned plan handle, the
812
- * owning store's identity token, and the query's marshaling tables (params
813
- * in declaration order, select columns in head order). Held in
814
- * {@link preparedPlans} — the plan handle is never a public value.
815
- */
816
691
  interface PreparedPlan {
817
692
  readonly handle: PreparedHandle
818
693
  readonly owner: object
@@ -820,15 +695,8 @@ interface PreparedPlan {
820
695
  readonly finds: readonly FindColumn[]
821
696
  }
822
697
 
823
- /** The private engine halves of this module's prepared values. */
824
698
  const preparedPlans = new WeakMap<object, PreparedPlan>()
825
699
 
826
- /**
827
- * Reclaims the engine-side plan of a garbage-collected {@link Prepared}
828
- * value. RECLAMATION ONLY, never correctness: a plan the collector never
829
- * visits is idle engine memory until process exit, and a failure to close
830
- * is swallowed (there is no one left to care — the owning value is gone).
831
- */
832
700
  const planReclaimer = new FinalizationRegistry<PreparedHandle>(function reclaimPlan(handle) {
833
701
  const closed = errors.trySync(function closePlan() {
834
702
  native.preparedClose(handle)
@@ -838,145 +706,314 @@ const planReclaimer = new FinalizationRegistry<PreparedHandle>(function reclaimP
838
706
  }
839
707
  })
840
708
 
841
- /**
842
- * The typed generation-moved refusal `writeFrom` throws when a
843
- * state-changing commit landed since the witness snapshot: retry is host
844
- * policy. Match with `errors.is`.
845
- */
846
- const ErrGenerationMoved = errors.new(
847
- "bumbledb generationMoved: a state-changing commit landed since the witness snapshot"
709
+ const ErrAsyncCallback = errors.new(
710
+ "bumbledb asyncCallback: a read or write callback returned a thenable — the callback is synchronous"
848
711
  )
712
+ const ErrSpentHandle = errors.new("bumbledb spentHandle: a consumed builder, instance, or witness was used")
713
+ const ErrUseAfterScope = errors.new(
714
+ "bumbledb useAfterScope: a stashed read instance or write transaction was used after its callback returned"
715
+ )
716
+ const ErrForeignPrepared = errors.new("bumbledb foreignPrepared: a prepared query met a foreign instance")
717
+ const ErrForeignWitness = errors.new("bumbledb foreignWitness: a witness met a foreign store")
718
+
719
+ interface CatalogNative {
720
+ scan(relationId: number): FactValue[][]
721
+ count(relationId: number): bigint
722
+ contains(relationId: number, values: readonly FactValue[]): boolean
723
+ get(relationId: number, statementId: number, keyValues: readonly FactValue[]): FactValue[] | null
724
+ prepare(query: ReturnType<typeof lowerQuery>): ReturnType<typeof native.instancePrepare>
725
+ execute(prepared: PreparedHandle, params: ReturnType<typeof wireParams>): FactValue[][]
726
+ }
849
727
 
850
- /**
851
- * Constructs one open `Db` over an already-admitted handle: builds the
852
- * id-resolution tables once and closes over them — the `Db` owns handle
853
- * and tables and nothing else. Handle lifetime is the process's: the store
854
- * cache holds the environment handle until the exit hook closes it.
855
- */
856
- function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<Rels>, manifest: Manifest): Db<Rels> {
857
- const tables = tablesOf(theory, manifest)
858
- /** This store's identity token: read scopes and prepared values carry it, so cross-store use is a typed refusal. */
859
- const owner = Object.freeze({})
860
-
861
- function isMemberName(name: string): name is keyof Rels & string {
862
- return tables.relations.has(name)
863
- }
864
-
865
- function resolveOrdinary(relation: AnyRelation): RelationEntry {
866
- const entry = tables.relations.get(relation.name)
867
- if (entry === undefined || entry.member !== relation) {
868
- throw errors.new(`relation ${relation.name} is not a member of schema ${theory.name}`)
728
+ function catalogMethods<Rels extends SchemaRelations>(
729
+ theory: Schema<Rels>,
730
+ tables: Tables,
731
+ owner: object,
732
+ assertLive: () => void,
733
+ ops: CatalogNative
734
+ ): Pick<ReadInstance<Rels>, "scan" | "count" | "get" | "contains" | "execute" | "prepare"> {
735
+ function planOf(prepared: object): PreparedPlan {
736
+ const plan = preparedPlans.get(prepared)
737
+ if (plan === undefined) {
738
+ throw errors.wrap(ErrForeignPrepared, "bumbledb execute target is not a prepared value of this SDK")
869
739
  }
870
- if (isClosedMember(relation)) {
871
- throw errors.new(
872
- `relation ${relation.name} is closed — its extension is schema data (axioms), never scanned or written`
740
+ if (plan.owner !== owner) {
741
+ throw errors.wrap(
742
+ ErrForeignPrepared,
743
+ `bumbledb prepared value was prepared by a different store than this one (schema ${theory.name})`
873
744
  )
874
745
  }
875
- return entry
746
+ return plan
876
747
  }
877
-
878
- function offendingFactOf(fact: WireViolationFact): OffendingFact<Rels> {
879
- const entry = tables.relations.get(fact.relation)
880
- if (entry === undefined || !isMemberName(fact.relation)) {
881
- throw errors.new(`bumbledb violation cites unknown relation ${fact.relation}`)
882
- }
883
- const declared = sealedFieldsOf(entry.member)
884
- const decoded: Record<string, FactValue> = {}
885
- for (const cell of fact.fields) {
886
- const cited = declared.find(function byName(candidate) {
887
- return candidate.name === cell.name
888
- })
889
- const roster = rosterOf(cited?.field)
890
- decoded[cell.name] =
891
- roster !== undefined
892
- ? handleOf(`violation fact ${fact.relation} field ${cell.name}`, roster, cell.value)
893
- : cell.value
894
- }
895
- return Object.freeze({ relation: fact.relation, fact: Object.freeze(decoded) })
748
+ function contains<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean {
749
+ assertLive()
750
+ const entry = ordinaryEntry(tables, theory, relation)
751
+ return bridged("bumbledb instance contains", function readContains() {
752
+ return ops.contains(entry.id, rowOf(relation.data, recordOf(fact)))
753
+ })
896
754
  }
897
-
898
- function violationOf(wire: WireViolation): Violation<Rels> {
899
- const entry = tables.statements[wire.statementId]
900
- if (entry === undefined) {
901
- throw errors.new(`bumbledb violation cites unknown statement id ${wire.statementId}`)
902
- }
903
- const facts = Object.freeze(wire.facts.map(offendingFactOf))
904
- const canonical = wire.canonical
905
- if (entry.kind === "functionality") {
906
- if (!("statement" in entry)) {
907
- return Object.freeze({ kind: "functionality", statement: undefined, canonical, facts })
908
- }
909
- return Object.freeze({ kind: "functionality", statement: entry.statement, canonical, facts })
910
- }
911
- if (entry.kind === "capacity") {
912
- if (wire.kind !== "capacity") {
913
- throw errors.new(`bumbledb violation ${wire.statementId} is a capacity slot without a measure`)
755
+ function get<R extends MemberRelation<Rels>, const P extends readonly string[]>(
756
+ relation: R,
757
+ keyOrStatement: KeyFact<R> | KeyStatement<R, P>,
758
+ declaredKey?: DeclaredKeyFact<R, P>
759
+ ): Fact<R> | undefined {
760
+ assertLive()
761
+ const entry = ordinaryEntry(tables, theory, relation)
762
+ return selectKeyRead(
763
+ keyOrStatement,
764
+ declaredKey,
765
+ function byStatement(statement, key) {
766
+ const selected = declaredKeyOf(tables, theory, relation, statement)
767
+ const row = bridged("bumbledb instance get", function readGet() {
768
+ return ops.get(entry.id, selected.statementId, keyRowOf(relation.data, selected.projection, recordOf(key)))
769
+ })
770
+ return row === null ? undefined : factOf(relation, row)
771
+ },
772
+ function byPrimary(key) {
773
+ const primaryKey = entry.primaryKey
774
+ if (primaryKey === undefined) {
775
+ throw errors.new(
776
+ `relation ${relation.name} has no candidate key — keyed get requires a fresh field or a declared key statement`
777
+ )
778
+ }
779
+ const row = bridged("bumbledb instance get", function readGet() {
780
+ return ops.get(
781
+ entry.id,
782
+ primaryKey.statementId,
783
+ keyRowOf(relation.data, primaryKey.projection, recordOf(key))
784
+ )
785
+ })
786
+ return row === null ? undefined : factOf(relation, row)
914
787
  }
915
- return Object.freeze({
916
- kind: "capacity",
917
- statement: entry.statement,
918
- canonical,
919
- measure: wire.measure,
920
- facts
921
- })
922
- }
923
- if (wire.kind !== "containment") {
924
- throw errors.new(`bumbledb violation ${wire.statementId} is a containment slot without a direction`)
925
- }
926
- if (entry.kind === "mirrors") {
927
- return Object.freeze({
928
- kind: "containment",
929
- statement: entry.statement,
930
- canonical,
931
- direction: wire.direction,
932
- orientation: entry.orientation,
933
- facts
934
- })
935
- }
936
- return Object.freeze({
937
- kind: "containment",
938
- statement: entry.statement,
939
- canonical,
940
- direction: wire.direction,
941
- facts
788
+ )
789
+ }
790
+ function scan<R extends MemberRelation<Rels>>(relation: R): Fact<R>[] {
791
+ assertLive()
792
+ const entry = ordinaryEntry(tables, theory, relation)
793
+ const rows = bridged("bumbledb instance scan", function readScan() {
794
+ return ops.scan(entry.id)
795
+ })
796
+ return rows.map(function decodeRow(row) {
797
+ return factOf(relation, row)
942
798
  })
943
799
  }
944
-
945
- /**
946
- * Resolves a key-statement-selected read: the statement must be the
947
- * IDENTICAL `key()` value this schema declared (identity is the
948
- * membership rule) and must key `relation` — its materialized statement
949
- * id comes from the positional mirror, so the engine point-reads through
950
- * exactly the declared projection.
951
- */
952
- function declaredKeyOf(relation: AnyRelation, statement: Statement): PrimaryKey {
953
- const statementId = tables.statements.findIndex(function byIdentity(candidate) {
954
- return "statement" in candidate && candidate.statement === statement
800
+ function count<R extends MemberRelation<Rels>>(relation: R): bigint {
801
+ assertLive()
802
+ const entry = ordinaryEntry(tables, theory, relation)
803
+ return bridged("bumbledb instance count", function readCount() {
804
+ return ops.count(entry.id)
955
805
  })
956
- const entry = tables.statements[statementId]
957
- if (entry === undefined) {
806
+ }
807
+ function execute<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Row[] {
808
+ assertLive()
809
+ const plan = planOf(prepared)
810
+ const wire = wireParams(plan.params, recordOf(params))
811
+ const rows = bridged("execute bumbledb prepared query", function callExecute() {
812
+ return ops.execute(plan.handle, wire)
813
+ })
814
+ return decodeAnswers<Row>(plan.finds, rows)
815
+ }
816
+ function prepare<Row, Params extends ParamsRecord>(q: Query<Rels, Row, Params>): Prepared<Rels, Row, Params> {
817
+ assertLive()
818
+ if (q.schema !== theory) {
958
819
  throw errors.new(
959
- `keyed get statement is not a declared statement of schema ${theory.name} — statement identity is the membership rule`
820
+ `query was built against schema ${q.schema.name}, not the identical schema value this store opened with — schema identity is the membership rule`
960
821
  )
961
822
  }
962
- if (entry.kind !== "functionality") {
963
- throw errors.new("keyed get takes a key() statement — containments and capacity statements key nothing")
823
+ const queryIr = lowerQuery(q)
824
+ const outcome = bridged("prepare bumbledb query", function callPrepare() {
825
+ return ops.prepare(queryIr)
826
+ })
827
+ if (!outcome.ok) {
828
+ throwPrepareRefusal(outcome.message)
964
829
  }
965
- if (entry.owner !== relation.name) {
966
- throw errors.new(
967
- `keyed get statement keys ${entry.owner}, not ${relation.name} — the statement must be a declared key of the relation it reads`
830
+ const prepared: Prepared<Rels, Row, Params> = Object.freeze({})
831
+ preparedPlans.set(
832
+ prepared,
833
+ Object.freeze({
834
+ handle: outcome.prepared,
835
+ owner,
836
+ params: q.data.params,
837
+ finds: q.data.finds
838
+ })
839
+ )
840
+ planReclaimer.register(prepared, outcome.prepared)
841
+ return prepared
842
+ }
843
+ return { scan, count, get, contains, execute, prepare }
844
+ }
845
+
846
+ function ordinaryEntry(tables: Tables, theory: AnySchema, relation: AnyRelation): RelationEntry {
847
+ const entry = tables.relations.get(relation.name)
848
+ if (entry === undefined || entry.member !== relation) {
849
+ throw errors.new(`relation ${relation.name} is not a member of schema ${theory.name}`)
850
+ }
851
+ if (isClosedMember(relation)) {
852
+ throw errors.new(
853
+ `relation ${relation.name} is closed — its extension is schema data (axioms), never scanned or written`
854
+ )
855
+ }
856
+ return entry
857
+ }
858
+
859
+ function declaredKeyOf(tables: Tables, theory: AnySchema, relation: AnyRelation, statement: Statement): PrimaryKey {
860
+ const statementId = tables.statements.findIndex(function byIdentity(candidate) {
861
+ return "statement" in candidate && candidate.statement === statement
862
+ })
863
+ const entry = tables.statements[statementId]
864
+ if (entry === undefined) {
865
+ throw errors.new(
866
+ `keyed get statement is not a declared statement of schema ${theory.name} — statement identity is the membership rule`
867
+ )
868
+ }
869
+ if (entry.kind !== "functionality") {
870
+ throw errors.new("keyed get takes a key() statement — containments and capacity statements key nothing")
871
+ }
872
+ if (entry.owner !== relation.name) {
873
+ throw errors.new(
874
+ `keyed get statement keys ${entry.owner}, not ${relation.name} — the statement must be a declared key of the relation it reads`
875
+ )
876
+ }
877
+ return Object.freeze({ statementId, projection: entry.projection })
878
+ }
879
+
880
+ function overlayMethods<Rels extends SchemaRelations>(
881
+ theory: Schema<Rels>,
882
+ tables: Tables,
883
+ assertLive: () => void,
884
+ reads: PointReads
885
+ ): Pick<WriteTx<Rels>, "contains" | "get"> {
886
+ function contains<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean {
887
+ assertLive()
888
+ const entry = ordinaryEntry(tables, theory, relation)
889
+ return reads.contains(entry.id, rowOf(relation.data, recordOf(fact)))
890
+ }
891
+ function readThroughKey<R extends MemberRelation<Rels>>(
892
+ relation: R,
893
+ entry: RelationEntry,
894
+ selected: PrimaryKey,
895
+ key: Readonly<Record<string, unknown>>
896
+ ): Fact<R> | undefined {
897
+ const row = reads.get(entry.id, selected.statementId, keyRowOf(relation.data, selected.projection, key))
898
+ if (row === null) {
899
+ return undefined
900
+ }
901
+ return factOf(relation, row)
902
+ }
903
+ function get<R extends MemberRelation<Rels>>(relation: R, key: KeyFact<R>): Fact<R> | undefined
904
+ function get<R extends MemberRelation<Rels>, const P extends readonly string[]>(
905
+ relation: R,
906
+ keyStatement: KeyStatement<R, P>,
907
+ key: DeclaredKeyFact<R, P>
908
+ ): Fact<R> | undefined
909
+ function get<R extends MemberRelation<Rels>, const P extends readonly string[]>(
910
+ relation: R,
911
+ keyOrStatement: KeyFact<R> | KeyStatement<R, P>,
912
+ declaredKey?: DeclaredKeyFact<R, P>
913
+ ): Fact<R> | undefined {
914
+ assertLive()
915
+ const entry = ordinaryEntry(tables, theory, relation)
916
+ return selectKeyRead(
917
+ keyOrStatement,
918
+ declaredKey,
919
+ function byStatement(statement, key) {
920
+ return readThroughKey(relation, entry, declaredKeyOf(tables, theory, relation, statement), recordOf(key))
921
+ },
922
+ function byPrimary(key) {
923
+ const primaryKey = entry.primaryKey
924
+ if (primaryKey === undefined) {
925
+ throw errors.new(
926
+ `relation ${relation.name} has no candidate key — keyed get requires a fresh field or a declared key statement`
927
+ )
928
+ }
929
+ return readThroughKey(relation, entry, primaryKey, recordOf(key))
930
+ }
931
+ )
932
+ }
933
+ return { contains, get }
934
+ }
935
+
936
+ function createReadInstance<Rels extends SchemaRelations>(
937
+ nativeHandle: InstanceHandle,
938
+ theory: Schema<Rels>,
939
+ tables: Tables,
940
+ owner: object
941
+ ): ReadInstance<Rels> {
942
+ const state: InstanceState = { handle: nativeHandle, live: true, owner }
943
+ function assertLive(): void {
944
+ if (!state.live) {
945
+ throw errors.wrap(
946
+ ErrUseAfterScope,
947
+ "bumbledb read instance is invalidated — its owning callback already returned"
968
948
  )
969
949
  }
970
- return Object.freeze({ statementId, projection: entry.projection })
950
+ }
951
+ const methods = catalogMethods(theory, tables, owner, assertLive, {
952
+ scan(relationId) {
953
+ return native.instanceScan(state.handle, relationId)
954
+ },
955
+ count(relationId) {
956
+ return native.instanceCount(state.handle, relationId)
957
+ },
958
+ contains(relationId, values) {
959
+ return native.instanceContains(state.handle, relationId, values)
960
+ },
961
+ get(relationId, statementId, keyValues) {
962
+ return native.instanceGet(state.handle, relationId, statementId, keyValues)
963
+ },
964
+ prepare(query) {
965
+ return native.instancePrepare(state.handle, query)
966
+ },
967
+ execute(prepared, params) {
968
+ return native.preparedExecute(prepared, state.handle, params)
969
+ }
970
+ })
971
+ const instance: ReadInstance<Rels> = Object.freeze({
972
+ get generation() {
973
+ assertLive()
974
+ return bridged("bumbledb instance generation", function readGeneration() {
975
+ return native.instanceGeneration(state.handle)
976
+ })
977
+ },
978
+ ...methods
979
+ })
980
+ instanceStates.set(instance, state)
981
+ return instance
982
+ }
983
+
984
+ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<Rels>, manifest: Manifest): Db<Rels> {
985
+ const tables = tablesOf(theory, manifest)
986
+ /** This store's identity token: read scopes and prepared values carry it, so cross-store use is a typed refusal. */
987
+ const owner = Object.freeze({})
988
+
989
+ function isMemberName(name: string): name is keyof Rels & string {
990
+ return tables.relations.has(name)
991
+ }
992
+
993
+ function violationOf(wire: WireViolation): Violation<Rels> {
994
+ const entry = tables.statements[wire.statementId]
995
+ if (entry === undefined) {
996
+ throw errors.new(`bumbledb violation cites unknown statement id ${wire.statementId}`)
997
+ }
998
+ const facts = Object.freeze(
999
+ wire.facts.map(function offending(fact) {
1000
+ const rel = tables.relations.get(fact.relation)
1001
+ if (rel === undefined || !isMemberName(fact.relation)) {
1002
+ throw errors.new(`bumbledb violation cites unknown relation ${fact.relation}`)
1003
+ }
1004
+ return decodeOffendingFact<Rels>(rel.member, fact.relation, fact)
1005
+ })
1006
+ )
1007
+ return violationFromEntry(entry, wire, facts)
971
1008
  }
972
1009
 
973
1010
  function pointReadsOf(assertLive: () => void, reads: PointReads) {
974
1011
  function contains<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean {
975
1012
  assertLive()
976
- const entry = resolveOrdinary(relation)
1013
+ const entry = ordinaryEntry(tables, theory, relation)
977
1014
  return reads.contains(entry.id, rowOf(relation.data, recordOf(fact)))
978
1015
  }
979
- /** One keyed point read through an already-resolved key, decoded to a fact (`undefined` on a miss). */
1016
+
980
1017
  function readThroughKey<R extends MemberRelation<Rels>>(
981
1018
  relation: R,
982
1019
  entry: RelationEntry,
@@ -1001,12 +1038,12 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
1001
1038
  declaredKey?: DeclaredKeyFact<R, P>
1002
1039
  ): Fact<R> | undefined {
1003
1040
  assertLive()
1004
- const entry = resolveOrdinary(relation)
1041
+ const entry = ordinaryEntry(tables, theory, relation)
1005
1042
  return selectKeyRead(
1006
1043
  keyOrStatement,
1007
1044
  declaredKey,
1008
1045
  function byStatement(statement, key) {
1009
- return readThroughKey(relation, entry, declaredKeyOf(relation, statement), recordOf(key))
1046
+ return readThroughKey(relation, entry, declaredKeyOf(tables, theory, relation, statement), recordOf(key))
1010
1047
  },
1011
1048
  function byPrimary(key) {
1012
1049
  const primaryKey = entry.primaryKey
@@ -1022,201 +1059,72 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
1022
1059
  return { contains, get }
1023
1060
  }
1024
1061
 
1025
- /**
1026
- * Resolves a prepared value's private plan, refusing foreign objects
1027
- * and prepared values of other stores as typed errors.
1028
- */
1029
- function planOf(prepared: object): PreparedPlan {
1030
- const plan = preparedPlans.get(prepared)
1031
- if (plan === undefined) {
1032
- throw errors.new("bumbledb execute target is not a prepared value of this SDK")
1033
- }
1034
- if (plan.owner !== owner) {
1035
- throw errors.new(
1036
- `bumbledb prepared value was prepared by a different store than this one (schema ${theory.name})`
1037
- )
1038
- }
1039
- return plan
1062
+ function pinPrepared<Row, Params extends ParamsRecord>(
1063
+ preparedHandle: PreparedHandle,
1064
+ q: Query<Rels, Row, Params>
1065
+ ): Prepared<Rels, Row, Params> {
1066
+ const prepared: Prepared<Rels, Row, Params> = Object.freeze({})
1067
+ preparedPlans.set(
1068
+ prepared,
1069
+ Object.freeze({
1070
+ handle: preparedHandle,
1071
+ owner,
1072
+ params: q.data.params,
1073
+ finds: q.data.finds
1074
+ })
1075
+ )
1076
+ planReclaimer.register(prepared, preparedHandle)
1077
+ return prepared
1040
1078
  }
1041
1079
 
1042
- /**
1043
- * Builds one {@link ReadScope} over a live scope state. Every verb
1044
- * asserts liveness first: the owning call flips `state.live` the moment
1045
- * its callback returns (or the scope's own `Symbol.dispose` does, for a
1046
- * `using`-acquired scope), so a leaked scope is a typed refusal forever
1047
- * after.
1048
- */
1049
- function makeScope(state: ScopeState): ReadScope<Rels> {
1050
- function assertLive(): void {
1051
- if (!state.live) {
1052
- throw errors.new("bumbledb read scope is invalidated — its owning read callback already returned")
1053
- }
1054
- }
1055
- const reads = pointReadsOf(assertLive, {
1056
- contains(relationId, row) {
1057
- return bridged("bumbledb snapshot contains", function readContains() {
1058
- return native.snapshotContains(state.handle, relationId, row)
1059
- })
1060
- },
1061
- get(relationId, statementId, key) {
1062
- return bridged("bumbledb snapshot get", function readGet() {
1063
- return native.snapshotGet(state.handle, relationId, statementId, key)
1080
+ function makeWitness(nativeHandle: WitnessHandle): Witness<Rels> {
1081
+ const state: WitnessState = { handle: nativeHandle, spent: false, owner }
1082
+ const witness: Witness<Rels> = Object.freeze({
1083
+ [Symbol.dispose](): void {
1084
+ if (state.spent) {
1085
+ return
1086
+ }
1087
+ state.spent = true
1088
+ bridged("close bumbledb witness", function closeWitness() {
1089
+ native.witnessClose(nativeHandle)
1064
1090
  })
1065
1091
  }
1066
1092
  })
1067
- function scan<R extends MemberRelation<Rels>>(relation: R): Fact<R>[] {
1068
- assertLive()
1069
- const entry = resolveOrdinary(relation)
1070
- const rows = bridged("bumbledb snapshot scan", function readScan() {
1071
- return native.snapshotScan(state.handle, entry.id)
1072
- })
1073
- return rows.map(function decodeRow(row) {
1074
- return factOf(relation, row)
1075
- })
1076
- }
1077
- function execute<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Row[] {
1078
- assertLive()
1079
- const plan = planOf(prepared)
1080
- const wire = wireParams(plan.params, recordOf(params))
1081
- const rows = bridged("execute bumbledb prepared query", function callExecute() {
1082
- return native.preparedExecute(plan.handle, state.handle, wire)
1083
- })
1084
- return decodeAnswers<Row>(plan.finds, rows)
1085
- }
1086
- /** The R12 teardown: invalidate, then close — idempotent through the state's close latch. */
1087
- function dispose(): void {
1088
- state.live = false
1089
- closeScopeState(state)
1090
- }
1091
- const scope: ReadScope<Rels> = Object.freeze({
1092
- generation: state.generation,
1093
- scan,
1094
- get: reads.get,
1095
- contains: reads.contains,
1096
- execute,
1097
- [Symbol.dispose]: dispose
1098
- })
1099
- scopeStates.set(scope, state)
1100
- return scope
1101
- }
1102
-
1103
- /**
1104
- * Live-handle accounting (diagnostic law, prod EINVAL 2026-07-17): every
1105
- * snapshot open/close is counted so a write-begin failure can report how
1106
- * many read handles were live at the fault — a leaked scope is invisible
1107
- * until the exact moment it matters, so the failure carries the census.
1108
- */
1109
- let liveSnapshots = 0
1110
-
1111
- /**
1112
- * Opens one snapshot and its scope state (live until the owner flips
1113
- * it). The witnessed generation rides the snapshot open itself — one
1114
- * crossing carries both (finding 016), so the fault-pairing close
1115
- * branch a second `dbGeneration` call needed is structurally gone.
1116
- */
1117
- function openScopeState(): ScopeState {
1118
- const opened = bridged("open bumbledb snapshot", function openSnapshot() {
1119
- return native.dbSnapshot(handle)
1120
- })
1121
- liveSnapshots += 1
1122
- return { handle: opened.snapshot, generation: opened.generation, live: true, closed: false, owner }
1093
+ witnessStates.set(witness, state)
1094
+ witnessReclaimer.register(witness, nativeHandle)
1095
+ return witness
1123
1096
  }
1124
1097
 
1125
- /**
1126
- * Closes a scope's snapshot after the owner invalidated it — a LATCH:
1127
- * the snapshot closes exactly once, whichever of the owning call and
1128
- * the scope's own `Symbol.dispose` gets there first, so an early
1129
- * in-callback disposal never double-closes (and never double-counts
1130
- * the census).
1131
- */
1132
- function closeScopeState(state: ScopeState): void {
1133
- if (state.closed) {
1134
- return
1135
- }
1136
- state.closed = true
1137
- bridged("close bumbledb snapshot", function closeSnapshot() {
1138
- native.snapshotClose(state.handle)
1139
- })
1140
- liveSnapshots -= 1
1098
+ function makeInstance(nativeHandle: InstanceHandle): ReadInstance<Rels> {
1099
+ return createReadInstance(nativeHandle, theory, tables, owner)
1141
1100
  }
1142
1101
 
1143
- function read<T>(fn: (snap: ReadScope<Rels>) => T): T
1144
- function read(): ReadScope<Rels>
1145
- function read<T>(fn?: (snap: ReadScope<Rels>) => T): T | ReadScope<Rels> {
1146
- const state = openScopeState()
1147
- const scope = makeScope(state)
1148
- if (fn === undefined) {
1149
- /**
1150
- * The `using` acquisition (R12): the caller owns the lifetime —
1151
- * `using snap = db.read()` — and the scope's `Symbol.dispose`
1152
- * is the deterministic release, scope-shaped in the language's
1153
- * own syntax.
1154
- */
1155
- return scope
1156
- }
1157
- const result = errors.trySync(function runRead() {
1158
- return fn(scope)
1159
- })
1160
- state.live = false
1161
- closeScopeState(state)
1162
- if (result.error) {
1163
- throw errors.wrap(result.error, "bumbledb read")
1164
- }
1165
- return result.data
1166
- }
1167
-
1168
- function scan<R extends MemberRelation<Rels>>(relation: R): Fact<R>[] {
1169
- return read(function scanInScope(snap) {
1170
- return snap.scan(relation)
1171
- })
1172
- }
1173
-
1174
- function get<R extends MemberRelation<Rels>>(relation: R, key: KeyFact<R>): Fact<R> | undefined
1175
- function get<R extends MemberRelation<Rels>, const P extends readonly string[]>(
1176
- relation: R,
1177
- keyStatement: KeyStatement<R, P>,
1178
- key: DeclaredKeyFact<R, P>
1179
- ): Fact<R> | undefined
1180
- function get<R extends MemberRelation<Rels>, const P extends readonly string[]>(
1181
- relation: R,
1182
- keyOrStatement: KeyFact<R> | KeyStatement<R, P>,
1183
- declaredKey?: DeclaredKeyFact<R, P>
1184
- ): Fact<R> | undefined {
1185
- return read(function getInScope(snap) {
1186
- return selectKeyRead(
1187
- keyOrStatement,
1188
- declaredKey,
1189
- function byStatement(statement, key) {
1190
- return snap.get(relation, statement, key)
1191
- },
1192
- function byPrimary(key) {
1193
- return snap.get(relation, key)
1102
+ function read<R>(body: (instance: ReadInstance<Rels>, witness: Witness<Rels>) => SyncResult<R>): SyncResult<R> {
1103
+ let captured: R | undefined
1104
+ const result = bridged("bumbledb read", function runRead() {
1105
+ return native.dbRead(handle, function onRead(nativeInstance, nativeWitness) {
1106
+ const instance = makeInstance(nativeInstance)
1107
+ const witness = makeWitness(nativeWitness)
1108
+ const value = body(instance, witness)
1109
+ const state = instanceStates.get(instance)
1110
+ if (state !== undefined) {
1111
+ state.live = false
1194
1112
  }
1195
- )
1196
- })
1197
- }
1198
-
1199
- function contains<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean {
1200
- return read(function containsInScope(snap) {
1201
- return snap.contains(relation, fact)
1202
- })
1203
- }
1204
-
1205
- function execute<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Row[] {
1206
- return read(function executeInScope(snap) {
1207
- return snap.execute(prepared, params)
1113
+ if (isThenable(value)) {
1114
+ throw errors.wrap(ErrAsyncCallback, "bumbledb read callback returned a thenable")
1115
+ }
1116
+ captured = value
1117
+ return value
1118
+ })
1208
1119
  })
1120
+ return (captured ?? result) as SyncResult<R>
1209
1121
  }
1210
1122
 
1211
- /**
1212
- * Builds one {@link Tx} over a transaction-handle thunk: `write` and
1213
- * `writeFrom` pass an already-begun handle.
1214
- */
1215
- function makeTx(resolveTx: () => TxHandle): { readonly tx: Tx<Rels>; spend(): void } {
1123
+ function makeTx(resolveTx: () => TxHandle): { readonly tx: WriteTx<Rels>; spend(): void } {
1216
1124
  const txState = { spent: false }
1217
1125
  function assertLive(): void {
1218
1126
  if (txState.spent) {
1219
- throw errors.new("bumbledb write transaction is spent")
1127
+ throw errors.wrap(ErrUseAfterScope, "bumbledb write transaction is spent")
1220
1128
  }
1221
1129
  }
1222
1130
  const reads = pointReadsOf(assertLive, {
@@ -1233,29 +1141,23 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
1233
1141
  })
1234
1142
  }
1235
1143
  })
1236
- function insert<R extends MemberRelation<Rels>>(relation: R, facts: Iterable<Fact<R>>): MutationReport {
1144
+ function insert<R extends MemberRelation<Rels>>(relation: R, facts: CollectionWrite<R>): MutationReport {
1237
1145
  assertLive()
1238
- const rows: FactValue[][] = []
1239
- for (const fact of facts) {
1240
- rows.push(rowOf(relation.data, recordOf(fact)))
1241
- }
1242
- const entry = resolveOrdinary(relation)
1146
+ const entry = ordinaryEntry(tables, theory, relation)
1243
1147
  const txHandle = resolveTx()
1244
- const report = bridged("bumbledb tx insert", function record() {
1245
- return native.txInsert(txHandle, entry.id, rows)
1148
+ return mutateCollection(relation, facts, function applyCells(rows, cells) {
1149
+ return bridged("bumbledb tx insert", function record() {
1150
+ return native.txInsert(txHandle, entry.id, rows, cells)
1151
+ })
1246
1152
  })
1247
- return Object.freeze({ submitted: report.submitted, changed: report.changed })
1248
1153
  }
1249
1154
  function remove<R extends MemberRelation<Rels>>(relation: R, facts: Iterable<Fact<R>>): MutationReport {
1250
1155
  assertLive()
1251
- const rows: FactValue[][] = []
1252
- for (const fact of facts) {
1253
- rows.push(rowOf(relation.data, recordOf(fact)))
1254
- }
1255
- const entry = resolveOrdinary(relation)
1156
+ const entry = ordinaryEntry(tables, theory, relation)
1256
1157
  const txHandle = resolveTx()
1158
+ const flat = rowsOf(relation, facts)
1257
1159
  const report = bridged("bumbledb tx delete", function record() {
1258
- return native.txDelete(txHandle, entry.id, rows)
1160
+ return native.txDelete(txHandle, entry.id, flat.rows, flat.cells)
1259
1161
  })
1260
1162
  return Object.freeze({ submitted: report.submitted, changed: report.changed })
1261
1163
  }
@@ -1265,7 +1167,7 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
1265
1167
  count: bigint
1266
1168
  ): FreshRange {
1267
1169
  assertLive()
1268
- const entry = resolveOrdinary(relation)
1170
+ const entry = ordinaryEntry(tables, theory, relation)
1269
1171
  const declared = relation.data.fields.find(function byName(candidate) {
1270
1172
  return candidate.name === field
1271
1173
  })
@@ -1282,7 +1184,7 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
1282
1184
  })
1283
1185
  return freshRangeOf(range)
1284
1186
  }
1285
- const tx: Tx<Rels> = Object.freeze({
1187
+ const tx: WriteTx<Rels> = Object.freeze({
1286
1188
  insert,
1287
1189
  delete: remove,
1288
1190
  reserve,
@@ -1295,111 +1197,92 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
1295
1197
  return { tx, spend }
1296
1198
  }
1297
1199
 
1298
- function runDelta<R>(txHandle: TxHandle, fn: DeltaBuild<Rels, R>): WriteResult<Rels, R> {
1299
- const made = makeTx(function resolveTx() {
1300
- return txHandle
1301
- })
1302
- const built = errors.trySync(function buildDelta() {
1303
- return fn(made.tx)
1304
- })
1305
- made.spend()
1306
- if (built.error) {
1307
- bridged("abort bumbledb write transaction", function abort() {
1308
- native.txAbort(txHandle)
1200
+ function mapNativeWrite<R>(nativeOutcome: NativeWriteOutcome, built: R | undefined): WriteFromOutcome<Rels, R> {
1201
+ if (nativeOutcome.tag === "moved") {
1202
+ return Object.freeze({
1203
+ tag: "moved" as const,
1204
+ witnessed: nativeOutcome.witnessed,
1205
+ current: nativeOutcome.current
1309
1206
  })
1310
- throw errors.wrap(built.error, "build write delta")
1311
1207
  }
1312
- if (isThenable(built.data)) {
1313
- /**
1314
- * An `async` callback TYPECHECKS (Promise<void> is assignable where
1315
- * a `void` return is expected) but its body runs after the tx is
1316
- * spent: committing here would be a silent EMPTY commit reported
1317
- * ok while the callback's real inserts throw "spent" as unhandled
1318
- * rejections. Refused typed instead — abort, nothing committed
1319
- * (the same one-writer law as the thrown-callback path).
1320
- */
1321
- bridged("abort bumbledb write transaction", function abort() {
1322
- native.txAbort(txHandle)
1323
- })
1324
- throw errors.new(
1325
- "bumbledb write callback returned a thenable — the delta build is synchronous; an async callback is refused, nothing was committed"
1326
- )
1327
- }
1328
- if (isAbandon(built.data)) {
1329
- /**
1330
- * The caller's explicit decline to commit (R10): the sentinel's
1331
- * contract is unconditional — roll back, nothing committed, not
1332
- * even an empty commit; commit is unreachable for a sentinel
1333
- * result.
1334
- */
1335
- bridged("abort bumbledb write transaction", function abort() {
1336
- native.txAbort(txHandle)
1208
+ if (nativeOutcome.tag === "rejected") {
1209
+ return Object.freeze({
1210
+ tag: "rejected" as const,
1211
+ violations: Object.freeze(nativeOutcome.violations.map(violationOf))
1337
1212
  })
1338
- return abandonedOutcome<Rels, R>(built.data)
1339
1213
  }
1340
- const committed = errors.trySync(function commitDelta() {
1341
- return bridged("commit bumbledb write transaction", function commit() {
1342
- return native.txCommit(txHandle)
1343
- })
1344
- })
1345
- if (committed.error) {
1346
- /**
1347
- * A THROWN commit (engine I/O failure, bridge fault) must never
1348
- * leave the write transaction live: LMDB holds one writer per
1349
- * environment, and a leaked handle turns every later begin into
1350
- * EINVAL for the process's lifetime. The abort is best-effort —
1351
- * the native side may already have consumed the handle.
1352
- */
1353
- const aborted = errors.trySync(function abortAfterFailedCommit() {
1354
- native.txAbort(txHandle)
1355
- })
1356
- if (aborted.error) {
1214
+ if (nativeOutcome.tag === "abandoned") {
1215
+ if (built === undefined || !isAbandon(built)) {
1216
+ throw errors.new("bumbledb write abandoned without an abandon sentinel")
1357
1217
  }
1358
- throw errors.wrap(committed.error, "commit bumbledb write transaction")
1359
- }
1360
- const outcome = committed.data
1361
- if (outcome.ok) {
1362
- return Object.freeze({ ok: true, generation: outcome.generation })
1218
+ return abandonedOutcome<Rels, R>(built)
1363
1219
  }
1364
1220
  return Object.freeze({
1365
- ok: false,
1366
- violations: Object.freeze(outcome.violations.map(violationOf))
1221
+ tag: "accepted" as const,
1222
+ value: Object.freeze({
1223
+ value: built as Exclude<R, Abandon<unknown>>,
1224
+ generation: nativeOutcome.generation
1225
+ })
1367
1226
  })
1368
1227
  }
1369
1228
 
1370
- function write<R = void>(fn: DeltaBuild<Rels, R>): WriteResult<Rels, R> {
1371
- const txHandle = bridged(`begin bumbledb write transaction (live snapshots: ${liveSnapshots})`, function begin() {
1372
- return native.dbWriteBegin(handle)
1229
+ function runWrite<R>(
1230
+ invoke: (callback: (tx: TxHandle) => boolean) => NativeWriteOutcome,
1231
+ fn: (tx: WriteTx<Rels>) => SyncResult<R>
1232
+ ): WriteFromOutcome<Rels, SyncResult<R>> {
1233
+ let built: SyncResult<R> | undefined
1234
+ const nativeOutcome = bridged("bumbledb write", function callWrite() {
1235
+ return invoke(function onWrite(txHandle) {
1236
+ const made = makeTx(function resolveTx() {
1237
+ return txHandle
1238
+ })
1239
+ const result = errors.trySync(function buildDelta() {
1240
+ return fn(made.tx)
1241
+ })
1242
+ made.spend()
1243
+ if (result.error) {
1244
+ throw errors.wrap(result.error, "build write delta")
1245
+ }
1246
+ if (isThenable(result.data)) {
1247
+ throw errors.wrap(ErrAsyncCallback, "bumbledb write callback returned a thenable")
1248
+ }
1249
+ built = result.data
1250
+ return !isAbandon(result.data)
1251
+ })
1373
1252
  })
1374
- return runDelta(txHandle, fn)
1253
+ return mapNativeWrite(nativeOutcome, built)
1375
1254
  }
1376
1255
 
1377
- /**
1378
- * One-shot write from a live snapshot this store owns. Begins immediately;
1379
- * a moved generation throws {@link ErrGenerationMoved} and `fn` never
1380
- * runs. The snapshot stays owned by the caller's read callback.
1381
- */
1382
- function writeFrom<R>(snap: ReadScope<Rels>, fn: DeltaBuild<Rels, R>): WriteResult<Rels, R> {
1383
- const snapState = scopeStates.get(snap)
1384
- if (snapState === undefined) {
1385
- throw errors.new("bumbledb writeFrom witness is not a read scope of this SDK")
1256
+ function write<R>(fn: (tx: WriteTx<Rels>) => SyncResult<R>): WriteOutcome<Rels, SyncResult<R>> {
1257
+ const outcome = runWrite(function invoke(callback) {
1258
+ return native.dbWrite(handle, callback)
1259
+ }, fn)
1260
+ if (outcome.tag === "moved") {
1261
+ throw errors.new("bumbledb write reported moved — unconditional writes cannot move")
1386
1262
  }
1387
- if (snapState.owner !== owner) {
1388
- throw errors.new(`bumbledb writeFrom snapshot belongs to a different store (schema ${theory.name})`)
1389
- }
1390
- if (!snapState.live) {
1391
- throw errors.new("bumbledb writeFrom snapshot is invalidated — its owning read callback already returned")
1263
+ return outcome
1264
+ }
1265
+
1266
+ function writeFrom<R>(
1267
+ witness: Witness<Rels>,
1268
+ fn: (tx: WriteTx<Rels>) => SyncResult<R>
1269
+ ): WriteFromOutcome<Rels, SyncResult<R>> {
1270
+ const state = witnessStates.get(witness)
1271
+ if (state === undefined) {
1272
+ throw errors.wrap(ErrForeignWitness, "bumbledb writeFrom witness is not a witness of this SDK")
1392
1273
  }
1393
- const witnessed = bridged("begin witnessed bumbledb write transaction", function begin() {
1394
- return native.dbWriteFrom(handle, snapState.handle)
1395
- })
1396
- if (!witnessed.ok) {
1274
+ if (state.owner !== owner) {
1397
1275
  throw errors.wrap(
1398
- ErrGenerationMoved,
1399
- `writeFrom: generation moved (witnessed ${witnessed.witnessed} current ${witnessed.current}) against schema ${theory.name}`
1276
+ ErrForeignWitness,
1277
+ `bumbledb writeFrom witness belongs to a different store (schema ${theory.name})`
1400
1278
  )
1401
1279
  }
1402
- return runDelta(witnessed.tx, fn)
1280
+ if (state.spent) {
1281
+ throw errors.wrap(ErrSpentHandle, "bumbledb writeFrom witness has been disposed")
1282
+ }
1283
+ return runWrite(function invoke(callback) {
1284
+ return native.dbWriteFrom(handle, state.handle, callback)
1285
+ }, fn)
1403
1286
  }
1404
1287
 
1405
1288
  function prepare<Row, Params extends ParamsRecord>(q: Query<Rels, Row, Params>): Prepared<Rels, Row, Params> {
@@ -1413,133 +1296,376 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
1413
1296
  return native.dbPrepare(handle, queryIr)
1414
1297
  })
1415
1298
  if (!outcome.ok) {
1416
- throw errors.new(`bumbledb ${outcome.kind} (prepare): ${outcome.message}`)
1299
+ throwPrepareRefusal(outcome.message)
1417
1300
  }
1418
- const preparedHandle = outcome.prepared
1419
- const prepared: Prepared<Rels, Row, Params> = Object.freeze({})
1420
- preparedPlans.set(
1421
- prepared,
1422
- Object.freeze({
1423
- handle: preparedHandle,
1424
- owner,
1425
- params: q.data.params,
1426
- finds: q.data.finds
1427
- })
1428
- )
1429
- planReclaimer.register(prepared, preparedHandle)
1430
- return prepared
1301
+ return pinPrepared(outcome.prepared, q)
1431
1302
  }
1432
1303
 
1433
1304
  return Object.freeze({
1434
1305
  schema: theory,
1435
1306
  read,
1436
- scan,
1437
- get,
1438
- contains,
1439
- execute,
1440
1307
  write,
1441
1308
  writeFrom,
1442
1309
  prepare
1443
1310
  })
1444
1311
  }
1445
1312
 
1446
- /**
1447
- * The engine twin of the schema-level class wall, as a matchable value
1448
- * (`errors.is`): the shared lowering rejected a spec whose statement pairs
1449
- * faces with disagreeing newtype labels — the faces of a dependency agree
1450
- * on their newtype, or neither carries one. UNREACHABLE through the typed
1451
- * builder (the SDK computes every label from the laws, so its lowered
1452
- * specs cohere by construction); a raw spec handed to the bridge is the
1453
- * one road here, and the runtime referee that proves the engine judges
1454
- * what the types claim.
1455
- */
1456
1313
  const ErrNewtypeMismatch = errors.new(
1457
1314
  "bumbledb newtypeMismatch: a statement pairs faces whose newtypes disagree — the faces of a dependency agree on their newtype, or neither carries one"
1458
1315
  )
1316
+ const ErrSchemaError = errors.new("bumbledb schemaError: the declaration failed validation")
1317
+ const ErrFingerprintMismatch = errors.new("bumbledb fingerprintMismatch: the store's schema does not match this theory")
1318
+ const ErrIrError = errors.new("bumbledb irError: the query failed validation")
1319
+
1320
+ function throwOpenRefusal(
1321
+ verb: string,
1322
+ canonical: string,
1323
+ kind: "schemaError" | "newtypeMismatch" | "fingerprintMismatch",
1324
+ message: string
1325
+ ): never {
1326
+ const detail = `${verb} ${canonical}: ${message}`
1327
+ if (kind === "newtypeMismatch") {
1328
+ throw errors.wrap(ErrNewtypeMismatch, detail)
1329
+ }
1330
+ if (kind === "schemaError") {
1331
+ throw errors.wrap(ErrSchemaError, detail)
1332
+ }
1333
+ throw errors.wrap(ErrFingerprintMismatch, detail)
1334
+ }
1459
1335
 
1460
- /**
1461
- * The one admission path both verbs share: lower the theory, run one
1462
- * bridge call, and wrap the domain refusals — `schemaError` (spec
1463
- * resolution + schema validation, every issue in one message),
1464
- * `newtypeMismatch` (the coherence wall, {@link ErrNewtypeMismatch}),
1465
- * and `fingerprintMismatch` (a different theory cannot open the store)
1466
- * — into typed errors carrying the engine's message intact.
1467
- * Environment failures (a second live writer on the same path, IO)
1468
- * throw from the bridge; the engine's `EnvironmentLocked` message is
1469
- * "another live handle holds this environment's lock".
1470
- */
1471
- function admit<Rels extends SchemaRelations>(
1472
- verb: "create" | "open",
1336
+ function throwPrepareRefusal(message: string): never {
1337
+ throw errors.wrap(ErrIrError, `prepare: ${message}`)
1338
+ }
1339
+
1340
+ function openFromHandle<Rels extends SchemaRelations>(dbHandle: DbHandle, theory: Schema<Rels>): Db<Rels> {
1341
+ const manifest = bridged("fetch bumbledb manifest", function fetchManifest() {
1342
+ return native.dbManifest(dbHandle)
1343
+ })
1344
+ return openDb(dbHandle, theory, manifest)
1345
+ }
1346
+
1347
+ async function createStore<Rels extends SchemaRelations>(
1473
1348
  storePath: string,
1474
1349
  theory: Schema<Rels>
1475
- ): Db<Rels> {
1350
+ ): Promise<Admission<Rels, Db<Rels>>> {
1476
1351
  const canonical = path.resolve(storePath)
1477
1352
  const spec = lower(theory)
1478
- const opened = bridged(`${verb} bumbledb store at ${canonical}`, function callBridge() {
1479
- if (verb === "create") {
1480
- return native.dbCreate(canonical, spec)
1481
- }
1353
+ const created = await bridgedAsync(`create bumbledb store at ${canonical}`, function callBridge() {
1354
+ return native.dbCreate(canonical, spec)
1355
+ })
1356
+ if (created.tag === "schemaError" || created.tag === "newtypeMismatch") {
1357
+ throwOpenRefusal("create", canonical, created.tag, created.message)
1358
+ }
1359
+ if (created.tag === "rejected") {
1360
+ return Object.freeze({
1361
+ tag: "rejected" as const,
1362
+ violations: Object.freeze(
1363
+ created.violations.map(function mapWire(wire) {
1364
+ return mapViolationWithoutStore<Rels>(theory, wire)
1365
+ })
1366
+ )
1367
+ })
1368
+ }
1369
+ return Object.freeze({ tag: "accepted" as const, value: openFromHandle(created.db, theory) })
1370
+ }
1371
+
1372
+ function mapViolationWithoutStore<Rels extends SchemaRelations>(
1373
+ theory: Schema<Rels>,
1374
+ wire: WireViolation
1375
+ ): Violation<Rels> {
1376
+ const entries = materializedEntries(theory)
1377
+ const entry = entries[wire.statementId]
1378
+ if (entry === undefined) {
1379
+ throw errors.new(`bumbledb violation cites unknown statement id ${wire.statementId}`)
1380
+ }
1381
+ const facts = Object.freeze(
1382
+ wire.facts.map(function offending(fact) {
1383
+ const member = theory.relations[fact.relation]
1384
+ if (member === undefined || !(fact.relation in theory.relations)) {
1385
+ throw errors.new(`bumbledb violation cites unknown relation ${fact.relation}`)
1386
+ }
1387
+ return decodeOffendingFact<Rels>(member, fact.relation as keyof Rels & string, fact)
1388
+ })
1389
+ )
1390
+ return violationFromEntry(entry, wire, facts)
1391
+ }
1392
+
1393
+ async function openStore<Rels extends SchemaRelations>(storePath: string, theory: Schema<Rels>): Promise<Db<Rels>> {
1394
+ const canonical = path.resolve(storePath)
1395
+ const spec = lower(theory)
1396
+ const opened = await bridgedAsync(`open bumbledb store at ${canonical}`, function callBridge() {
1482
1397
  return native.dbOpen(canonical, spec)
1483
1398
  })
1484
1399
  if (!opened.ok) {
1485
- if (opened.kind === "newtypeMismatch") {
1486
- throw errors.wrap(ErrNewtypeMismatch, `${verb} ${canonical}: ${opened.message}`)
1400
+ throwOpenRefusal("open", canonical, opened.kind, opened.message)
1401
+ }
1402
+ return openFromHandle(opened.db, theory)
1403
+ }
1404
+
1405
+ interface OwnedInstance<Rels extends SchemaRelations> extends Disposable {
1406
+ prepare<Row, Params extends ParamsRecord>(q: Query<Rels, Row, Params>): Prepared<Rels, Row, Params>
1407
+ execute<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Row[]
1408
+ scan<R extends MemberRelation<Rels>>(relation: R): Fact<R>[]
1409
+ count<R extends MemberRelation<Rels>>(relation: R): bigint
1410
+ contains<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean
1411
+
1412
+ get<R extends MemberRelation<Rels>>(relation: R, key: KeyFact<R>): Fact<R> | undefined
1413
+ get<R extends MemberRelation<Rels>, const P extends readonly string[]>(
1414
+ relation: R,
1415
+ keyStatement: KeyStatement<R, P>,
1416
+ key: DeclaredKeyFact<R, P>
1417
+ ): Fact<R> | undefined
1418
+ }
1419
+
1420
+ interface InstanceBuilder<Rels extends SchemaRelations> extends Disposable {
1421
+ load<R extends MemberRelation<Rels>>(relation: R, facts: CollectionWrite<R>): MutationReport
1422
+ delete<R extends MemberRelation<Rels>>(relation: R, facts: Iterable<Fact<R>>): MutationReport
1423
+ reserve<R extends MemberRelation<Rels>>(relation: R, field: FreshKeys<R> & string, count: bigint): FreshRange
1424
+ contains<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean
1425
+
1426
+ get<R extends MemberRelation<Rels>>(relation: R, key: KeyFact<R>): Fact<R> | undefined
1427
+ get<R extends MemberRelation<Rels>, const P extends readonly string[]>(
1428
+ relation: R,
1429
+ keyStatement: KeyStatement<R, P>,
1430
+ key: DeclaredKeyFact<R, P>
1431
+ ): Fact<R> | undefined
1432
+ admit(): Promise<Admission<Rels, OwnedInstance<Rels>>>
1433
+ }
1434
+
1435
+ const ownedRecords = new WeakMap<object, { handle: OwnedHandle; theory: AnySchema; spent: boolean; owner: object }>()
1436
+ const builderRecords = new WeakMap<object, { handle: BuilderHandle; theory: AnySchema; spent: boolean }>()
1437
+
1438
+ const ownedReclaimer = new FinalizationRegistry<OwnedHandle>(function reclaimOwned(handle) {
1439
+ const closed = errors.trySync(function closeOwned() {
1440
+ native.ownedInstanceClose(handle)
1441
+ })
1442
+ if (closed.error) {
1443
+ return
1444
+ }
1445
+ })
1446
+
1447
+ const builderReclaimer = new FinalizationRegistry<BuilderHandle>(function reclaimBuilder(handle) {
1448
+ const closed = errors.trySync(function closeBuilder() {
1449
+ native.instanceBuilderClose(handle)
1450
+ })
1451
+ if (closed.error) {
1452
+ return
1453
+ }
1454
+ })
1455
+
1456
+ function wrapOwned<Rels extends SchemaRelations>(nativeHandle: OwnedHandle, theory: Schema<Rels>): OwnedInstance<Rels> {
1457
+ const owner = Object.freeze({})
1458
+ const rec = { handle: nativeHandle, theory, spent: false, owner }
1459
+ const tables = tablesFromTheory(theory)
1460
+ function assertLive(): void {
1461
+ if (rec.spent) {
1462
+ throw errors.wrap(ErrSpentHandle, "bumbledb owned instance has been disposed")
1487
1463
  }
1488
- throw errors.new(`bumbledb ${opened.kind} (${verb} ${canonical}): ${opened.message}`)
1489
1464
  }
1490
- const manifest = bridged("fetch bumbledb manifest", function fetchManifest() {
1491
- return native.dbManifest(opened.db)
1465
+ const methods = catalogMethods(theory, tables, owner, assertLive, {
1466
+ scan(relationId) {
1467
+ return native.ownedScan(nativeHandle, relationId)
1468
+ },
1469
+ count(relationId) {
1470
+ return native.ownedCount(nativeHandle, relationId)
1471
+ },
1472
+ contains(relationId, values) {
1473
+ return native.ownedContains(nativeHandle, relationId, values)
1474
+ },
1475
+ get(relationId, statementId, keyValues) {
1476
+ return native.ownedGet(nativeHandle, relationId, statementId, keyValues)
1477
+ },
1478
+ prepare(query) {
1479
+ return native.ownedPrepare(nativeHandle, query)
1480
+ },
1481
+ execute(prepared, params) {
1482
+ return native.ownedExecute(prepared, nativeHandle, params)
1483
+ }
1484
+ })
1485
+ const instance: OwnedInstance<Rels> = Object.freeze({
1486
+ ...methods,
1487
+ [Symbol.dispose](): void {
1488
+ if (rec.spent) {
1489
+ return
1490
+ }
1491
+ try {
1492
+ native.ownedInstanceClose(nativeHandle)
1493
+ } catch (caught) {
1494
+ const error = errorFromThrow(caught)
1495
+ if (/leased for publish/.test(error.message)) {
1496
+ throw errors.wrap(ErrSpentHandle, "bumbledb owned instance is leased for publish")
1497
+ }
1498
+ throw errors.wrap(error, "close bumbledb owned instance")
1499
+ }
1500
+ rec.spent = true
1501
+ ownedReclaimer.unregister(instance)
1502
+ }
1503
+ })
1504
+ ownedRecords.set(instance, rec)
1505
+ ownedReclaimer.register(instance, nativeHandle, instance)
1506
+ return instance
1507
+ }
1508
+
1509
+ function wrapBuilder<Rels extends SchemaRelations>(
1510
+ nativeHandle: BuilderHandle,
1511
+ theory: Schema<Rels>
1512
+ ): InstanceBuilder<Rels> {
1513
+ const rec = { handle: nativeHandle, theory, spent: false }
1514
+ const tables = tablesFromTheory(theory)
1515
+ function assertLive(): void {
1516
+ if (rec.spent) {
1517
+ throw errors.wrap(ErrSpentHandle, "bumbledb instance builder has been spent")
1518
+ }
1519
+ }
1520
+ const overlay = overlayMethods(theory, tables, assertLive, {
1521
+ contains(relationId, row) {
1522
+ return bridged("bumbledb builder contains", function readContains() {
1523
+ return native.instanceBuilderContains(nativeHandle, relationId, row)
1524
+ })
1525
+ },
1526
+ get(relationId, statementId, key) {
1527
+ return bridged("bumbledb builder get", function readGet() {
1528
+ return native.instanceBuilderGet(nativeHandle, relationId, statementId, key)
1529
+ })
1530
+ }
1492
1531
  })
1493
- return openDb(opened.db, theory, manifest)
1532
+ const builder: InstanceBuilder<Rels> = Object.freeze({
1533
+ load<R extends MemberRelation<Rels>>(relation: R, facts: CollectionWrite<R>): MutationReport {
1534
+ assertLive()
1535
+ const entry = ordinaryEntry(tables, theory, relation)
1536
+ return mutateCollection(relation, facts, function applyCells(rows, cells) {
1537
+ return bridged("bumbledb builder load", function loadCells() {
1538
+ return native.instanceBuilderLoad(nativeHandle, entry.id, rows, cells)
1539
+ })
1540
+ })
1541
+ },
1542
+ delete<R extends MemberRelation<Rels>>(relation: R, facts: Iterable<Fact<R>>): MutationReport {
1543
+ assertLive()
1544
+ const entry = ordinaryEntry(tables, theory, relation)
1545
+ const flat = rowsOf(relation, facts)
1546
+ const report = bridged("bumbledb builder delete", function remove() {
1547
+ return native.instanceBuilderDelete(nativeHandle, entry.id, flat.rows, flat.cells)
1548
+ })
1549
+ return Object.freeze({ submitted: report.submitted, changed: report.changed })
1550
+ },
1551
+ reserve<R extends MemberRelation<Rels>>(relation: R, field: FreshKeys<R> & string, count: bigint): FreshRange {
1552
+ assertLive()
1553
+ const entry = ordinaryEntry(tables, theory, relation)
1554
+ const declared = relation.data.fields.find(function byName(candidate) {
1555
+ return candidate.name === field
1556
+ })
1557
+ if (declared === undefined || !isFreshField(declared.field)) {
1558
+ throw errors.new(`relation ${relation.name}: field ${field} is not a fresh cell`)
1559
+ }
1560
+ const fieldId = entry.fieldIds.get(field)
1561
+ if (fieldId === undefined) {
1562
+ throw errors.new(`bumbledb manifest drift: relation ${relation.name} has no field id for ${field}`)
1563
+ }
1564
+ const range = bridged("bumbledb builder reserve", function mint() {
1565
+ return native.instanceBuilderReserve(nativeHandle, entry.id, fieldId, count)
1566
+ })
1567
+ return freshRangeOf(range)
1568
+ },
1569
+ contains: overlay.contains,
1570
+ get: overlay.get,
1571
+ async admit(): Promise<Admission<Rels, OwnedInstance<Rels>>> {
1572
+ if (rec.spent) {
1573
+ throw errors.wrap(ErrSpentHandle, "bumbledb instance builder has been spent")
1574
+ }
1575
+ rec.spent = true
1576
+ builderReclaimer.unregister(builder)
1577
+ let outcome: AdmitResult
1578
+ try {
1579
+ outcome = await native.instanceBuilderAdmit(nativeHandle)
1580
+ } catch (caught) {
1581
+ throw errors.wrap(errorFromThrow(caught), "admit bumbledb instance")
1582
+ }
1583
+ if (outcome.tag === "rejected") {
1584
+ return Object.freeze({
1585
+ tag: "rejected" as const,
1586
+ violations: Object.freeze(
1587
+ outcome.violations.map(function mapWire(wire) {
1588
+ return mapViolationWithoutStore<Rels>(theory, wire)
1589
+ })
1590
+ )
1591
+ })
1592
+ }
1593
+ return Object.freeze({
1594
+ tag: "accepted" as const,
1595
+ value: wrapOwned(outcome.value, theory)
1596
+ })
1597
+ },
1598
+ [Symbol.dispose](): void {
1599
+ if (rec.spent) {
1600
+ return
1601
+ }
1602
+ rec.spent = true
1603
+ builderReclaimer.unregister(builder)
1604
+ bridged("close bumbledb instance builder", function closeBuilder() {
1605
+ native.instanceBuilderClose(nativeHandle)
1606
+ })
1607
+ }
1608
+ })
1609
+ builderRecords.set(builder, rec)
1610
+ builderReclaimer.register(builder, nativeHandle, builder)
1611
+ return builder
1494
1612
  }
1495
1613
 
1614
+ const InstanceBuilder = Object.freeze({
1615
+ create<Rels extends SchemaRelations>(theory: Schema<Rels>): InstanceBuilder<Rels> {
1616
+ const spec = lower(theory)
1617
+ const handle = bridged("create bumbledb instance builder", function make() {
1618
+ return native.instanceBuilderNew(spec)
1619
+ })
1620
+ return wrapBuilder(handle, theory)
1621
+ }
1622
+ })
1623
+
1496
1624
  /**
1497
1625
  * The store lifecycle — `Db.create(path, schema)` / `Db.open(path, schema)`.
1498
1626
  * Create refuses an already-initialized directory; open verifies format
1499
- * version, store kind, and the schema fingerprint. A second live handle
1500
- * on the same path is the engine's `EnvironmentLocked`. There is no close
1501
- * anywhere: the process owns the environment until GC/exit (durability is
1502
- * the engine's per-commit fsync). One store kind exists: durable —
1503
- * resume = reopen in a fresh process, or hold the `Db` this process opened.
1627
+ * version and the schema fingerprint. A second live handle on the same
1628
+ * path is the engine's `EnvironmentLocked`. There is no close anywhere:
1629
+ * the process owns the environment until GC/exit (durability is the
1630
+ * engine's per-commit fsync). Resume = reopen in a fresh process, or
1631
+ * hold the `Db` this process opened.
1504
1632
  */
1505
1633
  const Db = Object.freeze({
1506
- /** Creates a fresh durable store at `path` from the schema. */
1507
- async create<Rels extends SchemaRelations>(path: string, theory: Schema<Rels>): Promise<Db<Rels>> {
1508
- return admit("create", path, theory)
1634
+ async create<Rels extends SchemaRelations>(
1635
+ storePath: string,
1636
+ theory: Schema<Rels>
1637
+ ): Promise<Admission<Rels, Db<Rels>>> {
1638
+ return createStore(storePath, theory)
1509
1639
  },
1510
- /**
1511
- * Opens an existing durable store at `path` with the same theory.
1512
- * A fingerprint-matching open also BACK-FILLS the store's persisted
1513
- * schema descriptor when it is absent (self-describing stores, engine
1514
- * 50-storage.md § the `_meta` block), so a legacy store becomes
1515
- * exhumable after one ordinary open — adoption is automatic, never a
1516
- * separate verb. A second open of a still-live path is
1517
- * `EnvironmentLocked`.
1518
- */
1519
- async open<Rels extends SchemaRelations>(path: string, theory: Schema<Rels>): Promise<Db<Rels>> {
1520
- return admit("open", path, theory)
1640
+
1641
+ async open<Rels extends SchemaRelations>(storePath: string, theory: Schema<Rels>): Promise<Db<Rels>> {
1642
+ return openStore(storePath, theory)
1521
1643
  },
1522
- /**
1523
- * Opens a store READ-ONLY from its own persisted descriptor — the SDK's
1524
- * one schema-independent read path (no theory, no fingerprint check; the
1525
- * store rebirth tool's entry). Lives beside `open`/`create` so the path
1526
- * law stays in one place: the same `node:path.resolve` canonicalization,
1527
- * applied here. The value is NOT cached and is a DISPOSABLE lifetime
1528
- * (R12) — `using exhumed = await Db.exhume(path)` releases the engine
1529
- * handle and the store's exclusive lock at scope exit, so a same-path
1530
- * reopen never waits on GC. A store not yet adopted rejects with the
1531
- * typed `ErrExhumeNoDescriptor` (the remedy: one fingerprint-matching
1532
- * `Db.open` under the creating schema back-fills the descriptor).
1533
- */
1534
- async exhume(storePath: string): Promise<Exhumed> {
1535
- return exhumeStore(path.resolve(storePath))
1644
+ async fromInstance<Rels extends SchemaRelations>(
1645
+ storePath: string,
1646
+ instance: OwnedInstance<Rels>
1647
+ ): Promise<Db<Rels>> {
1648
+ const rec = ownedRecords.get(instance)
1649
+ if (rec === undefined) {
1650
+ throw errors.wrap(ErrSpentHandle, "bumbledb fromInstance target is not an owned instance of this SDK")
1651
+ }
1652
+ if (rec.spent) {
1653
+ throw errors.wrap(ErrSpentHandle, "bumbledb fromInstance target has been disposed")
1654
+ }
1655
+ const canonical = path.resolve(storePath)
1656
+ const dbHandle = await bridgedAsync(`publish bumbledb instance at ${canonical}`, function publish() {
1657
+ return native.dbFromInstance(canonical, rec.handle)
1658
+ })
1659
+ return openFromHandle(dbHandle, rec.theory as Schema<Rels>)
1536
1660
  }
1537
1661
  })
1538
1662
 
1539
1663
  export type {
1540
1664
  Abandon,
1541
1665
  AbandonedArm,
1666
+ Admission,
1542
1667
  CapacityViolation,
1668
+ Committed,
1543
1669
  ContainmentViolation,
1544
1670
  DeclaredKeyFact,
1545
1671
  DeclaredKeyViolation,
@@ -1550,10 +1676,27 @@ export type {
1550
1676
  MirrorViolation,
1551
1677
  MutationReport,
1552
1678
  OffendingFact,
1679
+ OwnedInstance,
1553
1680
  Prepared,
1554
- ReadScope,
1555
- Tx,
1681
+ ReadInstance,
1682
+ SyncResult,
1556
1683
  Violation,
1557
- WriteResult
1684
+ Witness,
1685
+ WriteFromOutcome,
1686
+ WriteOutcome,
1687
+ WriteTx
1688
+ }
1689
+ export {
1690
+ abandon,
1691
+ Db,
1692
+ ErrAsyncCallback,
1693
+ ErrFingerprintMismatch,
1694
+ ErrForeignPrepared,
1695
+ ErrForeignWitness,
1696
+ ErrIrError,
1697
+ ErrNewtypeMismatch,
1698
+ ErrSchemaError,
1699
+ ErrSpentHandle,
1700
+ ErrUseAfterScope,
1701
+ InstanceBuilder
1558
1702
  }
1559
- export { abandon, Db, ErrGenerationMoved, ErrNewtypeMismatch }