orez 0.2.24 → 0.2.26

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 (138) hide show
  1. package/dist/cf-do/test-protocol.d.ts +11 -0
  2. package/dist/cf-do/test-protocol.d.ts.map +1 -0
  3. package/dist/cf-do/test-protocol.js +137 -0
  4. package/dist/cf-do/test-protocol.js.map +1 -0
  5. package/dist/cf-do/watermark.d.ts +21 -0
  6. package/dist/cf-do/watermark.d.ts.map +1 -0
  7. package/dist/cf-do/watermark.js +93 -0
  8. package/dist/cf-do/watermark.js.map +1 -0
  9. package/dist/cf-do/worker.d.ts +91 -0
  10. package/dist/cf-do/worker.d.ts.map +1 -0
  11. package/dist/cf-do/worker.js +813 -0
  12. package/dist/cf-do/worker.js.map +1 -0
  13. package/dist/config.d.ts +4 -0
  14. package/dist/config.d.ts.map +1 -1
  15. package/dist/config.js +1 -0
  16. package/dist/config.js.map +1 -1
  17. package/dist/do-sql-tracking.d.ts +6 -0
  18. package/dist/do-sql-tracking.d.ts.map +1 -0
  19. package/dist/do-sql-tracking.js +14 -0
  20. package/dist/do-sql-tracking.js.map +1 -0
  21. package/dist/index.d.ts +2 -3
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +69 -23
  24. package/dist/index.js.map +1 -1
  25. package/dist/pg-proxy-browser.js +6 -6
  26. package/dist/pg-proxy-browser.js.map +1 -1
  27. package/dist/pg-proxy-do-backend.d.ts +128 -0
  28. package/dist/pg-proxy-do-backend.d.ts.map +1 -0
  29. package/dist/pg-proxy-do-backend.js +6292 -0
  30. package/dist/pg-proxy-do-backend.js.map +1 -0
  31. package/dist/pglite-ipc.d.ts +3 -0
  32. package/dist/pglite-ipc.d.ts.map +1 -1
  33. package/dist/pglite-ipc.js +34 -12
  34. package/dist/pglite-ipc.js.map +1 -1
  35. package/dist/pglite-web-proxy.d.ts +3 -0
  36. package/dist/pglite-web-proxy.d.ts.map +1 -1
  37. package/dist/pglite-web-proxy.js +50 -7
  38. package/dist/pglite-web-proxy.js.map +1 -1
  39. package/dist/query-rewrites.d.ts +2 -0
  40. package/dist/query-rewrites.d.ts.map +1 -0
  41. package/dist/query-rewrites.js +140 -0
  42. package/dist/query-rewrites.js.map +1 -0
  43. package/dist/replication/change-tracker.d.ts.map +1 -1
  44. package/dist/replication/change-tracker.js +18 -1
  45. package/dist/replication/change-tracker.js.map +1 -1
  46. package/dist/replication/handler.d.ts.map +1 -1
  47. package/dist/replication/handler.js +7 -2
  48. package/dist/replication/handler.js.map +1 -1
  49. package/dist/replication/pgoutput-encoder.d.ts.map +1 -1
  50. package/dist/replication/pgoutput-encoder.js +72 -30
  51. package/dist/replication/pgoutput-encoder.js.map +1 -1
  52. package/dist/worker/browser-build-config.d.ts.map +1 -1
  53. package/dist/worker/browser-build-config.js +2 -1
  54. package/dist/worker/browser-build-config.js.map +1 -1
  55. package/dist/worker/cf-patches.d.ts +5 -2
  56. package/dist/worker/cf-patches.d.ts.map +1 -1
  57. package/dist/worker/cf-patches.js +238 -4
  58. package/dist/worker/cf-patches.js.map +1 -1
  59. package/dist/worker/shims/node-stub.d.ts +35 -0
  60. package/dist/worker/shims/node-stub.d.ts.map +1 -1
  61. package/dist/worker/shims/node-stub.js +53 -1
  62. package/dist/worker/shims/node-stub.js.map +1 -1
  63. package/dist/worker/shims/oxfmt.d.ts +4 -0
  64. package/dist/worker/shims/oxfmt.d.ts.map +1 -0
  65. package/dist/worker/shims/oxfmt.js +4 -0
  66. package/dist/worker/shims/oxfmt.js.map +1 -0
  67. package/dist/worker/shims/postgres-socket.js +1 -1
  68. package/dist/worker/shims/postgres-socket.js.map +1 -1
  69. package/dist/worker/shims/sqlite.d.ts +1 -0
  70. package/dist/worker/shims/sqlite.d.ts.map +1 -1
  71. package/dist/worker/shims/sqlite.js +229 -9
  72. package/dist/worker/shims/sqlite.js.map +1 -1
  73. package/dist/worker/shims/ws.d.ts.map +1 -1
  74. package/dist/worker/shims/ws.js +45 -0
  75. package/dist/worker/shims/ws.js.map +1 -1
  76. package/dist/worker/shims/zero-process-env.d.ts +2 -0
  77. package/dist/worker/shims/zero-process-env.d.ts.map +1 -0
  78. package/dist/worker/shims/zero-process-env.js +9 -0
  79. package/dist/worker/shims/zero-process-env.js.map +1 -0
  80. package/dist/worker/zero-cache-embed-cf.d.ts +29 -12
  81. package/dist/worker/zero-cache-embed-cf.d.ts.map +1 -1
  82. package/dist/worker/zero-cache-embed-cf.js +83 -14
  83. package/dist/worker/zero-cache-embed-cf.js.map +1 -1
  84. package/package.json +6 -2
  85. package/src/cf-do/.wrangler/cache/cf.json +1 -0
  86. package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite +0 -0
  87. package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-shm +0 -0
  88. package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-wal +0 -0
  89. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/0f0f3bdf0abda097eb6f1246db4657d9fc622081362d894d82c1a1ce067b05b6.sqlite +0 -0
  90. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/1ddd3a4a48a11b51658444f5458a1fb175194b1d5b6a5bda20ef3fe3205b900c.sqlite +0 -0
  91. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/204a39120310d37e972c5914cfd71ad55c151bdb9e8ed289a5f8c5b052dd60e4.sqlite +0 -0
  92. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/3835f242df9728adba3d127a238793fd054ed3e51df3f60749ee744c469bf2a2.sqlite +0 -0
  93. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/4aa9c80eb716cf55b8995ccf7afab0b36c683e6da07d7c37a3f9c570136036df.sqlite +0 -0
  94. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/533e2fd1d6ea46e7a9a0017916ef341802d438d72583462755f2c1f8225e9bf2.sqlite +0 -0
  95. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/5ffa1aced1225ecaeac6366f7586aa3de92761cdff8711d81fbd81f248076abd.sqlite +0 -0
  96. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/686c3a9f0d7e59ed2ab607efd4b76d779c97cafeb3818380033bf7c7eb86c819.sqlite +0 -0
  97. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/6e8214e8dcfadd0deb52d64e5e9ca85c6b329ace11193909845995396914c473.sqlite +0 -0
  98. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/78d9ec9ff873d3fe3507ff53c2a6f6dfc408b4268eb0db3f2a146c0678965366.sqlite +0 -0
  99. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/7eff9f0ed7e27ad0d3f9d923de0682fab1928591172c1ba336c5f79a134a5d85.sqlite +0 -0
  100. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/836cda5b995b25867d722ed4f4c2292167e80351a3c6038db626648eb247dd8b.sqlite +0 -0
  101. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/91ef63b112209ab30172763acd8a0935106c248f7f1bcae5545ce37a9f201551.sqlite +0 -0
  102. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/a66ea4293a5f5938bc6d116edfa2522bb85bc37aea3541fbc09c3b613b9b32c0.sqlite +0 -0
  103. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/ceb2ab26b80590840b65651deb6e948d3bf81565c6751f3a58752cf4bf4aecae.sqlite +0 -0
  104. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite +0 -0
  105. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite-shm +0 -0
  106. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite-wal +0 -0
  107. package/src/cf-do/ARCHITECTURE.md +83 -0
  108. package/src/cf-do/watermark.test.ts +103 -0
  109. package/src/cf-do/watermark.ts +118 -0
  110. package/src/cf-do/worker.ts +1033 -0
  111. package/src/cf-do/wrangler.toml +11 -0
  112. package/src/config.ts +5 -0
  113. package/src/do-sql-tracking.test.ts +19 -0
  114. package/src/do-sql-tracking.ts +19 -0
  115. package/src/index.ts +76 -28
  116. package/src/pg-proxy-browser.ts +6 -6
  117. package/src/pg-proxy-do-backend.test.ts +3890 -0
  118. package/src/pg-proxy-do-backend.ts +7157 -0
  119. package/src/pglite-ipc.test.ts +17 -0
  120. package/src/pglite-ipc.ts +31 -12
  121. package/src/pglite-web-proxy.test.ts +57 -0
  122. package/src/pglite-web-proxy.ts +48 -7
  123. package/src/replication/change-tracker.ts +16 -1
  124. package/src/replication/handler.test.ts +35 -0
  125. package/src/replication/handler.ts +7 -2
  126. package/src/replication/pgoutput-encoder.test.ts +71 -2
  127. package/src/replication/pgoutput-encoder.ts +65 -30
  128. package/src/worker/browser-build-config.test.ts +12 -0
  129. package/src/worker/browser-build-config.ts +2 -1
  130. package/src/worker/cf-patches.ts +274 -4
  131. package/src/worker/shims/node-stub.ts +53 -1
  132. package/src/worker/shims/oxfmt.ts +3 -0
  133. package/src/worker/shims/postgres-socket.ts +1 -1
  134. package/src/worker/shims/sqlite.test.ts +145 -0
  135. package/src/worker/shims/sqlite.ts +256 -9
  136. package/src/worker/shims/ws.ts +45 -0
  137. package/src/worker/shims/zero-process-env.ts +11 -0
  138. package/src/worker/zero-cache-embed-cf.ts +114 -18
@@ -0,0 +1,813 @@
1
+ // @ts-nocheck — cloudflare:workers types not available in orez
2
+ import { DurableObject } from 'cloudflare:workers';
3
+ import { trackedChangeRow } from '../do-sql-tracking.js';
4
+ import { DurableWatermarkState } from './watermark.js';
5
+ const SCHEMA_VERSION = 1;
6
+ const SQL_ERROR_SNIPPET_RADIUS = 1600;
7
+ const SQL_ERROR_FALLBACK_LIMIT = 4000;
8
+ function quoteIdent(name) {
9
+ return `"${name.replace(/"/g, '""')}"`;
10
+ }
11
+ function sqliteErrorOffset(message) {
12
+ const marker = 'offset ';
13
+ const start = message.indexOf(marker);
14
+ if (start < 0)
15
+ return null;
16
+ let index = start + marker.length;
17
+ let digits = '';
18
+ while (index < message.length) {
19
+ const code = message.charCodeAt(index);
20
+ if (code < 48 || code > 57)
21
+ break;
22
+ digits += message[index];
23
+ index++;
24
+ }
25
+ if (!digits)
26
+ return null;
27
+ const offset = Number(digits);
28
+ return Number.isFinite(offset) ? offset : null;
29
+ }
30
+ function sqlErrorSnippet(sql, message) {
31
+ const offset = sqliteErrorOffset(message);
32
+ if (offset !== null) {
33
+ const start = Math.max(0, offset - SQL_ERROR_SNIPPET_RADIUS);
34
+ const end = Math.min(sql.length, offset + SQL_ERROR_SNIPPET_RADIUS);
35
+ return `${start > 0 ? '...' : ''}${sql.slice(start, end)}${end < sql.length ? '...' : ''}`;
36
+ }
37
+ if (sql.length <= SQL_ERROR_FALLBACK_LIMIT)
38
+ return sql;
39
+ return `${sql.slice(0, SQL_ERROR_FALLBACK_LIMIT)}...`;
40
+ }
41
+ export class ZeroDO extends DurableObject {
42
+ sql;
43
+ watermarks;
44
+ schemaTables = new Set();
45
+ tableSchemas = new Map();
46
+ constructor(ctx, env) {
47
+ super(ctx, env);
48
+ this.sql = ctx.storage.sql;
49
+ this.watermarks = new DurableWatermarkState(this.sql);
50
+ }
51
+ async fetch(request) {
52
+ const url = new URL(request.url);
53
+ if (request.method === 'OPTIONS') {
54
+ return new Response(null, {
55
+ headers: {
56
+ 'Access-Control-Allow-Origin': '*',
57
+ 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
58
+ 'Access-Control-Allow-Headers': '*',
59
+ },
60
+ });
61
+ }
62
+ if (url.pathname.startsWith('/sync/v') && url.pathname.endsWith('/connect'))
63
+ return this.handleSyncConnect(request, url);
64
+ if ((url.pathname === '/zero/push' || url.pathname === '/api/zero/push') &&
65
+ request.method === 'POST')
66
+ return this.handleHttpPush(request);
67
+ if (url.pathname === '/exec' && request.method === 'POST')
68
+ return this.handleExec(request);
69
+ if (url.pathname === '/batch' && request.method === 'POST')
70
+ return this.handleBatch(request);
71
+ if (url.pathname === '/changes' &&
72
+ (request.method === 'GET' || request.method === 'POST'))
73
+ return this.handleChanges(request, url);
74
+ if (url.pathname === '/notify' && request.method === 'POST')
75
+ return Response.json({ ok: true, cookie: this.cookie() });
76
+ return new Response('not found', { status: 404 });
77
+ }
78
+ // ── Zero sync protocol ──────────────────────────────────────────────────
79
+ handleSyncConnect(request, url) {
80
+ if (request.headers.get('upgrade')?.toLowerCase() !== 'websocket') {
81
+ return new Response('expected websocket upgrade', { status: 426 });
82
+ }
83
+ const pair = new WebSocketPair();
84
+ const client = pair[0];
85
+ const server = pair[1];
86
+ const clientID = url.searchParams.get('clientID') || 'anon';
87
+ const clientGroupID = url.searchParams.get('clientGroupID') || 'default';
88
+ const userID = url.searchParams.get('userID') || 'anon';
89
+ const wsid = url.searchParams.get('wsid') || crypto.randomUUID();
90
+ const baseCookie = url.searchParams.get('baseCookie');
91
+ this.ctx.acceptWebSocket(server);
92
+ server.serializeAttachment({
93
+ clientID,
94
+ clientGroupID,
95
+ userID,
96
+ cookie: baseCookie ? baseCookie : null,
97
+ initialized: false,
98
+ desiredTableNames: [],
99
+ desiredQueries: [],
100
+ });
101
+ this.sendJSON(server, ['connected', { wsid, timestamp: Date.now() }]);
102
+ const secProtocol = request.headers.get('sec-websocket-protocol');
103
+ if (secProtocol) {
104
+ const initData = decodeInitConnection(secProtocol);
105
+ if (initData) {
106
+ const clientSchema = initData[1]?.clientSchema;
107
+ const patch = (initData[1]?.desiredQueriesPatch || []);
108
+ this.applyDesiredQueries(server, patch, clientSchema);
109
+ }
110
+ }
111
+ return new Response(null, {
112
+ status: 101,
113
+ headers: secProtocol ? { 'Sec-WebSocket-Protocol': secProtocol } : undefined,
114
+ webSocket: client,
115
+ });
116
+ }
117
+ async webSocketMessage(socket, messageData) {
118
+ this.watermarks.ensureTables();
119
+ const ws = socket;
120
+ const attachment = this.readSocketAttachment(ws);
121
+ if (!attachment)
122
+ return;
123
+ const message = this.parseMessage(messageData);
124
+ if (!message)
125
+ return;
126
+ const body = message[1] || {};
127
+ switch (message[0]) {
128
+ case 'initConnection':
129
+ case 'changeDesiredQueries':
130
+ this.applyDesiredQueries(ws, (body.desiredQueriesPatch || []), body.clientSchema);
131
+ break;
132
+ case 'push':
133
+ this.handlePush(ws, attachment, message[1]);
134
+ break;
135
+ case 'pull':
136
+ this.handlePull(ws, message[1]);
137
+ break;
138
+ case 'ping':
139
+ this.sendJSON(ws, ['pong', {}]);
140
+ break;
141
+ }
142
+ }
143
+ webSocketClose(_socket, _code, _reason, _wasClean) { }
144
+ applyDesiredQueries(socket, patch, clientSchema) {
145
+ const attachment = this.readSocketAttachment(socket);
146
+ if (!attachment)
147
+ return;
148
+ if (clientSchema)
149
+ this.ensureSchemaTables(clientSchema);
150
+ let nextAttachment = this.applyDesiredQueryPatch(attachment, patch);
151
+ socket.serializeAttachment(nextAttachment);
152
+ if (!nextAttachment.initialized) {
153
+ nextAttachment = this.sendSyncPoke(socket, { ...nextAttachment, initialized: true }, { lastMutationIDChanges: {}, rowsPatch: [] });
154
+ }
155
+ if (patch.length === 0)
156
+ return;
157
+ const rowsPatch = [
158
+ { op: 'clear' },
159
+ ...this.rowsPatchForTables(nextAttachment.desiredTableNames),
160
+ ];
161
+ this.sendSyncPoke(socket, nextAttachment, {
162
+ gotQueriesPatch: this.gotQueriesPatch(patch),
163
+ rowsPatch,
164
+ });
165
+ }
166
+ applyDesiredQueryPatch(attachment, patch) {
167
+ const desiredQueries = new Map();
168
+ for (const query of attachment.desiredQueries || [])
169
+ desiredQueries.set(query.hash, query.tableNames);
170
+ for (const op of patch) {
171
+ if (op.op === 'clear') {
172
+ desiredQueries.clear();
173
+ }
174
+ else if (op.op === 'put' && op.hash) {
175
+ desiredQueries.set(op.hash, this.resolveTablesFromPatch([op]));
176
+ }
177
+ else if (op.op === 'del' && op.hash) {
178
+ desiredQueries.delete(op.hash);
179
+ }
180
+ }
181
+ const queries = [...desiredQueries.entries()].map(([hash, tableNames]) => ({
182
+ hash,
183
+ tableNames,
184
+ }));
185
+ return {
186
+ ...attachment,
187
+ desiredQueries: queries,
188
+ desiredTableNames: [...new Set(queries.flatMap((query) => query.tableNames))],
189
+ };
190
+ }
191
+ gotQueriesPatch(patch) {
192
+ const got = [];
193
+ for (const op of patch) {
194
+ if (op.op === 'clear')
195
+ got.push({ op: 'clear' });
196
+ else if (op.hash)
197
+ got.push({ op: op.op, hash: op.hash });
198
+ }
199
+ return got;
200
+ }
201
+ rowsPatchForTables(tableNames) {
202
+ const rowsPatch = [];
203
+ for (const tn of tableNames) {
204
+ if (!this.tableExists(tn))
205
+ continue;
206
+ for (const row of this.readAllRows(tn))
207
+ rowsPatch.push({ op: 'put', tableName: tn, value: row });
208
+ }
209
+ return rowsPatch;
210
+ }
211
+ resolveTablesFromPatch(patch) {
212
+ const tables = [];
213
+ for (const op of patch) {
214
+ const tableFromName = this.tableNameFromOperationName(op.name);
215
+ if (tableFromName)
216
+ tables.push(tableFromName);
217
+ if (op.ast)
218
+ this.extractTableFromAST(op.ast, tables);
219
+ }
220
+ return tables;
221
+ }
222
+ extractTableFromAST(ast, tables) {
223
+ if (ast?.table)
224
+ tables.push(ast.table);
225
+ if (ast?.related)
226
+ for (const rel of ast.related) {
227
+ if (rel?.subquery?.table)
228
+ tables.push(rel.subquery.table);
229
+ if (rel?.subquery?.related)
230
+ this.extractTableFromAST(rel.subquery, tables);
231
+ }
232
+ }
233
+ handlePush(socket, attachment, body) {
234
+ const mutations = Array.isArray(body?.mutations) ? body.mutations : [];
235
+ const before = this.watermark();
236
+ const mutationResults = [];
237
+ const lastMutationIDChanges = {};
238
+ for (const m of mutations) {
239
+ const result = this.applyMutation(m);
240
+ mutationResults.push({ id: { clientID: m.clientID, id: m.id }, result });
241
+ lastMutationIDChanges[m.clientID] = m.id;
242
+ }
243
+ this.sendJSON(socket, ['pushResponse', { mutations: mutationResults }]);
244
+ const after = this.watermark();
245
+ const changes = after > before ? this.readChangesSince(before) : [];
246
+ const rowsPatch = changes.map((c) => this.syncRowPatchFromChange(c));
247
+ if (Object.keys(lastMutationIDChanges).length > 0 || rowsPatch.length > 0)
248
+ this.broadcastMutationPoke(attachment, {
249
+ lastMutationIDChanges,
250
+ rowsPatch,
251
+ });
252
+ }
253
+ async handleHttpPush(request) {
254
+ try {
255
+ const body = (await request.json());
256
+ const before = this.watermark();
257
+ const mutations = Array.isArray(body?.mutations) ? body.mutations : [];
258
+ const mutationResults = [];
259
+ const lastMutationIDChanges = {};
260
+ for (const m of mutations) {
261
+ const result = this.applyMutation(m);
262
+ mutationResults.push({ id: { clientID: m.clientID, id: m.id }, result });
263
+ lastMutationIDChanges[m.clientID] = m.id;
264
+ }
265
+ const after = this.watermark();
266
+ const changes = after > before ? this.readChangesSince(before) : [];
267
+ const rowsPatch = changes.map((c) => this.syncRowPatchFromChange(c));
268
+ if (Object.keys(lastMutationIDChanges).length > 0 || rowsPatch.length > 0)
269
+ this.broadcastPoke(body?.clientGroupID || 'default', {
270
+ lastMutationIDChanges,
271
+ rowsPatch,
272
+ });
273
+ return Response.json({ mutations: mutationResults });
274
+ }
275
+ catch (err) {
276
+ return Response.json({ error: err.message }, { status: 500 });
277
+ }
278
+ }
279
+ handlePull(socket, body) {
280
+ this.sendJSON(socket, [
281
+ 'pull',
282
+ {
283
+ requestID: body?.requestID || crypto.randomUUID(),
284
+ cookie: this.cookie(),
285
+ lastMutationIDChanges: {},
286
+ patch: [],
287
+ },
288
+ ]);
289
+ }
290
+ // ── SQL execution endpoints ─────────────────────────────────────────────
291
+ async handleExec(request) {
292
+ let sql = '';
293
+ try {
294
+ const body = (await request.json());
295
+ sql = body.sql;
296
+ const params = Array.isArray(body.params) ? body.params : [];
297
+ const result = await this.ctx.storage.transaction(() => this.executeSQL(sql, params, body.track));
298
+ return Response.json(result);
299
+ }
300
+ catch (err) {
301
+ const suffix = sql ? ` while executing: ${sqlErrorSnippet(sql, err.message)}` : '';
302
+ console.error(`[exec-500] ${err.message} :: SQL=${sql.slice(0, 800)}`);
303
+ return Response.json({ error: `${err.message}${suffix}` }, { status: 500 });
304
+ }
305
+ }
306
+ /** Execute multiple statements atomically via ctx.storage.transaction() */
307
+ async handleBatch(request) {
308
+ try {
309
+ const { statements } = (await request.json());
310
+ const allRows = await this.ctx.storage.transaction(() => {
311
+ const results = [];
312
+ for (const statement of statements) {
313
+ const item = typeof statement === 'string' ? { sql: statement } : statement;
314
+ if (!item?.sql?.trim())
315
+ continue;
316
+ try {
317
+ results.push(this.executeSQL(item.sql, Array.isArray(item.params) ? item.params : [], item.track));
318
+ }
319
+ catch (err) {
320
+ throw new Error(`${err.message} while executing: ${sqlErrorSnippet(item.sql, err.message)}`);
321
+ }
322
+ }
323
+ return results;
324
+ });
325
+ return Response.json({ results: allRows });
326
+ }
327
+ catch (err) {
328
+ return Response.json({ error: err.message }, { status: 500 });
329
+ }
330
+ }
331
+ async handleChanges(request, url) {
332
+ try {
333
+ let watermark = Number(url.searchParams.get('watermark') ?? url.searchParams.get('since') ?? 0);
334
+ let limit = Number(url.searchParams.get('limit') ?? 1000);
335
+ if (request.method === 'POST') {
336
+ const body = (await request.json().catch(() => ({})));
337
+ watermark = Number(body.watermark ?? body.since ?? watermark);
338
+ limit = Number(body.limit ?? limit);
339
+ }
340
+ if (!Number.isFinite(watermark) || watermark < 0)
341
+ watermark = 0;
342
+ if (!Number.isFinite(limit) || limit <= 0)
343
+ limit = 1000;
344
+ return Response.json({
345
+ watermark: this.watermark(),
346
+ changes: this.readChangesSince(watermark).slice(0, Math.min(limit, 10_000)),
347
+ });
348
+ }
349
+ catch (err) {
350
+ return Response.json({ error: err.message }, { status: 500 });
351
+ }
352
+ }
353
+ executeSQL(sql, params = [], track) {
354
+ const cursor = this.sql.exec(sql, ...params);
355
+ const columns = Array.isArray(cursor.columnNames) ? cursor.columnNames : [];
356
+ const rows = this.cursorRows(cursor);
357
+ if (!track)
358
+ return { rows, columns };
359
+ for (const row of rows) {
360
+ const trackedRow = trackedChangeRow(row, track);
361
+ if (track.operation === 'DELETE')
362
+ this.appendTrackedChange(track.tableName, 'DELETE', null, trackedRow);
363
+ else
364
+ this.appendTrackedChange(track.tableName, track.operation, trackedRow, null);
365
+ }
366
+ return {
367
+ rows: track.returnRows ? rows : [],
368
+ columns: track.returnRows ? columns : [],
369
+ affectedRows: rows.length,
370
+ };
371
+ }
372
+ cursorRows(cursor) {
373
+ return cursor.toArray().map((row) => {
374
+ const obj = {};
375
+ for (const k of Object.keys(row))
376
+ obj[k] = row[k];
377
+ return obj;
378
+ });
379
+ }
380
+ // ── CRUD operations ──────────────────────────────────────────────────────
381
+ applyMutation(mutation) {
382
+ if (mutation.type === 'crud' && mutation.name === '_zero_crud') {
383
+ return this.applyCrudMutation(mutation);
384
+ }
385
+ if (mutation.name === '_zero_cleanupResults')
386
+ return {};
387
+ if (mutation.type === 'custom')
388
+ return this.applyTableMutation(mutation);
389
+ return {
390
+ error: 'app',
391
+ message: `unsupported mutation ${mutation.type}:${mutation.name}`,
392
+ };
393
+ }
394
+ applyTableMutation(mutation) {
395
+ const [tableName, action] = this.tableActionFromMutationName(mutation.name);
396
+ if (!tableName || !action)
397
+ return { error: 'app', message: `invalid mutation name ${mutation.name}` };
398
+ if (!this.tableExists(tableName))
399
+ return { error: 'app', message: `unknown table ${tableName}` };
400
+ const value = (mutation.args[0] || {});
401
+ const primaryKey = this.primaryKeyForTable(tableName, []);
402
+ if (action === 'insert')
403
+ this.insertRow(tableName, value, primaryKey);
404
+ else if (action === 'upsert')
405
+ this.upsertRow(tableName, value, primaryKey);
406
+ else if (action === 'delete')
407
+ this.deleteRow(tableName, value, primaryKey);
408
+ else
409
+ this.updateRow(tableName, value, primaryKey);
410
+ return {};
411
+ }
412
+ tableActionFromMutationName(name) {
413
+ if (name.includes('|'))
414
+ return name.split('|', 2);
415
+ return name.split('.', 2);
416
+ }
417
+ tableNameFromOperationName(name) {
418
+ if (!name)
419
+ return null;
420
+ return name.split(/[.|]/, 1)[0] || null;
421
+ }
422
+ applyCrudMutation(mutation) {
423
+ const arg = mutation.args[0];
424
+ const ops = Array.isArray(arg?.ops) ? arg.ops : [];
425
+ for (const crud of ops) {
426
+ if (!crud?.tableName)
427
+ return { error: 'app', message: 'invalid crud mutation' };
428
+ if (!this.tableExists(crud.tableName))
429
+ return { error: 'app', message: `unknown table ${crud.tableName}` };
430
+ const value = crud.value || {};
431
+ const primaryKey = this.primaryKeyForTable(crud.tableName, crud.primaryKey || []);
432
+ if (crud.op === 'insert')
433
+ this.insertRow(crud.tableName, value, primaryKey);
434
+ else if (crud.op === 'upsert')
435
+ this.upsertRow(crud.tableName, value, primaryKey);
436
+ else if (crud.op === 'update')
437
+ this.updateRow(crud.tableName, value, primaryKey);
438
+ else if (crud.op === 'delete')
439
+ this.deleteRow(crud.tableName, value, primaryKey);
440
+ }
441
+ return {};
442
+ }
443
+ insertRow(tn, value, pk) {
444
+ if (this.readRowByPrimaryKey(tn, value, pk))
445
+ return;
446
+ const row = this.storageRow(tn, value, true);
447
+ const cols = Object.keys(row);
448
+ if (!cols.length)
449
+ return;
450
+ const qc = cols.map((c) => quoteIdent(c)).join(', ');
451
+ const ph = cols.map(() => '?').join(', ');
452
+ this.sql.exec(`INSERT INTO ${quoteIdent(tn)} (${qc}) VALUES (${ph})`, ...cols.map((c) => row[c]));
453
+ const next = this.readRowByPrimaryKey(tn, value, pk) || this.normalizeRow(tn, row);
454
+ this.appendChange(tn, 'INSERT', next, null);
455
+ }
456
+ upsertRow(tn, value, pk) {
457
+ const existing = this.readRowByPrimaryKey(tn, value, pk);
458
+ if (existing) {
459
+ this.updateRow(tn, value, pk);
460
+ return;
461
+ }
462
+ this.insertRow(tn, value, pk);
463
+ }
464
+ updateRow(tn, value, pk) {
465
+ if (!pk.length)
466
+ return;
467
+ const existing = this.readRowByPrimaryKey(tn, value, pk);
468
+ if (!existing)
469
+ return;
470
+ const nk = Object.keys(value).filter((c) => !pk.includes(c));
471
+ if (!nk.length)
472
+ return;
473
+ const storage = this.storageRow(tn, value, false);
474
+ this.sql.exec(`UPDATE ${quoteIdent(tn)} SET ${nk.map((c) => `${quoteIdent(c)} = ?`).join(', ')} WHERE ${this.primaryKeyWhere(pk)}`, ...nk.map((c) => storage[c]), ...pk.map((c) => this.storageColumnValue(tn, c, value[c])));
475
+ const next = this.readRowByPrimaryKey(tn, value, pk);
476
+ if (next)
477
+ this.appendChange(tn, 'UPDATE', next, existing);
478
+ }
479
+ deleteRow(tn, value, pk) {
480
+ if (!pk.length)
481
+ return;
482
+ const existing = this.readRowByPrimaryKey(tn, value, pk);
483
+ if (!existing)
484
+ return;
485
+ this.sql.exec(`DELETE FROM ${quoteIdent(tn)} WHERE ${this.primaryKeyWhere(pk)}`, ...pk.map((c) => this.storageColumnValue(tn, c, value[c])));
486
+ this.appendChange(tn, 'DELETE', null, existing);
487
+ }
488
+ appendChange(tn, op, rowData, oldData) {
489
+ this.appendTrackedChange(tn, op, rowData, oldData);
490
+ }
491
+ appendTrackedChange(tableName, op, rowData, oldData) {
492
+ this.watermarks.ensureTables();
493
+ const watermark = this.watermarks.next();
494
+ this.sql.exec('INSERT INTO _zero_changes (watermark, table_name, op, row_data, old_data) VALUES (?, ?, ?, ?, ?)', watermark, tableName, op, rowData ? JSON.stringify(rowData) : null, oldData ? JSON.stringify(oldData) : null);
495
+ this.watermarks.mark(watermark);
496
+ }
497
+ readChangesSince(watermark) {
498
+ this.watermarks.ensureTables();
499
+ return this.sql
500
+ .exec('SELECT watermark, table_name, op, row_data, old_data FROM _zero_changes WHERE watermark > ? ORDER BY watermark', watermark)
501
+ .toArray()
502
+ .map((row) => ({
503
+ watermark: Number(row.watermark),
504
+ tableName: String(row.table_name),
505
+ op: String(row.op),
506
+ rowData: row.row_data ? JSON.parse(String(row.row_data)) : null,
507
+ oldData: row.old_data ? JSON.parse(String(row.old_data)) : null,
508
+ }));
509
+ }
510
+ watermark() {
511
+ return this.watermarks.current();
512
+ }
513
+ ensureSchemaTables(clientSchema) {
514
+ this.ensureSchemaMetadataTable();
515
+ for (const [name, def] of Object.entries(clientSchema.tables)) {
516
+ this.tableSchemas.set(name, def);
517
+ this.sql.exec('INSERT OR REPLACE INTO _zero_schema_tables (name, schema_json) VALUES (?, ?)', name, JSON.stringify(def));
518
+ if (this.schemaTables.has(name))
519
+ continue;
520
+ const pk = def.primaryKey.map((c) => quoteIdent(c));
521
+ const pkClause = pk.length ? `, PRIMARY KEY (${pk.join(', ')})` : '';
522
+ const colDefs = Object.entries(def.columns).map(([cn, cd]) => {
523
+ const t = {
524
+ string: 'TEXT',
525
+ number: 'REAL',
526
+ boolean: 'INTEGER',
527
+ json: 'TEXT',
528
+ bigint: 'TEXT',
529
+ };
530
+ return `${quoteIdent(cn)} ${t[cd.type] || 'TEXT'}`;
531
+ });
532
+ this.sql.exec(`CREATE TABLE IF NOT EXISTS ${quoteIdent(name)} (${colDefs.join(', ')}${pkClause})`);
533
+ this.schemaTables.add(name);
534
+ }
535
+ }
536
+ ensureSchemaMetadataTable() {
537
+ this.sql.exec('CREATE TABLE IF NOT EXISTS _zero_schema_tables (name TEXT PRIMARY KEY, schema_json TEXT NOT NULL)');
538
+ }
539
+ schemaForTable(tableName) {
540
+ const cached = this.tableSchemas.get(tableName);
541
+ if (cached)
542
+ return cached;
543
+ try {
544
+ this.ensureSchemaMetadataTable();
545
+ const row = this.sql
546
+ .exec('SELECT schema_json FROM _zero_schema_tables WHERE name = ?', tableName)
547
+ .one();
548
+ if (!row?.schema_json)
549
+ return undefined;
550
+ const schema = JSON.parse(String(row.schema_json));
551
+ this.tableSchemas.set(tableName, schema);
552
+ return schema;
553
+ }
554
+ catch {
555
+ return undefined;
556
+ }
557
+ }
558
+ tableExists(n) {
559
+ try {
560
+ return !!this.sql
561
+ .exec("SELECT name FROM sqlite_master WHERE type='table' AND name=?", n)
562
+ .one();
563
+ }
564
+ catch {
565
+ return false;
566
+ }
567
+ }
568
+ readAllRows(tn) {
569
+ try {
570
+ return this.sql
571
+ .exec(`SELECT * FROM ${quoteIdent(tn)}`)
572
+ .toArray()
573
+ .map((row) => this.normalizeRow(tn, row));
574
+ }
575
+ catch {
576
+ return [];
577
+ }
578
+ }
579
+ readRowByPrimaryKey(tn, value, pk) {
580
+ if (!pk.length)
581
+ return null;
582
+ try {
583
+ const row = this.sql
584
+ .exec(`SELECT * FROM ${quoteIdent(tn)} WHERE ${this.primaryKeyWhere(pk)}`, ...pk.map((c) => this.storageColumnValue(tn, c, value[c])))
585
+ .one();
586
+ return row ? this.normalizeRow(tn, row) : null;
587
+ }
588
+ catch {
589
+ return null;
590
+ }
591
+ }
592
+ primaryKeyWhere(pk) {
593
+ return pk.map((c) => `${quoteIdent(c)} = ?`).join(' AND ');
594
+ }
595
+ primaryKeyForTable(tn, fallback) {
596
+ const schema = this.schemaForTable(tn);
597
+ if (schema?.primaryKey?.length)
598
+ return schema.primaryKey;
599
+ return fallback;
600
+ }
601
+ storageRow(tn, value, includeMissingSchemaColumns) {
602
+ const schema = this.schemaForTable(tn);
603
+ const row = {};
604
+ if (schema && includeMissingSchemaColumns) {
605
+ for (const column of Object.keys(schema.columns))
606
+ row[column] = this.storageColumnValue(tn, column, value[column] ?? null);
607
+ }
608
+ for (const column of Object.keys(value)) {
609
+ if (value[column] !== undefined)
610
+ row[column] = this.storageColumnValue(tn, column, value[column]);
611
+ }
612
+ return row;
613
+ }
614
+ storageColumnValue(tn, column, value) {
615
+ if (value === undefined || value === null)
616
+ return null;
617
+ const type = this.schemaForTable(tn)?.columns?.[column]?.type;
618
+ if (type === 'boolean')
619
+ return value ? 1 : 0;
620
+ if (type === 'json')
621
+ return typeof value === 'string' ? value : JSON.stringify(value);
622
+ if (type === 'number')
623
+ return Number(value);
624
+ if (type === 'bigint')
625
+ return String(value);
626
+ return value;
627
+ }
628
+ normalizeRow(tn, row) {
629
+ const schema = this.schemaForTable(tn);
630
+ const normalized = {};
631
+ for (const key of Object.keys(row)) {
632
+ const type = schema?.columns?.[key]?.type;
633
+ const value = row[key];
634
+ if (value === null || value === undefined) {
635
+ normalized[key] = null;
636
+ }
637
+ else if (type === 'boolean') {
638
+ normalized[key] =
639
+ value === true || value === 1 || value === '1' || value === 'true';
640
+ }
641
+ else if (type === 'number') {
642
+ normalized[key] = Number(value);
643
+ }
644
+ else if (type === 'json' && typeof value === 'string') {
645
+ try {
646
+ normalized[key] = JSON.parse(value);
647
+ }
648
+ catch {
649
+ normalized[key] = value;
650
+ }
651
+ }
652
+ else {
653
+ normalized[key] = value;
654
+ }
655
+ }
656
+ return normalized;
657
+ }
658
+ sendSyncPoke(socket, attachment, part) {
659
+ const cookie = this.nextCookie();
660
+ const pokeID = crypto.randomUUID();
661
+ this.sendJSON(socket, [
662
+ 'pokeStart',
663
+ {
664
+ pokeID,
665
+ baseCookie: attachment.cookie,
666
+ schemaVersions: {
667
+ minSupportedVersion: SCHEMA_VERSION,
668
+ maxSupportedVersion: SCHEMA_VERSION,
669
+ },
670
+ timestamp: Date.now(),
671
+ },
672
+ ]);
673
+ this.sendJSON(socket, ['pokePart', { pokeID, ...part }]);
674
+ this.sendJSON(socket, ['pokeEnd', { pokeID, cookie }]);
675
+ const nextAttachment = { ...attachment, cookie };
676
+ socket.serializeAttachment(nextAttachment);
677
+ return nextAttachment;
678
+ }
679
+ broadcastPoke(clientGroupID, part) {
680
+ for (const socket of this.ctx.getWebSockets()) {
681
+ const ws = socket;
682
+ const attachment = this.readSocketAttachment(ws);
683
+ if (!attachment)
684
+ continue;
685
+ if (attachment.clientGroupID !== clientGroupID)
686
+ continue;
687
+ this.sendSyncPoke(ws, attachment, part);
688
+ }
689
+ }
690
+ broadcastMutationPoke(sourceAttachment, part) {
691
+ const rowsPatch = part.rowsPatch || [];
692
+ const changedTables = new Set(rowsPatch
693
+ .map((op) => op?.tableName)
694
+ .filter((tableName) => !!tableName));
695
+ const hasLastMutationIDChanges = Object.keys(part.lastMutationIDChanges || {}).length > 0;
696
+ for (const socket of this.ctx.getWebSockets()) {
697
+ const ws = socket;
698
+ const attachment = this.readSocketAttachment(ws);
699
+ if (!attachment)
700
+ continue;
701
+ if (attachment.userID !== sourceAttachment.userID)
702
+ continue;
703
+ const isSourceClientGroup = attachment.clientGroupID === sourceAttachment.clientGroupID;
704
+ const wantsChangedRows = changedTables.size > 0 &&
705
+ attachment.desiredTableNames.some((tableName) => changedTables.has(tableName));
706
+ const nextPart = {};
707
+ if (wantsChangedRows)
708
+ nextPart.rowsPatch = rowsPatch;
709
+ if (isSourceClientGroup && hasLastMutationIDChanges)
710
+ nextPart.lastMutationIDChanges = part.lastMutationIDChanges;
711
+ if (!nextPart.rowsPatch && !nextPart.lastMutationIDChanges)
712
+ continue;
713
+ this.sendSyncPoke(ws, attachment, nextPart);
714
+ }
715
+ }
716
+ syncRowPatchFromChange(change) {
717
+ if (change.op === 'DELETE')
718
+ return {
719
+ op: 'del',
720
+ tableName: change.tableName,
721
+ id: this.primaryKeyValue(change.tableName, change.oldData || {}),
722
+ };
723
+ return {
724
+ op: 'put',
725
+ tableName: change.tableName,
726
+ value: this.normalizeRow(change.tableName, change.rowData || {}),
727
+ };
728
+ }
729
+ primaryKeyValue(tableName, row) {
730
+ const pk = this.primaryKeyForTable(tableName, []);
731
+ if (pk.length)
732
+ return Object.fromEntries(pk.map((column) => [column, row[column]]));
733
+ if ('id' in row)
734
+ return { id: row.id };
735
+ return row;
736
+ }
737
+ cookie() {
738
+ return String(this.watermark()).padStart(20, '0');
739
+ }
740
+ nextCookie() {
741
+ const watermark = this.watermarks.next();
742
+ this.watermarks.mark(watermark);
743
+ return String(watermark).padStart(20, '0');
744
+ }
745
+ readSocketAttachment(socket) {
746
+ const attachment = socket.deserializeAttachment();
747
+ if (!attachment)
748
+ return null;
749
+ return {
750
+ ...attachment,
751
+ initialized: attachment.initialized === true,
752
+ desiredTableNames: attachment.desiredTableNames || [],
753
+ desiredQueries: attachment.desiredQueries || [],
754
+ };
755
+ }
756
+ sendJSON(socket, msg) {
757
+ try {
758
+ socket.send(JSON.stringify(msg));
759
+ }
760
+ catch { }
761
+ }
762
+ parseMessage(data) {
763
+ try {
764
+ return JSON.parse(typeof data === 'string' ? data : new TextDecoder().decode(data));
765
+ }
766
+ catch {
767
+ return null;
768
+ }
769
+ }
770
+ }
771
+ export default {
772
+ async fetch(request, env) {
773
+ const url = new URL(request.url);
774
+ const id = env.ZERO_DO.idFromName('singleton');
775
+ if (url.pathname.startsWith('/sync/v') && url.pathname.endsWith('/connect')) {
776
+ return env.ZERO_DO.get(id).fetch(request);
777
+ }
778
+ if ((url.pathname === '/zero/push' || url.pathname === '/api/zero/push') &&
779
+ request.method === 'POST') {
780
+ return env.ZERO_DO.get(id).fetch(request);
781
+ }
782
+ if (url.pathname === '/exec' && request.method === 'POST') {
783
+ return env.ZERO_DO.get(id).fetch(request);
784
+ }
785
+ if (url.pathname === '/batch' && request.method === 'POST') {
786
+ return env.ZERO_DO.get(id).fetch(request);
787
+ }
788
+ if (url.pathname === '/changes' &&
789
+ (request.method === 'GET' || request.method === 'POST')) {
790
+ return env.ZERO_DO.get(id).fetch(request);
791
+ }
792
+ if (url.pathname === '/notify' && request.method === 'POST') {
793
+ return env.ZERO_DO.get(id).fetch(request);
794
+ }
795
+ return new Response('not found', { status: 404 });
796
+ },
797
+ };
798
+ function decodeInitConnection(secProtocol) {
799
+ try {
800
+ const decoded = decodeURIComponent(secProtocol);
801
+ const bytes = Uint8Array.from(atob(decoded), (char) => char.charCodeAt(0));
802
+ const protocols = JSON.parse(new TextDecoder().decode(bytes));
803
+ const message = protocols.initConnectionMessage;
804
+ if (Array.isArray(message) && message[0] === 'initConnection') {
805
+ return message;
806
+ }
807
+ return null;
808
+ }
809
+ catch {
810
+ return null;
811
+ }
812
+ }
813
+ //# sourceMappingURL=worker.js.map