@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/changes.ts ADDED
@@ -0,0 +1,361 @@
1
+ import type { Scope } from "effect"
2
+ import { Effect, Exit } from "effect"
3
+ import { drainClose, releaseOwner } from "#close.ts"
4
+ import type { SchemaId } from "#compile.ts"
5
+ import { Schema as CoreSchema, schemaTables } from "#compile.ts"
6
+ import type { ChangesHandle, DraftHandle } from "#db-native.ts"
7
+ import { dbNative } from "#db-native.ts"
8
+ import { lower } from "#lower.ts"
9
+ import type { AnyRelation, Fact } from "#relation.ts"
10
+ import type { CellValue } from "#rows.ts"
11
+ import { assertHostCellFits, cellOf, recordOf } from "#rows.ts"
12
+ import type { ExecutionPolicy } from "#runtime.ts"
13
+ import { nativeOperationWith, policyWire, runtimeHandle } from "#runtime.ts"
14
+ import type { CloseReport } from "#runtime-errors.ts"
15
+ import { DbError } from "#runtime-errors.ts"
16
+ import type { AnySchema } from "#schema.ts"
17
+ import type { Rel } from "#shape.ts"
18
+
19
+ /**
20
+ * `ChangeSet` — the public name of the engine's checked immutable delta
21
+ * (chapter 30/34): schema-fingerprint-bound canonical native bytes with
22
+ * one-command `(add, remove ∖ add)` normalization and exact same-fact
23
+ * add-wins. Immutable and reusable while open; sealing/submitting it later
24
+ * retains the SAME native value — no second JS row walk ever happens.
25
+ */
26
+ interface ChangeSet<S extends AnySchema> {
27
+ readonly schemaId: SchemaId
28
+ /** Phantom schema brand: a ChangeSet is only ever applied to its own S. */
29
+ readonly schema?: S
30
+ close(): Effect.Effect<CloseReport>
31
+ }
32
+
33
+ /**
34
+ * `ChangeDraft` — the scoped, database-free construction capability.
35
+ * Every method constructs a LAZY effect; execution reads the then-current
36
+ * iterable and charges its work again on every sequential rerun (no hidden
37
+ * memoization, no automatic retry, no iterator replay). Input must stay
38
+ * stable from an ingestion effect's execution start through its Exit;
39
+ * after successful ingestion the accepted native bytes are independent.
40
+ * Failure/interruption SPENDS the draft and initiates tracked drain.
41
+ * Concurrent/reentrant construction refuses and spends/drains the draft.
42
+ * `finish` consumes the draft; later ingestion or a second finish refuses
43
+ * through the spent capability state.
44
+ */
45
+ interface ChangeDraft<S extends AnySchema> {
46
+ insert<R extends Rel<S>>(relation: R, rows: Iterable<Fact<R>>): Effect.Effect<void, DbError>
47
+ delete<R extends Rel<S>>(relation: R, rows: Iterable<Fact<R>>): Effect.Effect<void, DbError>
48
+ finish(): Effect.Effect<ChangeSet<S>, DbError, Scope.Scope>
49
+ close(): Effect.Effect<CloseReport>
50
+ }
51
+
52
+ /**
53
+ * Host-copy granularity: one bounded host-to-native message per chunk, so
54
+ * real event-loop turns happen between chunks (each chunk completes through
55
+ * a native callback, not a microtask chain). These are converter
56
+ * granularity bounds, not database-size policy — the draft's aggregate
57
+ * input/working/spill budget is charged natively and is CUMULATIVE across
58
+ * calls and chunks (chunks never reset it).
59
+ */
60
+ const CHUNK_BYTES = 65536n
61
+ const CHUNK_ROWS = 4096
62
+
63
+ interface DraftState {
64
+ readonly handle: DraftHandle
65
+ readonly policy: ExecutionPolicy
66
+ readonly theory: AnySchema
67
+ spent: boolean
68
+ inFlight: boolean
69
+ }
70
+
71
+ interface ChangesInternal {
72
+ readonly handle: ChangesHandle
73
+ readonly schemaId: SchemaId
74
+ closed: boolean
75
+ }
76
+
77
+ const changesInternals = new WeakMap<object, ChangesInternal>()
78
+
79
+ /** Private cross-module accessor (db.apply retains the native ChangeSet). */
80
+ function internalChanges(value: object): ChangesInternal | undefined {
81
+ return changesInternals.get(value)
82
+ }
83
+
84
+ function refusal(operation: string, reason: "SpentHandle" | "ClosedHandle" | "InvalidArgument"): DbError {
85
+ return new DbError({ operation, reason: { _tag: reason } })
86
+ }
87
+
88
+ interface Chunk {
89
+ readonly rows: bigint
90
+ readonly cells: readonly CellValue[]
91
+ readonly done: boolean
92
+ readonly leftover: object | undefined
93
+ }
94
+
95
+ function eventLoopTurn(): Effect.Effect<void> {
96
+ return Effect.callback<void>((resume) => {
97
+ const id = setImmediate(() => resume(Effect.void))
98
+ return Effect.sync(() => clearImmediate(id))
99
+ })
100
+ }
101
+
102
+ function hostFactCharge(relation: AnyRelation, record: Readonly<Record<string, unknown>>): bigint {
103
+ const data = relation.data
104
+ let bytes = 0n
105
+ for (const declared of data.fields) {
106
+ const value = record[declared.name]
107
+ if (value === undefined) {
108
+ throw refusal("ChangeDraft.ingest", "InvalidArgument")
109
+ }
110
+ bytes += assertHostCellFits(`relation ${data.name} field ${declared.name}`, value, CHUNK_BYTES)
111
+ }
112
+ return bytes
113
+ }
114
+
115
+ function projectFact(relation: AnyRelation, record: Readonly<Record<string, unknown>>): readonly CellValue[] {
116
+ const data = relation.data
117
+ const cells: CellValue[] = []
118
+ for (const declared of data.fields) {
119
+ const value = record[declared.name]
120
+ if (value === undefined) {
121
+ throw refusal("ChangeDraft.ingest", "InvalidArgument")
122
+ }
123
+ cells.push(cellOf(`relation ${data.name} field ${declared.name}`, declared.field, value))
124
+ }
125
+ return cells
126
+ }
127
+
128
+ /**
129
+ * Pulls one bounded chunk off the caller's iterator. Host length is judged
130
+ * before any string scan or byte copy. A leftover fact that does not fit
131
+ * the current chunk is returned unconverted for the next turn.
132
+ */
133
+ function pullChunk(relation: AnyRelation, iterator: Iterator<object>, pending: object | undefined): Chunk {
134
+ const cells: CellValue[] = []
135
+ let rows = 0n
136
+ let bytes = 0n
137
+ let leftover: object | undefined
138
+ let current: object | undefined = pending
139
+ while (rows < BigInt(CHUNK_ROWS) && leftover === undefined) {
140
+ if (current === undefined) {
141
+ const next = iterator.next()
142
+ if (next.done === true) {
143
+ return { rows, cells, done: true, leftover: undefined }
144
+ }
145
+ current = next.value
146
+ }
147
+ const record = recordOf(current)
148
+ const charge = hostFactCharge(relation, record)
149
+ if (rows > 0n && bytes + charge > CHUNK_BYTES) {
150
+ leftover = current
151
+ break
152
+ }
153
+ cells.push(...projectFact(relation, record))
154
+ bytes += charge
155
+ rows += 1n
156
+ current = undefined
157
+ }
158
+ return { rows, cells, done: false, leftover }
159
+ }
160
+
161
+ function spendAndDrain(state: DraftState, operation: string): Effect.Effect<void> {
162
+ return Effect.suspend(() => {
163
+ if (state.spent) {
164
+ return Effect.void
165
+ }
166
+ state.spent = true
167
+ // Tracked drain: join the native close transition; the report is
168
+ // diagnostic here (the ingestion failure itself is the caller's
169
+ // error), but native Closing accounting is never dropped.
170
+ return drainClose(operation, (callback) => dbNative.runtimeDraftClose(state.handle, callback)).pipe(Effect.asVoid)
171
+ })
172
+ }
173
+
174
+ function ingest(
175
+ state: DraftState,
176
+ operation: "ChangeDraft.insert" | "ChangeDraft.delete",
177
+ relation: AnyRelation,
178
+ rows: Iterable<object>
179
+ ): Effect.Effect<void, DbError> {
180
+ const verb = operation === "ChangeDraft.insert" ? dbNative.runtimeDraftInsert : dbNative.runtimeDraftDelete
181
+ return Effect.gen(function* () {
182
+ if (state.spent) {
183
+ return yield* Effect.fail(refusal(operation, "SpentHandle"))
184
+ }
185
+ if (state.inFlight) {
186
+ // Reentrant construction refuses AND spends/drains — there is
187
+ // no implicit queue (chapter 35).
188
+ yield* spendAndDrain(state, operation)
189
+ return yield* Effect.fail(refusal(operation, "SpentHandle"))
190
+ }
191
+ if (state.theory.relations[relation.name] !== relation) {
192
+ return yield* Effect.fail(refusal(operation, "InvalidArgument"))
193
+ }
194
+ const tables = schemaTables(state.theory)
195
+ const relationId = tables.relationIds.get(relation.name)
196
+ if (relationId === undefined) {
197
+ return yield* Effect.fail(refusal(operation, "InvalidArgument"))
198
+ }
199
+ state.inFlight = true
200
+ const wire = yield* Effect.try({
201
+ try: () => policyWire(state.policy, operation),
202
+ catch: () => refusal(operation, "InvalidArgument")
203
+ })
204
+ const body = Effect.gen(function* () {
205
+ const iterator = rows[Symbol.iterator]()
206
+ let leftover: object | undefined
207
+ let done = false
208
+ while (!done) {
209
+ const chunk = yield* Effect.try({
210
+ try: () => pullChunk(relation, iterator, leftover),
211
+ catch: (cause) => (cause instanceof DbError ? cause : refusal(operation, "InvalidArgument"))
212
+ }).pipe(Effect.catch((error) => spendAndDrain(state, operation).pipe(Effect.andThen(Effect.fail(error)))))
213
+ leftover = chunk.leftover
214
+ done = chunk.done && leftover === undefined
215
+ if (chunk.rows === 0n) {
216
+ continue
217
+ }
218
+ yield* eventLoopTurn()
219
+ yield* nativeOperationWith(
220
+ operation,
221
+ (callback) => verb(state.handle, wire, relationId, chunk.rows, chunk.cells, callback),
222
+ dbNative.runtimeReportTake,
223
+ () => undefined
224
+ ).pipe(
225
+ Effect.catch((error) =>
226
+ Effect.sync(() => {
227
+ state.spent = true
228
+ }).pipe(Effect.andThen(Effect.fail(error)))
229
+ )
230
+ )
231
+ }
232
+ })
233
+ return yield* Effect.onExit(body, (exit) => {
234
+ state.inFlight = false
235
+ if (state.spent || Exit.isSuccess(exit)) {
236
+ return Effect.void
237
+ }
238
+ return spendAndDrain(state, operation)
239
+ })
240
+ })
241
+ }
242
+
243
+ function makeChangeSet<S extends AnySchema>(handle: ChangesHandle, schemaId: SchemaId): ChangeSet<S> {
244
+ const value: ChangeSet<S> = {
245
+ schemaId,
246
+ close() {
247
+ return drainClose("ChangeSet.close", (callback) => dbNative.runtimeChangesClose(handle, callback))
248
+ }
249
+ }
250
+ Object.freeze(value)
251
+ changesInternals.set(value, { handle, schemaId, closed: false })
252
+ return value
253
+ }
254
+
255
+ function makeDraft<S extends AnySchema>(state: DraftState, schemaId: SchemaId): ChangeDraft<S> {
256
+ const draft: ChangeDraft<S> = {
257
+ insert(relation, rows) {
258
+ return ingest(state, "ChangeDraft.insert", relation, rows)
259
+ },
260
+ delete(relation, rows) {
261
+ return ingest(state, "ChangeDraft.delete", relation, rows)
262
+ },
263
+ finish() {
264
+ return Effect.acquireRelease(
265
+ Effect.gen(function* () {
266
+ if (state.spent) {
267
+ return yield* Effect.fail(refusal("ChangeDraft.finish", "SpentHandle"))
268
+ }
269
+ if (state.inFlight) {
270
+ yield* spendAndDrain(state, "ChangeDraft.finish")
271
+ return yield* Effect.fail(refusal("ChangeDraft.finish", "SpentHandle"))
272
+ }
273
+ // Finish CONSUMES the draft, success or failure.
274
+ state.spent = true
275
+ const wire = yield* nativeOperationWith(
276
+ "ChangeDraft.finish",
277
+ (callback) =>
278
+ dbNative.runtimeDraftFinish(state.handle, policyWire(state.policy, "ChangeDraft.finish"), callback),
279
+ dbNative.runtimeChangesTake,
280
+ (value) => value
281
+ )
282
+ return makeChangeSet<S>(wire.changes, schemaId)
283
+ }),
284
+ (changes) =>
285
+ Effect.suspend(() => {
286
+ const internal = changesInternals.get(changes)
287
+ if (internal === undefined || internal.closed) {
288
+ return Effect.void
289
+ }
290
+ internal.closed = true
291
+ return releaseOwner("ChangeSet.close", (callback) =>
292
+ dbNative.runtimeChangesClose(internal.handle, callback)
293
+ )
294
+ }),
295
+ { interruptible: true }
296
+ )
297
+ },
298
+ close() {
299
+ return Effect.suspend(() => {
300
+ state.spent = true
301
+ return drainClose("ChangeDraft.close", (callback) => dbNative.runtimeDraftClose(state.handle, callback))
302
+ })
303
+ }
304
+ }
305
+ return Object.freeze(draft)
306
+ }
307
+
308
+ /**
309
+ * `ChangeSet.builder(schema, work)` — lazy scoped acquisition of a
310
+ * database-free draft (chapter 35 roster). Requires the acquired
311
+ * `NativeRuntime`; the draft's native resources release with its scope, and
312
+ * the scope finalizer surfaces incomplete/failed teardown as a
313
+ * `CloseFailure` defect.
314
+ */
315
+ const builder = Effect.fn("ChangeSet.builder")(function* <S extends AnySchema>(schema: S, work: ExecutionPolicy) {
316
+ const handle = yield* runtimeHandle()
317
+ const compiled = yield* CoreSchema.compile(schema, work)
318
+ const spec = lower(schema)
319
+ return yield* Effect.acquireRelease(
320
+ Effect.gen(function* () {
321
+ const draftHandle = yield* nativeOperationWith(
322
+ "ChangeSet.builder",
323
+ (callback) => dbNative.runtimeDraftOpen(handle, policyWire(work, "ChangeSet.builder"), spec, callback),
324
+ dbNative.runtimeDraftTake,
325
+ (value) => value
326
+ )
327
+ const state: DraftState = {
328
+ handle: draftHandle,
329
+ policy: work,
330
+ theory: schema,
331
+ spent: false,
332
+ inFlight: false
333
+ }
334
+ const draft = makeDraft<S>(state, compiled.schemaId)
335
+ draftStates.set(draft, state)
336
+ return draft
337
+ }),
338
+ (draft) =>
339
+ Effect.suspend(() => {
340
+ const state = draftStates.get(draft)
341
+ if (state === undefined) {
342
+ return Effect.void
343
+ }
344
+ // Idempotent: repeated close joins the same native
345
+ // transition; the finalizer runs it unconditionally so an
346
+ // abandoned draft is always drained.
347
+ state.spent = true
348
+ return releaseOwner("ChangeDraft.close", (callback) => dbNative.runtimeDraftClose(state.handle, callback))
349
+ }),
350
+ { interruptible: true }
351
+ )
352
+ })
353
+
354
+ // The draft value → state registry lets the scope finalizer reach the
355
+ // native handle without exposing it on the public capability.
356
+ const draftStates = new WeakMap<object, DraftState>()
357
+
358
+ const ChangeSet = Object.freeze({ builder })
359
+
360
+ export type { ChangeDraft }
361
+ export { ChangeSet, internalChanges }
package/src/close.ts ADDED
@@ -0,0 +1,53 @@
1
+ import { Effect } from "effect"
2
+ import { finalizeClose } from "#runtime.ts"
3
+ import type { CloseReport } from "#runtime-errors.ts"
4
+ import { DbError } from "#runtime-errors.ts"
5
+ /**
6
+ * Shared close/drain adapters for scoped core owners (drafts, changes,
7
+ * snapshots, sessions, results, cursors, databases). One policy, chapter
8
+ * 35's: early `close()` starts/joins the native close transition and
9
+ * returns the honest {@link CloseReport}; a scope FINALIZER runs the same
10
+ * close and surfaces `incomplete`/`failed` as a structured `CloseFailure`
11
+ * DEFECT in the finalizer Cause — never catch-and-log, never false
12
+ * quiescence. Teardown uses the runtime's reserved cleanup envelope
13
+ * natively; repeated close joins the same stored transition (idempotent).
14
+ */
15
+ import type { CloseWire } from "#runtime-native.ts"
16
+
17
+ function reportOf(operation: string, wire: CloseWire): CloseReport {
18
+ if (wire.kind === "failed") {
19
+ return { kind: "failed", error: new DbError({ operation, reason: { _tag: "Internal" } }) }
20
+ }
21
+ return wire
22
+ }
23
+
24
+ /**
25
+ * Adapts one native close verb to `Effect<CloseReport>`: registration is
26
+ * synchronous, completion resumes exactly once, and the whole wait is
27
+ * uninterruptible — a close is a bounded registration/drain handshake, not
28
+ * long maskable work (the native side owns the cleanup deadline).
29
+ */
30
+ function drainClose(
31
+ operation: string,
32
+ start: (callback: (report: CloseWire) => void) => void
33
+ ): Effect.Effect<CloseReport> {
34
+ return Effect.callback<CloseReport>((resume) => {
35
+ try {
36
+ start((report) => resume(Effect.succeed(reportOf(operation, report))))
37
+ } catch (cause) {
38
+ resume(
39
+ Effect.succeed({
40
+ kind: "failed",
41
+ error: cause instanceof DbError ? cause : new DbError({ operation, reason: { _tag: "Internal" } })
42
+ })
43
+ )
44
+ }
45
+ }).pipe(Effect.uninterruptible)
46
+ }
47
+
48
+ /** The finalizer policy: run the close, then die on incomplete/failed (E stays never). */
49
+ function releaseOwner(operation: string, start: (callback: (report: CloseWire) => void) => void): Effect.Effect<void> {
50
+ return drainClose(operation, start).pipe(Effect.flatMap((report) => finalizeClose(operation, report)))
51
+ }
52
+
53
+ export { drainClose, releaseOwner }
package/src/closed.ts CHANGED
@@ -1,4 +1,4 @@
1
- import * as errors from "@superbuilders/errors"
1
+ import { AuthoringError } from "#errors.ts"
2
2
  import {
3
3
  type AnyClosedIdField,
4
4
  type AnyField,
@@ -14,7 +14,7 @@ import type { AnyRelation, RelationField } from "#relation.ts"
14
14
  import { resolveSelection, type SelectionBinding, type SelectionInput } from "#relation.ts"
15
15
  import type { LiteralSpec } from "#spec.ts"
16
16
 
17
- type PayloadField = Exclude<AnyField, { readonly fresh: true }>
17
+ type PayloadField = AnyField
18
18
 
19
19
  type PayloadColumns = Record<string, PayloadField> & { readonly id?: never }
20
20
 
@@ -178,7 +178,7 @@ function mintAxioms<Handles extends string, Cols extends Record<string, PayloadF
178
178
  }
179
179
  Object.freeze(out)
180
180
  if (!axiomsMinted<Handles, Cols>(out, handles, cols)) {
181
- throw errors.new(`closed relation ${name}: axiom-row minting incomplete`)
181
+ throw new AuthoringError({ message: `closed relation ${name}: axiom-row minting incomplete` })
182
182
  }
183
183
  return out
184
184
  }
@@ -202,23 +202,27 @@ function closed<Name extends string, Handles extends ClosedHandleTuple, Cols ext
202
202
  axioms?: Axioms<Handles[number], Cols>
203
203
  ): Closed<Name, Handles, Record<never, never>> | Closed<Name, Handles, Cols> {
204
204
  if (!Array.isArray(handles)) {
205
- throw errors.new(
206
- `closed relation ${name}: payload columns declared without ground axioms — the payload tier is spelled closed(name, handles, columns, axioms)`
207
- )
205
+ throw new AuthoringError({
206
+ message: `closed relation ${name}: payload columns declared without ground axioms — the payload tier is spelled closed(name, handles, columns, axioms)`
207
+ })
208
208
  }
209
209
  if (!isHandleTuple(handles)) {
210
- throw errors.new(`closed relation ${name}: at least one handle is required (an empty vocabulary declares nothing)`)
210
+ throw new AuthoringError({
211
+ message: `closed relation ${name}: at least one handle is required (an empty vocabulary declares nothing)`
212
+ })
211
213
  }
212
214
  if (columns === undefined) {
213
215
  if (axioms !== undefined) {
214
- throw errors.new(`closed relation ${name}: the bare tier declares no columns, so ground axioms are inadmissible`)
216
+ throw new AuthoringError({
217
+ message: `closed relation ${name}: the bare tier declares no columns, so ground axioms are inadmissible`
218
+ })
215
219
  }
216
220
  return closedBare(name, handles)
217
221
  }
218
222
  if (axioms === undefined) {
219
- throw errors.new(
220
- `closed relation ${name}: payload columns declared without ground axioms — the payload tier is spelled closed(name, handles, columns, axioms)`
221
- )
223
+ throw new AuthoringError({
224
+ message: `closed relation ${name}: payload columns declared without ground axioms — the payload tier is spelled closed(name, handles, columns, axioms)`
225
+ })
222
226
  }
223
227
  return closedPayload(name, handles, columns, axioms)
224
228
  }
@@ -236,7 +240,7 @@ function closedBare<Name extends string, const Hs extends ClosedHandleTuple>(
236
240
  }
237
241
  Object.freeze(empty)
238
242
  if (!axiomsMinted<Hs[number], Record<never, never>>(empty, handles, [])) {
239
- throw errors.new(`closed relation ${name}: bare-tier axiom-row minting incomplete`)
243
+ throw new AuthoringError({ message: `closed relation ${name}: bare-tier axiom-row minting incomplete` })
240
244
  }
241
245
  return mintClosed(name, handles, {}, empty)
242
246
  }
@@ -256,7 +260,7 @@ function closedPayload<Name extends string, Handles extends ClosedHandleTuple, C
256
260
  assertDeclarationOrderKey(`closed relation ${name} handle`, handle)
257
261
  }
258
262
  if (!handleKeysOwn(axioms, handles)) {
259
- throw errors.new(`closed relation ${name}: handle enumeration incomplete`)
263
+ throw new AuthoringError({ message: `closed relation ${name}: handle enumeration incomplete` })
260
264
  }
261
265
  return mintClosed(name, handles, columns, axioms)
262
266
  }
@@ -283,7 +287,7 @@ function mintClosed<Name extends string, Handles extends ClosedHandleTuple, Cols
283
287
  const seen = new Set<string>()
284
288
  for (const handle of handles) {
285
289
  if (seen.has(handle)) {
286
- throw errors.new(`closed relation ${name}: duplicate handle ${handle}`)
290
+ throw new AuthoringError({ message: `closed relation ${name}: duplicate handle ${handle}` })
287
291
  }
288
292
  seen.add(handle)
289
293
  }
@@ -293,9 +297,9 @@ function mintClosed<Name extends string, Handles extends ClosedHandleTuple, Cols
293
297
  for (const [columnName, field] of Object.entries(columns)) {
294
298
  assertDeclarationOrderKey(`closed relation ${name} column`, columnName)
295
299
  if (columnName === "id") {
296
- throw errors.new(
297
- `closed relation ${name}: the payload column id collides with the sealed shape's synthetic id (the relation mints its own id at ordinal 0; name the column something else)`
298
- )
300
+ throw new AuthoringError({
301
+ message: `closed relation ${name}: the payload column id collides with the sealed shape's synthetic id (the relation mints its own id at ordinal 0; name the column something else)`
302
+ })
299
303
  }
300
304
  cols.push(Object.freeze({ name: columnName, field }))
301
305
  }
@@ -323,7 +327,9 @@ function mintClosed<Name extends string, Handles extends ClosedHandleTuple, Cols
323
327
  function where(selection: ClosedSelectionInput<Cols>): SelectedClosed<Name, Handles, Cols> {
324
328
  const owner = holder.value
325
329
  if (owner === undefined) {
326
- throw errors.new(`closed relation ${name}: self-reference read before construction completed`)
330
+ throw new AuthoringError({
331
+ message: `closed relation ${name}: self-reference read before construction completed`
332
+ })
327
333
  }
328
334
  return Object.freeze({
329
335
  relation: owner,
@@ -334,7 +340,7 @@ function mintClosed<Name extends string, Handles extends ClosedHandleTuple, Cols
334
340
  const value: ClosedCore<Name, Handles, Cols> =
335
341
  cols.length > 0 ? Object.freeze({ ...core, where }) : Object.freeze(core)
336
342
  if (!surfaceMinted(value, cols)) {
337
- throw errors.new(`closed relation ${name}: ergonomic-surface minting incomplete`)
343
+ throw new AuthoringError({ message: `closed relation ${name}: ergonomic-surface minting incomplete` })
338
344
  }
339
345
  holder.value = value
340
346
  return value