@bjornpagen/bumbledb 0.14.0 → 0.17.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 +58 -62
- package/README.md +82 -56
- package/dist/capacity.d.ts +24 -136
- package/dist/capacity.d.ts.map +1 -1
- package/dist/capacity.js +18 -40
- package/dist/capacity.js.map +1 -1
- package/dist/closed.d.ts +0 -156
- package/dist/closed.d.ts.map +1 -1
- package/dist/closed.js +0 -104
- package/dist/closed.js.map +1 -1
- package/dist/db.d.ts +93 -290
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +713 -556
- package/dist/db.js.map +1 -1
- package/dist/face.d.ts +0 -133
- package/dist/face.d.ts.map +1 -1
- package/dist/face.js +0 -33
- package/dist/face.js.map +1 -1
- package/dist/fields.d.ts +1 -145
- package/dist/fields.d.ts.map +1 -1
- package/dist/fields.js +2 -91
- package/dist/fields.js.map +1 -1
- package/dist/index.d.ts +13 -23
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -20
- package/dist/index.js.map +1 -1
- package/dist/law.d.ts +111 -93
- package/dist/law.d.ts.map +1 -1
- package/dist/law.js +23 -27
- package/dist/law.js.map +1 -1
- package/dist/lower.d.ts +9 -35
- package/dist/lower.d.ts.map +1 -1
- package/dist/lower.js +8 -53
- package/dist/lower.js.map +1 -1
- package/dist/marshal.d.ts +0 -65
- package/dist/marshal.d.ts.map +1 -1
- package/dist/marshal.js +0 -72
- package/dist/marshal.js.map +1 -1
- package/dist/native.d.ts +97 -390
- package/dist/native.d.ts.map +1 -1
- package/dist/native.js +39 -61
- package/dist/native.js.map +1 -1
- package/dist/query/atom.d.ts +10 -276
- package/dist/query/atom.d.ts.map +1 -1
- package/dist/query/atom.js +1 -96
- package/dist/query/atom.js.map +1 -1
- package/dist/query/find.d.ts +10 -76
- package/dist/query/find.d.ts.map +1 -1
- package/dist/query/find.js +0 -30
- package/dist/query/find.js.map +1 -1
- package/dist/query/lower.d.ts +64 -146
- package/dist/query/lower.d.ts.map +1 -1
- package/dist/query/lower.js +19 -256
- package/dist/query/lower.js.map +1 -1
- package/dist/query/parse-ir.d.ts +0 -7
- package/dist/query/parse-ir.d.ts.map +1 -1
- package/dist/query/parse-ir.js +1 -13
- package/dist/query/parse-ir.js.map +1 -1
- package/dist/query/run.d.ts +0 -36
- package/dist/query/run.d.ts.map +1 -1
- package/dist/query/run.js +0 -44
- package/dist/query/run.js.map +1 -1
- package/dist/query/scope.d.ts +24 -180
- package/dist/query/scope.d.ts.map +1 -1
- package/dist/query/scope.js +2 -66
- package/dist/query/scope.js.map +1 -1
- package/dist/relation.d.ts +2 -50
- package/dist/relation.d.ts.map +1 -1
- package/dist/relation.js +2 -37
- package/dist/relation.js.map +1 -1
- package/dist/schema.d.ts +13 -63
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +118 -92
- package/dist/schema.js.map +1 -1
- package/dist/spec.d.ts +1 -140
- package/dist/spec.d.ts.map +1 -1
- package/dist/spec.js +1 -68
- package/dist/spec.js.map +1 -1
- package/dist/statements.d.ts +6 -137
- package/dist/statements.d.ts.map +1 -1
- package/dist/statements.js +16 -119
- package/dist/statements.js.map +1 -1
- package/package.json +3 -3
- package/src/capacity.ts +26 -140
- package/src/closed.ts +5 -206
- package/src/db.ts +997 -854
- package/src/face.ts +0 -142
- package/src/fields.ts +4 -172
- package/src/index.ts +32 -35
- package/src/law.ts +201 -129
- package/src/lower.ts +8 -53
- package/src/marshal.ts +1 -85
- package/src/native.ts +192 -413
- package/src/query/atom.ts +26 -313
- package/src/query/find.ts +24 -110
- package/src/query/lower.ts +132 -377
- package/src/query/parse-ir.ts +1 -14
- package/src/query/run.ts +0 -45
- package/src/query/scope.ts +25 -186
- package/src/relation.ts +2 -66
- package/src/schema.ts +143 -122
- package/src/spec.ts +1 -160
- package/src/statements.ts +22 -174
- 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,56 @@
|
|
|
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
|
+
/**
|
|
42
|
+
* The flat projector: every fact's cells land in ONE row-major
|
|
43
|
+
* `FactValue` array (length rows×arity) — no JS array per fact exists
|
|
44
|
+
* anywhere between the caller's objects and the native crossing
|
|
45
|
+
* (proposals/one-representation/20, V1) — and the row count is counted
|
|
46
|
+
* while projecting (the {@link FlatCollection} law: the stated count is
|
|
47
|
+
* what the bridge verifies against `rows × arity`, exactly, for every
|
|
48
|
+
* arity). The per-cell judgment is `cellOf` — the one cell judge `rowOf`
|
|
49
|
+
* also speaks (closed handle→id, well-formedness, interval shape) — and
|
|
50
|
+
* the missing-field refusal is `rowOf`'s, byte for byte; only the output
|
|
51
|
+
* form differs (flat, never per-row).
|
|
52
|
+
*/
|
|
53
|
+
function rowsOf(relation, facts) {
|
|
54
|
+
const data = relation.data;
|
|
55
|
+
const cells = [];
|
|
56
|
+
let rows = 0n;
|
|
57
|
+
for (const fact of facts) {
|
|
58
|
+
rows += 1n;
|
|
59
|
+
const record = recordOf(fact);
|
|
60
|
+
for (const declared of data.fields) {
|
|
61
|
+
const value = record[declared.name];
|
|
62
|
+
if (value === undefined) {
|
|
63
|
+
throw errors.new(`relation ${data.name}: fact is missing field ${declared.name}`);
|
|
64
|
+
}
|
|
65
|
+
cells.push(cellOf(`relation ${data.name} field ${declared.name}`, declared.field, value));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return { rows, cells };
|
|
69
|
+
}
|
|
70
|
+
function mutateCollection(relation, facts, apply) {
|
|
71
|
+
const flat = rowsOf(relation, facts);
|
|
72
|
+
const report = apply(flat.rows, flat.cells);
|
|
73
|
+
return Object.freeze({ submitted: report.submitted, changed: report.changed });
|
|
74
|
+
}
|
|
43
75
|
function freshRangeOf(wire) {
|
|
44
76
|
if (wire.empty) {
|
|
45
77
|
return Object.freeze({
|
|
@@ -74,70 +106,81 @@ function freshRangeOf(wire) {
|
|
|
74
106
|
}
|
|
75
107
|
});
|
|
76
108
|
}
|
|
77
|
-
/**
|
|
78
|
-
* The runtime discriminant of {@link Abandon} values — a property probe is
|
|
79
|
-
* how `write`/`writeFrom` distinguish "abort without committing" from an
|
|
80
|
-
* ordinary callback result, never a guess about the host's own value shapes.
|
|
81
|
-
*/
|
|
82
109
|
const abandonMark = Symbol("bumbledb.abandon");
|
|
83
|
-
/**
|
|
84
|
-
* Wraps a payload in the {@link Abandon} sentinel — the one way a write
|
|
85
|
-
* callback declines to commit: `return abandon(payload)` aborts the delta
|
|
86
|
-
* (nothing is committed, not even an empty commit) and the write resolves
|
|
87
|
-
* to `{ ok: false, abandoned: payload }`, from `write` and `writeFrom`
|
|
88
|
-
* alike (R10).
|
|
89
|
-
*/
|
|
90
110
|
function abandon(payload) {
|
|
91
111
|
return Object.freeze({ [abandonMark]: true, payload });
|
|
92
112
|
}
|
|
93
|
-
/**
|
|
94
|
-
* Narrows a write callback result to the abandon sentinel. The probe is
|
|
95
|
-
* the private {@link abandonMark} symbol only {@link abandon} sets, and
|
|
96
|
-
* `R`'s `Abandon` arm is the only way a sentinel can flow out of the
|
|
97
|
-
* callback — so the narrowed payload type is sound by construction.
|
|
98
|
-
*/
|
|
99
113
|
function isAbandon(value) {
|
|
100
114
|
return typeof value === "object" && value !== null && abandonMark in value;
|
|
101
115
|
}
|
|
102
|
-
/**
|
|
103
|
-
* The abandon outcome's trusted admission seam: the value's shape is the
|
|
104
|
-
* checkable half (the sentinel mark only {@link abandon} mints, and the
|
|
105
|
-
* outcome carrying that sentinel's own payload), and the sentinel's
|
|
106
|
-
* existence IS the proof `R` carries an `Abandon` arm — so the outcome is
|
|
107
|
-
* admitted at the conditional {@link AbandonedArm} face the type tier
|
|
108
|
-
* cannot resolve over an open `R`.
|
|
109
|
-
*/
|
|
110
116
|
function isAbandonedOutcome(outcome, sentinel) {
|
|
111
117
|
return isAbandon(sentinel) && outcome.abandoned === sentinel.payload;
|
|
112
118
|
}
|
|
113
|
-
/** Builds the abandoned write outcome from the callback's own sentinel (the R10 arm's one mint). */
|
|
114
119
|
function abandonedOutcome(sentinel) {
|
|
115
|
-
const outcome = Object.freeze({
|
|
120
|
+
const outcome = Object.freeze({ tag: "abandoned", abandoned: sentinel.payload });
|
|
116
121
|
if (!isAbandonedOutcome(outcome, sentinel)) {
|
|
117
122
|
throw errors.new("bumbledb abandon outcome construction incomplete");
|
|
118
123
|
}
|
|
119
124
|
return outcome;
|
|
120
125
|
}
|
|
121
|
-
|
|
122
|
-
* The module-private inference slot of {@link Prepared}: an optional symbol
|
|
123
|
-
* property (never set at runtime) that keeps the prepared value's `Row` and
|
|
124
|
-
* `Params` type arguments load-bearing, so `execute` infers the typed rows
|
|
125
|
-
* and the typed params object from the value alone — the query module's
|
|
126
|
-
* `inferred` pattern, local to this module. A type-level carrier only:
|
|
127
|
-
* values stay bare, nothing is asserted.
|
|
128
|
-
*/
|
|
126
|
+
const witnessTypes = Symbol("bumbledb.witness.types");
|
|
129
127
|
const preparedTypes = Symbol("bumbledb.prepared.types");
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
128
|
+
function decodeOffendingFact(member, relation, fact) {
|
|
129
|
+
const declared = sealedFieldsOf(member);
|
|
130
|
+
const decoded = {};
|
|
131
|
+
for (const cell of fact.fields) {
|
|
132
|
+
const cited = declared.find(function byName(candidate) {
|
|
133
|
+
return candidate.name === cell.name;
|
|
134
|
+
});
|
|
135
|
+
const roster = rosterOf(cited?.field);
|
|
136
|
+
decoded[cell.name] =
|
|
137
|
+
roster !== undefined
|
|
138
|
+
? handleOf(`violation fact ${fact.relation} field ${cell.name}`, roster, cell.value)
|
|
139
|
+
: cell.value;
|
|
140
|
+
}
|
|
141
|
+
return Object.freeze({ relation, fact: Object.freeze(decoded) });
|
|
142
|
+
}
|
|
143
|
+
function violationFromEntry(entry, wire, facts) {
|
|
144
|
+
const canonical = wire.canonical;
|
|
145
|
+
if (entry.kind === "functionality") {
|
|
146
|
+
if (!("statement" in entry)) {
|
|
147
|
+
return Object.freeze({ kind: "functionality", statement: undefined, canonical, facts });
|
|
148
|
+
}
|
|
149
|
+
return Object.freeze({ kind: "functionality", statement: entry.statement, canonical, facts });
|
|
150
|
+
}
|
|
151
|
+
if (entry.kind === "capacity") {
|
|
152
|
+
if (wire.kind !== "capacity") {
|
|
153
|
+
throw errors.new(`bumbledb violation ${wire.statementId} is a capacity slot without a measure`);
|
|
154
|
+
}
|
|
155
|
+
return Object.freeze({
|
|
156
|
+
kind: "capacity",
|
|
157
|
+
statement: entry.statement,
|
|
158
|
+
canonical,
|
|
159
|
+
measure: wire.measure,
|
|
160
|
+
facts
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
if (wire.kind !== "containment") {
|
|
164
|
+
throw errors.new(`bumbledb violation ${wire.statementId} is a containment slot without a direction`);
|
|
165
|
+
}
|
|
166
|
+
if (entry.kind === "mirrors") {
|
|
167
|
+
return Object.freeze({
|
|
168
|
+
kind: "containment",
|
|
169
|
+
statement: entry.statement,
|
|
170
|
+
canonical,
|
|
171
|
+
direction: wire.direction,
|
|
172
|
+
orientation: entry.orientation,
|
|
173
|
+
facts
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
return Object.freeze({
|
|
177
|
+
kind: "containment",
|
|
178
|
+
statement: entry.statement,
|
|
179
|
+
canonical,
|
|
180
|
+
direction: wire.direction,
|
|
181
|
+
facts
|
|
182
|
+
});
|
|
183
|
+
}
|
|
141
184
|
function materializedEntries(theory) {
|
|
142
185
|
const entries = impliedKeyEntries(theory);
|
|
143
186
|
for (const statement of theory.statements) {
|
|
@@ -145,13 +188,6 @@ function materializedEntries(theory) {
|
|
|
145
188
|
}
|
|
146
189
|
return entries;
|
|
147
190
|
}
|
|
148
|
-
/**
|
|
149
|
-
* The engine-materialized implied keys, in the engine's pinned order: one
|
|
150
|
-
* auto-key per fresh field (relation declaration order, then field order),
|
|
151
|
-
* then one closed auto-key `R(id) -> R` per closed relation (declaration
|
|
152
|
-
* order). These slots carry no SDK statement value — the engine owns them
|
|
153
|
-
* (`schema()` rejects an explicit duplicate).
|
|
154
|
-
*/
|
|
155
191
|
function impliedKeyEntries(theory) {
|
|
156
192
|
const entries = [];
|
|
157
193
|
for (const member of Object.values(theory.relations)) {
|
|
@@ -179,12 +215,6 @@ function impliedKeyEntries(theory) {
|
|
|
179
215
|
}
|
|
180
216
|
return entries;
|
|
181
217
|
}
|
|
182
|
-
/**
|
|
183
|
-
* One declared statement's materialized slots: a key or capacity statement
|
|
184
|
-
* occupies one, a `mirrors` occupies two adjacent slots (the engine lowers
|
|
185
|
-
* `==` to two containments, `source <= target` first), both owned by the
|
|
186
|
-
* one SDK value.
|
|
187
|
-
*/
|
|
188
218
|
function declaredEntries(statement) {
|
|
189
219
|
const data = statement.data;
|
|
190
220
|
switch (data.kind) {
|
|
@@ -221,26 +251,9 @@ function declaredEntries(statement) {
|
|
|
221
251
|
function isThenable(value) {
|
|
222
252
|
return typeof value === "object" && value !== null && "then" in value && typeof value.then === "function";
|
|
223
253
|
}
|
|
224
|
-
/**
|
|
225
|
-
* Narrows a keyed-get middle argument to a statement value (vs a key
|
|
226
|
-
* object) through the statement module's admission brand — a
|
|
227
|
-
* REPRESENTATION, never a shape probe: fact cell shapes are structurally
|
|
228
|
-
* OPEN (an interval value carrying an excess `kind` property is a legal
|
|
229
|
-
* cell), so no property probe could ever be sound here, but no host-built
|
|
230
|
-
* key object can spell the module-private brand symbol.
|
|
231
|
-
*/
|
|
232
254
|
function isStatementValue(value) {
|
|
233
255
|
return isStatement(value);
|
|
234
256
|
}
|
|
235
|
-
/**
|
|
236
|
-
* THE one selector dispatch of the `get` overload pair (primary-key vs
|
|
237
|
-
* key-statement, `docs/architecture/70-api.md` § the freeze): judges the
|
|
238
|
-
* middle argument once and hands the narrowed pieces to the chosen
|
|
239
|
-
* continuation. `Db.get` and the read scope's `get` both dispatch through
|
|
240
|
-
* here, so the two mismatch refusals speak with one voice and the symmetry
|
|
241
|
-
* rule (`db.get(...) === db.read(snap => snap.get(...))`) holds by
|
|
242
|
-
* construction.
|
|
243
|
-
*/
|
|
244
257
|
function selectKeyRead(keyOrStatement, declaredKey, byStatement, byPrimary) {
|
|
245
258
|
if (declaredKey !== undefined) {
|
|
246
259
|
if (!isStatementValue(keyOrStatement)) {
|
|
@@ -253,16 +266,6 @@ function selectKeyRead(keyOrStatement, declaredKey, byStatement, byPrimary) {
|
|
|
253
266
|
}
|
|
254
267
|
return byPrimary(keyOrStatement);
|
|
255
268
|
}
|
|
256
|
-
/**
|
|
257
|
-
* Builds the id-resolution tables from the manifest, verifying the SDK's
|
|
258
|
-
* positional mirror against the engine's reported order — any drift
|
|
259
|
-
* (count, kind, id, or membership) is a construction-time failure, never a
|
|
260
|
-
* silent misattribution of a violation to the wrong statement value. The
|
|
261
|
-
* declaration-ordinal law the query lowering leans on is verified in the
|
|
262
|
-
* same walks: relation ids and sealed field ids both equal declaration
|
|
263
|
-
* order, so a constructed `Tables` IS the proof and `prepare` inherits it
|
|
264
|
-
* structurally — never a silently misaddressed query.
|
|
265
|
-
*/
|
|
266
269
|
function tablesOf(theory, manifest) {
|
|
267
270
|
const entries = materializedEntries(theory);
|
|
268
271
|
if (entries.length !== manifest.statements.length) {
|
|
@@ -312,16 +315,39 @@ function tablesOf(theory, manifest) {
|
|
|
312
315
|
});
|
|
313
316
|
return Object.freeze({ relations, statements: Object.freeze(entries) });
|
|
314
317
|
}
|
|
315
|
-
|
|
316
|
-
const
|
|
317
|
-
|
|
318
|
+
function tablesFromTheory(theory) {
|
|
319
|
+
const entries = materializedEntries(theory);
|
|
320
|
+
const relations = new Map();
|
|
321
|
+
Object.keys(theory.relations).forEach(function byOrdinal(name, ordinal) {
|
|
322
|
+
const member = theory.relations[name];
|
|
323
|
+
if (member === undefined) {
|
|
324
|
+
throw errors.new(`bumbledb theory has no relation ${name}`);
|
|
325
|
+
}
|
|
326
|
+
const fieldIds = new Map();
|
|
327
|
+
sealedFieldsOf(member).forEach(function byField(declared, fieldOrdinal) {
|
|
328
|
+
fieldIds.set(declared.name, fieldOrdinal);
|
|
329
|
+
});
|
|
330
|
+
let primaryKey;
|
|
331
|
+
entries.forEach(function firstOwnedKey(entry, index) {
|
|
332
|
+
if (primaryKey === undefined && entry.kind === "functionality" && entry.owner === name) {
|
|
333
|
+
primaryKey = Object.freeze({ statementId: index, projection: entry.projection });
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
relations.set(name, Object.freeze({ id: ordinal, member, fieldIds, primaryKey }));
|
|
337
|
+
});
|
|
338
|
+
return Object.freeze({ relations, statements: Object.freeze(entries) });
|
|
339
|
+
}
|
|
340
|
+
const instanceStates = new WeakMap();
|
|
341
|
+
const witnessStates = new WeakMap();
|
|
342
|
+
const witnessReclaimer = new FinalizationRegistry(function reclaimWitness(handle) {
|
|
343
|
+
const closed = errors.trySync(function closeWitness() {
|
|
344
|
+
native.witnessClose(handle);
|
|
345
|
+
});
|
|
346
|
+
if (closed.error) {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
});
|
|
318
350
|
const preparedPlans = new WeakMap();
|
|
319
|
-
/**
|
|
320
|
-
* Reclaims the engine-side plan of a garbage-collected {@link Prepared}
|
|
321
|
-
* value. RECLAMATION ONLY, never correctness: a plan the collector never
|
|
322
|
-
* visits is idle engine memory until process exit, and a failure to close
|
|
323
|
-
* is swallowed (there is no one left to care — the owning value is gone).
|
|
324
|
-
*/
|
|
325
351
|
const planReclaimer = new FinalizationRegistry(function reclaimPlan(handle) {
|
|
326
352
|
const closed = errors.trySync(function closePlan() {
|
|
327
353
|
native.preparedClose(handle);
|
|
@@ -330,130 +356,219 @@ const planReclaimer = new FinalizationRegistry(function reclaimPlan(handle) {
|
|
|
330
356
|
return;
|
|
331
357
|
}
|
|
332
358
|
});
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
* cache holds the environment handle until the exit hook closes it.
|
|
344
|
-
*/
|
|
345
|
-
function openDb(handle, theory, manifest) {
|
|
346
|
-
const tables = tablesOf(theory, manifest);
|
|
347
|
-
/** This store's identity token: read scopes and prepared values carry it, so cross-store use is a typed refusal. */
|
|
348
|
-
const owner = Object.freeze({});
|
|
349
|
-
function isMemberName(name) {
|
|
350
|
-
return tables.relations.has(name);
|
|
351
|
-
}
|
|
352
|
-
function resolveOrdinary(relation) {
|
|
353
|
-
const entry = tables.relations.get(relation.name);
|
|
354
|
-
if (entry === undefined || entry.member !== relation) {
|
|
355
|
-
throw errors.new(`relation ${relation.name} is not a member of schema ${theory.name}`);
|
|
359
|
+
const ErrAsyncCallback = errors.new("bumbledb asyncCallback: a read or write callback returned a thenable — the callback is synchronous");
|
|
360
|
+
const ErrSpentHandle = errors.new("bumbledb spentHandle: a consumed builder, instance, or witness was used");
|
|
361
|
+
const ErrUseAfterScope = errors.new("bumbledb useAfterScope: a stashed read instance or write transaction was used after its callback returned");
|
|
362
|
+
const ErrForeignPrepared = errors.new("bumbledb foreignPrepared: a prepared query met a foreign instance");
|
|
363
|
+
const ErrForeignWitness = errors.new("bumbledb foreignWitness: a witness met a foreign store");
|
|
364
|
+
function catalogMethods(theory, tables, owner, assertLive, ops) {
|
|
365
|
+
function planOf(prepared) {
|
|
366
|
+
const plan = preparedPlans.get(prepared);
|
|
367
|
+
if (plan === undefined) {
|
|
368
|
+
throw errors.wrap(ErrForeignPrepared, "bumbledb execute target is not a prepared value of this SDK");
|
|
356
369
|
}
|
|
357
|
-
if (
|
|
358
|
-
throw errors.
|
|
370
|
+
if (plan.owner !== owner) {
|
|
371
|
+
throw errors.wrap(ErrForeignPrepared, `bumbledb prepared value was prepared by a different store than this one (schema ${theory.name})`);
|
|
359
372
|
}
|
|
360
|
-
return
|
|
373
|
+
return plan;
|
|
361
374
|
}
|
|
362
|
-
function
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
const decoded = {};
|
|
369
|
-
for (const cell of fact.fields) {
|
|
370
|
-
const cited = declared.find(function byName(candidate) {
|
|
371
|
-
return candidate.name === cell.name;
|
|
372
|
-
});
|
|
373
|
-
const roster = rosterOf(cited?.field);
|
|
374
|
-
decoded[cell.name] =
|
|
375
|
-
roster !== undefined
|
|
376
|
-
? handleOf(`violation fact ${fact.relation} field ${cell.name}`, roster, cell.value)
|
|
377
|
-
: cell.value;
|
|
378
|
-
}
|
|
379
|
-
return Object.freeze({ relation: fact.relation, fact: Object.freeze(decoded) });
|
|
375
|
+
function contains(relation, fact) {
|
|
376
|
+
assertLive();
|
|
377
|
+
const entry = ordinaryEntry(tables, theory, relation);
|
|
378
|
+
return bridged("bumbledb instance contains", function readContains() {
|
|
379
|
+
return ops.contains(entry.id, rowOf(relation.data, recordOf(fact)));
|
|
380
|
+
});
|
|
380
381
|
}
|
|
381
|
-
function
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
if (entry.kind === "capacity") {
|
|
395
|
-
if (wire.kind !== "capacity") {
|
|
396
|
-
throw errors.new(`bumbledb violation ${wire.statementId} is a capacity slot without a measure`);
|
|
382
|
+
function get(relation, keyOrStatement, declaredKey) {
|
|
383
|
+
assertLive();
|
|
384
|
+
const entry = ordinaryEntry(tables, theory, relation);
|
|
385
|
+
return selectKeyRead(keyOrStatement, declaredKey, function byStatement(statement, key) {
|
|
386
|
+
const selected = declaredKeyOf(tables, theory, relation, statement);
|
|
387
|
+
const row = bridged("bumbledb instance get", function readGet() {
|
|
388
|
+
return ops.get(entry.id, selected.statementId, keyRowOf(relation.data, selected.projection, recordOf(key)));
|
|
389
|
+
});
|
|
390
|
+
return row === null ? undefined : factOf(relation, row);
|
|
391
|
+
}, function byPrimary(key) {
|
|
392
|
+
const primaryKey = entry.primaryKey;
|
|
393
|
+
if (primaryKey === undefined) {
|
|
394
|
+
throw errors.new(`relation ${relation.name} has no candidate key — keyed get requires a fresh field or a declared key statement`);
|
|
397
395
|
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
statement: entry.statement,
|
|
401
|
-
canonical,
|
|
402
|
-
measure: wire.measure,
|
|
403
|
-
facts
|
|
396
|
+
const row = bridged("bumbledb instance get", function readGet() {
|
|
397
|
+
return ops.get(entry.id, primaryKey.statementId, keyRowOf(relation.data, primaryKey.projection, recordOf(key)));
|
|
404
398
|
});
|
|
399
|
+
return row === null ? undefined : factOf(relation, row);
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
function scan(relation) {
|
|
403
|
+
assertLive();
|
|
404
|
+
const entry = ordinaryEntry(tables, theory, relation);
|
|
405
|
+
const rows = bridged("bumbledb instance scan", function readScan() {
|
|
406
|
+
return ops.scan(entry.id);
|
|
407
|
+
});
|
|
408
|
+
return rows.map(function decodeRow(row) {
|
|
409
|
+
return factOf(relation, row);
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
function count(relation) {
|
|
413
|
+
assertLive();
|
|
414
|
+
const entry = ordinaryEntry(tables, theory, relation);
|
|
415
|
+
return bridged("bumbledb instance count", function readCount() {
|
|
416
|
+
return ops.count(entry.id);
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
function execute(prepared, params) {
|
|
420
|
+
assertLive();
|
|
421
|
+
const plan = planOf(prepared);
|
|
422
|
+
const wire = wireParams(plan.params, recordOf(params));
|
|
423
|
+
const rows = bridged("execute bumbledb prepared query", function callExecute() {
|
|
424
|
+
return ops.execute(plan.handle, wire);
|
|
425
|
+
});
|
|
426
|
+
return decodeAnswers(plan.finds, rows);
|
|
427
|
+
}
|
|
428
|
+
function prepare(q) {
|
|
429
|
+
assertLive();
|
|
430
|
+
if (q.schema !== theory) {
|
|
431
|
+
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`);
|
|
405
432
|
}
|
|
406
|
-
|
|
407
|
-
|
|
433
|
+
const queryIr = lowerQuery(q);
|
|
434
|
+
const outcome = bridged("prepare bumbledb query", function callPrepare() {
|
|
435
|
+
return ops.prepare(queryIr);
|
|
436
|
+
});
|
|
437
|
+
if (!outcome.ok) {
|
|
438
|
+
throwPrepareRefusal(outcome.message);
|
|
408
439
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
440
|
+
const prepared = Object.freeze({});
|
|
441
|
+
preparedPlans.set(prepared, Object.freeze({
|
|
442
|
+
handle: outcome.prepared,
|
|
443
|
+
owner,
|
|
444
|
+
params: q.data.params,
|
|
445
|
+
finds: q.data.finds
|
|
446
|
+
}));
|
|
447
|
+
planReclaimer.register(prepared, outcome.prepared);
|
|
448
|
+
return prepared;
|
|
449
|
+
}
|
|
450
|
+
return { scan, count, get, contains, execute, prepare };
|
|
451
|
+
}
|
|
452
|
+
function ordinaryEntry(tables, theory, relation) {
|
|
453
|
+
const entry = tables.relations.get(relation.name);
|
|
454
|
+
if (entry === undefined || entry.member !== relation) {
|
|
455
|
+
throw errors.new(`relation ${relation.name} is not a member of schema ${theory.name}`);
|
|
456
|
+
}
|
|
457
|
+
if (isClosedMember(relation)) {
|
|
458
|
+
throw errors.new(`relation ${relation.name} is closed — its extension is schema data (axioms), never scanned or written`);
|
|
459
|
+
}
|
|
460
|
+
return entry;
|
|
461
|
+
}
|
|
462
|
+
function declaredKeyOf(tables, theory, relation, statement) {
|
|
463
|
+
const statementId = tables.statements.findIndex(function byIdentity(candidate) {
|
|
464
|
+
return "statement" in candidate && candidate.statement === statement;
|
|
465
|
+
});
|
|
466
|
+
const entry = tables.statements[statementId];
|
|
467
|
+
if (entry === undefined) {
|
|
468
|
+
throw errors.new(`keyed get statement is not a declared statement of schema ${theory.name} — statement identity is the membership rule`);
|
|
469
|
+
}
|
|
470
|
+
if (entry.kind !== "functionality") {
|
|
471
|
+
throw errors.new("keyed get takes a key() statement — containments and capacity statements key nothing");
|
|
472
|
+
}
|
|
473
|
+
if (entry.owner !== relation.name) {
|
|
474
|
+
throw errors.new(`keyed get statement keys ${entry.owner}, not ${relation.name} — the statement must be a declared key of the relation it reads`);
|
|
475
|
+
}
|
|
476
|
+
return Object.freeze({ statementId, projection: entry.projection });
|
|
477
|
+
}
|
|
478
|
+
function overlayMethods(theory, tables, assertLive, reads) {
|
|
479
|
+
function contains(relation, fact) {
|
|
480
|
+
assertLive();
|
|
481
|
+
const entry = ordinaryEntry(tables, theory, relation);
|
|
482
|
+
return reads.contains(entry.id, rowOf(relation.data, recordOf(fact)));
|
|
483
|
+
}
|
|
484
|
+
function readThroughKey(relation, entry, selected, key) {
|
|
485
|
+
const row = reads.get(entry.id, selected.statementId, keyRowOf(relation.data, selected.projection, key));
|
|
486
|
+
if (row === null) {
|
|
487
|
+
return undefined;
|
|
418
488
|
}
|
|
419
|
-
return
|
|
420
|
-
kind: "containment",
|
|
421
|
-
statement: entry.statement,
|
|
422
|
-
canonical,
|
|
423
|
-
direction: wire.direction,
|
|
424
|
-
facts
|
|
425
|
-
});
|
|
489
|
+
return factOf(relation, row);
|
|
426
490
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
491
|
+
function get(relation, keyOrStatement, declaredKey) {
|
|
492
|
+
assertLive();
|
|
493
|
+
const entry = ordinaryEntry(tables, theory, relation);
|
|
494
|
+
return selectKeyRead(keyOrStatement, declaredKey, function byStatement(statement, key) {
|
|
495
|
+
return readThroughKey(relation, entry, declaredKeyOf(tables, theory, relation, statement), recordOf(key));
|
|
496
|
+
}, function byPrimary(key) {
|
|
497
|
+
const primaryKey = entry.primaryKey;
|
|
498
|
+
if (primaryKey === undefined) {
|
|
499
|
+
throw errors.new(`relation ${relation.name} has no candidate key — keyed get requires a fresh field or a declared key statement`);
|
|
500
|
+
}
|
|
501
|
+
return readThroughKey(relation, entry, primaryKey, recordOf(key));
|
|
437
502
|
});
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
503
|
+
}
|
|
504
|
+
return { contains, get };
|
|
505
|
+
}
|
|
506
|
+
function createReadInstance(nativeHandle, theory, tables, owner) {
|
|
507
|
+
const state = { handle: nativeHandle, live: true, owner };
|
|
508
|
+
function assertLive() {
|
|
509
|
+
if (!state.live) {
|
|
510
|
+
throw errors.wrap(ErrUseAfterScope, "bumbledb read instance is invalidated — its owning callback already returned");
|
|
441
511
|
}
|
|
442
|
-
|
|
443
|
-
|
|
512
|
+
}
|
|
513
|
+
const methods = catalogMethods(theory, tables, owner, assertLive, {
|
|
514
|
+
scan(relationId) {
|
|
515
|
+
return native.instanceScan(state.handle, relationId);
|
|
516
|
+
},
|
|
517
|
+
count(relationId) {
|
|
518
|
+
return native.instanceCount(state.handle, relationId);
|
|
519
|
+
},
|
|
520
|
+
contains(relationId, values) {
|
|
521
|
+
return native.instanceContains(state.handle, relationId, values);
|
|
522
|
+
},
|
|
523
|
+
get(relationId, statementId, keyValues) {
|
|
524
|
+
return native.instanceGet(state.handle, relationId, statementId, keyValues);
|
|
525
|
+
},
|
|
526
|
+
prepare(query) {
|
|
527
|
+
return native.instancePrepare(state.handle, query);
|
|
528
|
+
},
|
|
529
|
+
execute(prepared, params) {
|
|
530
|
+
return native.preparedExecute(prepared, state.handle, params);
|
|
444
531
|
}
|
|
445
|
-
|
|
446
|
-
|
|
532
|
+
});
|
|
533
|
+
const instance = Object.freeze({
|
|
534
|
+
get generation() {
|
|
535
|
+
assertLive();
|
|
536
|
+
return bridged("bumbledb instance generation", function readGeneration() {
|
|
537
|
+
return native.instanceGeneration(state.handle);
|
|
538
|
+
});
|
|
539
|
+
},
|
|
540
|
+
...methods
|
|
541
|
+
});
|
|
542
|
+
instanceStates.set(instance, state);
|
|
543
|
+
return instance;
|
|
544
|
+
}
|
|
545
|
+
function openDb(handle, theory, manifest) {
|
|
546
|
+
const tables = tablesOf(theory, manifest);
|
|
547
|
+
/** This store's identity token: read scopes and prepared values carry it, so cross-store use is a typed refusal. */
|
|
548
|
+
const owner = Object.freeze({});
|
|
549
|
+
function isMemberName(name) {
|
|
550
|
+
return tables.relations.has(name);
|
|
551
|
+
}
|
|
552
|
+
function violationOf(wire) {
|
|
553
|
+
const entry = tables.statements[wire.statementId];
|
|
554
|
+
if (entry === undefined) {
|
|
555
|
+
throw errors.new(`bumbledb violation cites unknown statement id ${wire.statementId}`);
|
|
447
556
|
}
|
|
448
|
-
|
|
557
|
+
const facts = Object.freeze(wire.facts.map(function offending(fact) {
|
|
558
|
+
const rel = tables.relations.get(fact.relation);
|
|
559
|
+
if (rel === undefined || !isMemberName(fact.relation)) {
|
|
560
|
+
throw errors.new(`bumbledb violation cites unknown relation ${fact.relation}`);
|
|
561
|
+
}
|
|
562
|
+
return decodeOffendingFact(rel.member, fact.relation, fact);
|
|
563
|
+
}));
|
|
564
|
+
return violationFromEntry(entry, wire, facts);
|
|
449
565
|
}
|
|
450
566
|
function pointReadsOf(assertLive, reads) {
|
|
451
567
|
function contains(relation, fact) {
|
|
452
568
|
assertLive();
|
|
453
|
-
const entry =
|
|
569
|
+
const entry = ordinaryEntry(tables, theory, relation);
|
|
454
570
|
return reads.contains(entry.id, rowOf(relation.data, recordOf(fact)));
|
|
455
571
|
}
|
|
456
|
-
/** One keyed point read through an already-resolved key, decoded to a fact (`undefined` on a miss). */
|
|
457
572
|
function readThroughKey(relation, entry, selected, key) {
|
|
458
573
|
const row = reads.get(entry.id, selected.statementId, keyRowOf(relation.data, selected.projection, key));
|
|
459
574
|
if (row === null) {
|
|
@@ -463,9 +578,9 @@ function openDb(handle, theory, manifest) {
|
|
|
463
578
|
}
|
|
464
579
|
function get(relation, keyOrStatement, declaredKey) {
|
|
465
580
|
assertLive();
|
|
466
|
-
const entry =
|
|
581
|
+
const entry = ordinaryEntry(tables, theory, relation);
|
|
467
582
|
return selectKeyRead(keyOrStatement, declaredKey, function byStatement(statement, key) {
|
|
468
|
-
return readThroughKey(relation, entry, declaredKeyOf(relation, statement), recordOf(key));
|
|
583
|
+
return readThroughKey(relation, entry, declaredKeyOf(tables, theory, relation, statement), recordOf(key));
|
|
469
584
|
}, function byPrimary(key) {
|
|
470
585
|
const primaryKey = entry.primaryKey;
|
|
471
586
|
if (primaryKey === undefined) {
|
|
@@ -476,172 +591,62 @@ function openDb(handle, theory, manifest) {
|
|
|
476
591
|
}
|
|
477
592
|
return { contains, get };
|
|
478
593
|
}
|
|
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;
|
|
594
|
+
function pinPrepared(preparedHandle, q) {
|
|
595
|
+
const prepared = Object.freeze({});
|
|
596
|
+
preparedPlans.set(prepared, Object.freeze({
|
|
597
|
+
handle: preparedHandle,
|
|
598
|
+
owner,
|
|
599
|
+
params: q.data.params,
|
|
600
|
+
finds: q.data.finds
|
|
601
|
+
}));
|
|
602
|
+
planReclaimer.register(prepared, preparedHandle);
|
|
603
|
+
return prepared;
|
|
492
604
|
}
|
|
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);
|
|
605
|
+
function makeWitness(nativeHandle) {
|
|
606
|
+
const state = { handle: nativeHandle, spent: false, owner };
|
|
607
|
+
const witness = Object.freeze({
|
|
608
|
+
[Symbol.dispose]() {
|
|
609
|
+
if (state.spent) {
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
state.spent = true;
|
|
613
|
+
bridged("close bumbledb witness", function closeWitness() {
|
|
614
|
+
native.witnessClose(nativeHandle);
|
|
515
615
|
});
|
|
516
616
|
}
|
|
517
617
|
});
|
|
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;
|
|
552
|
-
}
|
|
553
|
-
/**
|
|
554
|
-
* Live-handle accounting (diagnostic law, prod EINVAL 2026-07-17): every
|
|
555
|
-
* snapshot open/close is counted so a write-begin failure can report how
|
|
556
|
-
* many read handles were live at the fault — a leaked scope is invisible
|
|
557
|
-
* until the exact moment it matters, so the failure carries the census.
|
|
558
|
-
*/
|
|
559
|
-
let liveSnapshots = 0;
|
|
560
|
-
/**
|
|
561
|
-
* Opens one snapshot and its scope state (live until the owner flips
|
|
562
|
-
* it). The witnessed generation rides the snapshot open itself — one
|
|
563
|
-
* crossing carries both (finding 016), so the fault-pairing close
|
|
564
|
-
* branch a second `dbGeneration` call needed is structurally gone.
|
|
565
|
-
*/
|
|
566
|
-
function openScopeState() {
|
|
567
|
-
const opened = bridged("open bumbledb snapshot", function openSnapshot() {
|
|
568
|
-
return native.dbSnapshot(handle);
|
|
569
|
-
});
|
|
570
|
-
liveSnapshots += 1;
|
|
571
|
-
return { handle: opened.snapshot, generation: opened.generation, live: true, closed: false, owner };
|
|
572
|
-
}
|
|
573
|
-
/**
|
|
574
|
-
* Closes a scope's snapshot after the owner invalidated it — a LATCH:
|
|
575
|
-
* the snapshot closes exactly once, whichever of the owning call and
|
|
576
|
-
* the scope's own `Symbol.dispose` gets there first, so an early
|
|
577
|
-
* in-callback disposal never double-closes (and never double-counts
|
|
578
|
-
* the census).
|
|
579
|
-
*/
|
|
580
|
-
function closeScopeState(state) {
|
|
581
|
-
if (state.closed) {
|
|
582
|
-
return;
|
|
583
|
-
}
|
|
584
|
-
state.closed = true;
|
|
585
|
-
bridged("close bumbledb snapshot", function closeSnapshot() {
|
|
586
|
-
native.snapshotClose(state.handle);
|
|
587
|
-
});
|
|
588
|
-
liveSnapshots -= 1;
|
|
589
|
-
}
|
|
590
|
-
function read(fn) {
|
|
591
|
-
const state = openScopeState();
|
|
592
|
-
const scope = makeScope(state);
|
|
593
|
-
if (fn === undefined) {
|
|
594
|
-
/**
|
|
595
|
-
* The `using` acquisition (R12): the caller owns the lifetime —
|
|
596
|
-
* `using snap = db.read()` — and the scope's `Symbol.dispose`
|
|
597
|
-
* is the deterministic release, scope-shaped in the language's
|
|
598
|
-
* own syntax.
|
|
599
|
-
*/
|
|
600
|
-
return scope;
|
|
601
|
-
}
|
|
602
|
-
const result = errors.trySync(function runRead() {
|
|
603
|
-
return fn(scope);
|
|
604
|
-
});
|
|
605
|
-
state.live = false;
|
|
606
|
-
closeScopeState(state);
|
|
607
|
-
if (result.error) {
|
|
608
|
-
throw errors.wrap(result.error, "bumbledb read");
|
|
609
|
-
}
|
|
610
|
-
return result.data;
|
|
618
|
+
witnessStates.set(witness, state);
|
|
619
|
+
witnessReclaimer.register(witness, nativeHandle);
|
|
620
|
+
return witness;
|
|
611
621
|
}
|
|
612
|
-
function
|
|
613
|
-
return
|
|
614
|
-
return snap.scan(relation);
|
|
615
|
-
});
|
|
622
|
+
function makeInstance(nativeHandle) {
|
|
623
|
+
return createReadInstance(nativeHandle, theory, tables, owner);
|
|
616
624
|
}
|
|
617
|
-
function
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
625
|
+
function read(body) {
|
|
626
|
+
let captured;
|
|
627
|
+
const result = bridged("bumbledb read", function runRead() {
|
|
628
|
+
return native.dbRead(handle, function onRead(nativeInstance, nativeWitness) {
|
|
629
|
+
const instance = makeInstance(nativeInstance);
|
|
630
|
+
const witness = makeWitness(nativeWitness);
|
|
631
|
+
const value = body(instance, witness);
|
|
632
|
+
const state = instanceStates.get(instance);
|
|
633
|
+
if (state !== undefined) {
|
|
634
|
+
state.live = false;
|
|
635
|
+
}
|
|
636
|
+
if (isThenable(value)) {
|
|
637
|
+
throw errors.wrap(ErrAsyncCallback, "bumbledb read callback returned a thenable");
|
|
638
|
+
}
|
|
639
|
+
captured = value;
|
|
640
|
+
return value;
|
|
623
641
|
});
|
|
624
642
|
});
|
|
643
|
+
return (captured ?? result);
|
|
625
644
|
}
|
|
626
|
-
function contains(relation, fact) {
|
|
627
|
-
return read(function containsInScope(snap) {
|
|
628
|
-
return snap.contains(relation, fact);
|
|
629
|
-
});
|
|
630
|
-
}
|
|
631
|
-
function execute(prepared, params) {
|
|
632
|
-
return read(function executeInScope(snap) {
|
|
633
|
-
return snap.execute(prepared, params);
|
|
634
|
-
});
|
|
635
|
-
}
|
|
636
|
-
/**
|
|
637
|
-
* Builds one {@link Tx} over a transaction-handle thunk: `write` and
|
|
638
|
-
* `writeFrom` pass an already-begun handle.
|
|
639
|
-
*/
|
|
640
645
|
function makeTx(resolveTx) {
|
|
641
646
|
const txState = { spent: false };
|
|
642
647
|
function assertLive() {
|
|
643
648
|
if (txState.spent) {
|
|
644
|
-
throw errors.
|
|
649
|
+
throw errors.wrap(ErrUseAfterScope, "bumbledb write transaction is spent");
|
|
645
650
|
}
|
|
646
651
|
}
|
|
647
652
|
const reads = pointReadsOf(assertLive, {
|
|
@@ -660,33 +665,27 @@ function openDb(handle, theory, manifest) {
|
|
|
660
665
|
});
|
|
661
666
|
function insert(relation, facts) {
|
|
662
667
|
assertLive();
|
|
663
|
-
const
|
|
664
|
-
for (const fact of facts) {
|
|
665
|
-
rows.push(rowOf(relation.data, recordOf(fact)));
|
|
666
|
-
}
|
|
667
|
-
const entry = resolveOrdinary(relation);
|
|
668
|
+
const entry = ordinaryEntry(tables, theory, relation);
|
|
668
669
|
const txHandle = resolveTx();
|
|
669
|
-
|
|
670
|
-
return
|
|
670
|
+
return mutateCollection(relation, facts, function applyCells(rows, cells) {
|
|
671
|
+
return bridged("bumbledb tx insert", function record() {
|
|
672
|
+
return native.txInsert(txHandle, entry.id, rows, cells);
|
|
673
|
+
});
|
|
671
674
|
});
|
|
672
|
-
return Object.freeze({ submitted: report.submitted, changed: report.changed });
|
|
673
675
|
}
|
|
674
676
|
function remove(relation, facts) {
|
|
675
677
|
assertLive();
|
|
676
|
-
const
|
|
677
|
-
for (const fact of facts) {
|
|
678
|
-
rows.push(rowOf(relation.data, recordOf(fact)));
|
|
679
|
-
}
|
|
680
|
-
const entry = resolveOrdinary(relation);
|
|
678
|
+
const entry = ordinaryEntry(tables, theory, relation);
|
|
681
679
|
const txHandle = resolveTx();
|
|
680
|
+
const flat = rowsOf(relation, facts);
|
|
682
681
|
const report = bridged("bumbledb tx delete", function record() {
|
|
683
|
-
return native.txDelete(txHandle, entry.id, rows);
|
|
682
|
+
return native.txDelete(txHandle, entry.id, flat.rows, flat.cells);
|
|
684
683
|
});
|
|
685
684
|
return Object.freeze({ submitted: report.submitted, changed: report.changed });
|
|
686
685
|
}
|
|
687
686
|
function reserve(relation, field, count) {
|
|
688
687
|
assertLive();
|
|
689
|
-
const entry =
|
|
688
|
+
const entry = ordinaryEntry(tables, theory, relation);
|
|
690
689
|
const declared = relation.data.fields.find(function byName(candidate) {
|
|
691
690
|
return candidate.name === field;
|
|
692
691
|
});
|
|
@@ -715,104 +714,80 @@ function openDb(handle, theory, manifest) {
|
|
|
715
714
|
}
|
|
716
715
|
return { tx, spend };
|
|
717
716
|
}
|
|
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);
|
|
717
|
+
function mapNativeWrite(nativeOutcome, built) {
|
|
718
|
+
if (nativeOutcome.tag === "moved") {
|
|
719
|
+
return Object.freeze({
|
|
720
|
+
tag: "moved",
|
|
721
|
+
witnessed: nativeOutcome.witnessed,
|
|
722
|
+
current: nativeOutcome.current
|
|
755
723
|
});
|
|
756
|
-
return abandonedOutcome(built.data);
|
|
757
724
|
}
|
|
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);
|
|
725
|
+
if (nativeOutcome.tag === "rejected") {
|
|
726
|
+
return Object.freeze({
|
|
727
|
+
tag: "rejected",
|
|
728
|
+
violations: Object.freeze(nativeOutcome.violations.map(violationOf))
|
|
773
729
|
});
|
|
774
|
-
if (aborted.error) {
|
|
775
|
-
}
|
|
776
|
-
throw errors.wrap(committed.error, "commit bumbledb write transaction");
|
|
777
730
|
}
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
731
|
+
if (nativeOutcome.tag === "abandoned") {
|
|
732
|
+
if (built === undefined || !isAbandon(built)) {
|
|
733
|
+
throw errors.new("bumbledb write abandoned without an abandon sentinel");
|
|
734
|
+
}
|
|
735
|
+
return abandonedOutcome(built);
|
|
781
736
|
}
|
|
782
737
|
return Object.freeze({
|
|
783
|
-
|
|
784
|
-
|
|
738
|
+
tag: "accepted",
|
|
739
|
+
value: Object.freeze({
|
|
740
|
+
value: built,
|
|
741
|
+
generation: nativeOutcome.generation
|
|
742
|
+
})
|
|
785
743
|
});
|
|
786
744
|
}
|
|
787
|
-
function
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
throw errors.new("bumbledb writeFrom snapshot is invalidated — its owning read callback already returned");
|
|
808
|
-
}
|
|
809
|
-
const witnessed = bridged("begin witnessed bumbledb write transaction", function begin() {
|
|
810
|
-
return native.dbWriteFrom(handle, snapState.handle);
|
|
745
|
+
function runWrite(invoke, fn) {
|
|
746
|
+
let built;
|
|
747
|
+
const nativeOutcome = bridged("bumbledb write", function callWrite() {
|
|
748
|
+
return invoke(function onWrite(txHandle) {
|
|
749
|
+
const made = makeTx(function resolveTx() {
|
|
750
|
+
return txHandle;
|
|
751
|
+
});
|
|
752
|
+
const result = errors.trySync(function buildDelta() {
|
|
753
|
+
return fn(made.tx);
|
|
754
|
+
});
|
|
755
|
+
made.spend();
|
|
756
|
+
if (result.error) {
|
|
757
|
+
throw errors.wrap(result.error, "build write delta");
|
|
758
|
+
}
|
|
759
|
+
if (isThenable(result.data)) {
|
|
760
|
+
throw errors.wrap(ErrAsyncCallback, "bumbledb write callback returned a thenable");
|
|
761
|
+
}
|
|
762
|
+
built = result.data;
|
|
763
|
+
return !isAbandon(result.data);
|
|
764
|
+
});
|
|
811
765
|
});
|
|
812
|
-
|
|
813
|
-
|
|
766
|
+
return mapNativeWrite(nativeOutcome, built);
|
|
767
|
+
}
|
|
768
|
+
function write(fn) {
|
|
769
|
+
const outcome = runWrite(function invoke(callback) {
|
|
770
|
+
return native.dbWrite(handle, callback);
|
|
771
|
+
}, fn);
|
|
772
|
+
if (outcome.tag === "moved") {
|
|
773
|
+
throw errors.new("bumbledb write reported moved — unconditional writes cannot move");
|
|
774
|
+
}
|
|
775
|
+
return outcome;
|
|
776
|
+
}
|
|
777
|
+
function writeFrom(witness, fn) {
|
|
778
|
+
const state = witnessStates.get(witness);
|
|
779
|
+
if (state === undefined) {
|
|
780
|
+
throw errors.wrap(ErrForeignWitness, "bumbledb writeFrom witness is not a witness of this SDK");
|
|
781
|
+
}
|
|
782
|
+
if (state.owner !== owner) {
|
|
783
|
+
throw errors.wrap(ErrForeignWitness, `bumbledb writeFrom witness belongs to a different store (schema ${theory.name})`);
|
|
784
|
+
}
|
|
785
|
+
if (state.spent) {
|
|
786
|
+
throw errors.wrap(ErrSpentHandle, "bumbledb writeFrom witness has been disposed");
|
|
814
787
|
}
|
|
815
|
-
return
|
|
788
|
+
return runWrite(function invoke(callback) {
|
|
789
|
+
return native.dbWriteFrom(handle, state.handle, callback);
|
|
790
|
+
}, fn);
|
|
816
791
|
}
|
|
817
792
|
function prepare(q) {
|
|
818
793
|
if (q.schema !== theory) {
|
|
@@ -823,114 +798,296 @@ function openDb(handle, theory, manifest) {
|
|
|
823
798
|
return native.dbPrepare(handle, queryIr);
|
|
824
799
|
});
|
|
825
800
|
if (!outcome.ok) {
|
|
826
|
-
|
|
801
|
+
throwPrepareRefusal(outcome.message);
|
|
827
802
|
}
|
|
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;
|
|
803
|
+
return pinPrepared(outcome.prepared, q);
|
|
838
804
|
}
|
|
839
805
|
return Object.freeze({
|
|
840
806
|
schema: theory,
|
|
841
807
|
read,
|
|
842
|
-
scan,
|
|
843
|
-
get,
|
|
844
|
-
contains,
|
|
845
|
-
execute,
|
|
846
808
|
write,
|
|
847
809
|
writeFrom,
|
|
848
810
|
prepare
|
|
849
811
|
});
|
|
850
812
|
}
|
|
851
|
-
/**
|
|
852
|
-
* The engine twin of the schema-level class wall, as a matchable value
|
|
853
|
-
* (`errors.is`): the shared lowering rejected a spec whose statement pairs
|
|
854
|
-
* faces with disagreeing newtype labels — the faces of a dependency agree
|
|
855
|
-
* on their newtype, or neither carries one. UNREACHABLE through the typed
|
|
856
|
-
* builder (the SDK computes every label from the laws, so its lowered
|
|
857
|
-
* specs cohere by construction); a raw spec handed to the bridge is the
|
|
858
|
-
* one road here, and the runtime referee that proves the engine judges
|
|
859
|
-
* what the types claim.
|
|
860
|
-
*/
|
|
861
813
|
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
|
-
|
|
814
|
+
const ErrSchemaError = errors.new("bumbledb schemaError: the declaration failed validation");
|
|
815
|
+
const ErrFingerprintMismatch = errors.new("bumbledb fingerprintMismatch: the store's schema does not match this theory");
|
|
816
|
+
const ErrIrError = errors.new("bumbledb irError: the query failed validation");
|
|
817
|
+
function throwOpenRefusal(verb, canonical, kind, message) {
|
|
818
|
+
const detail = `${verb} ${canonical}: ${message}`;
|
|
819
|
+
if (kind === "newtypeMismatch") {
|
|
820
|
+
throw errors.wrap(ErrNewtypeMismatch, detail);
|
|
821
|
+
}
|
|
822
|
+
if (kind === "schemaError") {
|
|
823
|
+
throw errors.wrap(ErrSchemaError, detail);
|
|
824
|
+
}
|
|
825
|
+
throw errors.wrap(ErrFingerprintMismatch, detail);
|
|
826
|
+
}
|
|
827
|
+
function throwPrepareRefusal(message) {
|
|
828
|
+
throw errors.wrap(ErrIrError, `prepare: ${message}`);
|
|
829
|
+
}
|
|
830
|
+
function openFromHandle(dbHandle, theory) {
|
|
831
|
+
const manifest = bridged("fetch bumbledb manifest", function fetchManifest() {
|
|
832
|
+
return native.dbManifest(dbHandle);
|
|
833
|
+
});
|
|
834
|
+
return openDb(dbHandle, theory, manifest);
|
|
835
|
+
}
|
|
836
|
+
async function createStore(storePath, theory) {
|
|
874
837
|
const canonical = path.resolve(storePath);
|
|
875
838
|
const spec = lower(theory);
|
|
876
|
-
const
|
|
877
|
-
|
|
878
|
-
|
|
839
|
+
const created = await bridgedAsync(`create bumbledb store at ${canonical}`, function callBridge() {
|
|
840
|
+
return native.dbCreate(canonical, spec);
|
|
841
|
+
});
|
|
842
|
+
if (created.tag === "schemaError" || created.tag === "newtypeMismatch") {
|
|
843
|
+
throwOpenRefusal("create", canonical, created.tag, created.message);
|
|
844
|
+
}
|
|
845
|
+
if (created.tag === "rejected") {
|
|
846
|
+
return Object.freeze({
|
|
847
|
+
tag: "rejected",
|
|
848
|
+
violations: Object.freeze(created.violations.map(function mapWire(wire) {
|
|
849
|
+
return mapViolationWithoutStore(theory, wire);
|
|
850
|
+
}))
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
return Object.freeze({ tag: "accepted", value: openFromHandle(created.db, theory) });
|
|
854
|
+
}
|
|
855
|
+
function mapViolationWithoutStore(theory, wire) {
|
|
856
|
+
const entries = materializedEntries(theory);
|
|
857
|
+
const entry = entries[wire.statementId];
|
|
858
|
+
if (entry === undefined) {
|
|
859
|
+
throw errors.new(`bumbledb violation cites unknown statement id ${wire.statementId}`);
|
|
860
|
+
}
|
|
861
|
+
const facts = Object.freeze(wire.facts.map(function offending(fact) {
|
|
862
|
+
const member = theory.relations[fact.relation];
|
|
863
|
+
if (member === undefined || !(fact.relation in theory.relations)) {
|
|
864
|
+
throw errors.new(`bumbledb violation cites unknown relation ${fact.relation}`);
|
|
879
865
|
}
|
|
866
|
+
return decodeOffendingFact(member, fact.relation, fact);
|
|
867
|
+
}));
|
|
868
|
+
return violationFromEntry(entry, wire, facts);
|
|
869
|
+
}
|
|
870
|
+
async function openStore(storePath, theory) {
|
|
871
|
+
const canonical = path.resolve(storePath);
|
|
872
|
+
const spec = lower(theory);
|
|
873
|
+
const opened = await bridgedAsync(`open bumbledb store at ${canonical}`, function callBridge() {
|
|
880
874
|
return native.dbOpen(canonical, spec);
|
|
881
875
|
});
|
|
882
876
|
if (!opened.ok) {
|
|
883
|
-
|
|
884
|
-
|
|
877
|
+
throwOpenRefusal("open", canonical, opened.kind, opened.message);
|
|
878
|
+
}
|
|
879
|
+
return openFromHandle(opened.db, theory);
|
|
880
|
+
}
|
|
881
|
+
const ownedRecords = new WeakMap();
|
|
882
|
+
const builderRecords = new WeakMap();
|
|
883
|
+
const ownedReclaimer = new FinalizationRegistry(function reclaimOwned(handle) {
|
|
884
|
+
const closed = errors.trySync(function closeOwned() {
|
|
885
|
+
native.ownedInstanceClose(handle);
|
|
886
|
+
});
|
|
887
|
+
if (closed.error) {
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
});
|
|
891
|
+
const builderReclaimer = new FinalizationRegistry(function reclaimBuilder(handle) {
|
|
892
|
+
const closed = errors.trySync(function closeBuilder() {
|
|
893
|
+
native.instanceBuilderClose(handle);
|
|
894
|
+
});
|
|
895
|
+
if (closed.error) {
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
});
|
|
899
|
+
function wrapOwned(nativeHandle, theory) {
|
|
900
|
+
const owner = Object.freeze({});
|
|
901
|
+
const rec = { handle: nativeHandle, theory, spent: false, owner };
|
|
902
|
+
const tables = tablesFromTheory(theory);
|
|
903
|
+
function assertLive() {
|
|
904
|
+
if (rec.spent) {
|
|
905
|
+
throw errors.wrap(ErrSpentHandle, "bumbledb owned instance has been disposed");
|
|
885
906
|
}
|
|
886
|
-
throw errors.new(`bumbledb ${opened.kind} (${verb} ${canonical}): ${opened.message}`);
|
|
887
907
|
}
|
|
888
|
-
const
|
|
889
|
-
|
|
908
|
+
const methods = catalogMethods(theory, tables, owner, assertLive, {
|
|
909
|
+
scan(relationId) {
|
|
910
|
+
return native.ownedScan(nativeHandle, relationId);
|
|
911
|
+
},
|
|
912
|
+
count(relationId) {
|
|
913
|
+
return native.ownedCount(nativeHandle, relationId);
|
|
914
|
+
},
|
|
915
|
+
contains(relationId, values) {
|
|
916
|
+
return native.ownedContains(nativeHandle, relationId, values);
|
|
917
|
+
},
|
|
918
|
+
get(relationId, statementId, keyValues) {
|
|
919
|
+
return native.ownedGet(nativeHandle, relationId, statementId, keyValues);
|
|
920
|
+
},
|
|
921
|
+
prepare(query) {
|
|
922
|
+
return native.ownedPrepare(nativeHandle, query);
|
|
923
|
+
},
|
|
924
|
+
execute(prepared, params) {
|
|
925
|
+
return native.ownedExecute(prepared, nativeHandle, params);
|
|
926
|
+
}
|
|
927
|
+
});
|
|
928
|
+
const instance = Object.freeze({
|
|
929
|
+
...methods,
|
|
930
|
+
[Symbol.dispose]() {
|
|
931
|
+
if (rec.spent) {
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
try {
|
|
935
|
+
native.ownedInstanceClose(nativeHandle);
|
|
936
|
+
}
|
|
937
|
+
catch (caught) {
|
|
938
|
+
const error = errorFromThrow(caught);
|
|
939
|
+
if (/leased for publish/.test(error.message)) {
|
|
940
|
+
throw errors.wrap(ErrSpentHandle, "bumbledb owned instance is leased for publish");
|
|
941
|
+
}
|
|
942
|
+
throw errors.wrap(error, "close bumbledb owned instance");
|
|
943
|
+
}
|
|
944
|
+
rec.spent = true;
|
|
945
|
+
ownedReclaimer.unregister(instance);
|
|
946
|
+
}
|
|
947
|
+
});
|
|
948
|
+
ownedRecords.set(instance, rec);
|
|
949
|
+
ownedReclaimer.register(instance, nativeHandle, instance);
|
|
950
|
+
return instance;
|
|
951
|
+
}
|
|
952
|
+
function wrapBuilder(nativeHandle, theory) {
|
|
953
|
+
const rec = { handle: nativeHandle, theory, spent: false };
|
|
954
|
+
const tables = tablesFromTheory(theory);
|
|
955
|
+
function assertLive() {
|
|
956
|
+
if (rec.spent) {
|
|
957
|
+
throw errors.wrap(ErrSpentHandle, "bumbledb instance builder has been spent");
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
const overlay = overlayMethods(theory, tables, assertLive, {
|
|
961
|
+
contains(relationId, row) {
|
|
962
|
+
return bridged("bumbledb builder contains", function readContains() {
|
|
963
|
+
return native.instanceBuilderContains(nativeHandle, relationId, row);
|
|
964
|
+
});
|
|
965
|
+
},
|
|
966
|
+
get(relationId, statementId, key) {
|
|
967
|
+
return bridged("bumbledb builder get", function readGet() {
|
|
968
|
+
return native.instanceBuilderGet(nativeHandle, relationId, statementId, key);
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
});
|
|
972
|
+
const builder = Object.freeze({
|
|
973
|
+
load(relation, facts) {
|
|
974
|
+
assertLive();
|
|
975
|
+
const entry = ordinaryEntry(tables, theory, relation);
|
|
976
|
+
return mutateCollection(relation, facts, function applyCells(rows, cells) {
|
|
977
|
+
return bridged("bumbledb builder load", function loadCells() {
|
|
978
|
+
return native.instanceBuilderLoad(nativeHandle, entry.id, rows, cells);
|
|
979
|
+
});
|
|
980
|
+
});
|
|
981
|
+
},
|
|
982
|
+
delete(relation, facts) {
|
|
983
|
+
assertLive();
|
|
984
|
+
const entry = ordinaryEntry(tables, theory, relation);
|
|
985
|
+
const flat = rowsOf(relation, facts);
|
|
986
|
+
const report = bridged("bumbledb builder delete", function remove() {
|
|
987
|
+
return native.instanceBuilderDelete(nativeHandle, entry.id, flat.rows, flat.cells);
|
|
988
|
+
});
|
|
989
|
+
return Object.freeze({ submitted: report.submitted, changed: report.changed });
|
|
990
|
+
},
|
|
991
|
+
reserve(relation, field, count) {
|
|
992
|
+
assertLive();
|
|
993
|
+
const entry = ordinaryEntry(tables, theory, relation);
|
|
994
|
+
const declared = relation.data.fields.find(function byName(candidate) {
|
|
995
|
+
return candidate.name === field;
|
|
996
|
+
});
|
|
997
|
+
if (declared === undefined || !isFreshField(declared.field)) {
|
|
998
|
+
throw errors.new(`relation ${relation.name}: field ${field} is not a fresh cell`);
|
|
999
|
+
}
|
|
1000
|
+
const fieldId = entry.fieldIds.get(field);
|
|
1001
|
+
if (fieldId === undefined) {
|
|
1002
|
+
throw errors.new(`bumbledb manifest drift: relation ${relation.name} has no field id for ${field}`);
|
|
1003
|
+
}
|
|
1004
|
+
const range = bridged("bumbledb builder reserve", function mint() {
|
|
1005
|
+
return native.instanceBuilderReserve(nativeHandle, entry.id, fieldId, count);
|
|
1006
|
+
});
|
|
1007
|
+
return freshRangeOf(range);
|
|
1008
|
+
},
|
|
1009
|
+
contains: overlay.contains,
|
|
1010
|
+
get: overlay.get,
|
|
1011
|
+
async admit() {
|
|
1012
|
+
if (rec.spent) {
|
|
1013
|
+
throw errors.wrap(ErrSpentHandle, "bumbledb instance builder has been spent");
|
|
1014
|
+
}
|
|
1015
|
+
rec.spent = true;
|
|
1016
|
+
builderReclaimer.unregister(builder);
|
|
1017
|
+
let outcome;
|
|
1018
|
+
try {
|
|
1019
|
+
outcome = await native.instanceBuilderAdmit(nativeHandle);
|
|
1020
|
+
}
|
|
1021
|
+
catch (caught) {
|
|
1022
|
+
throw errors.wrap(errorFromThrow(caught), "admit bumbledb instance");
|
|
1023
|
+
}
|
|
1024
|
+
if (outcome.tag === "rejected") {
|
|
1025
|
+
return Object.freeze({
|
|
1026
|
+
tag: "rejected",
|
|
1027
|
+
violations: Object.freeze(outcome.violations.map(function mapWire(wire) {
|
|
1028
|
+
return mapViolationWithoutStore(theory, wire);
|
|
1029
|
+
}))
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
return Object.freeze({
|
|
1033
|
+
tag: "accepted",
|
|
1034
|
+
value: wrapOwned(outcome.value, theory)
|
|
1035
|
+
});
|
|
1036
|
+
},
|
|
1037
|
+
[Symbol.dispose]() {
|
|
1038
|
+
if (rec.spent) {
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
rec.spent = true;
|
|
1042
|
+
builderReclaimer.unregister(builder);
|
|
1043
|
+
bridged("close bumbledb instance builder", function closeBuilder() {
|
|
1044
|
+
native.instanceBuilderClose(nativeHandle);
|
|
1045
|
+
});
|
|
1046
|
+
}
|
|
890
1047
|
});
|
|
891
|
-
|
|
1048
|
+
builderRecords.set(builder, rec);
|
|
1049
|
+
builderReclaimer.register(builder, nativeHandle, builder);
|
|
1050
|
+
return builder;
|
|
892
1051
|
}
|
|
1052
|
+
const InstanceBuilder = Object.freeze({
|
|
1053
|
+
create(theory) {
|
|
1054
|
+
const spec = lower(theory);
|
|
1055
|
+
const handle = bridged("create bumbledb instance builder", function make() {
|
|
1056
|
+
return native.instanceBuilderNew(spec);
|
|
1057
|
+
});
|
|
1058
|
+
return wrapBuilder(handle, theory);
|
|
1059
|
+
}
|
|
1060
|
+
});
|
|
893
1061
|
/**
|
|
894
1062
|
* The store lifecycle — `Db.create(path, schema)` / `Db.open(path, schema)`.
|
|
895
1063
|
* Create refuses an already-initialized directory; open verifies format
|
|
896
|
-
* version
|
|
897
|
-
*
|
|
898
|
-
*
|
|
899
|
-
*
|
|
900
|
-
*
|
|
1064
|
+
* version and the schema fingerprint. A second live handle on the same
|
|
1065
|
+
* path is the engine's `EnvironmentLocked`. There is no close anywhere:
|
|
1066
|
+
* the process owns the environment until GC/exit (durability is the
|
|
1067
|
+
* engine's per-commit fsync). Resume = reopen in a fresh process, or
|
|
1068
|
+
* hold the `Db` this process opened.
|
|
901
1069
|
*/
|
|
902
1070
|
const Db = Object.freeze({
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
return admit("create", path, theory);
|
|
1071
|
+
async create(storePath, theory) {
|
|
1072
|
+
return createStore(storePath, theory);
|
|
906
1073
|
},
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
* A fingerprint-matching open also BACK-FILLS the store's persisted
|
|
910
|
-
* schema descriptor when it is absent (self-describing stores, engine
|
|
911
|
-
* 50-storage.md § the `_meta` block), so a legacy store becomes
|
|
912
|
-
* exhumable after one ordinary open — adoption is automatic, never a
|
|
913
|
-
* separate verb. A second open of a still-live path is
|
|
914
|
-
* `EnvironmentLocked`.
|
|
915
|
-
*/
|
|
916
|
-
async open(path, theory) {
|
|
917
|
-
return admit("open", path, theory);
|
|
1074
|
+
async open(storePath, theory) {
|
|
1075
|
+
return openStore(storePath, theory);
|
|
918
1076
|
},
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
return exhumeStore(path.resolve(storePath));
|
|
1077
|
+
async fromInstance(storePath, instance) {
|
|
1078
|
+
const rec = ownedRecords.get(instance);
|
|
1079
|
+
if (rec === undefined) {
|
|
1080
|
+
throw errors.wrap(ErrSpentHandle, "bumbledb fromInstance target is not an owned instance of this SDK");
|
|
1081
|
+
}
|
|
1082
|
+
if (rec.spent) {
|
|
1083
|
+
throw errors.wrap(ErrSpentHandle, "bumbledb fromInstance target has been disposed");
|
|
1084
|
+
}
|
|
1085
|
+
const canonical = path.resolve(storePath);
|
|
1086
|
+
const dbHandle = await bridgedAsync(`publish bumbledb instance at ${canonical}`, function publish() {
|
|
1087
|
+
return native.dbFromInstance(canonical, rec.handle);
|
|
1088
|
+
});
|
|
1089
|
+
return openFromHandle(dbHandle, rec.theory);
|
|
933
1090
|
}
|
|
934
1091
|
});
|
|
935
|
-
export { abandon, Db,
|
|
1092
|
+
export { abandon, Db, ErrAsyncCallback, ErrFingerprintMismatch, ErrForeignPrepared, ErrForeignWitness, ErrIrError, ErrNewtypeMismatch, ErrSchemaError, ErrSpentHandle, ErrUseAfterScope, InstanceBuilder };
|
|
936
1093
|
//# sourceMappingURL=db.js.map
|