@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/dist/db.js
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
|
import * as path from "node:path";
|
|
36
33
|
import * as errors from "@superbuilders/errors";
|
|
@@ -45,7 +42,7 @@ import { decodeAnswers, wireParams } from "#query/run.ts";
|
|
|
45
42
|
import { isStatement } from "#statements.ts";
|
|
46
43
|
/**
|
|
47
44
|
* The runtime discriminant of {@link Abandon} values — a property probe is
|
|
48
|
-
* how `
|
|
45
|
+
* how `write`/`writeFrom` distinguish "abort without committing" from an
|
|
49
46
|
* ordinary callback result, never a guess about the host's own value shapes.
|
|
50
47
|
*/
|
|
51
48
|
const abandonMark = Symbol("bumbledb.abandon");
|
|
@@ -53,7 +50,7 @@ const abandonMark = Symbol("bumbledb.abandon");
|
|
|
53
50
|
* Wraps a payload in the {@link Abandon} sentinel — the one way a write
|
|
54
51
|
* callback declines to commit: `return abandon(payload)` aborts the delta
|
|
55
52
|
* (nothing is committed, not even an empty commit) and the write resolves
|
|
56
|
-
* to `{ ok: false, abandoned: payload }`, from `write` and `
|
|
53
|
+
* to `{ ok: false, abandoned: payload }`, from `write` and `writeFrom`
|
|
57
54
|
* alike (R10).
|
|
58
55
|
*/
|
|
59
56
|
function abandon(payload) {
|
|
@@ -131,8 +128,8 @@ function impliedKeyEntries(theory) {
|
|
|
131
128
|
if (isFreshField(declared.field)) {
|
|
132
129
|
entries.push({
|
|
133
130
|
kind: "functionality",
|
|
134
|
-
|
|
135
|
-
|
|
131
|
+
owner: member.name,
|
|
132
|
+
projection: [declared.name]
|
|
136
133
|
});
|
|
137
134
|
}
|
|
138
135
|
}
|
|
@@ -141,8 +138,8 @@ function impliedKeyEntries(theory) {
|
|
|
141
138
|
if (isClosedMember(member)) {
|
|
142
139
|
entries.push({
|
|
143
140
|
kind: "functionality",
|
|
144
|
-
|
|
145
|
-
|
|
141
|
+
owner: member.name,
|
|
142
|
+
projection: ["id"]
|
|
146
143
|
});
|
|
147
144
|
}
|
|
148
145
|
}
|
|
@@ -162,21 +159,22 @@ function declaredEntries(statement) {
|
|
|
162
159
|
{
|
|
163
160
|
kind: "functionality",
|
|
164
161
|
statement,
|
|
165
|
-
|
|
162
|
+
owner: data.owner.name,
|
|
163
|
+
projection: data.projection
|
|
166
164
|
}
|
|
167
165
|
];
|
|
168
166
|
}
|
|
169
167
|
case "containment": {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
168
|
+
return [{ kind: "containment", statement }];
|
|
169
|
+
}
|
|
170
|
+
case "mirrors": {
|
|
171
|
+
return [
|
|
172
|
+
{ kind: "mirrors", statement, orientation: "written" },
|
|
173
|
+
{ kind: "mirrors", statement, orientation: "mirrored" }
|
|
174
|
+
];
|
|
177
175
|
}
|
|
178
176
|
case "capacity": {
|
|
179
|
-
return [{ kind: "capacity", statement
|
|
177
|
+
return [{ kind: "capacity", statement }];
|
|
180
178
|
}
|
|
181
179
|
}
|
|
182
180
|
}
|
|
@@ -221,16 +219,6 @@ function selectKeyRead(keyOrStatement, declaredKey, byStatement, byPrimary) {
|
|
|
221
219
|
}
|
|
222
220
|
return byPrimary(keyOrStatement);
|
|
223
221
|
}
|
|
224
|
-
/** Maps a slot's reversal flag to the violation's `orientation` payload. */
|
|
225
|
-
function orientationOf(reversed) {
|
|
226
|
-
if (reversed === undefined) {
|
|
227
|
-
return undefined;
|
|
228
|
-
}
|
|
229
|
-
if (reversed) {
|
|
230
|
-
return "mirrored";
|
|
231
|
-
}
|
|
232
|
-
return "written";
|
|
233
|
-
}
|
|
234
222
|
/**
|
|
235
223
|
* Builds the id-resolution tables from the manifest, verifying the SDK's
|
|
236
224
|
* positional mirror against the engine's reported order — any drift
|
|
@@ -248,9 +236,13 @@ function tablesOf(theory, manifest) {
|
|
|
248
236
|
}
|
|
249
237
|
manifest.statements.forEach(function verifySlot(statement, index) {
|
|
250
238
|
const entry = entries[index];
|
|
251
|
-
if (entry === undefined || statement.id !== index
|
|
239
|
+
if (entry === undefined || statement.id !== index) {
|
|
252
240
|
throw errors.new(`bumbledb manifest drift: statement ${statement.id} is ${statement.kind}, the SDK mirror at ${index} expected ${entry?.kind}`);
|
|
253
241
|
}
|
|
242
|
+
const engineKind = entry.kind === "mirrors" ? "containment" : entry.kind;
|
|
243
|
+
if (engineKind !== statement.kind) {
|
|
244
|
+
throw errors.new(`bumbledb manifest drift: statement ${statement.id} is ${statement.kind}, the SDK mirror at ${index} expected ${engineKind}`);
|
|
245
|
+
}
|
|
254
246
|
});
|
|
255
247
|
const relations = new Map();
|
|
256
248
|
for (const relation of manifest.relations) {
|
|
@@ -269,8 +261,8 @@ function tablesOf(theory, manifest) {
|
|
|
269
261
|
});
|
|
270
262
|
let primaryKey;
|
|
271
263
|
entries.forEach(function firstOwnedKey(entry, index) {
|
|
272
|
-
if (primaryKey === undefined && entry.
|
|
273
|
-
primaryKey = Object.freeze({ statementId: index, projection: entry.
|
|
264
|
+
if (primaryKey === undefined && entry.kind === "functionality" && entry.owner === relation.name) {
|
|
265
|
+
primaryKey = Object.freeze({ statementId: index, projection: entry.projection });
|
|
274
266
|
}
|
|
275
267
|
});
|
|
276
268
|
relations.set(relation.name, Object.freeze({ id: relation.id, member, fieldIds, primaryKey }));
|
|
@@ -305,34 +297,11 @@ const planReclaimer = new FinalizationRegistry(function reclaimPlan(handle) {
|
|
|
305
297
|
}
|
|
306
298
|
});
|
|
307
299
|
/**
|
|
308
|
-
* The
|
|
309
|
-
*
|
|
310
|
-
*
|
|
311
|
-
* callback on a fresh snapshot. It never escapes the SDK.
|
|
312
|
-
*/
|
|
313
|
-
const generationMovedSignal = errors.new("bumbledb witnessed generation moved");
|
|
314
|
-
/**
|
|
315
|
-
* The witnessed loop's attempt cap — a generous power of two. Benign
|
|
316
|
-
* self-inflicted contention (the host's own commits landing between an
|
|
317
|
-
* attempt's snapshot and its witnessed begin) converges in a handful of
|
|
318
|
-
* retries because each rerun reads a FRESHER snapshot; a workload that moves
|
|
319
|
-
* the generation on EVERY one of this many consecutive attempts is not
|
|
320
|
-
* converging and never will (see {@link ErrWitnessedLivelock}).
|
|
300
|
+
* The typed generation-moved refusal `writeFrom` throws when a
|
|
301
|
+
* state-changing commit landed since the witness snapshot: retry is host
|
|
302
|
+
* policy. Match with `errors.is`.
|
|
321
303
|
*/
|
|
322
|
-
const
|
|
323
|
-
/**
|
|
324
|
-
* The typed livelock refusal `writeWitnessed` throws past
|
|
325
|
-
* {@link WITNESSED_ATTEMPT_CAP} attempts: every attempt found the generation
|
|
326
|
-
* moved, which is only sustainable when the callback ITSELF (even
|
|
327
|
-
* indirectly) issues an interleaved plain `db.write` before its first tx
|
|
328
|
-
* verb on every attempt — each rerun then re-moves the generation it is
|
|
329
|
-
* about to witness, forever. That is host-policy pathology, not engine
|
|
330
|
-
* judgment (the engine ships the error, never a loop), so it THROWS rather
|
|
331
|
-
* than returning a result arm. Match with `errors.is`; the remedy is to
|
|
332
|
-
* move the interleaved write out of the callback (or make it first-attempt
|
|
333
|
-
* only — the delta belongs on `tx`, premise reads on `snap`).
|
|
334
|
-
*/
|
|
335
|
-
const ErrWitnessedLivelock = errors.new("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");
|
|
304
|
+
const ErrGenerationMoved = errors.new("bumbledb generationMoved: a state-changing commit landed since the witness snapshot");
|
|
336
305
|
/**
|
|
337
306
|
* Fills one insert's omitted fresh cells through the engine's
|
|
338
307
|
* alloc-then-insert dyn lane (there is no insert-with-omitted-fields wire
|
|
@@ -410,15 +379,32 @@ function openDb(handle, theory, manifest) {
|
|
|
410
379
|
if (entry === undefined) {
|
|
411
380
|
throw errors.new(`bumbledb violation cites unknown statement id ${wire.statementId}`);
|
|
412
381
|
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
382
|
+
const facts = Object.freeze(wire.facts.map(offendingFactOf));
|
|
383
|
+
const statement = entry.statement;
|
|
384
|
+
const canonical = wire.canonical;
|
|
385
|
+
if (entry.kind === "functionality") {
|
|
386
|
+
return Object.freeze({ kind: "functionality", statement, canonical, facts });
|
|
387
|
+
}
|
|
388
|
+
if (entry.kind === "capacity") {
|
|
389
|
+
if (wire.kind !== "capacity") {
|
|
390
|
+
throw errors.new(`bumbledb violation ${wire.statementId} is a capacity slot without a measure`);
|
|
391
|
+
}
|
|
392
|
+
return Object.freeze({ kind: "capacity", statement, canonical, measure: wire.measure, facts });
|
|
393
|
+
}
|
|
394
|
+
if (wire.kind !== "containment") {
|
|
395
|
+
throw errors.new(`bumbledb violation ${wire.statementId} is a containment slot without a direction`);
|
|
396
|
+
}
|
|
397
|
+
if (entry.kind === "mirrors") {
|
|
398
|
+
return Object.freeze({
|
|
399
|
+
kind: "containment",
|
|
400
|
+
statement,
|
|
401
|
+
canonical,
|
|
402
|
+
direction: wire.direction,
|
|
403
|
+
orientation: entry.orientation,
|
|
404
|
+
facts
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
return Object.freeze({ kind: "containment", statement, canonical, direction: wire.direction, facts });
|
|
422
408
|
}
|
|
423
409
|
/**
|
|
424
410
|
* Resolves a key-statement-selected read: the statement must be the
|
|
@@ -435,13 +421,13 @@ function openDb(handle, theory, manifest) {
|
|
|
435
421
|
if (entry === undefined) {
|
|
436
422
|
throw errors.new(`keyed get statement is not a declared statement of schema ${theory.name} — statement identity is the membership rule`);
|
|
437
423
|
}
|
|
438
|
-
if (entry.kind !== "functionality"
|
|
424
|
+
if (entry.kind !== "functionality") {
|
|
439
425
|
throw errors.new("keyed get takes a key() statement — containments and capacity statements key nothing");
|
|
440
426
|
}
|
|
441
|
-
if (entry.
|
|
442
|
-
throw errors.new(`keyed get statement keys ${entry.
|
|
427
|
+
if (entry.owner !== relation.name) {
|
|
428
|
+
throw errors.new(`keyed get statement keys ${entry.owner}, not ${relation.name} — the statement must be a declared key of the relation it reads`);
|
|
443
429
|
}
|
|
444
|
-
return Object.freeze({ statementId, projection: entry.
|
|
430
|
+
return Object.freeze({ statementId, projection: entry.projection });
|
|
445
431
|
}
|
|
446
432
|
function pointReadsOf(assertLive, reads) {
|
|
447
433
|
function contains(relation, fact) {
|
|
@@ -530,14 +516,6 @@ function openDb(handle, theory, manifest) {
|
|
|
530
516
|
});
|
|
531
517
|
return decodeAnswers(plan.finds, rows);
|
|
532
518
|
}
|
|
533
|
-
function explain(prepared, params) {
|
|
534
|
-
assertLive();
|
|
535
|
-
const plan = planOf(prepared);
|
|
536
|
-
const wire = wireParams(plan.params, recordOf(params));
|
|
537
|
-
return bridged("explain bumbledb prepared query", function callExplain() {
|
|
538
|
-
return native.preparedExplain(plan.handle, state.handle, wire);
|
|
539
|
-
});
|
|
540
|
-
}
|
|
541
519
|
/** The R12 teardown: invalidate, then close — idempotent through the state's close latch. */
|
|
542
520
|
function dispose() {
|
|
543
521
|
state.live = false;
|
|
@@ -549,7 +527,6 @@ function openDb(handle, theory, manifest) {
|
|
|
549
527
|
get: reads.get,
|
|
550
528
|
contains: reads.contains,
|
|
551
529
|
execute,
|
|
552
|
-
explain,
|
|
553
530
|
[Symbol.dispose]: dispose
|
|
554
531
|
});
|
|
555
532
|
scopeStates.set(scope, state);
|
|
@@ -638,17 +615,9 @@ function openDb(handle, theory, manifest) {
|
|
|
638
615
|
return snap.execute(prepared, params);
|
|
639
616
|
});
|
|
640
617
|
}
|
|
641
|
-
function explain(prepared, params) {
|
|
642
|
-
return read(function explainInScope(snap) {
|
|
643
|
-
return snap.explain(prepared, params);
|
|
644
|
-
});
|
|
645
|
-
}
|
|
646
618
|
/**
|
|
647
|
-
* Builds one {@link Tx} over a transaction-handle thunk: `write`
|
|
648
|
-
* an already-begun handle
|
|
649
|
-
* begins the witnessed transaction on the first delta verb (so premise
|
|
650
|
-
* reads and the host's own interleaved writes can precede it) and
|
|
651
|
-
* throws {@link generationMovedSignal} when the witness is stale.
|
|
619
|
+
* Builds one {@link Tx} over a transaction-handle thunk: `write` and
|
|
620
|
+
* `writeFrom` pass an already-begun handle.
|
|
652
621
|
*/
|
|
653
622
|
function makeTx(resolveTx) {
|
|
654
623
|
const txState = { spent: false };
|
|
@@ -784,181 +753,42 @@ function openDb(handle, theory, manifest) {
|
|
|
784
753
|
return runDelta(txHandle, fn);
|
|
785
754
|
}
|
|
786
755
|
/**
|
|
787
|
-
*
|
|
788
|
-
*
|
|
789
|
-
*
|
|
756
|
+
* One-shot write from a live snapshot this store owns. Begins immediately;
|
|
757
|
+
* a moved generation throws {@link ErrGenerationMoved} and `fn` never
|
|
758
|
+
* runs. The snapshot stays owned by the caller's read callback.
|
|
790
759
|
*/
|
|
791
|
-
function
|
|
792
|
-
const
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
});
|
|
796
|
-
});
|
|
797
|
-
if (committed.error) {
|
|
798
|
-
/** Same one-writer law as `runDelta`: a thrown commit aborts before rethrowing. */
|
|
799
|
-
const aborted = errors.trySync(function abortAfterFailedCommit() {
|
|
800
|
-
native.txAbort(txHandle);
|
|
801
|
-
});
|
|
802
|
-
if (aborted.error) {
|
|
803
|
-
}
|
|
804
|
-
closeScopeState(state);
|
|
805
|
-
throw errors.wrap(committed.error, "commit bumbledb witnessed write transaction");
|
|
760
|
+
function writeFrom(snap, fn) {
|
|
761
|
+
const snapState = scopeStates.get(snap);
|
|
762
|
+
if (snapState === undefined) {
|
|
763
|
+
throw errors.new("bumbledb writeFrom witness is not a read scope of this SDK");
|
|
806
764
|
}
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
if (outcome.ok) {
|
|
810
|
-
return Object.freeze({ ok: true, generation: outcome.generation });
|
|
765
|
+
if (snapState.owner !== owner) {
|
|
766
|
+
throw errors.new(`bumbledb writeFrom snapshot belongs to a different store (schema ${theory.name})`);
|
|
811
767
|
}
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
violations: Object.freeze(outcome.violations.map(violationOf))
|
|
815
|
-
});
|
|
816
|
-
}
|
|
817
|
-
/**
|
|
818
|
-
* One attempt of the witnessed loop: fresh snapshot, the callback over
|
|
819
|
-
* its scope and a LAZILY-begun witnessed transaction (the first delta
|
|
820
|
-
* verb begins it, so premise reads and the host's own interleaved
|
|
821
|
-
* writes can precede the witness check), then the witnessed commit —
|
|
822
|
-
* or the abandon abort, which never issues a commit. Returns
|
|
823
|
-
* `undefined` exactly when the generation moved and the whole callback
|
|
824
|
-
* must rerun on a fresh snapshot.
|
|
825
|
-
*/
|
|
826
|
-
function witnessedAttempt(fn) {
|
|
827
|
-
const state = openScopeState();
|
|
828
|
-
const scope = makeScope(state);
|
|
829
|
-
const pending = { tx: undefined };
|
|
830
|
-
function beginWitnessed() {
|
|
831
|
-
const witnessed = bridged("begin witnessed bumbledb write transaction", function begin() {
|
|
832
|
-
return native.dbWriteFrom(handle, state.handle);
|
|
833
|
-
});
|
|
834
|
-
if (!witnessed.ok) {
|
|
835
|
-
return undefined;
|
|
836
|
-
}
|
|
837
|
-
return witnessed.tx;
|
|
768
|
+
if (!snapState.live) {
|
|
769
|
+
throw errors.new("bumbledb writeFrom snapshot is invalidated — its owning read callback already returned");
|
|
838
770
|
}
|
|
839
|
-
const
|
|
840
|
-
|
|
841
|
-
const begun = beginWitnessed();
|
|
842
|
-
if (begun === undefined) {
|
|
843
|
-
throw generationMovedSignal;
|
|
844
|
-
}
|
|
845
|
-
pending.tx = begun;
|
|
846
|
-
}
|
|
847
|
-
return pending.tx;
|
|
771
|
+
const witnessed = bridged("begin witnessed bumbledb write transaction", function begin() {
|
|
772
|
+
return native.dbWriteFrom(handle, snapState.handle);
|
|
848
773
|
});
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
});
|
|
852
|
-
made.spend();
|
|
853
|
-
state.live = false;
|
|
854
|
-
/**
|
|
855
|
-
* Aborts the pending transaction if one was begun. A faulted abort
|
|
856
|
-
* still closes the attempt's snapshot BEFORE rethrowing — every
|
|
857
|
-
* openScopeState is paired with closeScopeState on every exit, or a
|
|
858
|
-
* reader slot and its snapshot worker leak for the process's lifetime.
|
|
859
|
-
*/
|
|
860
|
-
function abortPending() {
|
|
861
|
-
const txHandle = pending.tx;
|
|
862
|
-
if (txHandle === undefined) {
|
|
863
|
-
return;
|
|
864
|
-
}
|
|
865
|
-
const aborted = errors.trySync(function abort() {
|
|
866
|
-
native.txAbort(txHandle);
|
|
867
|
-
});
|
|
868
|
-
if (aborted.error) {
|
|
869
|
-
closeScopeState(state);
|
|
870
|
-
throw errors.wrap(aborted.error, "abort bumbledb witnessed write transaction");
|
|
871
|
-
}
|
|
872
|
-
}
|
|
873
|
-
if (built.error) {
|
|
874
|
-
abortPending();
|
|
875
|
-
closeScopeState(state);
|
|
876
|
-
if (errors.is(built.error, generationMovedSignal)) {
|
|
877
|
-
return undefined;
|
|
878
|
-
}
|
|
879
|
-
throw errors.wrap(built.error, "build witnessed write delta");
|
|
880
|
-
}
|
|
881
|
-
if (isThenable(built.data)) {
|
|
882
|
-
/** The same async-callback refusal as `runDelta` — a thenable means the real delta build races the commit; nothing is committed. */
|
|
883
|
-
abortPending();
|
|
884
|
-
closeScopeState(state);
|
|
885
|
-
throw errors.new("bumbledb writeWitnessed callback returned a thenable — the delta build is synchronous; an async callback is refused, nothing was committed");
|
|
886
|
-
}
|
|
887
|
-
if (isAbandon(built.data)) {
|
|
888
|
-
abortPending();
|
|
889
|
-
closeScopeState(state);
|
|
890
|
-
return abandonedOutcome(built.data);
|
|
891
|
-
}
|
|
892
|
-
const late = errors.trySync(function resolveCommitTx() {
|
|
893
|
-
if (pending.tx === undefined) {
|
|
894
|
-
return beginWitnessed();
|
|
895
|
-
}
|
|
896
|
-
return pending.tx;
|
|
897
|
-
});
|
|
898
|
-
if (late.error) {
|
|
899
|
-
/** A faulted late begin must not leak the attempt's snapshot either. */
|
|
900
|
-
closeScopeState(state);
|
|
901
|
-
throw late.error;
|
|
902
|
-
}
|
|
903
|
-
const txHandle = late.data;
|
|
904
|
-
if (txHandle === undefined) {
|
|
905
|
-
closeScopeState(state);
|
|
906
|
-
return undefined;
|
|
774
|
+
if (!witnessed.ok) {
|
|
775
|
+
throw errors.wrap(ErrGenerationMoved, `writeFrom: generation moved (witnessed ${witnessed.witnessed} current ${witnessed.current}) against schema ${theory.name}`);
|
|
907
776
|
}
|
|
908
|
-
return
|
|
909
|
-
}
|
|
910
|
-
/**
|
|
911
|
-
* The witnessed retry loop. What it retries: the benign race — the
|
|
912
|
-
* host's OWN interleaved commit landing between an attempt's snapshot
|
|
913
|
-
* and its witnessed begin (every writer shares this handle, so a move
|
|
914
|
-
* is always self-inflicted) — by rerunning the WHOLE callback on a
|
|
915
|
-
* fresh snapshot, which converges because each rerun witnesses a
|
|
916
|
-
* strictly newer generation. What it refuses: the pathology where the
|
|
917
|
-
* callback itself (even indirectly) issues a plain `db.write` before
|
|
918
|
-
* its first tx verb, moving the generation on EVERY attempt — an
|
|
919
|
-
* unbounded loop would spin forever with no diagnostic, so past
|
|
920
|
-
* {@link WITNESSED_ATTEMPT_CAP} attempts the loop throws the typed
|
|
921
|
-
* {@link ErrWitnessedLivelock} instead (the engine's ruling: the
|
|
922
|
-
* error, never a loop — retry is host policy, and this is the host
|
|
923
|
-
* policy's own honesty bound).
|
|
924
|
-
*/
|
|
925
|
-
function writeWitnessed(fn) {
|
|
926
|
-
for (let attempts = 0; attempts < WITNESSED_ATTEMPT_CAP; attempts += 1) {
|
|
927
|
-
const attempt = witnessedAttempt(fn);
|
|
928
|
-
if (attempt !== undefined) {
|
|
929
|
-
return attempt;
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
throw errors.wrap(ErrWitnessedLivelock, `writeWitnessed livelock: the generation moved on all ${WITNESSED_ATTEMPT_CAP} attempts against schema ${theory.name}`);
|
|
777
|
+
return runDelta(witnessed.tx, fn);
|
|
933
778
|
}
|
|
934
779
|
function prepare(q) {
|
|
935
780
|
if (q.schema !== theory) {
|
|
936
781
|
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`);
|
|
937
782
|
}
|
|
938
|
-
const
|
|
939
|
-
const outcome = bridged("prepare bumbledb
|
|
940
|
-
return native.dbPrepare(handle,
|
|
783
|
+
const queryIr = lowerQuery(q);
|
|
784
|
+
const outcome = bridged("prepare bumbledb query", function callPrepare() {
|
|
785
|
+
return native.dbPrepare(handle, queryIr);
|
|
941
786
|
});
|
|
942
787
|
if (!outcome.ok) {
|
|
943
788
|
throw errors.new(`bumbledb ${outcome.kind} (prepare): ${outcome.message}`);
|
|
944
789
|
}
|
|
945
790
|
const preparedHandle = outcome.prepared;
|
|
946
|
-
|
|
947
|
-
const snapState = scopeStates.get(snap);
|
|
948
|
-
if (snapState === undefined) {
|
|
949
|
-
throw errors.new("bumbledb staleness witness is not a read scope of this SDK");
|
|
950
|
-
}
|
|
951
|
-
if (snapState.owner !== owner) {
|
|
952
|
-
throw errors.new(`bumbledb read scope belongs to a different store than this prepared value (schema ${theory.name})`);
|
|
953
|
-
}
|
|
954
|
-
if (!snapState.live) {
|
|
955
|
-
throw errors.new("bumbledb read scope is invalidated — its owning read callback already returned");
|
|
956
|
-
}
|
|
957
|
-
return bridged("read bumbledb prepared staleness", function callStaleness() {
|
|
958
|
-
return native.preparedStaleness(preparedHandle, snapState.handle);
|
|
959
|
-
});
|
|
960
|
-
}
|
|
961
|
-
const prepared = Object.freeze({ staleness });
|
|
791
|
+
const prepared = Object.freeze({});
|
|
962
792
|
preparedPlans.set(prepared, Object.freeze({
|
|
963
793
|
handle: preparedHandle,
|
|
964
794
|
owner,
|
|
@@ -975,44 +805,11 @@ function openDb(handle, theory, manifest) {
|
|
|
975
805
|
get,
|
|
976
806
|
contains,
|
|
977
807
|
execute,
|
|
978
|
-
explain,
|
|
979
808
|
write,
|
|
980
|
-
|
|
809
|
+
writeFrom,
|
|
981
810
|
prepare
|
|
982
811
|
});
|
|
983
812
|
}
|
|
984
|
-
/**
|
|
985
|
-
* The per-process store cache, keyed by canonical path
|
|
986
|
-
* (`node:path.resolve` — absolute and normalized). Symlink aliasing is
|
|
987
|
-
* deliberately not resolved here: an aliased spelling misses the cache and
|
|
988
|
-
* reaches the engine, whose exclusive lock refuses a second live handle on
|
|
989
|
-
* the same store — the backstop that keeps "one store, one handle" true.
|
|
990
|
-
*/
|
|
991
|
-
const openStores = new Map();
|
|
992
|
-
/**
|
|
993
|
-
* The in-process fingerprint check and its typing proof in one probe:
|
|
994
|
-
* theory identity (`===`) implies `Rels` identity, because a cache entry's
|
|
995
|
-
* `db` was constructed from that very theory value — so a hit narrows the
|
|
996
|
-
* entry's `db` to `Db<Rels>` with no assertion anywhere.
|
|
997
|
-
*/
|
|
998
|
-
function holdsTheory(entry, theory) {
|
|
999
|
-
return entry.theory === theory;
|
|
1000
|
-
}
|
|
1001
|
-
/**
|
|
1002
|
-
* The best-effort exit hook: closes every cached environment so LMDB
|
|
1003
|
-
* releases its locks tidily on a clean exit. CORRECTNESS NEVER RESTS HERE —
|
|
1004
|
-
* the engine fsyncs every commit, so a process killed before (or during)
|
|
1005
|
-
* this hook loses nothing that was committed.
|
|
1006
|
-
*/
|
|
1007
|
-
process.once("exit", function closeCachedStores() {
|
|
1008
|
-
for (const cached of openStores.values()) {
|
|
1009
|
-
const closed = errors.trySync(function closeEnvironment() {
|
|
1010
|
-
native.dbClose(cached.handle);
|
|
1011
|
-
});
|
|
1012
|
-
if (closed.error) {
|
|
1013
|
-
}
|
|
1014
|
-
}
|
|
1015
|
-
});
|
|
1016
813
|
/**
|
|
1017
814
|
* The engine twin of the schema-level class wall, as a matchable value
|
|
1018
815
|
* (`errors.is`): the shared lowering rejected a spec whose statement pairs
|
|
@@ -1043,30 +840,19 @@ function refuseShadowedChanged(theory) {
|
|
|
1043
840
|
}
|
|
1044
841
|
}
|
|
1045
842
|
/**
|
|
1046
|
-
* The one admission path both verbs share:
|
|
1047
|
-
*
|
|
1048
|
-
*
|
|
1049
|
-
* `
|
|
1050
|
-
*
|
|
1051
|
-
*
|
|
1052
|
-
*
|
|
1053
|
-
*
|
|
1054
|
-
*
|
|
1055
|
-
* the engine's message intact.
|
|
843
|
+
* The one admission path both verbs share: lower the theory, run one
|
|
844
|
+
* bridge call, and wrap the domain refusals — `schemaError` (spec
|
|
845
|
+
* resolution + schema validation, every issue in one message),
|
|
846
|
+
* `newtypeMismatch` (the coherence wall, {@link ErrNewtypeMismatch}),
|
|
847
|
+
* and `fingerprintMismatch` (a different theory cannot open the store)
|
|
848
|
+
* — into typed errors carrying the engine's message intact.
|
|
849
|
+
* Environment failures (a second live writer on the same path, IO)
|
|
850
|
+
* throw from the bridge; the engine's `EnvironmentLocked` message is
|
|
851
|
+
* "another live handle holds this environment's lock".
|
|
1056
852
|
*/
|
|
1057
853
|
function admit(verb, storePath, theory) {
|
|
1058
854
|
refuseShadowedChanged(theory);
|
|
1059
855
|
const canonical = path.resolve(storePath);
|
|
1060
|
-
const cached = openStores.get(canonical);
|
|
1061
|
-
if (cached !== undefined) {
|
|
1062
|
-
if (verb === "create") {
|
|
1063
|
-
throw errors.new(`create bumbledb store at ${canonical}: the store is already open in this process — create refuses an already-initialized directory`);
|
|
1064
|
-
}
|
|
1065
|
-
if (!holdsTheory(cached, theory)) {
|
|
1066
|
-
throw errors.new(`bumbledb fingerprintMismatch (open ${canonical}): the cached store was opened with schema ${cached.theory.name}, not this theory value — schema identity is the membership rule`);
|
|
1067
|
-
}
|
|
1068
|
-
return cached.db;
|
|
1069
|
-
}
|
|
1070
856
|
const spec = lower(theory);
|
|
1071
857
|
const opened = bridged(`${verb} bumbledb store at ${canonical}`, function callBridge() {
|
|
1072
858
|
if (verb === "create") {
|
|
@@ -1083,34 +869,30 @@ function admit(verb, storePath, theory) {
|
|
|
1083
869
|
const manifest = bridged("fetch bumbledb manifest", function fetchManifest() {
|
|
1084
870
|
return native.dbManifest(opened.db);
|
|
1085
871
|
});
|
|
1086
|
-
|
|
1087
|
-
openStores.set(canonical, Object.freeze({ theory, db, handle: opened.db }));
|
|
1088
|
-
return db;
|
|
872
|
+
return openDb(opened.db, theory, manifest);
|
|
1089
873
|
}
|
|
1090
874
|
/**
|
|
1091
875
|
* The store lifecycle — `Db.create(path, schema)` / `Db.open(path, schema)`.
|
|
1092
876
|
* Create refuses an already-initialized directory; open verifies format
|
|
1093
|
-
* version, store kind, and the schema fingerprint.
|
|
1094
|
-
*
|
|
1095
|
-
*
|
|
1096
|
-
*
|
|
1097
|
-
*
|
|
1098
|
-
* hook closes them; durability is the engine's per-commit fsync). One
|
|
1099
|
-
* store kind exists: durable — resume = reopen, meaning this process's
|
|
1100
|
-
* cached value or a fresh process's open.
|
|
877
|
+
* version, store kind, and the schema fingerprint. A second live handle
|
|
878
|
+
* on the same path is the engine's `EnvironmentLocked`. There is no close
|
|
879
|
+
* anywhere: the process owns the environment until GC/exit (durability is
|
|
880
|
+
* the engine's per-commit fsync). One store kind exists: durable —
|
|
881
|
+
* resume = reopen in a fresh process, or hold the `Db` this process opened.
|
|
1101
882
|
*/
|
|
1102
883
|
const Db = Object.freeze({
|
|
1103
|
-
/** Creates a fresh durable store at `path` from the schema
|
|
884
|
+
/** Creates a fresh durable store at `path` from the schema. */
|
|
1104
885
|
async create(path, theory) {
|
|
1105
886
|
return admit("create", path, theory);
|
|
1106
887
|
},
|
|
1107
888
|
/**
|
|
1108
|
-
* Opens an existing durable store at `path` with the same theory
|
|
1109
|
-
*
|
|
1110
|
-
*
|
|
1111
|
-
*
|
|
1112
|
-
*
|
|
1113
|
-
*
|
|
889
|
+
* Opens an existing durable store at `path` with the same theory.
|
|
890
|
+
* A fingerprint-matching open also BACK-FILLS the store's persisted
|
|
891
|
+
* schema descriptor when it is absent (self-describing stores, engine
|
|
892
|
+
* 50-storage.md § the `_meta` block), so a legacy store becomes
|
|
893
|
+
* exhumable after one ordinary open — adoption is automatic, never a
|
|
894
|
+
* separate verb. A second open of a still-live path is
|
|
895
|
+
* `EnvironmentLocked`.
|
|
1114
896
|
*/
|
|
1115
897
|
async open(path, theory) {
|
|
1116
898
|
return admit("open", path, theory);
|
|
@@ -1131,5 +913,5 @@ const Db = Object.freeze({
|
|
|
1131
913
|
return exhumeStore(path.resolve(storePath));
|
|
1132
914
|
}
|
|
1133
915
|
});
|
|
1134
|
-
export { abandon, Db,
|
|
916
|
+
export { abandon, Db, ErrGenerationMoved, ErrNewtypeMismatch };
|
|
1135
917
|
//# sourceMappingURL=db.js.map
|