orez 0.2.27 → 0.2.30

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 (157) hide show
  1. package/dist/cf-do/worker.d.ts +3 -0
  2. package/dist/cf-do/worker.d.ts.map +1 -1
  3. package/dist/cf-do/worker.js +37 -15
  4. package/dist/cf-do/worker.js.map +1 -1
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +8 -0
  7. package/dist/index.js.map +1 -1
  8. package/package.json +3 -4
  9. package/src/admin/admin-data.test.ts +0 -348
  10. package/src/admin/http-proxy.ts +0 -252
  11. package/src/admin/log-store.ts +0 -192
  12. package/src/admin/server.ts +0 -471
  13. package/src/admin/ui.ts +0 -1322
  14. package/src/bench/proxy-throughput.bench.ts +0 -343
  15. package/src/bench/serial-mutations.bench.ts +0 -270
  16. package/src/browser.ts +0 -203
  17. package/src/cf-do/.wrangler/cache/cf.json +0 -1
  18. package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite +0 -0
  19. package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-shm +0 -0
  20. package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-wal +0 -0
  21. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/0ffaabee41a60e04dd0eb7db3073f0a40139e6a97ccd26823967acb652b89a7b.sqlite +0 -0
  22. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite +0 -0
  23. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite-shm +0 -0
  24. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite-wal +0 -0
  25. package/src/cf-do/.wrangler/tmp/bundle-0z4CpE/middleware-insertion-facade.js +0 -11
  26. package/src/cf-do/.wrangler/tmp/bundle-0z4CpE/middleware-loader.entry.ts +0 -134
  27. package/src/cf-do/.wrangler/tmp/bundle-vYmw0E/middleware-insertion-facade.js +0 -11
  28. package/src/cf-do/.wrangler/tmp/bundle-vYmw0E/middleware-loader.entry.ts +0 -134
  29. package/src/cf-do/.wrangler/tmp/dev-cbILNo/worker.js +0 -1059
  30. package/src/cf-do/.wrangler/tmp/dev-cbILNo/worker.js.map +0 -8
  31. package/src/cf-do/.wrangler/tmp/dev-qbho19/worker.js +0 -1059
  32. package/src/cf-do/.wrangler/tmp/dev-qbho19/worker.js.map +0 -8
  33. package/src/cf-do/ARCHITECTURE.md +0 -93
  34. package/src/cf-do/CHAT_E2E.md +0 -213
  35. package/src/cf-do/watermark.test.ts +0 -103
  36. package/src/cf-do/watermark.ts +0 -118
  37. package/src/cf-do/worker.ts +0 -1041
  38. package/src/cf-do/wrangler.toml +0 -11
  39. package/src/cf-pglite/README.md +0 -19
  40. package/src/change-tracking.ts +0 -25
  41. package/src/child-process.test.ts +0 -147
  42. package/src/child-process.ts +0 -90
  43. package/src/cli-entry.ts +0 -72
  44. package/src/cli.test.ts +0 -40
  45. package/src/cli.ts +0 -1214
  46. package/src/config.ts +0 -150
  47. package/src/do-sql-tracking.test.ts +0 -19
  48. package/src/do-sql-tracking.ts +0 -19
  49. package/src/index.ts +0 -1215
  50. package/src/integration/integration.test.ts +0 -517
  51. package/src/integration/native-binary.guard.test.ts +0 -13
  52. package/src/integration/native-startup.test.ts +0 -44
  53. package/src/integration/replication-latency.test.ts +0 -428
  54. package/src/integration/restore-live-stress.test.ts +0 -433
  55. package/src/integration/restore-reset.test.ts +0 -400
  56. package/src/integration/restore.test.ts +0 -274
  57. package/src/integration/test-permissions.ts +0 -147
  58. package/src/load-config.ts +0 -46
  59. package/src/log.ts +0 -96
  60. package/src/mutex.ts +0 -47
  61. package/src/pg-proxy-browser.singledb.test.ts +0 -233
  62. package/src/pg-proxy-browser.ts +0 -2022
  63. package/src/pg-proxy-do-backend.test.ts +0 -3890
  64. package/src/pg-proxy-do-backend.ts +0 -7191
  65. package/src/pg-proxy.ts +0 -1087
  66. package/src/pg-sqlite-compiler/README.md +0 -53
  67. package/src/pg-sqlite-compiler/catalog/seed.ts +0 -524
  68. package/src/pg-sqlite-compiler/fixtures/pgsqlite/arithmetic.json +0 -307
  69. package/src/pg-sqlite-compiler/fixtures/pgsqlite/array.json +0 -377
  70. package/src/pg-sqlite-compiler/fixtures/pgsqlite/cast.json +0 -12
  71. package/src/pg-sqlite-compiler/fixtures/pgsqlite/catalog.json +0 -447
  72. package/src/pg-sqlite-compiler/fixtures/pgsqlite/create-table.json +0 -32
  73. package/src/pg-sqlite-compiler/fixtures/pgsqlite/datetime.json +0 -397
  74. package/src/pg-sqlite-compiler/fixtures/pgsqlite/enum.json +0 -337
  75. package/src/pg-sqlite-compiler/fixtures/pgsqlite/insert.json +0 -337
  76. package/src/pg-sqlite-compiler/fixtures/pgsqlite/json.json +0 -537
  77. package/src/pg-sqlite-compiler/fixtures/pgsqlite/misc.json +0 -1837
  78. package/src/pg-sqlite-compiler/index.ts +0 -73
  79. package/src/pg-sqlite-compiler/integration.test.ts +0 -136
  80. package/src/pg-sqlite-compiler/passes/ast-utils.ts +0 -113
  81. package/src/pg-sqlite-compiler/passes/catalog.ts +0 -65
  82. package/src/pg-sqlite-compiler/passes/datetime.ts +0 -74
  83. package/src/pg-sqlite-compiler/passes/index.ts +0 -49
  84. package/src/pg-sqlite-compiler/passes/types.ts +0 -156
  85. package/src/pg-sqlite-compiler/smoke.test.ts +0 -69
  86. package/src/pg-sqlite-compiler/test/catalog.test.ts +0 -171
  87. package/src/pg-sqlite-compiler/test/corpus.test.ts +0 -161
  88. package/src/pg-sqlite-compiler/test/datetime.oracle.test.ts +0 -102
  89. package/src/pg-sqlite-compiler/test/oracle.ts +0 -237
  90. package/src/pg-sqlite-compiler/test/types.test.ts +0 -109
  91. package/src/pg-sqlite-compiler/types.ts +0 -63
  92. package/src/pglite-ipc.test.ts +0 -116
  93. package/src/pglite-ipc.ts +0 -266
  94. package/src/pglite-manager.ts +0 -557
  95. package/src/pglite-web-proxy.test.ts +0 -57
  96. package/src/pglite-web-proxy.ts +0 -221
  97. package/src/pglite-web-worker.ts +0 -152
  98. package/src/pglite-worker-thread.ts +0 -253
  99. package/src/port.ts +0 -25
  100. package/src/process-title.ts +0 -9
  101. package/src/recovery.ts +0 -155
  102. package/src/replication/change-tracker.test.ts +0 -357
  103. package/src/replication/change-tracker.ts +0 -279
  104. package/src/replication/handler.test.ts +0 -511
  105. package/src/replication/handler.ts +0 -1190
  106. package/src/replication/pgoutput-encoder.test.ts +0 -697
  107. package/src/replication/pgoutput-encoder.ts +0 -373
  108. package/src/replication/tcp-replication.test.ts +0 -876
  109. package/src/replication/zero-compat.test.ts +0 -1150
  110. package/src/restore-stress.test.ts +0 -188
  111. package/src/s3-local.ts +0 -203
  112. package/src/shim/hooks.mjs +0 -120
  113. package/src/shim/register.mjs +0 -4
  114. package/src/sqlite-mode/apply-mode.ts +0 -224
  115. package/src/sqlite-mode/index.ts +0 -15
  116. package/src/sqlite-mode/native-binary.ts +0 -89
  117. package/src/sqlite-mode/package-resolve.ts +0 -17
  118. package/src/sqlite-mode/resolve-mode.ts +0 -80
  119. package/src/sqlite-mode/shim-template.ts +0 -159
  120. package/src/sqlite-mode/sqlite-mode.test.ts +0 -427
  121. package/src/sqlite-mode/types.ts +0 -30
  122. package/src/vite-plugin.ts +0 -67
  123. package/src/wasm-sqlite.test.ts +0 -537
  124. package/src/worker/browser-admin.ts +0 -52
  125. package/src/worker/browser-build-config.test.ts +0 -71
  126. package/src/worker/browser-build-config.ts +0 -109
  127. package/src/worker/browser-embed-admin.test.ts +0 -75
  128. package/src/worker/browser-embed.ts +0 -345
  129. package/src/worker/cf-patches.ts +0 -384
  130. package/src/worker/embed-integration.test.ts +0 -321
  131. package/src/worker/index.ts +0 -138
  132. package/src/worker/shims/fastify.test.ts +0 -255
  133. package/src/worker/shims/fastify.ts +0 -306
  134. package/src/worker/shims/http-service.test.ts +0 -355
  135. package/src/worker/shims/http-service.ts +0 -293
  136. package/src/worker/shims/node-stub.ts +0 -290
  137. package/src/worker/shims/oxfmt.ts +0 -3
  138. package/src/worker/shims/postgres-browser.ts +0 -59
  139. package/src/worker/shims/postgres-socket.test.ts +0 -576
  140. package/src/worker/shims/postgres-socket.ts +0 -310
  141. package/src/worker/shims/postgres.test.ts +0 -364
  142. package/src/worker/shims/postgres.ts +0 -1454
  143. package/src/worker/shims/sqlite-browser.test.ts +0 -233
  144. package/src/worker/shims/sqlite-browser.ts +0 -175
  145. package/src/worker/shims/sqlite.test.ts +0 -786
  146. package/src/worker/shims/sqlite.ts +0 -978
  147. package/src/worker/shims/stream-browser.ts +0 -15
  148. package/src/worker/shims/ws-browser.test.ts +0 -205
  149. package/src/worker/shims/ws-browser.ts +0 -248
  150. package/src/worker/shims/ws.test.ts +0 -288
  151. package/src/worker/shims/ws.ts +0 -467
  152. package/src/worker/shims/zero-process-env.ts +0 -11
  153. package/src/worker/types.ts +0 -75
  154. package/src/worker/worker-integration.test.ts +0 -223
  155. package/src/worker/worker.test.ts +0 -136
  156. package/src/worker/zero-cache-embed-cf.ts +0 -463
  157. package/src/worker/zero-cache-embed.ts +0 -277
@@ -1,1041 +0,0 @@
1
- // @ts-nocheck — cloudflare:workers types not available in orez
2
- import { DurableObject } from 'cloudflare:workers'
3
-
4
- import { trackedChangeRow } from '../do-sql-tracking.js'
5
- import { DurableWatermarkState } from './watermark.js'
6
-
7
- /**
8
- * zero-do: Durable Object that exposes raw SQL execution over ctx.storage.sql.
9
- *
10
- * The production Cloudflare path runs real zero-cache via
11
- * src/worker/zero-cache-embed-cf.ts, with DoBackend calling this DO for
12
- * Postgres-protocol-backed SQL. The WS sync handler here is kept for
13
- * development/protocol experiments only; it is not the production replacement
14
- * for zero-cache.
15
- *
16
- * Modes:
17
- * WS /sync/v49/connect — bespoke Zero sync protocol (dev/protocol testing)
18
- * POST /exec — raw SQL execution (from DoBackend adapter)
19
- * POST /batch — atomic batch execution via ctx.storage.transaction()
20
- */
21
-
22
- interface Env {
23
- ZERO_DO: DurableObjectNamespace
24
- }
25
- interface SchemaTable {
26
- primaryKey: string[]
27
- columns: Record<string, { type: string; optional?: boolean }>
28
- }
29
- interface ClientSchema {
30
- tables: Record<string, SchemaTable>
31
- }
32
- interface DesiredQuery {
33
- hash: string
34
- tableNames: string[]
35
- }
36
- interface DesiredQueryPatchOp {
37
- op: 'put' | 'del' | 'clear'
38
- hash?: string
39
- name?: string
40
- ast?: any
41
- }
42
- interface CrudOp {
43
- op: 'insert' | 'update' | 'upsert' | 'delete'
44
- tableName: string
45
- value?: Record<string, unknown>
46
- primaryKey?: string[]
47
- }
48
- interface PushMutation {
49
- type: string
50
- name: string
51
- clientID: string
52
- id: number
53
- args: unknown[]
54
- }
55
- interface PushBody {
56
- clientGroupID?: string
57
- mutations: PushMutation[]
58
- }
59
- interface SqlTrack {
60
- tableName: string
61
- operation: 'INSERT' | 'UPDATE' | 'DELETE'
62
- returnRows?: boolean
63
- rowColumns?: string[]
64
- }
65
- interface SqlExecStatement {
66
- sql: string
67
- params?: unknown[]
68
- track?: SqlTrack
69
- }
70
- interface SocketAttachment {
71
- clientID: string
72
- clientGroupID: string
73
- userID: string
74
- cookie: string | null
75
- initialized: boolean
76
- desiredTableNames: string[]
77
- desiredQueries: DesiredQuery[]
78
- }
79
- interface HibernatableWebSocket extends WebSocket {
80
- serializeAttachment(value: SocketAttachment): void
81
- deserializeAttachment(): SocketAttachment | undefined
82
- }
83
-
84
- const SCHEMA_VERSION = 1
85
- const SQL_ERROR_SNIPPET_RADIUS = 1600
86
- const SQL_ERROR_FALLBACK_LIMIT = 4000
87
-
88
- function quoteIdent(name: string): string {
89
- return `"${name.replace(/"/g, '""')}"`
90
- }
91
-
92
- function sqliteErrorOffset(message: string): number | null {
93
- const marker = 'offset '
94
- const start = message.indexOf(marker)
95
- if (start < 0) return null
96
- let index = start + marker.length
97
- let digits = ''
98
- while (index < message.length) {
99
- const code = message.charCodeAt(index)
100
- if (code < 48 || code > 57) break
101
- digits += message[index]
102
- index++
103
- }
104
- if (!digits) return null
105
- const offset = Number(digits)
106
- return Number.isFinite(offset) ? offset : null
107
- }
108
-
109
- function sqlErrorSnippet(sql: string, message: string): string {
110
- const offset = sqliteErrorOffset(message)
111
- if (offset !== null) {
112
- const start = Math.max(0, offset - SQL_ERROR_SNIPPET_RADIUS)
113
- const end = Math.min(sql.length, offset + SQL_ERROR_SNIPPET_RADIUS)
114
- return `${start > 0 ? '...' : ''}${sql.slice(start, end)}${end < sql.length ? '...' : ''}`
115
- }
116
- if (sql.length <= SQL_ERROR_FALLBACK_LIMIT) return sql
117
- return `${sql.slice(0, SQL_ERROR_FALLBACK_LIMIT)}...`
118
- }
119
-
120
- export class ZeroDO extends DurableObject {
121
- private sql: any
122
- private watermarks: DurableWatermarkState
123
- private schemaTables = new Set<string>()
124
- private tableSchemas = new Map<string, SchemaTable>()
125
-
126
- constructor(ctx: DurableObjectState, env: Env) {
127
- super(ctx, env)
128
- this.sql = ctx.storage.sql
129
- this.watermarks = new DurableWatermarkState(this.sql)
130
- }
131
-
132
- async fetch(request: Request): Promise<Response> {
133
- const url = new URL(request.url)
134
- if (request.method === 'OPTIONS') {
135
- return new Response(null, {
136
- headers: {
137
- 'Access-Control-Allow-Origin': '*',
138
- 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
139
- 'Access-Control-Allow-Headers': '*',
140
- },
141
- })
142
- }
143
- if (url.pathname.startsWith('/sync/v') && url.pathname.endsWith('/connect'))
144
- return this.handleSyncConnect(request, url)
145
- if (
146
- (url.pathname === '/zero/push' || url.pathname === '/api/zero/push') &&
147
- request.method === 'POST'
148
- )
149
- return this.handleHttpPush(request)
150
- if (url.pathname === '/exec' && request.method === 'POST')
151
- return this.handleExec(request)
152
- if (url.pathname === '/batch' && request.method === 'POST')
153
- return this.handleBatch(request)
154
- if (
155
- url.pathname === '/changes' &&
156
- (request.method === 'GET' || request.method === 'POST')
157
- )
158
- return this.handleChanges(request, url)
159
- if (url.pathname === '/notify' && request.method === 'POST')
160
- return Response.json({ ok: true, cookie: this.cookie() })
161
- return new Response('not found', { status: 404 })
162
- }
163
-
164
- // ── Zero sync protocol ──────────────────────────────────────────────────
165
-
166
- private handleSyncConnect(request: Request, url: URL): Response {
167
- if (request.headers.get('upgrade')?.toLowerCase() !== 'websocket') {
168
- return new Response('expected websocket upgrade', { status: 426 })
169
- }
170
- const pair = new WebSocketPair()
171
- const client = pair[0]
172
- const server = pair[1] as HibernatableWebSocket
173
-
174
- const clientID = url.searchParams.get('clientID') || 'anon'
175
- const clientGroupID = url.searchParams.get('clientGroupID') || 'default'
176
- const userID = url.searchParams.get('userID') || 'anon'
177
- const wsid = url.searchParams.get('wsid') || crypto.randomUUID()
178
- const baseCookie = url.searchParams.get('baseCookie')
179
-
180
- this.ctx.acceptWebSocket(server)
181
- server.serializeAttachment({
182
- clientID,
183
- clientGroupID,
184
- userID,
185
- cookie: baseCookie ? baseCookie : null,
186
- initialized: false,
187
- desiredTableNames: [],
188
- desiredQueries: [],
189
- })
190
- this.sendJSON(server, ['connected', { wsid, timestamp: Date.now() }])
191
-
192
- const secProtocol = request.headers.get('sec-websocket-protocol')
193
- if (secProtocol) {
194
- const initData = decodeInitConnection(secProtocol)
195
- if (initData) {
196
- const clientSchema = initData[1]?.clientSchema as ClientSchema | undefined
197
- const patch = (initData[1]?.desiredQueriesPatch || []) as DesiredQueryPatchOp[]
198
- this.applyDesiredQueries(server, patch, clientSchema)
199
- }
200
- }
201
- return new Response(null, {
202
- status: 101,
203
- headers: secProtocol ? { 'Sec-WebSocket-Protocol': secProtocol } : undefined,
204
- webSocket: client,
205
- } as ResponseInit & { webSocket: WebSocket })
206
- }
207
-
208
- async webSocketMessage(socket: WebSocket, messageData: string | ArrayBuffer) {
209
- this.watermarks.ensureTables()
210
- const ws = socket as HibernatableWebSocket
211
- const attachment = this.readSocketAttachment(ws)
212
- if (!attachment) return
213
- const message = this.parseMessage(messageData)
214
- if (!message) return
215
- const body = message[1] || {}
216
-
217
- switch (message[0]) {
218
- case 'initConnection':
219
- case 'changeDesiredQueries':
220
- this.applyDesiredQueries(
221
- ws,
222
- (body.desiredQueriesPatch || []) as DesiredQueryPatchOp[],
223
- body.clientSchema as ClientSchema | undefined
224
- )
225
- break
226
- case 'push':
227
- this.handlePush(ws, attachment, message[1] as PushBody)
228
- break
229
- case 'pull':
230
- this.handlePull(ws, message[1] as any)
231
- break
232
- case 'ping':
233
- this.sendJSON(ws, ['pong', {}])
234
- break
235
- }
236
- }
237
-
238
- webSocketClose(
239
- _socket: WebSocket,
240
- _code: number,
241
- _reason: string,
242
- _wasClean: boolean
243
- ) {}
244
-
245
- private applyDesiredQueries(
246
- socket: HibernatableWebSocket,
247
- patch: DesiredQueryPatchOp[],
248
- clientSchema?: ClientSchema
249
- ) {
250
- const attachment = this.readSocketAttachment(socket)
251
- if (!attachment) return
252
- if (clientSchema) this.ensureSchemaTables(clientSchema)
253
-
254
- let nextAttachment = this.applyDesiredQueryPatch(attachment, patch)
255
- socket.serializeAttachment(nextAttachment)
256
-
257
- if (!nextAttachment.initialized) {
258
- nextAttachment = this.sendSyncPoke(
259
- socket,
260
- { ...nextAttachment, initialized: true },
261
- { lastMutationIDChanges: {}, rowsPatch: [] }
262
- )
263
- }
264
-
265
- if (patch.length === 0) return
266
-
267
- const rowsPatch = [
268
- { op: 'clear' as const },
269
- ...this.rowsPatchForTables(nextAttachment.desiredTableNames),
270
- ]
271
- this.sendSyncPoke(socket, nextAttachment, {
272
- gotQueriesPatch: this.gotQueriesPatch(patch),
273
- rowsPatch,
274
- })
275
- }
276
-
277
- private applyDesiredQueryPatch(
278
- attachment: SocketAttachment,
279
- patch: DesiredQueryPatchOp[]
280
- ): SocketAttachment {
281
- const desiredQueries = new Map<string, string[]>()
282
- for (const query of attachment.desiredQueries || [])
283
- desiredQueries.set(query.hash, query.tableNames)
284
-
285
- for (const op of patch) {
286
- if (op.op === 'clear') {
287
- desiredQueries.clear()
288
- } else if (op.op === 'put' && op.hash) {
289
- desiredQueries.set(op.hash, this.resolveTablesFromPatch([op]))
290
- } else if (op.op === 'del' && op.hash) {
291
- desiredQueries.delete(op.hash)
292
- }
293
- }
294
-
295
- const queries = [...desiredQueries.entries()].map(([hash, tableNames]) => ({
296
- hash,
297
- tableNames,
298
- }))
299
- return {
300
- ...attachment,
301
- desiredQueries: queries,
302
- desiredTableNames: [...new Set(queries.flatMap((query) => query.tableNames))],
303
- }
304
- }
305
-
306
- private gotQueriesPatch(patch: DesiredQueryPatchOp[]) {
307
- const got: Array<{ op: 'put' | 'del'; hash: string } | { op: 'clear' }> = []
308
- for (const op of patch) {
309
- if (op.op === 'clear') got.push({ op: 'clear' })
310
- else if (op.hash) got.push({ op: op.op, hash: op.hash })
311
- }
312
- return got
313
- }
314
-
315
- private rowsPatchForTables(tableNames: string[]): any[] {
316
- const rowsPatch: any[] = []
317
- for (const tn of tableNames) {
318
- if (!this.tableExists(tn)) continue
319
- for (const row of this.readAllRows(tn))
320
- rowsPatch.push({ op: 'put', tableName: tn, value: row })
321
- }
322
- return rowsPatch
323
- }
324
-
325
- private resolveTablesFromPatch(patch: DesiredQueryPatchOp[]): string[] {
326
- const tables: string[] = []
327
- for (const op of patch) {
328
- const tableFromName = this.tableNameFromOperationName(op.name)
329
- if (tableFromName) tables.push(tableFromName)
330
- if (op.ast) this.extractTableFromAST(op.ast, tables)
331
- }
332
- return tables
333
- }
334
-
335
- private extractTableFromAST(ast: any, tables: string[]) {
336
- if (ast?.table) tables.push(ast.table)
337
- if (ast?.related)
338
- for (const rel of ast.related) {
339
- if (rel?.subquery?.table) tables.push(rel.subquery.table)
340
- if (rel?.subquery?.related) this.extractTableFromAST(rel.subquery, tables)
341
- }
342
- }
343
-
344
- private handlePush(socket: WebSocket, attachment: SocketAttachment, body: PushBody) {
345
- const mutations = Array.isArray(body?.mutations) ? body.mutations : []
346
- const before = this.watermark()
347
- const mutationResults: any[] = []
348
- const lastMutationIDChanges: Record<string, number> = {}
349
- for (const m of mutations) {
350
- const result = this.applyMutation(m)
351
- mutationResults.push({ id: { clientID: m.clientID, id: m.id }, result })
352
- lastMutationIDChanges[m.clientID] = m.id
353
- }
354
- this.sendJSON(socket, ['pushResponse', { mutations: mutationResults }])
355
- const after = this.watermark()
356
- const changes = after > before ? this.readChangesSince(before) : []
357
- const rowsPatch = changes.map((c) => this.syncRowPatchFromChange(c))
358
- if (Object.keys(lastMutationIDChanges).length > 0 || rowsPatch.length > 0)
359
- this.broadcastMutationPoke(attachment, {
360
- lastMutationIDChanges,
361
- rowsPatch,
362
- })
363
- }
364
-
365
- private async handleHttpPush(request: Request): Promise<Response> {
366
- try {
367
- const body = (await request.json()) as any
368
- const before = this.watermark()
369
- const mutations = Array.isArray(body?.mutations) ? body.mutations : []
370
- const mutationResults: any[] = []
371
- const lastMutationIDChanges: Record<string, number> = {}
372
- for (const m of mutations) {
373
- const result = this.applyMutation(m)
374
- mutationResults.push({ id: { clientID: m.clientID, id: m.id }, result })
375
- lastMutationIDChanges[m.clientID] = m.id
376
- }
377
- const after = this.watermark()
378
- const changes = after > before ? this.readChangesSince(before) : []
379
- const rowsPatch = changes.map((c) => this.syncRowPatchFromChange(c))
380
- if (Object.keys(lastMutationIDChanges).length > 0 || rowsPatch.length > 0)
381
- this.broadcastPoke(body?.clientGroupID || 'default', {
382
- lastMutationIDChanges,
383
- rowsPatch,
384
- })
385
- return Response.json({ mutations: mutationResults })
386
- } catch (err: any) {
387
- return Response.json({ error: err.message }, { status: 500 })
388
- }
389
- }
390
-
391
- private handlePull(socket: HibernatableWebSocket, body: { requestID?: string }) {
392
- this.sendJSON(socket, [
393
- 'pull',
394
- {
395
- requestID: body?.requestID || crypto.randomUUID(),
396
- cookie: this.cookie(),
397
- lastMutationIDChanges: {},
398
- patch: [],
399
- },
400
- ])
401
- }
402
-
403
- // ── SQL execution endpoints ─────────────────────────────────────────────
404
-
405
- private async handleExec(request: Request): Promise<Response> {
406
- let sql = ''
407
- try {
408
- const body = (await request.json()) as {
409
- sql: string
410
- params?: unknown[]
411
- track?: SqlTrack
412
- }
413
- sql = body.sql
414
- const params = Array.isArray(body.params) ? body.params : []
415
- // Only wrap in ctx.storage.transaction() when the call has change-tracking
416
- // side effects (executeSQL writes BOTH the user table AND _zero_changes,
417
- // which must commit together to keep source-tab sync flicker-free). A
418
- // bare /exec is single-statement and ctx.storage.sql already serializes;
419
- // the transaction wrap was adding ~2-5ms × every call, which on chat's
420
- // 27k-stmt boot pushed orez backend startup past chat's 60s wait-for-port.
421
- const result = body.track
422
- ? await this.ctx.storage.transaction(() =>
423
- this.executeSQL(sql, params, body.track)
424
- )
425
- : this.executeSQL(sql, params)
426
- return Response.json(result)
427
- } catch (err: any) {
428
- const suffix = sql ? ` while executing: ${sqlErrorSnippet(sql, err.message)}` : ''
429
- console.error(`[exec-500] ${err.message} :: SQL=${sql.slice(0, 800)}`)
430
- return Response.json({ error: `${err.message}${suffix}` }, { status: 500 })
431
- }
432
- }
433
-
434
- /** Execute multiple statements atomically via ctx.storage.transaction() */
435
- private async handleBatch(request: Request): Promise<Response> {
436
- try {
437
- const { statements } = (await request.json()) as {
438
- statements: Array<string | SqlExecStatement>
439
- }
440
- const allRows = await this.ctx.storage.transaction(() => {
441
- const results: any[] = []
442
- for (const statement of statements) {
443
- const item = typeof statement === 'string' ? { sql: statement } : statement
444
- if (!item?.sql?.trim()) continue
445
- try {
446
- results.push(
447
- this.executeSQL(
448
- item.sql,
449
- Array.isArray(item.params) ? item.params : [],
450
- item.track
451
- )
452
- )
453
- } catch (err: any) {
454
- throw new Error(
455
- `${err.message} while executing: ${sqlErrorSnippet(item.sql, err.message)}`
456
- )
457
- }
458
- }
459
- return results
460
- })
461
- return Response.json({ results: allRows })
462
- } catch (err: any) {
463
- return Response.json({ error: err.message }, { status: 500 })
464
- }
465
- }
466
-
467
- private async handleChanges(request: Request, url: URL): Promise<Response> {
468
- try {
469
- let watermark = Number(
470
- url.searchParams.get('watermark') ?? url.searchParams.get('since') ?? 0
471
- )
472
- let limit = Number(url.searchParams.get('limit') ?? 1000)
473
- if (request.method === 'POST') {
474
- const body = (await request.json().catch(() => ({}))) as {
475
- watermark?: unknown
476
- since?: unknown
477
- limit?: unknown
478
- }
479
- watermark = Number(body.watermark ?? body.since ?? watermark)
480
- limit = Number(body.limit ?? limit)
481
- }
482
- if (!Number.isFinite(watermark) || watermark < 0) watermark = 0
483
- if (!Number.isFinite(limit) || limit <= 0) limit = 1000
484
- return Response.json({
485
- watermark: this.watermark(),
486
- changes: this.readChangesSince(watermark).slice(0, Math.min(limit, 10_000)),
487
- })
488
- } catch (err: any) {
489
- return Response.json({ error: err.message }, { status: 500 })
490
- }
491
- }
492
-
493
- private executeSQL(
494
- sql: string,
495
- params: unknown[] = [],
496
- track?: SqlTrack
497
- ): { rows: Record<string, unknown>[]; columns: string[]; affectedRows?: number } {
498
- const cursor = this.sql.exec(sql, ...params)
499
- const columns = Array.isArray(cursor.columnNames) ? cursor.columnNames : []
500
- const rows = this.cursorRows(cursor)
501
- if (!track) return { rows, columns }
502
-
503
- for (const row of rows) {
504
- const trackedRow = trackedChangeRow(row, track)
505
- if (track.operation === 'DELETE')
506
- this.appendTrackedChange(track.tableName, 'DELETE', null, trackedRow)
507
- else this.appendTrackedChange(track.tableName, track.operation, trackedRow, null)
508
- }
509
-
510
- return {
511
- rows: track.returnRows ? rows : [],
512
- columns: track.returnRows ? columns : [],
513
- affectedRows: rows.length,
514
- }
515
- }
516
-
517
- private cursorRows(cursor: any): Record<string, unknown>[] {
518
- return cursor.toArray().map((row: any) => {
519
- const obj: Record<string, unknown> = {}
520
- for (const k of Object.keys(row)) obj[k] = row[k]
521
- return obj
522
- })
523
- }
524
-
525
- // ── CRUD operations ──────────────────────────────────────────────────────
526
-
527
- private applyMutation(mutation: PushMutation) {
528
- if (mutation.type === 'crud' && mutation.name === '_zero_crud') {
529
- return this.applyCrudMutation(mutation)
530
- }
531
- if (mutation.name === '_zero_cleanupResults') return {}
532
- if (mutation.type === 'custom') return this.applyTableMutation(mutation)
533
- return {
534
- error: 'app',
535
- message: `unsupported mutation ${mutation.type}:${mutation.name}`,
536
- }
537
- }
538
-
539
- private applyTableMutation(mutation: PushMutation) {
540
- const [tableName, action] = this.tableActionFromMutationName(mutation.name)
541
- if (!tableName || !action)
542
- return { error: 'app', message: `invalid mutation name ${mutation.name}` }
543
- if (!this.tableExists(tableName))
544
- return { error: 'app', message: `unknown table ${tableName}` }
545
- const value = (mutation.args[0] || {}) as Record<string, unknown>
546
- const primaryKey = this.primaryKeyForTable(tableName, [])
547
-
548
- if (action === 'insert') this.insertRow(tableName, value, primaryKey)
549
- else if (action === 'upsert') this.upsertRow(tableName, value, primaryKey)
550
- else if (action === 'delete') this.deleteRow(tableName, value, primaryKey)
551
- else this.updateRow(tableName, value, primaryKey)
552
- return {}
553
- }
554
-
555
- private tableActionFromMutationName(name: string): [string, string] {
556
- if (name.includes('|')) return name.split('|', 2) as [string, string]
557
- return name.split('.', 2) as [string, string]
558
- }
559
-
560
- private tableNameFromOperationName(name?: string): string | null {
561
- if (!name) return null
562
- return name.split(/[.|]/, 1)[0] || null
563
- }
564
-
565
- private applyCrudMutation(mutation: PushMutation) {
566
- const arg = mutation.args[0] as { ops?: CrudOp[] } | undefined
567
- const ops = Array.isArray(arg?.ops) ? arg.ops : []
568
- for (const crud of ops) {
569
- if (!crud?.tableName) return { error: 'app', message: 'invalid crud mutation' }
570
- if (!this.tableExists(crud.tableName))
571
- return { error: 'app', message: `unknown table ${crud.tableName}` }
572
- const value = crud.value || {}
573
- const primaryKey = this.primaryKeyForTable(crud.tableName, crud.primaryKey || [])
574
- if (crud.op === 'insert') this.insertRow(crud.tableName, value, primaryKey)
575
- else if (crud.op === 'upsert') this.upsertRow(crud.tableName, value, primaryKey)
576
- else if (crud.op === 'update') this.updateRow(crud.tableName, value, primaryKey)
577
- else if (crud.op === 'delete') this.deleteRow(crud.tableName, value, primaryKey)
578
- }
579
- return {}
580
- }
581
-
582
- private insertRow(tn: string, value: Record<string, unknown>, pk: string[]) {
583
- if (this.readRowByPrimaryKey(tn, value, pk)) return
584
- const row = this.storageRow(tn, value, true)
585
- const cols = Object.keys(row)
586
- if (!cols.length) return
587
- const qc = cols.map((c) => quoteIdent(c)).join(', ')
588
- const ph = cols.map(() => '?').join(', ')
589
- this.sql.exec(
590
- `INSERT INTO ${quoteIdent(tn)} (${qc}) VALUES (${ph})`,
591
- ...cols.map((c) => row[c])
592
- )
593
- const next = this.readRowByPrimaryKey(tn, value, pk) || this.normalizeRow(tn, row)
594
- this.appendChange(tn, 'INSERT', next, null)
595
- }
596
-
597
- private upsertRow(tn: string, value: Record<string, unknown>, pk: string[]) {
598
- const existing = this.readRowByPrimaryKey(tn, value, pk)
599
- if (existing) {
600
- this.updateRow(tn, value, pk)
601
- return
602
- }
603
- this.insertRow(tn, value, pk)
604
- }
605
-
606
- private updateRow(tn: string, value: Record<string, unknown>, pk: string[]) {
607
- if (!pk.length) return
608
- const existing = this.readRowByPrimaryKey(tn, value, pk)
609
- if (!existing) return
610
- const nk = Object.keys(value).filter((c) => !pk.includes(c))
611
- if (!nk.length) return
612
- const storage = this.storageRow(tn, value, false)
613
- this.sql.exec(
614
- `UPDATE ${quoteIdent(tn)} SET ${nk.map((c) => `${quoteIdent(c)} = ?`).join(', ')} WHERE ${this.primaryKeyWhere(pk)}`,
615
- ...nk.map((c) => storage[c]),
616
- ...pk.map((c) => this.storageColumnValue(tn, c, value[c]))
617
- )
618
- const next = this.readRowByPrimaryKey(tn, value, pk)
619
- if (next) this.appendChange(tn, 'UPDATE', next, existing)
620
- }
621
-
622
- private deleteRow(tn: string, value: Record<string, unknown>, pk: string[]) {
623
- if (!pk.length) return
624
- const existing = this.readRowByPrimaryKey(tn, value, pk)
625
- if (!existing) return
626
- this.sql.exec(
627
- `DELETE FROM ${quoteIdent(tn)} WHERE ${this.primaryKeyWhere(pk)}`,
628
- ...pk.map((c) => this.storageColumnValue(tn, c, value[c]))
629
- )
630
- this.appendChange(tn, 'DELETE', null, existing)
631
- }
632
-
633
- private appendChange(
634
- tn: string,
635
- op: 'INSERT' | 'UPDATE' | 'DELETE',
636
- rowData: Record<string, unknown> | null,
637
- oldData: Record<string, unknown> | null
638
- ) {
639
- this.appendTrackedChange(tn, op, rowData, oldData)
640
- }
641
-
642
- private appendTrackedChange(
643
- tableName: string,
644
- op: 'INSERT' | 'UPDATE' | 'DELETE',
645
- rowData: Record<string, unknown> | null,
646
- oldData: Record<string, unknown> | null
647
- ) {
648
- this.watermarks.ensureTables()
649
- const watermark = this.watermarks.next()
650
- this.sql.exec(
651
- 'INSERT INTO _zero_changes (watermark, table_name, op, row_data, old_data) VALUES (?, ?, ?, ?, ?)',
652
- watermark,
653
- tableName,
654
- op,
655
- rowData ? JSON.stringify(rowData) : null,
656
- oldData ? JSON.stringify(oldData) : null
657
- )
658
- this.watermarks.mark(watermark)
659
- }
660
-
661
- private readChangesSince(watermark: number) {
662
- this.watermarks.ensureTables()
663
- return this.sql
664
- .exec(
665
- 'SELECT watermark, table_name, op, row_data, old_data FROM _zero_changes WHERE watermark > ? ORDER BY watermark',
666
- watermark
667
- )
668
- .toArray()
669
- .map((row: any) => ({
670
- watermark: Number(row.watermark),
671
- tableName: String(row.table_name),
672
- op: String(row.op),
673
- rowData: row.row_data ? JSON.parse(String(row.row_data)) : null,
674
- oldData: row.old_data ? JSON.parse(String(row.old_data)) : null,
675
- }))
676
- }
677
-
678
- private watermark(): number {
679
- return this.watermarks.current()
680
- }
681
-
682
- private ensureSchemaTables(clientSchema: ClientSchema) {
683
- this.ensureSchemaMetadataTable()
684
- for (const [name, def] of Object.entries(clientSchema.tables)) {
685
- this.tableSchemas.set(name, def)
686
- this.sql.exec(
687
- 'INSERT OR REPLACE INTO _zero_schema_tables (name, schema_json) VALUES (?, ?)',
688
- name,
689
- JSON.stringify(def)
690
- )
691
- if (this.schemaTables.has(name)) continue
692
- const pk = def.primaryKey.map((c) => quoteIdent(c))
693
- const pkClause = pk.length ? `, PRIMARY KEY (${pk.join(', ')})` : ''
694
- const colDefs = Object.entries(def.columns).map(([cn, cd]) => {
695
- const t: Record<string, string> = {
696
- string: 'TEXT',
697
- number: 'REAL',
698
- boolean: 'INTEGER',
699
- json: 'TEXT',
700
- bigint: 'TEXT',
701
- }
702
- return `${quoteIdent(cn)} ${t[cd.type] || 'TEXT'}`
703
- })
704
- this.sql.exec(
705
- `CREATE TABLE IF NOT EXISTS ${quoteIdent(name)} (${colDefs.join(', ')}${pkClause})`
706
- )
707
- this.schemaTables.add(name)
708
- }
709
- }
710
-
711
- private ensureSchemaMetadataTable() {
712
- this.sql.exec(
713
- 'CREATE TABLE IF NOT EXISTS _zero_schema_tables (name TEXT PRIMARY KEY, schema_json TEXT NOT NULL)'
714
- )
715
- }
716
-
717
- private schemaForTable(tableName: string): SchemaTable | undefined {
718
- const cached = this.tableSchemas.get(tableName)
719
- if (cached) return cached
720
- try {
721
- this.ensureSchemaMetadataTable()
722
- const row = this.sql
723
- .exec('SELECT schema_json FROM _zero_schema_tables WHERE name = ?', tableName)
724
- .one()
725
- if (!row?.schema_json) return undefined
726
- const schema = JSON.parse(String(row.schema_json)) as SchemaTable
727
- this.tableSchemas.set(tableName, schema)
728
- return schema
729
- } catch {
730
- return undefined
731
- }
732
- }
733
-
734
- private tableExists(n: string): boolean {
735
- try {
736
- return !!this.sql
737
- .exec("SELECT name FROM sqlite_master WHERE type='table' AND name=?", n)
738
- .one()
739
- } catch {
740
- return false
741
- }
742
- }
743
-
744
- private readAllRows(tn: string): Record<string, unknown>[] {
745
- try {
746
- return this.sql
747
- .exec(`SELECT * FROM ${quoteIdent(tn)}`)
748
- .toArray()
749
- .map((row: any) => this.normalizeRow(tn, row))
750
- } catch {
751
- return []
752
- }
753
- }
754
-
755
- private readRowByPrimaryKey(
756
- tn: string,
757
- value: Record<string, unknown>,
758
- pk: string[]
759
- ): Record<string, unknown> | null {
760
- if (!pk.length) return null
761
- try {
762
- const row = this.sql
763
- .exec(
764
- `SELECT * FROM ${quoteIdent(tn)} WHERE ${this.primaryKeyWhere(pk)}`,
765
- ...pk.map((c) => this.storageColumnValue(tn, c, value[c]))
766
- )
767
- .one()
768
- return row ? this.normalizeRow(tn, row) : null
769
- } catch {
770
- return null
771
- }
772
- }
773
-
774
- private primaryKeyWhere(pk: string[]): string {
775
- return pk.map((c) => `${quoteIdent(c)} = ?`).join(' AND ')
776
- }
777
-
778
- private primaryKeyForTable(tn: string, fallback: string[]): string[] {
779
- const schema = this.schemaForTable(tn)
780
- if (schema?.primaryKey?.length) return schema.primaryKey
781
- return fallback
782
- }
783
-
784
- private storageRow(
785
- tn: string,
786
- value: Record<string, unknown>,
787
- includeMissingSchemaColumns: boolean
788
- ): Record<string, unknown> {
789
- const schema = this.schemaForTable(tn)
790
- const row: Record<string, unknown> = {}
791
- if (schema && includeMissingSchemaColumns) {
792
- for (const column of Object.keys(schema.columns))
793
- row[column] = this.storageColumnValue(tn, column, value[column] ?? null)
794
- }
795
- for (const column of Object.keys(value)) {
796
- if (value[column] !== undefined)
797
- row[column] = this.storageColumnValue(tn, column, value[column])
798
- }
799
- return row
800
- }
801
-
802
- private storageColumnValue(tn: string, column: string, value: unknown): unknown {
803
- if (value === undefined || value === null) return null
804
- const type = this.schemaForTable(tn)?.columns?.[column]?.type
805
- if (type === 'boolean') return value ? 1 : 0
806
- if (type === 'json') return typeof value === 'string' ? value : JSON.stringify(value)
807
- if (type === 'number') return Number(value)
808
- if (type === 'bigint') return String(value)
809
- return value
810
- }
811
-
812
- private normalizeRow(
813
- tn: string,
814
- row: Record<string, unknown>
815
- ): Record<string, unknown> {
816
- const schema = this.schemaForTable(tn)
817
- const normalized: Record<string, unknown> = {}
818
- for (const key of Object.keys(row)) {
819
- const type = schema?.columns?.[key]?.type
820
- const value = row[key]
821
- if (value === null || value === undefined) {
822
- normalized[key] = null
823
- } else if (type === 'boolean') {
824
- normalized[key] =
825
- value === true || value === 1 || value === '1' || value === 'true'
826
- } else if (type === 'number') {
827
- normalized[key] = Number(value)
828
- } else if (type === 'json' && typeof value === 'string') {
829
- try {
830
- normalized[key] = JSON.parse(value)
831
- } catch {
832
- normalized[key] = value
833
- }
834
- } else {
835
- normalized[key] = value
836
- }
837
- }
838
- return normalized
839
- }
840
-
841
- private sendSyncPoke(
842
- socket: HibernatableWebSocket,
843
- attachment: SocketAttachment,
844
- part: {
845
- rowsPatch?: any[]
846
- gotQueriesPatch?: any[]
847
- lastMutationIDChanges?: Record<string, number>
848
- }
849
- ): SocketAttachment {
850
- const cookie = this.nextCookie()
851
- const pokeID = crypto.randomUUID()
852
- this.sendJSON(socket, [
853
- 'pokeStart',
854
- {
855
- pokeID,
856
- baseCookie: attachment.cookie,
857
- schemaVersions: {
858
- minSupportedVersion: SCHEMA_VERSION,
859
- maxSupportedVersion: SCHEMA_VERSION,
860
- },
861
- timestamp: Date.now(),
862
- },
863
- ])
864
- this.sendJSON(socket, ['pokePart', { pokeID, ...part }])
865
- this.sendJSON(socket, ['pokeEnd', { pokeID, cookie }])
866
- const nextAttachment = { ...attachment, cookie }
867
- socket.serializeAttachment(nextAttachment)
868
- return nextAttachment
869
- }
870
-
871
- private broadcastPoke(
872
- clientGroupID: string,
873
- part: { rowsPatch?: any[]; lastMutationIDChanges?: Record<string, number> }
874
- ) {
875
- for (const socket of this.ctx.getWebSockets()) {
876
- const ws = socket as HibernatableWebSocket
877
- const attachment = this.readSocketAttachment(ws)
878
- if (!attachment) continue
879
- if (attachment.clientGroupID !== clientGroupID) continue
880
- this.sendSyncPoke(ws, attachment, part)
881
- }
882
- }
883
-
884
- private broadcastMutationPoke(
885
- sourceAttachment: SocketAttachment,
886
- part: { rowsPatch?: any[]; lastMutationIDChanges?: Record<string, number> }
887
- ) {
888
- const rowsPatch = part.rowsPatch || []
889
- const changedTables = new Set(
890
- rowsPatch
891
- .map((op) => op?.tableName)
892
- .filter((tableName): tableName is string => !!tableName)
893
- )
894
- const hasLastMutationIDChanges =
895
- Object.keys(part.lastMutationIDChanges || {}).length > 0
896
-
897
- for (const socket of this.ctx.getWebSockets()) {
898
- const ws = socket as HibernatableWebSocket
899
- const attachment = this.readSocketAttachment(ws)
900
- if (!attachment) continue
901
- if (attachment.userID !== sourceAttachment.userID) continue
902
-
903
- const isSourceClientGroup =
904
- attachment.clientGroupID === sourceAttachment.clientGroupID
905
- const wantsChangedRows =
906
- changedTables.size > 0 &&
907
- attachment.desiredTableNames.some((tableName) => changedTables.has(tableName))
908
-
909
- const nextPart: {
910
- rowsPatch?: any[]
911
- lastMutationIDChanges?: Record<string, number>
912
- } = {}
913
- if (wantsChangedRows) nextPart.rowsPatch = rowsPatch
914
- if (isSourceClientGroup && hasLastMutationIDChanges)
915
- nextPart.lastMutationIDChanges = part.lastMutationIDChanges
916
-
917
- if (!nextPart.rowsPatch && !nextPart.lastMutationIDChanges) continue
918
- this.sendSyncPoke(ws, attachment, nextPart)
919
- }
920
- }
921
-
922
- private syncRowPatchFromChange(change: any): any {
923
- if (change.op === 'DELETE')
924
- return {
925
- op: 'del',
926
- tableName: change.tableName,
927
- id: this.primaryKeyValue(change.tableName, change.oldData || {}),
928
- }
929
- return {
930
- op: 'put',
931
- tableName: change.tableName,
932
- value: this.normalizeRow(change.tableName, change.rowData || {}),
933
- }
934
- }
935
-
936
- private primaryKeyValue(
937
- tableName: string,
938
- row: Record<string, unknown>
939
- ): Record<string, unknown> {
940
- const pk = this.primaryKeyForTable(tableName, [])
941
- if (pk.length) return Object.fromEntries(pk.map((column) => [column, row[column]]))
942
- if ('id' in row) return { id: row.id }
943
- return row
944
- }
945
-
946
- private cookie(): string {
947
- return String(this.watermark()).padStart(20, '0')
948
- }
949
-
950
- private nextCookie(): string {
951
- const watermark = this.watermarks.next()
952
- this.watermarks.mark(watermark)
953
- return String(watermark).padStart(20, '0')
954
- }
955
-
956
- private readSocketAttachment(socket: HibernatableWebSocket): SocketAttachment | null {
957
- const attachment = socket.deserializeAttachment()
958
- if (!attachment) return null
959
- return {
960
- ...attachment,
961
- initialized: attachment.initialized === true,
962
- desiredTableNames: attachment.desiredTableNames || [],
963
- desiredQueries: attachment.desiredQueries || [],
964
- }
965
- }
966
-
967
- private sendJSON(socket: WebSocket, msg: unknown) {
968
- try {
969
- socket.send(JSON.stringify(msg))
970
- } catch {}
971
- }
972
- private parseMessage(data: string | ArrayBuffer): unknown {
973
- try {
974
- return JSON.parse(typeof data === 'string' ? data : new TextDecoder().decode(data))
975
- } catch {
976
- return null
977
- }
978
- }
979
- }
980
-
981
- export default {
982
- async fetch(request: Request, env: Env): Promise<Response> {
983
- const url = new URL(request.url)
984
- const id = env.ZERO_DO.idFromName('singleton')
985
- if (url.pathname.startsWith('/sync/v') && url.pathname.endsWith('/connect')) {
986
- return env.ZERO_DO.get(id).fetch(request)
987
- }
988
- if (
989
- (url.pathname === '/zero/push' || url.pathname === '/api/zero/push') &&
990
- request.method === 'POST'
991
- ) {
992
- return env.ZERO_DO.get(id).fetch(request)
993
- }
994
- if (url.pathname === '/exec' && request.method === 'POST') {
995
- return env.ZERO_DO.get(id).fetch(request)
996
- }
997
- if (url.pathname === '/batch' && request.method === 'POST') {
998
- return env.ZERO_DO.get(id).fetch(request)
999
- }
1000
- if (
1001
- url.pathname === '/changes' &&
1002
- (request.method === 'GET' || request.method === 'POST')
1003
- ) {
1004
- return env.ZERO_DO.get(id).fetch(request)
1005
- }
1006
- if (url.pathname === '/notify' && request.method === 'POST') {
1007
- return env.ZERO_DO.get(id).fetch(request)
1008
- }
1009
- return new Response('not found', { status: 404 })
1010
- },
1011
- }
1012
-
1013
- function decodeInitConnection(
1014
- secProtocol: string
1015
- ): [string, Record<string, unknown>] | null {
1016
- try {
1017
- const decoded = decodeURIComponent(secProtocol)
1018
- const bytes = Uint8Array.from(atob(decoded), (char) => char.charCodeAt(0))
1019
- const protocols = JSON.parse(new TextDecoder().decode(bytes)) as {
1020
- initConnectionMessage?: unknown
1021
- }
1022
- const message = protocols.initConnectionMessage
1023
- if (Array.isArray(message) && message[0] === 'initConnection') {
1024
- return message as [string, Record<string, unknown>]
1025
- }
1026
- return null
1027
- } catch {
1028
- return null
1029
- }
1030
- }
1031
-
1032
- interface DurableObjectState {
1033
- storage: { sql: any; transaction<T>(fn: () => T | Promise<T>): Promise<T> }
1034
- acceptWebSocket(socket: WebSocket, tags?: string[]): void
1035
- getWebSockets(tag?: string): WebSocket[]
1036
- }
1037
- interface WebSocketPair {
1038
- 0: WebSocket
1039
- 1: WebSocket
1040
- }
1041
- declare const WebSocketPair: { new (): { 0: WebSocket; 1: WebSocket } }