@bjornpagen/bumbledb 0.12.2 → 0.15.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 (46) hide show
  1. package/COOKBOOK.md +35 -21
  2. package/README.md +82 -55
  3. package/dist/db.d.ts +173 -130
  4. package/dist/db.d.ts.map +1 -1
  5. package/dist/db.js +782 -371
  6. package/dist/db.js.map +1 -1
  7. package/dist/index.d.ts +7 -13
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +4 -9
  10. package/dist/index.js.map +1 -1
  11. package/dist/marshal.d.ts +5 -27
  12. package/dist/marshal.d.ts.map +1 -1
  13. package/dist/marshal.js +4 -21
  14. package/dist/marshal.js.map +1 -1
  15. package/dist/native.d.ts +157 -168
  16. package/dist/native.d.ts.map +1 -1
  17. package/dist/native.js +46 -8
  18. package/dist/native.js.map +1 -1
  19. package/dist/query/find.d.ts +14 -9
  20. package/dist/query/find.d.ts.map +1 -1
  21. package/dist/query/find.js +2 -2
  22. package/dist/query/find.js.map +1 -1
  23. package/dist/query/lower.d.ts.map +1 -1
  24. package/dist/query/lower.js +5 -4
  25. package/dist/query/lower.js.map +1 -1
  26. package/dist/query/parse-ir.d.ts.map +1 -1
  27. package/dist/query/parse-ir.js +11 -9
  28. package/dist/query/parse-ir.js.map +1 -1
  29. package/dist/relation.d.ts +4 -25
  30. package/dist/relation.d.ts.map +1 -1
  31. package/dist/relation.js +3 -4
  32. package/dist/relation.js.map +1 -1
  33. package/package.json +3 -3
  34. package/src/db.ts +1151 -500
  35. package/src/index.ts +28 -24
  36. package/src/marshal.ts +5 -35
  37. package/src/native.ts +248 -175
  38. package/src/query/find.ts +19 -14
  39. package/src/query/lower.ts +7 -6
  40. package/src/query/parse-ir.ts +11 -9
  41. package/src/relation.ts +3 -28
  42. package/dist/exhume.d.ts +0 -143
  43. package/dist/exhume.d.ts.map +0 -1
  44. package/dist/exhume.js +0 -166
  45. package/dist/exhume.js.map +0 -1
  46. 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,48 +22,44 @@
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 {
41
- factOf,
42
- handleOf,
43
- isFreshField,
44
- isInserted,
45
- type KeyFact,
46
- keyRowOf,
47
- type Minted,
48
- recordOf,
49
- rowOf
50
- } from "#marshal.ts"
37
+ import { cellOf, factOf, handleOf, isFreshField, type KeyFact, keyRowOf, recordOf, rowOf } from "#marshal.ts"
51
38
 
52
39
  import type {
40
+ AdmitResult,
41
+ BuilderHandle,
53
42
  DbHandle,
54
43
  FactValue,
44
+ InstanceHandle,
55
45
  Manifest,
46
+ NativeWriteOutcome,
47
+ OwnedHandle,
56
48
  PreparedHandle,
57
- SnapshotHandle,
58
49
  TxHandle,
50
+ WireFreshRange,
59
51
  Violation as WireViolation,
60
- ViolationFact as WireViolationFact
52
+ ViolationFact as WireViolationFact,
53
+ WireMutationReport,
54
+ WitnessHandle
61
55
  } from "#native.ts"
62
- import { bridged, native } from "#native.ts"
56
+ import { bridged, bridgedAsync, errorFromThrow, native } from "#native.ts"
63
57
  import type { FindColumn } from "#query/atom.ts"
64
58
  import type { Query } from "#query/lower.ts"
65
59
  import { lowerQuery } from "#query/lower.ts"
66
60
  import { decodeAnswers, wireParams } from "#query/run.ts"
67
61
  import type { ParamEntry, ParamsRecord } from "#query/scope.ts"
68
- import type { AnyRelation, Fact, InsertFact } from "#relation.ts"
62
+ import type { AnyRelation, Fact, FreshKeys } from "#relation.ts"
69
63
  import type { AnySchema, Schema, SchemaRelation, SchemaRelations } from "#schema.ts"
70
64
  import { isStatement, type KeyStatement, type Statement } from "#statements.ts"
71
65
 
@@ -76,6 +70,130 @@ import { isStatement, type KeyStatement, type Statement } from "#statements.ts"
76
70
  */
77
71
  type MemberRelation<Rels extends SchemaRelations> = Extract<Rels[keyof Rels], AnyRelation>
78
72
 
73
+ /**
74
+ * Facts consumed vs facts that changed the in-memory final-state view.
75
+ * The length-1 report is `{ submitted: 1n, changed: 0n | 1n }`.
76
+ */
77
+ interface MutationReport {
78
+ readonly submitted: bigint
79
+ readonly changed: bigint
80
+ }
81
+
82
+ /**
83
+ * Column-major collection write: one array per declared field, every
84
+ * column the same length. The second transport of `load` / `insert` —
85
+ * objects and columns are two ways to spell the same batch.
86
+ */
87
+ type ColumnBatch<R extends AnyRelation> = {
88
+ readonly [K in keyof Fact<R> & string]: readonly Fact<R>[K][]
89
+ }
90
+
91
+ type CollectionWrite<R extends AnyRelation> = Iterable<Fact<R>> | ColumnBatch<R>
92
+
93
+ function isColumnBatch(value: object): boolean {
94
+ return !(Symbol.iterator in value)
95
+ }
96
+
97
+ function rowsOf<R extends AnyRelation>(relation: R, facts: Iterable<Fact<R>>): FactValue[][] {
98
+ const rows: FactValue[][] = []
99
+ for (const fact of facts) {
100
+ rows.push(rowOf(relation.data, recordOf(fact)))
101
+ }
102
+ return rows
103
+ }
104
+
105
+ /**
106
+ * Lowers a column batch to per-field wire arrays in sealed field order.
107
+ * Allocates one array per field — never a JS array per row.
108
+ */
109
+ function columnsOf(relation: AnyRelation, batch: object): FactValue[][] {
110
+ const record = recordOf(batch)
111
+ let count: number | undefined
112
+ return relation.data.fields.map(function marshalColumn(declared) {
113
+ const raw = record[declared.name]
114
+ if (!Array.isArray(raw)) {
115
+ throw errors.new(`relation ${relation.name}: column ${declared.name} is not an array`)
116
+ }
117
+ if (count === undefined) {
118
+ count = raw.length
119
+ } else if (raw.length !== count) {
120
+ throw errors.new(
121
+ `relation ${relation.name}: column ${declared.name} has length ${raw.length}, expected ${count}`
122
+ )
123
+ }
124
+ return raw.map(function marshalCell(value: unknown) {
125
+ return cellOf(`relation ${relation.name} field ${declared.name}`, declared.field, value)
126
+ })
127
+ })
128
+ }
129
+
130
+ function mutateCollection<R extends AnyRelation>(
131
+ relation: R,
132
+ facts: CollectionWrite<R>,
133
+ applyRows: (rows: readonly FactValue[][]) => WireMutationReport,
134
+ applyColumns: (columns: readonly FactValue[][]) => WireMutationReport
135
+ ): MutationReport {
136
+ const report = isColumnBatch(facts)
137
+ ? applyColumns(columnsOf(relation, facts))
138
+ : applyRows(rowsOf(relation, facts as Iterable<Fact<R>>))
139
+ return Object.freeze({ submitted: report.submitted, changed: report.changed })
140
+ }
141
+
142
+ /**
143
+ * Half-open fresh-id range from one `reserve`. Empty cannot yield a
144
+ * minted id — `start` exists only on the nonempty arm.
145
+ */
146
+ type FreshRange =
147
+ | {
148
+ readonly empty: true
149
+ readonly count: 0n
150
+ at(index: bigint): undefined
151
+ [Symbol.iterator](): IterableIterator<bigint>
152
+ }
153
+ | {
154
+ readonly empty: false
155
+ readonly start: bigint
156
+ readonly endExclusive: bigint
157
+ readonly count: bigint
158
+ at(index: bigint): bigint | undefined
159
+ [Symbol.iterator](): IterableIterator<bigint>
160
+ }
161
+
162
+ function freshRangeOf(wire: WireFreshRange): FreshRange {
163
+ if (wire.empty) {
164
+ return Object.freeze({
165
+ empty: true,
166
+ count: 0n,
167
+ at(_index: bigint) {
168
+ return undefined
169
+ },
170
+ *[Symbol.iterator](): IterableIterator<bigint> {}
171
+ })
172
+ }
173
+ const start = wire.start
174
+ const endExclusive = wire.endExclusive
175
+ const count = endExclusive - start
176
+ return Object.freeze({
177
+ empty: false,
178
+ start,
179
+ endExclusive,
180
+ get count() {
181
+ return count
182
+ },
183
+ at(index: bigint) {
184
+ if (index < 0n || index >= count) {
185
+ return undefined
186
+ }
187
+ return start + index
188
+ },
189
+ *[Symbol.iterator](): IterableIterator<bigint> {
190
+ for (let id = start; id < endExclusive; id++) {
191
+ yield id
192
+ }
193
+ }
194
+ })
195
+ }
196
+
79
197
  /**
80
198
  * The key object of a key-statement-selected `get`: exactly the selected
81
199
  * `key()` statement's projection fields, each at the relation's own BARE
@@ -190,29 +308,40 @@ type Violation<Rels extends SchemaRelations> =
190
308
  * `never` and the arm vanishes from the sum. The outcome is in the type;
191
309
  * a dead arm is never handled.
192
310
  */
193
- type AbandonedArm<R> = R extends Abandon<infer P> ? { readonly ok: false; readonly abandoned: P } : never
311
+ type AbandonedArm<R> = R extends Abandon<infer P> ? { readonly tag: "abandoned"; readonly abandoned: P } : never
312
+
313
+ /** A callback return that is not Promise-like. TypeScript `never` is not a runtime boundary. */
314
+ type SyncResult<R> = R extends PromiseLike<unknown> ? never : R
315
+
316
+ interface Committed<T> {
317
+ readonly value: T
318
+ readonly generation: bigint
319
+ }
320
+
321
+ type Admission<Rels extends SchemaRelations, T> =
322
+ | { readonly tag: "accepted"; readonly value: T }
323
+ | { readonly tag: "rejected"; readonly violations: readonly Violation<Rels>[] }
194
324
 
195
325
  /**
196
- * A write's domain outcome, one sum for BOTH verbs (ruled 2026-07-23,
197
- * R10): the committed generation; the COMPLETE violation set (every
198
- * violated statement cited once, per direction for a containment, in
199
- * materialized statement order); or the callback's own abandon payload —
200
- * commit-vs-abandon is in the type, so a caller's explicit decline to
201
- * commit can never be silently discarded. Narrows on `.ok`, then (when the
202
- * callback can abandon) on `"violations" in result`.
326
+ * A write's domain outcome. One discriminant: narrow on `tag`. The
327
+ * abandoned arm is present exactly when the callback can abandon.
203
328
  */
204
- type WriteResult<Rels extends SchemaRelations, R = void> =
205
- | { readonly ok: true; readonly generation: bigint }
206
- | { readonly ok: false; readonly violations: readonly Violation<Rels>[] }
329
+ type WriteOutcome<Rels extends SchemaRelations, R> =
330
+ | { readonly tag: "accepted"; readonly value: Committed<Exclude<R, Abandon<unknown>>> }
331
+ | { readonly tag: "rejected"; readonly violations: readonly Violation<Rels>[] }
207
332
  | AbandonedArm<R>
208
333
 
334
+ type WriteFromOutcome<Rels extends SchemaRelations, R> =
335
+ | WriteOutcome<Rels, R>
336
+ | { readonly tag: "moved"; readonly witnessed: bigint; readonly current: bigint }
337
+
209
338
  /**
210
339
  * The delta-building callback of a write: runs synchronously against the
211
340
  * live transaction. Returning {@link abandon}`(payload)` rolls the
212
341
  * transaction back (R10) — the result type carries the payload arm exactly
213
342
  * then.
214
343
  */
215
- type DeltaBuild<Rels extends SchemaRelations, R = void> = (tx: Tx<Rels>) => R
344
+ type DeltaBuild<Rels extends SchemaRelations, R = void> = (tx: WriteTx<Rels>) => R
216
345
 
217
346
  /**
218
347
  * The runtime discriminant of {@link Abandon} values — a property probe is
@@ -225,7 +354,7 @@ const abandonMark: unique symbol = Symbol("bumbledb.abandon")
225
354
  * The abandon sentinel {@link abandon} builds: returning one from a `write`
226
355
  * or `writeFrom` callback rolls the transaction back WITHOUT
227
356
  * committing (no empty commit is ever issued) and surfaces the payload as
228
- * `{ ok: false, abandoned: payload }` (ruled 2026-07-23, R10 — the
357
+ * `{ tag: "abandoned", abandoned: payload }` (ruled 2026-07-23, R10 — the
229
358
  * sentinel's contract is unconditional, whichever write verb received it).
230
359
  */
231
360
  interface Abandon<P> {
@@ -237,7 +366,7 @@ interface Abandon<P> {
237
366
  * Wraps a payload in the {@link Abandon} sentinel — the one way a write
238
367
  * callback declines to commit: `return abandon(payload)` aborts the delta
239
368
  * (nothing is committed, not even an empty commit) and the write resolves
240
- * to `{ ok: false, abandoned: payload }`, from `write` and `writeFrom`
369
+ * to `{ tag: "abandoned", abandoned: payload }`, from `write` and `writeFrom`
241
370
  * alike (R10).
242
371
  */
243
372
  function abandon<P>(payload: P): Abandon<P> {
@@ -271,17 +400,17 @@ function isAbandon<R>(value: R): value is R & Abandon<AbandonedPayload<R>> {
271
400
  * cannot resolve over an open `R`.
272
401
  */
273
402
  function isAbandonedOutcome<Rels extends SchemaRelations, R>(
274
- outcome: { readonly ok: false; readonly abandoned: AbandonedPayload<R> },
403
+ outcome: { readonly tag: "abandoned"; readonly abandoned: AbandonedPayload<R> },
275
404
  sentinel: Abandon<AbandonedPayload<R>>
276
- ): outcome is { readonly ok: false; readonly abandoned: AbandonedPayload<R> } & WriteResult<Rels, R> {
405
+ ): outcome is { readonly tag: "abandoned"; readonly abandoned: AbandonedPayload<R> } & WriteOutcome<Rels, R> {
277
406
  return isAbandon(sentinel) && outcome.abandoned === sentinel.payload
278
407
  }
279
408
 
280
409
  /** Builds the abandoned write outcome from the callback's own sentinel (the R10 arm's one mint). */
281
410
  function abandonedOutcome<Rels extends SchemaRelations, R>(
282
411
  sentinel: Abandon<AbandonedPayload<R>>
283
- ): WriteResult<Rels, R> {
284
- const outcome = Object.freeze({ ok: false as const, abandoned: sentinel.payload })
412
+ ): WriteOutcome<Rels, R> {
413
+ const outcome = Object.freeze({ tag: "abandoned" as const, abandoned: sentinel.payload })
285
414
  if (!isAbandonedOutcome<Rels, R>(outcome, sentinel)) {
286
415
  throw errors.new("bumbledb abandon outcome construction incomplete")
287
416
  }
@@ -295,23 +424,24 @@ function abandonedOutcome<Rels extends SchemaRelations, R>(
295
424
  * Spent when its owning `write`/`writeFrom` call resolves the attempt;
296
425
  * any later use throws.
297
426
  */
298
- interface Tx<Rels extends SchemaRelations> {
427
+ interface WriteTx<Rels extends SchemaRelations> {
299
428
  /**
300
- * Records one insert. Omitted fresh fields are MINTED through the
301
- * engine's alloc lane and returned as bare bigints; supplying them instead
302
- * preserves identity (the resupply idiom). Returns `{ changed, ...fresh }`
303
- * (ruled 2026-07-23, R11): the engine's changed-state report the Rust
304
- * surface's `insert(&fact) -> bool` bijection `delete` always honored —
305
- * beside the relation's fresh cells, minted or resupplied. The
306
- * idempotent-replay lane reads the bit from the insert itself; no extra
307
- * `contains` round trip exists. The flattened shape cannot carry a FRESH
308
- * cell literally named `changed` beside the report, so admission refuses
309
- * that one spelling ({@link refuseShadowedChanged}) — never a silent
310
- * shadow here.
429
+ * Records a collection of inserts. Singleton is `[fact]`. Empty is
430
+ * lawful. Returns how many facts were consumed and how many changed
431
+ * the in-memory final-state view. Every fact is complete — omitted
432
+ * fresh cells are a type error; mint first with {@link WriteTx.reserve}.
311
433
  */
312
- insert<R extends MemberRelation<Rels>>(relation: R, fact: InsertFact<R>): { readonly changed: boolean } & Minted<R>
313
- /** Records one delete; `true` iff the final state changed. */
314
- delete<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean
434
+ insert<R extends MemberRelation<Rels>>(relation: R, facts: CollectionWrite<R>): MutationReport
435
+ /**
436
+ * Records a collection of deletes. Singleton is `[fact]`. Returns
437
+ * how many facts were consumed and how many changed the view.
438
+ */
439
+ delete<R extends MemberRelation<Rels>>(relation: R, facts: Iterable<Fact<R>>): MutationReport
440
+ /**
441
+ * Mints `count` consecutive fresh values for a `.fresh` field.
442
+ * `count === 0n` is empty and does not yield a start.
443
+ */
444
+ reserve<R extends MemberRelation<Rels>>(relation: R, field: FreshKeys<R> & string, count: bigint): FreshRange
315
445
  /** Final-state membership of one complete fact. */
316
446
  contains<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean
317
447
  /**
@@ -331,21 +461,27 @@ interface Tx<Rels extends SchemaRelations> {
331
461
  ): Fact<R> | undefined
332
462
  }
333
463
 
464
+ type Tx<Rels extends SchemaRelations> = WriteTx<Rels>
465
+
466
+ const witnessTypes: unique symbol = Symbol("bumbledb.witness.types")
467
+
334
468
  /**
335
- * The read view one `db.read(fn)` call scopes — or one `using snap =
336
- * db.read()` acquisition owns (ruled 2026-07-23, R12: lifetimes are
337
- * disposables, never `close()`). The callback form invalidates the value
338
- * when `fn` returns; the `using` form releases it at scope exit through
339
- * `Symbol.dispose` — either way the release is deterministic and
340
- * scope-shaped, every later verb call throws a typed used-after-scope
341
- * error, and the underlying snapshot (with its LMDB reader slot) is
342
- * already closed.
469
+ * Cloneable generation evidence from one store read. May cross `await`.
470
+ * Disposal is idempotent; later use throws {@link ErrSpentHandle}.
343
471
  */
344
- interface ReadScope<Rels extends SchemaRelations> extends Disposable {
472
+ interface Witness<Rels extends SchemaRelations> extends Disposable {
473
+ readonly [witnessTypes]?: Rels
474
+ }
475
+
476
+ /**
477
+ * The borrowed instance one `db.read((instance, witness) => …)` callback
478
+ * receives. Invalid the moment the callback returns. A stashed value
479
+ * throws {@link ErrUseAfterScope}. Not a handle: there is no `db.read()`.
480
+ */
481
+ interface ReadInstance<Rels extends SchemaRelations> {
345
482
  /**
346
- * The committed generation this scope witnessed — carried by the
347
- * snapshot open itself (one crossing, inside the snapshot's own
348
- * transaction), so it is atomic with the snapshot by construction.
483
+ * The committed generation this instance witnessed — read inside the
484
+ * lease's own transaction.
349
485
  */
350
486
  readonly generation: bigint
351
487
  /** Full-relation export in row-id order, decoded to bare structural facts. */
@@ -368,12 +504,13 @@ interface ReadScope<Rels extends SchemaRelations> extends Disposable {
368
504
  /** Committed-state membership of one complete fact. */
369
505
  contains<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean
370
506
  /**
371
- * Executes a prepared query against this scope's snapshot with the
372
- * typed params object; returns the answer SET as plain rows with
373
- * bare structural values (no order — the host sorts). This is the ONE
507
+ * Executes a prepared query against this instance with the typed
508
+ * params object; returns the answer SET as plain rows with bare
509
+ * structural values (no order — the host sorts). This is the ONE
374
510
  * execution spelling ({@link Prepared} carries no `execute`).
375
511
  */
376
512
  execute<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Row[]
513
+ prepare<Row, Params extends ParamsRecord>(q: Query<Rels, Row, Params>): Prepared<Rels, Row, Params>
377
514
  }
378
515
 
379
516
  /**
@@ -390,7 +527,7 @@ const preparedTypes: unique symbol = Symbol("bumbledb.prepared.types")
390
527
  * One prepared query as a plain VALUE: explicit visible compilation
391
528
  * (`db.prepare(q)` lowers, pins the plan, and surfaces every engine roster
392
529
  * refusal), no lifecycle. Execution happens ONLY through
393
- * `snap.execute(prepared, params)` / `db.execute(prepared, params)` — the
530
+ * `instance.execute(prepared, params)` / `db.execute(prepared, params)` — the
394
531
  * symmetry rule's one spelling. The engine-side plan is reclaimed by a GC
395
532
  * finalizer when this value becomes unreachable (reclamation only, never
396
533
  * correctness — an unreclaimed plan is idle memory, and process exit frees
@@ -411,53 +548,42 @@ interface Db<Rels extends SchemaRelations> {
411
548
  /** The theory this store was opened with (fingerprint-verified by the engine). */
412
549
  readonly schema: Schema<Rels>
413
550
  /**
414
- * One scoped snapshot read: opens an MVCC snapshot, runs `fn`
415
- * SYNCHRONOUSLY against it, and closes the snapshot unconditionally
416
- * before returning `fn`'s result. The {@link ReadScope} is invalidated
417
- * when `fn` returns a used-after-scope call throws a typed error.
418
- */
419
- read<T>(fn: (snap: ReadScope<Rels>) => T): T
420
- /**
421
- * The `using` acquisition (ruled 2026-07-23, R12): `using snap =
422
- * db.read()` — the caller owns the scope's lifetime, and the scope's
423
- * `Symbol.dispose` releases the snapshot deterministically at scope
424
- * exit, in the language's own syntax. Lifetimes are disposables, never
425
- * `close()`.
551
+ * One store read: runs `body` SYNCHRONOUSLY inside the engine lease
552
+ * and returns its result. The {@link ReadInstance} is invalidated
553
+ * when `body` returns a stashed use throws {@link ErrUseAfterScope}.
554
+ * The {@link Witness} is a clone and may escape. A thenable return
555
+ * throws {@link ErrAsyncCallback}.
426
556
  */
427
- read(): ReadScope<Rels>
428
- /** `db.scan(r)` === `db.read(snap => snap.scan(r))` — the symmetry rule. */
557
+ read<R>(body: (instance: ReadInstance<Rels>, witness: Witness<Rels>) => SyncResult<R>): SyncResult<R>
558
+ /** `db.scan(r)` === `db.read(instance => instance.scan(r))` — the symmetry rule. */
429
559
  scan<R extends MemberRelation<Rels>>(relation: R): Fact<R>[]
430
- /** `db.get(r, k)` === `db.read(snap => snap.get(r, k))` — the symmetry rule. */
560
+ /** `db.get(r, k)` === `db.read(instance => instance.get(r, k))` — the symmetry rule. */
431
561
  get<R extends MemberRelation<Rels>>(relation: R, key: KeyFact<R>): Fact<R> | undefined
432
- /** `db.get(r, s, k)` === `db.read(snap => snap.get(r, s, k))` — the symmetry rule, keyed form. */
562
+ /** `db.get(r, s, k)` === `db.read(instance => instance.get(r, s, k))` — the symmetry rule, keyed form. */
433
563
  get<R extends MemberRelation<Rels>, const P extends readonly string[]>(
434
564
  relation: R,
435
565
  keyStatement: KeyStatement<R, P>,
436
566
  key: DeclaredKeyFact<R, P>
437
567
  ): Fact<R> | undefined
438
- /** `db.contains(r, f)` === `db.read(snap => snap.contains(r, f))` — the symmetry rule. */
568
+ /** `db.contains(r, f)` === `db.read(instance => instance.contains(r, f))` — the symmetry rule. */
439
569
  contains<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean
440
- /** `db.execute(p, params)` === `db.read(snap => snap.execute(p, params))` — the symmetry rule. */
570
+ /** `db.execute(p, params)` === `db.read(instance => instance.execute(p, params))` — the symmetry rule. */
441
571
  execute<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Row[]
442
572
  /**
443
573
  * One delta transaction: builds the delta synchronously through `fn`,
444
574
  * commits, and returns the domain outcome. A throw from `fn` aborts
445
575
  * the delta (LMDB untouched) and rethrows wrapped. `fn` may decline to
446
- * commit by returning {@link abandon}`(payload)` (ruled 2026-07-23,
447
- * R10): the transaction rolls back — nothing is committed, not even an
448
- * empty commit — and the outcome is `{ ok: false, abandoned: payload }`,
449
- * an arm the result type carries exactly when the callback can abandon.
576
+ * commit by returning {@link abandon}`(payload)`: the transaction rolls
577
+ * back — nothing is committed, not even an empty commit — and the
578
+ * outcome is `{ tag: "abandoned", abandoned: payload }`.
450
579
  */
451
- write<R = void>(fn: DeltaBuild<Rels, R>): WriteResult<Rels, R>
580
+ write<R>(fn: (tx: WriteTx<Rels>) => SyncResult<R>): WriteOutcome<Rels, SyncResult<R>>
452
581
  /**
453
- * One-shot witnessed write from a live read snapshot: begins a
454
- * transaction that commits only if no state-changing commit landed
455
- * since `snap` was taken. Must run inside the read callback that owns
456
- * `snap`. A moved generation is the typed {@link ErrGenerationMoved}
457
- * — retry is host policy; this method never loops. `fn` may decline to
458
- * commit by returning {@link abandon}`(payload)`.
582
+ * Witnessed write: commits only if no state-changing commit landed
583
+ * since `witness` was minted. A moved generation is
584
+ * `{ tag: "moved" }` retry is host policy; this method never loops.
459
585
  */
460
- writeFrom<R>(snap: ReadScope<Rels>, fn: DeltaBuild<Rels, R>): WriteResult<Rels, R>
586
+ writeFrom<R>(witness: Witness<Rels>, fn: (tx: WriteTx<Rels>) => SyncResult<R>): WriteFromOutcome<Rels, SyncResult<R>>
461
587
  /**
462
588
  * Prepares a query value built against THIS schema (identity is the
463
589
  * membership rule): lowers it to the engine IR, pins the plan, and
@@ -724,6 +850,29 @@ function tablesOf(theory: AnySchema, manifest: Manifest): Tables {
724
850
  return Object.freeze({ relations, statements: Object.freeze(entries) })
725
851
  }
726
852
 
853
+ function tablesFromTheory(theory: AnySchema): Tables {
854
+ const entries = materializedEntries(theory)
855
+ const relations = new Map<string, RelationEntry>()
856
+ Object.keys(theory.relations).forEach(function byOrdinal(name, ordinal) {
857
+ const member = theory.relations[name]
858
+ if (member === undefined) {
859
+ throw errors.new(`bumbledb theory has no relation ${name}`)
860
+ }
861
+ const fieldIds = new Map<string, number>()
862
+ sealedFieldsOf(member).forEach(function byField(declared, fieldOrdinal) {
863
+ fieldIds.set(declared.name, fieldOrdinal)
864
+ })
865
+ let primaryKey: PrimaryKey | undefined
866
+ entries.forEach(function firstOwnedKey(entry, index) {
867
+ if (primaryKey === undefined && entry.kind === "functionality" && entry.owner === name) {
868
+ primaryKey = Object.freeze({ statementId: index, projection: entry.projection })
869
+ }
870
+ })
871
+ relations.set(name, Object.freeze({ id: ordinal, member, fieldIds, primaryKey }))
872
+ })
873
+ return Object.freeze({ relations, statements: Object.freeze(entries) })
874
+ }
875
+
727
876
  /** The point-read half a transaction and a read scope share, over their own handle. */
728
877
  interface PointReads {
729
878
  contains(relationId: number, row: readonly FactValue[]): boolean
@@ -731,25 +880,33 @@ interface PointReads {
731
880
  }
732
881
 
733
882
  /**
734
- * One read scope's PRIVATE lifetime record: its live snapshot handle, the
735
- * generation it witnessed (carried by the snapshot open itself one
736
- * crossing, finding 016), its liveness flag (flipped when the owning
737
- * `read` callback returns, or by the scope's own
738
- * `Symbol.dispose`), its close latch (`closed` — the snapshot closes
739
- * exactly once, whichever of the owner and the dispose gets there first),
740
- * and its owning store's identity token. Held in {@link scopeStates} —
741
- * the snapshot handle is never a public value.
883
+ * One borrowed instance's PRIVATE lifetime record. Held in
884
+ * {@link instanceStates} the native handle is never a public value.
742
885
  */
743
- interface ScopeState {
744
- readonly handle: SnapshotHandle
745
- readonly generation: bigint
886
+ interface InstanceState {
887
+ readonly handle: InstanceHandle
746
888
  live: boolean
747
- closed: boolean
748
889
  readonly owner: object
749
890
  }
750
891
 
751
- /** The private lifetime records of this module's read scopes. */
752
- const scopeStates = new WeakMap<object, ScopeState>()
892
+ const instanceStates = new WeakMap<object, InstanceState>()
893
+
894
+ interface WitnessState {
895
+ readonly handle: WitnessHandle
896
+ spent: boolean
897
+ readonly owner: object
898
+ }
899
+
900
+ const witnessStates = new WeakMap<object, WitnessState>()
901
+
902
+ const witnessReclaimer = new FinalizationRegistry<WitnessHandle>(function reclaimWitness(handle) {
903
+ const closed = errors.trySync(function closeWitness() {
904
+ native.witnessClose(handle)
905
+ })
906
+ if (closed.error) {
907
+ return
908
+ }
909
+ })
753
910
 
754
911
  /**
755
912
  * One prepared value's PRIVATE engine half: the pinned plan handle, the
@@ -782,51 +939,304 @@ const planReclaimer = new FinalizationRegistry<PreparedHandle>(function reclaimP
782
939
  }
783
940
  })
784
941
 
785
- /**
786
- * The typed generation-moved refusal `writeFrom` throws when a
787
- * state-changing commit landed since the witness snapshot: retry is host
788
- * policy. Match with `errors.is`.
789
- */
790
- const ErrGenerationMoved = errors.new(
791
- "bumbledb generationMoved: a state-changing commit landed since the witness snapshot"
942
+ const ErrAsyncCallback = errors.new(
943
+ "bumbledb asyncCallback: a read or write callback returned a thenable — the callback is synchronous"
944
+ )
945
+ const ErrSpentHandle = errors.new("bumbledb spentHandle: a consumed builder, instance, or witness was used")
946
+ const ErrUseAfterScope = errors.new(
947
+ "bumbledb useAfterScope: a stashed read instance or write transaction was used after its callback returned"
792
948
  )
949
+ const ErrForeignPrepared = errors.new("bumbledb foreignPrepared: a prepared query met a foreign instance")
950
+ const ErrForeignWitness = errors.new("bumbledb foreignWitness: a witness met a foreign store")
793
951
 
794
952
  /**
795
- * Fills one insert's omitted fresh cells through the engine's
796
- * alloc-then-insert dyn lane (there is no insert-with-omitted-fields wire
797
- * spelling) and collects every fresh cell minted or resupplied — for the
798
- * insert's return. Mutates `values` in place with the minted cells.
953
+ * The shared typed read surface: store leases and owned instances both
954
+ * expose scan/get/contains/execute/prepare. The native ops are the only
955
+ * difference one way to read, two handle kinds.
799
956
  */
800
- function mintFreshCells(
801
- txHandle: TxHandle,
802
- entry: RelationEntry,
803
- relation: AnyRelation,
804
- values: Record<string, unknown>
805
- ): Record<string, FactValue> {
806
- const fresh: Record<string, FactValue> = {}
807
- for (const declared of relation.data.fields) {
808
- if (!isFreshField(declared.field)) {
809
- continue
957
+ interface CatalogNative {
958
+ scan(relationId: number): FactValue[][]
959
+ contains(relationId: number, values: readonly FactValue[]): boolean
960
+ get(relationId: number, statementId: number, keyValues: readonly FactValue[]): FactValue[] | null
961
+ prepare(query: ReturnType<typeof lowerQuery>): ReturnType<typeof native.instancePrepare>
962
+ execute(prepared: PreparedHandle, params: ReturnType<typeof wireParams>): FactValue[][]
963
+ }
964
+
965
+ function catalogMethods<Rels extends SchemaRelations>(
966
+ theory: Schema<Rels>,
967
+ tables: Tables,
968
+ owner: object,
969
+ assertLive: () => void,
970
+ ops: CatalogNative
971
+ ): Pick<ReadInstance<Rels>, "scan" | "get" | "contains" | "execute" | "prepare"> {
972
+ function resolveOrdinary(relation: AnyRelation): RelationEntry {
973
+ const entry = tables.relations.get(relation.name)
974
+ if (entry === undefined || entry.member !== relation) {
975
+ throw errors.new(`relation ${relation.name} is not a member of schema ${theory.name}`)
810
976
  }
811
- let cell = values[declared.name]
812
- if (cell === undefined) {
813
- const fieldId = entry.fieldIds.get(declared.name)
814
- if (fieldId === undefined) {
815
- throw errors.new(`bumbledb manifest drift: relation ${relation.name} has no field id for ${declared.name}`)
977
+ if (isClosedMember(relation)) {
978
+ throw errors.new(
979
+ `relation ${relation.name} is closed — its extension is schema data (axioms), never scanned or written`
980
+ )
981
+ }
982
+ return entry
983
+ }
984
+ function declaredKeyOf(relation: AnyRelation, statement: Statement): PrimaryKey {
985
+ const statementId = tables.statements.findIndex(function byIdentity(candidate) {
986
+ return "statement" in candidate && candidate.statement === statement
987
+ })
988
+ const entry = tables.statements[statementId]
989
+ if (entry === undefined) {
990
+ throw errors.new(
991
+ `keyed get statement is not a declared statement of schema ${theory.name} — statement identity is the membership rule`
992
+ )
993
+ }
994
+ if (entry.kind !== "functionality") {
995
+ throw errors.new("keyed get takes a key() statement — containments and capacity statements key nothing")
996
+ }
997
+ if (entry.owner !== relation.name) {
998
+ throw errors.new(
999
+ `keyed get statement keys ${entry.owner}, not ${relation.name} — the statement must be a declared key of the relation it reads`
1000
+ )
1001
+ }
1002
+ return Object.freeze({ statementId, projection: entry.projection })
1003
+ }
1004
+ function planOf(prepared: object): PreparedPlan {
1005
+ const plan = preparedPlans.get(prepared)
1006
+ if (plan === undefined) {
1007
+ throw errors.wrap(ErrForeignPrepared, "bumbledb execute target is not a prepared value of this SDK")
1008
+ }
1009
+ if (plan.owner !== owner) {
1010
+ throw errors.wrap(
1011
+ ErrForeignPrepared,
1012
+ `bumbledb prepared value was prepared by a different store than this one (schema ${theory.name})`
1013
+ )
1014
+ }
1015
+ return plan
1016
+ }
1017
+ function contains<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean {
1018
+ assertLive()
1019
+ const entry = resolveOrdinary(relation)
1020
+ return bridged("bumbledb instance contains", function readContains() {
1021
+ return ops.contains(entry.id, rowOf(relation.data, recordOf(fact)))
1022
+ })
1023
+ }
1024
+ function get<R extends MemberRelation<Rels>, const P extends readonly string[]>(
1025
+ relation: R,
1026
+ keyOrStatement: KeyFact<R> | KeyStatement<R, P>,
1027
+ declaredKey?: DeclaredKeyFact<R, P>
1028
+ ): Fact<R> | undefined {
1029
+ assertLive()
1030
+ const entry = resolveOrdinary(relation)
1031
+ return selectKeyRead(
1032
+ keyOrStatement,
1033
+ declaredKey,
1034
+ function byStatement(statement, key) {
1035
+ const selected = declaredKeyOf(relation, statement)
1036
+ const row = bridged("bumbledb instance get", function readGet() {
1037
+ return ops.get(entry.id, selected.statementId, keyRowOf(relation.data, selected.projection, recordOf(key)))
1038
+ })
1039
+ return row === null ? undefined : factOf(relation, row)
1040
+ },
1041
+ function byPrimary(key) {
1042
+ const primaryKey = entry.primaryKey
1043
+ if (primaryKey === undefined) {
1044
+ throw errors.new(
1045
+ `relation ${relation.name} has no candidate key — keyed get requires a fresh field or a declared key statement`
1046
+ )
1047
+ }
1048
+ const row = bridged("bumbledb instance get", function readGet() {
1049
+ return ops.get(
1050
+ entry.id,
1051
+ primaryKey.statementId,
1052
+ keyRowOf(relation.data, primaryKey.projection, recordOf(key))
1053
+ )
1054
+ })
1055
+ return row === null ? undefined : factOf(relation, row)
816
1056
  }
817
- cell = bridged("bumbledb tx alloc", function mint() {
818
- return native.txAlloc(txHandle, entry.id, fieldId)
1057
+ )
1058
+ }
1059
+ function scan<R extends MemberRelation<Rels>>(relation: R): Fact<R>[] {
1060
+ assertLive()
1061
+ const entry = resolveOrdinary(relation)
1062
+ const rows = bridged("bumbledb instance scan", function readScan() {
1063
+ return ops.scan(entry.id)
1064
+ })
1065
+ return rows.map(function decodeRow(row) {
1066
+ return factOf(relation, row)
1067
+ })
1068
+ }
1069
+ function execute<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Row[] {
1070
+ assertLive()
1071
+ const plan = planOf(prepared)
1072
+ const wire = wireParams(plan.params, recordOf(params))
1073
+ const rows = bridged("execute bumbledb prepared query", function callExecute() {
1074
+ return ops.execute(plan.handle, wire)
1075
+ })
1076
+ return decodeAnswers<Row>(plan.finds, rows)
1077
+ }
1078
+ function prepare<Row, Params extends ParamsRecord>(q: Query<Rels, Row, Params>): Prepared<Rels, Row, Params> {
1079
+ assertLive()
1080
+ if (q.schema !== theory) {
1081
+ throw errors.new(
1082
+ `query was built against schema ${q.schema.name}, not the identical schema value this store opened with — schema identity is the membership rule`
1083
+ )
1084
+ }
1085
+ const queryIr = lowerQuery(q)
1086
+ const outcome = bridged("prepare bumbledb query", function callPrepare() {
1087
+ return ops.prepare(queryIr)
1088
+ })
1089
+ if (!outcome.ok) {
1090
+ throwPrepareRefusal(outcome.message)
1091
+ }
1092
+ const prepared: Prepared<Rels, Row, Params> = Object.freeze({})
1093
+ preparedPlans.set(
1094
+ prepared,
1095
+ Object.freeze({
1096
+ handle: outcome.prepared,
1097
+ owner,
1098
+ params: q.data.params,
1099
+ finds: q.data.finds
819
1100
  })
820
- values[declared.name] = cell
1101
+ )
1102
+ planReclaimer.register(prepared, outcome.prepared)
1103
+ return prepared
1104
+ }
1105
+ return { scan, get, contains, execute, prepare }
1106
+ }
1107
+
1108
+ function ordinaryEntry(tables: Tables, theory: AnySchema, relation: AnyRelation): RelationEntry {
1109
+ const entry = tables.relations.get(relation.name)
1110
+ if (entry === undefined || entry.member !== relation) {
1111
+ throw errors.new(`relation ${relation.name} is not a member of schema ${theory.name}`)
1112
+ }
1113
+ if (isClosedMember(relation)) {
1114
+ throw errors.new(
1115
+ `relation ${relation.name} is closed — its extension is schema data (axioms), never scanned or written`
1116
+ )
1117
+ }
1118
+ return entry
1119
+ }
1120
+
1121
+ function overlayMethods<Rels extends SchemaRelations>(
1122
+ theory: Schema<Rels>,
1123
+ tables: Tables,
1124
+ assertLive: () => void,
1125
+ reads: PointReads
1126
+ ): Pick<WriteTx<Rels>, "contains" | "get"> {
1127
+ function declaredKeyOf(relation: AnyRelation, statement: Statement): PrimaryKey {
1128
+ const statementId = tables.statements.findIndex(function byIdentity(candidate) {
1129
+ return "statement" in candidate && candidate.statement === statement
1130
+ })
1131
+ const entry = tables.statements[statementId]
1132
+ if (entry === undefined) {
1133
+ throw errors.new(
1134
+ `keyed get statement is not a declared statement of schema ${theory.name} — statement identity is the membership rule`
1135
+ )
1136
+ }
1137
+ if (entry.kind !== "functionality") {
1138
+ throw errors.new("keyed get takes a key() statement — containments and capacity statements key nothing")
821
1139
  }
822
- if (typeof cell !== "bigint") {
1140
+ if (entry.owner !== relation.name) {
823
1141
  throw errors.new(
824
- `relation ${relation.name} field ${declared.name}: a fresh cell is a u64 bigint, got ${typeof cell}`
1142
+ `keyed get statement keys ${entry.owner}, not ${relation.name} the statement must be a declared key of the relation it reads`
1143
+ )
1144
+ }
1145
+ return Object.freeze({ statementId, projection: entry.projection })
1146
+ }
1147
+ function contains<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean {
1148
+ assertLive()
1149
+ const entry = ordinaryEntry(tables, theory, relation)
1150
+ return reads.contains(entry.id, rowOf(relation.data, recordOf(fact)))
1151
+ }
1152
+ function readThroughKey<R extends MemberRelation<Rels>>(
1153
+ relation: R,
1154
+ entry: RelationEntry,
1155
+ selected: PrimaryKey,
1156
+ key: Readonly<Record<string, unknown>>
1157
+ ): Fact<R> | undefined {
1158
+ const row = reads.get(entry.id, selected.statementId, keyRowOf(relation.data, selected.projection, key))
1159
+ if (row === null) {
1160
+ return undefined
1161
+ }
1162
+ return factOf(relation, row)
1163
+ }
1164
+ function get<R extends MemberRelation<Rels>>(relation: R, key: KeyFact<R>): Fact<R> | undefined
1165
+ function get<R extends MemberRelation<Rels>, const P extends readonly string[]>(
1166
+ relation: R,
1167
+ keyStatement: KeyStatement<R, P>,
1168
+ key: DeclaredKeyFact<R, P>
1169
+ ): Fact<R> | undefined
1170
+ function get<R extends MemberRelation<Rels>, const P extends readonly string[]>(
1171
+ relation: R,
1172
+ keyOrStatement: KeyFact<R> | KeyStatement<R, P>,
1173
+ declaredKey?: DeclaredKeyFact<R, P>
1174
+ ): Fact<R> | undefined {
1175
+ assertLive()
1176
+ const entry = ordinaryEntry(tables, theory, relation)
1177
+ return selectKeyRead(
1178
+ keyOrStatement,
1179
+ declaredKey,
1180
+ function byStatement(statement, key) {
1181
+ return readThroughKey(relation, entry, declaredKeyOf(relation, statement), recordOf(key))
1182
+ },
1183
+ function byPrimary(key) {
1184
+ const primaryKey = entry.primaryKey
1185
+ if (primaryKey === undefined) {
1186
+ throw errors.new(
1187
+ `relation ${relation.name} has no candidate key — keyed get requires a fresh field or a declared key statement`
1188
+ )
1189
+ }
1190
+ return readThroughKey(relation, entry, primaryKey, recordOf(key))
1191
+ }
1192
+ )
1193
+ }
1194
+ return { contains, get }
1195
+ }
1196
+
1197
+ function createReadInstance<Rels extends SchemaRelations>(
1198
+ nativeHandle: InstanceHandle,
1199
+ theory: Schema<Rels>,
1200
+ tables: Tables,
1201
+ owner: object
1202
+ ): ReadInstance<Rels> {
1203
+ const state: InstanceState = { handle: nativeHandle, live: true, owner }
1204
+ function assertLive(): void {
1205
+ if (!state.live) {
1206
+ throw errors.wrap(
1207
+ ErrUseAfterScope,
1208
+ "bumbledb read instance is invalidated — its owning callback already returned"
825
1209
  )
826
1210
  }
827
- fresh[declared.name] = cell
828
1211
  }
829
- return fresh
1212
+ const methods = catalogMethods(theory, tables, owner, assertLive, {
1213
+ scan(relationId) {
1214
+ return native.instanceScan(state.handle, relationId)
1215
+ },
1216
+ contains(relationId, values) {
1217
+ return native.instanceContains(state.handle, relationId, values)
1218
+ },
1219
+ get(relationId, statementId, keyValues) {
1220
+ return native.instanceGet(state.handle, relationId, statementId, keyValues)
1221
+ },
1222
+ prepare(query) {
1223
+ return native.instancePrepare(state.handle, query)
1224
+ },
1225
+ execute(prepared, params) {
1226
+ return native.preparedExecute(prepared, state.handle, params)
1227
+ }
1228
+ })
1229
+ const instance: ReadInstance<Rels> = Object.freeze({
1230
+ get generation() {
1231
+ assertLive()
1232
+ return bridged("bumbledb instance generation", function readGeneration() {
1233
+ return native.instanceGeneration(state.handle)
1234
+ })
1235
+ },
1236
+ ...methods
1237
+ })
1238
+ instanceStates.set(instance, state)
1239
+ return instance
830
1240
  }
831
1241
 
832
1242
  /**
@@ -1004,152 +1414,70 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
1004
1414
  return { contains, get }
1005
1415
  }
1006
1416
 
1007
- /**
1008
- * Resolves a prepared value's private plan, refusing foreign objects
1009
- * and prepared values of other stores as typed errors.
1010
- */
1011
- function planOf(prepared: object): PreparedPlan {
1012
- const plan = preparedPlans.get(prepared)
1013
- if (plan === undefined) {
1014
- throw errors.new("bumbledb execute target is not a prepared value of this SDK")
1015
- }
1016
- if (plan.owner !== owner) {
1017
- throw errors.new(
1018
- `bumbledb prepared value was prepared by a different store than this one (schema ${theory.name})`
1019
- )
1020
- }
1021
- return plan
1417
+ function pinPrepared<Row, Params extends ParamsRecord>(
1418
+ preparedHandle: PreparedHandle,
1419
+ q: Query<Rels, Row, Params>
1420
+ ): Prepared<Rels, Row, Params> {
1421
+ const prepared: Prepared<Rels, Row, Params> = Object.freeze({})
1422
+ preparedPlans.set(
1423
+ prepared,
1424
+ Object.freeze({
1425
+ handle: preparedHandle,
1426
+ owner,
1427
+ params: q.data.params,
1428
+ finds: q.data.finds
1429
+ })
1430
+ )
1431
+ planReclaimer.register(prepared, preparedHandle)
1432
+ return prepared
1022
1433
  }
1023
1434
 
1024
- /**
1025
- * Builds one {@link ReadScope} over a live scope state. Every verb
1026
- * asserts liveness first: the owning call flips `state.live` the moment
1027
- * its callback returns (or the scope's own `Symbol.dispose` does, for a
1028
- * `using`-acquired scope), so a leaked scope is a typed refusal forever
1029
- * after.
1030
- */
1031
- function makeScope(state: ScopeState): ReadScope<Rels> {
1032
- function assertLive(): void {
1033
- if (!state.live) {
1034
- throw errors.new("bumbledb read scope is invalidated — its owning read callback already returned")
1035
- }
1036
- }
1037
- const reads = pointReadsOf(assertLive, {
1038
- contains(relationId, row) {
1039
- return bridged("bumbledb snapshot contains", function readContains() {
1040
- return native.snapshotContains(state.handle, relationId, row)
1041
- })
1042
- },
1043
- get(relationId, statementId, key) {
1044
- return bridged("bumbledb snapshot get", function readGet() {
1045
- return native.snapshotGet(state.handle, relationId, statementId, key)
1435
+ function makeWitness(nativeHandle: WitnessHandle): Witness<Rels> {
1436
+ const state: WitnessState = { handle: nativeHandle, spent: false, owner }
1437
+ const witness: Witness<Rels> = Object.freeze({
1438
+ [Symbol.dispose](): void {
1439
+ if (state.spent) {
1440
+ return
1441
+ }
1442
+ state.spent = true
1443
+ bridged("close bumbledb witness", function closeWitness() {
1444
+ native.witnessClose(nativeHandle)
1046
1445
  })
1047
1446
  }
1048
1447
  })
1049
- function scan<R extends MemberRelation<Rels>>(relation: R): Fact<R>[] {
1050
- assertLive()
1051
- const entry = resolveOrdinary(relation)
1052
- const rows = bridged("bumbledb snapshot scan", function readScan() {
1053
- return native.snapshotScan(state.handle, entry.id)
1054
- })
1055
- return rows.map(function decodeRow(row) {
1056
- return factOf(relation, row)
1057
- })
1058
- }
1059
- function execute<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Row[] {
1060
- assertLive()
1061
- const plan = planOf(prepared)
1062
- const wire = wireParams(plan.params, recordOf(params))
1063
- const rows = bridged("execute bumbledb prepared query", function callExecute() {
1064
- return native.preparedExecute(plan.handle, state.handle, wire)
1065
- })
1066
- return decodeAnswers<Row>(plan.finds, rows)
1067
- }
1068
- /** The R12 teardown: invalidate, then close — idempotent through the state's close latch. */
1069
- function dispose(): void {
1070
- state.live = false
1071
- closeScopeState(state)
1072
- }
1073
- const scope: ReadScope<Rels> = Object.freeze({
1074
- generation: state.generation,
1075
- scan,
1076
- get: reads.get,
1077
- contains: reads.contains,
1078
- execute,
1079
- [Symbol.dispose]: dispose
1080
- })
1081
- scopeStates.set(scope, state)
1082
- return scope
1448
+ witnessStates.set(witness, state)
1449
+ witnessReclaimer.register(witness, nativeHandle)
1450
+ return witness
1083
1451
  }
1084
1452
 
1085
- /**
1086
- * Live-handle accounting (diagnostic law, prod EINVAL 2026-07-17): every
1087
- * snapshot open/close is counted so a write-begin failure can report how
1088
- * many read handles were live at the fault — a leaked scope is invisible
1089
- * until the exact moment it matters, so the failure carries the census.
1090
- */
1091
- let liveSnapshots = 0
1092
-
1093
- /**
1094
- * Opens one snapshot and its scope state (live until the owner flips
1095
- * it). The witnessed generation rides the snapshot open itself — one
1096
- * crossing carries both (finding 016), so the fault-pairing close
1097
- * branch a second `dbGeneration` call needed is structurally gone.
1098
- */
1099
- function openScopeState(): ScopeState {
1100
- const opened = bridged("open bumbledb snapshot", function openSnapshot() {
1101
- return native.dbSnapshot(handle)
1102
- })
1103
- liveSnapshots += 1
1104
- return { handle: opened.snapshot, generation: opened.generation, live: true, closed: false, owner }
1453
+ function makeInstance(nativeHandle: InstanceHandle): ReadInstance<Rels> {
1454
+ return createReadInstance(nativeHandle, theory, tables, owner)
1105
1455
  }
1106
1456
 
1107
- /**
1108
- * Closes a scope's snapshot after the owner invalidated it — a LATCH:
1109
- * the snapshot closes exactly once, whichever of the owning call and
1110
- * the scope's own `Symbol.dispose` gets there first, so an early
1111
- * in-callback disposal never double-closes (and never double-counts
1112
- * the census).
1113
- */
1114
- function closeScopeState(state: ScopeState): void {
1115
- if (state.closed) {
1116
- return
1117
- }
1118
- state.closed = true
1119
- bridged("close bumbledb snapshot", function closeSnapshot() {
1120
- native.snapshotClose(state.handle)
1121
- })
1122
- liveSnapshots -= 1
1123
- }
1124
-
1125
- function read<T>(fn: (snap: ReadScope<Rels>) => T): T
1126
- function read(): ReadScope<Rels>
1127
- function read<T>(fn?: (snap: ReadScope<Rels>) => T): T | ReadScope<Rels> {
1128
- const state = openScopeState()
1129
- const scope = makeScope(state)
1130
- if (fn === undefined) {
1131
- /**
1132
- * The `using` acquisition (R12): the caller owns the lifetime —
1133
- * `using snap = db.read()` — and the scope's `Symbol.dispose`
1134
- * is the deterministic release, scope-shaped in the language's
1135
- * own syntax.
1136
- */
1137
- return scope
1138
- }
1139
- const result = errors.trySync(function runRead() {
1140
- return fn(scope)
1457
+ function read<R>(body: (instance: ReadInstance<Rels>, witness: Witness<Rels>) => SyncResult<R>): SyncResult<R> {
1458
+ let captured: R | undefined
1459
+ const result = bridged("bumbledb read", function runRead() {
1460
+ return native.dbRead(handle, function onRead(nativeInstance, nativeWitness) {
1461
+ const instance = makeInstance(nativeInstance)
1462
+ const witness = makeWitness(nativeWitness)
1463
+ const value = body(instance, witness)
1464
+ const state = instanceStates.get(instance)
1465
+ if (state !== undefined) {
1466
+ state.live = false
1467
+ }
1468
+ if (isThenable(value)) {
1469
+ throw errors.wrap(ErrAsyncCallback, "bumbledb read callback returned a thenable")
1470
+ }
1471
+ captured = value
1472
+ return value
1473
+ })
1141
1474
  })
1142
- state.live = false
1143
- closeScopeState(state)
1144
- if (result.error) {
1145
- throw errors.wrap(result.error, "bumbledb read")
1146
- }
1147
- return result.data
1475
+ return (captured ?? result) as SyncResult<R>
1148
1476
  }
1149
1477
 
1150
1478
  function scan<R extends MemberRelation<Rels>>(relation: R): Fact<R>[] {
1151
- return read(function scanInScope(snap) {
1152
- return snap.scan(relation)
1479
+ return read(function scanInScope(instance) {
1480
+ return instance.scan(relation)
1153
1481
  })
1154
1482
  }
1155
1483
 
@@ -1164,41 +1492,39 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
1164
1492
  keyOrStatement: KeyFact<R> | KeyStatement<R, P>,
1165
1493
  declaredKey?: DeclaredKeyFact<R, P>
1166
1494
  ): Fact<R> | undefined {
1167
- return read(function getInScope(snap) {
1168
- return selectKeyRead(
1495
+ let found: Fact<R> | undefined
1496
+ read(function getInScope(instance) {
1497
+ found = selectKeyRead(
1169
1498
  keyOrStatement,
1170
1499
  declaredKey,
1171
1500
  function byStatement(statement, key) {
1172
- return snap.get(relation, statement, key)
1501
+ return instance.get(relation, statement, key)
1173
1502
  },
1174
1503
  function byPrimary(key) {
1175
- return snap.get(relation, key)
1504
+ return instance.get(relation, key)
1176
1505
  }
1177
1506
  )
1178
1507
  })
1508
+ return found
1179
1509
  }
1180
1510
 
1181
1511
  function contains<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean {
1182
- return read(function containsInScope(snap) {
1183
- return snap.contains(relation, fact)
1512
+ return read(function containsInScope(instance) {
1513
+ return instance.contains(relation, fact)
1184
1514
  })
1185
1515
  }
1186
1516
 
1187
1517
  function execute<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Row[] {
1188
- return read(function executeInScope(snap) {
1189
- return snap.execute(prepared, params)
1518
+ return read(function executeInScope(instance) {
1519
+ return instance.execute(prepared, params)
1190
1520
  })
1191
1521
  }
1192
1522
 
1193
- /**
1194
- * Builds one {@link Tx} over a transaction-handle thunk: `write` and
1195
- * `writeFrom` pass an already-begun handle.
1196
- */
1197
- function makeTx(resolveTx: () => TxHandle): { readonly tx: Tx<Rels>; spend(): void } {
1523
+ function makeTx(resolveTx: () => TxHandle): { readonly tx: WriteTx<Rels>; spend(): void } {
1198
1524
  const txState = { spent: false }
1199
1525
  function assertLive(): void {
1200
1526
  if (txState.spent) {
1201
- throw errors.new("bumbledb write transaction is spent")
1527
+ throw errors.wrap(ErrUseAfterScope, "bumbledb write transaction is spent")
1202
1528
  }
1203
1529
  }
1204
1530
  const reads = pointReadsOf(assertLive, {
@@ -1215,38 +1541,61 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
1215
1541
  })
1216
1542
  }
1217
1543
  })
1218
- function insert<R extends MemberRelation<Rels>>(
1219
- relation: R,
1220
- fact: InsertFact<R>
1221
- ): { readonly changed: boolean } & Minted<R> {
1544
+ function insert<R extends MemberRelation<Rels>>(relation: R, facts: CollectionWrite<R>): MutationReport {
1222
1545
  assertLive()
1223
1546
  const entry = resolveOrdinary(relation)
1224
1547
  const txHandle = resolveTx()
1225
- /** The one spread copy of the write path: `mintFreshCells` writes minted cells in place, and they must never land in the caller's own fact object. */
1226
- const values: Record<string, unknown> = { ...recordOf(fact) }
1227
- const fresh = mintFreshCells(txHandle, entry, relation, values)
1228
- const row = rowOf(relation.data, values)
1229
- const changed = bridged("bumbledb tx insert", function record() {
1230
- return native.txInsert(txHandle, entry.id, row)
1548
+ return mutateCollection(
1549
+ relation,
1550
+ facts,
1551
+ function applyRows(rows) {
1552
+ return bridged("bumbledb tx insert", function record() {
1553
+ return native.txInsert(txHandle, entry.id, rows)
1554
+ })
1555
+ },
1556
+ function applyColumns(columns) {
1557
+ return bridged("bumbledb tx insert", function recordColumns() {
1558
+ return native.txInsertColumns(txHandle, entry.id, columns)
1559
+ })
1560
+ }
1561
+ )
1562
+ }
1563
+ function remove<R extends MemberRelation<Rels>>(relation: R, facts: Iterable<Fact<R>>): MutationReport {
1564
+ assertLive()
1565
+ const entry = resolveOrdinary(relation)
1566
+ const txHandle = resolveTx()
1567
+ const report = bridged("bumbledb tx delete", function record() {
1568
+ return native.txDelete(txHandle, entry.id, rowsOf(relation, facts))
1231
1569
  })
1232
- const inserted: Readonly<Record<string, FactValue | boolean>> = Object.freeze({ changed, ...fresh })
1233
- if (!isInserted(relation, inserted)) {
1234
- throw errors.new(`relation ${relation.name}: insert return record is incomplete`)
1235
- }
1236
- return inserted
1570
+ return Object.freeze({ submitted: report.submitted, changed: report.changed })
1237
1571
  }
1238
- function remove<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean {
1572
+ function reserve<R extends MemberRelation<Rels>>(
1573
+ relation: R,
1574
+ field: FreshKeys<R> & string,
1575
+ count: bigint
1576
+ ): FreshRange {
1239
1577
  assertLive()
1240
1578
  const entry = resolveOrdinary(relation)
1579
+ const declared = relation.data.fields.find(function byName(candidate) {
1580
+ return candidate.name === field
1581
+ })
1582
+ if (declared === undefined || !isFreshField(declared.field)) {
1583
+ throw errors.new(`relation ${relation.name}: field ${field} is not a fresh cell`)
1584
+ }
1585
+ const fieldId = entry.fieldIds.get(field)
1586
+ if (fieldId === undefined) {
1587
+ throw errors.new(`bumbledb manifest drift: relation ${relation.name} has no field id for ${field}`)
1588
+ }
1241
1589
  const txHandle = resolveTx()
1242
- const row = rowOf(relation.data, recordOf(fact))
1243
- return bridged("bumbledb tx delete", function record() {
1244
- return native.txDelete(txHandle, entry.id, row)
1590
+ const range = bridged("bumbledb tx reserve", function mint() {
1591
+ return native.txReserve(txHandle, entry.id, fieldId, count)
1245
1592
  })
1593
+ return freshRangeOf(range)
1246
1594
  }
1247
- const tx: Tx<Rels> = Object.freeze({
1595
+ const tx: WriteTx<Rels> = Object.freeze({
1248
1596
  insert,
1249
1597
  delete: remove,
1598
+ reserve,
1250
1599
  contains: reads.contains,
1251
1600
  get: reads.get
1252
1601
  })
@@ -1256,111 +1605,92 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
1256
1605
  return { tx, spend }
1257
1606
  }
1258
1607
 
1259
- function runDelta<R>(txHandle: TxHandle, fn: DeltaBuild<Rels, R>): WriteResult<Rels, R> {
1260
- const made = makeTx(function resolveTx() {
1261
- return txHandle
1262
- })
1263
- const built = errors.trySync(function buildDelta() {
1264
- return fn(made.tx)
1265
- })
1266
- made.spend()
1267
- if (built.error) {
1268
- bridged("abort bumbledb write transaction", function abort() {
1269
- native.txAbort(txHandle)
1270
- })
1271
- throw errors.wrap(built.error, "build write delta")
1272
- }
1273
- if (isThenable(built.data)) {
1274
- /**
1275
- * An `async` callback TYPECHECKS (Promise<void> is assignable where
1276
- * a `void` return is expected) but its body runs after the tx is
1277
- * spent: committing here would be a silent EMPTY commit reported
1278
- * ok while the callback's real inserts throw "spent" as unhandled
1279
- * rejections. Refused typed instead — abort, nothing committed
1280
- * (the same one-writer law as the thrown-callback path).
1281
- */
1282
- bridged("abort bumbledb write transaction", function abort() {
1283
- native.txAbort(txHandle)
1608
+ function mapNativeWrite<R>(nativeOutcome: NativeWriteOutcome, built: R | undefined): WriteFromOutcome<Rels, R> {
1609
+ if (nativeOutcome.tag === "moved") {
1610
+ return Object.freeze({
1611
+ tag: "moved" as const,
1612
+ witnessed: nativeOutcome.witnessed,
1613
+ current: nativeOutcome.current
1284
1614
  })
1285
- throw errors.new(
1286
- "bumbledb write callback returned a thenable — the delta build is synchronous; an async callback is refused, nothing was committed"
1287
- )
1288
1615
  }
1289
- if (isAbandon(built.data)) {
1290
- /**
1291
- * The caller's explicit decline to commit (R10): the sentinel's
1292
- * contract is unconditional — roll back, nothing committed, not
1293
- * even an empty commit; commit is unreachable for a sentinel
1294
- * result.
1295
- */
1296
- bridged("abort bumbledb write transaction", function abort() {
1297
- native.txAbort(txHandle)
1616
+ if (nativeOutcome.tag === "rejected") {
1617
+ return Object.freeze({
1618
+ tag: "rejected" as const,
1619
+ violations: Object.freeze(nativeOutcome.violations.map(violationOf))
1298
1620
  })
1299
- return abandonedOutcome<Rels, R>(built.data)
1300
1621
  }
1301
- const committed = errors.trySync(function commitDelta() {
1302
- return bridged("commit bumbledb write transaction", function commit() {
1303
- return native.txCommit(txHandle)
1304
- })
1305
- })
1306
- if (committed.error) {
1307
- /**
1308
- * A THROWN commit (engine I/O failure, bridge fault) must never
1309
- * leave the write transaction live: LMDB holds one writer per
1310
- * environment, and a leaked handle turns every later begin into
1311
- * EINVAL for the process's lifetime. The abort is best-effort —
1312
- * the native side may already have consumed the handle.
1313
- */
1314
- const aborted = errors.trySync(function abortAfterFailedCommit() {
1315
- native.txAbort(txHandle)
1316
- })
1317
- if (aborted.error) {
1622
+ if (nativeOutcome.tag === "abandoned") {
1623
+ if (built === undefined || !isAbandon(built)) {
1624
+ throw errors.new("bumbledb write abandoned without an abandon sentinel")
1318
1625
  }
1319
- throw errors.wrap(committed.error, "commit bumbledb write transaction")
1320
- }
1321
- const outcome = committed.data
1322
- if (outcome.ok) {
1323
- return Object.freeze({ ok: true, generation: outcome.generation })
1626
+ return abandonedOutcome<Rels, R>(built)
1324
1627
  }
1325
1628
  return Object.freeze({
1326
- ok: false,
1327
- violations: Object.freeze(outcome.violations.map(violationOf))
1629
+ tag: "accepted" as const,
1630
+ value: Object.freeze({
1631
+ value: built as Exclude<R, Abandon<unknown>>,
1632
+ generation: nativeOutcome.generation
1633
+ })
1328
1634
  })
1329
1635
  }
1330
1636
 
1331
- function write<R = void>(fn: DeltaBuild<Rels, R>): WriteResult<Rels, R> {
1332
- const txHandle = bridged(`begin bumbledb write transaction (live snapshots: ${liveSnapshots})`, function begin() {
1333
- return native.dbWriteBegin(handle)
1637
+ function runWrite<R>(
1638
+ invoke: (callback: (tx: TxHandle) => boolean) => NativeWriteOutcome,
1639
+ fn: (tx: WriteTx<Rels>) => SyncResult<R>
1640
+ ): WriteFromOutcome<Rels, SyncResult<R>> {
1641
+ let built: SyncResult<R> | undefined
1642
+ const nativeOutcome = bridged("bumbledb write", function callWrite() {
1643
+ return invoke(function onWrite(txHandle) {
1644
+ const made = makeTx(function resolveTx() {
1645
+ return txHandle
1646
+ })
1647
+ const result = errors.trySync(function buildDelta() {
1648
+ return fn(made.tx)
1649
+ })
1650
+ made.spend()
1651
+ if (result.error) {
1652
+ throw errors.wrap(result.error, "build write delta")
1653
+ }
1654
+ if (isThenable(result.data)) {
1655
+ throw errors.wrap(ErrAsyncCallback, "bumbledb write callback returned a thenable")
1656
+ }
1657
+ built = result.data
1658
+ return !isAbandon(result.data)
1659
+ })
1334
1660
  })
1335
- return runDelta(txHandle, fn)
1661
+ return mapNativeWrite(nativeOutcome, built)
1336
1662
  }
1337
1663
 
1338
- /**
1339
- * One-shot write from a live snapshot this store owns. Begins immediately;
1340
- * a moved generation throws {@link ErrGenerationMoved} and `fn` never
1341
- * runs. The snapshot stays owned by the caller's read callback.
1342
- */
1343
- function writeFrom<R>(snap: ReadScope<Rels>, fn: DeltaBuild<Rels, R>): WriteResult<Rels, R> {
1344
- const snapState = scopeStates.get(snap)
1345
- if (snapState === undefined) {
1346
- throw errors.new("bumbledb writeFrom witness is not a read scope of this SDK")
1347
- }
1348
- if (snapState.owner !== owner) {
1349
- throw errors.new(`bumbledb writeFrom snapshot belongs to a different store (schema ${theory.name})`)
1664
+ function write<R>(fn: (tx: WriteTx<Rels>) => SyncResult<R>): WriteOutcome<Rels, SyncResult<R>> {
1665
+ const outcome = runWrite(function invoke(callback) {
1666
+ return native.dbWrite(handle, callback)
1667
+ }, fn)
1668
+ if (outcome.tag === "moved") {
1669
+ throw errors.new("bumbledb write reported moved unconditional writes cannot move")
1350
1670
  }
1351
- if (!snapState.live) {
1352
- throw errors.new("bumbledb writeFrom snapshot is invalidated — its owning read callback already returned")
1671
+ return outcome
1672
+ }
1673
+
1674
+ function writeFrom<R>(
1675
+ witness: Witness<Rels>,
1676
+ fn: (tx: WriteTx<Rels>) => SyncResult<R>
1677
+ ): WriteFromOutcome<Rels, SyncResult<R>> {
1678
+ const state = witnessStates.get(witness)
1679
+ if (state === undefined) {
1680
+ throw errors.wrap(ErrForeignWitness, "bumbledb writeFrom witness is not a witness of this SDK")
1353
1681
  }
1354
- const witnessed = bridged("begin witnessed bumbledb write transaction", function begin() {
1355
- return native.dbWriteFrom(handle, snapState.handle)
1356
- })
1357
- if (!witnessed.ok) {
1682
+ if (state.owner !== owner) {
1358
1683
  throw errors.wrap(
1359
- ErrGenerationMoved,
1360
- `writeFrom: generation moved (witnessed ${witnessed.witnessed} current ${witnessed.current}) against schema ${theory.name}`
1684
+ ErrForeignWitness,
1685
+ `bumbledb writeFrom witness belongs to a different store (schema ${theory.name})`
1361
1686
  )
1362
1687
  }
1363
- return runDelta(witnessed.tx, fn)
1688
+ if (state.spent) {
1689
+ throw errors.wrap(ErrSpentHandle, "bumbledb writeFrom witness has been disposed")
1690
+ }
1691
+ return runWrite(function invoke(callback) {
1692
+ return native.dbWriteFrom(handle, state.handle, callback)
1693
+ }, fn)
1364
1694
  }
1365
1695
 
1366
1696
  function prepare<Row, Params extends ParamsRecord>(q: Query<Rels, Row, Params>): Prepared<Rels, Row, Params> {
@@ -1374,21 +1704,9 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
1374
1704
  return native.dbPrepare(handle, queryIr)
1375
1705
  })
1376
1706
  if (!outcome.ok) {
1377
- throw errors.new(`bumbledb ${outcome.kind} (prepare): ${outcome.message}`)
1707
+ throwPrepareRefusal(outcome.message)
1378
1708
  }
1379
- const preparedHandle = outcome.prepared
1380
- const prepared: Prepared<Rels, Row, Params> = Object.freeze({})
1381
- preparedPlans.set(
1382
- prepared,
1383
- Object.freeze({
1384
- handle: preparedHandle,
1385
- owner,
1386
- params: q.data.params,
1387
- finds: q.data.finds
1388
- })
1389
- )
1390
- planReclaimer.register(prepared, preparedHandle)
1391
- return prepared
1709
+ return pinPrepared(outcome.prepared, q)
1392
1710
  }
1393
1711
 
1394
1712
  return Object.freeze({
@@ -1417,124 +1735,457 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
1417
1735
  const ErrNewtypeMismatch = errors.new(
1418
1736
  "bumbledb newtypeMismatch: a statement pairs faces whose newtypes disagree — the faces of a dependency agree on their newtype, or neither carries one"
1419
1737
  )
1738
+ const ErrSchemaError = errors.new("bumbledb schemaError: the declaration failed validation")
1739
+ const ErrFingerprintMismatch = errors.new(
1740
+ "bumbledb fingerprintMismatch: the store's schema does not match this theory"
1741
+ )
1742
+ const ErrIrError = errors.new("bumbledb irError: the query failed validation")
1743
+
1744
+ function throwOpenRefusal(
1745
+ verb: string,
1746
+ canonical: string,
1747
+ kind: "schemaError" | "newtypeMismatch" | "fingerprintMismatch",
1748
+ message: string
1749
+ ): never {
1750
+ const detail = `${verb} ${canonical}: ${message}`
1751
+ if (kind === "newtypeMismatch") {
1752
+ throw errors.wrap(ErrNewtypeMismatch, detail)
1753
+ }
1754
+ if (kind === "schemaError") {
1755
+ throw errors.wrap(ErrSchemaError, detail)
1756
+ }
1757
+ throw errors.wrap(ErrFingerprintMismatch, detail)
1758
+ }
1420
1759
 
1421
- /**
1422
- * `Tx.insert` returns the flattened `{ changed, ...fresh }` record (R11),
1423
- * where the spread wins: a FRESH field literally named `changed` would
1424
- * shadow the engine's changed-state report on every insert of its
1425
- * relation. No field name is reserved SILENTLY — the one unspeakable
1426
- * spelling is refused here at admission, before any store is touched.
1427
- * Supplied (non-fresh) fields named `changed` never enter the return
1428
- * record and stay legal.
1429
- */
1430
- function refuseShadowedChanged(theory: AnySchema): void {
1431
- for (const [name, member] of Object.entries(theory.relations)) {
1432
- for (const declared of sealedFieldsOf(member)) {
1433
- if (declared.name === "changed" && isFreshField(declared.field)) {
1434
- throw errors.new(
1435
- `relation ${name}: a fresh field named "changed" would shadow tx.insert's changed-state report in its { changed, ...fresh } return (R11) — rename the fresh field; a supplied field named "changed" stays legal (only fresh cells ride the return)`
1436
- )
1437
- }
1760
+ function throwPrepareRefusal(message: string): never {
1761
+ throw errors.wrap(ErrIrError, `prepare: ${message}`)
1762
+ }
1763
+
1764
+ function openFromHandle<Rels extends SchemaRelations>(dbHandle: DbHandle, theory: Schema<Rels>): Db<Rels> {
1765
+ const manifest = bridged("fetch bumbledb manifest", function fetchManifest() {
1766
+ return native.dbManifest(dbHandle)
1767
+ })
1768
+ return openDb(dbHandle, theory, manifest)
1769
+ }
1770
+
1771
+ async function createStore<Rels extends SchemaRelations>(
1772
+ storePath: string,
1773
+ theory: Schema<Rels>
1774
+ ): Promise<Admission<Rels, Db<Rels>>> {
1775
+ const canonical = path.resolve(storePath)
1776
+ const spec = lower(theory)
1777
+ const created = await bridgedAsync(`create bumbledb store at ${canonical}`, function callBridge() {
1778
+ return native.dbCreate(canonical, spec)
1779
+ })
1780
+ if (created.tag === "schemaError" || created.tag === "newtypeMismatch") {
1781
+ throwOpenRefusal("create", canonical, created.tag, created.message)
1782
+ }
1783
+ if (created.tag === "rejected") {
1784
+ return Object.freeze({
1785
+ tag: "rejected" as const,
1786
+ violations: Object.freeze(
1787
+ created.violations.map(function mapWire(wire) {
1788
+ return mapViolationWithoutStore<Rels>(theory, wire)
1789
+ })
1790
+ )
1791
+ })
1792
+ }
1793
+ return Object.freeze({ tag: "accepted" as const, value: openFromHandle(created.db, theory) })
1794
+ }
1795
+
1796
+ function mapViolationWithoutStore<Rels extends SchemaRelations>(
1797
+ theory: Schema<Rels>,
1798
+ wire: WireViolation
1799
+ ): Violation<Rels> {
1800
+ const entries = materializedEntries(theory)
1801
+ const entry = entries[wire.statementId]
1802
+ if (entry === undefined) {
1803
+ throw errors.new(`bumbledb violation cites unknown statement id ${wire.statementId}`)
1804
+ }
1805
+ function offending(fact: WireViolationFact): OffendingFact<Rels> {
1806
+ const member = theory.relations[fact.relation]
1807
+ if (member === undefined || !(fact.relation in theory.relations)) {
1808
+ throw errors.new(`bumbledb violation cites unknown relation ${fact.relation}`)
1809
+ }
1810
+ const declared = sealedFieldsOf(member)
1811
+ const decoded: Record<string, FactValue> = {}
1812
+ for (const cell of fact.fields) {
1813
+ const cited = declared.find(function byName(candidate) {
1814
+ return candidate.name === cell.name
1815
+ })
1816
+ const roster = rosterOf(cited?.field)
1817
+ decoded[cell.name] =
1818
+ roster !== undefined
1819
+ ? handleOf(`violation fact ${fact.relation} field ${cell.name}`, roster, cell.value)
1820
+ : cell.value
1821
+ }
1822
+ return Object.freeze({ relation: fact.relation as keyof Rels & string, fact: Object.freeze(decoded) })
1823
+ }
1824
+ const facts = Object.freeze(wire.facts.map(offending))
1825
+ const canonical = wire.canonical
1826
+ if (entry.kind === "functionality") {
1827
+ if (!("statement" in entry)) {
1828
+ return Object.freeze({ kind: "functionality", statement: undefined, canonical, facts })
1438
1829
  }
1830
+ return Object.freeze({ kind: "functionality", statement: entry.statement, canonical, facts })
1831
+ }
1832
+ if (entry.kind === "capacity") {
1833
+ if (wire.kind !== "capacity") {
1834
+ throw errors.new(`bumbledb violation ${wire.statementId} is a capacity slot without a measure`)
1835
+ }
1836
+ return Object.freeze({
1837
+ kind: "capacity",
1838
+ statement: entry.statement,
1839
+ canonical,
1840
+ measure: wire.measure,
1841
+ facts
1842
+ })
1843
+ }
1844
+ if (wire.kind !== "containment") {
1845
+ throw errors.new(`bumbledb violation ${wire.statementId} is a containment slot without a direction`)
1846
+ }
1847
+ if (entry.kind === "mirrors") {
1848
+ return Object.freeze({
1849
+ kind: "containment",
1850
+ statement: entry.statement,
1851
+ canonical,
1852
+ direction: wire.direction,
1853
+ orientation: entry.orientation,
1854
+ facts
1855
+ })
1439
1856
  }
1857
+ return Object.freeze({
1858
+ kind: "containment",
1859
+ statement: entry.statement,
1860
+ canonical,
1861
+ direction: wire.direction,
1862
+ facts
1863
+ })
1440
1864
  }
1441
1865
 
1442
- /**
1443
- * The one admission path both verbs share: lower the theory, run one
1444
- * bridge call, and wrap the domain refusals — `schemaError` (spec
1445
- * resolution + schema validation, every issue in one message),
1446
- * `newtypeMismatch` (the coherence wall, {@link ErrNewtypeMismatch}),
1447
- * and `fingerprintMismatch` (a different theory cannot open the store)
1448
- * — into typed errors carrying the engine's message intact.
1449
- * Environment failures (a second live writer on the same path, IO)
1450
- * throw from the bridge; the engine's `EnvironmentLocked` message is
1451
- * "another live handle holds this environment's lock".
1452
- */
1453
- function admit<Rels extends SchemaRelations>(
1454
- verb: "create" | "open",
1866
+ async function openStore<Rels extends SchemaRelations>(
1455
1867
  storePath: string,
1456
1868
  theory: Schema<Rels>
1457
- ): Db<Rels> {
1458
- refuseShadowedChanged(theory)
1869
+ ): Promise<Db<Rels>> {
1459
1870
  const canonical = path.resolve(storePath)
1460
1871
  const spec = lower(theory)
1461
- const opened = bridged(`${verb} bumbledb store at ${canonical}`, function callBridge() {
1462
- if (verb === "create") {
1463
- return native.dbCreate(canonical, spec)
1464
- }
1872
+ const opened = await bridgedAsync(`open bumbledb store at ${canonical}`, function callBridge() {
1465
1873
  return native.dbOpen(canonical, spec)
1466
1874
  })
1467
1875
  if (!opened.ok) {
1468
- if (opened.kind === "newtypeMismatch") {
1469
- throw errors.wrap(ErrNewtypeMismatch, `${verb} ${canonical}: ${opened.message}`)
1876
+ throwOpenRefusal("open", canonical, opened.kind, opened.message)
1877
+ }
1878
+ return openFromHandle(opened.db, theory)
1879
+ }
1880
+
1881
+ interface OwnedInstance<Rels extends SchemaRelations> extends Disposable {
1882
+ prepare<Row, Params extends ParamsRecord>(q: Query<Rels, Row, Params>): Prepared<Rels, Row, Params>
1883
+ execute<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Row[]
1884
+ scan<R extends MemberRelation<Rels>>(relation: R): Fact<R>[]
1885
+ contains<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean
1886
+ get<R extends MemberRelation<Rels>>(relation: R, key: KeyFact<R>): Fact<R> | undefined
1887
+ get<R extends MemberRelation<Rels>, const P extends readonly string[]>(
1888
+ relation: R,
1889
+ keyStatement: KeyStatement<R, P>,
1890
+ key: DeclaredKeyFact<R, P>
1891
+ ): Fact<R> | undefined
1892
+ }
1893
+
1894
+ interface InstanceBuilder<Rels extends SchemaRelations> extends Disposable {
1895
+ load<R extends MemberRelation<Rels>>(relation: R, facts: CollectionWrite<R>): MutationReport
1896
+ delete<R extends MemberRelation<Rels>>(relation: R, facts: Iterable<Fact<R>>): MutationReport
1897
+ reserve<R extends MemberRelation<Rels>>(relation: R, field: FreshKeys<R> & string, count: bigint): FreshRange
1898
+ contains<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean
1899
+ get<R extends MemberRelation<Rels>>(relation: R, key: KeyFact<R>): Fact<R> | undefined
1900
+ get<R extends MemberRelation<Rels>, const P extends readonly string[]>(
1901
+ relation: R,
1902
+ keyStatement: KeyStatement<R, P>,
1903
+ key: DeclaredKeyFact<R, P>
1904
+ ): Fact<R> | undefined
1905
+ admit(): Promise<Admission<Rels, OwnedInstance<Rels>>>
1906
+ }
1907
+
1908
+ const ownedRecords = new WeakMap<object, { handle: OwnedHandle; theory: AnySchema; spent: boolean; owner: object }>()
1909
+ const builderRecords = new WeakMap<object, { handle: BuilderHandle; theory: AnySchema; spent: boolean }>()
1910
+
1911
+ const ownedReclaimer = new FinalizationRegistry<OwnedHandle>(function reclaimOwned(handle) {
1912
+ const closed = errors.trySync(function closeOwned() {
1913
+ native.ownedInstanceClose(handle)
1914
+ })
1915
+ if (closed.error) {
1916
+ return
1917
+ }
1918
+ })
1919
+
1920
+ const builderReclaimer = new FinalizationRegistry<BuilderHandle>(function reclaimBuilder(handle) {
1921
+ const closed = errors.trySync(function closeBuilder() {
1922
+ native.instanceBuilderClose(handle)
1923
+ })
1924
+ if (closed.error) {
1925
+ return
1926
+ }
1927
+ })
1928
+
1929
+ function wrapOwned<Rels extends SchemaRelations>(nativeHandle: OwnedHandle, theory: Schema<Rels>): OwnedInstance<Rels> {
1930
+ const owner = Object.freeze({})
1931
+ const rec = { handle: nativeHandle, theory, spent: false, owner }
1932
+ const tables = tablesFromTheory(theory)
1933
+ function assertLive(): void {
1934
+ if (rec.spent) {
1935
+ throw errors.wrap(ErrSpentHandle, "bumbledb owned instance has been disposed")
1470
1936
  }
1471
- throw errors.new(`bumbledb ${opened.kind} (${verb} ${canonical}): ${opened.message}`)
1472
1937
  }
1473
- const manifest = bridged("fetch bumbledb manifest", function fetchManifest() {
1474
- return native.dbManifest(opened.db)
1938
+ const methods = catalogMethods(theory, tables, owner, assertLive, {
1939
+ scan(relationId) {
1940
+ return native.ownedScan(nativeHandle, relationId)
1941
+ },
1942
+ contains(relationId, values) {
1943
+ return native.ownedContains(nativeHandle, relationId, values)
1944
+ },
1945
+ get(relationId, statementId, keyValues) {
1946
+ return native.ownedGet(nativeHandle, relationId, statementId, keyValues)
1947
+ },
1948
+ prepare(query) {
1949
+ return native.ownedPrepare(nativeHandle, query)
1950
+ },
1951
+ execute(prepared, params) {
1952
+ return native.ownedExecute(prepared, nativeHandle, params)
1953
+ }
1954
+ })
1955
+ const instance: OwnedInstance<Rels> = Object.freeze({
1956
+ ...methods,
1957
+ [Symbol.dispose](): void {
1958
+ if (rec.spent) {
1959
+ return
1960
+ }
1961
+ try {
1962
+ native.ownedInstanceClose(nativeHandle)
1963
+ } catch (caught) {
1964
+ const error = errorFromThrow(caught)
1965
+ if (/leased for publish/.test(error.message)) {
1966
+ throw errors.wrap(ErrSpentHandle, "bumbledb owned instance is leased for publish")
1967
+ }
1968
+ throw errors.wrap(error, "close bumbledb owned instance")
1969
+ }
1970
+ rec.spent = true
1971
+ ownedReclaimer.unregister(instance)
1972
+ }
1973
+ })
1974
+ ownedRecords.set(instance, rec)
1975
+ ownedReclaimer.register(instance, nativeHandle, instance)
1976
+ return instance
1977
+ }
1978
+
1979
+ function wrapBuilder<Rels extends SchemaRelations>(
1980
+ nativeHandle: BuilderHandle,
1981
+ theory: Schema<Rels>
1982
+ ): InstanceBuilder<Rels> {
1983
+ const rec = { handle: nativeHandle, theory, spent: false }
1984
+ const tables = tablesFromTheory(theory)
1985
+ function assertLive(): void {
1986
+ if (rec.spent) {
1987
+ throw errors.wrap(ErrSpentHandle, "bumbledb instance builder has been spent")
1988
+ }
1989
+ }
1990
+ const overlay = overlayMethods(theory, tables, assertLive, {
1991
+ contains(relationId, row) {
1992
+ return bridged("bumbledb builder contains", function readContains() {
1993
+ return native.instanceBuilderContains(nativeHandle, relationId, row)
1994
+ })
1995
+ },
1996
+ get(relationId, statementId, key) {
1997
+ return bridged("bumbledb builder get", function readGet() {
1998
+ return native.instanceBuilderGet(nativeHandle, relationId, statementId, key)
1999
+ })
2000
+ }
2001
+ })
2002
+ const builder: InstanceBuilder<Rels> = Object.freeze({
2003
+ load<R extends MemberRelation<Rels>>(relation: R, facts: CollectionWrite<R>): MutationReport {
2004
+ assertLive()
2005
+ const entry = ordinaryEntry(tables, theory, relation)
2006
+ return mutateCollection(
2007
+ relation,
2008
+ facts,
2009
+ function applyRows(rows) {
2010
+ return bridged("bumbledb builder load", function loadRows() {
2011
+ return native.instanceBuilderLoad(nativeHandle, entry.id, rows)
2012
+ })
2013
+ },
2014
+ function applyColumns(columns) {
2015
+ return bridged("bumbledb builder load", function loadColumns() {
2016
+ return native.instanceBuilderLoadColumns(nativeHandle, entry.id, columns)
2017
+ })
2018
+ }
2019
+ )
2020
+ },
2021
+ delete<R extends MemberRelation<Rels>>(relation: R, facts: Iterable<Fact<R>>): MutationReport {
2022
+ assertLive()
2023
+ const entry = ordinaryEntry(tables, theory, relation)
2024
+ const report = bridged("bumbledb builder delete", function remove() {
2025
+ return native.instanceBuilderDelete(nativeHandle, entry.id, rowsOf(relation, facts))
2026
+ })
2027
+ return Object.freeze({ submitted: report.submitted, changed: report.changed })
2028
+ },
2029
+ reserve<R extends MemberRelation<Rels>>(
2030
+ relation: R,
2031
+ field: FreshKeys<R> & string,
2032
+ count: bigint
2033
+ ): FreshRange {
2034
+ assertLive()
2035
+ const entry = ordinaryEntry(tables, theory, relation)
2036
+ const declared = relation.data.fields.find(function byName(candidate) {
2037
+ return candidate.name === field
2038
+ })
2039
+ if (declared === undefined || !isFreshField(declared.field)) {
2040
+ throw errors.new(`relation ${relation.name}: field ${field} is not a fresh cell`)
2041
+ }
2042
+ const fieldId = entry.fieldIds.get(field)
2043
+ if (fieldId === undefined) {
2044
+ throw errors.new(`bumbledb manifest drift: relation ${relation.name} has no field id for ${field}`)
2045
+ }
2046
+ const range = bridged("bumbledb builder reserve", function mint() {
2047
+ return native.instanceBuilderReserve(nativeHandle, entry.id, fieldId, count)
2048
+ })
2049
+ return freshRangeOf(range)
2050
+ },
2051
+ contains: overlay.contains,
2052
+ get: overlay.get,
2053
+ async admit(): Promise<Admission<Rels, OwnedInstance<Rels>>> {
2054
+ if (rec.spent) {
2055
+ throw errors.wrap(ErrSpentHandle, "bumbledb instance builder has been spent")
2056
+ }
2057
+ rec.spent = true
2058
+ builderReclaimer.unregister(builder)
2059
+ let outcome: AdmitResult
2060
+ try {
2061
+ outcome = await native.instanceBuilderAdmit(nativeHandle)
2062
+ } catch (caught) {
2063
+ throw errors.wrap(errorFromThrow(caught), "admit bumbledb instance")
2064
+ }
2065
+ if (outcome.tag === "rejected") {
2066
+ return Object.freeze({
2067
+ tag: "rejected" as const,
2068
+ violations: Object.freeze(
2069
+ outcome.violations.map(function mapWire(wire) {
2070
+ return mapViolationWithoutStore<Rels>(theory, wire)
2071
+ })
2072
+ )
2073
+ })
2074
+ }
2075
+ return Object.freeze({
2076
+ tag: "accepted" as const,
2077
+ value: wrapOwned(outcome.value, theory)
2078
+ })
2079
+ },
2080
+ [Symbol.dispose](): void {
2081
+ if (rec.spent) {
2082
+ return
2083
+ }
2084
+ rec.spent = true
2085
+ builderReclaimer.unregister(builder)
2086
+ bridged("close bumbledb instance builder", function closeBuilder() {
2087
+ native.instanceBuilderClose(nativeHandle)
2088
+ })
2089
+ }
1475
2090
  })
1476
- return openDb(opened.db, theory, manifest)
2091
+ builderRecords.set(builder, rec)
2092
+ builderReclaimer.register(builder, nativeHandle, builder)
2093
+ return builder
1477
2094
  }
1478
2095
 
2096
+ const InstanceBuilder = Object.freeze({
2097
+ create<Rels extends SchemaRelations>(theory: Schema<Rels>): InstanceBuilder<Rels> {
2098
+ const spec = lower(theory)
2099
+ const handle = bridged("create bumbledb instance builder", function make() {
2100
+ return native.instanceBuilderNew(spec)
2101
+ })
2102
+ return wrapBuilder(handle, theory)
2103
+ }
2104
+ })
2105
+
1479
2106
  /**
1480
2107
  * The store lifecycle — `Db.create(path, schema)` / `Db.open(path, schema)`.
1481
2108
  * Create refuses an already-initialized directory; open verifies format
1482
- * version, store kind, and the schema fingerprint. A second live handle
1483
- * on the same path is the engine's `EnvironmentLocked`. There is no close
1484
- * anywhere: the process owns the environment until GC/exit (durability is
1485
- * the engine's per-commit fsync). One store kind exists: durable
1486
- * resume = reopen in a fresh process, or hold the `Db` this process opened.
2109
+ * version and the schema fingerprint. A second live handle on the same
2110
+ * path is the engine's `EnvironmentLocked`. There is no close anywhere:
2111
+ * the process owns the environment until GC/exit (durability is the
2112
+ * engine's per-commit fsync). Resume = reopen in a fresh process, or
2113
+ * hold the `Db` this process opened.
1487
2114
  */
1488
2115
  const Db = Object.freeze({
1489
2116
  /** Creates a fresh durable store at `path` from the schema. */
1490
- async create<Rels extends SchemaRelations>(path: string, theory: Schema<Rels>): Promise<Db<Rels>> {
1491
- return admit("create", path, theory)
2117
+ async create<Rels extends SchemaRelations>(
2118
+ storePath: string,
2119
+ theory: Schema<Rels>
2120
+ ): Promise<Admission<Rels, Db<Rels>>> {
2121
+ return createStore(storePath, theory)
1492
2122
  },
1493
2123
  /**
1494
2124
  * Opens an existing durable store at `path` with the same theory.
1495
- * A fingerprint-matching open also BACK-FILLS the store's persisted
1496
- * schema descriptor when it is absent (self-describing stores, engine
1497
- * 50-storage.md § the `_meta` block), so a legacy store becomes
1498
- * exhumable after one ordinary open — adoption is automatic, never a
1499
- * separate verb. A second open of a still-live path is
1500
- * `EnvironmentLocked`.
2125
+ * Format 8 open never back-fills a descriptor. A second open of a
2126
+ * still-live path is `EnvironmentLocked`.
1501
2127
  */
1502
- async open<Rels extends SchemaRelations>(path: string, theory: Schema<Rels>): Promise<Db<Rels>> {
1503
- return admit("open", path, theory)
2128
+ async open<Rels extends SchemaRelations>(storePath: string, theory: Schema<Rels>): Promise<Db<Rels>> {
2129
+ return openStore(storePath, theory)
1504
2130
  },
1505
- /**
1506
- * Opens a store READ-ONLY from its own persisted descriptor — the SDK's
1507
- * one schema-independent read path (no theory, no fingerprint check; the
1508
- * store rebirth tool's entry). Lives beside `open`/`create` so the path
1509
- * law stays in one place: the same `node:path.resolve` canonicalization,
1510
- * applied here. The value is NOT cached and is a DISPOSABLE lifetime
1511
- * (R12) `using exhumed = await Db.exhume(path)` releases the engine
1512
- * handle and the store's exclusive lock at scope exit, so a same-path
1513
- * reopen never waits on GC. A store not yet adopted rejects with the
1514
- * typed `ErrExhumeNoDescriptor` (the remedy: one fingerprint-matching
1515
- * `Db.open` under the creating schema back-fills the descriptor).
1516
- */
1517
- async exhume(storePath: string): Promise<Exhumed> {
1518
- return exhumeStore(path.resolve(storePath))
2131
+ async fromInstance<Rels extends SchemaRelations>(
2132
+ storePath: string,
2133
+ instance: OwnedInstance<Rels>
2134
+ ): Promise<Db<Rels>> {
2135
+ const rec = ownedRecords.get(instance)
2136
+ if (rec === undefined) {
2137
+ throw errors.wrap(ErrSpentHandle, "bumbledb fromInstance target is not an owned instance of this SDK")
2138
+ }
2139
+ if (rec.spent) {
2140
+ throw errors.wrap(ErrSpentHandle, "bumbledb fromInstance target has been disposed")
2141
+ }
2142
+ const canonical = path.resolve(storePath)
2143
+ const dbHandle = await bridgedAsync(`publish bumbledb instance at ${canonical}`, function publish() {
2144
+ return native.dbFromInstance(canonical, rec.handle)
2145
+ })
2146
+ return openFromHandle(dbHandle, rec.theory as Schema<Rels>)
1519
2147
  }
1520
2148
  })
1521
2149
 
1522
2150
  export type {
1523
2151
  Abandon,
1524
2152
  AbandonedArm,
2153
+ Admission,
1525
2154
  CapacityViolation,
2155
+ ColumnBatch,
2156
+ Committed,
1526
2157
  ContainmentViolation,
1527
2158
  DeclaredKeyFact,
1528
2159
  DeclaredKeyViolation,
1529
2160
  DeltaBuild,
2161
+ FreshRange,
1530
2162
  ImpliedKeyViolation,
1531
2163
  MemberRelation,
1532
2164
  MirrorViolation,
2165
+ MutationReport,
1533
2166
  OffendingFact,
2167
+ OwnedInstance,
1534
2168
  Prepared,
1535
- ReadScope,
2169
+ ReadInstance,
2170
+ SyncResult,
1536
2171
  Tx,
1537
2172
  Violation,
1538
- WriteResult
2173
+ Witness,
2174
+ WriteFromOutcome,
2175
+ WriteOutcome,
2176
+ WriteTx
2177
+ }
2178
+ export {
2179
+ abandon,
2180
+ Db,
2181
+ ErrAsyncCallback,
2182
+ ErrForeignPrepared,
2183
+ ErrForeignWitness,
2184
+ ErrFingerprintMismatch,
2185
+ ErrIrError,
2186
+ ErrNewtypeMismatch,
2187
+ ErrSchemaError,
2188
+ ErrSpentHandle,
2189
+ ErrUseAfterScope,
2190
+ InstanceBuilder
1539
2191
  }
1540
- export { abandon, Db, ErrGenerationMoved, ErrNewtypeMismatch }