@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.
- package/COOKBOOK.md +283 -1611
- package/README.md +156 -118
- package/dist/capacity.d.ts +16 -12
- package/dist/capacity.d.ts.map +1 -1
- package/dist/capacity.js +16 -48
- package/dist/capacity.js.map +1 -1
- package/dist/changes.d.ts +54 -0
- package/dist/changes.d.ts.map +1 -0
- package/dist/changes.js +245 -0
- package/dist/changes.js.map +1 -0
- package/dist/close.d.ts +24 -0
- package/dist/close.d.ts.map +1 -0
- package/dist/close.js +34 -0
- package/dist/close.js.map +1 -0
- package/dist/closed.d.ts +1 -3
- package/dist/closed.d.ts.map +1 -1
- package/dist/closed.js +24 -12
- package/dist/closed.js.map +1 -1
- package/dist/codec.d.ts +56 -0
- package/dist/codec.d.ts.map +1 -0
- package/dist/codec.js +289 -0
- package/dist/codec.js.map +1 -0
- package/dist/compile.d.ts +68 -0
- package/dist/compile.d.ts.map +1 -0
- package/dist/compile.js +75 -0
- package/dist/compile.js.map +1 -0
- package/dist/db-native.d.ts +170 -0
- package/dist/db-native.d.ts.map +1 -0
- package/dist/db-native.js +5 -0
- package/dist/db-native.js.map +1 -0
- package/dist/db.d.ts +101 -233
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +232 -1084
- package/dist/db.js.map +1 -1
- package/dist/errors.d.ts +57 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +32 -0
- package/dist/errors.js.map +1 -0
- package/dist/face.d.ts.map +1 -1
- package/dist/face.js +2 -2
- package/dist/face.js.map +1 -1
- package/dist/fields.d.ts +65 -24
- package/dist/fields.d.ts.map +1 -1
- package/dist/fields.js +94 -24
- package/dist/fields.js.map +1 -1
- package/dist/index.d.ts +37 -32
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -21
- package/dist/index.js.map +1 -1
- package/dist/internal/log.d.ts +24 -0
- package/dist/internal/log.d.ts.map +1 -0
- package/dist/internal/log.js +10 -0
- package/dist/internal/log.js.map +1 -0
- package/dist/law.d.ts +8 -13
- package/dist/law.d.ts.map +1 -1
- package/dist/law.js +6 -59
- package/dist/law.js.map +1 -1
- package/dist/lower.d.ts +0 -13
- package/dist/lower.d.ts.map +1 -1
- package/dist/lower.js +9 -17
- package/dist/lower.js.map +1 -1
- package/dist/migration.d.ts +33 -0
- package/dist/migration.d.ts.map +1 -0
- package/dist/migration.js +48 -0
- package/dist/migration.js.map +1 -0
- package/dist/native.d.ts +256 -301
- package/dist/native.d.ts.map +1 -1
- package/dist/native.js +65 -108
- package/dist/native.js.map +1 -1
- package/dist/query/atom.d.ts +24 -13
- package/dist/query/atom.d.ts.map +1 -1
- package/dist/query/atom.js +7 -3
- package/dist/query/atom.js.map +1 -1
- package/dist/query/compute.d.ts +68 -0
- package/dist/query/compute.d.ts.map +1 -0
- package/dist/query/compute.js +141 -0
- package/dist/query/compute.js.map +1 -0
- package/dist/query/find.d.ts +15 -8
- package/dist/query/find.d.ts.map +1 -1
- package/dist/query/find.js +6 -2
- package/dist/query/find.js.map +1 -1
- package/dist/query/lower.d.ts +23 -2
- package/dist/query/lower.d.ts.map +1 -1
- package/dist/query/lower.js +433 -80
- package/dist/query/lower.js.map +1 -1
- package/dist/query/parse-ir.d.ts.map +1 -1
- package/dist/query/parse-ir.js +17 -8
- package/dist/query/parse-ir.js.map +1 -1
- package/dist/query/run.d.ts +12 -2
- package/dist/query/run.d.ts.map +1 -1
- package/dist/query/run.js +26 -9
- package/dist/query/run.js.map +1 -1
- package/dist/query/scope.d.ts +47 -3
- package/dist/query/scope.d.ts.map +1 -1
- package/dist/query/scope.js +83 -19
- package/dist/query/scope.js.map +1 -1
- package/dist/relation.d.ts +3 -8
- package/dist/relation.d.ts.map +1 -1
- package/dist/relation.js +16 -10
- package/dist/relation.js.map +1 -1
- package/dist/result.d.ts +47 -0
- package/dist/result.d.ts.map +1 -0
- package/dist/result.js +50 -0
- package/dist/result.js.map +1 -0
- package/dist/rows.d.ts +69 -0
- package/dist/rows.d.ts.map +1 -0
- package/dist/rows.js +328 -0
- package/dist/rows.js.map +1 -0
- package/dist/runtime-codes.d.ts +2 -0
- package/dist/runtime-codes.d.ts.map +1 -0
- package/dist/runtime-codes.js +19 -0
- package/dist/runtime-codes.js.map +1 -0
- package/dist/runtime-errors.d.ts +106 -0
- package/dist/runtime-errors.d.ts.map +1 -0
- package/dist/runtime-errors.js +62 -0
- package/dist/runtime-errors.js.map +1 -0
- package/dist/runtime-native.d.ts +171 -0
- package/dist/runtime-native.d.ts.map +1 -0
- package/dist/runtime-native.js +5 -0
- package/dist/runtime-native.js.map +1 -0
- package/dist/runtime.d.ts +73 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +293 -0
- package/dist/runtime.js.map +1 -0
- package/dist/scalar.d.ts +165 -0
- package/dist/scalar.d.ts.map +1 -0
- package/dist/scalar.js +344 -0
- package/dist/scalar.js.map +1 -0
- package/dist/schema.d.ts +1 -1
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +34 -37
- package/dist/schema.js.map +1 -1
- package/dist/shape.d.ts +28 -0
- package/dist/shape.d.ts.map +1 -0
- package/dist/shape.js +2 -0
- package/dist/shape.js.map +1 -0
- package/dist/spec.d.ts +27 -3
- package/dist/spec.d.ts.map +1 -1
- package/dist/spec.js +22 -1
- package/dist/spec.js.map +1 -1
- package/dist/statements.d.ts +16 -3
- package/dist/statements.d.ts.map +1 -1
- package/dist/statements.js +54 -28
- package/dist/statements.js.map +1 -1
- package/dist/uuid.d.ts +23 -0
- package/dist/uuid.d.ts.map +1 -0
- package/dist/uuid.js +54 -0
- package/dist/uuid.js.map +1 -0
- package/pack-provenance.json +7 -0
- package/package.json +12 -16
- package/src/capacity.ts +34 -54
- package/src/changes.ts +361 -0
- package/src/close.ts +53 -0
- package/src/closed.ts +25 -19
- package/src/codec.ts +379 -0
- package/src/compile.ts +149 -0
- package/src/db-native.ts +247 -0
- package/src/db.ts +461 -1632
- package/src/errors.ts +50 -0
- package/src/face.ts +2 -2
- package/src/fields.ts +182 -55
- package/src/index.ts +62 -126
- package/src/internal/log.ts +51 -0
- package/src/law.ts +13 -28
- package/src/lower.ts +9 -4
- package/src/migration.ts +69 -0
- package/src/native.ts +326 -487
- package/src/query/atom.ts +61 -25
- package/src/query/compute.ts +237 -0
- package/src/query/find.ts +31 -12
- package/src/query/lower.ts +531 -143
- package/src/query/parse-ir.ts +18 -13
- package/src/query/run.ts +32 -16
- package/src/query/scope.ts +140 -8
- package/src/relation.ts +17 -21
- package/src/result.ts +130 -0
- package/src/rows.ts +404 -0
- package/src/runtime-codes.ts +18 -0
- package/src/runtime-errors.ts +69 -0
- package/src/runtime-native.ts +242 -0
- package/src/runtime.ts +440 -0
- package/src/scalar.ts +560 -0
- package/src/schema.ts +35 -37
- package/src/shape.ts +31 -0
- package/src/spec.ts +33 -3
- package/src/statements.ts +75 -68
- package/src/uuid.ts +69 -0
- package/dist/marshal.d.ts +0 -44
- package/dist/marshal.d.ts.map +0 -1
- package/dist/marshal.js +0 -165
- package/dist/marshal.js.map +0 -1
- 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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
343
|
+
throw new AuthoringError({ message: `closed relation ${name}: ergonomic-surface minting incomplete` })
|
|
338
344
|
}
|
|
339
345
|
holder.value = value
|
|
340
346
|
return value
|