@bjornpagen/bumbledb 0.20.2 → 1.0.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.
Files changed (192) hide show
  1. package/COOKBOOK.md +283 -1611
  2. package/README.md +156 -118
  3. package/dist/capacity.d.ts +16 -12
  4. package/dist/capacity.d.ts.map +1 -1
  5. package/dist/capacity.js +16 -48
  6. package/dist/capacity.js.map +1 -1
  7. package/dist/changes.d.ts +54 -0
  8. package/dist/changes.d.ts.map +1 -0
  9. package/dist/changes.js +245 -0
  10. package/dist/changes.js.map +1 -0
  11. package/dist/close.d.ts +24 -0
  12. package/dist/close.d.ts.map +1 -0
  13. package/dist/close.js +34 -0
  14. package/dist/close.js.map +1 -0
  15. package/dist/closed.d.ts +1 -3
  16. package/dist/closed.d.ts.map +1 -1
  17. package/dist/closed.js +24 -12
  18. package/dist/closed.js.map +1 -1
  19. package/dist/codec.d.ts +56 -0
  20. package/dist/codec.d.ts.map +1 -0
  21. package/dist/codec.js +289 -0
  22. package/dist/codec.js.map +1 -0
  23. package/dist/compile.d.ts +68 -0
  24. package/dist/compile.d.ts.map +1 -0
  25. package/dist/compile.js +75 -0
  26. package/dist/compile.js.map +1 -0
  27. package/dist/db-native.d.ts +170 -0
  28. package/dist/db-native.d.ts.map +1 -0
  29. package/dist/db-native.js +5 -0
  30. package/dist/db-native.js.map +1 -0
  31. package/dist/db.d.ts +101 -233
  32. package/dist/db.d.ts.map +1 -1
  33. package/dist/db.js +232 -1084
  34. package/dist/db.js.map +1 -1
  35. package/dist/errors.d.ts +57 -0
  36. package/dist/errors.d.ts.map +1 -0
  37. package/dist/errors.js +32 -0
  38. package/dist/errors.js.map +1 -0
  39. package/dist/face.d.ts.map +1 -1
  40. package/dist/face.js +2 -2
  41. package/dist/face.js.map +1 -1
  42. package/dist/fields.d.ts +65 -24
  43. package/dist/fields.d.ts.map +1 -1
  44. package/dist/fields.js +94 -24
  45. package/dist/fields.js.map +1 -1
  46. package/dist/index.d.ts +37 -32
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +17 -21
  49. package/dist/index.js.map +1 -1
  50. package/dist/internal/log.d.ts +24 -0
  51. package/dist/internal/log.d.ts.map +1 -0
  52. package/dist/internal/log.js +10 -0
  53. package/dist/internal/log.js.map +1 -0
  54. package/dist/law.d.ts +8 -13
  55. package/dist/law.d.ts.map +1 -1
  56. package/dist/law.js +6 -59
  57. package/dist/law.js.map +1 -1
  58. package/dist/lower.d.ts +0 -13
  59. package/dist/lower.d.ts.map +1 -1
  60. package/dist/lower.js +9 -17
  61. package/dist/lower.js.map +1 -1
  62. package/dist/migration.d.ts +33 -0
  63. package/dist/migration.d.ts.map +1 -0
  64. package/dist/migration.js +48 -0
  65. package/dist/migration.js.map +1 -0
  66. package/dist/native.d.ts +256 -301
  67. package/dist/native.d.ts.map +1 -1
  68. package/dist/native.js +65 -108
  69. package/dist/native.js.map +1 -1
  70. package/dist/query/atom.d.ts +24 -13
  71. package/dist/query/atom.d.ts.map +1 -1
  72. package/dist/query/atom.js +7 -3
  73. package/dist/query/atom.js.map +1 -1
  74. package/dist/query/compute.d.ts +68 -0
  75. package/dist/query/compute.d.ts.map +1 -0
  76. package/dist/query/compute.js +141 -0
  77. package/dist/query/compute.js.map +1 -0
  78. package/dist/query/find.d.ts +15 -8
  79. package/dist/query/find.d.ts.map +1 -1
  80. package/dist/query/find.js +6 -2
  81. package/dist/query/find.js.map +1 -1
  82. package/dist/query/lower.d.ts +23 -2
  83. package/dist/query/lower.d.ts.map +1 -1
  84. package/dist/query/lower.js +433 -80
  85. package/dist/query/lower.js.map +1 -1
  86. package/dist/query/parse-ir.d.ts.map +1 -1
  87. package/dist/query/parse-ir.js +17 -8
  88. package/dist/query/parse-ir.js.map +1 -1
  89. package/dist/query/run.d.ts +12 -2
  90. package/dist/query/run.d.ts.map +1 -1
  91. package/dist/query/run.js +26 -9
  92. package/dist/query/run.js.map +1 -1
  93. package/dist/query/scope.d.ts +47 -3
  94. package/dist/query/scope.d.ts.map +1 -1
  95. package/dist/query/scope.js +83 -19
  96. package/dist/query/scope.js.map +1 -1
  97. package/dist/relation.d.ts +3 -8
  98. package/dist/relation.d.ts.map +1 -1
  99. package/dist/relation.js +16 -10
  100. package/dist/relation.js.map +1 -1
  101. package/dist/result.d.ts +47 -0
  102. package/dist/result.d.ts.map +1 -0
  103. package/dist/result.js +50 -0
  104. package/dist/result.js.map +1 -0
  105. package/dist/rows.d.ts +69 -0
  106. package/dist/rows.d.ts.map +1 -0
  107. package/dist/rows.js +328 -0
  108. package/dist/rows.js.map +1 -0
  109. package/dist/runtime-codes.d.ts +2 -0
  110. package/dist/runtime-codes.d.ts.map +1 -0
  111. package/dist/runtime-codes.js +19 -0
  112. package/dist/runtime-codes.js.map +1 -0
  113. package/dist/runtime-errors.d.ts +106 -0
  114. package/dist/runtime-errors.d.ts.map +1 -0
  115. package/dist/runtime-errors.js +62 -0
  116. package/dist/runtime-errors.js.map +1 -0
  117. package/dist/runtime-native.d.ts +171 -0
  118. package/dist/runtime-native.d.ts.map +1 -0
  119. package/dist/runtime-native.js +5 -0
  120. package/dist/runtime-native.js.map +1 -0
  121. package/dist/runtime.d.ts +73 -0
  122. package/dist/runtime.d.ts.map +1 -0
  123. package/dist/runtime.js +293 -0
  124. package/dist/runtime.js.map +1 -0
  125. package/dist/scalar.d.ts +165 -0
  126. package/dist/scalar.d.ts.map +1 -0
  127. package/dist/scalar.js +344 -0
  128. package/dist/scalar.js.map +1 -0
  129. package/dist/schema.d.ts +1 -1
  130. package/dist/schema.d.ts.map +1 -1
  131. package/dist/schema.js +34 -37
  132. package/dist/schema.js.map +1 -1
  133. package/dist/shape.d.ts +28 -0
  134. package/dist/shape.d.ts.map +1 -0
  135. package/dist/shape.js +2 -0
  136. package/dist/shape.js.map +1 -0
  137. package/dist/spec.d.ts +27 -3
  138. package/dist/spec.d.ts.map +1 -1
  139. package/dist/spec.js +22 -1
  140. package/dist/spec.js.map +1 -1
  141. package/dist/statements.d.ts +16 -3
  142. package/dist/statements.d.ts.map +1 -1
  143. package/dist/statements.js +54 -28
  144. package/dist/statements.js.map +1 -1
  145. package/dist/uuid.d.ts +23 -0
  146. package/dist/uuid.d.ts.map +1 -0
  147. package/dist/uuid.js +54 -0
  148. package/dist/uuid.js.map +1 -0
  149. package/pack-provenance.json +7 -0
  150. package/package.json +12 -16
  151. package/src/capacity.ts +34 -54
  152. package/src/changes.ts +361 -0
  153. package/src/close.ts +53 -0
  154. package/src/closed.ts +25 -19
  155. package/src/codec.ts +379 -0
  156. package/src/compile.ts +149 -0
  157. package/src/db-native.ts +247 -0
  158. package/src/db.ts +461 -1632
  159. package/src/errors.ts +50 -0
  160. package/src/face.ts +2 -2
  161. package/src/fields.ts +182 -55
  162. package/src/index.ts +62 -126
  163. package/src/internal/log.ts +51 -0
  164. package/src/law.ts +13 -28
  165. package/src/lower.ts +9 -4
  166. package/src/migration.ts +69 -0
  167. package/src/native.ts +326 -487
  168. package/src/query/atom.ts +61 -25
  169. package/src/query/compute.ts +237 -0
  170. package/src/query/find.ts +31 -12
  171. package/src/query/lower.ts +531 -143
  172. package/src/query/parse-ir.ts +18 -13
  173. package/src/query/run.ts +32 -16
  174. package/src/query/scope.ts +140 -8
  175. package/src/relation.ts +17 -21
  176. package/src/result.ts +130 -0
  177. package/src/rows.ts +404 -0
  178. package/src/runtime-codes.ts +18 -0
  179. package/src/runtime-errors.ts +69 -0
  180. package/src/runtime-native.ts +242 -0
  181. package/src/runtime.ts +440 -0
  182. package/src/scalar.ts +560 -0
  183. package/src/schema.ts +35 -37
  184. package/src/shape.ts +31 -0
  185. package/src/spec.ts +33 -3
  186. package/src/statements.ts +75 -68
  187. package/src/uuid.ts +69 -0
  188. package/dist/marshal.d.ts +0 -44
  189. package/dist/marshal.d.ts.map +0 -1
  190. package/dist/marshal.js +0 -165
  191. package/dist/marshal.js.map +0 -1
  192. package/src/marshal.ts +0 -200
package/src/db.ts CHANGED
@@ -1,1720 +1,549 @@
1
- /**
2
- * `Db` the living half of the SDK (PRD-07): open/create a store from a
3
- * `Schema`, write typed facts through delta transactions with race-free
4
- * final-state point reads, receive rejections as typed violation VALUES
5
- * keyed to statements, and read through a synchronous instance callback —
6
- * all typed by the schema's relations record.
7
- *
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.
15
- *
16
- * PROCESS MODEL: one process, one exclusive-lock handle per store. The
17
- * `Db` value owns the LMDB environment's exclusive lock until process
18
- * exit (or until GC reclaims the native handle); a second engine-level
19
- * open of the same store is refused by the engine (`EnvironmentLocked`),
20
- * matching Rust. Resume = reopen in a fresh process, or hold the one `Db`
21
- * this process already opened. The host owns composition; retry is host
22
- * policy.
23
- *
24
- * REJECTION IS DATA: a rejected commit is a domain outcome (it becomes the
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.
30
- */
31
-
32
- import * as path from "node:path"
33
- import * as errors from "@superbuilders/errors"
34
- import { regex } from "arkregex"
35
- import { isClosedMember, sealedFieldsOf } from "#closed.ts"
36
- import { rosterOf } from "#fields.ts"
37
- import { lower } from "#lower.ts"
38
- import { cellOf, factOf, handleOf, isFreshField, type KeyFact, keyRowOf, recordOf, rowOf } from "#marshal.ts"
39
-
1
+ import type { Scope } from "effect"
2
+ import { Effect, Option } from "effect"
3
+ import type { ChangeSet } from "#changes.ts"
4
+ import { internalChanges } from "#changes.ts"
5
+ import { drainClose, releaseOwner } from "#close.ts"
6
+ import type { CompiledSchema, SchemaId } from "#compile.ts"
7
+ import { Schema as CoreSchema, schemaTables } from "#compile.ts"
40
8
  import type {
41
- AdmitResult,
42
- BuilderHandle,
43
- DbHandle,
44
- FactValue,
45
- InstanceHandle,
46
- Manifest,
47
- NativeWriteOutcome,
48
- OwnedHandle,
49
- PreparedHandle,
50
- TxHandle,
51
- WireFreshRange,
52
- WireMutationReport,
53
- Violation as WireViolation,
54
- ViolationFact as WireViolationFact,
55
- WitnessHandle
56
- } from "#native.ts"
57
- import { bridged, bridgedAsync, errorFromThrow, native } from "#native.ts"
58
- import type { FindColumn } from "#query/atom.ts"
59
- import type { Query } from "#query/lower.ts"
9
+ ApplyOutcomeWire,
10
+ DbInspectionWire,
11
+ ExpectedWire,
12
+ SessionHandle,
13
+ SnapshotHandle,
14
+ WitnessWire
15
+ } from "#db-native.ts"
16
+ import { dbNative } from "#db-native.ts"
17
+ import { lower } from "#lower.ts"
18
+ import type { DbHandle, Violation } from "#native.ts"
19
+ import type { AnyQuery } from "#query/lower.ts"
60
20
  import { lowerQuery } from "#query/lower.ts"
61
- import { decodeAnswers, wireParams } from "#query/run.ts"
62
- import type { ParamEntry, ParamsRecord } from "#query/scope.ts"
63
- import type { AnyRelation, Fact, FreshKeys } from "#relation.ts"
64
- import type { AnySchema, Schema, SchemaRelation, SchemaRelations } from "#schema.ts"
65
- import { isStatement, type KeyStatement, type Statement } from "#statements.ts"
66
-
67
- const LEASED_FOR_PUBLISH = regex("leased for publish")
68
-
69
- type MemberRelation<Rels extends SchemaRelations> = Extract<Rels[keyof Rels], AnyRelation>
70
-
71
- interface MutationReport {
72
- readonly submitted: bigint
73
- readonly changed: bigint
74
- }
75
-
76
- type CollectionWrite<R extends AnyRelation> = Iterable<Fact<R>>
77
-
78
- interface FlatCollection {
79
- readonly rows: bigint
80
- readonly cells: readonly FactValue[]
81
- }
82
-
83
- /**
84
- * The flat projector: every fact's cells land in ONE row-major
85
- * `FactValue` array (length rows×arity) — no JS array per fact exists
86
- * anywhere between the caller's objects and the native crossing — and
87
- * the row count is counted
88
- * while projecting (the {@link FlatCollection} law: the stated count is
89
- * what the bridge verifies against `rows × arity`, exactly, for every
90
- * arity). The per-cell judgment is `cellOf` — the one cell judge `rowOf`
91
- * also speaks (closed handle→id, well-formedness, interval shape) — and
92
- * the missing-field refusal is `rowOf`'s, byte for byte; only the output
93
- * form differs (flat, never per-row).
94
- */
95
- function rowsOf<R extends AnyRelation>(relation: R, facts: Iterable<Fact<R>>): FlatCollection {
96
- const data = relation.data
97
- const cells: FactValue[] = []
98
- let rows = 0n
99
- for (const fact of facts) {
100
- rows += 1n
101
- const record = recordOf(fact)
102
- for (const declared of data.fields) {
103
- const value = record[declared.name]
104
- if (value === undefined) {
105
- throw errors.new(`relation ${data.name}: fact is missing field ${declared.name}`)
106
- }
107
- cells.push(cellOf(`relation ${data.name} field ${declared.name}`, declared.field, value))
108
- }
109
- }
110
- return { rows, cells }
111
- }
112
-
113
- function mutateCollection<R extends AnyRelation>(
114
- relation: R,
115
- facts: CollectionWrite<R>,
116
- apply: (rows: bigint, cells: readonly FactValue[]) => WireMutationReport
117
- ): MutationReport {
118
- const flat = rowsOf(relation, facts)
119
- const report = apply(flat.rows, flat.cells)
120
- return Object.freeze({ submitted: report.submitted, changed: report.changed })
121
- }
122
-
123
- type FreshRange =
124
- | {
125
- readonly empty: true
126
- readonly count: 0n
127
- at(index: bigint): undefined
128
- [Symbol.iterator](): IterableIterator<bigint>
129
- }
130
- | {
131
- readonly empty: false
132
- readonly start: bigint
133
- readonly endExclusive: bigint
134
- readonly count: bigint
135
- at(index: bigint): bigint | undefined
136
- [Symbol.iterator](): IterableIterator<bigint>
137
- }
138
-
139
- function freshRangeOf(wire: WireFreshRange): FreshRange {
140
- if (wire.empty) {
141
- return Object.freeze({
142
- empty: true,
143
- count: 0n,
144
- at(_index: bigint) {
145
- return undefined
146
- },
147
- *[Symbol.iterator](): IterableIterator<bigint> {}
148
- })
149
- }
150
- const start = wire.start
151
- const endExclusive = wire.endExclusive
152
- const count = endExclusive - start
153
- return Object.freeze({
154
- empty: false,
155
- start,
156
- endExclusive,
157
- get count() {
158
- return count
159
- },
160
- at(index: bigint) {
161
- if (index < 0n || index >= count) {
162
- return undefined
163
- }
164
- return start + index
165
- },
166
- *[Symbol.iterator](): IterableIterator<bigint> {
167
- for (let id = start; id < endExclusive; id++) {
168
- yield id
169
- }
170
- }
171
- })
172
- }
173
-
174
- type DeclaredKeyFact<R extends AnyRelation, Projection extends readonly string[]> = {
175
- readonly [K in Projection[number] & keyof Fact<R>]: Fact<R>[K]
176
- }
177
-
178
- interface OffendingFact<Rels extends SchemaRelations> {
179
- readonly relation: keyof Rels & string
180
- readonly fact: Readonly<Record<string, FactValue>>
181
- }
182
-
183
- type ViolationBody<Rels extends SchemaRelations> = {
184
- readonly canonical: string
185
- readonly facts: readonly OffendingFact<Rels>[]
186
- }
187
-
188
- type ImpliedKeyViolation<Rels extends SchemaRelations> = ViolationBody<Rels> & {
189
- readonly kind: "functionality"
190
- readonly statement: undefined
191
- }
192
-
193
- type DeclaredKeyViolation<Rels extends SchemaRelations> = ViolationBody<Rels> & {
194
- readonly kind: "functionality"
195
- readonly statement: Statement
196
- }
197
-
198
- type ContainmentViolation<Rels extends SchemaRelations> = ViolationBody<Rels> & {
199
- readonly kind: "containment"
200
- readonly statement: Statement
201
- readonly direction: "sourceUnsatisfied" | "targetRequired"
202
- }
203
-
204
- type MirrorViolation<Rels extends SchemaRelations> = ViolationBody<Rels> & {
205
- readonly kind: "containment"
206
- readonly statement: Statement
207
- readonly direction: "sourceUnsatisfied" | "targetRequired"
208
- readonly orientation: "written" | "mirrored"
209
- }
210
-
211
- type CapacityViolation<Rels extends SchemaRelations> = ViolationBody<Rels> & {
212
- readonly kind: "capacity"
213
- readonly statement: Statement
214
- readonly measure: bigint
215
- }
216
-
217
- type Violation<Rels extends SchemaRelations> =
218
- | ImpliedKeyViolation<Rels>
219
- | DeclaredKeyViolation<Rels>
220
- | ContainmentViolation<Rels>
221
- | MirrorViolation<Rels>
222
- | CapacityViolation<Rels>
21
+ import { wireParams } from "#query/run.ts"
22
+ import type { ParamsRecord } from "#query/scope.ts"
23
+ import type { Fact } from "#relation.ts"
24
+ import type { CompleteResult } from "#result.ts"
25
+ import { internalResult, makeCompleteResult } from "#result.ts"
26
+ import type { CellValue } from "#rows.ts"
27
+ import { factOfCells, keyCellsOf } from "#rows.ts"
28
+ import type { ExecutionPolicy, NativeRuntime } from "#runtime.ts"
29
+ import { nativeOperationWith, policyWire, runtimeHandle } from "#runtime.ts"
30
+ import type { CloseReport } from "#runtime-errors.ts"
31
+ import { DbError } from "#runtime-errors.ts"
32
+ import type { DirectoryHandle } from "#runtime-native.ts"
33
+ import { runtimeNative } from "#runtime-native.ts"
34
+ import type { AnySchema } from "#schema.ts"
35
+ import type { Key, QueryTemplate, Rel } from "#shape.ts"
223
36
 
224
37
  /**
225
- * The abandoned arm of a write result (ruled 2026-07-23, R10): present in
226
- * the type EXACTLY when the callback can abandon — the conditional
227
- * distributes over `R`, so a callback with no `Abandon` arm contributes
228
- * `never` and the arm vanishes from the sum. The outcome is in the type;
229
- * a dead arm is never handled.
38
+ * The chapter 35 core surface: `Db.create`/`Db.open`, scoped coherent
39
+ * `Snapshot`s, reusable `ExecutionSession`s, the shared `QueryReader`
40
+ * capability, one immutable final-state `apply`, bounded `inspect` and
41
+ * honest `close`. Effect-only: every method constructs a lazy effect; all
42
+ * native work runs on the ONE bounded runtime executor under an explicit
43
+ * `ExecutionPolicy`; resources are scoped with `CloseFailure`-defect
44
+ * finalizers. There is no Promise/sync/disposal twin, no transaction
45
+ * callback, no per-row fiber and no Proxy row anywhere.
46
+ *
47
+ * Database open is the managed two-step handshake: acquire the kernel-held
48
+ * directory owner and register its finalizer before the interruptible
49
+ * child-open step. `Db.open` NEVER creates on missing/error; `Db.create`
50
+ * refuses existing authority. Published log snapshots use this same
51
+ * `QueryReader` literally — no second reader or adapter.
230
52
  */
231
- type AbandonedArm<R> = R extends Abandon<infer P> ? { readonly tag: "abandoned"; readonly abandoned: P } : never
232
53
 
233
- type SyncResult<R> = R extends PromiseLike<unknown> ? never : R
234
-
235
- interface Committed<T> {
236
- readonly value: T
54
+ /** The core-local witness: catalog/store identity plus generation — never a log StateStamp. */
55
+ interface CoreWitness {
56
+ readonly store: string
237
57
  readonly generation: bigint
238
58
  }
239
59
 
240
- type Admission<Rels extends SchemaRelations, T> =
241
- | { readonly tag: "accepted"; readonly value: T }
242
- | { readonly tag: "rejected"; readonly violations: readonly Violation<Rels>[] }
243
-
244
- type WriteOutcome<Rels extends SchemaRelations, R> =
245
- | { readonly tag: "accepted"; readonly value: Committed<Exclude<R, Abandon<unknown>>> }
246
- | { readonly tag: "rejected"; readonly violations: readonly Violation<Rels>[] }
247
- | AbandonedArm<R>
248
-
249
- type WriteFromOutcome<Rels extends SchemaRelations, R> =
250
- | WriteOutcome<Rels, R>
251
- | { readonly tag: "moved"; readonly witnessed: bigint; readonly current: bigint }
252
-
253
- type DeltaBuild<Rels extends SchemaRelations, R = void> = (tx: WriteTx<Rels>) => R
254
-
255
- const abandonMark: unique symbol = Symbol("bumbledb.abandon")
256
-
257
- /**
258
- * The abandon sentinel {@link abandon} builds: returning one from a `write`
259
- * or `writeFrom` callback rolls the transaction back WITHOUT
260
- * committing (no empty commit is ever issued) and surfaces the payload as
261
- * `{ tag: "abandoned", abandoned: payload }` (ruled 2026-07-23, R10 — the
262
- * sentinel's contract is unconditional, whichever write verb received it).
263
- */
264
- interface Abandon<P> {
265
- readonly [abandonMark]: true
266
- readonly payload: P
267
- }
268
-
269
- function abandon<P>(payload: P): Abandon<P> {
270
- return Object.freeze({ [abandonMark]: true as const, payload })
271
- }
272
-
273
- type AbandonedPayload<R> = R extends Abandon<infer P> ? P : never
274
-
275
- function isAbandon<R>(value: R): value is R & Abandon<AbandonedPayload<R>> {
276
- return typeof value === "object" && value !== null && abandonMark in value
277
- }
60
+ type ApplyExpected = { readonly kind: "any" } | { readonly kind: "exact"; readonly at: CoreWitness }
278
61
 
279
- function isAbandonedOutcome<Rels extends SchemaRelations, R>(
280
- outcome: { readonly tag: "abandoned"; readonly abandoned: AbandonedPayload<R> },
281
- sentinel: Abandon<AbandonedPayload<R>>
282
- ): outcome is { readonly tag: "abandoned"; readonly abandoned: AbandonedPayload<R> } & WriteOutcome<Rels, R> {
283
- return isAbandon(sentinel) && outcome.abandoned === sentinel.payload
284
- }
285
-
286
- function abandonedOutcome<Rels extends SchemaRelations, R>(
287
- sentinel: Abandon<AbandonedPayload<R>>
288
- ): WriteOutcome<Rels, R> {
289
- const outcome = Object.freeze({ tag: "abandoned" as const, abandoned: sentinel.payload })
290
- if (!isAbandonedOutcome<Rels, R>(outcome, sentinel)) {
291
- throw errors.new("bumbledb abandon outcome construction incomplete")
292
- }
293
- return outcome
294
- }
295
-
296
- interface WriteTx<Rels extends SchemaRelations> {
297
- insert<R extends MemberRelation<Rels>>(relation: R, facts: CollectionWrite<R>): MutationReport
298
-
299
- delete<R extends MemberRelation<Rels>>(relation: R, facts: Iterable<Fact<R>>): MutationReport
300
-
301
- reserve<R extends MemberRelation<Rels>>(relation: R, field: FreshKeys<R> & string, count: bigint): FreshRange
302
-
303
- contains<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean
304
-
305
- get<R extends MemberRelation<Rels>>(relation: R, key: KeyFact<R>): Fact<R> | undefined
306
- get<R extends MemberRelation<Rels>, const P extends readonly string[]>(
307
- relation: R,
308
- keyStatement: KeyStatement<R, P>,
309
- key: DeclaredKeyFact<R, P>
310
- ): Fact<R> | undefined
311
- }
62
+ /** Core work policy plus the expected-state intent (chapter 35). */
63
+ type ApplyOptions = ExecutionPolicy & { readonly expected: ApplyExpected }
312
64
 
313
- const witnessTypes: unique symbol = Symbol("bumbledb.witness.types")
65
+ type ApplyOutcome =
66
+ | { readonly kind: "accepted"; readonly witness: CoreWitness }
67
+ | { readonly kind: "no-change"; readonly witness: CoreWitness }
68
+ | { readonly kind: "invariant-rejected"; readonly violations: readonly Violation[] }
69
+ | { readonly kind: "moved"; readonly witnessed: CoreWitness; readonly current: CoreWitness }
314
70
 
315
- interface Witness<Rels extends SchemaRelations> extends Disposable {
316
- readonly [witnessTypes]?: Rels
317
- }
318
-
319
- interface ReadInstance<Rels extends SchemaRelations> {
71
+ /** Bounded database diagnostics: measurements, never retained rows. */
72
+ interface DbInspection {
73
+ readonly schemaId: SchemaId
320
74
  readonly generation: bigint
321
-
322
- scan<R extends MemberRelation<Rels>>(relation: R): Fact<R>[]
323
-
324
- count<R extends MemberRelation<Rels>>(relation: R): bigint
325
-
326
- get<R extends MemberRelation<Rels>>(relation: R, key: KeyFact<R>): Fact<R> | undefined
327
-
328
- get<R extends MemberRelation<Rels>, const P extends readonly string[]>(
329
- relation: R,
330
- keyStatement: KeyStatement<R, P>,
331
- key: DeclaredKeyFact<R, P>
332
- ): Fact<R> | undefined
333
-
334
- contains<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean
335
-
336
- execute<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Row[]
337
- prepare<Row, Params extends ParamsRecord>(q: Query<Rels, Row, Params>): Prepared<Rels, Row, Params>
75
+ readonly mapBytes: bigint
76
+ readonly populatedBytes: bigint
77
+ readonly diskBytes: bigint
78
+ readonly residentEstimateBytes: bigint
79
+ readonly retainedOperations: bigint
338
80
  }
339
81
 
340
- const preparedTypes: unique symbol = Symbol("bumbledb.prepared.types")
341
-
342
82
  /**
343
- * One prepared query as a plain VALUE: explicit visible compilation
344
- * (`db.prepare(q)` lowers, pins the plan, and surfaces every engine roster
345
- * refusal), no lifecycle. Execution happens ONLY through
346
- * `instance.execute(prepared, params)`. The engine-side plan is reclaimed by a GC
347
- * finalizer when this value becomes unreachable (reclamation only, never
348
- * correctness — an unreclaimed plan is idle memory, and process exit frees
349
- * everything).
83
+ * The one shared read capability (chapter 30): the same typed `get` and
84
+ * `execute` on a core snapshot and on a log published snapshot, so a
85
+ * cross-package read helper takes this interface with no adapter. Missing
86
+ * key is `Option.none`, never a fake I/O error or nullable row. It carries
87
+ * no writable authority never a `Db`, an `apply`, or a raw transaction.
350
88
  */
351
- interface Prepared<Rels extends SchemaRelations, Row, Params extends ParamsRecord> {
352
- readonly [preparedTypes]?: { readonly rels: Rels; readonly row: Row; readonly params: Params }
89
+ interface QueryReader<S extends AnySchema> {
90
+ get<R extends Rel<S>>(relation: R, key: Key<R>, work: ExecutionPolicy): Effect.Effect<Option.Option<Fact<R>>, DbError>
91
+ execute<P extends ParamsRecord, A>(
92
+ query: QueryTemplate<S, P, A>,
93
+ params: P,
94
+ work: ExecutionPolicy
95
+ ): Effect.Effect<CompleteResult<A>, DbError, Scope.Scope>
353
96
  }
354
97
 
355
- interface Db<Rels extends SchemaRelations> {
356
- readonly schema: Schema<Rels>
357
-
358
- read<R>(body: (instance: ReadInstance<Rels>, witness: Witness<Rels>) => SyncResult<R>): SyncResult<R>
359
- /**
360
- * One delta transaction: builds the delta synchronously through `fn`,
361
- * commits, and returns the domain outcome. A throw from `fn` aborts
362
- * the delta (LMDB untouched) and rethrows wrapped. `fn` may decline to
363
- * commit by returning {@link abandon}`(payload)`: the transaction rolls
364
- * back — nothing is committed, not even an empty commit — and the
365
- * outcome is `{ tag: "abandoned", abandoned: payload }`.
366
- */
367
- write<R>(fn: (tx: WriteTx<Rels>) => SyncResult<R>): WriteOutcome<Rels, SyncResult<R>>
368
-
369
- writeFrom<R>(witness: Witness<Rels>, fn: (tx: WriteTx<Rels>) => SyncResult<R>): WriteFromOutcome<Rels, SyncResult<R>>
370
- /**
371
- * blake3 over the canonical catalog enumeration — the replication
372
- * equality oracle: equal digests imply identical judged content
373
- * regardless of page layout or allocation history. Harness-tier
374
- * (bounded sequential passes over the store), off any hot path.
375
- */
376
- catalogDigest(): Uint8Array
377
- /**
378
- * Prepares a query value built against THIS schema (identity is the
379
- * membership rule): lowers it to the engine IR, pins the plan, and
380
- * returns the typed {@link Prepared} value. Every IR roster refusal —
381
- * rule caps, rec roster, type rules — is the ENGINE's typed
382
- * judgment and throws here carrying its message intact.
383
- */
384
- prepare<Row, Params extends ParamsRecord>(q: Query<Rels, Row, Params>): Prepared<Rels, Row, Params>
98
+ interface Snapshot<S extends AnySchema> extends QueryReader<S> {
99
+ readonly witness: CoreWitness
100
+ session(work: ExecutionPolicy): Effect.Effect<ExecutionSession<S>, DbError, Scope.Scope>
101
+ close(): Effect.Effect<CloseReport>
385
102
  }
386
103
 
387
- interface RelationEntry {
388
- readonly id: number
389
- readonly member: SchemaRelation
390
- readonly fieldIds: ReadonlyMap<string, number>
391
- readonly primaryKey: PrimaryKey | undefined
104
+ interface ExecutionSession<S extends AnySchema> {
105
+ execute<P extends ParamsRecord, A>(
106
+ query: QueryTemplate<S, P, A>,
107
+ params: P,
108
+ work: ExecutionPolicy
109
+ ): Effect.Effect<CompleteResult<A>, DbError, Scope.Scope>
110
+ close(): Effect.Effect<CloseReport>
392
111
  }
393
112
 
394
- interface PrimaryKey {
395
- readonly statementId: number
396
- readonly projection: readonly string[]
113
+ interface Db<S extends AnySchema> {
114
+ readonly schemaId: SchemaId
115
+ snapshot(work: ExecutionPolicy): Effect.Effect<Snapshot<S>, DbError, Scope.Scope>
116
+ apply(changes: ChangeSet<S>, options: ApplyOptions): Effect.Effect<ApplyOutcome, DbError>
117
+ inspect(work: ExecutionPolicy): Effect.Effect<DbInspection, DbError>
118
+ close(): Effect.Effect<CloseReport>
397
119
  }
398
120
 
399
- type ImpliedKeyEntry = {
400
- readonly kind: "functionality"
401
- readonly owner: string
402
- readonly projection: readonly string[]
121
+ function refusal(operation: string, reason: "InvalidArgument" | "ClosedHandle"): DbError {
122
+ return new DbError({ operation, reason: { _tag: reason } })
403
123
  }
404
124
 
405
- type DeclaredKeyEntry = {
406
- readonly kind: "functionality"
407
- readonly statement: Statement
408
- readonly owner: string
409
- readonly projection: readonly string[]
125
+ function witnessOf(wire: WitnessWire): CoreWitness {
126
+ return Object.freeze({ store: wire.store, generation: wire.generation })
410
127
  }
411
128
 
412
- type StatementEntry =
413
- | ImpliedKeyEntry
414
- | DeclaredKeyEntry
415
- | { readonly kind: "containment"; readonly statement: Statement }
416
- | { readonly kind: "mirrors"; readonly statement: Statement; readonly orientation: "written" | "mirrored" }
417
- | { readonly kind: "capacity"; readonly statement: Statement }
418
-
419
- function decodeOffendingFact<Rels extends SchemaRelations>(
420
- member: SchemaRelation,
421
- relation: keyof Rels & string,
422
- fact: WireViolationFact
423
- ): OffendingFact<Rels> {
424
- const declared = sealedFieldsOf(member)
425
- const decoded: Record<string, FactValue> = {}
426
- for (const cell of fact.fields) {
427
- const cited = declared.find(function byName(candidate) {
428
- return candidate.name === cell.name
429
- })
430
- const roster = rosterOf(cited?.field)
431
- decoded[cell.name] =
432
- roster !== undefined
433
- ? handleOf(`violation fact ${fact.relation} field ${cell.name}`, roster, cell.value)
434
- : cell.value
435
- }
436
- return Object.freeze({ relation, fact: Object.freeze(decoded) })
437
- }
438
-
439
- function violationFromEntry<Rels extends SchemaRelations>(
440
- entry: StatementEntry,
441
- wire: WireViolation,
442
- facts: readonly OffendingFact<Rels>[]
443
- ): Violation<Rels> {
444
- const canonical = wire.canonical
445
- if (entry.kind === "functionality") {
446
- if (!("statement" in entry)) {
447
- return Object.freeze({ kind: "functionality", statement: undefined, canonical, facts })
448
- }
449
- return Object.freeze({ kind: "functionality", statement: entry.statement, canonical, facts })
450
- }
451
- if (entry.kind === "capacity") {
452
- if (wire.kind !== "capacity") {
453
- throw errors.new(`bumbledb violation ${wire.statementId} is a capacity slot without a measure`)
454
- }
455
- return Object.freeze({
456
- kind: "capacity",
457
- statement: entry.statement,
458
- canonical,
459
- measure: wire.measure,
460
- facts
461
- })
462
- }
463
- if (wire.kind !== "containment") {
464
- throw errors.new(`bumbledb violation ${wire.statementId} is a containment slot without a direction`)
465
- }
466
- if (entry.kind === "mirrors") {
467
- return Object.freeze({
468
- kind: "containment",
469
- statement: entry.statement,
470
- canonical,
471
- direction: wire.direction,
472
- orientation: entry.orientation,
473
- facts
474
- })
475
- }
476
- return Object.freeze({
477
- kind: "containment",
478
- statement: entry.statement,
479
- canonical,
480
- direction: wire.direction,
481
- facts
482
- })
483
- }
484
-
485
- function materializedEntries(theory: AnySchema): StatementEntry[] {
486
- const entries = impliedKeyEntries(theory)
487
- for (const statement of theory.statements) {
488
- entries.push(...declaredEntries(statement))
489
- }
490
- return entries
491
- }
492
-
493
- function impliedKeyEntries(theory: AnySchema): StatementEntry[] {
494
- const entries: StatementEntry[] = []
495
- for (const member of Object.values(theory.relations)) {
496
- if (isClosedMember(member)) {
497
- continue
498
- }
499
- for (const declared of member.data.fields) {
500
- if (isFreshField(declared.field)) {
501
- entries.push({
502
- kind: "functionality",
503
- owner: member.name,
504
- projection: [declared.name]
505
- })
506
- }
507
- }
508
- }
509
- for (const member of Object.values(theory.relations)) {
510
- if (isClosedMember(member)) {
511
- entries.push({
512
- kind: "functionality",
513
- owner: member.name,
514
- projection: ["id"]
515
- })
516
- }
517
- }
518
- return entries
519
- }
520
-
521
- function declaredEntries(statement: Statement): StatementEntry[] {
522
- const data = statement.data
523
- switch (data.kind) {
524
- case "key": {
525
- return [
526
- {
527
- kind: "functionality",
528
- statement,
529
- owner: data.owner.name,
530
- projection: data.projection
531
- }
532
- ]
533
- }
534
- case "containment": {
535
- return [{ kind: "containment", statement }]
536
- }
537
- case "mirrors": {
538
- return [
539
- { kind: "mirrors", statement, orientation: "written" },
540
- { kind: "mirrors", statement, orientation: "mirrored" }
541
- ]
542
- }
543
- case "capacity": {
544
- return [{ kind: "capacity", statement }]
545
- }
129
+ function outcomeOf(wire: ApplyOutcomeWire): ApplyOutcome {
130
+ switch (wire.tag) {
131
+ case "accepted":
132
+ return Object.freeze({ kind: "accepted", witness: witnessOf(wire.witness) })
133
+ case "no-change":
134
+ return Object.freeze({ kind: "no-change", witness: witnessOf(wire.witness) })
135
+ case "invariant-rejected":
136
+ return Object.freeze({ kind: "invariant-rejected", violations: wire.violations })
137
+ case "moved":
138
+ return Object.freeze({ kind: "moved", witnessed: witnessOf(wire.witnessed), current: witnessOf(wire.current) })
546
139
  }
547
140
  }
548
141
 
549
142
  /**
550
- * Narrows a callback result to a thenable the async-callback probe both
551
- * commit sites share: an `async` build callback typechecks (`Promise<void>`
552
- * is assignable where a `void` return is expected), so the refusal has to
553
- * be a runtime probe on the returned value.
143
+ * Validates the query template's schema binding (identity, the membership
144
+ * rule) and lowers it to IR plus wire params. Pure host preparation; the
145
+ * engine's IR validation under budget remains the authority.
554
146
  */
555
- function isThenable(value: unknown): boolean {
556
- return typeof value === "object" && value !== null && "then" in value && typeof value.then === "function"
557
- }
558
-
559
- function isStatementValue<R extends AnyRelation, P extends readonly string[]>(
560
- value: KeyFact<R> | KeyStatement<R, P>
561
- ): value is KeyStatement<R, P> {
562
- return isStatement(value)
563
- }
564
-
565
- function selectKeyRead<R extends AnyRelation, P extends readonly string[], T>(
566
- keyOrStatement: KeyFact<R> | KeyStatement<R, P>,
567
- declaredKey: DeclaredKeyFact<R, P> | undefined,
568
- byStatement: (statement: KeyStatement<R, P>, key: DeclaredKeyFact<R, P>) => T,
569
- byPrimary: (key: KeyFact<R>) => T
570
- ): T {
571
- if (declaredKey !== undefined) {
572
- if (!isStatementValue(keyOrStatement)) {
573
- throw errors.new("keyed get takes a key() statement value as its second argument")
574
- }
575
- return byStatement(keyOrStatement, declaredKey)
576
- }
577
- if (isStatementValue(keyOrStatement)) {
578
- throw errors.new("keyed get with a statement selector also takes the key object — get(relation, keyStatement, key)")
579
- }
580
- return byPrimary(keyOrStatement)
581
- }
582
-
583
- interface Tables {
584
- readonly relations: ReadonlyMap<string, RelationEntry>
585
- readonly statements: readonly StatementEntry[]
586
- }
587
-
588
- function tablesOf(theory: AnySchema, manifest: Manifest): Tables {
589
- const entries = materializedEntries(theory)
590
- if (entries.length !== manifest.statements.length) {
591
- throw errors.new(
592
- `bumbledb manifest drift: the SDK lowering yields ${entries.length} materialized statements, the engine reports ${manifest.statements.length}`
593
- )
594
- }
595
- manifest.statements.forEach(function verifySlot(statement, index) {
596
- const entry = entries[index]
597
- if (entry === undefined || statement.id !== index) {
598
- throw errors.new(
599
- `bumbledb manifest drift: statement ${statement.id} is ${statement.kind}, the SDK mirror at ${index} expected ${entry?.kind}`
600
- )
601
- }
602
- const engineKind = entry.kind === "mirrors" ? "containment" : entry.kind
603
- if (engineKind !== statement.kind) {
604
- throw errors.new(
605
- `bumbledb manifest drift: statement ${statement.id} is ${statement.kind}, the SDK mirror at ${index} expected ${engineKind}`
606
- )
607
- }
608
- })
609
- const relations = new Map<string, RelationEntry>()
610
- for (const relation of manifest.relations) {
611
- const member = theory.relations[relation.name]
612
- if (member === undefined) {
613
- throw errors.new(`bumbledb manifest drift: relation ${relation.name} is not in schema ${theory.name}`)
614
- }
615
- const fieldIds = new Map<string, number>()
616
- for (const field of relation.fields) {
617
- fieldIds.set(field.name, field.id)
618
- }
619
- sealedFieldsOf(member).forEach(function verifyField(declared, fieldOrdinal) {
620
- if (fieldIds.get(declared.name) !== fieldOrdinal) {
621
- throw errors.new(
622
- `bumbledb manifest drift: ${relation.name}.${declared.name} has engine field id ${fieldIds.get(declared.name)}, its sealed ordinal is ${fieldOrdinal}`
623
- )
624
- }
625
- })
626
- let primaryKey: PrimaryKey | undefined
627
- entries.forEach(function firstOwnedKey(entry, index) {
628
- if (primaryKey === undefined && entry.kind === "functionality" && entry.owner === relation.name) {
629
- primaryKey = Object.freeze({ statementId: index, projection: entry.projection })
630
- }
631
- })
632
- relations.set(relation.name, Object.freeze({ id: relation.id, member, fieldIds, primaryKey }))
633
- }
634
- Object.keys(theory.relations).forEach(function verifyRelation(name, ordinal) {
635
- const entry = relations.get(name)
636
- if (entry === undefined) {
637
- throw errors.new(`bumbledb manifest drift: schema relation ${name} is not in the manifest`)
638
- }
639
- if (entry.id !== ordinal) {
640
- throw errors.new(
641
- `bumbledb manifest drift: relation ${name} has engine id ${entry.id}, its declaration ordinal is ${ordinal} — query lowering depends on declaration order = ids`
147
+ function preparedOf<S extends AnySchema>(
148
+ theory: S,
149
+ query: AnyQuery,
150
+ params: Readonly<Record<string, unknown>>
151
+ ): {
152
+ readonly ir: ReturnType<typeof lowerQuery>
153
+ readonly wire: ReturnType<typeof wireParams>
154
+ readonly finds: AnyQuery["data"]["finds"]
155
+ } {
156
+ if (query.schema !== theory) {
157
+ throw refusal("QueryReader.execute", "InvalidArgument")
158
+ }
159
+ const ir = lowerQuery(query)
160
+ const wire = wireParams(query.data.params, params)
161
+ return { ir, wire, finds: query.data.finds }
162
+ }
163
+
164
+ interface SnapshotState<S extends AnySchema> {
165
+ readonly theory: S
166
+ readonly handle: SnapshotHandle
167
+ }
168
+
169
+ function executeOn<S extends AnySchema, A>(
170
+ state: SnapshotState<S>,
171
+ session: SessionHandle | undefined,
172
+ query: AnyQuery,
173
+ params: Readonly<Record<string, unknown>>,
174
+ work: ExecutionPolicy
175
+ ): Effect.Effect<CompleteResult<A>, DbError, Scope.Scope> {
176
+ return Effect.acquireRelease(
177
+ Effect.gen(function* () {
178
+ const prepared = yield* Effect.try({
179
+ try: () => preparedOf(state.theory, query, params),
180
+ catch: (cause) => (cause instanceof DbError ? cause : refusal("QueryReader.execute", "InvalidArgument"))
181
+ })
182
+ const wire = policyWire(work, "QueryReader.execute")
183
+ const handle = yield* nativeOperationWith(
184
+ "QueryReader.execute",
185
+ (callback) =>
186
+ session === undefined
187
+ ? dbNative.runtimeSnapshotExecute(state.handle, wire, prepared.ir, prepared.wire, callback)
188
+ : dbNative.runtimeSessionExecute(session, wire, prepared.ir, prepared.wire, callback),
189
+ dbNative.runtimeResultTake,
190
+ (value) => value
642
191
  )
643
- }
644
- })
645
- return Object.freeze({ relations, statements: Object.freeze(entries) })
646
- }
647
-
648
- function tablesFromTheory(theory: AnySchema): Tables {
649
- const entries = materializedEntries(theory)
650
- const relations = new Map<string, RelationEntry>()
651
- Object.keys(theory.relations).forEach(function byOrdinal(name, ordinal) {
652
- const member = theory.relations[name]
653
- if (member === undefined) {
654
- throw errors.new(`bumbledb theory has no relation ${name}`)
655
- }
656
- const fieldIds = new Map<string, number>()
657
- sealedFieldsOf(member).forEach(function byField(declared, fieldOrdinal) {
658
- fieldIds.set(declared.name, fieldOrdinal)
659
- })
660
- let primaryKey: PrimaryKey | undefined
661
- entries.forEach(function firstOwnedKey(entry, index) {
662
- if (primaryKey === undefined && entry.kind === "functionality" && entry.owner === name) {
663
- primaryKey = Object.freeze({ statementId: index, projection: entry.projection })
664
- }
665
- })
666
- relations.set(name, Object.freeze({ id: ordinal, member, fieldIds, primaryKey }))
667
- })
668
- return Object.freeze({ relations, statements: Object.freeze(entries) })
669
- }
670
-
671
- interface PointReads {
672
- contains(relationId: number, row: readonly FactValue[]): boolean
673
- get(relationId: number, statementId: number, key: readonly FactValue[]): FactValue[] | null
674
- }
675
-
676
- interface InstanceState {
677
- readonly handle: InstanceHandle
678
- live: boolean
679
- readonly owner: object
680
- }
681
-
682
- const instanceStates = new WeakMap<object, InstanceState>()
683
-
684
- interface WitnessState {
685
- readonly handle: WitnessHandle
686
- spent: boolean
687
- readonly owner: object
688
- }
689
-
690
- const witnessStates = new WeakMap<object, WitnessState>()
691
-
692
- const witnessReclaimer = new FinalizationRegistry<WitnessHandle>(function reclaimWitness(handle) {
693
- const closed = errors.trySync(function closeWitness() {
694
- native.witnessClose(handle)
695
- })
696
- if (closed.error) {
697
- return
698
- }
699
- })
700
-
701
- interface PreparedPlan {
702
- readonly handle: PreparedHandle
703
- readonly owner: object
704
- readonly params: readonly ParamEntry[]
705
- readonly finds: readonly FindColumn[]
192
+ return makeCompleteResult<A>(handle, prepared.finds)
193
+ }),
194
+ (result) =>
195
+ Effect.suspend(() => {
196
+ const internal = internalResultHandle(result)
197
+ if (internal === undefined) {
198
+ return Effect.void
199
+ }
200
+ return releaseOwner("CompleteResult.close", (callback) => dbNative.runtimeResultClose(internal, callback))
201
+ }),
202
+ { interruptible: true }
203
+ )
706
204
  }
707
205
 
708
- const preparedPlans = new WeakMap<object, PreparedPlan>()
709
-
710
- const planReclaimer = new FinalizationRegistry<PreparedHandle>(function reclaimPlan(handle) {
711
- const closed = errors.trySync(function closePlan() {
712
- native.preparedClose(handle)
713
- })
714
- if (closed.error) {
715
- return
716
- }
717
- })
718
-
719
- const ErrAsyncCallback = errors.new(
720
- "bumbledb asyncCallback: a read or write callback returned a thenable — the callback is synchronous"
721
- )
722
- const ErrSpentHandle = errors.new("bumbledb spentHandle: a consumed builder, instance, or witness was used")
723
- const ErrUseAfterScope = errors.new(
724
- "bumbledb useAfterScope: a stashed read instance or write transaction was used after its callback returned"
725
- )
726
- const ErrForeignPrepared = errors.new("bumbledb foreignPrepared: a prepared query met a foreign instance")
727
- const ErrForeignWitness = errors.new("bumbledb foreignWitness: a witness met a foreign store")
728
-
729
- interface CatalogNative {
730
- scan(relationId: number): FactValue[][]
731
- count(relationId: number): bigint
732
- contains(relationId: number, values: readonly FactValue[]): boolean
733
- get(relationId: number, statementId: number, keyValues: readonly FactValue[]): FactValue[] | null
734
- prepare(query: ReturnType<typeof lowerQuery>): ReturnType<typeof native.instancePrepare>
735
- execute(prepared: PreparedHandle, params: ReturnType<typeof wireParams>): FactValue[][]
206
+ function internalResultHandle(result: object) {
207
+ return internalResult(result)?.handle
736
208
  }
737
209
 
738
- function catalogMethods<Rels extends SchemaRelations>(
739
- theory: Schema<Rels>,
740
- tables: Tables,
741
- owner: object,
742
- assertLive: () => void,
743
- ops: CatalogNative
744
- ): Pick<ReadInstance<Rels>, "scan" | "count" | "get" | "contains" | "execute" | "prepare"> {
745
- function planOf(prepared: object): PreparedPlan {
746
- const plan = preparedPlans.get(prepared)
747
- if (plan === undefined) {
748
- throw errors.wrap(ErrForeignPrepared, "bumbledb execute target is not a prepared value of this SDK")
749
- }
750
- if (plan.owner !== owner) {
751
- throw errors.wrap(
752
- ErrForeignPrepared,
753
- `bumbledb prepared value was prepared by a different store than this one (schema ${theory.name})`
754
- )
755
- }
756
- return plan
757
- }
758
- function contains<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean {
759
- assertLive()
760
- const entry = ordinaryEntry(tables, theory, relation)
761
- return bridged("bumbledb instance contains", function readContains() {
762
- return ops.contains(entry.id, rowOf(relation.data, recordOf(fact)))
210
+ function getOn<S extends AnySchema, R extends Rel<S>>(
211
+ state: SnapshotState<S>,
212
+ relation: R,
213
+ key: Key<R>,
214
+ work: ExecutionPolicy
215
+ ): Effect.Effect<Option.Option<Fact<R>>, DbError> {
216
+ return Effect.gen(function* () {
217
+ if (state.theory.relations[relation.name] !== relation) {
218
+ return yield* Effect.fail(refusal("QueryReader.get", "InvalidArgument"))
219
+ }
220
+ const tables = schemaTables(state.theory)
221
+ const relationId = tables.relationIds.get(relation.name)
222
+ const primary = tables.primaryKeys.get(relation.name)
223
+ if (relationId === undefined || primary === undefined) {
224
+ return yield* Effect.fail(refusal("QueryReader.get", "InvalidArgument"))
225
+ }
226
+ const cells = yield* Effect.try({
227
+ try: () => keyCellsOf(relation.data, primary.projection, key as Readonly<Record<string, unknown>>),
228
+ catch: (cause) => (cause instanceof DbError ? cause : refusal("QueryReader.get", "InvalidArgument"))
763
229
  })
764
- }
765
- function get<R extends MemberRelation<Rels>, const P extends readonly string[]>(
766
- relation: R,
767
- keyOrStatement: KeyFact<R> | KeyStatement<R, P>,
768
- declaredKey?: DeclaredKeyFact<R, P>
769
- ): Fact<R> | undefined {
770
- assertLive()
771
- const entry = ordinaryEntry(tables, theory, relation)
772
- return selectKeyRead(
773
- keyOrStatement,
774
- declaredKey,
775
- function byStatement(statement, key) {
776
- const selected = declaredKeyOf(tables, theory, relation, statement)
777
- const row = bridged("bumbledb instance get", function readGet() {
778
- return ops.get(entry.id, selected.statementId, keyRowOf(relation.data, selected.projection, recordOf(key)))
779
- })
780
- return row === null ? undefined : factOf(relation, row)
781
- },
782
- function byPrimary(key) {
783
- const primaryKey = entry.primaryKey
784
- if (primaryKey === undefined) {
785
- throw errors.new(
786
- `relation ${relation.name} has no candidate key — keyed get requires a fresh field or a declared key statement`
787
- )
788
- }
789
- const row = bridged("bumbledb instance get", function readGet() {
790
- return ops.get(
791
- entry.id,
792
- primaryKey.statementId,
793
- keyRowOf(relation.data, primaryKey.projection, recordOf(key))
794
- )
795
- })
796
- return row === null ? undefined : factOf(relation, row)
797
- }
230
+ const row = yield* nativeOperationWith(
231
+ "QueryReader.get",
232
+ (callback) =>
233
+ dbNative.runtimeSnapshotGet(
234
+ state.handle,
235
+ policyWire(work, "QueryReader.get"),
236
+ relationId,
237
+ primary.statementId,
238
+ cells,
239
+ callback
240
+ ),
241
+ dbNative.runtimeRowTake,
242
+ (value) => value
798
243
  )
799
- }
800
- function scan<R extends MemberRelation<Rels>>(relation: R): Fact<R>[] {
801
- assertLive()
802
- const entry = ordinaryEntry(tables, theory, relation)
803
- const rows = bridged("bumbledb instance scan", function readScan() {
804
- return ops.scan(entry.id)
805
- })
806
- return rows.map(function decodeRow(row) {
807
- return factOf(relation, row)
808
- })
809
- }
810
- function count<R extends MemberRelation<Rels>>(relation: R): bigint {
811
- assertLive()
812
- const entry = ordinaryEntry(tables, theory, relation)
813
- return bridged("bumbledb instance count", function readCount() {
814
- return ops.count(entry.id)
815
- })
816
- }
817
- function execute<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Row[] {
818
- assertLive()
819
- const plan = planOf(prepared)
820
- const wire = wireParams(plan.params, recordOf(params))
821
- const rows = bridged("execute bumbledb prepared query", function callExecute() {
822
- return ops.execute(plan.handle, wire)
823
- })
824
- return decodeAnswers<Row>(plan.finds, rows)
825
- }
826
- function prepare<Row, Params extends ParamsRecord>(q: Query<Rels, Row, Params>): Prepared<Rels, Row, Params> {
827
- assertLive()
828
- if (q.schema !== theory) {
829
- throw errors.new(
830
- `query was built against schema ${q.schema.name}, not the identical schema value this store opened with — schema identity is the membership rule`
831
- )
832
- }
833
- const queryIr = lowerQuery(q)
834
- const outcome = bridged("prepare bumbledb query", function callPrepare() {
835
- return ops.prepare(queryIr)
836
- })
837
- if (!outcome.ok) {
838
- throwPrepareRefusal(outcome.message)
244
+ if (row === null) {
245
+ return Option.none<Fact<R>>()
839
246
  }
840
- const prepared: Prepared<Rels, Row, Params> = Object.freeze({})
841
- preparedPlans.set(
842
- prepared,
843
- Object.freeze({
844
- handle: outcome.prepared,
845
- owner,
846
- params: q.data.params,
847
- finds: q.data.finds
848
- })
849
- )
850
- planReclaimer.register(prepared, outcome.prepared)
851
- return prepared
852
- }
853
- return { scan, count, get, contains, execute, prepare }
854
- }
855
-
856
- function ordinaryEntry(tables: Tables, theory: AnySchema, relation: AnyRelation): RelationEntry {
857
- const entry = tables.relations.get(relation.name)
858
- if (entry === undefined || entry.member !== relation) {
859
- throw errors.new(`relation ${relation.name} is not a member of schema ${theory.name}`)
860
- }
861
- if (isClosedMember(relation)) {
862
- throw errors.new(
863
- `relation ${relation.name} is closed — its extension is schema data (axioms), never scanned or written`
864
- )
865
- }
866
- return entry
867
- }
868
-
869
- function declaredKeyOf(tables: Tables, theory: AnySchema, relation: AnyRelation, statement: Statement): PrimaryKey {
870
- const statementId = tables.statements.findIndex(function byIdentity(candidate) {
871
- return "statement" in candidate && candidate.statement === statement
247
+ return Option.some(factOfCells(relation, row as readonly CellValue[]))
872
248
  })
873
- const entry = tables.statements[statementId]
874
- if (entry === undefined) {
875
- throw errors.new(
876
- `keyed get statement is not a declared statement of schema ${theory.name} — statement identity is the membership rule`
877
- )
878
- }
879
- if (entry.kind !== "functionality") {
880
- throw errors.new("keyed get takes a key() statement — containments and capacity statements key nothing")
881
- }
882
- if (entry.owner !== relation.name) {
883
- throw errors.new(
884
- `keyed get statement keys ${entry.owner}, not ${relation.name} — the statement must be a declared key of the relation it reads`
885
- )
886
- }
887
- return Object.freeze({ statementId, projection: entry.projection })
888
249
  }
889
250
 
890
- function overlayMethods<Rels extends SchemaRelations>(
891
- theory: Schema<Rels>,
892
- tables: Tables,
893
- assertLive: () => void,
894
- reads: PointReads
895
- ): Pick<WriteTx<Rels>, "contains" | "get"> {
896
- function contains<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean {
897
- assertLive()
898
- const entry = ordinaryEntry(tables, theory, relation)
899
- return reads.contains(entry.id, rowOf(relation.data, recordOf(fact)))
900
- }
901
- function readThroughKey<R extends MemberRelation<Rels>>(
902
- relation: R,
903
- entry: RelationEntry,
904
- selected: PrimaryKey,
905
- key: Readonly<Record<string, unknown>>
906
- ): Fact<R> | undefined {
907
- const row = reads.get(entry.id, selected.statementId, keyRowOf(relation.data, selected.projection, key))
908
- if (row === null) {
909
- return undefined
251
+ function makeSession<S extends AnySchema>(state: SnapshotState<S>, handle: SessionHandle): ExecutionSession<S> {
252
+ const session: ExecutionSession<S> = {
253
+ execute(query, params, work) {
254
+ return executeOn(state, handle, query, params, work)
255
+ },
256
+ close() {
257
+ return drainClose("ExecutionSession.close", (callback) => dbNative.runtimeSessionClose(handle, callback))
910
258
  }
911
- return factOf(relation, row)
912
259
  }
913
- function get<R extends MemberRelation<Rels>>(relation: R, key: KeyFact<R>): Fact<R> | undefined
914
- function get<R extends MemberRelation<Rels>, const P extends readonly string[]>(
915
- relation: R,
916
- keyStatement: KeyStatement<R, P>,
917
- key: DeclaredKeyFact<R, P>
918
- ): Fact<R> | undefined
919
- function get<R extends MemberRelation<Rels>, const P extends readonly string[]>(
920
- relation: R,
921
- keyOrStatement: KeyFact<R> | KeyStatement<R, P>,
922
- declaredKey?: DeclaredKeyFact<R, P>
923
- ): Fact<R> | undefined {
924
- assertLive()
925
- const entry = ordinaryEntry(tables, theory, relation)
926
- return selectKeyRead(
927
- keyOrStatement,
928
- declaredKey,
929
- function byStatement(statement, key) {
930
- return readThroughKey(relation, entry, declaredKeyOf(tables, theory, relation, statement), recordOf(key))
931
- },
932
- function byPrimary(key) {
933
- const primaryKey = entry.primaryKey
934
- if (primaryKey === undefined) {
935
- throw errors.new(
936
- `relation ${relation.name} has no candidate key — keyed get requires a fresh field or a declared key statement`
937
- )
938
- }
939
- return readThroughKey(relation, entry, primaryKey, recordOf(key))
940
- }
941
- )
942
- }
943
- return { contains, get }
260
+ Object.freeze(session)
261
+ sessionHandles.set(session, handle)
262
+ return session
944
263
  }
945
264
 
946
- function createReadInstance<Rels extends SchemaRelations>(
947
- nativeHandle: InstanceHandle,
948
- theory: Schema<Rels>,
949
- tables: Tables,
950
- owner: object
951
- ): ReadInstance<Rels> {
952
- const state: InstanceState = { handle: nativeHandle, live: true, owner }
953
- function assertLive(): void {
954
- if (!state.live) {
955
- throw errors.wrap(
956
- ErrUseAfterScope,
957
- "bumbledb read instance is invalidated — its owning callback already returned"
958
- )
959
- }
960
- }
961
- const methods = catalogMethods(theory, tables, owner, assertLive, {
962
- scan(relationId) {
963
- return native.instanceScan(state.handle, relationId)
265
+ function makeSnapshot<S extends AnySchema>(theory: S, handle: SnapshotHandle, witness: CoreWitness): Snapshot<S> {
266
+ const state: SnapshotState<S> = { theory, handle }
267
+ const snapshot: Snapshot<S> = {
268
+ witness,
269
+ get(relation, key, work) {
270
+ return getOn(state, relation, key, work)
964
271
  },
965
- count(relationId) {
966
- return native.instanceCount(state.handle, relationId)
272
+ execute(query, params, work) {
273
+ return executeOn(state, undefined, query, params, work)
967
274
  },
968
- contains(relationId, values) {
969
- return native.instanceContains(state.handle, relationId, values)
970
- },
971
- get(relationId, statementId, keyValues) {
972
- return native.instanceGet(state.handle, relationId, statementId, keyValues)
973
- },
974
- prepare(query) {
975
- return native.instancePrepare(state.handle, query)
275
+ session(work) {
276
+ return Effect.acquireRelease(
277
+ nativeOperationWith(
278
+ "Snapshot.session",
279
+ (callback) => dbNative.runtimeSnapshotSession(handle, policyWire(work, "Snapshot.session"), callback),
280
+ dbNative.runtimeSessionTake,
281
+ (value) => makeSession(state, value)
282
+ ),
283
+ (session) =>
284
+ Effect.suspend(() =>
285
+ releaseOwner("ExecutionSession.close", (callback) =>
286
+ dbNative.runtimeSessionClose(sessionHandles.get(session) ?? missingSession(), callback)
287
+ )
288
+ ),
289
+ { interruptible: true }
290
+ )
976
291
  },
977
- execute(prepared, params) {
978
- return native.preparedExecute(prepared, state.handle, params)
292
+ close() {
293
+ return drainClose("Snapshot.close", (callback) => dbNative.runtimeSnapshotClose(handle, callback))
979
294
  }
980
- })
981
- const instance: ReadInstance<Rels> = Object.freeze({
982
- get generation() {
983
- assertLive()
984
- return bridged("bumbledb instance generation", function readGeneration() {
985
- return native.instanceGeneration(state.handle)
986
- })
987
- },
988
- ...methods
989
- })
990
- instanceStates.set(instance, state)
991
- return instance
295
+ }
296
+ Object.freeze(snapshot)
297
+ snapshotHandles.set(snapshot, handle)
298
+ return snapshot
992
299
  }
993
300
 
994
- function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<Rels>, manifest: Manifest): Db<Rels> {
995
- const tables = tablesOf(theory, manifest)
996
- /** This store's identity token: read scopes and prepared values carry it, so cross-store use is a typed refusal. */
997
- const owner = Object.freeze({})
301
+ const sessionHandles = new WeakMap<object, SessionHandle>()
998
302
 
999
- function isMemberName(name: string): name is keyof Rels & string {
1000
- return tables.relations.has(name)
1001
- }
1002
-
1003
- function violationOf(wire: WireViolation): Violation<Rels> {
1004
- const entry = tables.statements[wire.statementId]
1005
- if (entry === undefined) {
1006
- throw errors.new(`bumbledb violation cites unknown statement id ${wire.statementId}`)
1007
- }
1008
- const facts = Object.freeze(
1009
- wire.facts.map(function offending(fact) {
1010
- const rel = tables.relations.get(fact.relation)
1011
- if (rel === undefined || !isMemberName(fact.relation)) {
1012
- throw errors.new(`bumbledb violation cites unknown relation ${fact.relation}`)
1013
- }
1014
- return decodeOffendingFact<Rels>(rel.member, fact.relation, fact)
1015
- })
1016
- )
1017
- return violationFromEntry(entry, wire, facts)
1018
- }
303
+ function missingSession(): never {
304
+ throw new DbError({ operation: "ExecutionSession.close", reason: { _tag: "Internal" } })
305
+ }
1019
306
 
1020
- function pointReadsOf(assertLive: () => void, reads: PointReads) {
1021
- function contains<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean {
1022
- assertLive()
1023
- const entry = ordinaryEntry(tables, theory, relation)
1024
- return reads.contains(entry.id, rowOf(relation.data, recordOf(fact)))
1025
- }
307
+ interface DbState {
308
+ readonly theory: AnySchema
309
+ readonly db: DbHandle
310
+ readonly directory: DirectoryHandle
311
+ readonly schemaId: SchemaId
312
+ }
1026
313
 
1027
- function readThroughKey<R extends MemberRelation<Rels>>(
1028
- relation: R,
1029
- entry: RelationEntry,
1030
- selected: PrimaryKey,
1031
- key: Readonly<Record<string, unknown>>
1032
- ): Fact<R> | undefined {
1033
- const row = reads.get(entry.id, selected.statementId, keyRowOf(relation.data, selected.projection, key))
1034
- if (row === null) {
1035
- return undefined
1036
- }
1037
- return factOf(relation, row)
1038
- }
1039
- function get<R extends MemberRelation<Rels>>(relation: R, key: KeyFact<R>): Fact<R> | undefined
1040
- function get<R extends MemberRelation<Rels>, const P extends readonly string[]>(
1041
- relation: R,
1042
- keyStatement: KeyStatement<R, P>,
1043
- key: DeclaredKeyFact<R, P>
1044
- ): Fact<R> | undefined
1045
- function get<R extends MemberRelation<Rels>, const P extends readonly string[]>(
1046
- relation: R,
1047
- keyOrStatement: KeyFact<R> | KeyStatement<R, P>,
1048
- declaredKey?: DeclaredKeyFact<R, P>
1049
- ): Fact<R> | undefined {
1050
- assertLive()
1051
- const entry = ordinaryEntry(tables, theory, relation)
1052
- return selectKeyRead(
1053
- keyOrStatement,
1054
- declaredKey,
1055
- function byStatement(statement, key) {
1056
- return readThroughKey(relation, entry, declaredKeyOf(tables, theory, relation, statement), recordOf(key))
1057
- },
1058
- function byPrimary(key) {
1059
- const primaryKey = entry.primaryKey
1060
- if (primaryKey === undefined) {
1061
- throw errors.new(
1062
- `relation ${relation.name} has no candidate key — keyed get requires a fresh field or a declared key statement`
314
+ function makeDb<S extends AnySchema>(theory: S, state: DbState): Db<S> {
315
+ const value: Db<S> = {
316
+ schemaId: state.schemaId,
317
+ snapshot(work) {
318
+ return Effect.acquireRelease(
319
+ nativeOperationWith(
320
+ "Db.snapshot",
321
+ (callback) => dbNative.runtimeDbSnapshot(state.db, policyWire(work, "Db.snapshot"), callback),
322
+ dbNative.runtimeSnapshotTake,
323
+ (wire) => makeSnapshot(theory, wire.snapshot, witnessOf(wire.witness))
324
+ ),
325
+ (snapshot) =>
326
+ Effect.suspend(() =>
327
+ releaseOwner("Snapshot.close", (callback) =>
328
+ dbNative.runtimeSnapshotClose(snapshotHandles.get(snapshot) ?? missingSnapshot(), callback)
1063
329
  )
1064
- }
1065
- return readThroughKey(relation, entry, primaryKey, recordOf(key))
1066
- }
330
+ ),
331
+ { interruptible: true }
1067
332
  )
1068
- }
1069
- return { contains, get }
1070
- }
1071
-
1072
- function pinPrepared<Row, Params extends ParamsRecord>(
1073
- preparedHandle: PreparedHandle,
1074
- q: Query<Rels, Row, Params>
1075
- ): Prepared<Rels, Row, Params> {
1076
- const prepared: Prepared<Rels, Row, Params> = Object.freeze({})
1077
- preparedPlans.set(
1078
- prepared,
1079
- Object.freeze({
1080
- handle: preparedHandle,
1081
- owner,
1082
- params: q.data.params,
1083
- finds: q.data.finds
1084
- })
1085
- )
1086
- planReclaimer.register(prepared, preparedHandle)
1087
- return prepared
1088
- }
1089
-
1090
- function makeWitness(nativeHandle: WitnessHandle): Witness<Rels> {
1091
- const state: WitnessState = { handle: nativeHandle, spent: false, owner }
1092
- const witness: Witness<Rels> = Object.freeze({
1093
- [Symbol.dispose](): void {
1094
- if (state.spent) {
1095
- return
1096
- }
1097
- state.spent = true
1098
- bridged("close bumbledb witness", function closeWitness() {
1099
- native.witnessClose(nativeHandle)
1100
- })
1101
- }
1102
- })
1103
- witnessStates.set(witness, state)
1104
- witnessReclaimer.register(witness, nativeHandle)
1105
- return witness
1106
- }
1107
-
1108
- function makeInstance(nativeHandle: InstanceHandle): ReadInstance<Rels> {
1109
- return createReadInstance(nativeHandle, theory, tables, owner)
1110
- }
1111
-
1112
- function read<R>(body: (instance: ReadInstance<Rels>, witness: Witness<Rels>) => SyncResult<R>): SyncResult<R> {
1113
- let captured: R | undefined
1114
- const result = bridged("bumbledb read", function runRead() {
1115
- return native.dbRead(handle, function onRead(nativeInstance, nativeWitness) {
1116
- const instance = makeInstance(nativeInstance)
1117
- const witness = makeWitness(nativeWitness)
1118
- const value = body(instance, witness)
1119
- const state = instanceStates.get(instance)
1120
- if (state !== undefined) {
1121
- state.live = false
1122
- }
1123
- if (isThenable(value)) {
1124
- throw errors.wrap(ErrAsyncCallback, "bumbledb read callback returned a thenable")
1125
- }
1126
- captured = value
1127
- return value
1128
- })
1129
- })
1130
- return (captured ?? result) as SyncResult<R>
1131
- }
1132
-
1133
- function makeTx(resolveTx: () => TxHandle): { readonly tx: WriteTx<Rels>; spend(): void } {
1134
- const txState = { spent: false }
1135
- function assertLive(): void {
1136
- if (txState.spent) {
1137
- throw errors.wrap(ErrUseAfterScope, "bumbledb write transaction is spent")
1138
- }
1139
- }
1140
- const reads = pointReadsOf(assertLive, {
1141
- contains(relationId, row) {
1142
- const txHandle = resolveTx()
1143
- return bridged("bumbledb tx contains", function readContains() {
1144
- return native.txContains(txHandle, relationId, row)
1145
- })
1146
- },
1147
- get(relationId, statementId, key) {
1148
- const txHandle = resolveTx()
1149
- return bridged("bumbledb tx get", function readGet() {
1150
- return native.txGet(txHandle, relationId, statementId, key)
1151
- })
1152
- }
1153
- })
1154
- function insert<R extends MemberRelation<Rels>>(relation: R, facts: CollectionWrite<R>): MutationReport {
1155
- assertLive()
1156
- const entry = ordinaryEntry(tables, theory, relation)
1157
- const txHandle = resolveTx()
1158
- return mutateCollection(relation, facts, function applyCells(rows, cells) {
1159
- return bridged("bumbledb tx insert", function record() {
1160
- return native.txInsert(txHandle, entry.id, rows, cells)
1161
- })
1162
- })
1163
- }
1164
- function remove<R extends MemberRelation<Rels>>(relation: R, facts: Iterable<Fact<R>>): MutationReport {
1165
- assertLive()
1166
- const entry = ordinaryEntry(tables, theory, relation)
1167
- const txHandle = resolveTx()
1168
- const flat = rowsOf(relation, facts)
1169
- const report = bridged("bumbledb tx delete", function record() {
1170
- return native.txDelete(txHandle, entry.id, flat.rows, flat.cells)
1171
- })
1172
- return Object.freeze({ submitted: report.submitted, changed: report.changed })
1173
- }
1174
- function reserve<R extends MemberRelation<Rels>>(
1175
- relation: R,
1176
- field: FreshKeys<R> & string,
1177
- count: bigint
1178
- ): FreshRange {
1179
- assertLive()
1180
- const entry = ordinaryEntry(tables, theory, relation)
1181
- const declared = relation.data.fields.find(function byName(candidate) {
1182
- return candidate.name === field
1183
- })
1184
- if (declared === undefined || !isFreshField(declared.field)) {
1185
- throw errors.new(`relation ${relation.name}: field ${field} is not a fresh cell`)
1186
- }
1187
- const fieldId = entry.fieldIds.get(field)
1188
- if (fieldId === undefined) {
1189
- throw errors.new(`bumbledb manifest drift: relation ${relation.name} has no field id for ${field}`)
1190
- }
1191
- const txHandle = resolveTx()
1192
- const range = bridged("bumbledb tx reserve", function mint() {
1193
- return native.txReserve(txHandle, entry.id, fieldId, count)
1194
- })
1195
- return freshRangeOf(range)
1196
- }
1197
- const tx: WriteTx<Rels> = Object.freeze({
1198
- insert,
1199
- delete: remove,
1200
- reserve,
1201
- contains: reads.contains,
1202
- get: reads.get
1203
- })
1204
- function spend(): void {
1205
- txState.spent = true
1206
- }
1207
- return { tx, spend }
1208
- }
1209
-
1210
- function mapNativeWrite<R>(nativeOutcome: NativeWriteOutcome, built: R | undefined): WriteFromOutcome<Rels, R> {
1211
- if (nativeOutcome.tag === "moved") {
1212
- return Object.freeze({
1213
- tag: "moved" as const,
1214
- witnessed: nativeOutcome.witnessed,
1215
- current: nativeOutcome.current
1216
- })
1217
- }
1218
- if (nativeOutcome.tag === "rejected") {
1219
- return Object.freeze({
1220
- tag: "rejected" as const,
1221
- violations: Object.freeze(nativeOutcome.violations.map(violationOf))
1222
- })
1223
- }
1224
- if (nativeOutcome.tag === "abandoned") {
1225
- if (built === undefined || !isAbandon(built)) {
1226
- throw errors.new("bumbledb write abandoned without an abandon sentinel")
1227
- }
1228
- return abandonedOutcome<Rels, R>(built)
1229
- }
1230
- return Object.freeze({
1231
- tag: "accepted" as const,
1232
- value: Object.freeze({
1233
- value: built as Exclude<R, Abandon<unknown>>,
1234
- generation: nativeOutcome.generation
1235
- })
1236
- })
1237
- }
1238
-
1239
- function runWrite<R>(
1240
- invoke: (callback: (tx: TxHandle) => boolean) => NativeWriteOutcome,
1241
- fn: (tx: WriteTx<Rels>) => SyncResult<R>
1242
- ): WriteFromOutcome<Rels, SyncResult<R>> {
1243
- let built: SyncResult<R> | undefined
1244
- const nativeOutcome = bridged("bumbledb write", function callWrite() {
1245
- return invoke(function onWrite(txHandle) {
1246
- const made = makeTx(function resolveTx() {
1247
- return txHandle
1248
- })
1249
- const result = errors.trySync(function buildDelta() {
1250
- return fn(made.tx)
1251
- })
1252
- made.spend()
1253
- if (result.error) {
1254
- throw errors.wrap(result.error, "build write delta")
333
+ },
334
+ apply(changes, options) {
335
+ return Effect.gen(function* () {
336
+ const internal = internalChanges(changes)
337
+ if (internal === undefined) {
338
+ // A foreign object supplied dynamically refuses before
339
+ // any native dispatch.
340
+ return yield* Effect.fail(refusal("Db.apply", "InvalidArgument"))
1255
341
  }
1256
- if (isThenable(result.data)) {
1257
- throw errors.wrap(ErrAsyncCallback, "bumbledb write callback returned a thenable")
342
+ if (internal.schemaId !== state.schemaId) {
343
+ return yield* Effect.fail(refusal("Db.apply", "InvalidArgument"))
1258
344
  }
1259
- built = result.data
1260
- return !isAbandon(result.data)
345
+ const expected: ExpectedWire =
346
+ options.expected.kind === "any"
347
+ ? { kind: "any" }
348
+ : { kind: "exact", store: options.expected.at.store, generation: options.expected.at.generation }
349
+ return yield* nativeOperationWith(
350
+ "Db.apply",
351
+ (callback) =>
352
+ dbNative.runtimeDbApply(state.db, policyWire(options, "Db.apply"), internal.handle, expected, callback),
353
+ dbNative.runtimeApplyTake,
354
+ outcomeOf
355
+ )
1261
356
  })
1262
- })
1263
- return mapNativeWrite(nativeOutcome, built)
1264
- }
1265
-
1266
- function write<R>(fn: (tx: WriteTx<Rels>) => SyncResult<R>): WriteOutcome<Rels, SyncResult<R>> {
1267
- const outcome = runWrite(function invoke(callback) {
1268
- return native.dbWrite(handle, callback)
1269
- }, fn)
1270
- if (outcome.tag === "moved") {
1271
- throw errors.new("bumbledb write reported moved — unconditional writes cannot move")
1272
- }
1273
- return outcome
1274
- }
1275
-
1276
- function writeFrom<R>(
1277
- witness: Witness<Rels>,
1278
- fn: (tx: WriteTx<Rels>) => SyncResult<R>
1279
- ): WriteFromOutcome<Rels, SyncResult<R>> {
1280
- const state = witnessStates.get(witness)
1281
- if (state === undefined) {
1282
- throw errors.wrap(ErrForeignWitness, "bumbledb writeFrom witness is not a witness of this SDK")
1283
- }
1284
- if (state.owner !== owner) {
1285
- throw errors.wrap(
1286
- ErrForeignWitness,
1287
- `bumbledb writeFrom witness belongs to a different store (schema ${theory.name})`
357
+ },
358
+ inspect(work) {
359
+ return nativeOperationWith(
360
+ "Db.inspect",
361
+ (callback) => dbNative.runtimeDbInspect(state.db, policyWire(work, "Db.inspect"), callback),
362
+ dbNative.runtimeDbInspectTake,
363
+ (wire: DbInspectionWire): DbInspection =>
364
+ Object.freeze({
365
+ schemaId: state.schemaId,
366
+ generation: wire.generation,
367
+ mapBytes: wire.mapBytes,
368
+ populatedBytes: wire.populatedBytes,
369
+ diskBytes: wire.diskBytes,
370
+ residentEstimateBytes: wire.residentEstimateBytes,
371
+ retainedOperations: wire.retainedOperations
372
+ })
1288
373
  )
1289
- }
1290
- if (state.spent) {
1291
- throw errors.wrap(ErrSpentHandle, "bumbledb writeFrom witness has been disposed")
1292
- }
1293
- return runWrite(function invoke(callback) {
1294
- return native.dbWriteFrom(handle, state.handle, callback)
1295
- }, fn)
1296
- }
1297
-
1298
- function catalogDigest(): Uint8Array {
1299
- return bridged("bumbledb catalog digest", function readCatalogDigest() {
1300
- return native.dbCatalogDigest(handle)
1301
- })
1302
- }
1303
-
1304
- function prepare<Row, Params extends ParamsRecord>(q: Query<Rels, Row, Params>): Prepared<Rels, Row, Params> {
1305
- if (q.schema !== theory) {
1306
- throw errors.new(
1307
- `query was built against schema ${q.schema.name}, not the identical schema value this store opened with — schema identity is the membership rule`
374
+ },
375
+ close() {
376
+ // The one close authority: database child first, then the
377
+ // directory owner releases its kernel lock LAST (C09). Both
378
+ // joins are idempotent natively.
379
+ return drainClose("Db.close", (callback) => runtimeNative.runtimeManagedDbClose(state.db, callback)).pipe(
380
+ Effect.flatMap((report) =>
381
+ drainClose("Db.directoryClose", (callback) =>
382
+ runtimeNative.runtimeDirectoryClose(state.directory, false, callback)
383
+ ).pipe(Effect.map((directoryReport) => (report.kind === "closed" ? directoryReport : report)))
384
+ )
1308
385
  )
1309
386
  }
1310
- const queryIr = lowerQuery(q)
1311
- const outcome = bridged("prepare bumbledb query", function callPrepare() {
1312
- return native.dbPrepare(handle, queryIr)
1313
- })
1314
- if (!outcome.ok) {
1315
- throwPrepareRefusal(outcome.message)
1316
- }
1317
- return pinPrepared(outcome.prepared, q)
1318
387
  }
1319
-
1320
- return Object.freeze({
1321
- schema: theory,
1322
- read,
1323
- write,
1324
- writeFrom,
1325
- catalogDigest,
1326
- prepare
1327
- })
388
+ Object.freeze(value)
389
+ return value
1328
390
  }
1329
391
 
1330
- const ErrNewtypeMismatch = errors.new(
1331
- "bumbledb newtypeMismatch: a statement pairs faces whose newtypes disagree — the faces of a dependency agree on their newtype, or neither carries one"
1332
- )
1333
- const ErrSchemaError = errors.new("bumbledb schemaError: the declaration failed validation")
1334
- const ErrFingerprintMismatch = errors.new("bumbledb fingerprintMismatch: the store's schema does not match this theory")
1335
- const ErrIrError = errors.new("bumbledb irError: the query failed validation")
392
+ const snapshotHandles = new WeakMap<object, SnapshotHandle>()
1336
393
 
1337
- function throwOpenRefusal(
1338
- verb: string,
1339
- canonical: string,
1340
- kind: "schemaError" | "newtypeMismatch" | "fingerprintMismatch",
1341
- message: string
1342
- ): never {
1343
- const detail = `${verb} ${canonical}: ${message}`
1344
- if (kind === "newtypeMismatch") {
1345
- throw errors.wrap(ErrNewtypeMismatch, detail)
1346
- }
1347
- if (kind === "schemaError") {
1348
- throw errors.wrap(ErrSchemaError, detail)
1349
- }
1350
- throw errors.wrap(ErrFingerprintMismatch, detail)
1351
- }
1352
-
1353
- function throwPrepareRefusal(message: string): never {
1354
- throw errors.wrap(ErrIrError, `prepare: ${message}`)
1355
- }
1356
-
1357
- function openFromHandle<Rels extends SchemaRelations>(dbHandle: DbHandle, theory: Schema<Rels>): Db<Rels> {
1358
- const manifest = bridged("fetch bumbledb manifest", function fetchManifest() {
1359
- return native.dbManifest(dbHandle)
1360
- })
1361
- return openDb(dbHandle, theory, manifest)
394
+ function missingSnapshot(): never {
395
+ throw new DbError({ operation: "Snapshot.close", reason: { _tag: "Internal" } })
1362
396
  }
1363
397
 
1364
- async function createStore<Rels extends SchemaRelations>(
1365
- storePath: string,
1366
- theory: Schema<Rels>
1367
- ): Promise<Admission<Rels, Db<Rels>>> {
1368
- const canonical = path.resolve(storePath)
1369
- const spec = lower(theory)
1370
- const created = await bridgedAsync(`create bumbledb store at ${canonical}`, function callBridge() {
1371
- return native.dbCreate(canonical, spec)
1372
- })
1373
- if (created.tag === "schemaError" || created.tag === "newtypeMismatch") {
1374
- throwOpenRefusal("create", canonical, created.tag, created.message)
1375
- }
1376
- if (created.tag === "rejected") {
1377
- return Object.freeze({
1378
- tag: "rejected" as const,
1379
- violations: Object.freeze(
1380
- created.violations.map(function mapWire(wire) {
1381
- return mapViolationWithoutStore<Rels>(theory, wire)
1382
- })
1383
- )
1384
- })
1385
- }
1386
- return Object.freeze({ tag: "accepted" as const, value: openFromHandle(created.db, theory) })
1387
- }
398
+ /** The managed child name under the database directory owner. */
399
+ const CHILD = "store"
1388
400
 
1389
- function mapViolationWithoutStore<Rels extends SchemaRelations>(
1390
- theory: Schema<Rels>,
1391
- wire: WireViolation
1392
- ): Violation<Rels> {
1393
- const entries = materializedEntries(theory)
1394
- const entry = entries[wire.statementId]
1395
- if (entry === undefined) {
1396
- throw errors.new(`bumbledb violation cites unknown statement id ${wire.statementId}`)
1397
- }
1398
- const facts = Object.freeze(
1399
- wire.facts.map(function offending(fact) {
1400
- const member = theory.relations[fact.relation]
1401
- if (member === undefined || !(fact.relation in theory.relations)) {
1402
- throw errors.new(`bumbledb violation cites unknown relation ${fact.relation}`)
1403
- }
1404
- return decodeOffendingFact<Rels>(member, fact.relation as keyof Rels & string, fact)
401
+ function openDatabase<S extends AnySchema>(
402
+ operation: "Db.create" | "Db.open",
403
+ path: string,
404
+ schema: S,
405
+ work: ExecutionPolicy,
406
+ create: boolean
407
+ ): Effect.Effect<Db<S>, DbError, NativeRuntime | Scope.Scope> {
408
+ return Effect.gen(function* () {
409
+ const runtime = yield* runtimeHandle()
410
+ const compiled: CompiledSchema<S> = yield* CoreSchema.compile(schema, work)
411
+ const spec = lower(schema)
412
+ const wire = yield* Effect.try({
413
+ try: () => policyWire(work, operation),
414
+ catch: () => refusal(operation, "InvalidArgument")
1405
415
  })
1406
- )
1407
- return violationFromEntry(entry, wire, facts)
1408
- }
1409
-
1410
- async function openStore<Rels extends SchemaRelations>(storePath: string, theory: Schema<Rels>): Promise<Db<Rels>> {
1411
- const canonical = path.resolve(storePath)
1412
- const spec = lower(theory)
1413
- const opened = await bridgedAsync(`open bumbledb store at ${canonical}`, function callBridge() {
1414
- return native.dbOpen(canonical, spec)
1415
- })
1416
- if (!opened.ok) {
1417
- throwOpenRefusal("open", canonical, opened.kind, opened.message)
1418
- }
1419
- return openFromHandle(opened.db, theory)
1420
- }
1421
-
1422
- interface OwnedInstance<Rels extends SchemaRelations> extends Disposable {
1423
- prepare<Row, Params extends ParamsRecord>(q: Query<Rels, Row, Params>): Prepared<Rels, Row, Params>
1424
- execute<Row, Params extends ParamsRecord>(prepared: Prepared<Rels, Row, Params>, params: Params): Row[]
1425
- scan<R extends MemberRelation<Rels>>(relation: R): Fact<R>[]
1426
- count<R extends MemberRelation<Rels>>(relation: R): bigint
1427
- contains<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean
1428
-
1429
- get<R extends MemberRelation<Rels>>(relation: R, key: KeyFact<R>): Fact<R> | undefined
1430
- get<R extends MemberRelation<Rels>, const P extends readonly string[]>(
1431
- relation: R,
1432
- keyStatement: KeyStatement<R, P>,
1433
- key: DeclaredKeyFact<R, P>
1434
- ): Fact<R> | undefined
1435
- }
1436
-
1437
- interface InstanceBuilder<Rels extends SchemaRelations> extends Disposable {
1438
- load<R extends MemberRelation<Rels>>(relation: R, facts: CollectionWrite<R>): MutationReport
1439
- delete<R extends MemberRelation<Rels>>(relation: R, facts: Iterable<Fact<R>>): MutationReport
1440
- reserve<R extends MemberRelation<Rels>>(relation: R, field: FreshKeys<R> & string, count: bigint): FreshRange
1441
- contains<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean
1442
-
1443
- get<R extends MemberRelation<Rels>>(relation: R, key: KeyFact<R>): Fact<R> | undefined
1444
- get<R extends MemberRelation<Rels>, const P extends readonly string[]>(
1445
- relation: R,
1446
- keyStatement: KeyStatement<R, P>,
1447
- key: DeclaredKeyFact<R, P>
1448
- ): Fact<R> | undefined
1449
- admit(): Promise<Admission<Rels, OwnedInstance<Rels>>>
1450
- }
1451
-
1452
- const ownedRecords = new WeakMap<object, { handle: OwnedHandle; theory: AnySchema; spent: boolean; owner: object }>()
1453
- const builderRecords = new WeakMap<object, { handle: BuilderHandle; theory: AnySchema; spent: boolean }>()
1454
-
1455
- const ownedReclaimer = new FinalizationRegistry<OwnedHandle>(function reclaimOwned(handle) {
1456
- const closed = errors.trySync(function closeOwned() {
1457
- native.ownedInstanceClose(handle)
1458
- })
1459
- if (closed.error) {
1460
- return
1461
- }
1462
- })
1463
-
1464
- const builderReclaimer = new FinalizationRegistry<BuilderHandle>(function reclaimBuilder(handle) {
1465
- const closed = errors.trySync(function closeBuilder() {
1466
- native.instanceBuilderClose(handle)
1467
- })
1468
- if (closed.error) {
1469
- return
1470
- }
1471
- })
1472
-
1473
- function wrapOwned<Rels extends SchemaRelations>(nativeHandle: OwnedHandle, theory: Schema<Rels>): OwnedInstance<Rels> {
1474
- const owner = Object.freeze({})
1475
- const rec = { handle: nativeHandle, theory, spent: false, owner }
1476
- const tables = tablesFromTheory(theory)
1477
- function assertLive(): void {
1478
- if (rec.spent) {
1479
- throw errors.wrap(ErrSpentHandle, "bumbledb owned instance has been disposed")
1480
- }
1481
- }
1482
- const methods = catalogMethods(theory, tables, owner, assertLive, {
1483
- scan(relationId) {
1484
- return native.ownedScan(nativeHandle, relationId)
1485
- },
1486
- count(relationId) {
1487
- return native.ownedCount(nativeHandle, relationId)
1488
- },
1489
- contains(relationId, values) {
1490
- return native.ownedContains(nativeHandle, relationId, values)
1491
- },
1492
- get(relationId, statementId, keyValues) {
1493
- return native.ownedGet(nativeHandle, relationId, statementId, keyValues)
1494
- },
1495
- prepare(query) {
1496
- return native.ownedPrepare(nativeHandle, query)
1497
- },
1498
- execute(prepared, params) {
1499
- return native.ownedExecute(prepared, nativeHandle, params)
1500
- }
1501
- })
1502
- const instance: OwnedInstance<Rels> = Object.freeze({
1503
- ...methods,
1504
- [Symbol.dispose](): void {
1505
- if (rec.spent) {
1506
- return
1507
- }
1508
- try {
1509
- native.ownedInstanceClose(nativeHandle)
1510
- } catch (caught) {
1511
- const error = errorFromThrow(caught)
1512
- if (LEASED_FOR_PUBLISH.test(error.message)) {
1513
- throw errors.wrap(ErrSpentHandle, "bumbledb owned instance is leased for publish")
416
+ // Compound acquisition (TS-003): register the directory owner and
417
+ // its finalizer BEFORE any interruptible child-open step.
418
+ const directory = yield* Effect.acquireRelease(
419
+ nativeOperationWith(
420
+ operation,
421
+ (callback) => runtimeNative.runtimeDirectoryAcquire(runtime, wire, path, callback),
422
+ runtimeNative.runtimeDirectoryTake,
423
+ (value) => value
424
+ ),
425
+ (dir) =>
426
+ releaseOwner(`${operation}.directoryClose`, (callback) =>
427
+ runtimeNative.runtimeDirectoryClose(dir, false, callback)
428
+ ),
429
+ { interruptible: true }
430
+ )
431
+ return yield* Effect.acquireRelease(
432
+ Effect.gen(function* () {
433
+ const outcome = yield* nativeOperationWith(
434
+ operation,
435
+ (callback) => runtimeNative.runtimeDirectoryDbOpen(directory, wire, CHILD, spec, create, callback),
436
+ runtimeNative.runtimeDbTake,
437
+ (value) => value
438
+ ).pipe(
439
+ Effect.catch((error) =>
440
+ drainClose(`${operation}.directoryClose`, (callback) =>
441
+ runtimeNative.runtimeDirectoryClose(directory, false, callback)
442
+ ).pipe(Effect.andThen(Effect.fail(error)))
443
+ )
444
+ )
445
+ if (outcome.tag !== "accepted") {
446
+ yield* drainClose(`${operation}.directoryClose`, (callback) =>
447
+ runtimeNative.runtimeDirectoryClose(directory, false, callback)
448
+ )
449
+ return yield* Effect.fail(refusal(operation, "InvalidArgument"))
1514
450
  }
1515
- throw errors.wrap(error, "close bumbledb owned instance")
1516
- }
1517
- rec.spent = true
1518
- ownedReclaimer.unregister(instance)
1519
- }
1520
- })
1521
- ownedRecords.set(instance, rec)
1522
- ownedReclaimer.register(instance, nativeHandle, instance)
1523
- return instance
1524
- }
1525
-
1526
- function wrapBuilder<Rels extends SchemaRelations>(
1527
- nativeHandle: BuilderHandle,
1528
- theory: Schema<Rels>
1529
- ): InstanceBuilder<Rels> {
1530
- const rec = { handle: nativeHandle, theory, spent: false }
1531
- const tables = tablesFromTheory(theory)
1532
- function assertLive(): void {
1533
- if (rec.spent) {
1534
- throw errors.wrap(ErrSpentHandle, "bumbledb instance builder has been spent")
1535
- }
1536
- }
1537
- const overlay = overlayMethods(theory, tables, assertLive, {
1538
- contains(relationId, row) {
1539
- return bridged("bumbledb builder contains", function readContains() {
1540
- return native.instanceBuilderContains(nativeHandle, relationId, row)
1541
- })
1542
- },
1543
- get(relationId, statementId, key) {
1544
- return bridged("bumbledb builder get", function readGet() {
1545
- return native.instanceBuilderGet(nativeHandle, relationId, statementId, key)
1546
- })
1547
- }
1548
- })
1549
- const builder: InstanceBuilder<Rels> = Object.freeze({
1550
- load<R extends MemberRelation<Rels>>(relation: R, facts: CollectionWrite<R>): MutationReport {
1551
- assertLive()
1552
- const entry = ordinaryEntry(tables, theory, relation)
1553
- return mutateCollection(relation, facts, function applyCells(rows, cells) {
1554
- return bridged("bumbledb builder load", function loadCells() {
1555
- return native.instanceBuilderLoad(nativeHandle, entry.id, rows, cells)
1556
- })
1557
- })
1558
- },
1559
- delete<R extends MemberRelation<Rels>>(relation: R, facts: Iterable<Fact<R>>): MutationReport {
1560
- assertLive()
1561
- const entry = ordinaryEntry(tables, theory, relation)
1562
- const flat = rowsOf(relation, facts)
1563
- const report = bridged("bumbledb builder delete", function remove() {
1564
- return native.instanceBuilderDelete(nativeHandle, entry.id, flat.rows, flat.cells)
1565
- })
1566
- return Object.freeze({ submitted: report.submitted, changed: report.changed })
1567
- },
1568
- reserve<R extends MemberRelation<Rels>>(relation: R, field: FreshKeys<R> & string, count: bigint): FreshRange {
1569
- assertLive()
1570
- const entry = ordinaryEntry(tables, theory, relation)
1571
- const declared = relation.data.fields.find(function byName(candidate) {
1572
- return candidate.name === field
1573
- })
1574
- if (declared === undefined || !isFreshField(declared.field)) {
1575
- throw errors.new(`relation ${relation.name}: field ${field} is not a fresh cell`)
1576
- }
1577
- const fieldId = entry.fieldIds.get(field)
1578
- if (fieldId === undefined) {
1579
- throw errors.new(`bumbledb manifest drift: relation ${relation.name} has no field id for ${field}`)
1580
- }
1581
- const range = bridged("bumbledb builder reserve", function mint() {
1582
- return native.instanceBuilderReserve(nativeHandle, entry.id, fieldId, count)
1583
- })
1584
- return freshRangeOf(range)
1585
- },
1586
- contains: overlay.contains,
1587
- get: overlay.get,
1588
- async admit(): Promise<Admission<Rels, OwnedInstance<Rels>>> {
1589
- if (rec.spent) {
1590
- throw errors.wrap(ErrSpentHandle, "bumbledb instance builder has been spent")
1591
- }
1592
- rec.spent = true
1593
- builderReclaimer.unregister(builder)
1594
- let outcome: AdmitResult
1595
- try {
1596
- outcome = await native.instanceBuilderAdmit(nativeHandle)
1597
- } catch (caught) {
1598
- throw errors.wrap(errorFromThrow(caught), "admit bumbledb instance")
1599
- }
1600
- if (outcome.tag === "rejected") {
1601
- return Object.freeze({
1602
- tag: "rejected" as const,
1603
- violations: Object.freeze(
1604
- outcome.violations.map(function mapWire(wire) {
1605
- return mapViolationWithoutStore<Rels>(theory, wire)
1606
- })
451
+ const state: DbState = { theory: schema, db: outcome.db, directory, schemaId: compiled.schemaId }
452
+ const db = makeDb(schema, state)
453
+ dbStates.set(db, state)
454
+ return db
455
+ }),
456
+ (db) =>
457
+ Effect.suspend(() => {
458
+ const state = dbStates.get(db)
459
+ if (state === undefined) {
460
+ return Effect.void
461
+ }
462
+ return releaseOwner("Db.close", (callback) => runtimeNative.runtimeManagedDbClose(state.db, callback)).pipe(
463
+ Effect.ensuring(
464
+ releaseOwner("Db.directoryClose", (callback) =>
465
+ runtimeNative.runtimeDirectoryClose(state.directory, false, callback)
466
+ )
467
+ )
1607
468
  )
1608
- })
1609
- }
1610
- return Object.freeze({
1611
- tag: "accepted" as const,
1612
- value: wrapOwned(outcome.value, theory)
1613
- })
1614
- },
1615
- [Symbol.dispose](): void {
1616
- if (rec.spent) {
1617
- return
1618
- }
1619
- rec.spent = true
1620
- builderReclaimer.unregister(builder)
1621
- bridged("close bumbledb instance builder", function closeBuilder() {
1622
- native.instanceBuilderClose(nativeHandle)
1623
- })
1624
- }
469
+ }),
470
+ { interruptible: true }
471
+ )
1625
472
  })
1626
- builderRecords.set(builder, rec)
1627
- builderReclaimer.register(builder, nativeHandle, builder)
1628
- return builder
1629
473
  }
1630
474
 
1631
- const InstanceBuilder = Object.freeze({
1632
- create<Rels extends SchemaRelations>(theory: Schema<Rels>): InstanceBuilder<Rels> {
1633
- const spec = lower(theory)
1634
- const handle = bridged("create bumbledb instance builder", function make() {
1635
- return native.instanceBuilderNew(spec)
1636
- })
1637
- return wrapBuilder(handle, theory)
1638
- }
1639
- })
475
+ const dbStates = new WeakMap<object, DbState>()
1640
476
 
1641
477
  /**
1642
- * The store lifecycle — `Db.create(path, schema)` / `Db.open(path, schema)`.
1643
- * Create refuses an already-initialized directory; open verifies format
1644
- * version and the schema fingerprint. A second live handle on the same
1645
- * path is the engine's `EnvironmentLocked`. There is no close anywhere:
1646
- * the process owns the environment until GC/exit (durability is the
1647
- * engine's per-commit fsync). Resume = reopen in a fresh process, or
1648
- * hold the `Db` this process opened.
478
+ * `Db.create` is the explicit constructor and refuses existing authority;
479
+ * `Db.open` of a missing or unreadable database never creates an empty
480
+ * replacement (chapter 30). Both compile the schema through the same
481
+ * implementation as `Schema.compile` prior compilation is optional.
1649
482
  */
1650
483
  const Db = Object.freeze({
1651
- async create<Rels extends SchemaRelations>(
1652
- storePath: string,
1653
- theory: Schema<Rels>
1654
- ): Promise<Admission<Rels, Db<Rels>>> {
1655
- return createStore(storePath, theory)
484
+ create<S extends AnySchema>(path: string, schema: S, work: ExecutionPolicy) {
485
+ return openDatabase("Db.create", path, schema, work, true)
1656
486
  },
1657
-
1658
- async open<Rels extends SchemaRelations>(storePath: string, theory: Schema<Rels>): Promise<Db<Rels>> {
1659
- return openStore(storePath, theory)
1660
- },
1661
- async fromInstance<Rels extends SchemaRelations>(
1662
- storePath: string,
1663
- instance: OwnedInstance<Rels>
1664
- ): Promise<Db<Rels>> {
1665
- const rec = ownedRecords.get(instance)
1666
- if (rec === undefined) {
1667
- throw errors.wrap(ErrSpentHandle, "bumbledb fromInstance target is not an owned instance of this SDK")
1668
- }
1669
- if (rec.spent) {
1670
- throw errors.wrap(ErrSpentHandle, "bumbledb fromInstance target has been disposed")
1671
- }
1672
- const canonical = path.resolve(storePath)
1673
- const dbHandle = await bridgedAsync(`publish bumbledb instance at ${canonical}`, function publish() {
1674
- return native.dbFromInstance(canonical, rec.handle)
1675
- })
1676
- return openFromHandle(dbHandle, rec.theory as Schema<Rels>)
487
+ open<S extends AnySchema>(path: string, schema: S, work: ExecutionPolicy) {
488
+ return openDatabase("Db.open", path, schema, work, false)
1677
489
  }
1678
490
  })
1679
491
 
1680
- export type {
1681
- Abandon,
1682
- AbandonedArm,
1683
- Admission,
1684
- CapacityViolation,
1685
- CollectionWrite,
1686
- Committed,
1687
- ContainmentViolation,
1688
- DeclaredKeyFact,
1689
- DeclaredKeyViolation,
1690
- DeltaBuild,
1691
- FreshRange,
1692
- ImpliedKeyViolation,
1693
- MemberRelation,
1694
- MirrorViolation,
1695
- MutationReport,
1696
- OffendingFact,
1697
- OwnedInstance,
1698
- Prepared,
1699
- ReadInstance,
1700
- SyncResult,
1701
- Violation,
1702
- Witness,
1703
- WriteFromOutcome,
1704
- WriteOutcome,
1705
- WriteTx
1706
- }
1707
- export {
1708
- abandon,
1709
- Db,
1710
- ErrAsyncCallback,
1711
- ErrFingerprintMismatch,
1712
- ErrForeignPrepared,
1713
- ErrForeignWitness,
1714
- ErrIrError,
1715
- ErrNewtypeMismatch,
1716
- ErrSchemaError,
1717
- ErrSpentHandle,
1718
- ErrUseAfterScope,
1719
- InstanceBuilder
492
+ /**
493
+ * Private log-integration seam (C10): wraps a PUBLISHED core snapshot
494
+ * handle — minted by the internal log machine's native open/snapshot verbs
495
+ * — in the exact core `QueryReader` plus the scoped session acquisition
496
+ * (`PublishedSnapshot extends QueryReader` in chapter 35; the log adds
497
+ * identity/stamps/freshness AROUND this capability, never a second reader).
498
+ * The argument is the log package's branded handle for the same native
499
+ * registry entry, so the one cast below is a cross-package respelling of
500
+ * one native capability — the native side re-judges kind/generation/owner
501
+ * on every verb, so a forged object refuses there, typed.
502
+ */
503
+ function internalPublishedReader<S extends AnySchema>(
504
+ core: object,
505
+ theory: S
506
+ ): {
507
+ readonly get: QueryReader<S>["get"]
508
+ readonly execute: QueryReader<S>["execute"]
509
+ readonly session: (work: ExecutionPolicy) => Effect.Effect<ExecutionSession<S>, DbError, Scope.Scope>
510
+ } {
511
+ const state: SnapshotState<S> = { theory, handle: core as SnapshotHandle }
512
+ return Object.freeze({
513
+ get<R extends Rel<S>>(relation: R, key: Key<R>, work: ExecutionPolicy) {
514
+ return getOn(state, relation, key, work)
515
+ },
516
+ execute<P extends ParamsRecord, A>(queryValue: QueryTemplate<S, P, A>, params: P, work: ExecutionPolicy) {
517
+ return executeOn<S, A>(state, undefined, queryValue as AnyQuery, params, work)
518
+ },
519
+ session(work: ExecutionPolicy) {
520
+ return Effect.acquireRelease(
521
+ nativeOperationWith(
522
+ "Snapshot.session",
523
+ (callback) => dbNative.runtimeSnapshotSession(state.handle, policyWire(work, "Snapshot.session"), callback),
524
+ dbNative.runtimeSessionTake,
525
+ (value) => makeSession(state, value)
526
+ ),
527
+ (session) =>
528
+ Effect.suspend(() =>
529
+ releaseOwner("ExecutionSession.close", (callback) =>
530
+ dbNative.runtimeSessionClose(sessionHandles.get(session) ?? missingSession(), callback)
531
+ )
532
+ ),
533
+ { interruptible: true }
534
+ )
535
+ }
536
+ })
1720
537
  }
538
+
539
+ export type {
540
+ ApplyExpected,
541
+ ApplyOptions,
542
+ ApplyOutcome,
543
+ CoreWitness,
544
+ DbInspection,
545
+ ExecutionSession,
546
+ QueryReader,
547
+ Snapshot
548
+ }
549
+ export { Db, internalPublishedReader }