orez-sync-cf-host 0.5.19 → 0.5.21
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/README.md +49 -6
- package/dist/bun-wasm-loader.d.ts +1 -0
- package/dist/bun-wasm-loader.js +20 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.js +93 -0
- package/{src → dist}/generated/sync_wasm_bg.wasm +0 -0
- package/dist/host.d.ts +12 -0
- package/dist/host.js +1512 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +4 -0
- package/dist/mutation-error.d.ts +9 -0
- package/dist/mutation-error.js +23 -0
- package/dist/node-wasm-loader.d.ts +1 -0
- package/dist/node-wasm-loader.js +22 -0
- package/dist/post-commit.d.ts +13 -0
- package/dist/post-commit.js +31 -0
- package/dist/query-compiler.d.ts +4 -0
- package/dist/query-compiler.js +4 -0
- package/dist/sql-storage-adapter.d.ts +69 -0
- package/dist/sql-storage-adapter.js +194 -0
- package/dist/transaction-query.d.ts +72 -0
- package/dist/transaction-query.js +379 -0
- package/dist/types.d.ts +159 -0
- package/dist/types.js +5 -0
- package/dist/vite-wasm-loader.d.ts +3 -0
- package/dist/vite-wasm-loader.js +24 -0
- package/dist/wasm.d.ts +1 -0
- package/dist/wasm.js +7 -0
- package/dist/write-safeguards.d.ts +53 -0
- package/dist/write-safeguards.js +186 -0
- package/package.json +59 -19
- package/src/bun-wasm-loader.ts +0 -16
- package/src/config.ts +0 -102
- package/src/harness-config.ts +0 -487
- package/src/harness-worker.ts +0 -33
- package/src/host.ts +0 -1958
- package/src/index.ts +0 -47
- package/src/ingest-harness-worker.ts +0 -535
- package/src/mutation-error.ts +0 -28
- package/src/platform-probe-worker.ts +0 -418
- package/src/post-commit.ts +0 -40
- package/src/query-compiler.ts +0 -16
- package/src/sql-storage-adapter.ts +0 -254
- package/src/transaction-query.ts +0 -494
- package/src/types.ts +0 -195
- package/src/wasm-platform.ts +0 -10
- package/src/wasm.ts +0 -34
- package/src/write-safeguards.ts +0 -248
- /package/{src → dist}/generated/package.json +0 -0
- /package/{src → dist}/generated/sync_wasm.d.ts +0 -0
- /package/{src → dist}/generated/sync_wasm.js +0 -0
- /package/{src → dist}/generated/sync_wasm_bg.wasm.d.ts +0 -0
package/src/transaction-query.ts
DELETED
|
@@ -1,494 +0,0 @@
|
|
|
1
|
-
export type TransactionQueryWireValue =
|
|
2
|
-
| { kind: 'null' }
|
|
3
|
-
| { kind: 'integer'; value: string }
|
|
4
|
-
| { kind: 'real'; value: number }
|
|
5
|
-
| { kind: 'text'; value: string }
|
|
6
|
-
| { kind: 'blob'; value: number[] }
|
|
7
|
-
|
|
8
|
-
export type TransactionQueryBinding =
|
|
9
|
-
| { kind: 'literal'; value: TransactionQueryWireValue }
|
|
10
|
-
| { kind: 'parent_field'; field: string }
|
|
11
|
-
|
|
12
|
-
export type TransactionQueryColumnType = 'string' | 'number' | 'boolean' | 'json' | 'null'
|
|
13
|
-
|
|
14
|
-
export type TransactionQueryColumn = {
|
|
15
|
-
name: string
|
|
16
|
-
columnType: TransactionQueryColumnType
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export type CompiledTransactionQueryRelationship = {
|
|
20
|
-
name: string
|
|
21
|
-
node: CompiledTransactionQueryNode
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export type CompiledTransactionQueryNode = {
|
|
25
|
-
table: string
|
|
26
|
-
singular: boolean
|
|
27
|
-
sql: string
|
|
28
|
-
bindings: TransactionQueryBinding[]
|
|
29
|
-
columns: TransactionQueryColumn[]
|
|
30
|
-
relationships: CompiledTransactionQueryRelationship[]
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export type CompiledTransactionQueryPlan = {
|
|
34
|
-
rootTable: string
|
|
35
|
-
planHash: string
|
|
36
|
-
root: CompiledTransactionQueryNode
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export type TransactionQueryBudget = {
|
|
40
|
-
maxSelects: number
|
|
41
|
-
maxRows: number
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export type TransactionQueryExecutionOptions = {
|
|
45
|
-
queryName?: string
|
|
46
|
-
budget?: Partial<TransactionQueryBudget>
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export type TransactionQueryFormat = {
|
|
50
|
-
singular: boolean
|
|
51
|
-
relationships: Readonly<Record<string, TransactionQueryFormat>>
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export const DEFAULT_TRANSACTION_QUERY_BUDGET: TransactionQueryBudget = {
|
|
55
|
-
maxSelects: 256,
|
|
56
|
-
maxRows: 10_000,
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export class TransactionQueryBudgetError extends Error {
|
|
60
|
-
readonly code = 'transaction_query_budget_exceeded'
|
|
61
|
-
|
|
62
|
-
constructor(
|
|
63
|
-
readonly query: string,
|
|
64
|
-
readonly selects: number,
|
|
65
|
-
readonly rows: number,
|
|
66
|
-
readonly maxSelects: number,
|
|
67
|
-
readonly maxRows: number
|
|
68
|
-
) {
|
|
69
|
-
super(
|
|
70
|
-
`transaction_query_budget_exceeded: query=${query} selects=${selects} rows=${rows} maxSelects=${maxSelects} maxRows=${maxRows}`
|
|
71
|
-
)
|
|
72
|
-
this.name = 'TransactionQueryBudgetError'
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
type MaterializedRow = Record<string, unknown>
|
|
77
|
-
type ExecuteSelect = (
|
|
78
|
-
sql: string,
|
|
79
|
-
params: readonly unknown[]
|
|
80
|
-
) => readonly MaterializedRow[]
|
|
81
|
-
type ExecuteSelectAsync = (
|
|
82
|
-
sql: string,
|
|
83
|
-
params: readonly unknown[]
|
|
84
|
-
) => readonly MaterializedRow[] | Promise<readonly MaterializedRow[]>
|
|
85
|
-
|
|
86
|
-
const MAX_SAFE = BigInt(Number.MAX_SAFE_INTEGER)
|
|
87
|
-
const MIN_SAFE = BigInt(Number.MIN_SAFE_INTEGER)
|
|
88
|
-
const PLAN_HASH = /^[0-9a-f]{16}$/
|
|
89
|
-
const COLUMN_TYPES = new Set<TransactionQueryColumnType>([
|
|
90
|
-
'string',
|
|
91
|
-
'number',
|
|
92
|
-
'boolean',
|
|
93
|
-
'json',
|
|
94
|
-
'null',
|
|
95
|
-
])
|
|
96
|
-
|
|
97
|
-
function isObject(value: unknown): value is Record<string, unknown> {
|
|
98
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function malformed(detail: string): TypeError {
|
|
102
|
-
return new TypeError(`compiled transaction query plan ${detail}`)
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function assertWireValue(value: unknown, path: string): TransactionQueryWireValue {
|
|
106
|
-
if (!isObject(value) || typeof value.kind !== 'string') {
|
|
107
|
-
throw malformed(`${path} has an invalid literal`)
|
|
108
|
-
}
|
|
109
|
-
switch (value.kind) {
|
|
110
|
-
case 'null':
|
|
111
|
-
return value as TransactionQueryWireValue
|
|
112
|
-
case 'integer':
|
|
113
|
-
if (typeof value.value !== 'string' || !/^-?[0-9]+$/.test(value.value)) {
|
|
114
|
-
throw malformed(`${path} has an invalid integer literal`)
|
|
115
|
-
}
|
|
116
|
-
return value as TransactionQueryWireValue
|
|
117
|
-
case 'real':
|
|
118
|
-
if (typeof value.value !== 'number' || !Number.isFinite(value.value)) {
|
|
119
|
-
throw malformed(`${path} has an invalid real literal`)
|
|
120
|
-
}
|
|
121
|
-
return value as TransactionQueryWireValue
|
|
122
|
-
case 'text':
|
|
123
|
-
if (typeof value.value !== 'string') {
|
|
124
|
-
throw malformed(`${path} has an invalid text literal`)
|
|
125
|
-
}
|
|
126
|
-
return value as TransactionQueryWireValue
|
|
127
|
-
case 'blob':
|
|
128
|
-
if (
|
|
129
|
-
!Array.isArray(value.value) ||
|
|
130
|
-
value.value.some(
|
|
131
|
-
(byte) => !Number.isInteger(byte) || Number(byte) < 0 || Number(byte) > 255
|
|
132
|
-
)
|
|
133
|
-
) {
|
|
134
|
-
throw malformed(`${path} has an invalid blob literal`)
|
|
135
|
-
}
|
|
136
|
-
return value as TransactionQueryWireValue
|
|
137
|
-
default:
|
|
138
|
-
throw malformed(`${path} has an unknown literal kind`)
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function assertNode(
|
|
143
|
-
value: unknown,
|
|
144
|
-
path: string,
|
|
145
|
-
depth: number
|
|
146
|
-
): asserts value is CompiledTransactionQueryNode {
|
|
147
|
-
if (depth > 64) throw malformed(`${path} exceeds the maximum relationship depth`)
|
|
148
|
-
if (!isObject(value)) throw malformed(`${path} must be an object`)
|
|
149
|
-
if (typeof value.table !== 'string' || value.table.length === 0) {
|
|
150
|
-
throw malformed(`${path}.table must be a non-empty string`)
|
|
151
|
-
}
|
|
152
|
-
if (typeof value.singular !== 'boolean') {
|
|
153
|
-
throw malformed(`${path}.singular must be a boolean`)
|
|
154
|
-
}
|
|
155
|
-
if (typeof value.sql !== 'string' || value.sql.length === 0) {
|
|
156
|
-
throw malformed(`${path}.sql must be a non-empty string`)
|
|
157
|
-
}
|
|
158
|
-
if (!Array.isArray(value.bindings)) {
|
|
159
|
-
throw malformed(`${path}.bindings must be an array`)
|
|
160
|
-
}
|
|
161
|
-
for (const [index, binding] of value.bindings.entries()) {
|
|
162
|
-
if (!isObject(binding)) throw malformed(`${path}.bindings[${index}] is invalid`)
|
|
163
|
-
if (binding.kind === 'literal') {
|
|
164
|
-
assertWireValue(binding.value, `${path}.bindings[${index}]`)
|
|
165
|
-
} else if (
|
|
166
|
-
binding.kind !== 'parent_field' ||
|
|
167
|
-
typeof binding.field !== 'string' ||
|
|
168
|
-
binding.field.length === 0
|
|
169
|
-
) {
|
|
170
|
-
throw malformed(`${path}.bindings[${index}] is invalid`)
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
if (!Array.isArray(value.columns) || value.columns.length === 0) {
|
|
174
|
-
throw malformed(`${path}.columns must be a non-empty array`)
|
|
175
|
-
}
|
|
176
|
-
const names = new Set<string>()
|
|
177
|
-
for (const [index, column] of value.columns.entries()) {
|
|
178
|
-
if (
|
|
179
|
-
!isObject(column) ||
|
|
180
|
-
typeof column.name !== 'string' ||
|
|
181
|
-
column.name.length === 0 ||
|
|
182
|
-
typeof column.columnType !== 'string' ||
|
|
183
|
-
!COLUMN_TYPES.has(column.columnType as TransactionQueryColumnType)
|
|
184
|
-
) {
|
|
185
|
-
throw malformed(`${path}.columns[${index}] is invalid`)
|
|
186
|
-
}
|
|
187
|
-
if (names.has(column.name)) {
|
|
188
|
-
throw malformed(`${path}.columns contains duplicate '${column.name}'`)
|
|
189
|
-
}
|
|
190
|
-
names.add(column.name)
|
|
191
|
-
}
|
|
192
|
-
if (!Array.isArray(value.relationships)) {
|
|
193
|
-
throw malformed(`${path}.relationships must be an array`)
|
|
194
|
-
}
|
|
195
|
-
const relationships = new Set<string>()
|
|
196
|
-
for (const [index, relationship] of value.relationships.entries()) {
|
|
197
|
-
if (
|
|
198
|
-
!isObject(relationship) ||
|
|
199
|
-
typeof relationship.name !== 'string' ||
|
|
200
|
-
relationship.name.length === 0
|
|
201
|
-
) {
|
|
202
|
-
throw malformed(`${path}.relationships[${index}] is invalid`)
|
|
203
|
-
}
|
|
204
|
-
if (relationships.has(relationship.name) || names.has(relationship.name)) {
|
|
205
|
-
throw malformed(`${path}.relationships contains conflicting '${relationship.name}'`)
|
|
206
|
-
}
|
|
207
|
-
relationships.add(relationship.name)
|
|
208
|
-
assertNode(relationship.node, `${path}.relationships[${index}].node`, depth + 1)
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function assertPlan(value: unknown): asserts value is CompiledTransactionQueryPlan {
|
|
213
|
-
if (!isObject(value)) throw malformed('must be an object')
|
|
214
|
-
if (typeof value.rootTable !== 'string' || value.rootTable.length === 0) {
|
|
215
|
-
throw malformed('rootTable must be a non-empty string')
|
|
216
|
-
}
|
|
217
|
-
if (typeof value.planHash !== 'string' || !PLAN_HASH.test(value.planHash)) {
|
|
218
|
-
throw malformed('planHash must be 16 lowercase hexadecimal characters')
|
|
219
|
-
}
|
|
220
|
-
assertNode(value.root, 'root', 0)
|
|
221
|
-
if (value.root.table !== value.rootTable) {
|
|
222
|
-
throw malformed('root.table must match rootTable')
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
function resolveBudget(
|
|
227
|
-
budget: Partial<TransactionQueryBudget> | undefined
|
|
228
|
-
): TransactionQueryBudget {
|
|
229
|
-
const resolved = { ...DEFAULT_TRANSACTION_QUERY_BUDGET, ...budget }
|
|
230
|
-
for (const [name, value] of Object.entries(resolved)) {
|
|
231
|
-
if (!Number.isSafeInteger(value) || value < 1) {
|
|
232
|
-
throw new TypeError(
|
|
233
|
-
`transaction query budget ${name} must be a positive safe integer`
|
|
234
|
-
)
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
return resolved
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
function decodeWireValue(value: TransactionQueryWireValue): unknown {
|
|
241
|
-
switch (value.kind) {
|
|
242
|
-
case 'null':
|
|
243
|
-
return null
|
|
244
|
-
case 'integer': {
|
|
245
|
-
const exact = BigInt(value.value)
|
|
246
|
-
return exact >= MIN_SAFE && exact <= MAX_SAFE ? Number(exact) : value.value
|
|
247
|
-
}
|
|
248
|
-
case 'real':
|
|
249
|
-
case 'text':
|
|
250
|
-
return value.value
|
|
251
|
-
case 'blob':
|
|
252
|
-
return Uint8Array.from(value.value)
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function bytes(value: unknown): Uint8Array | undefined {
|
|
257
|
-
if (value instanceof ArrayBuffer) return new Uint8Array(value)
|
|
258
|
-
if (ArrayBuffer.isView(value)) {
|
|
259
|
-
return new Uint8Array(value.buffer, value.byteOffset, value.byteLength)
|
|
260
|
-
}
|
|
261
|
-
return undefined
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
function decodeColumn(
|
|
265
|
-
value: unknown,
|
|
266
|
-
columnType: TransactionQueryColumnType,
|
|
267
|
-
path: string
|
|
268
|
-
): unknown {
|
|
269
|
-
if (value === null) return null
|
|
270
|
-
switch (columnType) {
|
|
271
|
-
case 'string':
|
|
272
|
-
if (typeof value === 'string') return value
|
|
273
|
-
break
|
|
274
|
-
case 'number':
|
|
275
|
-
if (typeof value === 'number' && Number.isFinite(value)) return value
|
|
276
|
-
if (typeof value === 'bigint' && value >= MIN_SAFE && value <= MAX_SAFE) {
|
|
277
|
-
return Number(value)
|
|
278
|
-
}
|
|
279
|
-
break
|
|
280
|
-
case 'boolean':
|
|
281
|
-
if (typeof value === 'boolean') return value
|
|
282
|
-
if (value === 0 || value === 0n || value === '0') return false
|
|
283
|
-
if (value === 1 || value === 1n || value === '1') return true
|
|
284
|
-
break
|
|
285
|
-
case 'json': {
|
|
286
|
-
if (typeof value === 'string') {
|
|
287
|
-
try {
|
|
288
|
-
return JSON.parse(value)
|
|
289
|
-
} catch {
|
|
290
|
-
throw new TypeError(`transaction query row ${path} contains invalid JSON`)
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
const encoded = bytes(value)
|
|
294
|
-
if (encoded) {
|
|
295
|
-
try {
|
|
296
|
-
return JSON.parse(new TextDecoder().decode(encoded))
|
|
297
|
-
} catch {
|
|
298
|
-
throw new TypeError(`transaction query row ${path} contains invalid JSON`)
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
if (typeof value === 'object') return value
|
|
302
|
-
break
|
|
303
|
-
}
|
|
304
|
-
case 'null':
|
|
305
|
-
break
|
|
306
|
-
}
|
|
307
|
-
throw new TypeError(
|
|
308
|
-
`transaction query row ${path} does not match schema type ${columnType}`
|
|
309
|
-
)
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
function decodeRow(
|
|
313
|
-
raw: MaterializedRow,
|
|
314
|
-
node: CompiledTransactionQueryNode
|
|
315
|
-
): MaterializedRow {
|
|
316
|
-
const decoded: MaterializedRow = {}
|
|
317
|
-
for (const column of node.columns) {
|
|
318
|
-
if (!Object.hasOwn(raw, column.name)) {
|
|
319
|
-
throw new TypeError(
|
|
320
|
-
`transaction query row for ${node.table} is missing column ${column.name}`
|
|
321
|
-
)
|
|
322
|
-
}
|
|
323
|
-
decoded[column.name] = decodeColumn(
|
|
324
|
-
raw[column.name],
|
|
325
|
-
column.columnType,
|
|
326
|
-
`${node.table}.${column.name}`
|
|
327
|
-
)
|
|
328
|
-
}
|
|
329
|
-
return decoded
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
type TransactionQueryExecutionState = {
|
|
333
|
-
budget: TransactionQueryBudget
|
|
334
|
-
query: string
|
|
335
|
-
rows: number
|
|
336
|
-
selects: number
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
type PreparedSelect = {
|
|
340
|
-
params: unknown[]
|
|
341
|
-
state: TransactionQueryExecutionState
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
type DecodedSelect = {
|
|
345
|
-
rows: Array<{ decoded: MaterializedRow; raw: MaterializedRow }>
|
|
346
|
-
state: TransactionQueryExecutionState
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
type NodeExecution = {
|
|
350
|
-
state: TransactionQueryExecutionState
|
|
351
|
-
value: MaterializedRow[] | MaterializedRow | null | undefined
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
function createExecutionState(
|
|
355
|
-
plan: CompiledTransactionQueryPlan,
|
|
356
|
-
options: TransactionQueryExecutionOptions
|
|
357
|
-
): TransactionQueryExecutionState {
|
|
358
|
-
assertPlan(plan)
|
|
359
|
-
return {
|
|
360
|
-
budget: resolveBudget(options.budget),
|
|
361
|
-
query: options.queryName?.trim() || `${plan.rootTable}:${plan.planHash}`,
|
|
362
|
-
rows: 0,
|
|
363
|
-
selects: 0,
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
function assertWithinBudget(state: TransactionQueryExecutionState): void {
|
|
368
|
-
if (state.selects <= state.budget.maxSelects && state.rows <= state.budget.maxRows) {
|
|
369
|
-
return
|
|
370
|
-
}
|
|
371
|
-
throw new TransactionQueryBudgetError(
|
|
372
|
-
state.query,
|
|
373
|
-
state.selects,
|
|
374
|
-
state.rows,
|
|
375
|
-
state.budget.maxSelects,
|
|
376
|
-
state.budget.maxRows
|
|
377
|
-
)
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
function prepareSelect(
|
|
381
|
-
state: TransactionQueryExecutionState,
|
|
382
|
-
node: CompiledTransactionQueryNode,
|
|
383
|
-
parent?: MaterializedRow
|
|
384
|
-
): PreparedSelect {
|
|
385
|
-
const params = node.bindings.map((binding) => {
|
|
386
|
-
if (binding.kind === 'literal') return decodeWireValue(binding.value)
|
|
387
|
-
if (!parent || !Object.hasOwn(parent, binding.field)) {
|
|
388
|
-
throw new TypeError(
|
|
389
|
-
`transaction query parent row for ${node.table} is missing field ${binding.field}`
|
|
390
|
-
)
|
|
391
|
-
}
|
|
392
|
-
return parent[binding.field]
|
|
393
|
-
})
|
|
394
|
-
const next = { ...state, selects: state.selects + 1 }
|
|
395
|
-
assertWithinBudget(next)
|
|
396
|
-
return { params, state: next }
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
function decodeSelect(
|
|
400
|
-
state: TransactionQueryExecutionState,
|
|
401
|
-
node: CompiledTransactionQueryNode,
|
|
402
|
-
materialized: readonly MaterializedRow[]
|
|
403
|
-
): DecodedSelect {
|
|
404
|
-
if (!Array.isArray(materialized)) {
|
|
405
|
-
throw new TypeError('transaction query select must return a materialized row array')
|
|
406
|
-
}
|
|
407
|
-
const next = { ...state, rows: state.rows + materialized.length }
|
|
408
|
-
assertWithinBudget(next)
|
|
409
|
-
const rows = materialized.map((raw) => {
|
|
410
|
-
if (!isObject(raw)) {
|
|
411
|
-
throw new TypeError(
|
|
412
|
-
`transaction query select for ${node.table} returned a non-object row`
|
|
413
|
-
)
|
|
414
|
-
}
|
|
415
|
-
return { decoded: decodeRow(raw, node), raw }
|
|
416
|
-
})
|
|
417
|
-
return { rows, state: next }
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
function shapeRows(
|
|
421
|
-
node: CompiledTransactionQueryNode,
|
|
422
|
-
rows: MaterializedRow[]
|
|
423
|
-
): NodeExecution['value'] {
|
|
424
|
-
return node.singular ? rows[0] : rows
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
export function executeTransactionQueryPlan<Result = unknown>(
|
|
428
|
-
plan: CompiledTransactionQueryPlan,
|
|
429
|
-
execute: ExecuteSelect,
|
|
430
|
-
options: TransactionQueryExecutionOptions = {}
|
|
431
|
-
): Result {
|
|
432
|
-
const run = (
|
|
433
|
-
node: CompiledTransactionQueryNode,
|
|
434
|
-
state: TransactionQueryExecutionState,
|
|
435
|
-
parent?: MaterializedRow
|
|
436
|
-
): NodeExecution => {
|
|
437
|
-
const prepared = prepareSelect(state, node, parent)
|
|
438
|
-
const selected = decodeSelect(
|
|
439
|
-
prepared.state,
|
|
440
|
-
node,
|
|
441
|
-
execute(node.sql, prepared.params)
|
|
442
|
-
)
|
|
443
|
-
const hydrated: MaterializedRow[] = []
|
|
444
|
-
let current = selected.state
|
|
445
|
-
for (const { decoded, raw } of selected.rows) {
|
|
446
|
-
for (const relationship of node.relationships) {
|
|
447
|
-
const related = run(relationship.node, current, raw)
|
|
448
|
-
current = related.state
|
|
449
|
-
decoded[relationship.name] =
|
|
450
|
-
relationship.node.singular && related.value === undefined ? null : related.value
|
|
451
|
-
}
|
|
452
|
-
hydrated.push(decoded)
|
|
453
|
-
}
|
|
454
|
-
return { state: current, value: shapeRows(node, hydrated) }
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
const state = createExecutionState(plan, options)
|
|
458
|
-
return run(plan.root, state).value as Result
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
export async function executeTransactionQueryPlanAsync<Result = unknown>(
|
|
462
|
-
plan: CompiledTransactionQueryPlan,
|
|
463
|
-
execute: ExecuteSelectAsync,
|
|
464
|
-
options: TransactionQueryExecutionOptions = {}
|
|
465
|
-
): Promise<Result> {
|
|
466
|
-
const run = async (
|
|
467
|
-
node: CompiledTransactionQueryNode,
|
|
468
|
-
state: TransactionQueryExecutionState,
|
|
469
|
-
parent?: MaterializedRow
|
|
470
|
-
): Promise<NodeExecution> => {
|
|
471
|
-
const prepared = prepareSelect(state, node, parent)
|
|
472
|
-
const selected = decodeSelect(
|
|
473
|
-
prepared.state,
|
|
474
|
-
node,
|
|
475
|
-
await execute(node.sql, prepared.params)
|
|
476
|
-
)
|
|
477
|
-
const hydrated: MaterializedRow[] = []
|
|
478
|
-
let current = selected.state
|
|
479
|
-
for (const { decoded, raw } of selected.rows) {
|
|
480
|
-
for (const relationship of node.relationships) {
|
|
481
|
-
const related = await run(relationship.node, current, raw)
|
|
482
|
-
current = related.state
|
|
483
|
-
decoded[relationship.name] =
|
|
484
|
-
relationship.node.singular && related.value === undefined ? null : related.value
|
|
485
|
-
}
|
|
486
|
-
hydrated.push(decoded)
|
|
487
|
-
}
|
|
488
|
-
return { state: current, value: shapeRows(node, hydrated) }
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
const state = createExecutionState(plan, options)
|
|
492
|
-
const { value } = await run(plan.root, state)
|
|
493
|
-
return value as Result
|
|
494
|
-
}
|
package/src/types.ts
DELETED
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
import type { DeferredEffect } from './post-commit.js'
|
|
2
|
-
import type {
|
|
3
|
-
TransactionQueryBudget,
|
|
4
|
-
TransactionQueryFormat,
|
|
5
|
-
} from './transaction-query.js'
|
|
6
|
-
|
|
7
|
-
export type { DeferredEffect } from './post-commit.js'
|
|
8
|
-
|
|
9
|
-
export type JsonPrimitive = string | number | boolean | null
|
|
10
|
-
export type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue }
|
|
11
|
-
|
|
12
|
-
export type NormalizedClaims = {
|
|
13
|
-
/** Stable consumer user id used for client-group ownership. */
|
|
14
|
-
userID: string
|
|
15
|
-
[claim: string]: JsonValue
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export type ZeroSchemaConfig = {
|
|
19
|
-
readonly tables: Readonly<
|
|
20
|
-
Record<
|
|
21
|
-
string,
|
|
22
|
-
{
|
|
23
|
-
readonly name?: string
|
|
24
|
-
readonly serverName?: string
|
|
25
|
-
readonly columns: Readonly<
|
|
26
|
-
Record<string, { readonly type: string; readonly serverName?: string }>
|
|
27
|
-
>
|
|
28
|
-
readonly primaryKey: readonly string[]
|
|
29
|
-
}
|
|
30
|
-
>
|
|
31
|
-
>
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export interface SyncSql {
|
|
35
|
-
exec(sql: string, params?: readonly unknown[]): void
|
|
36
|
-
query<Row extends Record<string, unknown> = Record<string, unknown>>(
|
|
37
|
-
sql: string,
|
|
38
|
-
params?: readonly unknown[]
|
|
39
|
-
): Row[]
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export interface MutatorSql {
|
|
43
|
-
exec(sql: string, params?: readonly unknown[]): Promise<void>
|
|
44
|
-
query<Row extends Record<string, unknown> = Record<string, unknown>>(
|
|
45
|
-
sql: string,
|
|
46
|
-
params?: readonly unknown[]
|
|
47
|
-
): Promise<Row[]>
|
|
48
|
-
/** Execute a validated Zero AST inside the current application transaction. */
|
|
49
|
-
queryAst<Result = unknown>(
|
|
50
|
-
ast: JsonValue,
|
|
51
|
-
format: TransactionQueryFormat,
|
|
52
|
-
queryName?: string
|
|
53
|
-
): Promise<Result>
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export type MutatorContext = {
|
|
57
|
-
claims: NormalizedClaims
|
|
58
|
-
/** Current Zero mutation identity, when invoked by the production host. */
|
|
59
|
-
clientID?: string
|
|
60
|
-
mutationID?: string | number
|
|
61
|
-
defer(effect: DeferredEffect): void
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export type RegisteredMutator = (
|
|
65
|
-
tx: MutatorSql,
|
|
66
|
-
args: JsonValue,
|
|
67
|
-
context: MutatorContext
|
|
68
|
-
) => void | Promise<void>
|
|
69
|
-
|
|
70
|
-
export type MutatorRegistry = Readonly<Record<string, RegisteredMutator>>
|
|
71
|
-
|
|
72
|
-
/** Preserve mutator names while making the host-facing registry immutable. */
|
|
73
|
-
export function registerMutators<
|
|
74
|
-
const Registry extends Record<string, RegisteredMutator>,
|
|
75
|
-
>(registry: Registry): Readonly<Registry> {
|
|
76
|
-
return Object.freeze({ ...registry })
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export { MutationApplicationError, isMutationApplicationError } from './mutation-error.js'
|
|
80
|
-
|
|
81
|
-
export type VisibilityFilter = {
|
|
82
|
-
/** SQL WHERE fragment only, without the WHERE keyword. */
|
|
83
|
-
sql: string
|
|
84
|
-
params?: readonly JsonPrimitive[]
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export type VisibilityConfig = {
|
|
88
|
-
/** True only when every predicate depends on the selected row alone. */
|
|
89
|
-
rowLocal: boolean | ((claims: NormalizedClaims) => boolean)
|
|
90
|
-
filter(table: string, claims: NormalizedClaims): VisibilityFilter | undefined
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export type QueryResolver = (
|
|
94
|
-
name: string,
|
|
95
|
-
args: readonly JsonValue[],
|
|
96
|
-
claims: NormalizedClaims,
|
|
97
|
-
env: SyncHostEnv
|
|
98
|
-
) => JsonValue | Promise<JsonValue>
|
|
99
|
-
|
|
100
|
-
export type PullCaps = {
|
|
101
|
-
maxChangeRows: number
|
|
102
|
-
maxChangeBytes: number
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export interface SyncHostEnv {
|
|
106
|
-
SYNC_DO: DurableObjectNamespace
|
|
107
|
-
ADMIN_KEY?: string
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export type ServiceBinding = {
|
|
111
|
-
fetch(input: string | Request, init?: RequestInit): Promise<Response>
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export type UpstreamConfig = {
|
|
115
|
-
/** Env key for the service binding that owns the app write endpoint and feed. */
|
|
116
|
-
binding: string
|
|
117
|
-
/** Path to this namespace on the bound service (for example `/data/<id>`). */
|
|
118
|
-
namespacePath: string | ((namespace: string) => string)
|
|
119
|
-
/** Feed page size; the cursor loop continues until the reported head is reached. */
|
|
120
|
-
changeLimit?: number
|
|
121
|
-
/** Active wake-socket alarm safety net. Defaults to 15 seconds. */
|
|
122
|
-
intervalMs?: number
|
|
123
|
-
/** Billable SQLite rows written by ingest per rolling window. Defaults to 150,000. */
|
|
124
|
-
ingestBudgetRows?: number
|
|
125
|
-
/** Rolling ingest budget window. Defaults to five minutes. */
|
|
126
|
-
ingestBudgetWindowMs?: number
|
|
127
|
-
/** Initial breaker cooldown. Defaults to one second. */
|
|
128
|
-
ingestBackoffMs?: number
|
|
129
|
-
/** Maximum breaker cooldown. Defaults to one minute. */
|
|
130
|
-
ingestMaxBackoffMs?: number
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
export type DelegatedPushRetryConfig = {
|
|
134
|
-
/** Total attempts including the first request. Defaults to 3. */
|
|
135
|
-
maxAttempts?: number
|
|
136
|
-
/** Initial exponential delay. Defaults to 100ms. */
|
|
137
|
-
initialBackoffMs?: number
|
|
138
|
-
/** Delay cap. Defaults to 1,000ms. */
|
|
139
|
-
maxBackoffMs?: number
|
|
140
|
-
/** Per-attempt service-binding timeout. Defaults to 5,000ms. */
|
|
141
|
-
timeoutMs?: number
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
export type SyncHostConfig<Env extends SyncHostEnv = SyncHostEnv> = {
|
|
145
|
-
hostVersion: string
|
|
146
|
-
schema: ZeroSchemaConfig
|
|
147
|
-
mutators?: MutatorRegistry
|
|
148
|
-
/**
|
|
149
|
-
* absolute app push path on the delegated mutation service. a successful
|
|
150
|
-
* response must be causally visible through the configured upstream data
|
|
151
|
-
* feed before the app returns, because the host ingests effects before it
|
|
152
|
-
* records the mutation's lmid.
|
|
153
|
-
*/
|
|
154
|
-
mutateUrl?: string
|
|
155
|
-
/** Absolute origin used for delegated push requests through the service binding. */
|
|
156
|
-
mutateOrigin?: string
|
|
157
|
-
/**
|
|
158
|
-
* env binding for delegated pushes; defaults to upstream.binding. the bound
|
|
159
|
-
* service and upstream feed must satisfy the mutateUrl causality contract.
|
|
160
|
-
*/
|
|
161
|
-
mutateBinding?: string
|
|
162
|
-
delegatedPushRetry?: DelegatedPushRetryConfig
|
|
163
|
-
/** Required for delegated push; forbidden with local mutators (no dual apply). */
|
|
164
|
-
upstream?: UpstreamConfig
|
|
165
|
-
/** Application DDL and optional seed, called before sync-core schema init. */
|
|
166
|
-
initialize(sql: SyncSql): void
|
|
167
|
-
authenticate(
|
|
168
|
-
request: Request,
|
|
169
|
-
env: Env
|
|
170
|
-
): NormalizedClaims | null | Promise<NormalizedClaims | null>
|
|
171
|
-
/** Authorize the advisory wake socket before selecting a namespace DO.
|
|
172
|
-
* Browser clients should present a short-lived, namespace-scoped capability
|
|
173
|
-
* in the query string because WebSocket cannot set request headers. */
|
|
174
|
-
authorizeWake(request: Request, env: Env): boolean | Promise<boolean>
|
|
175
|
-
/** Authorize upstream service notifications before selecting a namespace DO. */
|
|
176
|
-
authorizeNotify(request: Request, env: Env): boolean | Promise<boolean>
|
|
177
|
-
/** Resolve the first path component or another consumer-defined namespace. */
|
|
178
|
-
namespace(request: Request): string | null
|
|
179
|
-
visibility?: VisibilityConfig
|
|
180
|
-
/** Enable desired-query pulls for this namespace and resolve named queries
|
|
181
|
-
* into validated Zero ASTs before they reach sync-core. */
|
|
182
|
-
queryAware?: boolean | ((claims: NormalizedClaims) => boolean)
|
|
183
|
-
resolveQuery?: QueryResolver
|
|
184
|
-
/** Server-owned invalidation epoch for permission/schema transforms. */
|
|
185
|
-
queryTransformVersion?: number | ((claims: NormalizedClaims) => number)
|
|
186
|
-
/** Enable consumer visibility from the first request. Defaults to false for harnesses. */
|
|
187
|
-
visibilityEnabled?: boolean
|
|
188
|
-
retainChanges?: number
|
|
189
|
-
caps?: Partial<PullCaps>
|
|
190
|
-
idleTeardownMs?: number
|
|
191
|
-
wakeCoalesceMs?: number
|
|
192
|
-
/** per-query guard for recursive transaction query materialization. */
|
|
193
|
-
transactionQueryBudget?: Partial<TransactionQueryBudget>
|
|
194
|
-
authorizeAdmin?: (request: Request, env: Env) => boolean | Promise<boolean>
|
|
195
|
-
}
|