orez-sync-cf-host 0.5.18 → 0.5.20
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 +117 -7
- 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.d.ts +2 -2
- package/{src → dist}/generated/sync_wasm.js +3 -2
- package/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 +192 -0
- package/dist/transaction-query.d.ts +72 -0
- package/dist/transaction-query.js +379 -0
- package/dist/types.d.ts +144 -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 +63 -12
- package/src/config.ts +0 -93
- package/src/generated/sync_wasm_bg.wasm +0 -0
- package/src/harness-config.ts +0 -487
- package/src/harness-worker.ts +0 -33
- package/src/host.ts +0 -1948
- package/src/index.ts +0 -22
- package/src/ingest-harness-worker.ts +0 -440
- package/src/platform-probe-worker.ts +0 -305
- package/src/sql-storage-adapter.ts +0 -185
- package/src/types.ts +0 -181
- package/src/write-safeguards.ts +0 -248
- /package/{src → dist}/generated/package.json +0 -0
- /package/{src → dist}/generated/sync_wasm_bg.wasm.d.ts +0 -0
|
@@ -1,305 +0,0 @@
|
|
|
1
|
-
import { DurableObject } from 'cloudflare:workers'
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
initSync,
|
|
5
|
-
init_probe_schema,
|
|
6
|
-
pull_snapshot,
|
|
7
|
-
push_finalize,
|
|
8
|
-
push_preflight,
|
|
9
|
-
rust_panic_after_writes,
|
|
10
|
-
value_round_trip,
|
|
11
|
-
} from './generated/sync_wasm.js'
|
|
12
|
-
import wasmModule from './generated/sync_wasm_bg.wasm'
|
|
13
|
-
import { SqlStorageSyncDb } from './sql-storage-adapter.js'
|
|
14
|
-
|
|
15
|
-
initSync({ module: wasmModule })
|
|
16
|
-
|
|
17
|
-
interface Env {
|
|
18
|
-
PROBE_DO: DurableObjectNamespace<ProbeDurableObject>
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
type DeferredEffect = { mutationID: string; kind: string }
|
|
22
|
-
type MutatorName = 'read-then-write' | 'multi-table' | 'application-error'
|
|
23
|
-
|
|
24
|
-
// Deterministic local analogue of normal DO idle eviction, matching the
|
|
25
|
-
// harness/cf probe: discard all in-memory state after an idle gap while SQL
|
|
26
|
-
// storage remains intact. This keeps the test sub-second instead of waiting
|
|
27
|
-
// for workerd/platform eviction heuristics.
|
|
28
|
-
const IDLE_TEARDOWN_MS = 250
|
|
29
|
-
|
|
30
|
-
const json = (value: unknown, status = 200) =>
|
|
31
|
-
Response.json(value, {
|
|
32
|
-
status,
|
|
33
|
-
headers: { 'cache-control': 'no-store' },
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
export class ProbeDurableObject extends DurableObject<Env> {
|
|
37
|
-
readonly #db: SqlStorageSyncDb
|
|
38
|
-
#bootID = crypto.randomUUID()
|
|
39
|
-
#lastRequestAt = 0
|
|
40
|
-
#reinstantiations = 0
|
|
41
|
-
#effects: Array<DeferredEffect & { observedCommitted: boolean }> = []
|
|
42
|
-
|
|
43
|
-
constructor(ctx: DurableObjectState, env: Env) {
|
|
44
|
-
super(ctx, env)
|
|
45
|
-
this.#db = new SqlStorageSyncDb(ctx.storage.sql)
|
|
46
|
-
ctx.storage.transactionSync(() => init_probe_schema(this.#db))
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
#maybeReinstantiate(now: number): void {
|
|
50
|
-
if (this.#lastRequestAt > 0 && now - this.#lastRequestAt >= IDLE_TEARDOWN_MS) {
|
|
51
|
-
this.#bootID = crypto.randomUUID()
|
|
52
|
-
this.#effects = []
|
|
53
|
-
this.#reinstantiations++
|
|
54
|
-
}
|
|
55
|
-
this.#lastRequestAt = now
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
#state(): Record<string, unknown> {
|
|
59
|
-
const row = this.ctx.storage.sql
|
|
60
|
-
.exec(
|
|
61
|
-
`SELECT
|
|
62
|
-
CAST((SELECT lmid FROM probe_state WHERE singleton = 1) AS TEXT) AS lmid,
|
|
63
|
-
(SELECT balance FROM accounts WHERE id = 'primary') AS balance,
|
|
64
|
-
(SELECT COUNT(*) FROM ledger) AS ledgerCount,
|
|
65
|
-
(SELECT COUNT(*) FROM outbox) AS outboxCount,
|
|
66
|
-
(SELECT COUNT(*) FROM mutation_log) AS mutationCount,
|
|
67
|
-
(SELECT COUNT(*) FROM value_probe) AS valueCount`
|
|
68
|
-
)
|
|
69
|
-
.one()
|
|
70
|
-
return {
|
|
71
|
-
...row,
|
|
72
|
-
sideEffectCount: this.#effects.length,
|
|
73
|
-
sideEffects: this.#effects,
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
async #mutate(name: MutatorName, deferred: DeferredEffect[]): Promise<void> {
|
|
78
|
-
if (name === 'read-then-write') {
|
|
79
|
-
const { balance } = this.ctx.storage.sql
|
|
80
|
-
.exec("SELECT balance FROM accounts WHERE id = 'primary'")
|
|
81
|
-
.one() as { balance: number }
|
|
82
|
-
await Promise.resolve()
|
|
83
|
-
this.ctx.storage.sql.exec(
|
|
84
|
-
"UPDATE accounts SET balance = ? WHERE id = 'primary'",
|
|
85
|
-
balance + 10
|
|
86
|
-
)
|
|
87
|
-
this.ctx.storage.sql.exec(
|
|
88
|
-
'INSERT INTO ledger (account_id, amount, note) VALUES (?, ?, ?)',
|
|
89
|
-
'primary',
|
|
90
|
-
10,
|
|
91
|
-
'read-then-write'
|
|
92
|
-
)
|
|
93
|
-
deferred.push({ mutationID: '', kind: 'balance-notification' })
|
|
94
|
-
return
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (name === 'multi-table') {
|
|
98
|
-
this.ctx.storage.sql.exec(
|
|
99
|
-
"UPDATE accounts SET balance = balance - 5 WHERE id = 'primary'"
|
|
100
|
-
)
|
|
101
|
-
await Promise.resolve()
|
|
102
|
-
this.ctx.storage.sql.exec(
|
|
103
|
-
'INSERT INTO ledger (account_id, amount, note) VALUES (?, ?, ?)',
|
|
104
|
-
'primary',
|
|
105
|
-
-5,
|
|
106
|
-
'multi-table'
|
|
107
|
-
)
|
|
108
|
-
this.ctx.storage.sql.exec(
|
|
109
|
-
'INSERT INTO outbox (topic, payload) VALUES (?, ?)',
|
|
110
|
-
'account.changed',
|
|
111
|
-
JSON.stringify({ id: 'primary', delta: -5 })
|
|
112
|
-
)
|
|
113
|
-
deferred.push({ mutationID: '', kind: 'outbox-notification' })
|
|
114
|
-
return
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const { balance } = this.ctx.storage.sql
|
|
118
|
-
.exec("SELECT balance FROM accounts WHERE id = 'primary'")
|
|
119
|
-
.one() as { balance: number }
|
|
120
|
-
this.ctx.storage.sql.exec(
|
|
121
|
-
"UPDATE accounts SET balance = ? WHERE id = 'primary'",
|
|
122
|
-
balance + 777
|
|
123
|
-
)
|
|
124
|
-
await Promise.resolve()
|
|
125
|
-
this.ctx.storage.sql.exec(
|
|
126
|
-
'INSERT INTO ledger (account_id, amount, note) VALUES (?, ?, ?)',
|
|
127
|
-
'primary',
|
|
128
|
-
777,
|
|
129
|
-
'application-error'
|
|
130
|
-
)
|
|
131
|
-
deferred.push({ mutationID: '', kind: 'must-not-run' })
|
|
132
|
-
throw new Error('intentional application mutator error')
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
async #push(name: MutatorName, mutationID: string): Promise<Response> {
|
|
136
|
-
this.#db.resetStats()
|
|
137
|
-
const deferred: DeferredEffect[] = []
|
|
138
|
-
const started = performance.now()
|
|
139
|
-
let lmid: string | undefined
|
|
140
|
-
let wasmMs = 0
|
|
141
|
-
try {
|
|
142
|
-
await this.ctx.storage.transaction(async () => {
|
|
143
|
-
let wasmStarted = performance.now()
|
|
144
|
-
const expected = push_preflight(this.#db, mutationID)
|
|
145
|
-
wasmMs += performance.now() - wasmStarted
|
|
146
|
-
|
|
147
|
-
// MutatorSql methods resolve through the microtask queue. Cross that
|
|
148
|
-
// same boundary here without admitting timers or other external work
|
|
149
|
-
// into the storage transaction.
|
|
150
|
-
await Promise.resolve()
|
|
151
|
-
await this.#mutate(name, deferred)
|
|
152
|
-
|
|
153
|
-
wasmStarted = performance.now()
|
|
154
|
-
lmid = push_finalize(this.#db, mutationID, expected)
|
|
155
|
-
wasmMs += performance.now() - wasmStarted
|
|
156
|
-
})
|
|
157
|
-
} catch (error) {
|
|
158
|
-
return json(
|
|
159
|
-
{
|
|
160
|
-
ok: false,
|
|
161
|
-
error: String(error),
|
|
162
|
-
awaitedInsideTransaction: true,
|
|
163
|
-
state: this.#state(),
|
|
164
|
-
effectsDeferredButNotRun: deferred.length,
|
|
165
|
-
},
|
|
166
|
-
409
|
|
167
|
-
)
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// External effects are not even inspected until the transaction promise
|
|
171
|
-
// resolves. Each effect independently verifies its mutation is durable.
|
|
172
|
-
for (const effect of deferred) {
|
|
173
|
-
effect.mutationID = mutationID
|
|
174
|
-
const committed =
|
|
175
|
-
this.ctx.storage.sql
|
|
176
|
-
.exec(
|
|
177
|
-
'SELECT 1 AS committed FROM mutation_log WHERE mutation_id = ?',
|
|
178
|
-
mutationID
|
|
179
|
-
)
|
|
180
|
-
.toArray().length === 1
|
|
181
|
-
this.#effects.push({ ...effect, observedCommitted: committed })
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
return json({
|
|
185
|
-
ok: true,
|
|
186
|
-
lmid,
|
|
187
|
-
awaitedInsideTransaction: true,
|
|
188
|
-
state: this.#state(),
|
|
189
|
-
timing: {
|
|
190
|
-
elapsedMs: performance.now() - started,
|
|
191
|
-
wasmMs,
|
|
192
|
-
...this.#db.stats,
|
|
193
|
-
},
|
|
194
|
-
})
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
async fetch(request: Request): Promise<Response> {
|
|
198
|
-
this.#maybeReinstantiate(Date.now())
|
|
199
|
-
const url = new URL(request.url)
|
|
200
|
-
const [, , ...routeParts] = url.pathname.split('/')
|
|
201
|
-
const route = `/${routeParts.join('/')}`
|
|
202
|
-
|
|
203
|
-
if (route === '/status') {
|
|
204
|
-
return json({
|
|
205
|
-
bootID: this.#bootID,
|
|
206
|
-
idleTeardownMs: IDLE_TEARDOWN_MS,
|
|
207
|
-
reinstantiations: this.#reinstantiations,
|
|
208
|
-
state: this.#state(),
|
|
209
|
-
})
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
if (route === '/pull') {
|
|
213
|
-
this.#db.resetStats()
|
|
214
|
-
const started = performance.now()
|
|
215
|
-
const snapshot = this.ctx.storage.transactionSync(() => pull_snapshot(this.#db))
|
|
216
|
-
return json({
|
|
217
|
-
snapshot,
|
|
218
|
-
transaction: 'transactionSync',
|
|
219
|
-
timing: { elapsedMs: performance.now() - started, ...this.#db.stats },
|
|
220
|
-
})
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
if (route.startsWith('/push/')) {
|
|
224
|
-
const name = route.slice('/push/'.length) as MutatorName
|
|
225
|
-
if (!['read-then-write', 'multi-table', 'application-error'].includes(name)) {
|
|
226
|
-
return json({ error: 'unknown mutator' }, 404)
|
|
227
|
-
}
|
|
228
|
-
const body = (await request.json()) as { mutationID: string }
|
|
229
|
-
return this.#push(name, body.mutationID)
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
if (route === '/js-exception') {
|
|
233
|
-
const before = this.#state()
|
|
234
|
-
try {
|
|
235
|
-
await this.ctx.storage.transaction(async () => {
|
|
236
|
-
const expected = push_preflight(this.#db, 'js-exception')
|
|
237
|
-
this.ctx.storage.sql.exec(
|
|
238
|
-
"UPDATE accounts SET balance = balance + 123 WHERE id = 'primary'"
|
|
239
|
-
)
|
|
240
|
-
await Promise.resolve()
|
|
241
|
-
this.ctx.storage.sql.exec(
|
|
242
|
-
'INSERT INTO outbox (topic, payload) VALUES (?, ?)',
|
|
243
|
-
'must.rollback',
|
|
244
|
-
'{}'
|
|
245
|
-
)
|
|
246
|
-
push_finalize(this.#db, 'js-exception', expected)
|
|
247
|
-
throw new Error('intentional JS exception after finalization')
|
|
248
|
-
})
|
|
249
|
-
} catch (error) {
|
|
250
|
-
return json(
|
|
251
|
-
{ ok: false, error: String(error), before, after: this.#state() },
|
|
252
|
-
409
|
|
253
|
-
)
|
|
254
|
-
}
|
|
255
|
-
return json({ error: 'JS exception did not escape transaction' }, 500)
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
if (route === '/rust-panic') {
|
|
259
|
-
const before = this.#state()
|
|
260
|
-
try {
|
|
261
|
-
this.ctx.storage.transactionSync(() => rust_panic_after_writes(this.#db))
|
|
262
|
-
} catch (error) {
|
|
263
|
-
return json(
|
|
264
|
-
{ ok: false, error: String(error), before, after: this.#state() },
|
|
265
|
-
409
|
|
266
|
-
)
|
|
267
|
-
}
|
|
268
|
-
return json({ error: 'Rust panic did not escape transaction' }, 500)
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
if (route === '/values') {
|
|
272
|
-
const input = await request.json()
|
|
273
|
-
const output = this.ctx.storage.transactionSync(() =>
|
|
274
|
-
value_round_trip(this.#db, input)
|
|
275
|
-
)
|
|
276
|
-
return json(output)
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
if (route === '/adapter-guard') {
|
|
280
|
-
const errors: string[] = []
|
|
281
|
-
for (const [sql, params] of [
|
|
282
|
-
['BEGIN TRANSACTION', []],
|
|
283
|
-
['SELECT ?1', [{ kind: 'integer', value: '1' }]],
|
|
284
|
-
] as const) {
|
|
285
|
-
try {
|
|
286
|
-
this.#db.exec(sql, [...params] as never)
|
|
287
|
-
} catch (error) {
|
|
288
|
-
errors.push(String(error))
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
return json({ errors })
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
return json({ error: 'not found', route }, 404)
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
export default {
|
|
299
|
-
fetch(request: Request, env: Env): Promise<Response> {
|
|
300
|
-
const url = new URL(request.url)
|
|
301
|
-
const [, namespace] = url.pathname.split('/')
|
|
302
|
-
if (!namespace) return Promise.resolve(new Response('orez rust sync M0 probe'))
|
|
303
|
-
return env.PROBE_DO.get(env.PROBE_DO.idFromName(namespace)).fetch(request)
|
|
304
|
-
},
|
|
305
|
-
} satisfies ExportedHandler<Env>
|
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
import { trackBillableCursorRows } from './write-safeguards.js'
|
|
2
|
-
|
|
3
|
-
import type { MutatorSql, SyncSql } from './types.js'
|
|
4
|
-
|
|
5
|
-
type WireValue =
|
|
6
|
-
| { kind: 'null' }
|
|
7
|
-
| { kind: 'integer'; value: string }
|
|
8
|
-
| { kind: 'real'; value: number }
|
|
9
|
-
| { kind: 'text'; value: string }
|
|
10
|
-
| { kind: 'blob'; value: number[] }
|
|
11
|
-
|
|
12
|
-
type WireRow = { columns: string[]; values: WireValue[] }
|
|
13
|
-
|
|
14
|
-
interface JsSyncDb {
|
|
15
|
-
exec(sql: string, params: WireValue[]): void
|
|
16
|
-
query(sql: string, params: WireValue[]): WireRow[]
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export type CompiledQuery = { sql: string; params: WireValue[] }
|
|
20
|
-
|
|
21
|
-
export type AdapterStats = {
|
|
22
|
-
execCalls: number
|
|
23
|
-
queryCalls: number
|
|
24
|
-
sqlMs: number
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// Match transaction control only as the statement itself. CREATE TRIGGER uses
|
|
28
|
-
// `... BEGIN ... END` for its body, which is valid DO SQL and not a transaction.
|
|
29
|
-
const TX_SQL = /^\s*(BEGIN|COMMIT|ROLLBACK|SAVEPOINT|RELEASE)(?=\s|;|$)/i
|
|
30
|
-
const NUMBERED_PARAMETER = /\?[0-9]+/
|
|
31
|
-
const MAX_SAFE = BigInt(Number.MAX_SAFE_INTEGER)
|
|
32
|
-
const MIN_SAFE = BigInt(Number.MIN_SAFE_INTEGER)
|
|
33
|
-
|
|
34
|
-
export function assertHostSql(sql: string): void {
|
|
35
|
-
if (TX_SQL.test(sql)) {
|
|
36
|
-
throw new TypeError('transaction SQL is host-owned and forbidden')
|
|
37
|
-
}
|
|
38
|
-
if (NUMBERED_PARAMETER.test(sql)) {
|
|
39
|
-
throw new TypeError('numbered parameters are forbidden; use positional ? bindings')
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function decodeBinding(value: WireValue): unknown {
|
|
44
|
-
switch (value.kind) {
|
|
45
|
-
case 'null':
|
|
46
|
-
return null
|
|
47
|
-
case 'integer': {
|
|
48
|
-
const exact = BigInt(value.value)
|
|
49
|
-
// SqlStorage does not accept bigint. Safe values can use Number; unsafe
|
|
50
|
-
// values use decimal text and rely on the destination's INTEGER affinity.
|
|
51
|
-
return exact >= MIN_SAFE && exact <= MAX_SAFE ? Number(exact) : value.value
|
|
52
|
-
}
|
|
53
|
-
case 'real':
|
|
54
|
-
case 'text':
|
|
55
|
-
return value.value
|
|
56
|
-
case 'blob':
|
|
57
|
-
return Uint8Array.from(value.value).buffer
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function encodeResult(value: unknown): WireValue {
|
|
62
|
-
if (value === null) return { kind: 'null' }
|
|
63
|
-
if (typeof value === 'number') {
|
|
64
|
-
return Number.isInteger(value)
|
|
65
|
-
? { kind: 'integer', value: String(value) }
|
|
66
|
-
: { kind: 'real', value }
|
|
67
|
-
}
|
|
68
|
-
if (typeof value === 'string') return { kind: 'text', value }
|
|
69
|
-
if (value instanceof ArrayBuffer) {
|
|
70
|
-
return { kind: 'blob', value: Array.from(new Uint8Array(value)) }
|
|
71
|
-
}
|
|
72
|
-
if (ArrayBuffer.isView(value)) {
|
|
73
|
-
return {
|
|
74
|
-
kind: 'blob',
|
|
75
|
-
value: Array.from(new Uint8Array(value.buffer, value.byteOffset, value.byteLength)),
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
throw new TypeError(
|
|
79
|
-
`unsupported SqlStorage result: ${Object.prototype.toString.call(value)}`
|
|
80
|
-
)
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* The sole JS SQL adapter used by wasm. It is synchronous, consumes every
|
|
85
|
-
* cursor before returning, accepts positional `?` only, and never starts or
|
|
86
|
-
* finishes a transaction.
|
|
87
|
-
*/
|
|
88
|
-
export class SqlStorageSyncDb implements JsSyncDb {
|
|
89
|
-
readonly stats: AdapterStats = { execCalls: 0, queryCalls: 0, sqlMs: 0 }
|
|
90
|
-
|
|
91
|
-
constructor(
|
|
92
|
-
private readonly sql: SqlStorage,
|
|
93
|
-
private readonly recordRowsWritten: (rows: number) => void = () => {}
|
|
94
|
-
) {}
|
|
95
|
-
|
|
96
|
-
resetStats(): void {
|
|
97
|
-
this.stats.execCalls = 0
|
|
98
|
-
this.stats.queryCalls = 0
|
|
99
|
-
this.stats.sqlMs = 0
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
exec(sql: string, params: WireValue[]): void {
|
|
103
|
-
assertHostSql(sql)
|
|
104
|
-
const start = performance.now()
|
|
105
|
-
trackBillableCursorRows(
|
|
106
|
-
this.sql.exec(sql, ...params.map(decodeBinding)),
|
|
107
|
-
this.recordRowsWritten
|
|
108
|
-
)
|
|
109
|
-
this.stats.execCalls++
|
|
110
|
-
this.stats.sqlMs += performance.now() - start
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
query(sql: string, params: WireValue[]): WireRow[] {
|
|
114
|
-
assertHostSql(sql)
|
|
115
|
-
const start = performance.now()
|
|
116
|
-
const cursor = trackBillableCursorRows(
|
|
117
|
-
this.sql.exec(sql, ...params.map(decodeBinding)),
|
|
118
|
-
this.recordRowsWritten
|
|
119
|
-
)
|
|
120
|
-
const columns = [...cursor.columnNames]
|
|
121
|
-
// A SqlStorage cursor must be fully consumed before any await. Returning a
|
|
122
|
-
// materialized array here makes that property structural.
|
|
123
|
-
const rows = Array.from(cursor.raw()).map((values) => ({
|
|
124
|
-
columns,
|
|
125
|
-
values: values.map(encodeResult),
|
|
126
|
-
}))
|
|
127
|
-
this.stats.queryCalls++
|
|
128
|
-
this.stats.sqlMs += performance.now() - start
|
|
129
|
-
return rows
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/** Direct application SQL surface used for initialization and admin reads. */
|
|
134
|
-
export class SqlStorageDirect implements SyncSql {
|
|
135
|
-
constructor(
|
|
136
|
-
private readonly sql: SqlStorage,
|
|
137
|
-
private readonly recordRowsWritten: (rows: number) => void = () => {}
|
|
138
|
-
) {}
|
|
139
|
-
|
|
140
|
-
exec(sql: string, params: readonly unknown[] = []): void {
|
|
141
|
-
assertHostSql(sql)
|
|
142
|
-
trackBillableCursorRows(this.sql.exec(sql, ...params), this.recordRowsWritten)
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
query<Row extends Record<string, unknown> = Record<string, unknown>>(
|
|
146
|
-
sql: string,
|
|
147
|
-
params: readonly unknown[] = []
|
|
148
|
-
): Row[] {
|
|
149
|
-
assertHostSql(sql)
|
|
150
|
-
return trackBillableCursorRows(
|
|
151
|
-
this.sql.exec(sql, ...params),
|
|
152
|
-
this.recordRowsWritten
|
|
153
|
-
).toArray() as Row[]
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Async-compatible consumer mutator surface. Each method fully completes and
|
|
159
|
-
* materializes its SqlStorage operation before its promise resolves, so an
|
|
160
|
-
* existing `await tx.query(...)` cannot carry a cursor across an await.
|
|
161
|
-
*/
|
|
162
|
-
export class SqlStorageMutatorTransaction implements MutatorSql {
|
|
163
|
-
constructor(
|
|
164
|
-
private readonly direct: SqlStorageDirect,
|
|
165
|
-
private readonly compileQuery: (ast: unknown) => CompiledQuery
|
|
166
|
-
) {}
|
|
167
|
-
|
|
168
|
-
async exec(sql: string, params: readonly unknown[] = []): Promise<void> {
|
|
169
|
-
this.direct.exec(sql, params)
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
async query<Row extends Record<string, unknown> = Record<string, unknown>>(
|
|
173
|
-
sql: string,
|
|
174
|
-
params: readonly unknown[] = []
|
|
175
|
-
): Promise<Row[]> {
|
|
176
|
-
return this.direct.query<Row>(sql, params)
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
async queryAst<Row extends Record<string, unknown> = Record<string, unknown>>(
|
|
180
|
-
ast: unknown
|
|
181
|
-
): Promise<Row[]> {
|
|
182
|
-
const compiled = this.compileQuery(ast)
|
|
183
|
-
return this.direct.query<Row>(compiled.sql, compiled.params.map(decodeBinding))
|
|
184
|
-
}
|
|
185
|
-
}
|
package/src/types.ts
DELETED
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
export type JsonPrimitive = string | number | boolean | null
|
|
2
|
-
export type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue }
|
|
3
|
-
|
|
4
|
-
export type NormalizedClaims = {
|
|
5
|
-
/** Stable consumer user id used for client-group ownership. */
|
|
6
|
-
userID: string
|
|
7
|
-
[claim: string]: JsonValue
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export type ZeroSchemaConfig = {
|
|
11
|
-
readonly tables: Readonly<
|
|
12
|
-
Record<
|
|
13
|
-
string,
|
|
14
|
-
{
|
|
15
|
-
readonly columns: Readonly<Record<string, { readonly type: string }>>
|
|
16
|
-
readonly primaryKey: readonly string[]
|
|
17
|
-
}
|
|
18
|
-
>
|
|
19
|
-
>
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface SyncSql {
|
|
23
|
-
exec(sql: string, params?: readonly unknown[]): void
|
|
24
|
-
query<Row extends Record<string, unknown> = Record<string, unknown>>(
|
|
25
|
-
sql: string,
|
|
26
|
-
params?: readonly unknown[]
|
|
27
|
-
): Row[]
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export interface MutatorSql {
|
|
31
|
-
exec(sql: string, params?: readonly unknown[]): Promise<void>
|
|
32
|
-
query<Row extends Record<string, unknown> = Record<string, unknown>>(
|
|
33
|
-
sql: string,
|
|
34
|
-
params?: readonly unknown[]
|
|
35
|
-
): Promise<Row[]>
|
|
36
|
-
/** Execute a validated Zero AST inside the current application transaction. */
|
|
37
|
-
queryAst<Row extends Record<string, unknown> = Record<string, unknown>>(
|
|
38
|
-
ast: JsonValue
|
|
39
|
-
): Promise<Row[]>
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export type DeferredEffect = () => void | Promise<void>
|
|
43
|
-
|
|
44
|
-
export type MutatorContext = {
|
|
45
|
-
claims: NormalizedClaims
|
|
46
|
-
/** Current Zero mutation identity, when invoked by the production host. */
|
|
47
|
-
clientID?: string
|
|
48
|
-
mutationID?: string | number
|
|
49
|
-
defer(effect: DeferredEffect): void
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export type RegisteredMutator = (
|
|
53
|
-
tx: MutatorSql,
|
|
54
|
-
args: JsonValue,
|
|
55
|
-
context: MutatorContext
|
|
56
|
-
) => void | Promise<void>
|
|
57
|
-
|
|
58
|
-
export type MutatorRegistry = Readonly<Record<string, RegisteredMutator>>
|
|
59
|
-
|
|
60
|
-
/** Preserve mutator names while making the host-facing registry immutable. */
|
|
61
|
-
export function registerMutators<
|
|
62
|
-
const Registry extends Record<string, RegisteredMutator>,
|
|
63
|
-
>(registry: Registry): Readonly<Registry> {
|
|
64
|
-
return Object.freeze({ ...registry })
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export class MutationApplicationError extends Error {
|
|
68
|
-
constructor(
|
|
69
|
-
readonly details: string,
|
|
70
|
-
message = details
|
|
71
|
-
) {
|
|
72
|
-
super(message)
|
|
73
|
-
this.name = 'MutationApplicationError'
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export type VisibilityFilter = {
|
|
78
|
-
/** SQL WHERE fragment only, without the WHERE keyword. */
|
|
79
|
-
sql: string
|
|
80
|
-
params?: readonly JsonPrimitive[]
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export type VisibilityConfig = {
|
|
84
|
-
/** True only when every predicate depends on the selected row alone. */
|
|
85
|
-
rowLocal: boolean | ((claims: NormalizedClaims) => boolean)
|
|
86
|
-
filter(table: string, claims: NormalizedClaims): VisibilityFilter | undefined
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export type QueryResolver = (
|
|
90
|
-
name: string,
|
|
91
|
-
args: readonly JsonValue[],
|
|
92
|
-
claims: NormalizedClaims,
|
|
93
|
-
env: SyncHostEnv
|
|
94
|
-
) => JsonValue | Promise<JsonValue>
|
|
95
|
-
|
|
96
|
-
export type PullCaps = {
|
|
97
|
-
maxChangeRows: number
|
|
98
|
-
maxChangeBytes: number
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export interface SyncHostEnv {
|
|
102
|
-
SYNC_DO: DurableObjectNamespace
|
|
103
|
-
ADMIN_KEY?: string
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export type ServiceBinding = {
|
|
107
|
-
fetch(input: string | Request, init?: RequestInit): Promise<Response>
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export type UpstreamConfig = {
|
|
111
|
-
/** Env key for the service binding that owns the app write endpoint and feed. */
|
|
112
|
-
binding: string
|
|
113
|
-
/** Path to this namespace on the bound service (for example `/data/<id>`). */
|
|
114
|
-
namespacePath: string | ((namespace: string) => string)
|
|
115
|
-
/** Feed page size; the cursor loop continues until the reported head is reached. */
|
|
116
|
-
changeLimit?: number
|
|
117
|
-
/** Active wake-socket alarm safety net. Defaults to 15 seconds. */
|
|
118
|
-
intervalMs?: number
|
|
119
|
-
/** Billable SQLite rows written by ingest per rolling window. Defaults to 150,000. */
|
|
120
|
-
ingestBudgetRows?: number
|
|
121
|
-
/** Rolling ingest budget window. Defaults to five minutes. */
|
|
122
|
-
ingestBudgetWindowMs?: number
|
|
123
|
-
/** Initial breaker cooldown. Defaults to one second. */
|
|
124
|
-
ingestBackoffMs?: number
|
|
125
|
-
/** Maximum breaker cooldown. Defaults to one minute. */
|
|
126
|
-
ingestMaxBackoffMs?: number
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
export type DelegatedPushRetryConfig = {
|
|
130
|
-
/** Total attempts including the first request. Defaults to 3. */
|
|
131
|
-
maxAttempts?: number
|
|
132
|
-
/** Initial exponential delay. Defaults to 100ms. */
|
|
133
|
-
initialBackoffMs?: number
|
|
134
|
-
/** Delay cap. Defaults to 1,000ms. */
|
|
135
|
-
maxBackoffMs?: number
|
|
136
|
-
/** Per-attempt service-binding timeout. Defaults to 5,000ms. */
|
|
137
|
-
timeoutMs?: number
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
export type SyncHostConfig<Env extends SyncHostEnv = SyncHostEnv> = {
|
|
141
|
-
hostVersion: string
|
|
142
|
-
schema: ZeroSchemaConfig
|
|
143
|
-
mutators?: MutatorRegistry
|
|
144
|
-
/** Absolute app push path on the upstream service binding. */
|
|
145
|
-
mutateUrl?: string
|
|
146
|
-
/** Absolute origin used for delegated push requests through the service binding. */
|
|
147
|
-
mutateOrigin?: string
|
|
148
|
-
/** Env binding for delegated pushes; defaults to upstream.binding. */
|
|
149
|
-
mutateBinding?: string
|
|
150
|
-
delegatedPushRetry?: DelegatedPushRetryConfig
|
|
151
|
-
/** Required for delegated push; forbidden with local mutators (no dual apply). */
|
|
152
|
-
upstream?: UpstreamConfig
|
|
153
|
-
/** Application DDL and optional seed, called before sync-core schema init. */
|
|
154
|
-
initialize(sql: SyncSql): void
|
|
155
|
-
authenticate(
|
|
156
|
-
request: Request,
|
|
157
|
-
env: Env
|
|
158
|
-
): NormalizedClaims | null | Promise<NormalizedClaims | null>
|
|
159
|
-
/** Authorize the advisory wake socket before selecting a namespace DO.
|
|
160
|
-
* Browser clients should present a short-lived, namespace-scoped capability
|
|
161
|
-
* in the query string because WebSocket cannot set request headers. */
|
|
162
|
-
authorizeWake(request: Request, env: Env): boolean | Promise<boolean>
|
|
163
|
-
/** Authorize upstream service notifications before selecting a namespace DO. */
|
|
164
|
-
authorizeNotify(request: Request, env: Env): boolean | Promise<boolean>
|
|
165
|
-
/** Resolve the first path component or another consumer-defined namespace. */
|
|
166
|
-
namespace(request: Request): string | null
|
|
167
|
-
visibility?: VisibilityConfig
|
|
168
|
-
/** Enable desired-query pulls for this namespace and resolve named queries
|
|
169
|
-
* into validated Zero ASTs before they reach sync-core. */
|
|
170
|
-
queryAware?: boolean | ((claims: NormalizedClaims) => boolean)
|
|
171
|
-
resolveQuery?: QueryResolver
|
|
172
|
-
/** Server-owned invalidation epoch for permission/schema transforms. */
|
|
173
|
-
queryTransformVersion?: number | ((claims: NormalizedClaims) => number)
|
|
174
|
-
/** Enable consumer visibility from the first request. Defaults to false for harnesses. */
|
|
175
|
-
visibilityEnabled?: boolean
|
|
176
|
-
retainChanges?: number
|
|
177
|
-
caps?: Partial<PullCaps>
|
|
178
|
-
idleTeardownMs?: number
|
|
179
|
-
wakeCoalesceMs?: number
|
|
180
|
-
authorizeAdmin?: (request: Request, env: Env) => boolean | Promise<boolean>
|
|
181
|
-
}
|