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.
Files changed (46) hide show
  1. package/README.md +117 -7
  2. package/dist/bun-wasm-loader.d.ts +1 -0
  3. package/dist/bun-wasm-loader.js +20 -0
  4. package/dist/config.d.ts +3 -0
  5. package/dist/config.js +93 -0
  6. package/{src → dist}/generated/sync_wasm.d.ts +2 -2
  7. package/{src → dist}/generated/sync_wasm.js +3 -2
  8. package/dist/generated/sync_wasm_bg.wasm +0 -0
  9. package/dist/host.d.ts +12 -0
  10. package/dist/host.js +1512 -0
  11. package/dist/index.d.ts +8 -0
  12. package/dist/index.js +4 -0
  13. package/dist/mutation-error.d.ts +9 -0
  14. package/dist/mutation-error.js +23 -0
  15. package/dist/node-wasm-loader.d.ts +1 -0
  16. package/dist/node-wasm-loader.js +22 -0
  17. package/dist/post-commit.d.ts +13 -0
  18. package/dist/post-commit.js +31 -0
  19. package/dist/query-compiler.d.ts +4 -0
  20. package/dist/query-compiler.js +4 -0
  21. package/dist/sql-storage-adapter.d.ts +69 -0
  22. package/dist/sql-storage-adapter.js +192 -0
  23. package/dist/transaction-query.d.ts +72 -0
  24. package/dist/transaction-query.js +379 -0
  25. package/dist/types.d.ts +144 -0
  26. package/dist/types.js +5 -0
  27. package/dist/vite-wasm-loader.d.ts +3 -0
  28. package/dist/vite-wasm-loader.js +24 -0
  29. package/dist/wasm.d.ts +1 -0
  30. package/dist/wasm.js +7 -0
  31. package/dist/write-safeguards.d.ts +53 -0
  32. package/dist/write-safeguards.js +186 -0
  33. package/package.json +63 -12
  34. package/src/config.ts +0 -93
  35. package/src/generated/sync_wasm_bg.wasm +0 -0
  36. package/src/harness-config.ts +0 -487
  37. package/src/harness-worker.ts +0 -33
  38. package/src/host.ts +0 -1948
  39. package/src/index.ts +0 -22
  40. package/src/ingest-harness-worker.ts +0 -440
  41. package/src/platform-probe-worker.ts +0 -305
  42. package/src/sql-storage-adapter.ts +0 -185
  43. package/src/types.ts +0 -181
  44. package/src/write-safeguards.ts +0 -248
  45. /package/{src → dist}/generated/package.json +0 -0
  46. /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
- }