@bjornpagen/bumbledb 0.14.0 → 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.
package/dist/db.js CHANGED
@@ -2,18 +2,16 @@
2
2
  * `Db` — the living half of the SDK (PRD-07): open/create a store from a
3
3
  * `Schema`, write typed facts through delta transactions with race-free
4
4
  * final-state point reads, receive rejections as typed violation VALUES
5
- * keyed to statements, and read through scoped snapshots — all typed by
6
- * the schema's relations record.
5
+ * keyed to statements, and read through a synchronous instance callback —
6
+ * all typed by the schema's relations record.
7
7
  *
8
- * LIFETIMES ARE DISPOSABLES, never `close()` (ruled 2026-07-23, R12): no
9
- * value this module returns carries a close spelling — release is
10
- * deterministic and scope-shaped in the language's own syntax. Snapshots
11
- * are scope-shaped both ways: `read(fn)` opens one before `fn` and closes
12
- * it after unconditionally (the {@link ReadScope} handed to `fn` is
13
- * invalidated the moment `fn` returns), and `using snap = db.read()` hands
14
- * the caller the lifetime, released by the scope's own `Symbol.dispose`
15
- * at scope exit. Prepared plans are plain values whose engine-side half
16
- * is reclaimed by a GC finalizer — reclamation only, never correctness.
8
+ * A store read is one callback: `db.read((instance, witness) => …)`. The
9
+ * instance is invalid the moment the callback returns; the witness is a
10
+ * cloneable token and may escape. There is no handle-shaped read and no
11
+ * `using snap = db.read()`. Builder, owned instance, and witness
12
+ * implement `Symbol.dispose`. Prepared plans are plain values whose
13
+ * engine-side half is reclaimed by a GC finalizer — reclamation only,
14
+ * never correctness.
17
15
  *
18
16
  * PROCESS MODEL: one process, one exclusive-lock handle per store. The
19
17
  * `Db` value owns the LMDB environment's exclusive lock until process
@@ -24,22 +22,61 @@
24
22
  * policy.
25
23
  *
26
24
  * REJECTION IS DATA: a rejected commit is a domain outcome (it becomes the
27
- * LLM repair prompt downstream), returned as a {@link WriteResult} carrying
28
- * {@link Violation} values. Genuine failures — I/O, used-after-scope,
29
- * marshal shape, a moved generation on {@link Db.writeFrom} — throw
30
- * `@superbuilders/errors` wrapped errors instead.
25
+ * LLM repair prompt downstream), returned as a {@link WriteOutcome}
26
+ * carrying {@link Violation} values. A moved generation on
27
+ * {@link Db.writeFrom} is the `{ tag: "moved" }` arm, not an exception.
28
+ * Genuine failures — I/O, used-after-scope, spent handle, marshal shape —
29
+ * throw `@superbuilders/errors` wrapped errors instead.
31
30
  */
32
31
  import * as path from "node:path";
33
32
  import * as errors from "@superbuilders/errors";
34
33
  import { isClosedMember, sealedFieldsOf } from "#closed.ts";
35
- import { exhumeStore } from "#exhume.ts";
36
34
  import { rosterOf } from "#fields.ts";
37
35
  import { lower } from "#lower.ts";
38
- import { factOf, handleOf, isFreshField, keyRowOf, recordOf, rowOf } from "#marshal.ts";
39
- import { bridged, native } from "#native.ts";
36
+ import { cellOf, factOf, handleOf, isFreshField, keyRowOf, recordOf, rowOf } from "#marshal.ts";
37
+ import { bridged, bridgedAsync, errorFromThrow, native } from "#native.ts";
40
38
  import { lowerQuery } from "#query/lower.ts";
41
39
  import { decodeAnswers, wireParams } from "#query/run.ts";
42
40
  import { isStatement } from "#statements.ts";
41
+ function isColumnBatch(value) {
42
+ return !(Symbol.iterator in value);
43
+ }
44
+ function rowsOf(relation, facts) {
45
+ const rows = [];
46
+ for (const fact of facts) {
47
+ rows.push(rowOf(relation.data, recordOf(fact)));
48
+ }
49
+ return rows;
50
+ }
51
+ /**
52
+ * Lowers a column batch to per-field wire arrays in sealed field order.
53
+ * Allocates one array per field — never a JS array per row.
54
+ */
55
+ function columnsOf(relation, batch) {
56
+ const record = recordOf(batch);
57
+ let count;
58
+ return relation.data.fields.map(function marshalColumn(declared) {
59
+ const raw = record[declared.name];
60
+ if (!Array.isArray(raw)) {
61
+ throw errors.new(`relation ${relation.name}: column ${declared.name} is not an array`);
62
+ }
63
+ if (count === undefined) {
64
+ count = raw.length;
65
+ }
66
+ else if (raw.length !== count) {
67
+ throw errors.new(`relation ${relation.name}: column ${declared.name} has length ${raw.length}, expected ${count}`);
68
+ }
69
+ return raw.map(function marshalCell(value) {
70
+ return cellOf(`relation ${relation.name} field ${declared.name}`, declared.field, value);
71
+ });
72
+ });
73
+ }
74
+ function mutateCollection(relation, facts, applyRows, applyColumns) {
75
+ const report = isColumnBatch(facts)
76
+ ? applyColumns(columnsOf(relation, facts))
77
+ : applyRows(rowsOf(relation, facts));
78
+ return Object.freeze({ submitted: report.submitted, changed: report.changed });
79
+ }
43
80
  function freshRangeOf(wire) {
44
81
  if (wire.empty) {
45
82
  return Object.freeze({
@@ -84,7 +121,7 @@ const abandonMark = Symbol("bumbledb.abandon");
84
121
  * Wraps a payload in the {@link Abandon} sentinel — the one way a write
85
122
  * callback declines to commit: `return abandon(payload)` aborts the delta
86
123
  * (nothing is committed, not even an empty commit) and the write resolves
87
- * to `{ ok: false, abandoned: payload }`, from `write` and `writeFrom`
124
+ * to `{ tag: "abandoned", abandoned: payload }`, from `write` and `writeFrom`
88
125
  * alike (R10).
89
126
  */
90
127
  function abandon(payload) {
@@ -112,12 +149,13 @@ function isAbandonedOutcome(outcome, sentinel) {
112
149
  }
113
150
  /** Builds the abandoned write outcome from the callback's own sentinel (the R10 arm's one mint). */
114
151
  function abandonedOutcome(sentinel) {
115
- const outcome = Object.freeze({ ok: false, abandoned: sentinel.payload });
152
+ const outcome = Object.freeze({ tag: "abandoned", abandoned: sentinel.payload });
116
153
  if (!isAbandonedOutcome(outcome, sentinel)) {
117
154
  throw errors.new("bumbledb abandon outcome construction incomplete");
118
155
  }
119
156
  return outcome;
120
157
  }
158
+ const witnessTypes = Symbol("bumbledb.witness.types");
121
159
  /**
122
160
  * The module-private inference slot of {@link Prepared}: an optional symbol
123
161
  * property (never set at runtime) that keeps the prepared value's `Row` and
@@ -312,8 +350,38 @@ function tablesOf(theory, manifest) {
312
350
  });
313
351
  return Object.freeze({ relations, statements: Object.freeze(entries) });
314
352
  }
315
- /** The private lifetime records of this module's read scopes. */
316
- const scopeStates = new WeakMap();
353
+ function tablesFromTheory(theory) {
354
+ const entries = materializedEntries(theory);
355
+ const relations = new Map();
356
+ Object.keys(theory.relations).forEach(function byOrdinal(name, ordinal) {
357
+ const member = theory.relations[name];
358
+ if (member === undefined) {
359
+ throw errors.new(`bumbledb theory has no relation ${name}`);
360
+ }
361
+ const fieldIds = new Map();
362
+ sealedFieldsOf(member).forEach(function byField(declared, fieldOrdinal) {
363
+ fieldIds.set(declared.name, fieldOrdinal);
364
+ });
365
+ let primaryKey;
366
+ entries.forEach(function firstOwnedKey(entry, index) {
367
+ if (primaryKey === undefined && entry.kind === "functionality" && entry.owner === name) {
368
+ primaryKey = Object.freeze({ statementId: index, projection: entry.projection });
369
+ }
370
+ });
371
+ relations.set(name, Object.freeze({ id: ordinal, member, fieldIds, primaryKey }));
372
+ });
373
+ return Object.freeze({ relations, statements: Object.freeze(entries) });
374
+ }
375
+ const instanceStates = new WeakMap();
376
+ const witnessStates = new WeakMap();
377
+ const witnessReclaimer = new FinalizationRegistry(function reclaimWitness(handle) {
378
+ const closed = errors.trySync(function closeWitness() {
379
+ native.witnessClose(handle);
380
+ });
381
+ if (closed.error) {
382
+ return;
383
+ }
384
+ });
317
385
  /** The private engine halves of this module's prepared values. */
318
386
  const preparedPlans = new WeakMap();
319
387
  /**
@@ -330,12 +398,208 @@ const planReclaimer = new FinalizationRegistry(function reclaimPlan(handle) {
330
398
  return;
331
399
  }
332
400
  });
333
- /**
334
- * The typed generation-moved refusal `writeFrom` throws when a
335
- * state-changing commit landed since the witness snapshot: retry is host
336
- * policy. Match with `errors.is`.
337
- */
338
- const ErrGenerationMoved = errors.new("bumbledb generationMoved: a state-changing commit landed since the witness snapshot");
401
+ const ErrAsyncCallback = errors.new("bumbledb asyncCallback: a read or write callback returned a thenable — the callback is synchronous");
402
+ const ErrSpentHandle = errors.new("bumbledb spentHandle: a consumed builder, instance, or witness was used");
403
+ const ErrUseAfterScope = errors.new("bumbledb useAfterScope: a stashed read instance or write transaction was used after its callback returned");
404
+ const ErrForeignPrepared = errors.new("bumbledb foreignPrepared: a prepared query met a foreign instance");
405
+ const ErrForeignWitness = errors.new("bumbledb foreignWitness: a witness met a foreign store");
406
+ function catalogMethods(theory, tables, owner, assertLive, ops) {
407
+ function resolveOrdinary(relation) {
408
+ const entry = tables.relations.get(relation.name);
409
+ if (entry === undefined || entry.member !== relation) {
410
+ throw errors.new(`relation ${relation.name} is not a member of schema ${theory.name}`);
411
+ }
412
+ if (isClosedMember(relation)) {
413
+ throw errors.new(`relation ${relation.name} is closed — its extension is schema data (axioms), never scanned or written`);
414
+ }
415
+ return entry;
416
+ }
417
+ function declaredKeyOf(relation, statement) {
418
+ const statementId = tables.statements.findIndex(function byIdentity(candidate) {
419
+ return "statement" in candidate && candidate.statement === statement;
420
+ });
421
+ const entry = tables.statements[statementId];
422
+ if (entry === undefined) {
423
+ throw errors.new(`keyed get statement is not a declared statement of schema ${theory.name} — statement identity is the membership rule`);
424
+ }
425
+ if (entry.kind !== "functionality") {
426
+ throw errors.new("keyed get takes a key() statement — containments and capacity statements key nothing");
427
+ }
428
+ if (entry.owner !== relation.name) {
429
+ throw errors.new(`keyed get statement keys ${entry.owner}, not ${relation.name} — the statement must be a declared key of the relation it reads`);
430
+ }
431
+ return Object.freeze({ statementId, projection: entry.projection });
432
+ }
433
+ function planOf(prepared) {
434
+ const plan = preparedPlans.get(prepared);
435
+ if (plan === undefined) {
436
+ throw errors.wrap(ErrForeignPrepared, "bumbledb execute target is not a prepared value of this SDK");
437
+ }
438
+ if (plan.owner !== owner) {
439
+ throw errors.wrap(ErrForeignPrepared, `bumbledb prepared value was prepared by a different store than this one (schema ${theory.name})`);
440
+ }
441
+ return plan;
442
+ }
443
+ function contains(relation, fact) {
444
+ assertLive();
445
+ const entry = resolveOrdinary(relation);
446
+ return bridged("bumbledb instance contains", function readContains() {
447
+ return ops.contains(entry.id, rowOf(relation.data, recordOf(fact)));
448
+ });
449
+ }
450
+ function get(relation, keyOrStatement, declaredKey) {
451
+ assertLive();
452
+ const entry = resolveOrdinary(relation);
453
+ return selectKeyRead(keyOrStatement, declaredKey, function byStatement(statement, key) {
454
+ const selected = declaredKeyOf(relation, statement);
455
+ const row = bridged("bumbledb instance get", function readGet() {
456
+ return ops.get(entry.id, selected.statementId, keyRowOf(relation.data, selected.projection, recordOf(key)));
457
+ });
458
+ return row === null ? undefined : factOf(relation, row);
459
+ }, function byPrimary(key) {
460
+ const primaryKey = entry.primaryKey;
461
+ if (primaryKey === undefined) {
462
+ throw errors.new(`relation ${relation.name} has no candidate key — keyed get requires a fresh field or a declared key statement`);
463
+ }
464
+ const row = bridged("bumbledb instance get", function readGet() {
465
+ return ops.get(entry.id, primaryKey.statementId, keyRowOf(relation.data, primaryKey.projection, recordOf(key)));
466
+ });
467
+ return row === null ? undefined : factOf(relation, row);
468
+ });
469
+ }
470
+ function scan(relation) {
471
+ assertLive();
472
+ const entry = resolveOrdinary(relation);
473
+ const rows = bridged("bumbledb instance scan", function readScan() {
474
+ return ops.scan(entry.id);
475
+ });
476
+ return rows.map(function decodeRow(row) {
477
+ return factOf(relation, row);
478
+ });
479
+ }
480
+ function execute(prepared, params) {
481
+ assertLive();
482
+ const plan = planOf(prepared);
483
+ const wire = wireParams(plan.params, recordOf(params));
484
+ const rows = bridged("execute bumbledb prepared query", function callExecute() {
485
+ return ops.execute(plan.handle, wire);
486
+ });
487
+ return decodeAnswers(plan.finds, rows);
488
+ }
489
+ function prepare(q) {
490
+ assertLive();
491
+ if (q.schema !== theory) {
492
+ throw errors.new(`query was built against schema ${q.schema.name}, not the identical schema value this store opened with — schema identity is the membership rule`);
493
+ }
494
+ const queryIr = lowerQuery(q);
495
+ const outcome = bridged("prepare bumbledb query", function callPrepare() {
496
+ return ops.prepare(queryIr);
497
+ });
498
+ if (!outcome.ok) {
499
+ throwPrepareRefusal(outcome.message);
500
+ }
501
+ const prepared = Object.freeze({});
502
+ preparedPlans.set(prepared, Object.freeze({
503
+ handle: outcome.prepared,
504
+ owner,
505
+ params: q.data.params,
506
+ finds: q.data.finds
507
+ }));
508
+ planReclaimer.register(prepared, outcome.prepared);
509
+ return prepared;
510
+ }
511
+ return { scan, get, contains, execute, prepare };
512
+ }
513
+ function ordinaryEntry(tables, theory, relation) {
514
+ const entry = tables.relations.get(relation.name);
515
+ if (entry === undefined || entry.member !== relation) {
516
+ throw errors.new(`relation ${relation.name} is not a member of schema ${theory.name}`);
517
+ }
518
+ if (isClosedMember(relation)) {
519
+ throw errors.new(`relation ${relation.name} is closed — its extension is schema data (axioms), never scanned or written`);
520
+ }
521
+ return entry;
522
+ }
523
+ function overlayMethods(theory, tables, assertLive, reads) {
524
+ function declaredKeyOf(relation, statement) {
525
+ const statementId = tables.statements.findIndex(function byIdentity(candidate) {
526
+ return "statement" in candidate && candidate.statement === statement;
527
+ });
528
+ const entry = tables.statements[statementId];
529
+ if (entry === undefined) {
530
+ throw errors.new(`keyed get statement is not a declared statement of schema ${theory.name} — statement identity is the membership rule`);
531
+ }
532
+ if (entry.kind !== "functionality") {
533
+ throw errors.new("keyed get takes a key() statement — containments and capacity statements key nothing");
534
+ }
535
+ if (entry.owner !== relation.name) {
536
+ throw errors.new(`keyed get statement keys ${entry.owner}, not ${relation.name} — the statement must be a declared key of the relation it reads`);
537
+ }
538
+ return Object.freeze({ statementId, projection: entry.projection });
539
+ }
540
+ function contains(relation, fact) {
541
+ assertLive();
542
+ const entry = ordinaryEntry(tables, theory, relation);
543
+ return reads.contains(entry.id, rowOf(relation.data, recordOf(fact)));
544
+ }
545
+ function readThroughKey(relation, entry, selected, key) {
546
+ const row = reads.get(entry.id, selected.statementId, keyRowOf(relation.data, selected.projection, key));
547
+ if (row === null) {
548
+ return undefined;
549
+ }
550
+ return factOf(relation, row);
551
+ }
552
+ function get(relation, keyOrStatement, declaredKey) {
553
+ assertLive();
554
+ const entry = ordinaryEntry(tables, theory, relation);
555
+ return selectKeyRead(keyOrStatement, declaredKey, function byStatement(statement, key) {
556
+ return readThroughKey(relation, entry, declaredKeyOf(relation, statement), recordOf(key));
557
+ }, function byPrimary(key) {
558
+ const primaryKey = entry.primaryKey;
559
+ if (primaryKey === undefined) {
560
+ throw errors.new(`relation ${relation.name} has no candidate key — keyed get requires a fresh field or a declared key statement`);
561
+ }
562
+ return readThroughKey(relation, entry, primaryKey, recordOf(key));
563
+ });
564
+ }
565
+ return { contains, get };
566
+ }
567
+ function createReadInstance(nativeHandle, theory, tables, owner) {
568
+ const state = { handle: nativeHandle, live: true, owner };
569
+ function assertLive() {
570
+ if (!state.live) {
571
+ throw errors.wrap(ErrUseAfterScope, "bumbledb read instance is invalidated — its owning callback already returned");
572
+ }
573
+ }
574
+ const methods = catalogMethods(theory, tables, owner, assertLive, {
575
+ scan(relationId) {
576
+ return native.instanceScan(state.handle, relationId);
577
+ },
578
+ contains(relationId, values) {
579
+ return native.instanceContains(state.handle, relationId, values);
580
+ },
581
+ get(relationId, statementId, keyValues) {
582
+ return native.instanceGet(state.handle, relationId, statementId, keyValues);
583
+ },
584
+ prepare(query) {
585
+ return native.instancePrepare(state.handle, query);
586
+ },
587
+ execute(prepared, params) {
588
+ return native.preparedExecute(prepared, state.handle, params);
589
+ }
590
+ });
591
+ const instance = Object.freeze({
592
+ get generation() {
593
+ assertLive();
594
+ return bridged("bumbledb instance generation", function readGeneration() {
595
+ return native.instanceGeneration(state.handle);
596
+ });
597
+ },
598
+ ...methods
599
+ });
600
+ instanceStates.set(instance, state);
601
+ return instance;
602
+ }
339
603
  /**
340
604
  * Constructs one open `Db` over an already-admitted handle: builds the
341
605
  * id-resolution tables once and closes over them — the `Db` owns handle
@@ -476,172 +740,88 @@ function openDb(handle, theory, manifest) {
476
740
  }
477
741
  return { contains, get };
478
742
  }
479
- /**
480
- * Resolves a prepared value's private plan, refusing foreign objects
481
- * and prepared values of other stores as typed errors.
482
- */
483
- function planOf(prepared) {
484
- const plan = preparedPlans.get(prepared);
485
- if (plan === undefined) {
486
- throw errors.new("bumbledb execute target is not a prepared value of this SDK");
487
- }
488
- if (plan.owner !== owner) {
489
- throw errors.new(`bumbledb prepared value was prepared by a different store than this one (schema ${theory.name})`);
490
- }
491
- return plan;
743
+ function pinPrepared(preparedHandle, q) {
744
+ const prepared = Object.freeze({});
745
+ preparedPlans.set(prepared, Object.freeze({
746
+ handle: preparedHandle,
747
+ owner,
748
+ params: q.data.params,
749
+ finds: q.data.finds
750
+ }));
751
+ planReclaimer.register(prepared, preparedHandle);
752
+ return prepared;
492
753
  }
493
- /**
494
- * Builds one {@link ReadScope} over a live scope state. Every verb
495
- * asserts liveness first: the owning call flips `state.live` the moment
496
- * its callback returns (or the scope's own `Symbol.dispose` does, for a
497
- * `using`-acquired scope), so a leaked scope is a typed refusal forever
498
- * after.
499
- */
500
- function makeScope(state) {
501
- function assertLive() {
502
- if (!state.live) {
503
- throw errors.new("bumbledb read scope is invalidated — its owning read callback already returned");
504
- }
505
- }
506
- const reads = pointReadsOf(assertLive, {
507
- contains(relationId, row) {
508
- return bridged("bumbledb snapshot contains", function readContains() {
509
- return native.snapshotContains(state.handle, relationId, row);
510
- });
511
- },
512
- get(relationId, statementId, key) {
513
- return bridged("bumbledb snapshot get", function readGet() {
514
- return native.snapshotGet(state.handle, relationId, statementId, key);
754
+ function makeWitness(nativeHandle) {
755
+ const state = { handle: nativeHandle, spent: false, owner };
756
+ const witness = Object.freeze({
757
+ [Symbol.dispose]() {
758
+ if (state.spent) {
759
+ return;
760
+ }
761
+ state.spent = true;
762
+ bridged("close bumbledb witness", function closeWitness() {
763
+ native.witnessClose(nativeHandle);
515
764
  });
516
765
  }
517
766
  });
518
- function scan(relation) {
519
- assertLive();
520
- const entry = resolveOrdinary(relation);
521
- const rows = bridged("bumbledb snapshot scan", function readScan() {
522
- return native.snapshotScan(state.handle, entry.id);
523
- });
524
- return rows.map(function decodeRow(row) {
525
- return factOf(relation, row);
526
- });
527
- }
528
- function execute(prepared, params) {
529
- assertLive();
530
- const plan = planOf(prepared);
531
- const wire = wireParams(plan.params, recordOf(params));
532
- const rows = bridged("execute bumbledb prepared query", function callExecute() {
533
- return native.preparedExecute(plan.handle, state.handle, wire);
534
- });
535
- return decodeAnswers(plan.finds, rows);
536
- }
537
- /** The R12 teardown: invalidate, then close — idempotent through the state's close latch. */
538
- function dispose() {
539
- state.live = false;
540
- closeScopeState(state);
541
- }
542
- const scope = Object.freeze({
543
- generation: state.generation,
544
- scan,
545
- get: reads.get,
546
- contains: reads.contains,
547
- execute,
548
- [Symbol.dispose]: dispose
549
- });
550
- scopeStates.set(scope, state);
551
- return scope;
767
+ witnessStates.set(witness, state);
768
+ witnessReclaimer.register(witness, nativeHandle);
769
+ return witness;
552
770
  }
553
- /**
554
- * Live-handle accounting (diagnostic law, prod EINVAL 2026-07-17): every
555
- * snapshot open/close is counted so a write-begin failure can report how
556
- * many read handles were live at the fault — a leaked scope is invisible
557
- * until the exact moment it matters, so the failure carries the census.
558
- */
559
- let liveSnapshots = 0;
560
- /**
561
- * Opens one snapshot and its scope state (live until the owner flips
562
- * it). The witnessed generation rides the snapshot open itself — one
563
- * crossing carries both (finding 016), so the fault-pairing close
564
- * branch a second `dbGeneration` call needed is structurally gone.
565
- */
566
- function openScopeState() {
567
- const opened = bridged("open bumbledb snapshot", function openSnapshot() {
568
- return native.dbSnapshot(handle);
569
- });
570
- liveSnapshots += 1;
571
- return { handle: opened.snapshot, generation: opened.generation, live: true, closed: false, owner };
771
+ function makeInstance(nativeHandle) {
772
+ return createReadInstance(nativeHandle, theory, tables, owner);
572
773
  }
573
- /**
574
- * Closes a scope's snapshot after the owner invalidated it — a LATCH:
575
- * the snapshot closes exactly once, whichever of the owning call and
576
- * the scope's own `Symbol.dispose` gets there first, so an early
577
- * in-callback disposal never double-closes (and never double-counts
578
- * the census).
579
- */
580
- function closeScopeState(state) {
581
- if (state.closed) {
582
- return;
583
- }
584
- state.closed = true;
585
- bridged("close bumbledb snapshot", function closeSnapshot() {
586
- native.snapshotClose(state.handle);
587
- });
588
- liveSnapshots -= 1;
589
- }
590
- function read(fn) {
591
- const state = openScopeState();
592
- const scope = makeScope(state);
593
- if (fn === undefined) {
594
- /**
595
- * The `using` acquisition (R12): the caller owns the lifetime —
596
- * `using snap = db.read()` — and the scope's `Symbol.dispose`
597
- * is the deterministic release, scope-shaped in the language's
598
- * own syntax.
599
- */
600
- return scope;
601
- }
602
- const result = errors.trySync(function runRead() {
603
- return fn(scope);
604
- });
605
- state.live = false;
606
- closeScopeState(state);
607
- if (result.error) {
608
- throw errors.wrap(result.error, "bumbledb read");
609
- }
610
- return result.data;
774
+ function read(body) {
775
+ let captured;
776
+ const result = bridged("bumbledb read", function runRead() {
777
+ return native.dbRead(handle, function onRead(nativeInstance, nativeWitness) {
778
+ const instance = makeInstance(nativeInstance);
779
+ const witness = makeWitness(nativeWitness);
780
+ const value = body(instance, witness);
781
+ const state = instanceStates.get(instance);
782
+ if (state !== undefined) {
783
+ state.live = false;
784
+ }
785
+ if (isThenable(value)) {
786
+ throw errors.wrap(ErrAsyncCallback, "bumbledb read callback returned a thenable");
787
+ }
788
+ captured = value;
789
+ return value;
790
+ });
791
+ });
792
+ return (captured ?? result);
611
793
  }
612
794
  function scan(relation) {
613
- return read(function scanInScope(snap) {
614
- return snap.scan(relation);
795
+ return read(function scanInScope(instance) {
796
+ return instance.scan(relation);
615
797
  });
616
798
  }
617
799
  function get(relation, keyOrStatement, declaredKey) {
618
- return read(function getInScope(snap) {
619
- return selectKeyRead(keyOrStatement, declaredKey, function byStatement(statement, key) {
620
- return snap.get(relation, statement, key);
800
+ let found;
801
+ read(function getInScope(instance) {
802
+ found = selectKeyRead(keyOrStatement, declaredKey, function byStatement(statement, key) {
803
+ return instance.get(relation, statement, key);
621
804
  }, function byPrimary(key) {
622
- return snap.get(relation, key);
805
+ return instance.get(relation, key);
623
806
  });
624
807
  });
808
+ return found;
625
809
  }
626
810
  function contains(relation, fact) {
627
- return read(function containsInScope(snap) {
628
- return snap.contains(relation, fact);
811
+ return read(function containsInScope(instance) {
812
+ return instance.contains(relation, fact);
629
813
  });
630
814
  }
631
815
  function execute(prepared, params) {
632
- return read(function executeInScope(snap) {
633
- return snap.execute(prepared, params);
816
+ return read(function executeInScope(instance) {
817
+ return instance.execute(prepared, params);
634
818
  });
635
819
  }
636
- /**
637
- * Builds one {@link Tx} over a transaction-handle thunk: `write` and
638
- * `writeFrom` pass an already-begun handle.
639
- */
640
820
  function makeTx(resolveTx) {
641
821
  const txState = { spent: false };
642
822
  function assertLive() {
643
823
  if (txState.spent) {
644
- throw errors.new("bumbledb write transaction is spent");
824
+ throw errors.wrap(ErrUseAfterScope, "bumbledb write transaction is spent");
645
825
  }
646
826
  }
647
827
  const reads = pointReadsOf(assertLive, {
@@ -660,27 +840,24 @@ function openDb(handle, theory, manifest) {
660
840
  });
661
841
  function insert(relation, facts) {
662
842
  assertLive();
663
- const rows = [];
664
- for (const fact of facts) {
665
- rows.push(rowOf(relation.data, recordOf(fact)));
666
- }
667
843
  const entry = resolveOrdinary(relation);
668
844
  const txHandle = resolveTx();
669
- const report = bridged("bumbledb tx insert", function record() {
670
- return native.txInsert(txHandle, entry.id, rows);
845
+ return mutateCollection(relation, facts, function applyRows(rows) {
846
+ return bridged("bumbledb tx insert", function record() {
847
+ return native.txInsert(txHandle, entry.id, rows);
848
+ });
849
+ }, function applyColumns(columns) {
850
+ return bridged("bumbledb tx insert", function recordColumns() {
851
+ return native.txInsertColumns(txHandle, entry.id, columns);
852
+ });
671
853
  });
672
- return Object.freeze({ submitted: report.submitted, changed: report.changed });
673
854
  }
674
855
  function remove(relation, facts) {
675
856
  assertLive();
676
- const rows = [];
677
- for (const fact of facts) {
678
- rows.push(rowOf(relation.data, recordOf(fact)));
679
- }
680
857
  const entry = resolveOrdinary(relation);
681
858
  const txHandle = resolveTx();
682
859
  const report = bridged("bumbledb tx delete", function record() {
683
- return native.txDelete(txHandle, entry.id, rows);
860
+ return native.txDelete(txHandle, entry.id, rowsOf(relation, facts));
684
861
  });
685
862
  return Object.freeze({ submitted: report.submitted, changed: report.changed });
686
863
  }
@@ -715,104 +892,80 @@ function openDb(handle, theory, manifest) {
715
892
  }
716
893
  return { tx, spend };
717
894
  }
718
- function runDelta(txHandle, fn) {
719
- const made = makeTx(function resolveTx() {
720
- return txHandle;
721
- });
722
- const built = errors.trySync(function buildDelta() {
723
- return fn(made.tx);
724
- });
725
- made.spend();
726
- if (built.error) {
727
- bridged("abort bumbledb write transaction", function abort() {
728
- native.txAbort(txHandle);
729
- });
730
- throw errors.wrap(built.error, "build write delta");
731
- }
732
- if (isThenable(built.data)) {
733
- /**
734
- * An `async` callback TYPECHECKS (Promise<void> is assignable where
735
- * a `void` return is expected) but its body runs after the tx is
736
- * spent: committing here would be a silent EMPTY commit reported
737
- * ok while the callback's real inserts throw "spent" as unhandled
738
- * rejections. Refused typed instead — abort, nothing committed
739
- * (the same one-writer law as the thrown-callback path).
740
- */
741
- bridged("abort bumbledb write transaction", function abort() {
742
- native.txAbort(txHandle);
743
- });
744
- throw errors.new("bumbledb write callback returned a thenable — the delta build is synchronous; an async callback is refused, nothing was committed");
745
- }
746
- if (isAbandon(built.data)) {
747
- /**
748
- * The caller's explicit decline to commit (R10): the sentinel's
749
- * contract is unconditional — roll back, nothing committed, not
750
- * even an empty commit; commit is unreachable for a sentinel
751
- * result.
752
- */
753
- bridged("abort bumbledb write transaction", function abort() {
754
- native.txAbort(txHandle);
895
+ function mapNativeWrite(nativeOutcome, built) {
896
+ if (nativeOutcome.tag === "moved") {
897
+ return Object.freeze({
898
+ tag: "moved",
899
+ witnessed: nativeOutcome.witnessed,
900
+ current: nativeOutcome.current
755
901
  });
756
- return abandonedOutcome(built.data);
757
902
  }
758
- const committed = errors.trySync(function commitDelta() {
759
- return bridged("commit bumbledb write transaction", function commit() {
760
- return native.txCommit(txHandle);
761
- });
762
- });
763
- if (committed.error) {
764
- /**
765
- * A THROWN commit (engine I/O failure, bridge fault) must never
766
- * leave the write transaction live: LMDB holds one writer per
767
- * environment, and a leaked handle turns every later begin into
768
- * EINVAL for the process's lifetime. The abort is best-effort —
769
- * the native side may already have consumed the handle.
770
- */
771
- const aborted = errors.trySync(function abortAfterFailedCommit() {
772
- native.txAbort(txHandle);
903
+ if (nativeOutcome.tag === "rejected") {
904
+ return Object.freeze({
905
+ tag: "rejected",
906
+ violations: Object.freeze(nativeOutcome.violations.map(violationOf))
773
907
  });
774
- if (aborted.error) {
775
- }
776
- throw errors.wrap(committed.error, "commit bumbledb write transaction");
777
908
  }
778
- const outcome = committed.data;
779
- if (outcome.ok) {
780
- return Object.freeze({ ok: true, generation: outcome.generation });
909
+ if (nativeOutcome.tag === "abandoned") {
910
+ if (built === undefined || !isAbandon(built)) {
911
+ throw errors.new("bumbledb write abandoned without an abandon sentinel");
912
+ }
913
+ return abandonedOutcome(built);
781
914
  }
782
915
  return Object.freeze({
783
- ok: false,
784
- violations: Object.freeze(outcome.violations.map(violationOf))
916
+ tag: "accepted",
917
+ value: Object.freeze({
918
+ value: built,
919
+ generation: nativeOutcome.generation
920
+ })
785
921
  });
786
922
  }
787
- function write(fn) {
788
- const txHandle = bridged(`begin bumbledb write transaction (live snapshots: ${liveSnapshots})`, function begin() {
789
- return native.dbWriteBegin(handle);
923
+ function runWrite(invoke, fn) {
924
+ let built;
925
+ const nativeOutcome = bridged("bumbledb write", function callWrite() {
926
+ return invoke(function onWrite(txHandle) {
927
+ const made = makeTx(function resolveTx() {
928
+ return txHandle;
929
+ });
930
+ const result = errors.trySync(function buildDelta() {
931
+ return fn(made.tx);
932
+ });
933
+ made.spend();
934
+ if (result.error) {
935
+ throw errors.wrap(result.error, "build write delta");
936
+ }
937
+ if (isThenable(result.data)) {
938
+ throw errors.wrap(ErrAsyncCallback, "bumbledb write callback returned a thenable");
939
+ }
940
+ built = result.data;
941
+ return !isAbandon(result.data);
942
+ });
790
943
  });
791
- return runDelta(txHandle, fn);
944
+ return mapNativeWrite(nativeOutcome, built);
792
945
  }
793
- /**
794
- * One-shot write from a live snapshot this store owns. Begins immediately;
795
- * a moved generation throws {@link ErrGenerationMoved} and `fn` never
796
- * runs. The snapshot stays owned by the caller's read callback.
797
- */
798
- function writeFrom(snap, fn) {
799
- const snapState = scopeStates.get(snap);
800
- if (snapState === undefined) {
801
- throw errors.new("bumbledb writeFrom witness is not a read scope of this SDK");
946
+ function write(fn) {
947
+ const outcome = runWrite(function invoke(callback) {
948
+ return native.dbWrite(handle, callback);
949
+ }, fn);
950
+ if (outcome.tag === "moved") {
951
+ throw errors.new("bumbledb write reported moved — unconditional writes cannot move");
802
952
  }
803
- if (snapState.owner !== owner) {
804
- throw errors.new(`bumbledb writeFrom snapshot belongs to a different store (schema ${theory.name})`);
953
+ return outcome;
954
+ }
955
+ function writeFrom(witness, fn) {
956
+ const state = witnessStates.get(witness);
957
+ if (state === undefined) {
958
+ throw errors.wrap(ErrForeignWitness, "bumbledb writeFrom witness is not a witness of this SDK");
805
959
  }
806
- if (!snapState.live) {
807
- throw errors.new("bumbledb writeFrom snapshot is invalidated — its owning read callback already returned");
960
+ if (state.owner !== owner) {
961
+ throw errors.wrap(ErrForeignWitness, `bumbledb writeFrom witness belongs to a different store (schema ${theory.name})`);
808
962
  }
809
- const witnessed = bridged("begin witnessed bumbledb write transaction", function begin() {
810
- return native.dbWriteFrom(handle, snapState.handle);
811
- });
812
- if (!witnessed.ok) {
813
- throw errors.wrap(ErrGenerationMoved, `writeFrom: generation moved (witnessed ${witnessed.witnessed} current ${witnessed.current}) against schema ${theory.name}`);
963
+ if (state.spent) {
964
+ throw errors.wrap(ErrSpentHandle, "bumbledb writeFrom witness has been disposed");
814
965
  }
815
- return runDelta(witnessed.tx, fn);
966
+ return runWrite(function invoke(callback) {
967
+ return native.dbWriteFrom(handle, state.handle, callback);
968
+ }, fn);
816
969
  }
817
970
  function prepare(q) {
818
971
  if (q.schema !== theory) {
@@ -823,18 +976,9 @@ function openDb(handle, theory, manifest) {
823
976
  return native.dbPrepare(handle, queryIr);
824
977
  });
825
978
  if (!outcome.ok) {
826
- throw errors.new(`bumbledb ${outcome.kind} (prepare): ${outcome.message}`);
979
+ throwPrepareRefusal(outcome.message);
827
980
  }
828
- const preparedHandle = outcome.prepared;
829
- const prepared = Object.freeze({});
830
- preparedPlans.set(prepared, Object.freeze({
831
- handle: preparedHandle,
832
- owner,
833
- params: q.data.params,
834
- finds: q.data.finds
835
- }));
836
- planReclaimer.register(prepared, preparedHandle);
837
- return prepared;
981
+ return pinPrepared(outcome.prepared, q);
838
982
  }
839
983
  return Object.freeze({
840
984
  schema: theory,
@@ -859,78 +1003,340 @@ function openDb(handle, theory, manifest) {
859
1003
  * what the types claim.
860
1004
  */
861
1005
  const ErrNewtypeMismatch = errors.new("bumbledb newtypeMismatch: a statement pairs faces whose newtypes disagree — the faces of a dependency agree on their newtype, or neither carries one");
862
- /**
863
- * The one admission path both verbs share: lower the theory, run one
864
- * bridge call, and wrap the domain refusals — `schemaError` (spec
865
- * resolution + schema validation, every issue in one message),
866
- * `newtypeMismatch` (the coherence wall, {@link ErrNewtypeMismatch}),
867
- * and `fingerprintMismatch` (a different theory cannot open the store)
868
- * — into typed errors carrying the engine's message intact.
869
- * Environment failures (a second live writer on the same path, IO)
870
- * throw from the bridge; the engine's `EnvironmentLocked` message is
871
- * "another live handle holds this environment's lock".
872
- */
873
- function admit(verb, storePath, theory) {
1006
+ const ErrSchemaError = errors.new("bumbledb schemaError: the declaration failed validation");
1007
+ const ErrFingerprintMismatch = errors.new("bumbledb fingerprintMismatch: the store's schema does not match this theory");
1008
+ const ErrIrError = errors.new("bumbledb irError: the query failed validation");
1009
+ function throwOpenRefusal(verb, canonical, kind, message) {
1010
+ const detail = `${verb} ${canonical}: ${message}`;
1011
+ if (kind === "newtypeMismatch") {
1012
+ throw errors.wrap(ErrNewtypeMismatch, detail);
1013
+ }
1014
+ if (kind === "schemaError") {
1015
+ throw errors.wrap(ErrSchemaError, detail);
1016
+ }
1017
+ throw errors.wrap(ErrFingerprintMismatch, detail);
1018
+ }
1019
+ function throwPrepareRefusal(message) {
1020
+ throw errors.wrap(ErrIrError, `prepare: ${message}`);
1021
+ }
1022
+ function openFromHandle(dbHandle, theory) {
1023
+ const manifest = bridged("fetch bumbledb manifest", function fetchManifest() {
1024
+ return native.dbManifest(dbHandle);
1025
+ });
1026
+ return openDb(dbHandle, theory, manifest);
1027
+ }
1028
+ async function createStore(storePath, theory) {
874
1029
  const canonical = path.resolve(storePath);
875
1030
  const spec = lower(theory);
876
- const opened = bridged(`${verb} bumbledb store at ${canonical}`, function callBridge() {
877
- if (verb === "create") {
878
- return native.dbCreate(canonical, spec);
1031
+ const created = await bridgedAsync(`create bumbledb store at ${canonical}`, function callBridge() {
1032
+ return native.dbCreate(canonical, spec);
1033
+ });
1034
+ if (created.tag === "schemaError" || created.tag === "newtypeMismatch") {
1035
+ throwOpenRefusal("create", canonical, created.tag, created.message);
1036
+ }
1037
+ if (created.tag === "rejected") {
1038
+ return Object.freeze({
1039
+ tag: "rejected",
1040
+ violations: Object.freeze(created.violations.map(function mapWire(wire) {
1041
+ return mapViolationWithoutStore(theory, wire);
1042
+ }))
1043
+ });
1044
+ }
1045
+ return Object.freeze({ tag: "accepted", value: openFromHandle(created.db, theory) });
1046
+ }
1047
+ function mapViolationWithoutStore(theory, wire) {
1048
+ const entries = materializedEntries(theory);
1049
+ const entry = entries[wire.statementId];
1050
+ if (entry === undefined) {
1051
+ throw errors.new(`bumbledb violation cites unknown statement id ${wire.statementId}`);
1052
+ }
1053
+ function offending(fact) {
1054
+ const member = theory.relations[fact.relation];
1055
+ if (member === undefined || !(fact.relation in theory.relations)) {
1056
+ throw errors.new(`bumbledb violation cites unknown relation ${fact.relation}`);
879
1057
  }
1058
+ const declared = sealedFieldsOf(member);
1059
+ const decoded = {};
1060
+ for (const cell of fact.fields) {
1061
+ const cited = declared.find(function byName(candidate) {
1062
+ return candidate.name === cell.name;
1063
+ });
1064
+ const roster = rosterOf(cited?.field);
1065
+ decoded[cell.name] =
1066
+ roster !== undefined
1067
+ ? handleOf(`violation fact ${fact.relation} field ${cell.name}`, roster, cell.value)
1068
+ : cell.value;
1069
+ }
1070
+ return Object.freeze({ relation: fact.relation, fact: Object.freeze(decoded) });
1071
+ }
1072
+ const facts = Object.freeze(wire.facts.map(offending));
1073
+ const canonical = wire.canonical;
1074
+ if (entry.kind === "functionality") {
1075
+ if (!("statement" in entry)) {
1076
+ return Object.freeze({ kind: "functionality", statement: undefined, canonical, facts });
1077
+ }
1078
+ return Object.freeze({ kind: "functionality", statement: entry.statement, canonical, facts });
1079
+ }
1080
+ if (entry.kind === "capacity") {
1081
+ if (wire.kind !== "capacity") {
1082
+ throw errors.new(`bumbledb violation ${wire.statementId} is a capacity slot without a measure`);
1083
+ }
1084
+ return Object.freeze({
1085
+ kind: "capacity",
1086
+ statement: entry.statement,
1087
+ canonical,
1088
+ measure: wire.measure,
1089
+ facts
1090
+ });
1091
+ }
1092
+ if (wire.kind !== "containment") {
1093
+ throw errors.new(`bumbledb violation ${wire.statementId} is a containment slot without a direction`);
1094
+ }
1095
+ if (entry.kind === "mirrors") {
1096
+ return Object.freeze({
1097
+ kind: "containment",
1098
+ statement: entry.statement,
1099
+ canonical,
1100
+ direction: wire.direction,
1101
+ orientation: entry.orientation,
1102
+ facts
1103
+ });
1104
+ }
1105
+ return Object.freeze({
1106
+ kind: "containment",
1107
+ statement: entry.statement,
1108
+ canonical,
1109
+ direction: wire.direction,
1110
+ facts
1111
+ });
1112
+ }
1113
+ async function openStore(storePath, theory) {
1114
+ const canonical = path.resolve(storePath);
1115
+ const spec = lower(theory);
1116
+ const opened = await bridgedAsync(`open bumbledb store at ${canonical}`, function callBridge() {
880
1117
  return native.dbOpen(canonical, spec);
881
1118
  });
882
1119
  if (!opened.ok) {
883
- if (opened.kind === "newtypeMismatch") {
884
- throw errors.wrap(ErrNewtypeMismatch, `${verb} ${canonical}: ${opened.message}`);
1120
+ throwOpenRefusal("open", canonical, opened.kind, opened.message);
1121
+ }
1122
+ return openFromHandle(opened.db, theory);
1123
+ }
1124
+ const ownedRecords = new WeakMap();
1125
+ const builderRecords = new WeakMap();
1126
+ const ownedReclaimer = new FinalizationRegistry(function reclaimOwned(handle) {
1127
+ const closed = errors.trySync(function closeOwned() {
1128
+ native.ownedInstanceClose(handle);
1129
+ });
1130
+ if (closed.error) {
1131
+ return;
1132
+ }
1133
+ });
1134
+ const builderReclaimer = new FinalizationRegistry(function reclaimBuilder(handle) {
1135
+ const closed = errors.trySync(function closeBuilder() {
1136
+ native.instanceBuilderClose(handle);
1137
+ });
1138
+ if (closed.error) {
1139
+ return;
1140
+ }
1141
+ });
1142
+ function wrapOwned(nativeHandle, theory) {
1143
+ const owner = Object.freeze({});
1144
+ const rec = { handle: nativeHandle, theory, spent: false, owner };
1145
+ const tables = tablesFromTheory(theory);
1146
+ function assertLive() {
1147
+ if (rec.spent) {
1148
+ throw errors.wrap(ErrSpentHandle, "bumbledb owned instance has been disposed");
885
1149
  }
886
- throw errors.new(`bumbledb ${opened.kind} (${verb} ${canonical}): ${opened.message}`);
887
1150
  }
888
- const manifest = bridged("fetch bumbledb manifest", function fetchManifest() {
889
- return native.dbManifest(opened.db);
1151
+ const methods = catalogMethods(theory, tables, owner, assertLive, {
1152
+ scan(relationId) {
1153
+ return native.ownedScan(nativeHandle, relationId);
1154
+ },
1155
+ contains(relationId, values) {
1156
+ return native.ownedContains(nativeHandle, relationId, values);
1157
+ },
1158
+ get(relationId, statementId, keyValues) {
1159
+ return native.ownedGet(nativeHandle, relationId, statementId, keyValues);
1160
+ },
1161
+ prepare(query) {
1162
+ return native.ownedPrepare(nativeHandle, query);
1163
+ },
1164
+ execute(prepared, params) {
1165
+ return native.ownedExecute(prepared, nativeHandle, params);
1166
+ }
1167
+ });
1168
+ const instance = Object.freeze({
1169
+ ...methods,
1170
+ [Symbol.dispose]() {
1171
+ if (rec.spent) {
1172
+ return;
1173
+ }
1174
+ try {
1175
+ native.ownedInstanceClose(nativeHandle);
1176
+ }
1177
+ catch (caught) {
1178
+ const error = errorFromThrow(caught);
1179
+ if (/leased for publish/.test(error.message)) {
1180
+ throw errors.wrap(ErrSpentHandle, "bumbledb owned instance is leased for publish");
1181
+ }
1182
+ throw errors.wrap(error, "close bumbledb owned instance");
1183
+ }
1184
+ rec.spent = true;
1185
+ ownedReclaimer.unregister(instance);
1186
+ }
1187
+ });
1188
+ ownedRecords.set(instance, rec);
1189
+ ownedReclaimer.register(instance, nativeHandle, instance);
1190
+ return instance;
1191
+ }
1192
+ function wrapBuilder(nativeHandle, theory) {
1193
+ const rec = { handle: nativeHandle, theory, spent: false };
1194
+ const tables = tablesFromTheory(theory);
1195
+ function assertLive() {
1196
+ if (rec.spent) {
1197
+ throw errors.wrap(ErrSpentHandle, "bumbledb instance builder has been spent");
1198
+ }
1199
+ }
1200
+ const overlay = overlayMethods(theory, tables, assertLive, {
1201
+ contains(relationId, row) {
1202
+ return bridged("bumbledb builder contains", function readContains() {
1203
+ return native.instanceBuilderContains(nativeHandle, relationId, row);
1204
+ });
1205
+ },
1206
+ get(relationId, statementId, key) {
1207
+ return bridged("bumbledb builder get", function readGet() {
1208
+ return native.instanceBuilderGet(nativeHandle, relationId, statementId, key);
1209
+ });
1210
+ }
890
1211
  });
891
- return openDb(opened.db, theory, manifest);
1212
+ const builder = Object.freeze({
1213
+ load(relation, facts) {
1214
+ assertLive();
1215
+ const entry = ordinaryEntry(tables, theory, relation);
1216
+ return mutateCollection(relation, facts, function applyRows(rows) {
1217
+ return bridged("bumbledb builder load", function loadRows() {
1218
+ return native.instanceBuilderLoad(nativeHandle, entry.id, rows);
1219
+ });
1220
+ }, function applyColumns(columns) {
1221
+ return bridged("bumbledb builder load", function loadColumns() {
1222
+ return native.instanceBuilderLoadColumns(nativeHandle, entry.id, columns);
1223
+ });
1224
+ });
1225
+ },
1226
+ delete(relation, facts) {
1227
+ assertLive();
1228
+ const entry = ordinaryEntry(tables, theory, relation);
1229
+ const report = bridged("bumbledb builder delete", function remove() {
1230
+ return native.instanceBuilderDelete(nativeHandle, entry.id, rowsOf(relation, facts));
1231
+ });
1232
+ return Object.freeze({ submitted: report.submitted, changed: report.changed });
1233
+ },
1234
+ reserve(relation, field, count) {
1235
+ assertLive();
1236
+ const entry = ordinaryEntry(tables, theory, relation);
1237
+ const declared = relation.data.fields.find(function byName(candidate) {
1238
+ return candidate.name === field;
1239
+ });
1240
+ if (declared === undefined || !isFreshField(declared.field)) {
1241
+ throw errors.new(`relation ${relation.name}: field ${field} is not a fresh cell`);
1242
+ }
1243
+ const fieldId = entry.fieldIds.get(field);
1244
+ if (fieldId === undefined) {
1245
+ throw errors.new(`bumbledb manifest drift: relation ${relation.name} has no field id for ${field}`);
1246
+ }
1247
+ const range = bridged("bumbledb builder reserve", function mint() {
1248
+ return native.instanceBuilderReserve(nativeHandle, entry.id, fieldId, count);
1249
+ });
1250
+ return freshRangeOf(range);
1251
+ },
1252
+ contains: overlay.contains,
1253
+ get: overlay.get,
1254
+ async admit() {
1255
+ if (rec.spent) {
1256
+ throw errors.wrap(ErrSpentHandle, "bumbledb instance builder has been spent");
1257
+ }
1258
+ rec.spent = true;
1259
+ builderReclaimer.unregister(builder);
1260
+ let outcome;
1261
+ try {
1262
+ outcome = await native.instanceBuilderAdmit(nativeHandle);
1263
+ }
1264
+ catch (caught) {
1265
+ throw errors.wrap(errorFromThrow(caught), "admit bumbledb instance");
1266
+ }
1267
+ if (outcome.tag === "rejected") {
1268
+ return Object.freeze({
1269
+ tag: "rejected",
1270
+ violations: Object.freeze(outcome.violations.map(function mapWire(wire) {
1271
+ return mapViolationWithoutStore(theory, wire);
1272
+ }))
1273
+ });
1274
+ }
1275
+ return Object.freeze({
1276
+ tag: "accepted",
1277
+ value: wrapOwned(outcome.value, theory)
1278
+ });
1279
+ },
1280
+ [Symbol.dispose]() {
1281
+ if (rec.spent) {
1282
+ return;
1283
+ }
1284
+ rec.spent = true;
1285
+ builderReclaimer.unregister(builder);
1286
+ bridged("close bumbledb instance builder", function closeBuilder() {
1287
+ native.instanceBuilderClose(nativeHandle);
1288
+ });
1289
+ }
1290
+ });
1291
+ builderRecords.set(builder, rec);
1292
+ builderReclaimer.register(builder, nativeHandle, builder);
1293
+ return builder;
892
1294
  }
1295
+ const InstanceBuilder = Object.freeze({
1296
+ create(theory) {
1297
+ const spec = lower(theory);
1298
+ const handle = bridged("create bumbledb instance builder", function make() {
1299
+ return native.instanceBuilderNew(spec);
1300
+ });
1301
+ return wrapBuilder(handle, theory);
1302
+ }
1303
+ });
893
1304
  /**
894
1305
  * The store lifecycle — `Db.create(path, schema)` / `Db.open(path, schema)`.
895
1306
  * Create refuses an already-initialized directory; open verifies format
896
- * version, store kind, and the schema fingerprint. A second live handle
897
- * on the same path is the engine's `EnvironmentLocked`. There is no close
898
- * anywhere: the process owns the environment until GC/exit (durability is
899
- * the engine's per-commit fsync). One store kind exists: durable —
900
- * resume = reopen in a fresh process, or hold the `Db` this process opened.
1307
+ * version and the schema fingerprint. A second live handle on the same
1308
+ * path is the engine's `EnvironmentLocked`. There is no close anywhere:
1309
+ * the process owns the environment until GC/exit (durability is the
1310
+ * engine's per-commit fsync). Resume = reopen in a fresh process, or
1311
+ * hold the `Db` this process opened.
901
1312
  */
902
1313
  const Db = Object.freeze({
903
1314
  /** Creates a fresh durable store at `path` from the schema. */
904
- async create(path, theory) {
905
- return admit("create", path, theory);
1315
+ async create(storePath, theory) {
1316
+ return createStore(storePath, theory);
906
1317
  },
907
1318
  /**
908
1319
  * Opens an existing durable store at `path` with the same theory.
909
- * A fingerprint-matching open also BACK-FILLS the store's persisted
910
- * schema descriptor when it is absent (self-describing stores, engine
911
- * 50-storage.md § the `_meta` block), so a legacy store becomes
912
- * exhumable after one ordinary open — adoption is automatic, never a
913
- * separate verb. A second open of a still-live path is
914
- * `EnvironmentLocked`.
1320
+ * Format 8 open never back-fills a descriptor. A second open of a
1321
+ * still-live path is `EnvironmentLocked`.
915
1322
  */
916
- async open(path, theory) {
917
- return admit("open", path, theory);
1323
+ async open(storePath, theory) {
1324
+ return openStore(storePath, theory);
918
1325
  },
919
- /**
920
- * Opens a store READ-ONLY from its own persisted descriptor — the SDK's
921
- * one schema-independent read path (no theory, no fingerprint check; the
922
- * store rebirth tool's entry). Lives beside `open`/`create` so the path
923
- * law stays in one place: the same `node:path.resolve` canonicalization,
924
- * applied here. The value is NOT cached and is a DISPOSABLE lifetime
925
- * (R12) — `using exhumed = await Db.exhume(path)` releases the engine
926
- * handle and the store's exclusive lock at scope exit, so a same-path
927
- * reopen never waits on GC. A store not yet adopted rejects with the
928
- * typed `ErrExhumeNoDescriptor` (the remedy: one fingerprint-matching
929
- * `Db.open` under the creating schema back-fills the descriptor).
930
- */
931
- async exhume(storePath) {
932
- return exhumeStore(path.resolve(storePath));
1326
+ async fromInstance(storePath, instance) {
1327
+ const rec = ownedRecords.get(instance);
1328
+ if (rec === undefined) {
1329
+ throw errors.wrap(ErrSpentHandle, "bumbledb fromInstance target is not an owned instance of this SDK");
1330
+ }
1331
+ if (rec.spent) {
1332
+ throw errors.wrap(ErrSpentHandle, "bumbledb fromInstance target has been disposed");
1333
+ }
1334
+ const canonical = path.resolve(storePath);
1335
+ const dbHandle = await bridgedAsync(`publish bumbledb instance at ${canonical}`, function publish() {
1336
+ return native.dbFromInstance(canonical, rec.handle);
1337
+ });
1338
+ return openFromHandle(dbHandle, rec.theory);
933
1339
  }
934
1340
  });
935
- export { abandon, Db, ErrGenerationMoved, ErrNewtypeMismatch };
1341
+ export { abandon, Db, ErrAsyncCallback, ErrForeignPrepared, ErrForeignWitness, ErrFingerprintMismatch, ErrIrError, ErrNewtypeMismatch, ErrSchemaError, ErrSpentHandle, ErrUseAfterScope, InstanceBuilder };
936
1342
  //# sourceMappingURL=db.js.map