orez 0.2.26 → 0.2.29

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 (172) hide show
  1. package/dist/cf-do/worker.d.ts.map +1 -1
  2. package/dist/cf-do/worker.js +9 -1
  3. package/dist/cf-do/worker.js.map +1 -1
  4. package/dist/pg-proxy-do-backend.d.ts +2 -0
  5. package/dist/pg-proxy-do-backend.d.ts.map +1 -1
  6. package/dist/pg-proxy-do-backend.js +49 -7
  7. package/dist/pg-proxy-do-backend.js.map +1 -1
  8. package/dist/pg-sqlite-compiler/catalog/seed.d.ts +67 -0
  9. package/dist/pg-sqlite-compiler/catalog/seed.d.ts.map +1 -0
  10. package/dist/pg-sqlite-compiler/catalog/seed.js +436 -0
  11. package/dist/pg-sqlite-compiler/catalog/seed.js.map +1 -0
  12. package/dist/pg-sqlite-compiler/index.d.ts +12 -0
  13. package/dist/pg-sqlite-compiler/index.d.ts.map +1 -0
  14. package/dist/pg-sqlite-compiler/index.js +59 -0
  15. package/dist/pg-sqlite-compiler/index.js.map +1 -0
  16. package/dist/pg-sqlite-compiler/passes/ast-utils.d.ts +48 -0
  17. package/dist/pg-sqlite-compiler/passes/ast-utils.d.ts.map +1 -0
  18. package/dist/pg-sqlite-compiler/passes/ast-utils.js +93 -0
  19. package/dist/pg-sqlite-compiler/passes/ast-utils.js.map +1 -0
  20. package/dist/pg-sqlite-compiler/passes/catalog.d.ts +34 -0
  21. package/dist/pg-sqlite-compiler/passes/catalog.d.ts.map +1 -0
  22. package/dist/pg-sqlite-compiler/passes/catalog.js +30 -0
  23. package/dist/pg-sqlite-compiler/passes/catalog.js.map +1 -0
  24. package/dist/pg-sqlite-compiler/passes/datetime.d.ts +21 -0
  25. package/dist/pg-sqlite-compiler/passes/datetime.d.ts.map +1 -0
  26. package/dist/pg-sqlite-compiler/passes/datetime.js +53 -0
  27. package/dist/pg-sqlite-compiler/passes/datetime.js.map +1 -0
  28. package/dist/pg-sqlite-compiler/passes/index.d.ts +21 -0
  29. package/dist/pg-sqlite-compiler/passes/index.d.ts.map +1 -0
  30. package/dist/pg-sqlite-compiler/passes/index.js +39 -0
  31. package/dist/pg-sqlite-compiler/passes/index.js.map +1 -0
  32. package/dist/pg-sqlite-compiler/passes/types.d.ts +41 -0
  33. package/dist/pg-sqlite-compiler/passes/types.d.ts.map +1 -0
  34. package/dist/pg-sqlite-compiler/passes/types.js +103 -0
  35. package/dist/pg-sqlite-compiler/passes/types.js.map +1 -0
  36. package/dist/pg-sqlite-compiler/test/oracle.d.ts +34 -0
  37. package/dist/pg-sqlite-compiler/test/oracle.d.ts.map +1 -0
  38. package/dist/pg-sqlite-compiler/test/oracle.js +204 -0
  39. package/dist/pg-sqlite-compiler/test/oracle.js.map +1 -0
  40. package/dist/pg-sqlite-compiler/types.d.ts +55 -0
  41. package/dist/pg-sqlite-compiler/types.d.ts.map +1 -0
  42. package/dist/pg-sqlite-compiler/types.js +2 -0
  43. package/dist/pg-sqlite-compiler/types.js.map +1 -0
  44. package/package.json +8 -4
  45. package/src/admin/admin-data.test.ts +0 -348
  46. package/src/admin/http-proxy.ts +0 -252
  47. package/src/admin/log-store.ts +0 -192
  48. package/src/admin/server.ts +0 -471
  49. package/src/admin/ui.ts +0 -1322
  50. package/src/bench/proxy-throughput.bench.ts +0 -343
  51. package/src/bench/serial-mutations.bench.ts +0 -270
  52. package/src/browser.ts +0 -203
  53. package/src/cf-do/.wrangler/cache/cf.json +0 -1
  54. package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite +0 -0
  55. package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-shm +0 -0
  56. package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-wal +0 -0
  57. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/0f0f3bdf0abda097eb6f1246db4657d9fc622081362d894d82c1a1ce067b05b6.sqlite +0 -0
  58. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/1ddd3a4a48a11b51658444f5458a1fb175194b1d5b6a5bda20ef3fe3205b900c.sqlite +0 -0
  59. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/204a39120310d37e972c5914cfd71ad55c151bdb9e8ed289a5f8c5b052dd60e4.sqlite +0 -0
  60. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/3835f242df9728adba3d127a238793fd054ed3e51df3f60749ee744c469bf2a2.sqlite +0 -0
  61. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/4aa9c80eb716cf55b8995ccf7afab0b36c683e6da07d7c37a3f9c570136036df.sqlite +0 -0
  62. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/533e2fd1d6ea46e7a9a0017916ef341802d438d72583462755f2c1f8225e9bf2.sqlite +0 -0
  63. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/5ffa1aced1225ecaeac6366f7586aa3de92761cdff8711d81fbd81f248076abd.sqlite +0 -0
  64. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/686c3a9f0d7e59ed2ab607efd4b76d779c97cafeb3818380033bf7c7eb86c819.sqlite +0 -0
  65. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/6e8214e8dcfadd0deb52d64e5e9ca85c6b329ace11193909845995396914c473.sqlite +0 -0
  66. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/78d9ec9ff873d3fe3507ff53c2a6f6dfc408b4268eb0db3f2a146c0678965366.sqlite +0 -0
  67. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/7eff9f0ed7e27ad0d3f9d923de0682fab1928591172c1ba336c5f79a134a5d85.sqlite +0 -0
  68. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/836cda5b995b25867d722ed4f4c2292167e80351a3c6038db626648eb247dd8b.sqlite +0 -0
  69. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/91ef63b112209ab30172763acd8a0935106c248f7f1bcae5545ce37a9f201551.sqlite +0 -0
  70. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/a66ea4293a5f5938bc6d116edfa2522bb85bc37aea3541fbc09c3b613b9b32c0.sqlite +0 -0
  71. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/ceb2ab26b80590840b65651deb6e948d3bf81565c6751f3a58752cf4bf4aecae.sqlite +0 -0
  72. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite +0 -0
  73. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite-shm +0 -0
  74. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite-wal +0 -0
  75. package/src/cf-do/ARCHITECTURE.md +0 -83
  76. package/src/cf-do/watermark.test.ts +0 -103
  77. package/src/cf-do/watermark.ts +0 -118
  78. package/src/cf-do/worker.ts +0 -1033
  79. package/src/cf-do/wrangler.toml +0 -11
  80. package/src/cf-pglite/README.md +0 -19
  81. package/src/change-tracking.ts +0 -25
  82. package/src/child-process.test.ts +0 -147
  83. package/src/child-process.ts +0 -90
  84. package/src/cli-entry.ts +0 -72
  85. package/src/cli.test.ts +0 -38
  86. package/src/cli.ts +0 -1214
  87. package/src/config.ts +0 -150
  88. package/src/do-sql-tracking.test.ts +0 -19
  89. package/src/do-sql-tracking.ts +0 -19
  90. package/src/index.ts +0 -1215
  91. package/src/integration/integration.test.ts +0 -517
  92. package/src/integration/native-binary.guard.test.ts +0 -13
  93. package/src/integration/native-startup.test.ts +0 -44
  94. package/src/integration/replication-latency.test.ts +0 -428
  95. package/src/integration/restore-live-stress.test.ts +0 -433
  96. package/src/integration/restore-reset.test.ts +0 -400
  97. package/src/integration/restore.test.ts +0 -274
  98. package/src/integration/test-permissions.ts +0 -147
  99. package/src/load-config.ts +0 -46
  100. package/src/log.ts +0 -96
  101. package/src/mutex.ts +0 -47
  102. package/src/pg-proxy-browser.singledb.test.ts +0 -233
  103. package/src/pg-proxy-browser.ts +0 -2022
  104. package/src/pg-proxy-do-backend.test.ts +0 -3890
  105. package/src/pg-proxy-do-backend.ts +0 -7157
  106. package/src/pg-proxy.ts +0 -1087
  107. package/src/pglite-ipc.test.ts +0 -116
  108. package/src/pglite-ipc.ts +0 -266
  109. package/src/pglite-manager.ts +0 -557
  110. package/src/pglite-web-proxy.test.ts +0 -57
  111. package/src/pglite-web-proxy.ts +0 -221
  112. package/src/pglite-web-worker.ts +0 -152
  113. package/src/pglite-worker-thread.ts +0 -253
  114. package/src/port.ts +0 -25
  115. package/src/process-title.ts +0 -9
  116. package/src/recovery.ts +0 -155
  117. package/src/replication/change-tracker.test.ts +0 -357
  118. package/src/replication/change-tracker.ts +0 -279
  119. package/src/replication/handler.test.ts +0 -511
  120. package/src/replication/handler.ts +0 -1190
  121. package/src/replication/pgoutput-encoder.test.ts +0 -697
  122. package/src/replication/pgoutput-encoder.ts +0 -373
  123. package/src/replication/tcp-replication.test.ts +0 -876
  124. package/src/replication/zero-compat.test.ts +0 -1150
  125. package/src/restore-stress.test.ts +0 -188
  126. package/src/s3-local.ts +0 -203
  127. package/src/shim/hooks.mjs +0 -120
  128. package/src/shim/register.mjs +0 -4
  129. package/src/sqlite-mode/apply-mode.ts +0 -224
  130. package/src/sqlite-mode/index.ts +0 -15
  131. package/src/sqlite-mode/native-binary.ts +0 -89
  132. package/src/sqlite-mode/package-resolve.ts +0 -17
  133. package/src/sqlite-mode/resolve-mode.ts +0 -80
  134. package/src/sqlite-mode/shim-template.ts +0 -159
  135. package/src/sqlite-mode/sqlite-mode.test.ts +0 -427
  136. package/src/sqlite-mode/types.ts +0 -30
  137. package/src/vite-plugin.ts +0 -67
  138. package/src/wasm-sqlite.test.ts +0 -537
  139. package/src/worker/browser-admin.ts +0 -52
  140. package/src/worker/browser-build-config.test.ts +0 -71
  141. package/src/worker/browser-build-config.ts +0 -109
  142. package/src/worker/browser-embed-admin.test.ts +0 -75
  143. package/src/worker/browser-embed.ts +0 -345
  144. package/src/worker/cf-patches.ts +0 -384
  145. package/src/worker/embed-integration.test.ts +0 -321
  146. package/src/worker/index.ts +0 -138
  147. package/src/worker/shims/fastify.test.ts +0 -255
  148. package/src/worker/shims/fastify.ts +0 -306
  149. package/src/worker/shims/http-service.test.ts +0 -355
  150. package/src/worker/shims/http-service.ts +0 -293
  151. package/src/worker/shims/node-stub.ts +0 -290
  152. package/src/worker/shims/oxfmt.ts +0 -3
  153. package/src/worker/shims/postgres-browser.ts +0 -59
  154. package/src/worker/shims/postgres-socket.test.ts +0 -576
  155. package/src/worker/shims/postgres-socket.ts +0 -310
  156. package/src/worker/shims/postgres.test.ts +0 -364
  157. package/src/worker/shims/postgres.ts +0 -1454
  158. package/src/worker/shims/sqlite-browser.test.ts +0 -233
  159. package/src/worker/shims/sqlite-browser.ts +0 -175
  160. package/src/worker/shims/sqlite.test.ts +0 -786
  161. package/src/worker/shims/sqlite.ts +0 -978
  162. package/src/worker/shims/stream-browser.ts +0 -15
  163. package/src/worker/shims/ws-browser.test.ts +0 -205
  164. package/src/worker/shims/ws-browser.ts +0 -248
  165. package/src/worker/shims/ws.test.ts +0 -288
  166. package/src/worker/shims/ws.ts +0 -467
  167. package/src/worker/shims/zero-process-env.ts +0 -11
  168. package/src/worker/types.ts +0 -75
  169. package/src/worker/worker-integration.test.ts +0 -223
  170. package/src/worker/worker.test.ts +0 -136
  171. package/src/worker/zero-cache-embed-cf.ts +0 -463
  172. package/src/worker/zero-cache-embed.ts +0 -277
@@ -1,1033 +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
- const result = await this.ctx.storage.transaction(() =>
416
- this.executeSQL(sql, params, body.track)
417
- )
418
- return Response.json(result)
419
- } catch (err: any) {
420
- const suffix = sql ? ` while executing: ${sqlErrorSnippet(sql, err.message)}` : ''
421
- console.error(`[exec-500] ${err.message} :: SQL=${sql.slice(0, 800)}`)
422
- return Response.json({ error: `${err.message}${suffix}` }, { status: 500 })
423
- }
424
- }
425
-
426
- /** Execute multiple statements atomically via ctx.storage.transaction() */
427
- private async handleBatch(request: Request): Promise<Response> {
428
- try {
429
- const { statements } = (await request.json()) as {
430
- statements: Array<string | SqlExecStatement>
431
- }
432
- const allRows = await this.ctx.storage.transaction(() => {
433
- const results: any[] = []
434
- for (const statement of statements) {
435
- const item = typeof statement === 'string' ? { sql: statement } : statement
436
- if (!item?.sql?.trim()) continue
437
- try {
438
- results.push(
439
- this.executeSQL(
440
- item.sql,
441
- Array.isArray(item.params) ? item.params : [],
442
- item.track
443
- )
444
- )
445
- } catch (err: any) {
446
- throw new Error(
447
- `${err.message} while executing: ${sqlErrorSnippet(item.sql, err.message)}`
448
- )
449
- }
450
- }
451
- return results
452
- })
453
- return Response.json({ results: allRows })
454
- } catch (err: any) {
455
- return Response.json({ error: err.message }, { status: 500 })
456
- }
457
- }
458
-
459
- private async handleChanges(request: Request, url: URL): Promise<Response> {
460
- try {
461
- let watermark = Number(
462
- url.searchParams.get('watermark') ?? url.searchParams.get('since') ?? 0
463
- )
464
- let limit = Number(url.searchParams.get('limit') ?? 1000)
465
- if (request.method === 'POST') {
466
- const body = (await request.json().catch(() => ({}))) as {
467
- watermark?: unknown
468
- since?: unknown
469
- limit?: unknown
470
- }
471
- watermark = Number(body.watermark ?? body.since ?? watermark)
472
- limit = Number(body.limit ?? limit)
473
- }
474
- if (!Number.isFinite(watermark) || watermark < 0) watermark = 0
475
- if (!Number.isFinite(limit) || limit <= 0) limit = 1000
476
- return Response.json({
477
- watermark: this.watermark(),
478
- changes: this.readChangesSince(watermark).slice(0, Math.min(limit, 10_000)),
479
- })
480
- } catch (err: any) {
481
- return Response.json({ error: err.message }, { status: 500 })
482
- }
483
- }
484
-
485
- private executeSQL(
486
- sql: string,
487
- params: unknown[] = [],
488
- track?: SqlTrack
489
- ): { rows: Record<string, unknown>[]; columns: string[]; affectedRows?: number } {
490
- const cursor = this.sql.exec(sql, ...params)
491
- const columns = Array.isArray(cursor.columnNames) ? cursor.columnNames : []
492
- const rows = this.cursorRows(cursor)
493
- if (!track) return { rows, columns }
494
-
495
- for (const row of rows) {
496
- const trackedRow = trackedChangeRow(row, track)
497
- if (track.operation === 'DELETE')
498
- this.appendTrackedChange(track.tableName, 'DELETE', null, trackedRow)
499
- else this.appendTrackedChange(track.tableName, track.operation, trackedRow, null)
500
- }
501
-
502
- return {
503
- rows: track.returnRows ? rows : [],
504
- columns: track.returnRows ? columns : [],
505
- affectedRows: rows.length,
506
- }
507
- }
508
-
509
- private cursorRows(cursor: any): Record<string, unknown>[] {
510
- return cursor.toArray().map((row: any) => {
511
- const obj: Record<string, unknown> = {}
512
- for (const k of Object.keys(row)) obj[k] = row[k]
513
- return obj
514
- })
515
- }
516
-
517
- // ── CRUD operations ──────────────────────────────────────────────────────
518
-
519
- private applyMutation(mutation: PushMutation) {
520
- if (mutation.type === 'crud' && mutation.name === '_zero_crud') {
521
- return this.applyCrudMutation(mutation)
522
- }
523
- if (mutation.name === '_zero_cleanupResults') return {}
524
- if (mutation.type === 'custom') return this.applyTableMutation(mutation)
525
- return {
526
- error: 'app',
527
- message: `unsupported mutation ${mutation.type}:${mutation.name}`,
528
- }
529
- }
530
-
531
- private applyTableMutation(mutation: PushMutation) {
532
- const [tableName, action] = this.tableActionFromMutationName(mutation.name)
533
- if (!tableName || !action)
534
- return { error: 'app', message: `invalid mutation name ${mutation.name}` }
535
- if (!this.tableExists(tableName))
536
- return { error: 'app', message: `unknown table ${tableName}` }
537
- const value = (mutation.args[0] || {}) as Record<string, unknown>
538
- const primaryKey = this.primaryKeyForTable(tableName, [])
539
-
540
- if (action === 'insert') this.insertRow(tableName, value, primaryKey)
541
- else if (action === 'upsert') this.upsertRow(tableName, value, primaryKey)
542
- else if (action === 'delete') this.deleteRow(tableName, value, primaryKey)
543
- else this.updateRow(tableName, value, primaryKey)
544
- return {}
545
- }
546
-
547
- private tableActionFromMutationName(name: string): [string, string] {
548
- if (name.includes('|')) return name.split('|', 2) as [string, string]
549
- return name.split('.', 2) as [string, string]
550
- }
551
-
552
- private tableNameFromOperationName(name?: string): string | null {
553
- if (!name) return null
554
- return name.split(/[.|]/, 1)[0] || null
555
- }
556
-
557
- private applyCrudMutation(mutation: PushMutation) {
558
- const arg = mutation.args[0] as { ops?: CrudOp[] } | undefined
559
- const ops = Array.isArray(arg?.ops) ? arg.ops : []
560
- for (const crud of ops) {
561
- if (!crud?.tableName) return { error: 'app', message: 'invalid crud mutation' }
562
- if (!this.tableExists(crud.tableName))
563
- return { error: 'app', message: `unknown table ${crud.tableName}` }
564
- const value = crud.value || {}
565
- const primaryKey = this.primaryKeyForTable(crud.tableName, crud.primaryKey || [])
566
- if (crud.op === 'insert') this.insertRow(crud.tableName, value, primaryKey)
567
- else if (crud.op === 'upsert') this.upsertRow(crud.tableName, value, primaryKey)
568
- else if (crud.op === 'update') this.updateRow(crud.tableName, value, primaryKey)
569
- else if (crud.op === 'delete') this.deleteRow(crud.tableName, value, primaryKey)
570
- }
571
- return {}
572
- }
573
-
574
- private insertRow(tn: string, value: Record<string, unknown>, pk: string[]) {
575
- if (this.readRowByPrimaryKey(tn, value, pk)) return
576
- const row = this.storageRow(tn, value, true)
577
- const cols = Object.keys(row)
578
- if (!cols.length) return
579
- const qc = cols.map((c) => quoteIdent(c)).join(', ')
580
- const ph = cols.map(() => '?').join(', ')
581
- this.sql.exec(
582
- `INSERT INTO ${quoteIdent(tn)} (${qc}) VALUES (${ph})`,
583
- ...cols.map((c) => row[c])
584
- )
585
- const next = this.readRowByPrimaryKey(tn, value, pk) || this.normalizeRow(tn, row)
586
- this.appendChange(tn, 'INSERT', next, null)
587
- }
588
-
589
- private upsertRow(tn: string, value: Record<string, unknown>, pk: string[]) {
590
- const existing = this.readRowByPrimaryKey(tn, value, pk)
591
- if (existing) {
592
- this.updateRow(tn, value, pk)
593
- return
594
- }
595
- this.insertRow(tn, value, pk)
596
- }
597
-
598
- private updateRow(tn: string, value: Record<string, unknown>, pk: string[]) {
599
- if (!pk.length) return
600
- const existing = this.readRowByPrimaryKey(tn, value, pk)
601
- if (!existing) return
602
- const nk = Object.keys(value).filter((c) => !pk.includes(c))
603
- if (!nk.length) return
604
- const storage = this.storageRow(tn, value, false)
605
- this.sql.exec(
606
- `UPDATE ${quoteIdent(tn)} SET ${nk.map((c) => `${quoteIdent(c)} = ?`).join(', ')} WHERE ${this.primaryKeyWhere(pk)}`,
607
- ...nk.map((c) => storage[c]),
608
- ...pk.map((c) => this.storageColumnValue(tn, c, value[c]))
609
- )
610
- const next = this.readRowByPrimaryKey(tn, value, pk)
611
- if (next) this.appendChange(tn, 'UPDATE', next, existing)
612
- }
613
-
614
- private deleteRow(tn: string, value: Record<string, unknown>, pk: string[]) {
615
- if (!pk.length) return
616
- const existing = this.readRowByPrimaryKey(tn, value, pk)
617
- if (!existing) return
618
- this.sql.exec(
619
- `DELETE FROM ${quoteIdent(tn)} WHERE ${this.primaryKeyWhere(pk)}`,
620
- ...pk.map((c) => this.storageColumnValue(tn, c, value[c]))
621
- )
622
- this.appendChange(tn, 'DELETE', null, existing)
623
- }
624
-
625
- private appendChange(
626
- tn: string,
627
- op: 'INSERT' | 'UPDATE' | 'DELETE',
628
- rowData: Record<string, unknown> | null,
629
- oldData: Record<string, unknown> | null
630
- ) {
631
- this.appendTrackedChange(tn, op, rowData, oldData)
632
- }
633
-
634
- private appendTrackedChange(
635
- tableName: string,
636
- op: 'INSERT' | 'UPDATE' | 'DELETE',
637
- rowData: Record<string, unknown> | null,
638
- oldData: Record<string, unknown> | null
639
- ) {
640
- this.watermarks.ensureTables()
641
- const watermark = this.watermarks.next()
642
- this.sql.exec(
643
- 'INSERT INTO _zero_changes (watermark, table_name, op, row_data, old_data) VALUES (?, ?, ?, ?, ?)',
644
- watermark,
645
- tableName,
646
- op,
647
- rowData ? JSON.stringify(rowData) : null,
648
- oldData ? JSON.stringify(oldData) : null
649
- )
650
- this.watermarks.mark(watermark)
651
- }
652
-
653
- private readChangesSince(watermark: number) {
654
- this.watermarks.ensureTables()
655
- return this.sql
656
- .exec(
657
- 'SELECT watermark, table_name, op, row_data, old_data FROM _zero_changes WHERE watermark > ? ORDER BY watermark',
658
- watermark
659
- )
660
- .toArray()
661
- .map((row: any) => ({
662
- watermark: Number(row.watermark),
663
- tableName: String(row.table_name),
664
- op: String(row.op),
665
- rowData: row.row_data ? JSON.parse(String(row.row_data)) : null,
666
- oldData: row.old_data ? JSON.parse(String(row.old_data)) : null,
667
- }))
668
- }
669
-
670
- private watermark(): number {
671
- return this.watermarks.current()
672
- }
673
-
674
- private ensureSchemaTables(clientSchema: ClientSchema) {
675
- this.ensureSchemaMetadataTable()
676
- for (const [name, def] of Object.entries(clientSchema.tables)) {
677
- this.tableSchemas.set(name, def)
678
- this.sql.exec(
679
- 'INSERT OR REPLACE INTO _zero_schema_tables (name, schema_json) VALUES (?, ?)',
680
- name,
681
- JSON.stringify(def)
682
- )
683
- if (this.schemaTables.has(name)) continue
684
- const pk = def.primaryKey.map((c) => quoteIdent(c))
685
- const pkClause = pk.length ? `, PRIMARY KEY (${pk.join(', ')})` : ''
686
- const colDefs = Object.entries(def.columns).map(([cn, cd]) => {
687
- const t: Record<string, string> = {
688
- string: 'TEXT',
689
- number: 'REAL',
690
- boolean: 'INTEGER',
691
- json: 'TEXT',
692
- bigint: 'TEXT',
693
- }
694
- return `${quoteIdent(cn)} ${t[cd.type] || 'TEXT'}`
695
- })
696
- this.sql.exec(
697
- `CREATE TABLE IF NOT EXISTS ${quoteIdent(name)} (${colDefs.join(', ')}${pkClause})`
698
- )
699
- this.schemaTables.add(name)
700
- }
701
- }
702
-
703
- private ensureSchemaMetadataTable() {
704
- this.sql.exec(
705
- 'CREATE TABLE IF NOT EXISTS _zero_schema_tables (name TEXT PRIMARY KEY, schema_json TEXT NOT NULL)'
706
- )
707
- }
708
-
709
- private schemaForTable(tableName: string): SchemaTable | undefined {
710
- const cached = this.tableSchemas.get(tableName)
711
- if (cached) return cached
712
- try {
713
- this.ensureSchemaMetadataTable()
714
- const row = this.sql
715
- .exec('SELECT schema_json FROM _zero_schema_tables WHERE name = ?', tableName)
716
- .one()
717
- if (!row?.schema_json) return undefined
718
- const schema = JSON.parse(String(row.schema_json)) as SchemaTable
719
- this.tableSchemas.set(tableName, schema)
720
- return schema
721
- } catch {
722
- return undefined
723
- }
724
- }
725
-
726
- private tableExists(n: string): boolean {
727
- try {
728
- return !!this.sql
729
- .exec("SELECT name FROM sqlite_master WHERE type='table' AND name=?", n)
730
- .one()
731
- } catch {
732
- return false
733
- }
734
- }
735
-
736
- private readAllRows(tn: string): Record<string, unknown>[] {
737
- try {
738
- return this.sql
739
- .exec(`SELECT * FROM ${quoteIdent(tn)}`)
740
- .toArray()
741
- .map((row: any) => this.normalizeRow(tn, row))
742
- } catch {
743
- return []
744
- }
745
- }
746
-
747
- private readRowByPrimaryKey(
748
- tn: string,
749
- value: Record<string, unknown>,
750
- pk: string[]
751
- ): Record<string, unknown> | null {
752
- if (!pk.length) return null
753
- try {
754
- const row = this.sql
755
- .exec(
756
- `SELECT * FROM ${quoteIdent(tn)} WHERE ${this.primaryKeyWhere(pk)}`,
757
- ...pk.map((c) => this.storageColumnValue(tn, c, value[c]))
758
- )
759
- .one()
760
- return row ? this.normalizeRow(tn, row) : null
761
- } catch {
762
- return null
763
- }
764
- }
765
-
766
- private primaryKeyWhere(pk: string[]): string {
767
- return pk.map((c) => `${quoteIdent(c)} = ?`).join(' AND ')
768
- }
769
-
770
- private primaryKeyForTable(tn: string, fallback: string[]): string[] {
771
- const schema = this.schemaForTable(tn)
772
- if (schema?.primaryKey?.length) return schema.primaryKey
773
- return fallback
774
- }
775
-
776
- private storageRow(
777
- tn: string,
778
- value: Record<string, unknown>,
779
- includeMissingSchemaColumns: boolean
780
- ): Record<string, unknown> {
781
- const schema = this.schemaForTable(tn)
782
- const row: Record<string, unknown> = {}
783
- if (schema && includeMissingSchemaColumns) {
784
- for (const column of Object.keys(schema.columns))
785
- row[column] = this.storageColumnValue(tn, column, value[column] ?? null)
786
- }
787
- for (const column of Object.keys(value)) {
788
- if (value[column] !== undefined)
789
- row[column] = this.storageColumnValue(tn, column, value[column])
790
- }
791
- return row
792
- }
793
-
794
- private storageColumnValue(tn: string, column: string, value: unknown): unknown {
795
- if (value === undefined || value === null) return null
796
- const type = this.schemaForTable(tn)?.columns?.[column]?.type
797
- if (type === 'boolean') return value ? 1 : 0
798
- if (type === 'json') return typeof value === 'string' ? value : JSON.stringify(value)
799
- if (type === 'number') return Number(value)
800
- if (type === 'bigint') return String(value)
801
- return value
802
- }
803
-
804
- private normalizeRow(
805
- tn: string,
806
- row: Record<string, unknown>
807
- ): Record<string, unknown> {
808
- const schema = this.schemaForTable(tn)
809
- const normalized: Record<string, unknown> = {}
810
- for (const key of Object.keys(row)) {
811
- const type = schema?.columns?.[key]?.type
812
- const value = row[key]
813
- if (value === null || value === undefined) {
814
- normalized[key] = null
815
- } else if (type === 'boolean') {
816
- normalized[key] =
817
- value === true || value === 1 || value === '1' || value === 'true'
818
- } else if (type === 'number') {
819
- normalized[key] = Number(value)
820
- } else if (type === 'json' && typeof value === 'string') {
821
- try {
822
- normalized[key] = JSON.parse(value)
823
- } catch {
824
- normalized[key] = value
825
- }
826
- } else {
827
- normalized[key] = value
828
- }
829
- }
830
- return normalized
831
- }
832
-
833
- private sendSyncPoke(
834
- socket: HibernatableWebSocket,
835
- attachment: SocketAttachment,
836
- part: {
837
- rowsPatch?: any[]
838
- gotQueriesPatch?: any[]
839
- lastMutationIDChanges?: Record<string, number>
840
- }
841
- ): SocketAttachment {
842
- const cookie = this.nextCookie()
843
- const pokeID = crypto.randomUUID()
844
- this.sendJSON(socket, [
845
- 'pokeStart',
846
- {
847
- pokeID,
848
- baseCookie: attachment.cookie,
849
- schemaVersions: {
850
- minSupportedVersion: SCHEMA_VERSION,
851
- maxSupportedVersion: SCHEMA_VERSION,
852
- },
853
- timestamp: Date.now(),
854
- },
855
- ])
856
- this.sendJSON(socket, ['pokePart', { pokeID, ...part }])
857
- this.sendJSON(socket, ['pokeEnd', { pokeID, cookie }])
858
- const nextAttachment = { ...attachment, cookie }
859
- socket.serializeAttachment(nextAttachment)
860
- return nextAttachment
861
- }
862
-
863
- private broadcastPoke(
864
- clientGroupID: string,
865
- part: { rowsPatch?: any[]; lastMutationIDChanges?: Record<string, number> }
866
- ) {
867
- for (const socket of this.ctx.getWebSockets()) {
868
- const ws = socket as HibernatableWebSocket
869
- const attachment = this.readSocketAttachment(ws)
870
- if (!attachment) continue
871
- if (attachment.clientGroupID !== clientGroupID) continue
872
- this.sendSyncPoke(ws, attachment, part)
873
- }
874
- }
875
-
876
- private broadcastMutationPoke(
877
- sourceAttachment: SocketAttachment,
878
- part: { rowsPatch?: any[]; lastMutationIDChanges?: Record<string, number> }
879
- ) {
880
- const rowsPatch = part.rowsPatch || []
881
- const changedTables = new Set(
882
- rowsPatch
883
- .map((op) => op?.tableName)
884
- .filter((tableName): tableName is string => !!tableName)
885
- )
886
- const hasLastMutationIDChanges =
887
- Object.keys(part.lastMutationIDChanges || {}).length > 0
888
-
889
- for (const socket of this.ctx.getWebSockets()) {
890
- const ws = socket as HibernatableWebSocket
891
- const attachment = this.readSocketAttachment(ws)
892
- if (!attachment) continue
893
- if (attachment.userID !== sourceAttachment.userID) continue
894
-
895
- const isSourceClientGroup =
896
- attachment.clientGroupID === sourceAttachment.clientGroupID
897
- const wantsChangedRows =
898
- changedTables.size > 0 &&
899
- attachment.desiredTableNames.some((tableName) => changedTables.has(tableName))
900
-
901
- const nextPart: {
902
- rowsPatch?: any[]
903
- lastMutationIDChanges?: Record<string, number>
904
- } = {}
905
- if (wantsChangedRows) nextPart.rowsPatch = rowsPatch
906
- if (isSourceClientGroup && hasLastMutationIDChanges)
907
- nextPart.lastMutationIDChanges = part.lastMutationIDChanges
908
-
909
- if (!nextPart.rowsPatch && !nextPart.lastMutationIDChanges) continue
910
- this.sendSyncPoke(ws, attachment, nextPart)
911
- }
912
- }
913
-
914
- private syncRowPatchFromChange(change: any): any {
915
- if (change.op === 'DELETE')
916
- return {
917
- op: 'del',
918
- tableName: change.tableName,
919
- id: this.primaryKeyValue(change.tableName, change.oldData || {}),
920
- }
921
- return {
922
- op: 'put',
923
- tableName: change.tableName,
924
- value: this.normalizeRow(change.tableName, change.rowData || {}),
925
- }
926
- }
927
-
928
- private primaryKeyValue(
929
- tableName: string,
930
- row: Record<string, unknown>
931
- ): Record<string, unknown> {
932
- const pk = this.primaryKeyForTable(tableName, [])
933
- if (pk.length) return Object.fromEntries(pk.map((column) => [column, row[column]]))
934
- if ('id' in row) return { id: row.id }
935
- return row
936
- }
937
-
938
- private cookie(): string {
939
- return String(this.watermark()).padStart(20, '0')
940
- }
941
-
942
- private nextCookie(): string {
943
- const watermark = this.watermarks.next()
944
- this.watermarks.mark(watermark)
945
- return String(watermark).padStart(20, '0')
946
- }
947
-
948
- private readSocketAttachment(socket: HibernatableWebSocket): SocketAttachment | null {
949
- const attachment = socket.deserializeAttachment()
950
- if (!attachment) return null
951
- return {
952
- ...attachment,
953
- initialized: attachment.initialized === true,
954
- desiredTableNames: attachment.desiredTableNames || [],
955
- desiredQueries: attachment.desiredQueries || [],
956
- }
957
- }
958
-
959
- private sendJSON(socket: WebSocket, msg: unknown) {
960
- try {
961
- socket.send(JSON.stringify(msg))
962
- } catch {}
963
- }
964
- private parseMessage(data: string | ArrayBuffer): unknown {
965
- try {
966
- return JSON.parse(typeof data === 'string' ? data : new TextDecoder().decode(data))
967
- } catch {
968
- return null
969
- }
970
- }
971
- }
972
-
973
- export default {
974
- async fetch(request: Request, env: Env): Promise<Response> {
975
- const url = new URL(request.url)
976
- const id = env.ZERO_DO.idFromName('singleton')
977
- if (url.pathname.startsWith('/sync/v') && url.pathname.endsWith('/connect')) {
978
- return env.ZERO_DO.get(id).fetch(request)
979
- }
980
- if (
981
- (url.pathname === '/zero/push' || url.pathname === '/api/zero/push') &&
982
- request.method === 'POST'
983
- ) {
984
- return env.ZERO_DO.get(id).fetch(request)
985
- }
986
- if (url.pathname === '/exec' && request.method === 'POST') {
987
- return env.ZERO_DO.get(id).fetch(request)
988
- }
989
- if (url.pathname === '/batch' && request.method === 'POST') {
990
- return env.ZERO_DO.get(id).fetch(request)
991
- }
992
- if (
993
- url.pathname === '/changes' &&
994
- (request.method === 'GET' || request.method === 'POST')
995
- ) {
996
- return env.ZERO_DO.get(id).fetch(request)
997
- }
998
- if (url.pathname === '/notify' && request.method === 'POST') {
999
- return env.ZERO_DO.get(id).fetch(request)
1000
- }
1001
- return new Response('not found', { status: 404 })
1002
- },
1003
- }
1004
-
1005
- function decodeInitConnection(
1006
- secProtocol: string
1007
- ): [string, Record<string, unknown>] | null {
1008
- try {
1009
- const decoded = decodeURIComponent(secProtocol)
1010
- const bytes = Uint8Array.from(atob(decoded), (char) => char.charCodeAt(0))
1011
- const protocols = JSON.parse(new TextDecoder().decode(bytes)) as {
1012
- initConnectionMessage?: unknown
1013
- }
1014
- const message = protocols.initConnectionMessage
1015
- if (Array.isArray(message) && message[0] === 'initConnection') {
1016
- return message as [string, Record<string, unknown>]
1017
- }
1018
- return null
1019
- } catch {
1020
- return null
1021
- }
1022
- }
1023
-
1024
- interface DurableObjectState {
1025
- storage: { sql: any; transaction<T>(fn: () => T | Promise<T>): Promise<T> }
1026
- acceptWebSocket(socket: WebSocket, tags?: string[]): void
1027
- getWebSockets(tag?: string): WebSocket[]
1028
- }
1029
- interface WebSocketPair {
1030
- 0: WebSocket
1031
- 1: WebSocket
1032
- }
1033
- declare const WebSocketPair: { new (): { 0: WebSocket; 1: WebSocket } }