orez-sync-cf-host 0.5.18 → 0.5.19
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 +74 -7
- package/package.json +15 -4
- package/src/bun-wasm-loader.ts +16 -0
- package/src/config.ts +9 -0
- package/src/generated/sync_wasm.d.ts +2 -2
- package/src/generated/sync_wasm.js +3 -2
- package/src/generated/sync_wasm_bg.wasm +0 -0
- package/src/host.ts +53 -43
- package/src/index.ts +26 -1
- package/src/ingest-harness-worker.ts +97 -2
- package/src/mutation-error.ts +28 -0
- package/src/platform-probe-worker.ts +122 -9
- package/src/post-commit.ts +40 -0
- package/src/query-compiler.ts +16 -0
- package/src/sql-storage-adapter.ts +88 -19
- package/src/transaction-query.ts +494 -0
- package/src/types.ts +31 -17
- package/src/wasm-platform.ts +10 -0
- package/src/wasm.ts +34 -0
|
@@ -1,18 +1,21 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { ZeroDO } from '../../../src/cf-do/worker.js'
|
|
2
|
+
import { createQueryCompiler } from './query-compiler.js'
|
|
3
|
+
import {
|
|
4
|
+
SqlStorageDirect,
|
|
5
|
+
SqlStorageMutatorTransaction,
|
|
6
|
+
SqlStorageSyncDb,
|
|
7
|
+
} from './sql-storage-adapter.js'
|
|
3
8
|
import {
|
|
4
|
-
initSync,
|
|
5
9
|
init_probe_schema,
|
|
6
10
|
pull_snapshot,
|
|
7
11
|
push_finalize,
|
|
8
12
|
push_preflight,
|
|
9
13
|
rust_panic_after_writes,
|
|
10
14
|
value_round_trip,
|
|
11
|
-
} from './
|
|
12
|
-
import wasmModule from './generated/sync_wasm_bg.wasm'
|
|
13
|
-
import { SqlStorageSyncDb } from './sql-storage-adapter.js'
|
|
15
|
+
} from './wasm-platform.js'
|
|
14
16
|
|
|
15
|
-
|
|
17
|
+
import type { TransactionQueryFormat } from './transaction-query.js'
|
|
18
|
+
import type { ZeroSchemaConfig } from './types.js'
|
|
16
19
|
|
|
17
20
|
interface Env {
|
|
18
21
|
PROBE_DO: DurableObjectNamespace<ProbeDurableObject>
|
|
@@ -21,6 +24,54 @@ interface Env {
|
|
|
21
24
|
type DeferredEffect = { mutationID: string; kind: string }
|
|
22
25
|
type MutatorName = 'read-then-write' | 'multi-table' | 'application-error'
|
|
23
26
|
|
|
27
|
+
const transactionQuerySchema = {
|
|
28
|
+
tables: {
|
|
29
|
+
account: {
|
|
30
|
+
name: 'account',
|
|
31
|
+
serverName: 'accounts',
|
|
32
|
+
columns: {
|
|
33
|
+
id: { type: 'string' },
|
|
34
|
+
balance: { type: 'number' },
|
|
35
|
+
},
|
|
36
|
+
primaryKey: ['id'],
|
|
37
|
+
},
|
|
38
|
+
entry: {
|
|
39
|
+
name: 'entry',
|
|
40
|
+
serverName: 'ledger',
|
|
41
|
+
columns: {
|
|
42
|
+
id: { type: 'number' },
|
|
43
|
+
accountId: { type: 'string', serverName: 'account_id' },
|
|
44
|
+
amount: { type: 'number' },
|
|
45
|
+
note: { type: 'string' },
|
|
46
|
+
},
|
|
47
|
+
primaryKey: ['id'],
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
} as const satisfies ZeroSchemaConfig
|
|
51
|
+
|
|
52
|
+
const transactionQueryAst = {
|
|
53
|
+
table: 'account',
|
|
54
|
+
where: {
|
|
55
|
+
type: 'simple',
|
|
56
|
+
op: '=',
|
|
57
|
+
left: { type: 'column', name: 'id' },
|
|
58
|
+
right: { type: 'literal', value: 'primary' },
|
|
59
|
+
},
|
|
60
|
+
related: [
|
|
61
|
+
{
|
|
62
|
+
correlation: { parentField: ['id'], childField: ['accountId'] },
|
|
63
|
+
subquery: { table: 'entry', alias: 'entries', orderBy: [['id', 'asc']] },
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const transactionQueryFormat = {
|
|
69
|
+
singular: true,
|
|
70
|
+
relationships: { entries: { singular: false, relationships: {} } },
|
|
71
|
+
} as const satisfies TransactionQueryFormat
|
|
72
|
+
|
|
73
|
+
const compileTransactionQuery = createQueryCompiler(transactionQuerySchema)
|
|
74
|
+
|
|
24
75
|
// Deterministic local analogue of normal DO idle eviction, matching the
|
|
25
76
|
// harness/cf probe: discard all in-memory state after an idle gap while SQL
|
|
26
77
|
// storage remains intact. This keeps the test sub-second instead of waiting
|
|
@@ -33,7 +84,7 @@ const json = (value: unknown, status = 200) =>
|
|
|
33
84
|
headers: { 'cache-control': 'no-store' },
|
|
34
85
|
})
|
|
35
86
|
|
|
36
|
-
export class ProbeDurableObject extends
|
|
87
|
+
export class ProbeDurableObject extends ZeroDO {
|
|
37
88
|
readonly #db: SqlStorageSyncDb
|
|
38
89
|
#bootID = crypto.randomUUID()
|
|
39
90
|
#lastRequestAt = 0
|
|
@@ -41,7 +92,7 @@ export class ProbeDurableObject extends DurableObject<Env> {
|
|
|
41
92
|
#effects: Array<DeferredEffect & { observedCommitted: boolean }> = []
|
|
42
93
|
|
|
43
94
|
constructor(ctx: DurableObjectState, env: Env) {
|
|
44
|
-
super(ctx, env)
|
|
95
|
+
super(ctx, env as never)
|
|
45
96
|
this.#db = new SqlStorageSyncDb(ctx.storage.sql)
|
|
46
97
|
ctx.storage.transactionSync(() => init_probe_schema(this.#db))
|
|
47
98
|
}
|
|
@@ -291,6 +342,68 @@ export class ProbeDurableObject extends DurableObject<Env> {
|
|
|
291
342
|
return json({ errors })
|
|
292
343
|
}
|
|
293
344
|
|
|
345
|
+
if (route === '/transaction-query') {
|
|
346
|
+
let plan: ReturnType<typeof compileTransactionQuery> | undefined
|
|
347
|
+
const tx = new SqlStorageMutatorTransaction(
|
|
348
|
+
new SqlStorageDirect(this.ctx.storage.sql),
|
|
349
|
+
(queryAst, queryFormat) => {
|
|
350
|
+
plan = compileTransactionQuery(queryAst, queryFormat)
|
|
351
|
+
return plan
|
|
352
|
+
}
|
|
353
|
+
)
|
|
354
|
+
const result = await tx.queryAst(
|
|
355
|
+
transactionQueryAst,
|
|
356
|
+
transactionQueryFormat,
|
|
357
|
+
'platformTransactionQuery'
|
|
358
|
+
)
|
|
359
|
+
let malformedFormatStatus: unknown
|
|
360
|
+
try {
|
|
361
|
+
await tx.queryAst(transactionQueryAst, undefined as never)
|
|
362
|
+
} catch (error) {
|
|
363
|
+
malformedFormatStatus = (error as { status?: unknown }).status
|
|
364
|
+
}
|
|
365
|
+
return json({ result, malformedFormatStatus, plan })
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (route === '/application-transaction-query') {
|
|
369
|
+
const result = await this.runApplicationTransaction(
|
|
370
|
+
compileTransactionQuery,
|
|
371
|
+
(tx) =>
|
|
372
|
+
tx.queryAst(
|
|
373
|
+
transactionQueryAst,
|
|
374
|
+
transactionQueryFormat,
|
|
375
|
+
'applicationTransactionQuery'
|
|
376
|
+
),
|
|
377
|
+
{ maxSelects: 8, maxRows: 20 }
|
|
378
|
+
)
|
|
379
|
+
return json({ result })
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (route === '/application-transaction-query-budget') {
|
|
383
|
+
try {
|
|
384
|
+
await this.runApplicationTransaction(
|
|
385
|
+
compileTransactionQuery,
|
|
386
|
+
(tx) =>
|
|
387
|
+
tx.queryAst(
|
|
388
|
+
transactionQueryAst,
|
|
389
|
+
transactionQueryFormat,
|
|
390
|
+
'budgetedApplicationTransactionQuery'
|
|
391
|
+
),
|
|
392
|
+
{ maxSelects: 1 }
|
|
393
|
+
)
|
|
394
|
+
} catch (error) {
|
|
395
|
+
return json(
|
|
396
|
+
{
|
|
397
|
+
code: Reflect.get(error as object, 'code'),
|
|
398
|
+
query: Reflect.get(error as object, 'query'),
|
|
399
|
+
selects: Reflect.get(error as object, 'selects'),
|
|
400
|
+
},
|
|
401
|
+
409
|
|
402
|
+
)
|
|
403
|
+
}
|
|
404
|
+
return json({ error: 'query budget did not abort the transaction' }, 500)
|
|
405
|
+
}
|
|
406
|
+
|
|
294
407
|
return json({ error: 'not found', route }, 404)
|
|
295
408
|
}
|
|
296
409
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export type DeferredEffect = () => void | Promise<void>
|
|
2
|
+
|
|
3
|
+
export type PostCommitEffects = {
|
|
4
|
+
/** discard effects from a storage-transaction attempt before starting another */
|
|
5
|
+
beginAttempt(): void
|
|
6
|
+
defer(effect: DeferredEffect): void
|
|
7
|
+
/** run the committed attempt's effects in order without rejecting */
|
|
8
|
+
runAfterCommit(onError: (error: unknown) => void): Promise<void>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* collect external work during a retryable transaction and run only the effects
|
|
13
|
+
* belonging to the attempt that commits.
|
|
14
|
+
*/
|
|
15
|
+
export function createPostCommitEffects(): PostCommitEffects {
|
|
16
|
+
const effects: DeferredEffect[] = []
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
beginAttempt() {
|
|
20
|
+
effects.length = 0
|
|
21
|
+
},
|
|
22
|
+
defer(effect) {
|
|
23
|
+
effects.push(effect)
|
|
24
|
+
},
|
|
25
|
+
async runAfterCommit(onError) {
|
|
26
|
+
const committed = effects.splice(0)
|
|
27
|
+
for (const effect of committed) {
|
|
28
|
+
try {
|
|
29
|
+
await effect()
|
|
30
|
+
} catch (error) {
|
|
31
|
+
try {
|
|
32
|
+
onError(error)
|
|
33
|
+
} catch {
|
|
34
|
+
// reporting must not turn a committed effect failure into a rejection
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { engine_compile_query } from './wasm.js'
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
CompiledTransactionQueryPlan,
|
|
5
|
+
TransactionQueryFormat,
|
|
6
|
+
} from './transaction-query.js'
|
|
7
|
+
import type { ZeroSchemaConfig } from './types.js'
|
|
8
|
+
|
|
9
|
+
export type TransactionQueryCompiler = (
|
|
10
|
+
ast: unknown,
|
|
11
|
+
format: TransactionQueryFormat
|
|
12
|
+
) => CompiledTransactionQueryPlan
|
|
13
|
+
|
|
14
|
+
export function createQueryCompiler(schema: ZeroSchemaConfig): TransactionQueryCompiler {
|
|
15
|
+
return (ast, format) => engine_compile_query(schema, ast, format)
|
|
16
|
+
}
|
|
@@ -1,5 +1,11 @@
|
|
|
1
|
+
import { executeTransactionQueryPlan } from './transaction-query.js'
|
|
1
2
|
import { trackBillableCursorRows } from './write-safeguards.js'
|
|
2
3
|
|
|
4
|
+
import type {
|
|
5
|
+
CompiledTransactionQueryPlan,
|
|
6
|
+
TransactionQueryBudget,
|
|
7
|
+
TransactionQueryFormat,
|
|
8
|
+
} from './transaction-query.js'
|
|
3
9
|
import type { MutatorSql, SyncSql } from './types.js'
|
|
4
10
|
|
|
5
11
|
type WireValue =
|
|
@@ -16,8 +22,6 @@ interface JsSyncDb {
|
|
|
16
22
|
query(sql: string, params: WireValue[]): WireRow[]
|
|
17
23
|
}
|
|
18
24
|
|
|
19
|
-
export type CompiledQuery = { sql: string; params: WireValue[] }
|
|
20
|
-
|
|
21
25
|
export type AdapterStats = {
|
|
22
26
|
execCalls: number
|
|
23
27
|
queryCalls: number
|
|
@@ -40,24 +44,79 @@ export function assertHostSql(sql: string): void {
|
|
|
40
44
|
}
|
|
41
45
|
}
|
|
42
46
|
|
|
43
|
-
function decodeBinding(value:
|
|
44
|
-
|
|
47
|
+
function decodeBinding(value: unknown, path = 'binding'): unknown {
|
|
48
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
49
|
+
throw new TypeError(`${path} must be a typed SQL value`)
|
|
50
|
+
}
|
|
51
|
+
const binding = value as { kind?: unknown; value?: unknown }
|
|
52
|
+
const keys = Object.keys(binding)
|
|
53
|
+
const assertValueShape = () => {
|
|
54
|
+
if (keys.length !== 2 || !keys.includes('kind') || !keys.includes('value')) {
|
|
55
|
+
throw new TypeError(`${path} must contain only kind and value`)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
switch (binding.kind) {
|
|
45
59
|
case 'null':
|
|
60
|
+
if (keys.length !== 1 || keys[0] !== 'kind') {
|
|
61
|
+
throw new TypeError(`${path} null binding must contain only kind`)
|
|
62
|
+
}
|
|
46
63
|
return null
|
|
47
64
|
case 'integer': {
|
|
48
|
-
|
|
65
|
+
assertValueShape()
|
|
66
|
+
if (typeof binding.value !== 'string') {
|
|
67
|
+
throw new TypeError(`${path}.value must be an i64 decimal string`)
|
|
68
|
+
}
|
|
69
|
+
if (!/^-?\d+$/.test(binding.value)) {
|
|
70
|
+
throw new TypeError(`${path}.value must be an i64 decimal string`)
|
|
71
|
+
}
|
|
72
|
+
let exact: bigint
|
|
73
|
+
try {
|
|
74
|
+
exact = BigInt(binding.value)
|
|
75
|
+
} catch {
|
|
76
|
+
throw new TypeError(`${path}.value must be an i64 decimal string`)
|
|
77
|
+
}
|
|
78
|
+
if (exact < -(1n << 63n) || exact > (1n << 63n) - 1n) {
|
|
79
|
+
throw new TypeError(`${path}.value is outside the i64 range`)
|
|
80
|
+
}
|
|
49
81
|
// SqlStorage does not accept bigint. Safe values can use Number; unsafe
|
|
50
82
|
// values use decimal text and rely on the destination's INTEGER affinity.
|
|
51
|
-
return exact >= MIN_SAFE && exact <= MAX_SAFE ? Number(exact) :
|
|
83
|
+
return exact >= MIN_SAFE && exact <= MAX_SAFE ? Number(exact) : binding.value
|
|
84
|
+
}
|
|
85
|
+
case 'real': {
|
|
86
|
+
assertValueShape()
|
|
87
|
+
if (typeof binding.value !== 'number' || !Number.isFinite(binding.value)) {
|
|
88
|
+
throw new TypeError(`${path}.value must be a finite number`)
|
|
89
|
+
}
|
|
90
|
+
return binding.value
|
|
91
|
+
}
|
|
92
|
+
case 'text': {
|
|
93
|
+
assertValueShape()
|
|
94
|
+
if (typeof binding.value !== 'string') {
|
|
95
|
+
throw new TypeError(`${path}.value must be a string`)
|
|
96
|
+
}
|
|
97
|
+
return binding.value
|
|
52
98
|
}
|
|
53
|
-
case '
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
99
|
+
case 'blob': {
|
|
100
|
+
assertValueShape()
|
|
101
|
+
if (
|
|
102
|
+
!Array.isArray(binding.value) ||
|
|
103
|
+
binding.value.some((byte) => !Number.isInteger(byte) || byte < 0 || byte > 255)
|
|
104
|
+
) {
|
|
105
|
+
throw new TypeError(`${path}.value must be an array of bytes`)
|
|
106
|
+
}
|
|
107
|
+
return Uint8Array.from(binding.value).buffer
|
|
108
|
+
}
|
|
109
|
+
default:
|
|
110
|
+
throw new TypeError(`${path}.kind must be null, integer, real, text, or blob`)
|
|
58
111
|
}
|
|
59
112
|
}
|
|
60
113
|
|
|
114
|
+
export function decodeSqlParams(value: unknown): unknown[] {
|
|
115
|
+
if (value === undefined) return []
|
|
116
|
+
if (!Array.isArray(value)) throw new TypeError('params must be an array')
|
|
117
|
+
return value.map((binding, index) => decodeBinding(binding, `params[${index}]`))
|
|
118
|
+
}
|
|
119
|
+
|
|
61
120
|
function encodeResult(value: unknown): WireValue {
|
|
62
121
|
if (value === null) return { kind: 'null' }
|
|
63
122
|
if (typeof value === 'number') {
|
|
@@ -103,7 +162,7 @@ export class SqlStorageSyncDb implements JsSyncDb {
|
|
|
103
162
|
assertHostSql(sql)
|
|
104
163
|
const start = performance.now()
|
|
105
164
|
trackBillableCursorRows(
|
|
106
|
-
this.sql.exec(sql, ...params.map(decodeBinding)),
|
|
165
|
+
this.sql.exec(sql, ...params.map((value) => decodeBinding(value))),
|
|
107
166
|
this.recordRowsWritten
|
|
108
167
|
)
|
|
109
168
|
this.stats.execCalls++
|
|
@@ -114,7 +173,7 @@ export class SqlStorageSyncDb implements JsSyncDb {
|
|
|
114
173
|
assertHostSql(sql)
|
|
115
174
|
const start = performance.now()
|
|
116
175
|
const cursor = trackBillableCursorRows(
|
|
117
|
-
this.sql.exec(sql, ...params.map(decodeBinding)),
|
|
176
|
+
this.sql.exec(sql, ...params.map((value) => decodeBinding(value))),
|
|
118
177
|
this.recordRowsWritten
|
|
119
178
|
)
|
|
120
179
|
const columns = [...cursor.columnNames]
|
|
@@ -162,7 +221,11 @@ export class SqlStorageDirect implements SyncSql {
|
|
|
162
221
|
export class SqlStorageMutatorTransaction implements MutatorSql {
|
|
163
222
|
constructor(
|
|
164
223
|
private readonly direct: SqlStorageDirect,
|
|
165
|
-
private readonly compileQuery: (
|
|
224
|
+
private readonly compileQuery: (
|
|
225
|
+
ast: unknown,
|
|
226
|
+
format: TransactionQueryFormat
|
|
227
|
+
) => CompiledTransactionQueryPlan,
|
|
228
|
+
private readonly queryBudget?: Partial<TransactionQueryBudget>
|
|
166
229
|
) {}
|
|
167
230
|
|
|
168
231
|
async exec(sql: string, params: readonly unknown[] = []): Promise<void> {
|
|
@@ -176,10 +239,16 @@ export class SqlStorageMutatorTransaction implements MutatorSql {
|
|
|
176
239
|
return this.direct.query<Row>(sql, params)
|
|
177
240
|
}
|
|
178
241
|
|
|
179
|
-
async queryAst<
|
|
180
|
-
ast: unknown
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
242
|
+
async queryAst<Result = unknown>(
|
|
243
|
+
ast: unknown,
|
|
244
|
+
format: TransactionQueryFormat,
|
|
245
|
+
queryName?: string
|
|
246
|
+
): Promise<Result> {
|
|
247
|
+
const compiled = this.compileQuery(ast, format)
|
|
248
|
+
return executeTransactionQueryPlan<Result>(
|
|
249
|
+
compiled,
|
|
250
|
+
(sql, params) => this.direct.query(sql, params),
|
|
251
|
+
{ queryName, budget: this.queryBudget }
|
|
252
|
+
)
|
|
184
253
|
}
|
|
185
254
|
}
|