@bjornpagen/bumbledb 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/COOKBOOK.md +155 -136
- package/README.md +3 -7
- package/dist/capacity.d.ts +14 -1
- package/dist/capacity.d.ts.map +1 -1
- package/dist/capacity.js.map +1 -1
- package/dist/db.d.ts +77 -109
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +121 -339
- package/dist/db.js.map +1 -1
- package/dist/index.d.ts +11 -16
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -10
- package/dist/index.js.map +1 -1
- package/dist/lower.d.ts.map +1 -1
- package/dist/lower.js +8 -1
- package/dist/lower.js.map +1 -1
- package/dist/native.d.ts +69 -50
- package/dist/native.d.ts.map +1 -1
- package/dist/native.js.map +1 -1
- package/dist/query/atom.d.ts +162 -122
- package/dist/query/atom.d.ts.map +1 -1
- package/dist/query/atom.js +26 -22
- package/dist/query/atom.js.map +1 -1
- package/dist/query/find.d.ts +18 -35
- package/dist/query/find.d.ts.map +1 -1
- package/dist/query/find.js +13 -32
- package/dist/query/find.js.map +1 -1
- package/dist/query/lower.d.ts +113 -122
- package/dist/query/lower.d.ts.map +1 -1
- package/dist/query/lower.js +336 -260
- package/dist/query/lower.js.map +1 -1
- package/dist/query/parse-ir.d.ts +12 -0
- package/dist/query/parse-ir.d.ts.map +1 -0
- package/dist/query/parse-ir.js +71 -0
- package/dist/query/parse-ir.js.map +1 -0
- package/dist/query/run.d.ts +2 -2
- package/dist/query/run.d.ts.map +1 -1
- package/dist/query/run.js +2 -13
- package/dist/query/run.js.map +1 -1
- package/dist/query/scope.d.ts +4 -16
- package/dist/query/scope.d.ts.map +1 -1
- package/dist/query/scope.js +1 -6
- package/dist/query/scope.js.map +1 -1
- package/dist/schema.js +2 -2
- package/dist/schema.js.map +1 -1
- package/dist/statements.d.ts +15 -9
- package/dist/statements.d.ts.map +1 -1
- package/dist/statements.js +14 -9
- package/dist/statements.js.map +1 -1
- package/package.json +2 -2
- package/src/capacity.ts +24 -1
- package/src/db.ts +182 -443
- package/src/index.ts +9 -23
- package/src/lower.ts +8 -1
- package/src/native.ts +69 -42
- package/src/query/atom.ts +255 -165
- package/src/query/find.ts +39 -80
- package/src/query/lower.ts +578 -432
- package/src/query/parse-ir.ts +82 -0
- package/src/query/run.ts +2 -14
- package/src/query/scope.ts +3 -21
- package/src/schema.ts +2 -2
- package/src/statements.ts +33 -17
- package/dist/order.d.ts +0 -87
- package/dist/order.d.ts.map +0 -1
- package/dist/order.js +0 -153
- package/dist/order.js.map +0 -1
- package/dist/query/predicate.d.ts +0 -91
- package/dist/query/predicate.d.ts.map +0 -1
- package/dist/query/predicate.js +0 -156
- package/dist/query/predicate.js.map +0 -1
- package/src/order.ts +0 -234
- package/src/query/predicate.ts +0 -269
package/src/db.ts
CHANGED
|
@@ -2,35 +2,32 @@
|
|
|
2
2
|
* `Db` — the living half of the SDK (PRD-07): open/create a store from a
|
|
3
3
|
* `Schema`, write typed facts through delta transactions with race-free
|
|
4
4
|
* final-state point reads, receive rejections as typed violation VALUES
|
|
5
|
-
* keyed to statements, read through scoped snapshots
|
|
6
|
-
*
|
|
5
|
+
* keyed to statements, and read through scoped snapshots — all typed by
|
|
6
|
+
* the schema's relations record.
|
|
7
7
|
*
|
|
8
8
|
* LIFETIMES ARE DISPOSABLES, never `close()` (ruled 2026-07-23, R12): no
|
|
9
9
|
* value this module returns carries a close spelling — release is
|
|
10
|
-
* deterministic and scope-shaped in the language's own syntax.
|
|
11
|
-
* are
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* returns), and `using snap = db.read()` hands the caller the lifetime,
|
|
18
|
-
* released by the scope's own `Symbol.dispose` at scope exit. Prepared
|
|
19
|
-
* plans are plain values whose engine-side half is reclaimed by a GC
|
|
20
|
-
* finalizer — reclamation only, never correctness.
|
|
10
|
+
* deterministic and scope-shaped in the language's own syntax. Snapshots
|
|
11
|
+
* are scope-shaped both ways: `read(fn)` opens one before `fn` and closes
|
|
12
|
+
* it after unconditionally (the {@link ReadScope} handed to `fn` is
|
|
13
|
+
* invalidated the moment `fn` returns), and `using snap = db.read()` hands
|
|
14
|
+
* the caller the lifetime, released by the scope's own `Symbol.dispose`
|
|
15
|
+
* at scope exit. Prepared plans are plain values whose engine-side half
|
|
16
|
+
* is reclaimed by a GC finalizer — reclamation only, never correctness.
|
|
21
17
|
*
|
|
22
18
|
* PROCESS MODEL: one process, one exclusive-lock handle per store. The
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
19
|
+
* `Db` value owns the LMDB environment's exclusive lock until process
|
|
20
|
+
* exit (or until GC reclaims the native handle); a second engine-level
|
|
21
|
+
* open of the same store is refused by the engine (`EnvironmentLocked`),
|
|
22
|
+
* matching Rust. Resume = reopen in a fresh process, or hold the one `Db`
|
|
23
|
+
* this process already opened. The host owns composition; retry is host
|
|
24
|
+
* policy.
|
|
29
25
|
*
|
|
30
26
|
* REJECTION IS DATA: a rejected commit is a domain outcome (it becomes the
|
|
31
27
|
* LLM repair prompt downstream), returned as a {@link WriteResult} carrying
|
|
32
28
|
* {@link Violation} values. Genuine failures — I/O, used-after-scope,
|
|
33
|
-
* marshal shape
|
|
29
|
+
* marshal shape, a moved generation on {@link Db.writeFrom} — throw
|
|
30
|
+
* `@superbuilders/errors` wrapped errors instead.
|
|
34
31
|
*/
|
|
35
32
|
|
|
36
33
|
import * as path from "node:path"
|
|
@@ -54,13 +51,10 @@ import {
|
|
|
54
51
|
|
|
55
52
|
import type {
|
|
56
53
|
DbHandle,
|
|
57
|
-
Explain,
|
|
58
54
|
FactValue,
|
|
59
55
|
Manifest,
|
|
60
56
|
PreparedHandle,
|
|
61
57
|
SnapshotHandle,
|
|
62
|
-
Staleness,
|
|
63
|
-
StatementKindTag,
|
|
64
58
|
TxHandle,
|
|
65
59
|
Violation as WireViolation,
|
|
66
60
|
ViolationFact as WireViolationFact
|
|
@@ -127,15 +121,35 @@ interface OffendingFact<Rels extends SchemaRelations> {
|
|
|
127
121
|
* `source <= target` slot as the statement was spelled, `mirrored` the
|
|
128
122
|
* engine-materialized `target <= source` partner.
|
|
129
123
|
*/
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
124
|
+
type Violation<Rels extends SchemaRelations> =
|
|
125
|
+
| {
|
|
126
|
+
readonly kind: "functionality"
|
|
127
|
+
readonly statement?: Statement
|
|
128
|
+
readonly canonical: string
|
|
129
|
+
readonly facts: readonly OffendingFact<Rels>[]
|
|
130
|
+
}
|
|
131
|
+
| {
|
|
132
|
+
readonly kind: "containment"
|
|
133
|
+
readonly statement?: Statement
|
|
134
|
+
readonly canonical: string
|
|
135
|
+
readonly direction: "sourceUnsatisfied" | "targetRequired"
|
|
136
|
+
readonly facts: readonly OffendingFact<Rels>[]
|
|
137
|
+
}
|
|
138
|
+
| {
|
|
139
|
+
readonly kind: "containment"
|
|
140
|
+
readonly statement?: Statement
|
|
141
|
+
readonly canonical: string
|
|
142
|
+
readonly direction: "sourceUnsatisfied" | "targetRequired"
|
|
143
|
+
readonly orientation: "written" | "mirrored"
|
|
144
|
+
readonly facts: readonly OffendingFact<Rels>[]
|
|
145
|
+
}
|
|
146
|
+
| {
|
|
147
|
+
readonly kind: "capacity"
|
|
148
|
+
readonly statement?: Statement
|
|
149
|
+
readonly canonical: string
|
|
150
|
+
readonly measure: bigint
|
|
151
|
+
readonly facts: readonly OffendingFact<Rels>[]
|
|
152
|
+
}
|
|
139
153
|
|
|
140
154
|
/**
|
|
141
155
|
* The abandoned arm of a write result (ruled 2026-07-23, R10): present in
|
|
@@ -170,14 +184,14 @@ type DeltaBuild<Rels extends SchemaRelations, R = void> = (tx: Tx<Rels>) => R
|
|
|
170
184
|
|
|
171
185
|
/**
|
|
172
186
|
* The runtime discriminant of {@link Abandon} values — a property probe is
|
|
173
|
-
* how `
|
|
187
|
+
* how `write`/`writeFrom` distinguish "abort without committing" from an
|
|
174
188
|
* ordinary callback result, never a guess about the host's own value shapes.
|
|
175
189
|
*/
|
|
176
190
|
const abandonMark: unique symbol = Symbol("bumbledb.abandon")
|
|
177
191
|
|
|
178
192
|
/**
|
|
179
193
|
* The abandon sentinel {@link abandon} builds: returning one from a `write`
|
|
180
|
-
* or `
|
|
194
|
+
* or `writeFrom` callback rolls the transaction back WITHOUT
|
|
181
195
|
* committing (no empty commit is ever issued) and surfaces the payload as
|
|
182
196
|
* `{ ok: false, abandoned: payload }` (ruled 2026-07-23, R10 — the
|
|
183
197
|
* sentinel's contract is unconditional, whichever write verb received it).
|
|
@@ -191,7 +205,7 @@ interface Abandon<P> {
|
|
|
191
205
|
* Wraps a payload in the {@link Abandon} sentinel — the one way a write
|
|
192
206
|
* callback declines to commit: `return abandon(payload)` aborts the delta
|
|
193
207
|
* (nothing is committed, not even an empty commit) and the write resolves
|
|
194
|
-
* to `{ ok: false, abandoned: payload }`, from `write` and `
|
|
208
|
+
* to `{ ok: false, abandoned: payload }`, from `write` and `writeFrom`
|
|
195
209
|
* alike (R10).
|
|
196
210
|
*/
|
|
197
211
|
function abandon<P>(payload: P): Abandon<P> {
|
|
@@ -246,7 +260,7 @@ function abandonedOutcome<Rels extends SchemaRelations, R>(
|
|
|
246
260
|
* One live write transaction: the submitted delta with the engine's
|
|
247
261
|
* FINAL-STATE point-read view (base + pending delta — the exact state the
|
|
248
262
|
* commit judgment judges, so check-then-act is race-free by construction).
|
|
249
|
-
* Spent when its owning `write`/`
|
|
263
|
+
* Spent when its owning `write`/`writeFrom` call resolves the attempt;
|
|
250
264
|
* any later use throws.
|
|
251
265
|
*/
|
|
252
266
|
interface Tx<Rels extends SchemaRelations> {
|
|
@@ -328,16 +342,6 @@ interface ReadScope<Rels extends SchemaRelations> extends Disposable {
|
|
|
328
342
|
* execution spelling ({@link Prepared} carries no `execute`).
|
|
329
343
|
*/
|
|
330
344
|
execute<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Row[]
|
|
331
|
-
/**
|
|
332
|
-
* Plan introspection as data (ruled 2026-07-23, R13): runs the prepared
|
|
333
|
-
* query against this scope's snapshot with counting instrumentation
|
|
334
|
-
* (the engine's ANALYZE semantics) and returns the structured
|
|
335
|
-
* {@link Explain} report — plan sections and counters as plain values,
|
|
336
|
-
* so the host reads what the engine did with its query without a
|
|
337
|
-
* second toolchain. A diagnostic surface, EXPLICITLY UNFROZEN: the
|
|
338
|
-
* shape follows the plan representation wherever it goes.
|
|
339
|
-
*/
|
|
340
|
-
explain<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Explain
|
|
341
345
|
}
|
|
342
346
|
|
|
343
347
|
/**
|
|
@@ -361,21 +365,15 @@ const preparedTypes: unique symbol = Symbol("bumbledb.prepared.types")
|
|
|
361
365
|
* everything).
|
|
362
366
|
*/
|
|
363
367
|
interface Prepared<Rels extends SchemaRelations, Row, Params extends ParamsRecord> {
|
|
364
|
-
|
|
365
|
-
* The pull-based plan-drift report against a read scope's snapshot —
|
|
366
|
-
* engine-policy-free: no threshold exists engine-side; the host owns
|
|
367
|
-
* re-prepare.
|
|
368
|
-
*/
|
|
369
|
-
staleness(snap: ReadScope<Rels>): Staleness
|
|
370
|
-
readonly [preparedTypes]?: { readonly row: Row; readonly params: Params }
|
|
368
|
+
readonly [preparedTypes]?: { readonly rels: Rels; readonly row: Row; readonly params: Params }
|
|
371
369
|
}
|
|
372
370
|
|
|
373
371
|
/**
|
|
374
|
-
* An open store
|
|
375
|
-
*
|
|
376
|
-
*
|
|
377
|
-
*
|
|
378
|
-
*
|
|
372
|
+
* An open store. There is no close: read through `read`/the read sugar,
|
|
373
|
+
* write through `write`/`writeFrom`, and let the process own the
|
|
374
|
+
* environment's lifetime (the engine fsyncs every commit, so durability
|
|
375
|
+
* never waits on a close). A second `open`/`create` of the same path
|
|
376
|
+
* while this handle lives is the engine's `EnvironmentLocked`.
|
|
379
377
|
*/
|
|
380
378
|
interface Db<Rels extends SchemaRelations> {
|
|
381
379
|
/** The theory this store was opened with (fingerprint-verified by the engine). */
|
|
@@ -409,8 +407,6 @@ interface Db<Rels extends SchemaRelations> {
|
|
|
409
407
|
contains<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean
|
|
410
408
|
/** `db.execute(p, params)` === `db.read(snap => snap.execute(p, params))` — the symmetry rule. */
|
|
411
409
|
execute<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Row[]
|
|
412
|
-
/** `db.explain(p, params)` === `db.read(snap => snap.explain(p, params))` — the symmetry rule (R13). */
|
|
413
|
-
explain<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Explain
|
|
414
410
|
/**
|
|
415
411
|
* One delta transaction: builds the delta synchronously through `fn`,
|
|
416
412
|
* commits, and returns the domain outcome. A throw from `fn` aborts
|
|
@@ -422,29 +418,19 @@ interface Db<Rels extends SchemaRelations> {
|
|
|
422
418
|
*/
|
|
423
419
|
write<R = void>(fn: DeltaBuild<Rels, R>): WriteResult<Rels, R>
|
|
424
420
|
/**
|
|
425
|
-
*
|
|
426
|
-
*
|
|
427
|
-
*
|
|
428
|
-
*
|
|
429
|
-
*
|
|
430
|
-
*
|
|
431
|
-
* witnesses a strictly newer generation — the benign race converges.
|
|
432
|
-
* The loop's honesty bound is {@link WITNESSED_ATTEMPT_CAP}: a callback
|
|
433
|
-
* that moves the generation on EVERY attempt (a plain `db.write` before
|
|
434
|
-
* its first tx verb) would spin forever, so past the cap the typed
|
|
435
|
-
* {@link ErrWitnessedLivelock} is thrown instead of a silent loop (the
|
|
436
|
-
* engine's ruling: the error, never a loop — retry is host policy, and
|
|
437
|
-
* the cap is that policy's own diagnostic). `fn` may decline to commit
|
|
438
|
-
* by returning {@link abandon}`(payload)` — the outcome is then
|
|
439
|
-
* `{ ok: false, abandoned: payload }` and NO commit (not even an empty
|
|
440
|
-
* one) is issued.
|
|
421
|
+
* One-shot witnessed write from a live read snapshot: begins a
|
|
422
|
+
* transaction that commits only if no state-changing commit landed
|
|
423
|
+
* since `snap` was taken. Must run inside the read callback that owns
|
|
424
|
+
* `snap`. A moved generation is the typed {@link ErrGenerationMoved}
|
|
425
|
+
* — retry is host policy; this method never loops. `fn` may decline to
|
|
426
|
+
* commit by returning {@link abandon}`(payload)`.
|
|
441
427
|
*/
|
|
442
|
-
|
|
428
|
+
writeFrom<R>(snap: ReadScope<Rels>, fn: DeltaBuild<Rels, R>): WriteResult<Rels, R>
|
|
443
429
|
/**
|
|
444
430
|
* Prepares a query value built against THIS schema (identity is the
|
|
445
431
|
* membership rule): lowers it to the engine IR, pins the plan, and
|
|
446
432
|
* returns the typed {@link Prepared} value. Every IR roster refusal —
|
|
447
|
-
* rule caps,
|
|
433
|
+
* rule caps, rec roster, type rules — is the ENGINE's typed
|
|
448
434
|
* judgment and throws here carrying its message intact.
|
|
449
435
|
*/
|
|
450
436
|
prepare<Row, Params extends ParamsRecord>(q: Query<Rels, Row, Params>): Prepared<Rels, Row, Params>
|
|
@@ -470,18 +456,16 @@ interface PrimaryKey {
|
|
|
470
456
|
* engine-materialized implied keys), and — for functionality forms — the
|
|
471
457
|
* key's owner and projection (what keyed point reads resolve through).
|
|
472
458
|
*/
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
readonly reversed?: boolean
|
|
484
|
-
}
|
|
459
|
+
type StatementEntry =
|
|
460
|
+
| {
|
|
461
|
+
readonly kind: "functionality"
|
|
462
|
+
readonly statement?: Statement
|
|
463
|
+
readonly owner: string
|
|
464
|
+
readonly projection: readonly string[]
|
|
465
|
+
}
|
|
466
|
+
| { readonly kind: "containment"; readonly statement: Statement }
|
|
467
|
+
| { readonly kind: "mirrors"; readonly statement: Statement; readonly orientation: "written" | "mirrored" }
|
|
468
|
+
| { readonly kind: "capacity"; readonly statement: Statement }
|
|
485
469
|
|
|
486
470
|
/**
|
|
487
471
|
* Mirrors the engine's materialized statement order
|
|
@@ -519,8 +503,8 @@ function impliedKeyEntries(theory: AnySchema): StatementEntry[] {
|
|
|
519
503
|
if (isFreshField(declared.field)) {
|
|
520
504
|
entries.push({
|
|
521
505
|
kind: "functionality",
|
|
522
|
-
|
|
523
|
-
|
|
506
|
+
owner: member.name,
|
|
507
|
+
projection: [declared.name]
|
|
524
508
|
})
|
|
525
509
|
}
|
|
526
510
|
}
|
|
@@ -529,8 +513,8 @@ function impliedKeyEntries(theory: AnySchema): StatementEntry[] {
|
|
|
529
513
|
if (isClosedMember(member)) {
|
|
530
514
|
entries.push({
|
|
531
515
|
kind: "functionality",
|
|
532
|
-
|
|
533
|
-
|
|
516
|
+
owner: member.name,
|
|
517
|
+
projection: ["id"]
|
|
534
518
|
})
|
|
535
519
|
}
|
|
536
520
|
}
|
|
@@ -551,21 +535,22 @@ function declaredEntries(statement: Statement): StatementEntry[] {
|
|
|
551
535
|
{
|
|
552
536
|
kind: "functionality",
|
|
553
537
|
statement,
|
|
554
|
-
|
|
538
|
+
owner: data.owner.name,
|
|
539
|
+
projection: data.projection
|
|
555
540
|
}
|
|
556
541
|
]
|
|
557
542
|
}
|
|
558
543
|
case "containment": {
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
544
|
+
return [{ kind: "containment", statement }]
|
|
545
|
+
}
|
|
546
|
+
case "mirrors": {
|
|
547
|
+
return [
|
|
548
|
+
{ kind: "mirrors", statement, orientation: "written" },
|
|
549
|
+
{ kind: "mirrors", statement, orientation: "mirrored" }
|
|
550
|
+
]
|
|
566
551
|
}
|
|
567
552
|
case "capacity": {
|
|
568
|
-
return [{ kind: "capacity", statement
|
|
553
|
+
return [{ kind: "capacity", statement }]
|
|
569
554
|
}
|
|
570
555
|
}
|
|
571
556
|
}
|
|
@@ -621,17 +606,6 @@ function selectKeyRead<R extends AnyRelation, P extends readonly string[], T>(
|
|
|
621
606
|
return byPrimary(keyOrStatement)
|
|
622
607
|
}
|
|
623
608
|
|
|
624
|
-
/** Maps a slot's reversal flag to the violation's `orientation` payload. */
|
|
625
|
-
function orientationOf(reversed: boolean | undefined): "written" | "mirrored" | undefined {
|
|
626
|
-
if (reversed === undefined) {
|
|
627
|
-
return undefined
|
|
628
|
-
}
|
|
629
|
-
if (reversed) {
|
|
630
|
-
return "mirrored"
|
|
631
|
-
}
|
|
632
|
-
return "written"
|
|
633
|
-
}
|
|
634
|
-
|
|
635
609
|
/** The id-resolution tables one open builds: relation entries by name, statement slots by id. */
|
|
636
610
|
interface Tables {
|
|
637
611
|
readonly relations: ReadonlyMap<string, RelationEntry>
|
|
@@ -657,11 +631,17 @@ function tablesOf(theory: AnySchema, manifest: Manifest): Tables {
|
|
|
657
631
|
}
|
|
658
632
|
manifest.statements.forEach(function verifySlot(statement, index) {
|
|
659
633
|
const entry = entries[index]
|
|
660
|
-
if (entry === undefined || statement.id !== index
|
|
634
|
+
if (entry === undefined || statement.id !== index) {
|
|
661
635
|
throw errors.new(
|
|
662
636
|
`bumbledb manifest drift: statement ${statement.id} is ${statement.kind}, the SDK mirror at ${index} expected ${entry?.kind}`
|
|
663
637
|
)
|
|
664
638
|
}
|
|
639
|
+
const engineKind = entry.kind === "mirrors" ? "containment" : entry.kind
|
|
640
|
+
if (engineKind !== statement.kind) {
|
|
641
|
+
throw errors.new(
|
|
642
|
+
`bumbledb manifest drift: statement ${statement.id} is ${statement.kind}, the SDK mirror at ${index} expected ${engineKind}`
|
|
643
|
+
)
|
|
644
|
+
}
|
|
665
645
|
})
|
|
666
646
|
const relations = new Map<string, RelationEntry>()
|
|
667
647
|
for (const relation of manifest.relations) {
|
|
@@ -682,8 +662,8 @@ function tablesOf(theory: AnySchema, manifest: Manifest): Tables {
|
|
|
682
662
|
})
|
|
683
663
|
let primaryKey: PrimaryKey | undefined
|
|
684
664
|
entries.forEach(function firstOwnedKey(entry, index) {
|
|
685
|
-
if (primaryKey === undefined && entry.
|
|
686
|
-
primaryKey = Object.freeze({ statementId: index, projection: entry.
|
|
665
|
+
if (primaryKey === undefined && entry.kind === "functionality" && entry.owner === relation.name) {
|
|
666
|
+
primaryKey = Object.freeze({ statementId: index, projection: entry.projection })
|
|
687
667
|
}
|
|
688
668
|
})
|
|
689
669
|
relations.set(relation.name, Object.freeze({ id: relation.id, member, fieldIds, primaryKey }))
|
|
@@ -712,7 +692,7 @@ interface PointReads {
|
|
|
712
692
|
* One read scope's PRIVATE lifetime record: its live snapshot handle, the
|
|
713
693
|
* generation it witnessed (carried by the snapshot open itself — one
|
|
714
694
|
* crossing, finding 016), its liveness flag (flipped when the owning
|
|
715
|
-
* `read
|
|
695
|
+
* `read` callback returns, or by the scope's own
|
|
716
696
|
* `Symbol.dispose`), its close latch (`closed` — the snapshot closes
|
|
717
697
|
* exactly once, whichever of the owner and the dispose gets there first),
|
|
718
698
|
* and its owning store's identity token. Held in {@link scopeStates} —
|
|
@@ -761,37 +741,12 @@ const planReclaimer = new FinalizationRegistry<PreparedHandle>(function reclaimP
|
|
|
761
741
|
})
|
|
762
742
|
|
|
763
743
|
/**
|
|
764
|
-
* The
|
|
765
|
-
*
|
|
766
|
-
*
|
|
767
|
-
* callback on a fresh snapshot. It never escapes the SDK.
|
|
744
|
+
* The typed generation-moved refusal `writeFrom` throws when a
|
|
745
|
+
* state-changing commit landed since the witness snapshot: retry is host
|
|
746
|
+
* policy. Match with `errors.is`.
|
|
768
747
|
*/
|
|
769
|
-
const
|
|
770
|
-
|
|
771
|
-
/**
|
|
772
|
-
* The witnessed loop's attempt cap — a generous power of two. Benign
|
|
773
|
-
* self-inflicted contention (the host's own commits landing between an
|
|
774
|
-
* attempt's snapshot and its witnessed begin) converges in a handful of
|
|
775
|
-
* retries because each rerun reads a FRESHER snapshot; a workload that moves
|
|
776
|
-
* the generation on EVERY one of this many consecutive attempts is not
|
|
777
|
-
* converging and never will (see {@link ErrWitnessedLivelock}).
|
|
778
|
-
*/
|
|
779
|
-
const WITNESSED_ATTEMPT_CAP = 64
|
|
780
|
-
|
|
781
|
-
/**
|
|
782
|
-
* The typed livelock refusal `writeWitnessed` throws past
|
|
783
|
-
* {@link WITNESSED_ATTEMPT_CAP} attempts: every attempt found the generation
|
|
784
|
-
* moved, which is only sustainable when the callback ITSELF (even
|
|
785
|
-
* indirectly) issues an interleaved plain `db.write` before its first tx
|
|
786
|
-
* verb on every attempt — each rerun then re-moves the generation it is
|
|
787
|
-
* about to witness, forever. That is host-policy pathology, not engine
|
|
788
|
-
* judgment (the engine ships the error, never a loop), so it THROWS rather
|
|
789
|
-
* than returning a result arm. Match with `errors.is`; the remedy is to
|
|
790
|
-
* move the interleaved write out of the callback (or make it first-attempt
|
|
791
|
-
* only — the delta belongs on `tx`, premise reads on `snap`).
|
|
792
|
-
*/
|
|
793
|
-
const ErrWitnessedLivelock = errors.new(
|
|
794
|
-
"bumbledb writeWitnessed livelock: the generation moved on every attempt — the callback itself commits an interleaved write each try, so no snapshot can ever stay current"
|
|
748
|
+
const ErrGenerationMoved = errors.new(
|
|
749
|
+
"bumbledb generationMoved: a state-changing commit landed since the witness snapshot"
|
|
795
750
|
)
|
|
796
751
|
|
|
797
752
|
/**
|
|
@@ -885,15 +840,32 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
885
840
|
if (entry === undefined) {
|
|
886
841
|
throw errors.new(`bumbledb violation cites unknown statement id ${wire.statementId}`)
|
|
887
842
|
}
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
843
|
+
const facts = Object.freeze(wire.facts.map(offendingFactOf))
|
|
844
|
+
const statement = entry.statement
|
|
845
|
+
const canonical = wire.canonical
|
|
846
|
+
if (entry.kind === "functionality") {
|
|
847
|
+
return Object.freeze({ kind: "functionality", statement, canonical, facts })
|
|
848
|
+
}
|
|
849
|
+
if (entry.kind === "capacity") {
|
|
850
|
+
if (wire.kind !== "capacity") {
|
|
851
|
+
throw errors.new(`bumbledb violation ${wire.statementId} is a capacity slot without a measure`)
|
|
852
|
+
}
|
|
853
|
+
return Object.freeze({ kind: "capacity", statement, canonical, measure: wire.measure, facts })
|
|
854
|
+
}
|
|
855
|
+
if (wire.kind !== "containment") {
|
|
856
|
+
throw errors.new(`bumbledb violation ${wire.statementId} is a containment slot without a direction`)
|
|
857
|
+
}
|
|
858
|
+
if (entry.kind === "mirrors") {
|
|
859
|
+
return Object.freeze({
|
|
860
|
+
kind: "containment",
|
|
861
|
+
statement,
|
|
862
|
+
canonical,
|
|
863
|
+
direction: wire.direction,
|
|
864
|
+
orientation: entry.orientation,
|
|
865
|
+
facts
|
|
866
|
+
})
|
|
867
|
+
}
|
|
868
|
+
return Object.freeze({ kind: "containment", statement, canonical, direction: wire.direction, facts })
|
|
897
869
|
}
|
|
898
870
|
|
|
899
871
|
/**
|
|
@@ -913,15 +885,15 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
913
885
|
`keyed get statement is not a declared statement of schema ${theory.name} — statement identity is the membership rule`
|
|
914
886
|
)
|
|
915
887
|
}
|
|
916
|
-
if (entry.kind !== "functionality"
|
|
888
|
+
if (entry.kind !== "functionality") {
|
|
917
889
|
throw errors.new("keyed get takes a key() statement — containments and capacity statements key nothing")
|
|
918
890
|
}
|
|
919
|
-
if (entry.
|
|
891
|
+
if (entry.owner !== relation.name) {
|
|
920
892
|
throw errors.new(
|
|
921
|
-
`keyed get statement keys ${entry.
|
|
893
|
+
`keyed get statement keys ${entry.owner}, not ${relation.name} — the statement must be a declared key of the relation it reads`
|
|
922
894
|
)
|
|
923
895
|
}
|
|
924
|
-
return Object.freeze({ statementId, projection: entry.
|
|
896
|
+
return Object.freeze({ statementId, projection: entry.projection })
|
|
925
897
|
}
|
|
926
898
|
|
|
927
899
|
function pointReadsOf(assertLive: () => void, reads: PointReads) {
|
|
@@ -1037,14 +1009,6 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
1037
1009
|
})
|
|
1038
1010
|
return decodeAnswers<Row>(plan.finds, rows)
|
|
1039
1011
|
}
|
|
1040
|
-
function explain<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Explain {
|
|
1041
|
-
assertLive()
|
|
1042
|
-
const plan = planOf(prepared)
|
|
1043
|
-
const wire = wireParams(plan.params, recordOf(params))
|
|
1044
|
-
return bridged("explain bumbledb prepared query", function callExplain() {
|
|
1045
|
-
return native.preparedExplain(plan.handle, state.handle, wire)
|
|
1046
|
-
})
|
|
1047
|
-
}
|
|
1048
1012
|
/** The R12 teardown: invalidate, then close — idempotent through the state's close latch. */
|
|
1049
1013
|
function dispose(): void {
|
|
1050
1014
|
state.live = false
|
|
@@ -1056,7 +1020,6 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
1056
1020
|
get: reads.get,
|
|
1057
1021
|
contains: reads.contains,
|
|
1058
1022
|
execute,
|
|
1059
|
-
explain,
|
|
1060
1023
|
[Symbol.dispose]: dispose
|
|
1061
1024
|
})
|
|
1062
1025
|
scopeStates.set(scope, state)
|
|
@@ -1171,18 +1134,9 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
1171
1134
|
})
|
|
1172
1135
|
}
|
|
1173
1136
|
|
|
1174
|
-
function explain<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Explain {
|
|
1175
|
-
return read(function explainInScope(snap) {
|
|
1176
|
-
return snap.explain(prepared, params)
|
|
1177
|
-
})
|
|
1178
|
-
}
|
|
1179
|
-
|
|
1180
1137
|
/**
|
|
1181
|
-
* Builds one {@link Tx} over a transaction-handle thunk: `write`
|
|
1182
|
-
* an already-begun handle
|
|
1183
|
-
* begins the witnessed transaction on the first delta verb (so premise
|
|
1184
|
-
* reads and the host's own interleaved writes can precede it) and
|
|
1185
|
-
* throws {@link generationMovedSignal} when the witness is stale.
|
|
1138
|
+
* Builds one {@link Tx} over a transaction-handle thunk: `write` and
|
|
1139
|
+
* `writeFrom` pass an already-begun handle.
|
|
1186
1140
|
*/
|
|
1187
1141
|
function makeTx(resolveTx: () => TxHandle): { readonly tx: Tx<Rels>; spend(): void } {
|
|
1188
1142
|
const txState = { spent: false }
|
|
@@ -1326,159 +1280,31 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
1326
1280
|
}
|
|
1327
1281
|
|
|
1328
1282
|
/**
|
|
1329
|
-
*
|
|
1330
|
-
*
|
|
1331
|
-
*
|
|
1283
|
+
* One-shot write from a live snapshot this store owns. Begins immediately;
|
|
1284
|
+
* a moved generation throws {@link ErrGenerationMoved} and `fn` never
|
|
1285
|
+
* runs. The snapshot stays owned by the caller's read callback.
|
|
1332
1286
|
*/
|
|
1333
|
-
function
|
|
1334
|
-
const
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
})
|
|
1338
|
-
})
|
|
1339
|
-
if (committed.error) {
|
|
1340
|
-
/** Same one-writer law as `runDelta`: a thrown commit aborts before rethrowing. */
|
|
1341
|
-
const aborted = errors.trySync(function abortAfterFailedCommit() {
|
|
1342
|
-
native.txAbort(txHandle)
|
|
1343
|
-
})
|
|
1344
|
-
if (aborted.error) {
|
|
1345
|
-
}
|
|
1346
|
-
closeScopeState(state)
|
|
1347
|
-
throw errors.wrap(committed.error, "commit bumbledb witnessed write transaction")
|
|
1287
|
+
function writeFrom<R>(snap: ReadScope<Rels>, fn: DeltaBuild<Rels, R>): WriteResult<Rels, R> {
|
|
1288
|
+
const snapState = scopeStates.get(snap)
|
|
1289
|
+
if (snapState === undefined) {
|
|
1290
|
+
throw errors.new("bumbledb writeFrom witness is not a read scope of this SDK")
|
|
1348
1291
|
}
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
if (outcome.ok) {
|
|
1352
|
-
return Object.freeze({ ok: true, generation: outcome.generation })
|
|
1292
|
+
if (snapState.owner !== owner) {
|
|
1293
|
+
throw errors.new(`bumbledb writeFrom snapshot belongs to a different store (schema ${theory.name})`)
|
|
1353
1294
|
}
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
violations: Object.freeze(outcome.violations.map(violationOf))
|
|
1357
|
-
})
|
|
1358
|
-
}
|
|
1359
|
-
|
|
1360
|
-
/**
|
|
1361
|
-
* One attempt of the witnessed loop: fresh snapshot, the callback over
|
|
1362
|
-
* its scope and a LAZILY-begun witnessed transaction (the first delta
|
|
1363
|
-
* verb begins it, so premise reads and the host's own interleaved
|
|
1364
|
-
* writes can precede the witness check), then the witnessed commit —
|
|
1365
|
-
* or the abandon abort, which never issues a commit. Returns
|
|
1366
|
-
* `undefined` exactly when the generation moved and the whole callback
|
|
1367
|
-
* must rerun on a fresh snapshot.
|
|
1368
|
-
*/
|
|
1369
|
-
function witnessedAttempt<R>(fn: (snap: ReadScope<Rels>, tx: Tx<Rels>) => R): WriteResult<Rels, R> | undefined {
|
|
1370
|
-
const state = openScopeState()
|
|
1371
|
-
const scope = makeScope(state)
|
|
1372
|
-
const pending: { tx: TxHandle | undefined } = { tx: undefined }
|
|
1373
|
-
function beginWitnessed(): TxHandle | undefined {
|
|
1374
|
-
const witnessed = bridged("begin witnessed bumbledb write transaction", function begin() {
|
|
1375
|
-
return native.dbWriteFrom(handle, state.handle)
|
|
1376
|
-
})
|
|
1377
|
-
if (!witnessed.ok) {
|
|
1378
|
-
return undefined
|
|
1379
|
-
}
|
|
1380
|
-
return witnessed.tx
|
|
1295
|
+
if (!snapState.live) {
|
|
1296
|
+
throw errors.new("bumbledb writeFrom snapshot is invalidated — its owning read callback already returned")
|
|
1381
1297
|
}
|
|
1382
|
-
const
|
|
1383
|
-
|
|
1384
|
-
const begun = beginWitnessed()
|
|
1385
|
-
if (begun === undefined) {
|
|
1386
|
-
throw generationMovedSignal
|
|
1387
|
-
}
|
|
1388
|
-
pending.tx = begun
|
|
1389
|
-
}
|
|
1390
|
-
return pending.tx
|
|
1391
|
-
})
|
|
1392
|
-
const built = errors.trySync(function computeWitnessed() {
|
|
1393
|
-
return fn(scope, made.tx)
|
|
1298
|
+
const witnessed = bridged("begin witnessed bumbledb write transaction", function begin() {
|
|
1299
|
+
return native.dbWriteFrom(handle, snapState.handle)
|
|
1394
1300
|
})
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
* still closes the attempt's snapshot BEFORE rethrowing — every
|
|
1400
|
-
* openScopeState is paired with closeScopeState on every exit, or a
|
|
1401
|
-
* reader slot and its snapshot worker leak for the process's lifetime.
|
|
1402
|
-
*/
|
|
1403
|
-
function abortPending(): void {
|
|
1404
|
-
const txHandle = pending.tx
|
|
1405
|
-
if (txHandle === undefined) {
|
|
1406
|
-
return
|
|
1407
|
-
}
|
|
1408
|
-
const aborted = errors.trySync(function abort() {
|
|
1409
|
-
native.txAbort(txHandle)
|
|
1410
|
-
})
|
|
1411
|
-
if (aborted.error) {
|
|
1412
|
-
closeScopeState(state)
|
|
1413
|
-
throw errors.wrap(aborted.error, "abort bumbledb witnessed write transaction")
|
|
1414
|
-
}
|
|
1415
|
-
}
|
|
1416
|
-
if (built.error) {
|
|
1417
|
-
abortPending()
|
|
1418
|
-
closeScopeState(state)
|
|
1419
|
-
if (errors.is(built.error, generationMovedSignal)) {
|
|
1420
|
-
return undefined
|
|
1421
|
-
}
|
|
1422
|
-
throw errors.wrap(built.error, "build witnessed write delta")
|
|
1423
|
-
}
|
|
1424
|
-
if (isThenable(built.data)) {
|
|
1425
|
-
/** The same async-callback refusal as `runDelta` — a thenable means the real delta build races the commit; nothing is committed. */
|
|
1426
|
-
abortPending()
|
|
1427
|
-
closeScopeState(state)
|
|
1428
|
-
throw errors.new(
|
|
1429
|
-
"bumbledb writeWitnessed callback returned a thenable — the delta build is synchronous; an async callback is refused, nothing was committed"
|
|
1301
|
+
if (!witnessed.ok) {
|
|
1302
|
+
throw errors.wrap(
|
|
1303
|
+
ErrGenerationMoved,
|
|
1304
|
+
`writeFrom: generation moved (witnessed ${witnessed.witnessed} current ${witnessed.current}) against schema ${theory.name}`
|
|
1430
1305
|
)
|
|
1431
1306
|
}
|
|
1432
|
-
|
|
1433
|
-
abortPending()
|
|
1434
|
-
closeScopeState(state)
|
|
1435
|
-
return abandonedOutcome<Rels, R>(built.data)
|
|
1436
|
-
}
|
|
1437
|
-
const late = errors.trySync(function resolveCommitTx() {
|
|
1438
|
-
if (pending.tx === undefined) {
|
|
1439
|
-
return beginWitnessed()
|
|
1440
|
-
}
|
|
1441
|
-
return pending.tx
|
|
1442
|
-
})
|
|
1443
|
-
if (late.error) {
|
|
1444
|
-
/** A faulted late begin must not leak the attempt's snapshot either. */
|
|
1445
|
-
closeScopeState(state)
|
|
1446
|
-
throw late.error
|
|
1447
|
-
}
|
|
1448
|
-
const txHandle = late.data
|
|
1449
|
-
if (txHandle === undefined) {
|
|
1450
|
-
closeScopeState(state)
|
|
1451
|
-
return undefined
|
|
1452
|
-
}
|
|
1453
|
-
return commitWitnessed(state, txHandle)
|
|
1454
|
-
}
|
|
1455
|
-
|
|
1456
|
-
/**
|
|
1457
|
-
* The witnessed retry loop. What it retries: the benign race — the
|
|
1458
|
-
* host's OWN interleaved commit landing between an attempt's snapshot
|
|
1459
|
-
* and its witnessed begin (every writer shares this handle, so a move
|
|
1460
|
-
* is always self-inflicted) — by rerunning the WHOLE callback on a
|
|
1461
|
-
* fresh snapshot, which converges because each rerun witnesses a
|
|
1462
|
-
* strictly newer generation. What it refuses: the pathology where the
|
|
1463
|
-
* callback itself (even indirectly) issues a plain `db.write` before
|
|
1464
|
-
* its first tx verb, moving the generation on EVERY attempt — an
|
|
1465
|
-
* unbounded loop would spin forever with no diagnostic, so past
|
|
1466
|
-
* {@link WITNESSED_ATTEMPT_CAP} attempts the loop throws the typed
|
|
1467
|
-
* {@link ErrWitnessedLivelock} instead (the engine's ruling: the
|
|
1468
|
-
* error, never a loop — retry is host policy, and this is the host
|
|
1469
|
-
* policy's own honesty bound).
|
|
1470
|
-
*/
|
|
1471
|
-
function writeWitnessed<R>(fn: (snap: ReadScope<Rels>, tx: Tx<Rels>) => R): WriteResult<Rels, R> {
|
|
1472
|
-
for (let attempts = 0; attempts < WITNESSED_ATTEMPT_CAP; attempts += 1) {
|
|
1473
|
-
const attempt = witnessedAttempt(fn)
|
|
1474
|
-
if (attempt !== undefined) {
|
|
1475
|
-
return attempt
|
|
1476
|
-
}
|
|
1477
|
-
}
|
|
1478
|
-
throw errors.wrap(
|
|
1479
|
-
ErrWitnessedLivelock,
|
|
1480
|
-
`writeWitnessed livelock: the generation moved on all ${WITNESSED_ATTEMPT_CAP} attempts against schema ${theory.name}`
|
|
1481
|
-
)
|
|
1307
|
+
return runDelta(witnessed.tx, fn)
|
|
1482
1308
|
}
|
|
1483
1309
|
|
|
1484
1310
|
function prepare<Row, Params extends ParamsRecord>(q: Query<Rels, Row, Params>): Prepared<Rels, Row, Params> {
|
|
@@ -1487,32 +1313,15 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
1487
1313
|
`query was built against schema ${q.schema.name}, not the identical schema value this store opened with — schema identity is the membership rule`
|
|
1488
1314
|
)
|
|
1489
1315
|
}
|
|
1490
|
-
const
|
|
1491
|
-
const outcome = bridged("prepare bumbledb
|
|
1492
|
-
return native.dbPrepare(handle,
|
|
1316
|
+
const queryIr = lowerQuery(q)
|
|
1317
|
+
const outcome = bridged("prepare bumbledb query", function callPrepare() {
|
|
1318
|
+
return native.dbPrepare(handle, queryIr)
|
|
1493
1319
|
})
|
|
1494
1320
|
if (!outcome.ok) {
|
|
1495
1321
|
throw errors.new(`bumbledb ${outcome.kind} (prepare): ${outcome.message}`)
|
|
1496
1322
|
}
|
|
1497
1323
|
const preparedHandle = outcome.prepared
|
|
1498
|
-
|
|
1499
|
-
const snapState = scopeStates.get(snap)
|
|
1500
|
-
if (snapState === undefined) {
|
|
1501
|
-
throw errors.new("bumbledb staleness witness is not a read scope of this SDK")
|
|
1502
|
-
}
|
|
1503
|
-
if (snapState.owner !== owner) {
|
|
1504
|
-
throw errors.new(
|
|
1505
|
-
`bumbledb read scope belongs to a different store than this prepared value (schema ${theory.name})`
|
|
1506
|
-
)
|
|
1507
|
-
}
|
|
1508
|
-
if (!snapState.live) {
|
|
1509
|
-
throw errors.new("bumbledb read scope is invalidated — its owning read callback already returned")
|
|
1510
|
-
}
|
|
1511
|
-
return bridged("read bumbledb prepared staleness", function callStaleness() {
|
|
1512
|
-
return native.preparedStaleness(preparedHandle, snapState.handle)
|
|
1513
|
-
})
|
|
1514
|
-
}
|
|
1515
|
-
const prepared: Prepared<Rels, Row, Params> = Object.freeze({ staleness })
|
|
1324
|
+
const prepared: Prepared<Rels, Row, Params> = Object.freeze({})
|
|
1516
1325
|
preparedPlans.set(
|
|
1517
1326
|
prepared,
|
|
1518
1327
|
Object.freeze({
|
|
@@ -1533,63 +1342,12 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
1533
1342
|
get,
|
|
1534
1343
|
contains,
|
|
1535
1344
|
execute,
|
|
1536
|
-
explain,
|
|
1537
1345
|
write,
|
|
1538
|
-
|
|
1346
|
+
writeFrom,
|
|
1539
1347
|
prepare
|
|
1540
1348
|
})
|
|
1541
1349
|
}
|
|
1542
1350
|
|
|
1543
|
-
/**
|
|
1544
|
-
* One cached open store: the theory VALUE it was admitted with (identity is
|
|
1545
|
-
* the membership rule — the fingerprint check against a cached path is a
|
|
1546
|
-
* `===` on this), the `Db` value every same-path open returns, and the
|
|
1547
|
-
* environment handle the exit hook closes.
|
|
1548
|
-
*/
|
|
1549
|
-
interface CachedStore {
|
|
1550
|
-
readonly theory: AnySchema
|
|
1551
|
-
readonly db: unknown
|
|
1552
|
-
readonly handle: DbHandle
|
|
1553
|
-
}
|
|
1554
|
-
|
|
1555
|
-
/**
|
|
1556
|
-
* The per-process store cache, keyed by canonical path
|
|
1557
|
-
* (`node:path.resolve` — absolute and normalized). Symlink aliasing is
|
|
1558
|
-
* deliberately not resolved here: an aliased spelling misses the cache and
|
|
1559
|
-
* reaches the engine, whose exclusive lock refuses a second live handle on
|
|
1560
|
-
* the same store — the backstop that keeps "one store, one handle" true.
|
|
1561
|
-
*/
|
|
1562
|
-
const openStores = new Map<string, CachedStore>()
|
|
1563
|
-
|
|
1564
|
-
/**
|
|
1565
|
-
* The in-process fingerprint check and its typing proof in one probe:
|
|
1566
|
-
* theory identity (`===`) implies `Rels` identity, because a cache entry's
|
|
1567
|
-
* `db` was constructed from that very theory value — so a hit narrows the
|
|
1568
|
-
* entry's `db` to `Db<Rels>` with no assertion anywhere.
|
|
1569
|
-
*/
|
|
1570
|
-
function holdsTheory<Rels extends SchemaRelations>(
|
|
1571
|
-
entry: CachedStore,
|
|
1572
|
-
theory: Schema<Rels>
|
|
1573
|
-
): entry is CachedStore & { readonly db: Db<Rels> } {
|
|
1574
|
-
return entry.theory === theory
|
|
1575
|
-
}
|
|
1576
|
-
|
|
1577
|
-
/**
|
|
1578
|
-
* The best-effort exit hook: closes every cached environment so LMDB
|
|
1579
|
-
* releases its locks tidily on a clean exit. CORRECTNESS NEVER RESTS HERE —
|
|
1580
|
-
* the engine fsyncs every commit, so a process killed before (or during)
|
|
1581
|
-
* this hook loses nothing that was committed.
|
|
1582
|
-
*/
|
|
1583
|
-
process.once("exit", function closeCachedStores() {
|
|
1584
|
-
for (const cached of openStores.values()) {
|
|
1585
|
-
const closed = errors.trySync(function closeEnvironment() {
|
|
1586
|
-
native.dbClose(cached.handle)
|
|
1587
|
-
})
|
|
1588
|
-
if (closed.error) {
|
|
1589
|
-
}
|
|
1590
|
-
}
|
|
1591
|
-
})
|
|
1592
|
-
|
|
1593
1351
|
/**
|
|
1594
1352
|
* The engine twin of the schema-level class wall, as a matchable value
|
|
1595
1353
|
* (`errors.is`): the shared lowering rejected a spec whose statement pairs
|
|
@@ -1626,16 +1384,15 @@ function refuseShadowedChanged(theory: AnySchema): void {
|
|
|
1626
1384
|
}
|
|
1627
1385
|
|
|
1628
1386
|
/**
|
|
1629
|
-
* The one admission path both verbs share:
|
|
1630
|
-
*
|
|
1631
|
-
*
|
|
1632
|
-
* `
|
|
1633
|
-
*
|
|
1634
|
-
*
|
|
1635
|
-
*
|
|
1636
|
-
*
|
|
1637
|
-
*
|
|
1638
|
-
* the engine's message intact.
|
|
1387
|
+
* The one admission path both verbs share: lower the theory, run one
|
|
1388
|
+
* bridge call, and wrap the domain refusals — `schemaError` (spec
|
|
1389
|
+
* resolution + schema validation, every issue in one message),
|
|
1390
|
+
* `newtypeMismatch` (the coherence wall, {@link ErrNewtypeMismatch}),
|
|
1391
|
+
* and `fingerprintMismatch` (a different theory cannot open the store)
|
|
1392
|
+
* — into typed errors carrying the engine's message intact.
|
|
1393
|
+
* Environment failures (a second live writer on the same path, IO)
|
|
1394
|
+
* throw from the bridge; the engine's `EnvironmentLocked` message is
|
|
1395
|
+
* "another live handle holds this environment's lock".
|
|
1639
1396
|
*/
|
|
1640
1397
|
function admit<Rels extends SchemaRelations>(
|
|
1641
1398
|
verb: "create" | "open",
|
|
@@ -1644,20 +1401,6 @@ function admit<Rels extends SchemaRelations>(
|
|
|
1644
1401
|
): Db<Rels> {
|
|
1645
1402
|
refuseShadowedChanged(theory)
|
|
1646
1403
|
const canonical = path.resolve(storePath)
|
|
1647
|
-
const cached = openStores.get(canonical)
|
|
1648
|
-
if (cached !== undefined) {
|
|
1649
|
-
if (verb === "create") {
|
|
1650
|
-
throw errors.new(
|
|
1651
|
-
`create bumbledb store at ${canonical}: the store is already open in this process — create refuses an already-initialized directory`
|
|
1652
|
-
)
|
|
1653
|
-
}
|
|
1654
|
-
if (!holdsTheory(cached, theory)) {
|
|
1655
|
-
throw errors.new(
|
|
1656
|
-
`bumbledb fingerprintMismatch (open ${canonical}): the cached store was opened with schema ${cached.theory.name}, not this theory value — schema identity is the membership rule`
|
|
1657
|
-
)
|
|
1658
|
-
}
|
|
1659
|
-
return cached.db
|
|
1660
|
-
}
|
|
1661
1404
|
const spec = lower(theory)
|
|
1662
1405
|
const opened = bridged(`${verb} bumbledb store at ${canonical}`, function callBridge() {
|
|
1663
1406
|
if (verb === "create") {
|
|
@@ -1674,35 +1417,31 @@ function admit<Rels extends SchemaRelations>(
|
|
|
1674
1417
|
const manifest = bridged("fetch bumbledb manifest", function fetchManifest() {
|
|
1675
1418
|
return native.dbManifest(opened.db)
|
|
1676
1419
|
})
|
|
1677
|
-
|
|
1678
|
-
openStores.set(canonical, Object.freeze({ theory, db, handle: opened.db }))
|
|
1679
|
-
return db
|
|
1420
|
+
return openDb(opened.db, theory, manifest)
|
|
1680
1421
|
}
|
|
1681
1422
|
|
|
1682
1423
|
/**
|
|
1683
1424
|
* The store lifecycle — `Db.create(path, schema)` / `Db.open(path, schema)`.
|
|
1684
1425
|
* Create refuses an already-initialized directory; open verifies format
|
|
1685
|
-
* version, store kind, and the schema fingerprint.
|
|
1686
|
-
*
|
|
1687
|
-
*
|
|
1688
|
-
*
|
|
1689
|
-
*
|
|
1690
|
-
* hook closes them; durability is the engine's per-commit fsync). One
|
|
1691
|
-
* store kind exists: durable — resume = reopen, meaning this process's
|
|
1692
|
-
* cached value or a fresh process's open.
|
|
1426
|
+
* version, store kind, and the schema fingerprint. A second live handle
|
|
1427
|
+
* on the same path is the engine's `EnvironmentLocked`. There is no close
|
|
1428
|
+
* anywhere: the process owns the environment until GC/exit (durability is
|
|
1429
|
+
* the engine's per-commit fsync). One store kind exists: durable —
|
|
1430
|
+
* resume = reopen in a fresh process, or hold the `Db` this process opened.
|
|
1693
1431
|
*/
|
|
1694
1432
|
const Db = Object.freeze({
|
|
1695
|
-
/** Creates a fresh durable store at `path` from the schema
|
|
1433
|
+
/** Creates a fresh durable store at `path` from the schema. */
|
|
1696
1434
|
async create<Rels extends SchemaRelations>(path: string, theory: Schema<Rels>): Promise<Db<Rels>> {
|
|
1697
1435
|
return admit("create", path, theory)
|
|
1698
1436
|
},
|
|
1699
1437
|
/**
|
|
1700
|
-
* Opens an existing durable store at `path` with the same theory
|
|
1701
|
-
*
|
|
1702
|
-
*
|
|
1703
|
-
*
|
|
1704
|
-
*
|
|
1705
|
-
*
|
|
1438
|
+
* Opens an existing durable store at `path` with the same theory.
|
|
1439
|
+
* A fingerprint-matching open also BACK-FILLS the store's persisted
|
|
1440
|
+
* schema descriptor when it is absent (self-describing stores, engine
|
|
1441
|
+
* 50-storage.md § the `_meta` block), so a legacy store becomes
|
|
1442
|
+
* exhumable after one ordinary open — adoption is automatic, never a
|
|
1443
|
+
* separate verb. A second open of a still-live path is
|
|
1444
|
+
* `EnvironmentLocked`.
|
|
1706
1445
|
*/
|
|
1707
1446
|
async open<Rels extends SchemaRelations>(path: string, theory: Schema<Rels>): Promise<Db<Rels>> {
|
|
1708
1447
|
return admit("open", path, theory)
|
|
@@ -1737,4 +1476,4 @@ export type {
|
|
|
1737
1476
|
Violation,
|
|
1738
1477
|
WriteResult
|
|
1739
1478
|
}
|
|
1740
|
-
export { abandon, Db,
|
|
1479
|
+
export { abandon, Db, ErrGenerationMoved, ErrNewtypeMismatch }
|