@bjornpagen/bumbledb 1.0.1 → 1.2.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 (172) hide show
  1. package/COOKBOOK.md +176 -36
  2. package/README.md +70 -26
  3. package/dist/capacity.d.ts +15 -55
  4. package/dist/capacity.d.ts.map +1 -1
  5. package/dist/capacity.js +63 -12
  6. package/dist/capacity.js.map +1 -1
  7. package/dist/changes.d.ts +28 -6
  8. package/dist/changes.d.ts.map +1 -1
  9. package/dist/changes.js +119 -71
  10. package/dist/changes.js.map +1 -1
  11. package/dist/closed.d.ts +23 -51
  12. package/dist/closed.d.ts.map +1 -1
  13. package/dist/closed.js +110 -168
  14. package/dist/closed.js.map +1 -1
  15. package/dist/codec.d.ts +8 -14
  16. package/dist/codec.d.ts.map +1 -1
  17. package/dist/codec.js +113 -188
  18. package/dist/codec.js.map +1 -1
  19. package/dist/compile.d.ts +17 -18
  20. package/dist/compile.d.ts.map +1 -1
  21. package/dist/compile.js +41 -20
  22. package/dist/compile.js.map +1 -1
  23. package/dist/db-native.d.ts +76 -41
  24. package/dist/db-native.d.ts.map +1 -1
  25. package/dist/db-native.js +8 -0
  26. package/dist/db-native.js.map +1 -1
  27. package/dist/db.d.ts +61 -26
  28. package/dist/db.d.ts.map +1 -1
  29. package/dist/db.js +152 -85
  30. package/dist/db.js.map +1 -1
  31. package/dist/errors.d.ts +11 -3
  32. package/dist/errors.d.ts.map +1 -1
  33. package/dist/errors.js +13 -1
  34. package/dist/errors.js.map +1 -1
  35. package/dist/face.d.ts +19 -25
  36. package/dist/face.d.ts.map +1 -1
  37. package/dist/face.js +30 -24
  38. package/dist/face.js.map +1 -1
  39. package/dist/fields.d.ts +13 -26
  40. package/dist/fields.d.ts.map +1 -1
  41. package/dist/fields.js +89 -116
  42. package/dist/fields.js.map +1 -1
  43. package/dist/immutable.d.ts +10 -0
  44. package/dist/immutable.d.ts.map +1 -0
  45. package/dist/immutable.js +75 -0
  46. package/dist/immutable.js.map +1 -0
  47. package/dist/index.d.ts +16 -12
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +6 -4
  50. package/dist/index.js.map +1 -1
  51. package/dist/internal/log.d.ts +6 -4
  52. package/dist/internal/log.d.ts.map +1 -1
  53. package/dist/internal/log.js +3 -1
  54. package/dist/internal/log.js.map +1 -1
  55. package/dist/law.d.ts +11 -11
  56. package/dist/law.d.ts.map +1 -1
  57. package/dist/law.js +1 -1
  58. package/dist/law.js.map +1 -1
  59. package/dist/lower.d.ts.map +1 -1
  60. package/dist/lower.js +11 -6
  61. package/dist/lower.js.map +1 -1
  62. package/dist/migration.d.ts +2 -3
  63. package/dist/migration.d.ts.map +1 -1
  64. package/dist/migration.js +5 -5
  65. package/dist/migration.js.map +1 -1
  66. package/dist/native.d.ts +2 -2
  67. package/dist/native.d.ts.map +1 -1
  68. package/dist/native.js.map +1 -1
  69. package/dist/query/atom.d.ts +1 -1
  70. package/dist/query/atom.d.ts.map +1 -1
  71. package/dist/query/atom.js.map +1 -1
  72. package/dist/query/description.d.ts +33 -0
  73. package/dist/query/description.d.ts.map +1 -0
  74. package/dist/query/description.js +302 -0
  75. package/dist/query/description.js.map +1 -0
  76. package/dist/query/lower.d.ts +60 -4
  77. package/dist/query/lower.d.ts.map +1 -1
  78. package/dist/query/lower.js +126 -161
  79. package/dist/query/lower.js.map +1 -1
  80. package/dist/query/parse-ir.d.ts +7 -2
  81. package/dist/query/parse-ir.d.ts.map +1 -1
  82. package/dist/query/parse-ir.js +240 -53
  83. package/dist/query/parse-ir.js.map +1 -1
  84. package/dist/query/run.d.ts.map +1 -1
  85. package/dist/query/run.js +8 -10
  86. package/dist/query/run.js.map +1 -1
  87. package/dist/query/scope.d.ts +3 -4
  88. package/dist/query/scope.d.ts.map +1 -1
  89. package/dist/query/scope.js +5 -10
  90. package/dist/query/scope.js.map +1 -1
  91. package/dist/relation.d.ts +22 -48
  92. package/dist/relation.d.ts.map +1 -1
  93. package/dist/relation.js +30 -82
  94. package/dist/relation.js.map +1 -1
  95. package/dist/result.d.ts +10 -25
  96. package/dist/result.d.ts.map +1 -1
  97. package/dist/result.js +7 -12
  98. package/dist/result.js.map +1 -1
  99. package/dist/rows.d.ts +11 -14
  100. package/dist/rows.d.ts.map +1 -1
  101. package/dist/rows.js +49 -237
  102. package/dist/rows.js.map +1 -1
  103. package/dist/runtime-codes.d.ts +1 -1
  104. package/dist/runtime-codes.d.ts.map +1 -1
  105. package/dist/runtime-codes.js +1 -2
  106. package/dist/runtime-codes.js.map +1 -1
  107. package/dist/runtime-errors.d.ts +27 -15
  108. package/dist/runtime-errors.d.ts.map +1 -1
  109. package/dist/runtime-errors.js +44 -8
  110. package/dist/runtime-errors.js.map +1 -1
  111. package/dist/runtime-native.d.ts +16 -34
  112. package/dist/runtime-native.d.ts.map +1 -1
  113. package/dist/runtime-native.js.map +1 -1
  114. package/dist/runtime.d.ts +11 -28
  115. package/dist/runtime.d.ts.map +1 -1
  116. package/dist/runtime.js +14 -54
  117. package/dist/runtime.js.map +1 -1
  118. package/dist/schema.d.ts +9 -1
  119. package/dist/schema.d.ts.map +1 -1
  120. package/dist/schema.js +85 -24
  121. package/dist/schema.js.map +1 -1
  122. package/dist/selection.d.ts +27 -0
  123. package/dist/selection.d.ts.map +1 -0
  124. package/dist/selection.js +104 -0
  125. package/dist/selection.js.map +1 -0
  126. package/dist/shape.d.ts +6 -9
  127. package/dist/shape.d.ts.map +1 -1
  128. package/dist/statements.d.ts +17 -52
  129. package/dist/statements.d.ts.map +1 -1
  130. package/dist/statements.js +92 -111
  131. package/dist/statements.js.map +1 -1
  132. package/dist/values.d.ts +25 -0
  133. package/dist/values.d.ts.map +1 -0
  134. package/dist/values.js +203 -0
  135. package/dist/values.js.map +1 -0
  136. package/pack-provenance.json +3 -3
  137. package/package.json +4 -4
  138. package/src/capacity.ts +86 -76
  139. package/src/changes.ts +225 -94
  140. package/src/closed.ts +161 -314
  141. package/src/codec.ts +123 -213
  142. package/src/compile.ts +61 -38
  143. package/src/db-native.ts +73 -68
  144. package/src/db.ts +268 -165
  145. package/src/errors.ts +16 -1
  146. package/src/face.ts +44 -74
  147. package/src/fields.ts +100 -146
  148. package/src/immutable.ts +76 -0
  149. package/src/index.ts +38 -31
  150. package/src/internal/log.ts +4 -5
  151. package/src/law.ts +14 -14
  152. package/src/lower.ts +13 -10
  153. package/src/migration.ts +5 -12
  154. package/src/native.ts +2 -1
  155. package/src/query/atom.ts +1 -0
  156. package/src/query/description.ts +394 -0
  157. package/src/query/lower.ts +139 -173
  158. package/src/query/parse-ir.ts +247 -54
  159. package/src/query/run.ts +15 -9
  160. package/src/query/scope.ts +10 -18
  161. package/src/relation.ts +58 -142
  162. package/src/result.ts +16 -45
  163. package/src/rows.ts +57 -250
  164. package/src/runtime-codes.ts +1 -2
  165. package/src/runtime-errors.ts +48 -8
  166. package/src/runtime-native.ts +13 -46
  167. package/src/runtime.ts +51 -119
  168. package/src/schema.ts +99 -29
  169. package/src/selection.ts +124 -0
  170. package/src/shape.ts +9 -9
  171. package/src/statements.ts +138 -189
  172. package/src/values.ts +223 -0
package/src/changes.ts CHANGED
@@ -1,20 +1,22 @@
1
1
  import type { Scope } from "effect"
2
- import { Effect, Exit } from "effect"
2
+ import { Effect, Exit, Option, Stream } from "effect"
3
3
  import { drainClose, releaseOwner } from "#close.ts"
4
+ import { isClosedMember, membersAgree } from "#closed.ts"
4
5
  import type { SchemaId } from "#compile.ts"
5
6
  import { Schema as CoreSchema, schemaTables } from "#compile.ts"
6
- import type { ChangesHandle, DraftHandle } from "#db-native.ts"
7
+ import type { ChangeRecordWire, ChangesHandle, ChangesWire, DraftHandle } from "#db-native.ts"
7
8
  import { dbNative } from "#db-native.ts"
9
+ import { SdkInvariantError } from "#errors.ts"
8
10
  import { lower } from "#lower.ts"
9
- import type { AnyRelation, Fact } from "#relation.ts"
11
+ import { type AnyRelation, type Fact, relationFields } from "#relation.ts"
10
12
  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"
13
+ import { factCellsOf, factOfCells, hostCellCharge } from "#rows.ts"
14
+ import { nativeOperationWith, runtimeHandle } from "#runtime.ts"
14
15
  import type { CloseReport } from "#runtime-errors.ts"
15
- import { DbError } from "#runtime-errors.ts"
16
- import type { AnySchema } from "#schema.ts"
16
+ import { argumentError, DbError } from "#runtime-errors.ts"
17
+ import type { AnySchema, SchemaRelation } from "#schema.ts"
17
18
  import type { Rel } from "#shape.ts"
19
+ import { bytesValue, recordValue } from "#values.ts"
18
20
 
19
21
  /**
20
22
  * `ChangeSet` — the engine's checked immutable delta:
@@ -23,17 +25,39 @@ import type { Rel } from "#shape.ts"
23
25
  * add-wins. Immutable and reusable while open; sealing/submitting it later
24
26
  * retains the SAME native value — no second JS row walk ever happens.
25
27
  */
28
+ /** Counts of distinct fact additions and removals, never input events. */
29
+ interface ChangeCounts {
30
+ readonly added: bigint
31
+ readonly removed: bigint
32
+ }
33
+
34
+ /** A relation-name-discriminated union of plain, fully typed facts. */
35
+ type ChangeRecord<S extends AnySchema> = {
36
+ [N in keyof S["relations"]]: S["relations"][N] extends AnyRelation
37
+ ? { readonly relation: N; readonly kind: "add" | "remove"; readonly fact: Fact<S["relations"][N]> }
38
+ : never
39
+ }[keyof S["relations"]]
40
+
26
41
  interface ChangeSet<S extends AnySchema> {
27
42
  readonly schemaId: SchemaId
43
+ /** Distinct requested actions; use judge for effective counts against a store. */
44
+ readonly counts: ChangeCounts
45
+ readonly byteLength: bigint
28
46
  /** Phantom schema brand: a ChangeSet is only ever applied to its own S. */
29
47
  readonly schema?: S
48
+ /** Independent scoped traversal, delivered in bounded native batches. Reusable. */
49
+ records(): Stream.Stream<ChangeRecord<S>, DbError>
50
+ /** Explicit full byte materialization. Mutating returned bytes cannot alter this value. */
51
+ toBytes(): Effect.Effect<Uint8Array, DbError>
52
+ /** Commutative add-wins composition into one command, not sequential replay. */
53
+ compose(other: ChangeSet<S>): Effect.Effect<ChangeSet<S>, DbError, Scope.Scope>
30
54
  close(): Effect.Effect<CloseReport>
31
55
  }
32
56
 
33
57
  /**
34
58
  * `ChangeDraft` — the scoped, database-free construction capability.
35
59
  * Every method constructs a LAZY effect; execution reads the then-current
36
- * iterable and charges its work again on every sequential rerun (no hidden
60
+ * iterable again on every sequential rerun (no hidden
37
61
  * memoization, no automatic retry, no iterator replay). Input must stay
38
62
  * stable from an ingestion effect's execution start through its Exit;
39
63
  * after successful ingestion the accepted native bytes are independent.
@@ -53,16 +77,14 @@ interface ChangeDraft<S extends AnySchema> {
53
77
  * Host-copy granularity: one bounded host-to-native message per chunk, so
54
78
  * real event-loop turns happen between chunks (each chunk completes through
55
79
  * 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).
80
+ * granularity targets, not database-size limits. One larger row travels
81
+ * alone; otherwise native callbacks provide backpressure between batches.
59
82
  */
60
83
  const CHUNK_BYTES = 65536n
61
84
  const CHUNK_ROWS = 4096
62
85
 
63
86
  interface DraftState {
64
87
  readonly handle: DraftHandle
65
- readonly policy: ExecutionPolicy
66
88
  readonly theory: AnySchema
67
89
  spent: boolean
68
90
  inFlight: boolean
@@ -100,31 +122,15 @@ function eventLoopTurn(): Effect.Effect<void> {
100
122
  }
101
123
 
102
124
  function hostFactCharge(relation: AnyRelation, record: Readonly<Record<string, unknown>>): bigint {
103
- const data = relation.data
125
+ const fields = relationFields(relation)
104
126
  let bytes = 0n
105
- for (const declared of data.fields) {
127
+ for (const declared of fields) {
106
128
  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)
129
+ bytes += hostCellCharge(value)
111
130
  }
112
131
  return bytes
113
132
  }
114
133
 
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
134
  /**
129
135
  * Pulls one bounded chunk off the caller's iterator. Host length is judged
130
136
  * before any string scan or byte copy. A leftover fact that does not fit
@@ -144,13 +150,17 @@ function pullChunk(relation: AnyRelation, iterator: Iterator<object>, pending: o
144
150
  }
145
151
  current = next.value
146
152
  }
147
- const record = recordOf(current)
153
+ const record = recordValue(
154
+ `relation ${relation.name}`,
155
+ current,
156
+ relationFields(relation).map((field) => field.name)
157
+ )
148
158
  const charge = hostFactCharge(relation, record)
149
159
  if (rows > 0n && bytes + charge > CHUNK_BYTES) {
150
160
  leftover = current
151
161
  break
152
162
  }
153
- cells.push(...projectFact(relation, record))
163
+ for (const cell of factCellsOf(relation, record)) cells.push(cell)
154
164
  bytes += charge
155
165
  rows += 1n
156
166
  current = undefined
@@ -188,7 +198,7 @@ function ingest(
188
198
  yield* spendAndDrain(state, operation)
189
199
  return yield* Effect.fail(refusal(operation, "SpentHandle"))
190
200
  }
191
- if (state.theory.relations[relation.name] !== relation) {
201
+ if (!membersAgree(state.theory.relations[relation.name], relation)) {
192
202
  return yield* Effect.fail(refusal(operation, "InvalidArgument"))
193
203
  }
194
204
  const tables = schemaTables(state.theory)
@@ -197,38 +207,45 @@ function ingest(
197
207
  return yield* Effect.fail(refusal(operation, "InvalidArgument"))
198
208
  }
199
209
  state.inFlight = true
200
- const wire = yield* Effect.try({
201
- try: () => policyWire(state.policy, operation),
202
- catch: () => refusal(operation, "InvalidArgument")
203
- })
204
210
  const body = Effect.gen(function* () {
205
- const iterator = rows[Symbol.iterator]()
211
+ const iterator = yield* Effect.try({
212
+ try: () => rows[Symbol.iterator](),
213
+ catch: (cause) => argumentError(operation, cause)
214
+ })
206
215
  let leftover: object | undefined
207
216
  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)))
217
+ return yield* Effect.gen(function* () {
218
+ while (!done) {
219
+ const chunk = yield* Effect.try({
220
+ try: () => pullChunk(relation, iterator, leftover),
221
+ catch: (cause) => argumentError(operation, cause)
222
+ }).pipe(Effect.catch((error) => spendAndDrain(state, operation).pipe(Effect.andThen(Effect.fail(error)))))
223
+ leftover = chunk.leftover
224
+ done = chunk.done && leftover === undefined
225
+ if (chunk.rows === 0n) {
226
+ continue
227
+ }
228
+ yield* eventLoopTurn()
229
+ yield* nativeOperationWith(
230
+ operation,
231
+ (callback) => verb(state.handle, relationId, chunk.rows, chunk.cells, callback),
232
+ dbNative.runtimeReportTake,
233
+ () => undefined
234
+ ).pipe(
235
+ Effect.catch((error) =>
236
+ Effect.sync(() => {
237
+ state.spent = true
238
+ }).pipe(Effect.andThen(Effect.fail(error)))
239
+ )
229
240
  )
241
+ }
242
+ }).pipe(
243
+ Effect.ensuring(
244
+ Effect.sync(() => {
245
+ if (!done) iterator.return?.()
246
+ })
230
247
  )
231
- }
248
+ )
232
249
  })
233
250
  return yield* Effect.onExit(body, (exit) => {
234
251
  state.inFlight = false
@@ -240,19 +257,126 @@ function ingest(
240
257
  })
241
258
  }
242
259
 
243
- function makeChangeSet<S extends AnySchema>(handle: ChangesHandle, schemaId: SchemaId): ChangeSet<S> {
260
+ function decodeChangeRecord<S extends AnySchema>(
261
+ relations: readonly SchemaRelation[],
262
+ record: ChangeRecordWire
263
+ ): ChangeRecord<S> {
264
+ const relation = relations[record.relation]
265
+ if (relation === undefined || isClosedMember(relation) || (record.kind !== "add" && record.kind !== "remove")) {
266
+ throw new SdkInvariantError({ message: "ChangeSet.records: invalid native record descriptor" })
267
+ }
268
+ // Relation id resolves through this schema's own ordered roster. The
269
+ // shared projector validates all field values before this union seam.
270
+ return Object.freeze({
271
+ relation: relation.name,
272
+ kind: record.kind,
273
+ fact: factOfCells(relation, record.values)
274
+ }) as ChangeRecord<S>
275
+ }
276
+
277
+ function changeRecords<S extends AnySchema>(
278
+ handle: ChangesHandle,
279
+ relations: readonly SchemaRelation[]
280
+ ): Stream.Stream<ChangeRecord<S>, DbError> {
281
+ return Stream.unwrap(
282
+ Effect.gen(function* () {
283
+ const cursor = yield* Effect.acquireRelease(
284
+ nativeOperationWith(
285
+ "ChangeSet.records",
286
+ (callback) => dbNative.runtimeChangesCursor(handle, callback),
287
+ dbNative.runtimeChangesCursorTake,
288
+ (value) => value
289
+ ),
290
+ (value) =>
291
+ releaseOwner("ChangeCursor.close", (callback) => dbNative.runtimeChangesCursorClose(value, callback)),
292
+ { interruptible: true }
293
+ )
294
+ return Stream.paginate(undefined, () =>
295
+ nativeOperationWith(
296
+ "ChangeSet.records",
297
+ (callback) => dbNative.runtimeChangesCursorNext(cursor, callback),
298
+ dbNative.runtimeChangePageTake,
299
+ (page) => page
300
+ ).pipe(
301
+ Effect.map((page) =>
302
+ page === null
303
+ ? ([[], Option.none<undefined>()] as const)
304
+ : ([page.map((record) => decodeChangeRecord<S>(relations, record)), Option.some(undefined)] as const)
305
+ )
306
+ )
307
+ )
308
+ })
309
+ )
310
+ }
311
+
312
+ function acquireChanges<S extends AnySchema>(
313
+ theory: S,
314
+ schemaId: SchemaId,
315
+ acquire: Effect.Effect<ChangesWire, DbError>
316
+ ): Effect.Effect<ChangeSet<S>, DbError, Scope.Scope> {
317
+ return Effect.acquireRelease(
318
+ acquire.pipe(Effect.map((wire) => makeChangeSet(theory, wire, schemaId))),
319
+ (changes) =>
320
+ Effect.suspend(() => {
321
+ const internal = changesInternals.get(changes)
322
+ if (internal === undefined) return Effect.void
323
+ internal.closed = true
324
+ return releaseOwner("ChangeSet.close", (callback) => dbNative.runtimeChangesClose(internal.handle, callback))
325
+ }),
326
+ { interruptible: true }
327
+ )
328
+ }
329
+
330
+ function makeChangeSet<S extends AnySchema>(theory: S, wire: ChangesWire, schemaId: SchemaId): ChangeSet<S> {
331
+ const handle = wire.changes
332
+ const relations = Object.values(theory.relations)
333
+ const internal: ChangesInternal = { handle, schemaId, closed: false }
244
334
  const value: ChangeSet<S> = {
245
335
  schemaId,
336
+ counts: Object.freeze({ ...wire.counts }),
337
+ byteLength: wire.byteLength,
338
+ records() {
339
+ return changeRecords<S>(handle, relations)
340
+ },
341
+ toBytes() {
342
+ return nativeOperationWith(
343
+ "ChangeSet.toBytes",
344
+ (callback) => dbNative.runtimeChangesBytes(handle, callback),
345
+ dbNative.runtimeBytesTake,
346
+ (bytes) => bytes
347
+ )
348
+ },
349
+ compose(other) {
350
+ return Effect.suspend(() => {
351
+ const right = internalChanges(other)
352
+ if (right === undefined || right.schemaId !== schemaId)
353
+ return Effect.fail(refusal("ChangeSet.compose", "InvalidArgument"))
354
+ if (internal.closed || right.closed) return Effect.fail(refusal("ChangeSet.compose", "ClosedHandle"))
355
+ return acquireChanges(
356
+ theory,
357
+ schemaId,
358
+ nativeOperationWith(
359
+ "ChangeSet.compose",
360
+ (callback) => dbNative.runtimeChangesCompose(handle, right.handle, callback),
361
+ dbNative.runtimeChangesTake,
362
+ (result) => result
363
+ )
364
+ )
365
+ })
366
+ },
246
367
  close() {
247
- return drainClose("ChangeSet.close", (callback) => dbNative.runtimeChangesClose(handle, callback))
368
+ return Effect.suspend(() => {
369
+ internal.closed = true
370
+ return drainClose("ChangeSet.close", (callback) => dbNative.runtimeChangesClose(handle, callback))
371
+ })
248
372
  }
249
373
  }
250
374
  Object.freeze(value)
251
- changesInternals.set(value, { handle, schemaId, closed: false })
375
+ changesInternals.set(value, internal)
252
376
  return value
253
377
  }
254
378
 
255
- function makeDraft<S extends AnySchema>(state: DraftState, schemaId: SchemaId): ChangeDraft<S> {
379
+ function makeDraft<S extends AnySchema>(theory: S, state: DraftState, schemaId: SchemaId): ChangeDraft<S> {
256
380
  const draft: ChangeDraft<S> = {
257
381
  insert(relation, rows) {
258
382
  return ingest(state, "ChangeDraft.insert", relation, rows)
@@ -261,7 +385,9 @@ function makeDraft<S extends AnySchema>(state: DraftState, schemaId: SchemaId):
261
385
  return ingest(state, "ChangeDraft.delete", relation, rows)
262
386
  },
263
387
  finish() {
264
- return Effect.acquireRelease(
388
+ return acquireChanges(
389
+ theory,
390
+ schemaId,
265
391
  Effect.gen(function* () {
266
392
  if (state.spent) {
267
393
  return yield* Effect.fail(refusal("ChangeDraft.finish", "SpentHandle"))
@@ -272,27 +398,13 @@ function makeDraft<S extends AnySchema>(state: DraftState, schemaId: SchemaId):
272
398
  }
273
399
  // Finish CONSUMES the draft, success or failure.
274
400
  state.spent = true
275
- const wire = yield* nativeOperationWith(
401
+ return yield* nativeOperationWith(
276
402
  "ChangeDraft.finish",
277
- (callback) =>
278
- dbNative.runtimeDraftFinish(state.handle, policyWire(state.policy, "ChangeDraft.finish"), callback),
403
+ (callback) => dbNative.runtimeDraftFinish(state.handle, callback),
279
404
  dbNative.runtimeChangesTake,
280
405
  (value) => value
281
406
  )
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 }
407
+ })
296
408
  )
297
409
  },
298
410
  close() {
@@ -306,32 +418,31 @@ function makeDraft<S extends AnySchema>(state: DraftState, schemaId: SchemaId):
306
418
  }
307
419
 
308
420
  /**
309
- * `ChangeSet.builder(schema, work)` — lazy scoped acquisition of a
421
+ * `ChangeSet.builder(schema)` — lazy scoped acquisition of a
310
422
  * database-free draft. Requires the acquired
311
423
  * `NativeRuntime`; the draft's native resources release with its scope, and
312
424
  * the scope finalizer surfaces incomplete/failed teardown as a
313
425
  * `CloseFailure` defect.
314
426
  */
315
- const builder = Effect.fn("ChangeSet.builder")(function* <S extends AnySchema>(schema: S, work: ExecutionPolicy) {
427
+ const builder = Effect.fn("ChangeSet.builder")(function* <S extends AnySchema>(schema: S) {
316
428
  const handle = yield* runtimeHandle()
317
- const compiled = yield* CoreSchema.compile(schema, work)
318
- const spec = lower(schema)
429
+ const compiled = yield* CoreSchema.compile(schema)
430
+ const spec = lower(compiled.schema)
319
431
  return yield* Effect.acquireRelease(
320
432
  Effect.gen(function* () {
321
433
  const draftHandle = yield* nativeOperationWith(
322
434
  "ChangeSet.builder",
323
- (callback) => dbNative.runtimeDraftOpen(handle, policyWire(work, "ChangeSet.builder"), spec, callback),
435
+ (callback) => dbNative.runtimeDraftOpen(handle, spec, callback),
324
436
  dbNative.runtimeDraftTake,
325
437
  (value) => value
326
438
  )
327
439
  const state: DraftState = {
328
440
  handle: draftHandle,
329
- policy: work,
330
- theory: schema,
441
+ theory: compiled.schema,
331
442
  spent: false,
332
443
  inFlight: false
333
444
  }
334
- const draft = makeDraft<S>(state, compiled.schemaId)
445
+ const draft = makeDraft(compiled.schema, state, compiled.schemaId)
335
446
  draftStates.set(draft, state)
336
447
  return draft
337
448
  }),
@@ -355,7 +466,27 @@ const builder = Effect.fn("ChangeSet.builder")(function* <S extends AnySchema>(s
355
466
  // native handle without exposing it on the public capability.
356
467
  const draftStates = new WeakMap<object, DraftState>()
357
468
 
358
- const ChangeSet = Object.freeze({ builder })
469
+ /** Parse canonical native bytes, owning the input when the Effect starts. */
470
+ const fromBytes = Effect.fn("ChangeSet.fromBytes")(function* <S extends AnySchema>(schema: S, bytes: Uint8Array) {
471
+ const ownedBytes = yield* Effect.try({
472
+ try: () => bytesValue("ChangeSet.fromBytes", bytes),
473
+ catch: (cause) => argumentError("ChangeSet.fromBytes", cause)
474
+ })
475
+ const handle = yield* runtimeHandle()
476
+ const compiled = yield* CoreSchema.compile(schema)
477
+ return yield* acquireChanges(
478
+ compiled.schema,
479
+ compiled.schemaId,
480
+ nativeOperationWith(
481
+ "ChangeSet.fromBytes",
482
+ (callback) => dbNative.runtimeChangesParse(handle, lower(compiled.schema), ownedBytes, callback),
483
+ dbNative.runtimeChangesTake,
484
+ (wire) => wire
485
+ )
486
+ )
487
+ })
488
+
489
+ const ChangeSet = Object.freeze({ builder, fromBytes })
359
490
 
360
- export type { ChangeDraft }
491
+ export type { ChangeCounts, ChangeDraft, ChangeRecord }
361
492
  export { ChangeSet, internalChanges }