@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/src/db.ts
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,33 +22,38 @@
|
|
|
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
|
|
|
33
32
|
import * as path from "node:path"
|
|
34
33
|
import * as errors from "@superbuilders/errors"
|
|
35
34
|
import { isClosedMember, sealedFieldsOf } from "#closed.ts"
|
|
36
|
-
import type { Exhumed } from "#exhume.ts"
|
|
37
|
-
import { exhumeStore } from "#exhume.ts"
|
|
38
35
|
import { rosterOf } from "#fields.ts"
|
|
39
36
|
import { lower } from "#lower.ts"
|
|
40
|
-
import { factOf, handleOf, isFreshField, type KeyFact, keyRowOf, recordOf, rowOf } from "#marshal.ts"
|
|
37
|
+
import { cellOf, factOf, handleOf, isFreshField, type KeyFact, keyRowOf, recordOf, rowOf } from "#marshal.ts"
|
|
41
38
|
|
|
42
39
|
import type {
|
|
40
|
+
AdmitResult,
|
|
41
|
+
BuilderHandle,
|
|
43
42
|
DbHandle,
|
|
44
43
|
FactValue,
|
|
44
|
+
InstanceHandle,
|
|
45
45
|
Manifest,
|
|
46
|
+
NativeWriteOutcome,
|
|
47
|
+
OwnedHandle,
|
|
46
48
|
PreparedHandle,
|
|
47
|
-
SnapshotHandle,
|
|
48
49
|
TxHandle,
|
|
49
50
|
WireFreshRange,
|
|
50
51
|
Violation as WireViolation,
|
|
51
|
-
ViolationFact as WireViolationFact
|
|
52
|
+
ViolationFact as WireViolationFact,
|
|
53
|
+
WireMutationReport,
|
|
54
|
+
WitnessHandle
|
|
52
55
|
} from "#native.ts"
|
|
53
|
-
import { bridged, native } from "#native.ts"
|
|
56
|
+
import { bridged, bridgedAsync, errorFromThrow, native } from "#native.ts"
|
|
54
57
|
import type { FindColumn } from "#query/atom.ts"
|
|
55
58
|
import type { Query } from "#query/lower.ts"
|
|
56
59
|
import { lowerQuery } from "#query/lower.ts"
|
|
@@ -76,6 +79,66 @@ interface MutationReport {
|
|
|
76
79
|
readonly changed: bigint
|
|
77
80
|
}
|
|
78
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Column-major collection write: one array per declared field, every
|
|
84
|
+
* column the same length. The second transport of `load` / `insert` —
|
|
85
|
+
* objects and columns are two ways to spell the same batch.
|
|
86
|
+
*/
|
|
87
|
+
type ColumnBatch<R extends AnyRelation> = {
|
|
88
|
+
readonly [K in keyof Fact<R> & string]: readonly Fact<R>[K][]
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
type CollectionWrite<R extends AnyRelation> = Iterable<Fact<R>> | ColumnBatch<R>
|
|
92
|
+
|
|
93
|
+
function isColumnBatch(value: object): boolean {
|
|
94
|
+
return !(Symbol.iterator in value)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function rowsOf<R extends AnyRelation>(relation: R, facts: Iterable<Fact<R>>): FactValue[][] {
|
|
98
|
+
const rows: FactValue[][] = []
|
|
99
|
+
for (const fact of facts) {
|
|
100
|
+
rows.push(rowOf(relation.data, recordOf(fact)))
|
|
101
|
+
}
|
|
102
|
+
return rows
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Lowers a column batch to per-field wire arrays in sealed field order.
|
|
107
|
+
* Allocates one array per field — never a JS array per row.
|
|
108
|
+
*/
|
|
109
|
+
function columnsOf(relation: AnyRelation, batch: object): FactValue[][] {
|
|
110
|
+
const record = recordOf(batch)
|
|
111
|
+
let count: number | undefined
|
|
112
|
+
return relation.data.fields.map(function marshalColumn(declared) {
|
|
113
|
+
const raw = record[declared.name]
|
|
114
|
+
if (!Array.isArray(raw)) {
|
|
115
|
+
throw errors.new(`relation ${relation.name}: column ${declared.name} is not an array`)
|
|
116
|
+
}
|
|
117
|
+
if (count === undefined) {
|
|
118
|
+
count = raw.length
|
|
119
|
+
} else if (raw.length !== count) {
|
|
120
|
+
throw errors.new(
|
|
121
|
+
`relation ${relation.name}: column ${declared.name} has length ${raw.length}, expected ${count}`
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
return raw.map(function marshalCell(value: unknown) {
|
|
125
|
+
return cellOf(`relation ${relation.name} field ${declared.name}`, declared.field, value)
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function mutateCollection<R extends AnyRelation>(
|
|
131
|
+
relation: R,
|
|
132
|
+
facts: CollectionWrite<R>,
|
|
133
|
+
applyRows: (rows: readonly FactValue[][]) => WireMutationReport,
|
|
134
|
+
applyColumns: (columns: readonly FactValue[][]) => WireMutationReport
|
|
135
|
+
): MutationReport {
|
|
136
|
+
const report = isColumnBatch(facts)
|
|
137
|
+
? applyColumns(columnsOf(relation, facts))
|
|
138
|
+
: applyRows(rowsOf(relation, facts as Iterable<Fact<R>>))
|
|
139
|
+
return Object.freeze({ submitted: report.submitted, changed: report.changed })
|
|
140
|
+
}
|
|
141
|
+
|
|
79
142
|
/**
|
|
80
143
|
* Half-open fresh-id range from one `reserve`. Empty cannot yield a
|
|
81
144
|
* minted id — `start` exists only on the nonempty arm.
|
|
@@ -245,29 +308,40 @@ type Violation<Rels extends SchemaRelations> =
|
|
|
245
308
|
* `never` and the arm vanishes from the sum. The outcome is in the type;
|
|
246
309
|
* a dead arm is never handled.
|
|
247
310
|
*/
|
|
248
|
-
type AbandonedArm<R> = R extends Abandon<infer P> ? { readonly
|
|
311
|
+
type AbandonedArm<R> = R extends Abandon<infer P> ? { readonly tag: "abandoned"; readonly abandoned: P } : never
|
|
312
|
+
|
|
313
|
+
/** A callback return that is not Promise-like. TypeScript `never` is not a runtime boundary. */
|
|
314
|
+
type SyncResult<R> = R extends PromiseLike<unknown> ? never : R
|
|
315
|
+
|
|
316
|
+
interface Committed<T> {
|
|
317
|
+
readonly value: T
|
|
318
|
+
readonly generation: bigint
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
type Admission<Rels extends SchemaRelations, T> =
|
|
322
|
+
| { readonly tag: "accepted"; readonly value: T }
|
|
323
|
+
| { readonly tag: "rejected"; readonly violations: readonly Violation<Rels>[] }
|
|
249
324
|
|
|
250
325
|
/**
|
|
251
|
-
* A write's domain outcome
|
|
252
|
-
*
|
|
253
|
-
* violated statement cited once, per direction for a containment, in
|
|
254
|
-
* materialized statement order); or the callback's own abandon payload —
|
|
255
|
-
* commit-vs-abandon is in the type, so a caller's explicit decline to
|
|
256
|
-
* commit can never be silently discarded. Narrows on `.ok`, then (when the
|
|
257
|
-
* callback can abandon) on `"violations" in result`.
|
|
326
|
+
* A write's domain outcome. One discriminant: narrow on `tag`. The
|
|
327
|
+
* abandoned arm is present exactly when the callback can abandon.
|
|
258
328
|
*/
|
|
259
|
-
type
|
|
260
|
-
| { readonly
|
|
261
|
-
| { readonly
|
|
329
|
+
type WriteOutcome<Rels extends SchemaRelations, R> =
|
|
330
|
+
| { readonly tag: "accepted"; readonly value: Committed<Exclude<R, Abandon<unknown>>> }
|
|
331
|
+
| { readonly tag: "rejected"; readonly violations: readonly Violation<Rels>[] }
|
|
262
332
|
| AbandonedArm<R>
|
|
263
333
|
|
|
334
|
+
type WriteFromOutcome<Rels extends SchemaRelations, R> =
|
|
335
|
+
| WriteOutcome<Rels, R>
|
|
336
|
+
| { readonly tag: "moved"; readonly witnessed: bigint; readonly current: bigint }
|
|
337
|
+
|
|
264
338
|
/**
|
|
265
339
|
* The delta-building callback of a write: runs synchronously against the
|
|
266
340
|
* live transaction. Returning {@link abandon}`(payload)` rolls the
|
|
267
341
|
* transaction back (R10) — the result type carries the payload arm exactly
|
|
268
342
|
* then.
|
|
269
343
|
*/
|
|
270
|
-
type DeltaBuild<Rels extends SchemaRelations, R = void> = (tx:
|
|
344
|
+
type DeltaBuild<Rels extends SchemaRelations, R = void> = (tx: WriteTx<Rels>) => R
|
|
271
345
|
|
|
272
346
|
/**
|
|
273
347
|
* The runtime discriminant of {@link Abandon} values — a property probe is
|
|
@@ -280,7 +354,7 @@ const abandonMark: unique symbol = Symbol("bumbledb.abandon")
|
|
|
280
354
|
* The abandon sentinel {@link abandon} builds: returning one from a `write`
|
|
281
355
|
* or `writeFrom` callback rolls the transaction back WITHOUT
|
|
282
356
|
* committing (no empty commit is ever issued) and surfaces the payload as
|
|
283
|
-
* `{
|
|
357
|
+
* `{ tag: "abandoned", abandoned: payload }` (ruled 2026-07-23, R10 — the
|
|
284
358
|
* sentinel's contract is unconditional, whichever write verb received it).
|
|
285
359
|
*/
|
|
286
360
|
interface Abandon<P> {
|
|
@@ -292,7 +366,7 @@ interface Abandon<P> {
|
|
|
292
366
|
* Wraps a payload in the {@link Abandon} sentinel — the one way a write
|
|
293
367
|
* callback declines to commit: `return abandon(payload)` aborts the delta
|
|
294
368
|
* (nothing is committed, not even an empty commit) and the write resolves
|
|
295
|
-
* to `{
|
|
369
|
+
* to `{ tag: "abandoned", abandoned: payload }`, from `write` and `writeFrom`
|
|
296
370
|
* alike (R10).
|
|
297
371
|
*/
|
|
298
372
|
function abandon<P>(payload: P): Abandon<P> {
|
|
@@ -326,17 +400,17 @@ function isAbandon<R>(value: R): value is R & Abandon<AbandonedPayload<R>> {
|
|
|
326
400
|
* cannot resolve over an open `R`.
|
|
327
401
|
*/
|
|
328
402
|
function isAbandonedOutcome<Rels extends SchemaRelations, R>(
|
|
329
|
-
outcome: { readonly
|
|
403
|
+
outcome: { readonly tag: "abandoned"; readonly abandoned: AbandonedPayload<R> },
|
|
330
404
|
sentinel: Abandon<AbandonedPayload<R>>
|
|
331
|
-
): outcome is { readonly
|
|
405
|
+
): outcome is { readonly tag: "abandoned"; readonly abandoned: AbandonedPayload<R> } & WriteOutcome<Rels, R> {
|
|
332
406
|
return isAbandon(sentinel) && outcome.abandoned === sentinel.payload
|
|
333
407
|
}
|
|
334
408
|
|
|
335
409
|
/** Builds the abandoned write outcome from the callback's own sentinel (the R10 arm's one mint). */
|
|
336
410
|
function abandonedOutcome<Rels extends SchemaRelations, R>(
|
|
337
411
|
sentinel: Abandon<AbandonedPayload<R>>
|
|
338
|
-
):
|
|
339
|
-
const outcome = Object.freeze({
|
|
412
|
+
): WriteOutcome<Rels, R> {
|
|
413
|
+
const outcome = Object.freeze({ tag: "abandoned" as const, abandoned: sentinel.payload })
|
|
340
414
|
if (!isAbandonedOutcome<Rels, R>(outcome, sentinel)) {
|
|
341
415
|
throw errors.new("bumbledb abandon outcome construction incomplete")
|
|
342
416
|
}
|
|
@@ -350,14 +424,14 @@ function abandonedOutcome<Rels extends SchemaRelations, R>(
|
|
|
350
424
|
* Spent when its owning `write`/`writeFrom` call resolves the attempt;
|
|
351
425
|
* any later use throws.
|
|
352
426
|
*/
|
|
353
|
-
interface
|
|
427
|
+
interface WriteTx<Rels extends SchemaRelations> {
|
|
354
428
|
/**
|
|
355
429
|
* Records a collection of inserts. Singleton is `[fact]`. Empty is
|
|
356
430
|
* lawful. Returns how many facts were consumed and how many changed
|
|
357
431
|
* the in-memory final-state view. Every fact is complete — omitted
|
|
358
|
-
* fresh cells are a type error; mint first with {@link
|
|
432
|
+
* fresh cells are a type error; mint first with {@link WriteTx.reserve}.
|
|
359
433
|
*/
|
|
360
|
-
insert<R extends MemberRelation<Rels>>(relation: R, facts:
|
|
434
|
+
insert<R extends MemberRelation<Rels>>(relation: R, facts: CollectionWrite<R>): MutationReport
|
|
361
435
|
/**
|
|
362
436
|
* Records a collection of deletes. Singleton is `[fact]`. Returns
|
|
363
437
|
* how many facts were consumed and how many changed the view.
|
|
@@ -387,21 +461,27 @@ interface Tx<Rels extends SchemaRelations> {
|
|
|
387
461
|
): Fact<R> | undefined
|
|
388
462
|
}
|
|
389
463
|
|
|
464
|
+
type Tx<Rels extends SchemaRelations> = WriteTx<Rels>
|
|
465
|
+
|
|
466
|
+
const witnessTypes: unique symbol = Symbol("bumbledb.witness.types")
|
|
467
|
+
|
|
390
468
|
/**
|
|
391
|
-
*
|
|
392
|
-
*
|
|
393
|
-
* disposables, never `close()`). The callback form invalidates the value
|
|
394
|
-
* when `fn` returns; the `using` form releases it at scope exit through
|
|
395
|
-
* `Symbol.dispose` — either way the release is deterministic and
|
|
396
|
-
* scope-shaped, every later verb call throws a typed used-after-scope
|
|
397
|
-
* error, and the underlying snapshot (with its LMDB reader slot) is
|
|
398
|
-
* already closed.
|
|
469
|
+
* Cloneable generation evidence from one store read. May cross `await`.
|
|
470
|
+
* Disposal is idempotent; later use throws {@link ErrSpentHandle}.
|
|
399
471
|
*/
|
|
400
|
-
interface
|
|
472
|
+
interface Witness<Rels extends SchemaRelations> extends Disposable {
|
|
473
|
+
readonly [witnessTypes]?: Rels
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* The borrowed instance one `db.read((instance, witness) => …)` callback
|
|
478
|
+
* receives. Invalid the moment the callback returns. A stashed value
|
|
479
|
+
* throws {@link ErrUseAfterScope}. Not a handle: there is no `db.read()`.
|
|
480
|
+
*/
|
|
481
|
+
interface ReadInstance<Rels extends SchemaRelations> {
|
|
401
482
|
/**
|
|
402
|
-
* The committed generation this
|
|
403
|
-
*
|
|
404
|
-
* transaction), so it is atomic with the snapshot by construction.
|
|
483
|
+
* The committed generation this instance witnessed — read inside the
|
|
484
|
+
* lease's own transaction.
|
|
405
485
|
*/
|
|
406
486
|
readonly generation: bigint
|
|
407
487
|
/** Full-relation export in row-id order, decoded to bare structural facts. */
|
|
@@ -424,12 +504,13 @@ interface ReadScope<Rels extends SchemaRelations> extends Disposable {
|
|
|
424
504
|
/** Committed-state membership of one complete fact. */
|
|
425
505
|
contains<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean
|
|
426
506
|
/**
|
|
427
|
-
* Executes a prepared query against this
|
|
428
|
-
*
|
|
429
|
-
*
|
|
507
|
+
* Executes a prepared query against this instance with the typed
|
|
508
|
+
* params object; returns the answer SET as plain rows with bare
|
|
509
|
+
* structural values (no order — the host sorts). This is the ONE
|
|
430
510
|
* execution spelling ({@link Prepared} carries no `execute`).
|
|
431
511
|
*/
|
|
432
512
|
execute<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Row[]
|
|
513
|
+
prepare<Row, Params extends ParamsRecord>(q: Query<Rels, Row, Params>): Prepared<Rels, Row, Params>
|
|
433
514
|
}
|
|
434
515
|
|
|
435
516
|
/**
|
|
@@ -446,7 +527,7 @@ const preparedTypes: unique symbol = Symbol("bumbledb.prepared.types")
|
|
|
446
527
|
* One prepared query as a plain VALUE: explicit visible compilation
|
|
447
528
|
* (`db.prepare(q)` lowers, pins the plan, and surfaces every engine roster
|
|
448
529
|
* refusal), no lifecycle. Execution happens ONLY through
|
|
449
|
-
* `
|
|
530
|
+
* `instance.execute(prepared, params)` / `db.execute(prepared, params)` — the
|
|
450
531
|
* symmetry rule's one spelling. The engine-side plan is reclaimed by a GC
|
|
451
532
|
* finalizer when this value becomes unreachable (reclamation only, never
|
|
452
533
|
* correctness — an unreclaimed plan is idle memory, and process exit frees
|
|
@@ -467,53 +548,42 @@ interface Db<Rels extends SchemaRelations> {
|
|
|
467
548
|
/** The theory this store was opened with (fingerprint-verified by the engine). */
|
|
468
549
|
readonly schema: Schema<Rels>
|
|
469
550
|
/**
|
|
470
|
-
* One
|
|
471
|
-
*
|
|
472
|
-
*
|
|
473
|
-
*
|
|
474
|
-
|
|
475
|
-
read<T>(fn: (snap: ReadScope<Rels>) => T): T
|
|
476
|
-
/**
|
|
477
|
-
* The `using` acquisition (ruled 2026-07-23, R12): `using snap =
|
|
478
|
-
* db.read()` — the caller owns the scope's lifetime, and the scope's
|
|
479
|
-
* `Symbol.dispose` releases the snapshot deterministically at scope
|
|
480
|
-
* exit, in the language's own syntax. Lifetimes are disposables, never
|
|
481
|
-
* `close()`.
|
|
551
|
+
* One store read: runs `body` SYNCHRONOUSLY inside the engine lease
|
|
552
|
+
* and returns its result. The {@link ReadInstance} is invalidated
|
|
553
|
+
* when `body` returns — a stashed use throws {@link ErrUseAfterScope}.
|
|
554
|
+
* The {@link Witness} is a clone and may escape. A thenable return
|
|
555
|
+
* throws {@link ErrAsyncCallback}.
|
|
482
556
|
*/
|
|
483
|
-
read(
|
|
484
|
-
/** `db.scan(r)` === `db.read(
|
|
557
|
+
read<R>(body: (instance: ReadInstance<Rels>, witness: Witness<Rels>) => SyncResult<R>): SyncResult<R>
|
|
558
|
+
/** `db.scan(r)` === `db.read(instance => instance.scan(r))` — the symmetry rule. */
|
|
485
559
|
scan<R extends MemberRelation<Rels>>(relation: R): Fact<R>[]
|
|
486
|
-
/** `db.get(r, k)` === `db.read(
|
|
560
|
+
/** `db.get(r, k)` === `db.read(instance => instance.get(r, k))` — the symmetry rule. */
|
|
487
561
|
get<R extends MemberRelation<Rels>>(relation: R, key: KeyFact<R>): Fact<R> | undefined
|
|
488
|
-
/** `db.get(r, s, k)` === `db.read(
|
|
562
|
+
/** `db.get(r, s, k)` === `db.read(instance => instance.get(r, s, k))` — the symmetry rule, keyed form. */
|
|
489
563
|
get<R extends MemberRelation<Rels>, const P extends readonly string[]>(
|
|
490
564
|
relation: R,
|
|
491
565
|
keyStatement: KeyStatement<R, P>,
|
|
492
566
|
key: DeclaredKeyFact<R, P>
|
|
493
567
|
): Fact<R> | undefined
|
|
494
|
-
/** `db.contains(r, f)` === `db.read(
|
|
568
|
+
/** `db.contains(r, f)` === `db.read(instance => instance.contains(r, f))` — the symmetry rule. */
|
|
495
569
|
contains<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean
|
|
496
|
-
/** `db.execute(p, params)` === `db.read(
|
|
570
|
+
/** `db.execute(p, params)` === `db.read(instance => instance.execute(p, params))` — the symmetry rule. */
|
|
497
571
|
execute<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Row[]
|
|
498
572
|
/**
|
|
499
573
|
* One delta transaction: builds the delta synchronously through `fn`,
|
|
500
574
|
* commits, and returns the domain outcome. A throw from `fn` aborts
|
|
501
575
|
* the delta (LMDB untouched) and rethrows wrapped. `fn` may decline to
|
|
502
|
-
* commit by returning {@link abandon}`(payload)
|
|
503
|
-
*
|
|
504
|
-
*
|
|
505
|
-
* an arm the result type carries exactly when the callback can abandon.
|
|
576
|
+
* commit by returning {@link abandon}`(payload)`: the transaction rolls
|
|
577
|
+
* back — nothing is committed, not even an empty commit — and the
|
|
578
|
+
* outcome is `{ tag: "abandoned", abandoned: payload }`.
|
|
506
579
|
*/
|
|
507
|
-
write<R
|
|
580
|
+
write<R>(fn: (tx: WriteTx<Rels>) => SyncResult<R>): WriteOutcome<Rels, SyncResult<R>>
|
|
508
581
|
/**
|
|
509
|
-
*
|
|
510
|
-
*
|
|
511
|
-
*
|
|
512
|
-
* `snap`. A moved generation is the typed {@link ErrGenerationMoved}
|
|
513
|
-
* — retry is host policy; this method never loops. `fn` may decline to
|
|
514
|
-
* commit by returning {@link abandon}`(payload)`.
|
|
582
|
+
* Witnessed write: commits only if no state-changing commit landed
|
|
583
|
+
* since `witness` was minted. A moved generation is
|
|
584
|
+
* `{ tag: "moved" }` — retry is host policy; this method never loops.
|
|
515
585
|
*/
|
|
516
|
-
writeFrom<R>(
|
|
586
|
+
writeFrom<R>(witness: Witness<Rels>, fn: (tx: WriteTx<Rels>) => SyncResult<R>): WriteFromOutcome<Rels, SyncResult<R>>
|
|
517
587
|
/**
|
|
518
588
|
* Prepares a query value built against THIS schema (identity is the
|
|
519
589
|
* membership rule): lowers it to the engine IR, pins the plan, and
|
|
@@ -780,6 +850,29 @@ function tablesOf(theory: AnySchema, manifest: Manifest): Tables {
|
|
|
780
850
|
return Object.freeze({ relations, statements: Object.freeze(entries) })
|
|
781
851
|
}
|
|
782
852
|
|
|
853
|
+
function tablesFromTheory(theory: AnySchema): Tables {
|
|
854
|
+
const entries = materializedEntries(theory)
|
|
855
|
+
const relations = new Map<string, RelationEntry>()
|
|
856
|
+
Object.keys(theory.relations).forEach(function byOrdinal(name, ordinal) {
|
|
857
|
+
const member = theory.relations[name]
|
|
858
|
+
if (member === undefined) {
|
|
859
|
+
throw errors.new(`bumbledb theory has no relation ${name}`)
|
|
860
|
+
}
|
|
861
|
+
const fieldIds = new Map<string, number>()
|
|
862
|
+
sealedFieldsOf(member).forEach(function byField(declared, fieldOrdinal) {
|
|
863
|
+
fieldIds.set(declared.name, fieldOrdinal)
|
|
864
|
+
})
|
|
865
|
+
let primaryKey: PrimaryKey | undefined
|
|
866
|
+
entries.forEach(function firstOwnedKey(entry, index) {
|
|
867
|
+
if (primaryKey === undefined && entry.kind === "functionality" && entry.owner === name) {
|
|
868
|
+
primaryKey = Object.freeze({ statementId: index, projection: entry.projection })
|
|
869
|
+
}
|
|
870
|
+
})
|
|
871
|
+
relations.set(name, Object.freeze({ id: ordinal, member, fieldIds, primaryKey }))
|
|
872
|
+
})
|
|
873
|
+
return Object.freeze({ relations, statements: Object.freeze(entries) })
|
|
874
|
+
}
|
|
875
|
+
|
|
783
876
|
/** The point-read half a transaction and a read scope share, over their own handle. */
|
|
784
877
|
interface PointReads {
|
|
785
878
|
contains(relationId: number, row: readonly FactValue[]): boolean
|
|
@@ -787,25 +880,33 @@ interface PointReads {
|
|
|
787
880
|
}
|
|
788
881
|
|
|
789
882
|
/**
|
|
790
|
-
* One
|
|
791
|
-
*
|
|
792
|
-
* crossing, finding 016), its liveness flag (flipped when the owning
|
|
793
|
-
* `read` callback returns, or by the scope's own
|
|
794
|
-
* `Symbol.dispose`), its close latch (`closed` — the snapshot closes
|
|
795
|
-
* exactly once, whichever of the owner and the dispose gets there first),
|
|
796
|
-
* and its owning store's identity token. Held in {@link scopeStates} —
|
|
797
|
-
* the snapshot handle is never a public value.
|
|
883
|
+
* One borrowed instance's PRIVATE lifetime record. Held in
|
|
884
|
+
* {@link instanceStates} — the native handle is never a public value.
|
|
798
885
|
*/
|
|
799
|
-
interface
|
|
800
|
-
readonly handle:
|
|
801
|
-
readonly generation: bigint
|
|
886
|
+
interface InstanceState {
|
|
887
|
+
readonly handle: InstanceHandle
|
|
802
888
|
live: boolean
|
|
803
|
-
closed: boolean
|
|
804
889
|
readonly owner: object
|
|
805
890
|
}
|
|
806
891
|
|
|
807
|
-
|
|
808
|
-
|
|
892
|
+
const instanceStates = new WeakMap<object, InstanceState>()
|
|
893
|
+
|
|
894
|
+
interface WitnessState {
|
|
895
|
+
readonly handle: WitnessHandle
|
|
896
|
+
spent: boolean
|
|
897
|
+
readonly owner: object
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
const witnessStates = new WeakMap<object, WitnessState>()
|
|
901
|
+
|
|
902
|
+
const witnessReclaimer = new FinalizationRegistry<WitnessHandle>(function reclaimWitness(handle) {
|
|
903
|
+
const closed = errors.trySync(function closeWitness() {
|
|
904
|
+
native.witnessClose(handle)
|
|
905
|
+
})
|
|
906
|
+
if (closed.error) {
|
|
907
|
+
return
|
|
908
|
+
}
|
|
909
|
+
})
|
|
809
910
|
|
|
810
911
|
/**
|
|
811
912
|
* One prepared value's PRIVATE engine half: the pinned plan handle, the
|
|
@@ -838,14 +939,305 @@ const planReclaimer = new FinalizationRegistry<PreparedHandle>(function reclaimP
|
|
|
838
939
|
}
|
|
839
940
|
})
|
|
840
941
|
|
|
942
|
+
const ErrAsyncCallback = errors.new(
|
|
943
|
+
"bumbledb asyncCallback: a read or write callback returned a thenable — the callback is synchronous"
|
|
944
|
+
)
|
|
945
|
+
const ErrSpentHandle = errors.new("bumbledb spentHandle: a consumed builder, instance, or witness was used")
|
|
946
|
+
const ErrUseAfterScope = errors.new(
|
|
947
|
+
"bumbledb useAfterScope: a stashed read instance or write transaction was used after its callback returned"
|
|
948
|
+
)
|
|
949
|
+
const ErrForeignPrepared = errors.new("bumbledb foreignPrepared: a prepared query met a foreign instance")
|
|
950
|
+
const ErrForeignWitness = errors.new("bumbledb foreignWitness: a witness met a foreign store")
|
|
951
|
+
|
|
841
952
|
/**
|
|
842
|
-
* The typed
|
|
843
|
-
*
|
|
844
|
-
*
|
|
953
|
+
* The shared typed read surface: store leases and owned instances both
|
|
954
|
+
* expose scan/get/contains/execute/prepare. The native ops are the only
|
|
955
|
+
* difference — one way to read, two handle kinds.
|
|
845
956
|
*/
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
)
|
|
957
|
+
interface CatalogNative {
|
|
958
|
+
scan(relationId: number): FactValue[][]
|
|
959
|
+
contains(relationId: number, values: readonly FactValue[]): boolean
|
|
960
|
+
get(relationId: number, statementId: number, keyValues: readonly FactValue[]): FactValue[] | null
|
|
961
|
+
prepare(query: ReturnType<typeof lowerQuery>): ReturnType<typeof native.instancePrepare>
|
|
962
|
+
execute(prepared: PreparedHandle, params: ReturnType<typeof wireParams>): FactValue[][]
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
function catalogMethods<Rels extends SchemaRelations>(
|
|
966
|
+
theory: Schema<Rels>,
|
|
967
|
+
tables: Tables,
|
|
968
|
+
owner: object,
|
|
969
|
+
assertLive: () => void,
|
|
970
|
+
ops: CatalogNative
|
|
971
|
+
): Pick<ReadInstance<Rels>, "scan" | "get" | "contains" | "execute" | "prepare"> {
|
|
972
|
+
function resolveOrdinary(relation: AnyRelation): RelationEntry {
|
|
973
|
+
const entry = tables.relations.get(relation.name)
|
|
974
|
+
if (entry === undefined || entry.member !== relation) {
|
|
975
|
+
throw errors.new(`relation ${relation.name} is not a member of schema ${theory.name}`)
|
|
976
|
+
}
|
|
977
|
+
if (isClosedMember(relation)) {
|
|
978
|
+
throw errors.new(
|
|
979
|
+
`relation ${relation.name} is closed — its extension is schema data (axioms), never scanned or written`
|
|
980
|
+
)
|
|
981
|
+
}
|
|
982
|
+
return entry
|
|
983
|
+
}
|
|
984
|
+
function declaredKeyOf(relation: AnyRelation, statement: Statement): PrimaryKey {
|
|
985
|
+
const statementId = tables.statements.findIndex(function byIdentity(candidate) {
|
|
986
|
+
return "statement" in candidate && candidate.statement === statement
|
|
987
|
+
})
|
|
988
|
+
const entry = tables.statements[statementId]
|
|
989
|
+
if (entry === undefined) {
|
|
990
|
+
throw errors.new(
|
|
991
|
+
`keyed get statement is not a declared statement of schema ${theory.name} — statement identity is the membership rule`
|
|
992
|
+
)
|
|
993
|
+
}
|
|
994
|
+
if (entry.kind !== "functionality") {
|
|
995
|
+
throw errors.new("keyed get takes a key() statement — containments and capacity statements key nothing")
|
|
996
|
+
}
|
|
997
|
+
if (entry.owner !== relation.name) {
|
|
998
|
+
throw errors.new(
|
|
999
|
+
`keyed get statement keys ${entry.owner}, not ${relation.name} — the statement must be a declared key of the relation it reads`
|
|
1000
|
+
)
|
|
1001
|
+
}
|
|
1002
|
+
return Object.freeze({ statementId, projection: entry.projection })
|
|
1003
|
+
}
|
|
1004
|
+
function planOf(prepared: object): PreparedPlan {
|
|
1005
|
+
const plan = preparedPlans.get(prepared)
|
|
1006
|
+
if (plan === undefined) {
|
|
1007
|
+
throw errors.wrap(ErrForeignPrepared, "bumbledb execute target is not a prepared value of this SDK")
|
|
1008
|
+
}
|
|
1009
|
+
if (plan.owner !== owner) {
|
|
1010
|
+
throw errors.wrap(
|
|
1011
|
+
ErrForeignPrepared,
|
|
1012
|
+
`bumbledb prepared value was prepared by a different store than this one (schema ${theory.name})`
|
|
1013
|
+
)
|
|
1014
|
+
}
|
|
1015
|
+
return plan
|
|
1016
|
+
}
|
|
1017
|
+
function contains<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean {
|
|
1018
|
+
assertLive()
|
|
1019
|
+
const entry = resolveOrdinary(relation)
|
|
1020
|
+
return bridged("bumbledb instance contains", function readContains() {
|
|
1021
|
+
return ops.contains(entry.id, rowOf(relation.data, recordOf(fact)))
|
|
1022
|
+
})
|
|
1023
|
+
}
|
|
1024
|
+
function get<R extends MemberRelation<Rels>, const P extends readonly string[]>(
|
|
1025
|
+
relation: R,
|
|
1026
|
+
keyOrStatement: KeyFact<R> | KeyStatement<R, P>,
|
|
1027
|
+
declaredKey?: DeclaredKeyFact<R, P>
|
|
1028
|
+
): Fact<R> | undefined {
|
|
1029
|
+
assertLive()
|
|
1030
|
+
const entry = resolveOrdinary(relation)
|
|
1031
|
+
return selectKeyRead(
|
|
1032
|
+
keyOrStatement,
|
|
1033
|
+
declaredKey,
|
|
1034
|
+
function byStatement(statement, key) {
|
|
1035
|
+
const selected = declaredKeyOf(relation, statement)
|
|
1036
|
+
const row = bridged("bumbledb instance get", function readGet() {
|
|
1037
|
+
return ops.get(entry.id, selected.statementId, keyRowOf(relation.data, selected.projection, recordOf(key)))
|
|
1038
|
+
})
|
|
1039
|
+
return row === null ? undefined : factOf(relation, row)
|
|
1040
|
+
},
|
|
1041
|
+
function byPrimary(key) {
|
|
1042
|
+
const primaryKey = entry.primaryKey
|
|
1043
|
+
if (primaryKey === undefined) {
|
|
1044
|
+
throw errors.new(
|
|
1045
|
+
`relation ${relation.name} has no candidate key — keyed get requires a fresh field or a declared key statement`
|
|
1046
|
+
)
|
|
1047
|
+
}
|
|
1048
|
+
const row = bridged("bumbledb instance get", function readGet() {
|
|
1049
|
+
return ops.get(
|
|
1050
|
+
entry.id,
|
|
1051
|
+
primaryKey.statementId,
|
|
1052
|
+
keyRowOf(relation.data, primaryKey.projection, recordOf(key))
|
|
1053
|
+
)
|
|
1054
|
+
})
|
|
1055
|
+
return row === null ? undefined : factOf(relation, row)
|
|
1056
|
+
}
|
|
1057
|
+
)
|
|
1058
|
+
}
|
|
1059
|
+
function scan<R extends MemberRelation<Rels>>(relation: R): Fact<R>[] {
|
|
1060
|
+
assertLive()
|
|
1061
|
+
const entry = resolveOrdinary(relation)
|
|
1062
|
+
const rows = bridged("bumbledb instance scan", function readScan() {
|
|
1063
|
+
return ops.scan(entry.id)
|
|
1064
|
+
})
|
|
1065
|
+
return rows.map(function decodeRow(row) {
|
|
1066
|
+
return factOf(relation, row)
|
|
1067
|
+
})
|
|
1068
|
+
}
|
|
1069
|
+
function execute<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Row[] {
|
|
1070
|
+
assertLive()
|
|
1071
|
+
const plan = planOf(prepared)
|
|
1072
|
+
const wire = wireParams(plan.params, recordOf(params))
|
|
1073
|
+
const rows = bridged("execute bumbledb prepared query", function callExecute() {
|
|
1074
|
+
return ops.execute(plan.handle, wire)
|
|
1075
|
+
})
|
|
1076
|
+
return decodeAnswers<Row>(plan.finds, rows)
|
|
1077
|
+
}
|
|
1078
|
+
function prepare<Row, Params extends ParamsRecord>(q: Query<Rels, Row, Params>): Prepared<Rels, Row, Params> {
|
|
1079
|
+
assertLive()
|
|
1080
|
+
if (q.schema !== theory) {
|
|
1081
|
+
throw errors.new(
|
|
1082
|
+
`query was built against schema ${q.schema.name}, not the identical schema value this store opened with — schema identity is the membership rule`
|
|
1083
|
+
)
|
|
1084
|
+
}
|
|
1085
|
+
const queryIr = lowerQuery(q)
|
|
1086
|
+
const outcome = bridged("prepare bumbledb query", function callPrepare() {
|
|
1087
|
+
return ops.prepare(queryIr)
|
|
1088
|
+
})
|
|
1089
|
+
if (!outcome.ok) {
|
|
1090
|
+
throwPrepareRefusal(outcome.message)
|
|
1091
|
+
}
|
|
1092
|
+
const prepared: Prepared<Rels, Row, Params> = Object.freeze({})
|
|
1093
|
+
preparedPlans.set(
|
|
1094
|
+
prepared,
|
|
1095
|
+
Object.freeze({
|
|
1096
|
+
handle: outcome.prepared,
|
|
1097
|
+
owner,
|
|
1098
|
+
params: q.data.params,
|
|
1099
|
+
finds: q.data.finds
|
|
1100
|
+
})
|
|
1101
|
+
)
|
|
1102
|
+
planReclaimer.register(prepared, outcome.prepared)
|
|
1103
|
+
return prepared
|
|
1104
|
+
}
|
|
1105
|
+
return { scan, get, contains, execute, prepare }
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
function ordinaryEntry(tables: Tables, theory: AnySchema, relation: AnyRelation): RelationEntry {
|
|
1109
|
+
const entry = tables.relations.get(relation.name)
|
|
1110
|
+
if (entry === undefined || entry.member !== relation) {
|
|
1111
|
+
throw errors.new(`relation ${relation.name} is not a member of schema ${theory.name}`)
|
|
1112
|
+
}
|
|
1113
|
+
if (isClosedMember(relation)) {
|
|
1114
|
+
throw errors.new(
|
|
1115
|
+
`relation ${relation.name} is closed — its extension is schema data (axioms), never scanned or written`
|
|
1116
|
+
)
|
|
1117
|
+
}
|
|
1118
|
+
return entry
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
function overlayMethods<Rels extends SchemaRelations>(
|
|
1122
|
+
theory: Schema<Rels>,
|
|
1123
|
+
tables: Tables,
|
|
1124
|
+
assertLive: () => void,
|
|
1125
|
+
reads: PointReads
|
|
1126
|
+
): Pick<WriteTx<Rels>, "contains" | "get"> {
|
|
1127
|
+
function declaredKeyOf(relation: AnyRelation, statement: Statement): PrimaryKey {
|
|
1128
|
+
const statementId = tables.statements.findIndex(function byIdentity(candidate) {
|
|
1129
|
+
return "statement" in candidate && candidate.statement === statement
|
|
1130
|
+
})
|
|
1131
|
+
const entry = tables.statements[statementId]
|
|
1132
|
+
if (entry === undefined) {
|
|
1133
|
+
throw errors.new(
|
|
1134
|
+
`keyed get statement is not a declared statement of schema ${theory.name} — statement identity is the membership rule`
|
|
1135
|
+
)
|
|
1136
|
+
}
|
|
1137
|
+
if (entry.kind !== "functionality") {
|
|
1138
|
+
throw errors.new("keyed get takes a key() statement — containments and capacity statements key nothing")
|
|
1139
|
+
}
|
|
1140
|
+
if (entry.owner !== relation.name) {
|
|
1141
|
+
throw errors.new(
|
|
1142
|
+
`keyed get statement keys ${entry.owner}, not ${relation.name} — the statement must be a declared key of the relation it reads`
|
|
1143
|
+
)
|
|
1144
|
+
}
|
|
1145
|
+
return Object.freeze({ statementId, projection: entry.projection })
|
|
1146
|
+
}
|
|
1147
|
+
function contains<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean {
|
|
1148
|
+
assertLive()
|
|
1149
|
+
const entry = ordinaryEntry(tables, theory, relation)
|
|
1150
|
+
return reads.contains(entry.id, rowOf(relation.data, recordOf(fact)))
|
|
1151
|
+
}
|
|
1152
|
+
function readThroughKey<R extends MemberRelation<Rels>>(
|
|
1153
|
+
relation: R,
|
|
1154
|
+
entry: RelationEntry,
|
|
1155
|
+
selected: PrimaryKey,
|
|
1156
|
+
key: Readonly<Record<string, unknown>>
|
|
1157
|
+
): Fact<R> | undefined {
|
|
1158
|
+
const row = reads.get(entry.id, selected.statementId, keyRowOf(relation.data, selected.projection, key))
|
|
1159
|
+
if (row === null) {
|
|
1160
|
+
return undefined
|
|
1161
|
+
}
|
|
1162
|
+
return factOf(relation, row)
|
|
1163
|
+
}
|
|
1164
|
+
function get<R extends MemberRelation<Rels>>(relation: R, key: KeyFact<R>): Fact<R> | undefined
|
|
1165
|
+
function get<R extends MemberRelation<Rels>, const P extends readonly string[]>(
|
|
1166
|
+
relation: R,
|
|
1167
|
+
keyStatement: KeyStatement<R, P>,
|
|
1168
|
+
key: DeclaredKeyFact<R, P>
|
|
1169
|
+
): Fact<R> | undefined
|
|
1170
|
+
function get<R extends MemberRelation<Rels>, const P extends readonly string[]>(
|
|
1171
|
+
relation: R,
|
|
1172
|
+
keyOrStatement: KeyFact<R> | KeyStatement<R, P>,
|
|
1173
|
+
declaredKey?: DeclaredKeyFact<R, P>
|
|
1174
|
+
): Fact<R> | undefined {
|
|
1175
|
+
assertLive()
|
|
1176
|
+
const entry = ordinaryEntry(tables, theory, relation)
|
|
1177
|
+
return selectKeyRead(
|
|
1178
|
+
keyOrStatement,
|
|
1179
|
+
declaredKey,
|
|
1180
|
+
function byStatement(statement, key) {
|
|
1181
|
+
return readThroughKey(relation, entry, declaredKeyOf(relation, statement), recordOf(key))
|
|
1182
|
+
},
|
|
1183
|
+
function byPrimary(key) {
|
|
1184
|
+
const primaryKey = entry.primaryKey
|
|
1185
|
+
if (primaryKey === undefined) {
|
|
1186
|
+
throw errors.new(
|
|
1187
|
+
`relation ${relation.name} has no candidate key — keyed get requires a fresh field or a declared key statement`
|
|
1188
|
+
)
|
|
1189
|
+
}
|
|
1190
|
+
return readThroughKey(relation, entry, primaryKey, recordOf(key))
|
|
1191
|
+
}
|
|
1192
|
+
)
|
|
1193
|
+
}
|
|
1194
|
+
return { contains, get }
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
function createReadInstance<Rels extends SchemaRelations>(
|
|
1198
|
+
nativeHandle: InstanceHandle,
|
|
1199
|
+
theory: Schema<Rels>,
|
|
1200
|
+
tables: Tables,
|
|
1201
|
+
owner: object
|
|
1202
|
+
): ReadInstance<Rels> {
|
|
1203
|
+
const state: InstanceState = { handle: nativeHandle, live: true, owner }
|
|
1204
|
+
function assertLive(): void {
|
|
1205
|
+
if (!state.live) {
|
|
1206
|
+
throw errors.wrap(
|
|
1207
|
+
ErrUseAfterScope,
|
|
1208
|
+
"bumbledb read instance is invalidated — its owning callback already returned"
|
|
1209
|
+
)
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
const methods = catalogMethods(theory, tables, owner, assertLive, {
|
|
1213
|
+
scan(relationId) {
|
|
1214
|
+
return native.instanceScan(state.handle, relationId)
|
|
1215
|
+
},
|
|
1216
|
+
contains(relationId, values) {
|
|
1217
|
+
return native.instanceContains(state.handle, relationId, values)
|
|
1218
|
+
},
|
|
1219
|
+
get(relationId, statementId, keyValues) {
|
|
1220
|
+
return native.instanceGet(state.handle, relationId, statementId, keyValues)
|
|
1221
|
+
},
|
|
1222
|
+
prepare(query) {
|
|
1223
|
+
return native.instancePrepare(state.handle, query)
|
|
1224
|
+
},
|
|
1225
|
+
execute(prepared, params) {
|
|
1226
|
+
return native.preparedExecute(prepared, state.handle, params)
|
|
1227
|
+
}
|
|
1228
|
+
})
|
|
1229
|
+
const instance: ReadInstance<Rels> = Object.freeze({
|
|
1230
|
+
get generation() {
|
|
1231
|
+
assertLive()
|
|
1232
|
+
return bridged("bumbledb instance generation", function readGeneration() {
|
|
1233
|
+
return native.instanceGeneration(state.handle)
|
|
1234
|
+
})
|
|
1235
|
+
},
|
|
1236
|
+
...methods
|
|
1237
|
+
})
|
|
1238
|
+
instanceStates.set(instance, state)
|
|
1239
|
+
return instance
|
|
1240
|
+
}
|
|
849
1241
|
|
|
850
1242
|
/**
|
|
851
1243
|
* Constructs one open `Db` over an already-admitted handle: builds the
|
|
@@ -1022,152 +1414,70 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
1022
1414
|
return { contains, get }
|
|
1023
1415
|
}
|
|
1024
1416
|
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
)
|
|
1038
|
-
|
|
1039
|
-
|
|
1417
|
+
function pinPrepared<Row, Params extends ParamsRecord>(
|
|
1418
|
+
preparedHandle: PreparedHandle,
|
|
1419
|
+
q: Query<Rels, Row, Params>
|
|
1420
|
+
): Prepared<Rels, Row, Params> {
|
|
1421
|
+
const prepared: Prepared<Rels, Row, Params> = Object.freeze({})
|
|
1422
|
+
preparedPlans.set(
|
|
1423
|
+
prepared,
|
|
1424
|
+
Object.freeze({
|
|
1425
|
+
handle: preparedHandle,
|
|
1426
|
+
owner,
|
|
1427
|
+
params: q.data.params,
|
|
1428
|
+
finds: q.data.finds
|
|
1429
|
+
})
|
|
1430
|
+
)
|
|
1431
|
+
planReclaimer.register(prepared, preparedHandle)
|
|
1432
|
+
return prepared
|
|
1040
1433
|
}
|
|
1041
1434
|
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
throw errors.new("bumbledb read scope is invalidated — its owning read callback already returned")
|
|
1053
|
-
}
|
|
1054
|
-
}
|
|
1055
|
-
const reads = pointReadsOf(assertLive, {
|
|
1056
|
-
contains(relationId, row) {
|
|
1057
|
-
return bridged("bumbledb snapshot contains", function readContains() {
|
|
1058
|
-
return native.snapshotContains(state.handle, relationId, row)
|
|
1059
|
-
})
|
|
1060
|
-
},
|
|
1061
|
-
get(relationId, statementId, key) {
|
|
1062
|
-
return bridged("bumbledb snapshot get", function readGet() {
|
|
1063
|
-
return native.snapshotGet(state.handle, relationId, statementId, key)
|
|
1435
|
+
function makeWitness(nativeHandle: WitnessHandle): Witness<Rels> {
|
|
1436
|
+
const state: WitnessState = { handle: nativeHandle, spent: false, owner }
|
|
1437
|
+
const witness: Witness<Rels> = Object.freeze({
|
|
1438
|
+
[Symbol.dispose](): void {
|
|
1439
|
+
if (state.spent) {
|
|
1440
|
+
return
|
|
1441
|
+
}
|
|
1442
|
+
state.spent = true
|
|
1443
|
+
bridged("close bumbledb witness", function closeWitness() {
|
|
1444
|
+
native.witnessClose(nativeHandle)
|
|
1064
1445
|
})
|
|
1065
1446
|
}
|
|
1066
1447
|
})
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
const rows = bridged("bumbledb snapshot scan", function readScan() {
|
|
1071
|
-
return native.snapshotScan(state.handle, entry.id)
|
|
1072
|
-
})
|
|
1073
|
-
return rows.map(function decodeRow(row) {
|
|
1074
|
-
return factOf(relation, row)
|
|
1075
|
-
})
|
|
1076
|
-
}
|
|
1077
|
-
function execute<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Row[] {
|
|
1078
|
-
assertLive()
|
|
1079
|
-
const plan = planOf(prepared)
|
|
1080
|
-
const wire = wireParams(plan.params, recordOf(params))
|
|
1081
|
-
const rows = bridged("execute bumbledb prepared query", function callExecute() {
|
|
1082
|
-
return native.preparedExecute(plan.handle, state.handle, wire)
|
|
1083
|
-
})
|
|
1084
|
-
return decodeAnswers<Row>(plan.finds, rows)
|
|
1085
|
-
}
|
|
1086
|
-
/** The R12 teardown: invalidate, then close — idempotent through the state's close latch. */
|
|
1087
|
-
function dispose(): void {
|
|
1088
|
-
state.live = false
|
|
1089
|
-
closeScopeState(state)
|
|
1090
|
-
}
|
|
1091
|
-
const scope: ReadScope<Rels> = Object.freeze({
|
|
1092
|
-
generation: state.generation,
|
|
1093
|
-
scan,
|
|
1094
|
-
get: reads.get,
|
|
1095
|
-
contains: reads.contains,
|
|
1096
|
-
execute,
|
|
1097
|
-
[Symbol.dispose]: dispose
|
|
1098
|
-
})
|
|
1099
|
-
scopeStates.set(scope, state)
|
|
1100
|
-
return scope
|
|
1448
|
+
witnessStates.set(witness, state)
|
|
1449
|
+
witnessReclaimer.register(witness, nativeHandle)
|
|
1450
|
+
return witness
|
|
1101
1451
|
}
|
|
1102
1452
|
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
* snapshot open/close is counted so a write-begin failure can report how
|
|
1106
|
-
* many read handles were live at the fault — a leaked scope is invisible
|
|
1107
|
-
* until the exact moment it matters, so the failure carries the census.
|
|
1108
|
-
*/
|
|
1109
|
-
let liveSnapshots = 0
|
|
1110
|
-
|
|
1111
|
-
/**
|
|
1112
|
-
* Opens one snapshot and its scope state (live until the owner flips
|
|
1113
|
-
* it). The witnessed generation rides the snapshot open itself — one
|
|
1114
|
-
* crossing carries both (finding 016), so the fault-pairing close
|
|
1115
|
-
* branch a second `dbGeneration` call needed is structurally gone.
|
|
1116
|
-
*/
|
|
1117
|
-
function openScopeState(): ScopeState {
|
|
1118
|
-
const opened = bridged("open bumbledb snapshot", function openSnapshot() {
|
|
1119
|
-
return native.dbSnapshot(handle)
|
|
1120
|
-
})
|
|
1121
|
-
liveSnapshots += 1
|
|
1122
|
-
return { handle: opened.snapshot, generation: opened.generation, live: true, closed: false, owner }
|
|
1453
|
+
function makeInstance(nativeHandle: InstanceHandle): ReadInstance<Rels> {
|
|
1454
|
+
return createReadInstance(nativeHandle, theory, tables, owner)
|
|
1123
1455
|
}
|
|
1124
1456
|
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
function read<T>(fn: (snap: ReadScope<Rels>) => T): T
|
|
1144
|
-
function read(): ReadScope<Rels>
|
|
1145
|
-
function read<T>(fn?: (snap: ReadScope<Rels>) => T): T | ReadScope<Rels> {
|
|
1146
|
-
const state = openScopeState()
|
|
1147
|
-
const scope = makeScope(state)
|
|
1148
|
-
if (fn === undefined) {
|
|
1149
|
-
/**
|
|
1150
|
-
* The `using` acquisition (R12): the caller owns the lifetime —
|
|
1151
|
-
* `using snap = db.read()` — and the scope's `Symbol.dispose`
|
|
1152
|
-
* is the deterministic release, scope-shaped in the language's
|
|
1153
|
-
* own syntax.
|
|
1154
|
-
*/
|
|
1155
|
-
return scope
|
|
1156
|
-
}
|
|
1157
|
-
const result = errors.trySync(function runRead() {
|
|
1158
|
-
return fn(scope)
|
|
1457
|
+
function read<R>(body: (instance: ReadInstance<Rels>, witness: Witness<Rels>) => SyncResult<R>): SyncResult<R> {
|
|
1458
|
+
let captured: R | undefined
|
|
1459
|
+
const result = bridged("bumbledb read", function runRead() {
|
|
1460
|
+
return native.dbRead(handle, function onRead(nativeInstance, nativeWitness) {
|
|
1461
|
+
const instance = makeInstance(nativeInstance)
|
|
1462
|
+
const witness = makeWitness(nativeWitness)
|
|
1463
|
+
const value = body(instance, witness)
|
|
1464
|
+
const state = instanceStates.get(instance)
|
|
1465
|
+
if (state !== undefined) {
|
|
1466
|
+
state.live = false
|
|
1467
|
+
}
|
|
1468
|
+
if (isThenable(value)) {
|
|
1469
|
+
throw errors.wrap(ErrAsyncCallback, "bumbledb read callback returned a thenable")
|
|
1470
|
+
}
|
|
1471
|
+
captured = value
|
|
1472
|
+
return value
|
|
1473
|
+
})
|
|
1159
1474
|
})
|
|
1160
|
-
|
|
1161
|
-
closeScopeState(state)
|
|
1162
|
-
if (result.error) {
|
|
1163
|
-
throw errors.wrap(result.error, "bumbledb read")
|
|
1164
|
-
}
|
|
1165
|
-
return result.data
|
|
1475
|
+
return (captured ?? result) as SyncResult<R>
|
|
1166
1476
|
}
|
|
1167
1477
|
|
|
1168
1478
|
function scan<R extends MemberRelation<Rels>>(relation: R): Fact<R>[] {
|
|
1169
|
-
return read(function scanInScope(
|
|
1170
|
-
return
|
|
1479
|
+
return read(function scanInScope(instance) {
|
|
1480
|
+
return instance.scan(relation)
|
|
1171
1481
|
})
|
|
1172
1482
|
}
|
|
1173
1483
|
|
|
@@ -1182,41 +1492,39 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
1182
1492
|
keyOrStatement: KeyFact<R> | KeyStatement<R, P>,
|
|
1183
1493
|
declaredKey?: DeclaredKeyFact<R, P>
|
|
1184
1494
|
): Fact<R> | undefined {
|
|
1185
|
-
|
|
1186
|
-
|
|
1495
|
+
let found: Fact<R> | undefined
|
|
1496
|
+
read(function getInScope(instance) {
|
|
1497
|
+
found = selectKeyRead(
|
|
1187
1498
|
keyOrStatement,
|
|
1188
1499
|
declaredKey,
|
|
1189
1500
|
function byStatement(statement, key) {
|
|
1190
|
-
return
|
|
1501
|
+
return instance.get(relation, statement, key)
|
|
1191
1502
|
},
|
|
1192
1503
|
function byPrimary(key) {
|
|
1193
|
-
return
|
|
1504
|
+
return instance.get(relation, key)
|
|
1194
1505
|
}
|
|
1195
1506
|
)
|
|
1196
1507
|
})
|
|
1508
|
+
return found
|
|
1197
1509
|
}
|
|
1198
1510
|
|
|
1199
1511
|
function contains<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean {
|
|
1200
|
-
return read(function containsInScope(
|
|
1201
|
-
return
|
|
1512
|
+
return read(function containsInScope(instance) {
|
|
1513
|
+
return instance.contains(relation, fact)
|
|
1202
1514
|
})
|
|
1203
1515
|
}
|
|
1204
1516
|
|
|
1205
1517
|
function execute<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Row[] {
|
|
1206
|
-
return read(function executeInScope(
|
|
1207
|
-
return
|
|
1518
|
+
return read(function executeInScope(instance) {
|
|
1519
|
+
return instance.execute(prepared, params)
|
|
1208
1520
|
})
|
|
1209
1521
|
}
|
|
1210
1522
|
|
|
1211
|
-
|
|
1212
|
-
* Builds one {@link Tx} over a transaction-handle thunk: `write` and
|
|
1213
|
-
* `writeFrom` pass an already-begun handle.
|
|
1214
|
-
*/
|
|
1215
|
-
function makeTx(resolveTx: () => TxHandle): { readonly tx: Tx<Rels>; spend(): void } {
|
|
1523
|
+
function makeTx(resolveTx: () => TxHandle): { readonly tx: WriteTx<Rels>; spend(): void } {
|
|
1216
1524
|
const txState = { spent: false }
|
|
1217
1525
|
function assertLive(): void {
|
|
1218
1526
|
if (txState.spent) {
|
|
1219
|
-
throw errors.
|
|
1527
|
+
throw errors.wrap(ErrUseAfterScope, "bumbledb write transaction is spent")
|
|
1220
1528
|
}
|
|
1221
1529
|
}
|
|
1222
1530
|
const reads = pointReadsOf(assertLive, {
|
|
@@ -1233,29 +1541,31 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
1233
1541
|
})
|
|
1234
1542
|
}
|
|
1235
1543
|
})
|
|
1236
|
-
function insert<R extends MemberRelation<Rels>>(relation: R, facts:
|
|
1544
|
+
function insert<R extends MemberRelation<Rels>>(relation: R, facts: CollectionWrite<R>): MutationReport {
|
|
1237
1545
|
assertLive()
|
|
1238
|
-
const rows: FactValue[][] = []
|
|
1239
|
-
for (const fact of facts) {
|
|
1240
|
-
rows.push(rowOf(relation.data, recordOf(fact)))
|
|
1241
|
-
}
|
|
1242
1546
|
const entry = resolveOrdinary(relation)
|
|
1243
1547
|
const txHandle = resolveTx()
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1548
|
+
return mutateCollection(
|
|
1549
|
+
relation,
|
|
1550
|
+
facts,
|
|
1551
|
+
function applyRows(rows) {
|
|
1552
|
+
return bridged("bumbledb tx insert", function record() {
|
|
1553
|
+
return native.txInsert(txHandle, entry.id, rows)
|
|
1554
|
+
})
|
|
1555
|
+
},
|
|
1556
|
+
function applyColumns(columns) {
|
|
1557
|
+
return bridged("bumbledb tx insert", function recordColumns() {
|
|
1558
|
+
return native.txInsertColumns(txHandle, entry.id, columns)
|
|
1559
|
+
})
|
|
1560
|
+
}
|
|
1561
|
+
)
|
|
1248
1562
|
}
|
|
1249
1563
|
function remove<R extends MemberRelation<Rels>>(relation: R, facts: Iterable<Fact<R>>): MutationReport {
|
|
1250
1564
|
assertLive()
|
|
1251
|
-
const rows: FactValue[][] = []
|
|
1252
|
-
for (const fact of facts) {
|
|
1253
|
-
rows.push(rowOf(relation.data, recordOf(fact)))
|
|
1254
|
-
}
|
|
1255
1565
|
const entry = resolveOrdinary(relation)
|
|
1256
1566
|
const txHandle = resolveTx()
|
|
1257
1567
|
const report = bridged("bumbledb tx delete", function record() {
|
|
1258
|
-
return native.txDelete(txHandle, entry.id,
|
|
1568
|
+
return native.txDelete(txHandle, entry.id, rowsOf(relation, facts))
|
|
1259
1569
|
})
|
|
1260
1570
|
return Object.freeze({ submitted: report.submitted, changed: report.changed })
|
|
1261
1571
|
}
|
|
@@ -1282,7 +1592,7 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
1282
1592
|
})
|
|
1283
1593
|
return freshRangeOf(range)
|
|
1284
1594
|
}
|
|
1285
|
-
const tx:
|
|
1595
|
+
const tx: WriteTx<Rels> = Object.freeze({
|
|
1286
1596
|
insert,
|
|
1287
1597
|
delete: remove,
|
|
1288
1598
|
reserve,
|
|
@@ -1295,111 +1605,92 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
1295
1605
|
return { tx, spend }
|
|
1296
1606
|
}
|
|
1297
1607
|
|
|
1298
|
-
function
|
|
1299
|
-
|
|
1300
|
-
return
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
})
|
|
1305
|
-
made.spend()
|
|
1306
|
-
if (built.error) {
|
|
1307
|
-
bridged("abort bumbledb write transaction", function abort() {
|
|
1308
|
-
native.txAbort(txHandle)
|
|
1309
|
-
})
|
|
1310
|
-
throw errors.wrap(built.error, "build write delta")
|
|
1311
|
-
}
|
|
1312
|
-
if (isThenable(built.data)) {
|
|
1313
|
-
/**
|
|
1314
|
-
* An `async` callback TYPECHECKS (Promise<void> is assignable where
|
|
1315
|
-
* a `void` return is expected) but its body runs after the tx is
|
|
1316
|
-
* spent: committing here would be a silent EMPTY commit reported
|
|
1317
|
-
* ok while the callback's real inserts throw "spent" as unhandled
|
|
1318
|
-
* rejections. Refused typed instead — abort, nothing committed
|
|
1319
|
-
* (the same one-writer law as the thrown-callback path).
|
|
1320
|
-
*/
|
|
1321
|
-
bridged("abort bumbledb write transaction", function abort() {
|
|
1322
|
-
native.txAbort(txHandle)
|
|
1608
|
+
function mapNativeWrite<R>(nativeOutcome: NativeWriteOutcome, built: R | undefined): WriteFromOutcome<Rels, R> {
|
|
1609
|
+
if (nativeOutcome.tag === "moved") {
|
|
1610
|
+
return Object.freeze({
|
|
1611
|
+
tag: "moved" as const,
|
|
1612
|
+
witnessed: nativeOutcome.witnessed,
|
|
1613
|
+
current: nativeOutcome.current
|
|
1323
1614
|
})
|
|
1324
|
-
throw errors.new(
|
|
1325
|
-
"bumbledb write callback returned a thenable — the delta build is synchronous; an async callback is refused, nothing was committed"
|
|
1326
|
-
)
|
|
1327
1615
|
}
|
|
1328
|
-
if (
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
* even an empty commit; commit is unreachable for a sentinel
|
|
1333
|
-
* result.
|
|
1334
|
-
*/
|
|
1335
|
-
bridged("abort bumbledb write transaction", function abort() {
|
|
1336
|
-
native.txAbort(txHandle)
|
|
1616
|
+
if (nativeOutcome.tag === "rejected") {
|
|
1617
|
+
return Object.freeze({
|
|
1618
|
+
tag: "rejected" as const,
|
|
1619
|
+
violations: Object.freeze(nativeOutcome.violations.map(violationOf))
|
|
1337
1620
|
})
|
|
1338
|
-
return abandonedOutcome<Rels, R>(built.data)
|
|
1339
1621
|
}
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
})
|
|
1344
|
-
})
|
|
1345
|
-
if (committed.error) {
|
|
1346
|
-
/**
|
|
1347
|
-
* A THROWN commit (engine I/O failure, bridge fault) must never
|
|
1348
|
-
* leave the write transaction live: LMDB holds one writer per
|
|
1349
|
-
* environment, and a leaked handle turns every later begin into
|
|
1350
|
-
* EINVAL for the process's lifetime. The abort is best-effort —
|
|
1351
|
-
* the native side may already have consumed the handle.
|
|
1352
|
-
*/
|
|
1353
|
-
const aborted = errors.trySync(function abortAfterFailedCommit() {
|
|
1354
|
-
native.txAbort(txHandle)
|
|
1355
|
-
})
|
|
1356
|
-
if (aborted.error) {
|
|
1622
|
+
if (nativeOutcome.tag === "abandoned") {
|
|
1623
|
+
if (built === undefined || !isAbandon(built)) {
|
|
1624
|
+
throw errors.new("bumbledb write abandoned without an abandon sentinel")
|
|
1357
1625
|
}
|
|
1358
|
-
|
|
1359
|
-
}
|
|
1360
|
-
const outcome = committed.data
|
|
1361
|
-
if (outcome.ok) {
|
|
1362
|
-
return Object.freeze({ ok: true, generation: outcome.generation })
|
|
1626
|
+
return abandonedOutcome<Rels, R>(built)
|
|
1363
1627
|
}
|
|
1364
1628
|
return Object.freeze({
|
|
1365
|
-
|
|
1366
|
-
|
|
1629
|
+
tag: "accepted" as const,
|
|
1630
|
+
value: Object.freeze({
|
|
1631
|
+
value: built as Exclude<R, Abandon<unknown>>,
|
|
1632
|
+
generation: nativeOutcome.generation
|
|
1633
|
+
})
|
|
1367
1634
|
})
|
|
1368
1635
|
}
|
|
1369
1636
|
|
|
1370
|
-
function
|
|
1371
|
-
|
|
1372
|
-
|
|
1637
|
+
function runWrite<R>(
|
|
1638
|
+
invoke: (callback: (tx: TxHandle) => boolean) => NativeWriteOutcome,
|
|
1639
|
+
fn: (tx: WriteTx<Rels>) => SyncResult<R>
|
|
1640
|
+
): WriteFromOutcome<Rels, SyncResult<R>> {
|
|
1641
|
+
let built: SyncResult<R> | undefined
|
|
1642
|
+
const nativeOutcome = bridged("bumbledb write", function callWrite() {
|
|
1643
|
+
return invoke(function onWrite(txHandle) {
|
|
1644
|
+
const made = makeTx(function resolveTx() {
|
|
1645
|
+
return txHandle
|
|
1646
|
+
})
|
|
1647
|
+
const result = errors.trySync(function buildDelta() {
|
|
1648
|
+
return fn(made.tx)
|
|
1649
|
+
})
|
|
1650
|
+
made.spend()
|
|
1651
|
+
if (result.error) {
|
|
1652
|
+
throw errors.wrap(result.error, "build write delta")
|
|
1653
|
+
}
|
|
1654
|
+
if (isThenable(result.data)) {
|
|
1655
|
+
throw errors.wrap(ErrAsyncCallback, "bumbledb write callback returned a thenable")
|
|
1656
|
+
}
|
|
1657
|
+
built = result.data
|
|
1658
|
+
return !isAbandon(result.data)
|
|
1659
|
+
})
|
|
1373
1660
|
})
|
|
1374
|
-
return
|
|
1661
|
+
return mapNativeWrite(nativeOutcome, built)
|
|
1375
1662
|
}
|
|
1376
1663
|
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
const snapState = scopeStates.get(snap)
|
|
1384
|
-
if (snapState === undefined) {
|
|
1385
|
-
throw errors.new("bumbledb writeFrom witness is not a read scope of this SDK")
|
|
1386
|
-
}
|
|
1387
|
-
if (snapState.owner !== owner) {
|
|
1388
|
-
throw errors.new(`bumbledb writeFrom snapshot belongs to a different store (schema ${theory.name})`)
|
|
1664
|
+
function write<R>(fn: (tx: WriteTx<Rels>) => SyncResult<R>): WriteOutcome<Rels, SyncResult<R>> {
|
|
1665
|
+
const outcome = runWrite(function invoke(callback) {
|
|
1666
|
+
return native.dbWrite(handle, callback)
|
|
1667
|
+
}, fn)
|
|
1668
|
+
if (outcome.tag === "moved") {
|
|
1669
|
+
throw errors.new("bumbledb write reported moved — unconditional writes cannot move")
|
|
1389
1670
|
}
|
|
1390
|
-
|
|
1391
|
-
|
|
1671
|
+
return outcome
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
function writeFrom<R>(
|
|
1675
|
+
witness: Witness<Rels>,
|
|
1676
|
+
fn: (tx: WriteTx<Rels>) => SyncResult<R>
|
|
1677
|
+
): WriteFromOutcome<Rels, SyncResult<R>> {
|
|
1678
|
+
const state = witnessStates.get(witness)
|
|
1679
|
+
if (state === undefined) {
|
|
1680
|
+
throw errors.wrap(ErrForeignWitness, "bumbledb writeFrom witness is not a witness of this SDK")
|
|
1392
1681
|
}
|
|
1393
|
-
|
|
1394
|
-
return native.dbWriteFrom(handle, snapState.handle)
|
|
1395
|
-
})
|
|
1396
|
-
if (!witnessed.ok) {
|
|
1682
|
+
if (state.owner !== owner) {
|
|
1397
1683
|
throw errors.wrap(
|
|
1398
|
-
|
|
1399
|
-
`writeFrom
|
|
1684
|
+
ErrForeignWitness,
|
|
1685
|
+
`bumbledb writeFrom witness belongs to a different store (schema ${theory.name})`
|
|
1400
1686
|
)
|
|
1401
1687
|
}
|
|
1402
|
-
|
|
1688
|
+
if (state.spent) {
|
|
1689
|
+
throw errors.wrap(ErrSpentHandle, "bumbledb writeFrom witness has been disposed")
|
|
1690
|
+
}
|
|
1691
|
+
return runWrite(function invoke(callback) {
|
|
1692
|
+
return native.dbWriteFrom(handle, state.handle, callback)
|
|
1693
|
+
}, fn)
|
|
1403
1694
|
}
|
|
1404
1695
|
|
|
1405
1696
|
function prepare<Row, Params extends ParamsRecord>(q: Query<Rels, Row, Params>): Prepared<Rels, Row, Params> {
|
|
@@ -1413,21 +1704,9 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
1413
1704
|
return native.dbPrepare(handle, queryIr)
|
|
1414
1705
|
})
|
|
1415
1706
|
if (!outcome.ok) {
|
|
1416
|
-
|
|
1707
|
+
throwPrepareRefusal(outcome.message)
|
|
1417
1708
|
}
|
|
1418
|
-
|
|
1419
|
-
const prepared: Prepared<Rels, Row, Params> = Object.freeze({})
|
|
1420
|
-
preparedPlans.set(
|
|
1421
|
-
prepared,
|
|
1422
|
-
Object.freeze({
|
|
1423
|
-
handle: preparedHandle,
|
|
1424
|
-
owner,
|
|
1425
|
-
params: q.data.params,
|
|
1426
|
-
finds: q.data.finds
|
|
1427
|
-
})
|
|
1428
|
-
)
|
|
1429
|
-
planReclaimer.register(prepared, preparedHandle)
|
|
1430
|
-
return prepared
|
|
1709
|
+
return pinPrepared(outcome.prepared, q)
|
|
1431
1710
|
}
|
|
1432
1711
|
|
|
1433
1712
|
return Object.freeze({
|
|
@@ -1456,90 +1735,425 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
1456
1735
|
const ErrNewtypeMismatch = errors.new(
|
|
1457
1736
|
"bumbledb newtypeMismatch: a statement pairs faces whose newtypes disagree — the faces of a dependency agree on their newtype, or neither carries one"
|
|
1458
1737
|
)
|
|
1738
|
+
const ErrSchemaError = errors.new("bumbledb schemaError: the declaration failed validation")
|
|
1739
|
+
const ErrFingerprintMismatch = errors.new(
|
|
1740
|
+
"bumbledb fingerprintMismatch: the store's schema does not match this theory"
|
|
1741
|
+
)
|
|
1742
|
+
const ErrIrError = errors.new("bumbledb irError: the query failed validation")
|
|
1743
|
+
|
|
1744
|
+
function throwOpenRefusal(
|
|
1745
|
+
verb: string,
|
|
1746
|
+
canonical: string,
|
|
1747
|
+
kind: "schemaError" | "newtypeMismatch" | "fingerprintMismatch",
|
|
1748
|
+
message: string
|
|
1749
|
+
): never {
|
|
1750
|
+
const detail = `${verb} ${canonical}: ${message}`
|
|
1751
|
+
if (kind === "newtypeMismatch") {
|
|
1752
|
+
throw errors.wrap(ErrNewtypeMismatch, detail)
|
|
1753
|
+
}
|
|
1754
|
+
if (kind === "schemaError") {
|
|
1755
|
+
throw errors.wrap(ErrSchemaError, detail)
|
|
1756
|
+
}
|
|
1757
|
+
throw errors.wrap(ErrFingerprintMismatch, detail)
|
|
1758
|
+
}
|
|
1459
1759
|
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
function
|
|
1472
|
-
verb: "create" | "open",
|
|
1760
|
+
function throwPrepareRefusal(message: string): never {
|
|
1761
|
+
throw errors.wrap(ErrIrError, `prepare: ${message}`)
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
function openFromHandle<Rels extends SchemaRelations>(dbHandle: DbHandle, theory: Schema<Rels>): Db<Rels> {
|
|
1765
|
+
const manifest = bridged("fetch bumbledb manifest", function fetchManifest() {
|
|
1766
|
+
return native.dbManifest(dbHandle)
|
|
1767
|
+
})
|
|
1768
|
+
return openDb(dbHandle, theory, manifest)
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
async function createStore<Rels extends SchemaRelations>(
|
|
1473
1772
|
storePath: string,
|
|
1474
1773
|
theory: Schema<Rels>
|
|
1475
|
-
): Db<Rels
|
|
1774
|
+
): Promise<Admission<Rels, Db<Rels>>> {
|
|
1476
1775
|
const canonical = path.resolve(storePath)
|
|
1477
1776
|
const spec = lower(theory)
|
|
1478
|
-
const
|
|
1479
|
-
|
|
1480
|
-
|
|
1777
|
+
const created = await bridgedAsync(`create bumbledb store at ${canonical}`, function callBridge() {
|
|
1778
|
+
return native.dbCreate(canonical, spec)
|
|
1779
|
+
})
|
|
1780
|
+
if (created.tag === "schemaError" || created.tag === "newtypeMismatch") {
|
|
1781
|
+
throwOpenRefusal("create", canonical, created.tag, created.message)
|
|
1782
|
+
}
|
|
1783
|
+
if (created.tag === "rejected") {
|
|
1784
|
+
return Object.freeze({
|
|
1785
|
+
tag: "rejected" as const,
|
|
1786
|
+
violations: Object.freeze(
|
|
1787
|
+
created.violations.map(function mapWire(wire) {
|
|
1788
|
+
return mapViolationWithoutStore<Rels>(theory, wire)
|
|
1789
|
+
})
|
|
1790
|
+
)
|
|
1791
|
+
})
|
|
1792
|
+
}
|
|
1793
|
+
return Object.freeze({ tag: "accepted" as const, value: openFromHandle(created.db, theory) })
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
function mapViolationWithoutStore<Rels extends SchemaRelations>(
|
|
1797
|
+
theory: Schema<Rels>,
|
|
1798
|
+
wire: WireViolation
|
|
1799
|
+
): Violation<Rels> {
|
|
1800
|
+
const entries = materializedEntries(theory)
|
|
1801
|
+
const entry = entries[wire.statementId]
|
|
1802
|
+
if (entry === undefined) {
|
|
1803
|
+
throw errors.new(`bumbledb violation cites unknown statement id ${wire.statementId}`)
|
|
1804
|
+
}
|
|
1805
|
+
function offending(fact: WireViolationFact): OffendingFact<Rels> {
|
|
1806
|
+
const member = theory.relations[fact.relation]
|
|
1807
|
+
if (member === undefined || !(fact.relation in theory.relations)) {
|
|
1808
|
+
throw errors.new(`bumbledb violation cites unknown relation ${fact.relation}`)
|
|
1481
1809
|
}
|
|
1810
|
+
const declared = sealedFieldsOf(member)
|
|
1811
|
+
const decoded: Record<string, FactValue> = {}
|
|
1812
|
+
for (const cell of fact.fields) {
|
|
1813
|
+
const cited = declared.find(function byName(candidate) {
|
|
1814
|
+
return candidate.name === cell.name
|
|
1815
|
+
})
|
|
1816
|
+
const roster = rosterOf(cited?.field)
|
|
1817
|
+
decoded[cell.name] =
|
|
1818
|
+
roster !== undefined
|
|
1819
|
+
? handleOf(`violation fact ${fact.relation} field ${cell.name}`, roster, cell.value)
|
|
1820
|
+
: cell.value
|
|
1821
|
+
}
|
|
1822
|
+
return Object.freeze({ relation: fact.relation as keyof Rels & string, fact: Object.freeze(decoded) })
|
|
1823
|
+
}
|
|
1824
|
+
const facts = Object.freeze(wire.facts.map(offending))
|
|
1825
|
+
const canonical = wire.canonical
|
|
1826
|
+
if (entry.kind === "functionality") {
|
|
1827
|
+
if (!("statement" in entry)) {
|
|
1828
|
+
return Object.freeze({ kind: "functionality", statement: undefined, canonical, facts })
|
|
1829
|
+
}
|
|
1830
|
+
return Object.freeze({ kind: "functionality", statement: entry.statement, canonical, facts })
|
|
1831
|
+
}
|
|
1832
|
+
if (entry.kind === "capacity") {
|
|
1833
|
+
if (wire.kind !== "capacity") {
|
|
1834
|
+
throw errors.new(`bumbledb violation ${wire.statementId} is a capacity slot without a measure`)
|
|
1835
|
+
}
|
|
1836
|
+
return Object.freeze({
|
|
1837
|
+
kind: "capacity",
|
|
1838
|
+
statement: entry.statement,
|
|
1839
|
+
canonical,
|
|
1840
|
+
measure: wire.measure,
|
|
1841
|
+
facts
|
|
1842
|
+
})
|
|
1843
|
+
}
|
|
1844
|
+
if (wire.kind !== "containment") {
|
|
1845
|
+
throw errors.new(`bumbledb violation ${wire.statementId} is a containment slot without a direction`)
|
|
1846
|
+
}
|
|
1847
|
+
if (entry.kind === "mirrors") {
|
|
1848
|
+
return Object.freeze({
|
|
1849
|
+
kind: "containment",
|
|
1850
|
+
statement: entry.statement,
|
|
1851
|
+
canonical,
|
|
1852
|
+
direction: wire.direction,
|
|
1853
|
+
orientation: entry.orientation,
|
|
1854
|
+
facts
|
|
1855
|
+
})
|
|
1856
|
+
}
|
|
1857
|
+
return Object.freeze({
|
|
1858
|
+
kind: "containment",
|
|
1859
|
+
statement: entry.statement,
|
|
1860
|
+
canonical,
|
|
1861
|
+
direction: wire.direction,
|
|
1862
|
+
facts
|
|
1863
|
+
})
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
async function openStore<Rels extends SchemaRelations>(
|
|
1867
|
+
storePath: string,
|
|
1868
|
+
theory: Schema<Rels>
|
|
1869
|
+
): Promise<Db<Rels>> {
|
|
1870
|
+
const canonical = path.resolve(storePath)
|
|
1871
|
+
const spec = lower(theory)
|
|
1872
|
+
const opened = await bridgedAsync(`open bumbledb store at ${canonical}`, function callBridge() {
|
|
1482
1873
|
return native.dbOpen(canonical, spec)
|
|
1483
1874
|
})
|
|
1484
1875
|
if (!opened.ok) {
|
|
1485
|
-
|
|
1486
|
-
|
|
1876
|
+
throwOpenRefusal("open", canonical, opened.kind, opened.message)
|
|
1877
|
+
}
|
|
1878
|
+
return openFromHandle(opened.db, theory)
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
interface OwnedInstance<Rels extends SchemaRelations> extends Disposable {
|
|
1882
|
+
prepare<Row, Params extends ParamsRecord>(q: Query<Rels, Row, Params>): Prepared<Rels, Row, Params>
|
|
1883
|
+
execute<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Row[]
|
|
1884
|
+
scan<R extends MemberRelation<Rels>>(relation: R): Fact<R>[]
|
|
1885
|
+
contains<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean
|
|
1886
|
+
get<R extends MemberRelation<Rels>>(relation: R, key: KeyFact<R>): Fact<R> | undefined
|
|
1887
|
+
get<R extends MemberRelation<Rels>, const P extends readonly string[]>(
|
|
1888
|
+
relation: R,
|
|
1889
|
+
keyStatement: KeyStatement<R, P>,
|
|
1890
|
+
key: DeclaredKeyFact<R, P>
|
|
1891
|
+
): Fact<R> | undefined
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
interface InstanceBuilder<Rels extends SchemaRelations> extends Disposable {
|
|
1895
|
+
load<R extends MemberRelation<Rels>>(relation: R, facts: CollectionWrite<R>): MutationReport
|
|
1896
|
+
delete<R extends MemberRelation<Rels>>(relation: R, facts: Iterable<Fact<R>>): MutationReport
|
|
1897
|
+
reserve<R extends MemberRelation<Rels>>(relation: R, field: FreshKeys<R> & string, count: bigint): FreshRange
|
|
1898
|
+
contains<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean
|
|
1899
|
+
get<R extends MemberRelation<Rels>>(relation: R, key: KeyFact<R>): Fact<R> | undefined
|
|
1900
|
+
get<R extends MemberRelation<Rels>, const P extends readonly string[]>(
|
|
1901
|
+
relation: R,
|
|
1902
|
+
keyStatement: KeyStatement<R, P>,
|
|
1903
|
+
key: DeclaredKeyFact<R, P>
|
|
1904
|
+
): Fact<R> | undefined
|
|
1905
|
+
admit(): Promise<Admission<Rels, OwnedInstance<Rels>>>
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
const ownedRecords = new WeakMap<object, { handle: OwnedHandle; theory: AnySchema; spent: boolean; owner: object }>()
|
|
1909
|
+
const builderRecords = new WeakMap<object, { handle: BuilderHandle; theory: AnySchema; spent: boolean }>()
|
|
1910
|
+
|
|
1911
|
+
const ownedReclaimer = new FinalizationRegistry<OwnedHandle>(function reclaimOwned(handle) {
|
|
1912
|
+
const closed = errors.trySync(function closeOwned() {
|
|
1913
|
+
native.ownedInstanceClose(handle)
|
|
1914
|
+
})
|
|
1915
|
+
if (closed.error) {
|
|
1916
|
+
return
|
|
1917
|
+
}
|
|
1918
|
+
})
|
|
1919
|
+
|
|
1920
|
+
const builderReclaimer = new FinalizationRegistry<BuilderHandle>(function reclaimBuilder(handle) {
|
|
1921
|
+
const closed = errors.trySync(function closeBuilder() {
|
|
1922
|
+
native.instanceBuilderClose(handle)
|
|
1923
|
+
})
|
|
1924
|
+
if (closed.error) {
|
|
1925
|
+
return
|
|
1926
|
+
}
|
|
1927
|
+
})
|
|
1928
|
+
|
|
1929
|
+
function wrapOwned<Rels extends SchemaRelations>(nativeHandle: OwnedHandle, theory: Schema<Rels>): OwnedInstance<Rels> {
|
|
1930
|
+
const owner = Object.freeze({})
|
|
1931
|
+
const rec = { handle: nativeHandle, theory, spent: false, owner }
|
|
1932
|
+
const tables = tablesFromTheory(theory)
|
|
1933
|
+
function assertLive(): void {
|
|
1934
|
+
if (rec.spent) {
|
|
1935
|
+
throw errors.wrap(ErrSpentHandle, "bumbledb owned instance has been disposed")
|
|
1487
1936
|
}
|
|
1488
|
-
throw errors.new(`bumbledb ${opened.kind} (${verb} ${canonical}): ${opened.message}`)
|
|
1489
1937
|
}
|
|
1490
|
-
const
|
|
1491
|
-
|
|
1938
|
+
const methods = catalogMethods(theory, tables, owner, assertLive, {
|
|
1939
|
+
scan(relationId) {
|
|
1940
|
+
return native.ownedScan(nativeHandle, relationId)
|
|
1941
|
+
},
|
|
1942
|
+
contains(relationId, values) {
|
|
1943
|
+
return native.ownedContains(nativeHandle, relationId, values)
|
|
1944
|
+
},
|
|
1945
|
+
get(relationId, statementId, keyValues) {
|
|
1946
|
+
return native.ownedGet(nativeHandle, relationId, statementId, keyValues)
|
|
1947
|
+
},
|
|
1948
|
+
prepare(query) {
|
|
1949
|
+
return native.ownedPrepare(nativeHandle, query)
|
|
1950
|
+
},
|
|
1951
|
+
execute(prepared, params) {
|
|
1952
|
+
return native.ownedExecute(prepared, nativeHandle, params)
|
|
1953
|
+
}
|
|
1492
1954
|
})
|
|
1493
|
-
|
|
1955
|
+
const instance: OwnedInstance<Rels> = Object.freeze({
|
|
1956
|
+
...methods,
|
|
1957
|
+
[Symbol.dispose](): void {
|
|
1958
|
+
if (rec.spent) {
|
|
1959
|
+
return
|
|
1960
|
+
}
|
|
1961
|
+
try {
|
|
1962
|
+
native.ownedInstanceClose(nativeHandle)
|
|
1963
|
+
} catch (caught) {
|
|
1964
|
+
const error = errorFromThrow(caught)
|
|
1965
|
+
if (/leased for publish/.test(error.message)) {
|
|
1966
|
+
throw errors.wrap(ErrSpentHandle, "bumbledb owned instance is leased for publish")
|
|
1967
|
+
}
|
|
1968
|
+
throw errors.wrap(error, "close bumbledb owned instance")
|
|
1969
|
+
}
|
|
1970
|
+
rec.spent = true
|
|
1971
|
+
ownedReclaimer.unregister(instance)
|
|
1972
|
+
}
|
|
1973
|
+
})
|
|
1974
|
+
ownedRecords.set(instance, rec)
|
|
1975
|
+
ownedReclaimer.register(instance, nativeHandle, instance)
|
|
1976
|
+
return instance
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
function wrapBuilder<Rels extends SchemaRelations>(
|
|
1980
|
+
nativeHandle: BuilderHandle,
|
|
1981
|
+
theory: Schema<Rels>
|
|
1982
|
+
): InstanceBuilder<Rels> {
|
|
1983
|
+
const rec = { handle: nativeHandle, theory, spent: false }
|
|
1984
|
+
const tables = tablesFromTheory(theory)
|
|
1985
|
+
function assertLive(): void {
|
|
1986
|
+
if (rec.spent) {
|
|
1987
|
+
throw errors.wrap(ErrSpentHandle, "bumbledb instance builder has been spent")
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
const overlay = overlayMethods(theory, tables, assertLive, {
|
|
1991
|
+
contains(relationId, row) {
|
|
1992
|
+
return bridged("bumbledb builder contains", function readContains() {
|
|
1993
|
+
return native.instanceBuilderContains(nativeHandle, relationId, row)
|
|
1994
|
+
})
|
|
1995
|
+
},
|
|
1996
|
+
get(relationId, statementId, key) {
|
|
1997
|
+
return bridged("bumbledb builder get", function readGet() {
|
|
1998
|
+
return native.instanceBuilderGet(nativeHandle, relationId, statementId, key)
|
|
1999
|
+
})
|
|
2000
|
+
}
|
|
2001
|
+
})
|
|
2002
|
+
const builder: InstanceBuilder<Rels> = Object.freeze({
|
|
2003
|
+
load<R extends MemberRelation<Rels>>(relation: R, facts: CollectionWrite<R>): MutationReport {
|
|
2004
|
+
assertLive()
|
|
2005
|
+
const entry = ordinaryEntry(tables, theory, relation)
|
|
2006
|
+
return mutateCollection(
|
|
2007
|
+
relation,
|
|
2008
|
+
facts,
|
|
2009
|
+
function applyRows(rows) {
|
|
2010
|
+
return bridged("bumbledb builder load", function loadRows() {
|
|
2011
|
+
return native.instanceBuilderLoad(nativeHandle, entry.id, rows)
|
|
2012
|
+
})
|
|
2013
|
+
},
|
|
2014
|
+
function applyColumns(columns) {
|
|
2015
|
+
return bridged("bumbledb builder load", function loadColumns() {
|
|
2016
|
+
return native.instanceBuilderLoadColumns(nativeHandle, entry.id, columns)
|
|
2017
|
+
})
|
|
2018
|
+
}
|
|
2019
|
+
)
|
|
2020
|
+
},
|
|
2021
|
+
delete<R extends MemberRelation<Rels>>(relation: R, facts: Iterable<Fact<R>>): MutationReport {
|
|
2022
|
+
assertLive()
|
|
2023
|
+
const entry = ordinaryEntry(tables, theory, relation)
|
|
2024
|
+
const report = bridged("bumbledb builder delete", function remove() {
|
|
2025
|
+
return native.instanceBuilderDelete(nativeHandle, entry.id, rowsOf(relation, facts))
|
|
2026
|
+
})
|
|
2027
|
+
return Object.freeze({ submitted: report.submitted, changed: report.changed })
|
|
2028
|
+
},
|
|
2029
|
+
reserve<R extends MemberRelation<Rels>>(
|
|
2030
|
+
relation: R,
|
|
2031
|
+
field: FreshKeys<R> & string,
|
|
2032
|
+
count: bigint
|
|
2033
|
+
): FreshRange {
|
|
2034
|
+
assertLive()
|
|
2035
|
+
const entry = ordinaryEntry(tables, theory, relation)
|
|
2036
|
+
const declared = relation.data.fields.find(function byName(candidate) {
|
|
2037
|
+
return candidate.name === field
|
|
2038
|
+
})
|
|
2039
|
+
if (declared === undefined || !isFreshField(declared.field)) {
|
|
2040
|
+
throw errors.new(`relation ${relation.name}: field ${field} is not a fresh cell`)
|
|
2041
|
+
}
|
|
2042
|
+
const fieldId = entry.fieldIds.get(field)
|
|
2043
|
+
if (fieldId === undefined) {
|
|
2044
|
+
throw errors.new(`bumbledb manifest drift: relation ${relation.name} has no field id for ${field}`)
|
|
2045
|
+
}
|
|
2046
|
+
const range = bridged("bumbledb builder reserve", function mint() {
|
|
2047
|
+
return native.instanceBuilderReserve(nativeHandle, entry.id, fieldId, count)
|
|
2048
|
+
})
|
|
2049
|
+
return freshRangeOf(range)
|
|
2050
|
+
},
|
|
2051
|
+
contains: overlay.contains,
|
|
2052
|
+
get: overlay.get,
|
|
2053
|
+
async admit(): Promise<Admission<Rels, OwnedInstance<Rels>>> {
|
|
2054
|
+
if (rec.spent) {
|
|
2055
|
+
throw errors.wrap(ErrSpentHandle, "bumbledb instance builder has been spent")
|
|
2056
|
+
}
|
|
2057
|
+
rec.spent = true
|
|
2058
|
+
builderReclaimer.unregister(builder)
|
|
2059
|
+
let outcome: AdmitResult
|
|
2060
|
+
try {
|
|
2061
|
+
outcome = await native.instanceBuilderAdmit(nativeHandle)
|
|
2062
|
+
} catch (caught) {
|
|
2063
|
+
throw errors.wrap(errorFromThrow(caught), "admit bumbledb instance")
|
|
2064
|
+
}
|
|
2065
|
+
if (outcome.tag === "rejected") {
|
|
2066
|
+
return Object.freeze({
|
|
2067
|
+
tag: "rejected" as const,
|
|
2068
|
+
violations: Object.freeze(
|
|
2069
|
+
outcome.violations.map(function mapWire(wire) {
|
|
2070
|
+
return mapViolationWithoutStore<Rels>(theory, wire)
|
|
2071
|
+
})
|
|
2072
|
+
)
|
|
2073
|
+
})
|
|
2074
|
+
}
|
|
2075
|
+
return Object.freeze({
|
|
2076
|
+
tag: "accepted" as const,
|
|
2077
|
+
value: wrapOwned(outcome.value, theory)
|
|
2078
|
+
})
|
|
2079
|
+
},
|
|
2080
|
+
[Symbol.dispose](): void {
|
|
2081
|
+
if (rec.spent) {
|
|
2082
|
+
return
|
|
2083
|
+
}
|
|
2084
|
+
rec.spent = true
|
|
2085
|
+
builderReclaimer.unregister(builder)
|
|
2086
|
+
bridged("close bumbledb instance builder", function closeBuilder() {
|
|
2087
|
+
native.instanceBuilderClose(nativeHandle)
|
|
2088
|
+
})
|
|
2089
|
+
}
|
|
2090
|
+
})
|
|
2091
|
+
builderRecords.set(builder, rec)
|
|
2092
|
+
builderReclaimer.register(builder, nativeHandle, builder)
|
|
2093
|
+
return builder
|
|
1494
2094
|
}
|
|
1495
2095
|
|
|
2096
|
+
const InstanceBuilder = Object.freeze({
|
|
2097
|
+
create<Rels extends SchemaRelations>(theory: Schema<Rels>): InstanceBuilder<Rels> {
|
|
2098
|
+
const spec = lower(theory)
|
|
2099
|
+
const handle = bridged("create bumbledb instance builder", function make() {
|
|
2100
|
+
return native.instanceBuilderNew(spec)
|
|
2101
|
+
})
|
|
2102
|
+
return wrapBuilder(handle, theory)
|
|
2103
|
+
}
|
|
2104
|
+
})
|
|
2105
|
+
|
|
1496
2106
|
/**
|
|
1497
2107
|
* The store lifecycle — `Db.create(path, schema)` / `Db.open(path, schema)`.
|
|
1498
2108
|
* Create refuses an already-initialized directory; open verifies format
|
|
1499
|
-
* version
|
|
1500
|
-
*
|
|
1501
|
-
*
|
|
1502
|
-
*
|
|
1503
|
-
*
|
|
2109
|
+
* version and the schema fingerprint. A second live handle on the same
|
|
2110
|
+
* path is the engine's `EnvironmentLocked`. There is no close anywhere:
|
|
2111
|
+
* the process owns the environment until GC/exit (durability is the
|
|
2112
|
+
* engine's per-commit fsync). Resume = reopen in a fresh process, or
|
|
2113
|
+
* hold the `Db` this process opened.
|
|
1504
2114
|
*/
|
|
1505
2115
|
const Db = Object.freeze({
|
|
1506
2116
|
/** Creates a fresh durable store at `path` from the schema. */
|
|
1507
|
-
async create<Rels extends SchemaRelations>(
|
|
1508
|
-
|
|
2117
|
+
async create<Rels extends SchemaRelations>(
|
|
2118
|
+
storePath: string,
|
|
2119
|
+
theory: Schema<Rels>
|
|
2120
|
+
): Promise<Admission<Rels, Db<Rels>>> {
|
|
2121
|
+
return createStore(storePath, theory)
|
|
1509
2122
|
},
|
|
1510
2123
|
/**
|
|
1511
2124
|
* Opens an existing durable store at `path` with the same theory.
|
|
1512
|
-
*
|
|
1513
|
-
*
|
|
1514
|
-
* 50-storage.md § the `_meta` block), so a legacy store becomes
|
|
1515
|
-
* exhumable after one ordinary open — adoption is automatic, never a
|
|
1516
|
-
* separate verb. A second open of a still-live path is
|
|
1517
|
-
* `EnvironmentLocked`.
|
|
2125
|
+
* Format 8 open never back-fills a descriptor. A second open of a
|
|
2126
|
+
* still-live path is `EnvironmentLocked`.
|
|
1518
2127
|
*/
|
|
1519
|
-
async open<Rels extends SchemaRelations>(
|
|
1520
|
-
return
|
|
2128
|
+
async open<Rels extends SchemaRelations>(storePath: string, theory: Schema<Rels>): Promise<Db<Rels>> {
|
|
2129
|
+
return openStore(storePath, theory)
|
|
1521
2130
|
},
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
2131
|
+
async fromInstance<Rels extends SchemaRelations>(
|
|
2132
|
+
storePath: string,
|
|
2133
|
+
instance: OwnedInstance<Rels>
|
|
2134
|
+
): Promise<Db<Rels>> {
|
|
2135
|
+
const rec = ownedRecords.get(instance)
|
|
2136
|
+
if (rec === undefined) {
|
|
2137
|
+
throw errors.wrap(ErrSpentHandle, "bumbledb fromInstance target is not an owned instance of this SDK")
|
|
2138
|
+
}
|
|
2139
|
+
if (rec.spent) {
|
|
2140
|
+
throw errors.wrap(ErrSpentHandle, "bumbledb fromInstance target has been disposed")
|
|
2141
|
+
}
|
|
2142
|
+
const canonical = path.resolve(storePath)
|
|
2143
|
+
const dbHandle = await bridgedAsync(`publish bumbledb instance at ${canonical}`, function publish() {
|
|
2144
|
+
return native.dbFromInstance(canonical, rec.handle)
|
|
2145
|
+
})
|
|
2146
|
+
return openFromHandle(dbHandle, rec.theory as Schema<Rels>)
|
|
1536
2147
|
}
|
|
1537
2148
|
})
|
|
1538
2149
|
|
|
1539
2150
|
export type {
|
|
1540
2151
|
Abandon,
|
|
1541
2152
|
AbandonedArm,
|
|
2153
|
+
Admission,
|
|
1542
2154
|
CapacityViolation,
|
|
2155
|
+
ColumnBatch,
|
|
2156
|
+
Committed,
|
|
1543
2157
|
ContainmentViolation,
|
|
1544
2158
|
DeclaredKeyFact,
|
|
1545
2159
|
DeclaredKeyViolation,
|
|
@@ -1550,10 +2164,28 @@ export type {
|
|
|
1550
2164
|
MirrorViolation,
|
|
1551
2165
|
MutationReport,
|
|
1552
2166
|
OffendingFact,
|
|
2167
|
+
OwnedInstance,
|
|
1553
2168
|
Prepared,
|
|
1554
|
-
|
|
2169
|
+
ReadInstance,
|
|
2170
|
+
SyncResult,
|
|
1555
2171
|
Tx,
|
|
1556
2172
|
Violation,
|
|
1557
|
-
|
|
2173
|
+
Witness,
|
|
2174
|
+
WriteFromOutcome,
|
|
2175
|
+
WriteOutcome,
|
|
2176
|
+
WriteTx
|
|
2177
|
+
}
|
|
2178
|
+
export {
|
|
2179
|
+
abandon,
|
|
2180
|
+
Db,
|
|
2181
|
+
ErrAsyncCallback,
|
|
2182
|
+
ErrForeignPrepared,
|
|
2183
|
+
ErrForeignWitness,
|
|
2184
|
+
ErrFingerprintMismatch,
|
|
2185
|
+
ErrIrError,
|
|
2186
|
+
ErrNewtypeMismatch,
|
|
2187
|
+
ErrSchemaError,
|
|
2188
|
+
ErrSpentHandle,
|
|
2189
|
+
ErrUseAfterScope,
|
|
2190
|
+
InstanceBuilder
|
|
1558
2191
|
}
|
|
1559
|
-
export { abandon, Db, ErrGenerationMoved, ErrNewtypeMismatch }
|