@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/COOKBOOK.md +25 -13
- package/README.md +79 -53
- package/dist/db.d.ts +133 -114
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +733 -327
- package/dist/db.js.map +1 -1
- package/dist/index.d.ts +5 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -9
- package/dist/index.js.map +1 -1
- package/dist/native.d.ts +124 -152
- package/dist/native.d.ts.map +1 -1
- package/dist/native.js +46 -8
- package/dist/native.js.map +1 -1
- package/package.json +3 -3
- package/src/db.ts +1058 -426
- package/src/index.ts +25 -22
- package/src/native.ts +215 -160
- package/dist/exhume.d.ts +0 -143
- package/dist/exhume.d.ts.map +0 -1
- package/dist/exhume.js +0 -166
- package/dist/exhume.js.map +0 -1
- package/src/exhume.ts +0 -267
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
|
|
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
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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
|
|
28
|
-
* {@link Violation} values.
|
|
29
|
-
*
|
|
30
|
-
*
|
|
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 `{
|
|
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({
|
|
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
|
-
|
|
316
|
-
const
|
|
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
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
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
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
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
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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
|
-
|
|
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
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
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(
|
|
614
|
-
return
|
|
795
|
+
return read(function scanInScope(instance) {
|
|
796
|
+
return instance.scan(relation);
|
|
615
797
|
});
|
|
616
798
|
}
|
|
617
799
|
function get(relation, keyOrStatement, declaredKey) {
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
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
|
|
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(
|
|
628
|
-
return
|
|
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(
|
|
633
|
-
return
|
|
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.
|
|
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
|
-
|
|
670
|
-
return
|
|
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,
|
|
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
|
|
719
|
-
|
|
720
|
-
return
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
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
|
-
|
|
759
|
-
return
|
|
760
|
-
|
|
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
|
-
|
|
779
|
-
|
|
780
|
-
|
|
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
|
-
|
|
784
|
-
|
|
916
|
+
tag: "accepted",
|
|
917
|
+
value: Object.freeze({
|
|
918
|
+
value: built,
|
|
919
|
+
generation: nativeOutcome.generation
|
|
920
|
+
})
|
|
785
921
|
});
|
|
786
922
|
}
|
|
787
|
-
function
|
|
788
|
-
|
|
789
|
-
|
|
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
|
|
944
|
+
return mapNativeWrite(nativeOutcome, built);
|
|
792
945
|
}
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
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
|
-
|
|
804
|
-
|
|
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 (
|
|
807
|
-
throw errors.
|
|
960
|
+
if (state.owner !== owner) {
|
|
961
|
+
throw errors.wrap(ErrForeignWitness, `bumbledb writeFrom witness belongs to a different store (schema ${theory.name})`);
|
|
808
962
|
}
|
|
809
|
-
|
|
810
|
-
|
|
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
|
|
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
|
-
|
|
979
|
+
throwPrepareRefusal(outcome.message);
|
|
827
980
|
}
|
|
828
|
-
|
|
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
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
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
|
|
877
|
-
|
|
878
|
-
|
|
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
|
-
|
|
884
|
-
|
|
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
|
|
889
|
-
|
|
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
|
-
|
|
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
|
|
897
|
-
*
|
|
898
|
-
*
|
|
899
|
-
*
|
|
900
|
-
*
|
|
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(
|
|
905
|
-
return
|
|
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
|
-
*
|
|
910
|
-
*
|
|
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(
|
|
917
|
-
return
|
|
1323
|
+
async open(storePath, theory) {
|
|
1324
|
+
return openStore(storePath, theory);
|
|
918
1325
|
},
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
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,
|
|
1341
|
+
export { abandon, Db, ErrAsyncCallback, ErrForeignPrepared, ErrForeignWitness, ErrFingerprintMismatch, ErrIrError, ErrNewtypeMismatch, ErrSchemaError, ErrSpentHandle, ErrUseAfterScope, InstanceBuilder };
|
|
936
1342
|
//# sourceMappingURL=db.js.map
|