orez 0.2.24 → 0.2.25

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 (42) 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/worker.d.ts +65 -0
  6. package/dist/cf-do/worker.d.ts.map +1 -0
  7. package/dist/cf-do/worker.js +440 -0
  8. package/dist/cf-do/worker.js.map +1 -0
  9. package/dist/config.d.ts +4 -0
  10. package/dist/config.d.ts.map +1 -1
  11. package/dist/config.js +1 -0
  12. package/dist/config.js.map +1 -1
  13. package/dist/index.d.ts +2 -3
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +60 -28
  16. package/dist/index.js.map +1 -1
  17. package/dist/pg-proxy-do-backend.d.ts +49 -0
  18. package/dist/pg-proxy-do-backend.d.ts.map +1 -0
  19. package/dist/pg-proxy-do-backend.js +713 -0
  20. package/dist/pg-proxy-do-backend.js.map +1 -0
  21. package/dist/pglite-ipc.d.ts +3 -0
  22. package/dist/pglite-ipc.d.ts.map +1 -1
  23. package/dist/pglite-ipc.js +34 -12
  24. package/dist/pglite-ipc.js.map +1 -1
  25. package/dist/pglite-web-proxy.d.ts +3 -0
  26. package/dist/pglite-web-proxy.d.ts.map +1 -1
  27. package/dist/pglite-web-proxy.js +50 -7
  28. package/dist/pglite-web-proxy.js.map +1 -1
  29. package/dist/query-rewrites.d.ts +2 -0
  30. package/dist/query-rewrites.d.ts.map +1 -0
  31. package/dist/query-rewrites.js +140 -0
  32. package/dist/query-rewrites.js.map +1 -0
  33. package/package.json +2 -2
  34. package/src/config.ts +5 -0
  35. package/src/index.ts +66 -33
  36. package/src/pg-proxy-do-backend.ts +840 -0
  37. package/src/pglite-ipc.test.ts +17 -0
  38. package/src/pglite-ipc.ts +31 -12
  39. package/src/pglite-web-proxy.test.ts +57 -0
  40. package/src/pglite-web-proxy.ts +48 -7
  41. package/src/query-rewrites.test.ts +30 -0
  42. package/src/query-rewrites.ts +152 -0
@@ -0,0 +1,11 @@
1
+ /**
2
+ * quick protocol test: connects to the DO, sends initConnection,
3
+ * does CRUD, verifies pokes.
4
+ *
5
+ * usage:
6
+ * npx wrangler dev --port 8799 &
7
+ * sleep 3
8
+ * bun run src/cf-do/test-protocol.ts
9
+ */
10
+ export {};
11
+ //# sourceMappingURL=test-protocol.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-protocol.d.ts","sourceRoot":"","sources":["../../src/cf-do/test-protocol.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG"}
@@ -0,0 +1,137 @@
1
+ "use strict";
2
+ /**
3
+ * quick protocol test: connects to the DO, sends initConnection,
4
+ * does CRUD, verifies pokes.
5
+ *
6
+ * usage:
7
+ * npx wrangler dev --port 8799 &
8
+ * sleep 3
9
+ * bun run src/cf-do/test-protocol.ts
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ const ws_1 = require("ws");
13
+ const DO_URL = 'http://127.0.0.1:8799';
14
+ const CLIENT_SCHEMA = {
15
+ tables: {
16
+ todo: {
17
+ primaryKey: ['id'],
18
+ columns: {
19
+ id: { type: 'string' },
20
+ title: { type: 'string' },
21
+ completed: { type: 'boolean' },
22
+ sortOrder: { type: 'number' },
23
+ createdAt: { type: 'number' },
24
+ updatedAt: { type: 'number' },
25
+ },
26
+ },
27
+ user: {
28
+ primaryKey: ['id'],
29
+ columns: {
30
+ id: { type: 'string' },
31
+ name: { type: 'string' },
32
+ },
33
+ },
34
+ },
35
+ };
36
+ const FOO_QUERY = {
37
+ table: 'todo',
38
+ orderBy: [['sortOrder', 'asc']],
39
+ };
40
+ const INIT_CONNECTION_MSG = [
41
+ 'initConnection',
42
+ {
43
+ desiredQueriesPatch: [
44
+ { op: 'put', hash: 'query-hash-1', ast: FOO_QUERY },
45
+ ],
46
+ clientSchema: CLIENT_SCHEMA,
47
+ },
48
+ ];
49
+ function makeInitURL(clientGroupID = 'test-group', clientID = 'test-client') {
50
+ const params = new URLSearchParams({
51
+ clientGroupID,
52
+ clientID,
53
+ userID: 'test-user',
54
+ wsid: 'ws-1',
55
+ schemaVersion: '1',
56
+ ts: String(Date.now()),
57
+ });
58
+ return `ws://127.0.0.1:8799/sync/v49/connect?${params}`;
59
+ }
60
+ async function waitForMessage(ws, timeoutMs = 5000) {
61
+ return new Promise((resolve, reject) => {
62
+ const timeout = setTimeout(() => reject(new Error('timeout')), timeoutMs);
63
+ ws.once('message', (data) => {
64
+ clearTimeout(timeout);
65
+ resolve(JSON.parse(data.toString()));
66
+ });
67
+ });
68
+ }
69
+ async function main() {
70
+ console.log('connecting to DO...');
71
+ const ws = new ws_1.WebSocket(makeInitURL(), {
72
+ headers: {
73
+ 'Sec-WebSocket-Protocol': btoa(JSON.stringify(INIT_CONNECTION_MSG)),
74
+ },
75
+ });
76
+ await new Promise((resolve, reject) => {
77
+ ws.on('open', resolve);
78
+ ws.on('error', reject);
79
+ setTimeout(() => reject(new Error('connect timeout')), 5000);
80
+ });
81
+ console.log('connected!');
82
+ // Should receive: connected, pokeStart, pokePart, pokeEnd
83
+ const connected = await waitForMessage(ws);
84
+ console.log('got:', JSON.stringify(connected).slice(0, 200));
85
+ const pokeStart = await waitForMessage(ws);
86
+ console.log('got:', JSON.stringify(pokeStart).slice(0, 200));
87
+ const pokePart = await waitForMessage(ws);
88
+ console.log('got:', JSON.stringify(pokePart).slice(0, 400));
89
+ const pokeEnd = await waitForMessage(ws);
90
+ console.log('got:', JSON.stringify(pokeEnd).slice(0, 200));
91
+ // Send CRUD push
92
+ console.log('\nsending push mutation...');
93
+ ws.send(JSON.stringify([
94
+ 'push',
95
+ {
96
+ mutations: [
97
+ {
98
+ type: 'crud',
99
+ name: '_zero_crud',
100
+ clientID: 'test-client',
101
+ id: 1,
102
+ args: [{
103
+ ops: [{
104
+ op: 'upsert',
105
+ tableName: 'todo',
106
+ value: {
107
+ id: 'todo-1',
108
+ title: 'test todo',
109
+ completed: false,
110
+ sortOrder: 0,
111
+ createdAt: Date.now(),
112
+ updatedAt: Date.now(),
113
+ },
114
+ primaryKey: ['id'],
115
+ }],
116
+ }],
117
+ },
118
+ ],
119
+ },
120
+ ]));
121
+ // Should receive: pushResponse + pokeStart/pokePart/pokeEnd
122
+ const pushResponse = await waitForMessage(ws);
123
+ console.log('pushResponse:', JSON.stringify(pushResponse).slice(0, 200));
124
+ const pokeStart2 = await waitForMessage(ws);
125
+ console.log('poke:', JSON.stringify(pokeStart2).slice(0, 200));
126
+ const pokePart2 = await waitForMessage(ws);
127
+ console.log('pokePart:', JSON.stringify(pokePart2).slice(0, 400));
128
+ const pokeEnd2 = await waitForMessage(ws);
129
+ console.log('pokeEnd:', JSON.stringify(pokeEnd2).slice(0, 200));
130
+ console.log('\n✓ protocol test passed!');
131
+ ws.close();
132
+ }
133
+ main().catch((err) => {
134
+ console.error('FAIL:', err);
135
+ process.exit(1);
136
+ });
137
+ //# sourceMappingURL=test-protocol.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-protocol.js","sourceRoot":"","sources":["../../src/cf-do/test-protocol.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,65 @@
1
+ import { DurableObject } from 'cloudflare:workers';
2
+ /**
3
+ * zero-do: a Durable Object that speaks the Zero sync protocol
4
+ * over DO-native SQLite. replaces zero-cache + pg-proxy + pGlite
5
+ * with a single DO that handles initConnection, push/pull, and pokes.
6
+ */
7
+ interface Env {
8
+ ZERO_DO: DurableObjectNamespace;
9
+ }
10
+ export declare class ZeroDO extends DurableObject {
11
+ private sql;
12
+ private initialized;
13
+ private schemaTables;
14
+ constructor(ctx: DurableObjectState, env: Env);
15
+ fetch(request: Request): Promise<Response>;
16
+ private handleSyncConnect;
17
+ webSocketMessage(socket: WebSocket, messageData: string | ArrayBuffer): Promise<void>;
18
+ webSocketClose(socket: WebSocket, code: number, reason: string, wasClean: boolean): Promise<void>;
19
+ private applyDesiredQueries;
20
+ private resolveTablesFromPatch;
21
+ private extractTableFromAST;
22
+ private handlePush;
23
+ private handleHttpPush;
24
+ private applyCrudMutation;
25
+ private upsertRow;
26
+ private updateRow;
27
+ private deleteRow;
28
+ private appendChange;
29
+ private readChangesSince;
30
+ private currentWatermark;
31
+ private ensureInternalTables;
32
+ private ensureSchemaTables;
33
+ private createTable;
34
+ private mapZeroTypeToSQLite;
35
+ private tableExists;
36
+ private readAllRows;
37
+ private sendSyncPoke;
38
+ private broadcastPoke;
39
+ private syncRowPatchFromChange;
40
+ private extractID;
41
+ private sendJSON;
42
+ private parseMessage;
43
+ private decodeInitConnection;
44
+ }
45
+ declare const _default: {
46
+ fetch(request: Request, env: Env): Promise<Response>;
47
+ };
48
+ export default _default;
49
+ interface DurableObjectState {
50
+ storage: {
51
+ sql: any;
52
+ };
53
+ acceptWebSocket(socket: WebSocket): void;
54
+ getWebSockets(): WebSocket[];
55
+ }
56
+ interface DurableObjectNamespace {
57
+ idFromName(name: string): DurableObjectId;
58
+ get(id: DurableObjectId): DurableObjectStub;
59
+ }
60
+ interface DurableObjectId {
61
+ }
62
+ interface DurableObjectStub {
63
+ fetch(request: Request): Promise<Response>;
64
+ }
65
+ //# sourceMappingURL=worker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../../src/cf-do/worker.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAElD;;;;GAIG;AAIH,UAAU,GAAG;IACX,OAAO,EAAE,sBAAsB,CAAA;CAChC;AA0DD,qBAAa,MAAO,SAAQ,aAAa;IACvC,OAAO,CAAC,GAAG,CAAK;IAChB,OAAO,CAAC,WAAW,CAAQ;IAE3B,OAAO,CAAC,YAAY,CAAoB;gBAE5B,GAAG,EAAE,kBAAkB,EAAE,GAAG,EAAE,GAAG;IAOvC,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IA0BhD,OAAO,CAAC,iBAAiB;IA+CnB,gBAAgB,CAAC,MAAM,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,GAAG,WAAW;IAiCrE,cAAc,CAAC,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO;IAMvF,OAAO,CAAC,mBAAmB;IA0C3B,OAAO,CAAC,sBAAsB;IAW9B,OAAO,CAAC,mBAAmB;IAiB3B,OAAO,CAAC,UAAU;YAsCJ,cAAc;IA6B5B,OAAO,CAAC,iBAAiB;IAuBzB,OAAO,CAAC,SAAS;IAiBjB,OAAO,CAAC,SAAS;IAgBjB,OAAO,CAAC,SAAS;IAajB,OAAO,CAAC,YAAY;IAUpB,OAAO,CAAC,gBAAgB;IAcxB,OAAO,CAAC,gBAAgB;IAOxB,OAAO,CAAC,oBAAoB;IAgB5B,OAAO,CAAC,kBAAkB;IAQ1B,OAAO,CAAC,WAAW;IAYnB,OAAO,CAAC,mBAAmB;IAW3B,OAAO,CAAC,WAAW;IASnB,OAAO,CAAC,WAAW;IAkBnB,OAAO,CAAC,YAAY;IAqBpB,OAAO,CAAC,aAAa;IAarB,OAAO,CAAC,sBAAsB;IAQ9B,OAAO,CAAC,SAAS;IAUjB,OAAO,CAAC,QAAQ;IAIhB,OAAO,CAAC,YAAY;IASpB,OAAO,CAAC,oBAAoB;CAQ7B;;mBAKsB,OAAO,OAAO,GAAG,GAAG,OAAO,CAAC,QAAQ,CAAC;;AAD5D,wBAqBC;AAID,UAAU,kBAAkB;IAC1B,OAAO,EAAE;QAAE,GAAG,EAAE,GAAG,CAAA;KAAE,CAAA;IACrB,eAAe,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAAA;IACxC,aAAa,IAAI,SAAS,EAAE,CAAA;CAC7B;AAED,UAAU,sBAAsB;IAC9B,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,CAAA;IACzC,GAAG,CAAC,EAAE,EAAE,eAAe,GAAG,iBAAiB,CAAA;CAC5C;AAED,UAAU,eAAe;CAAI;AAE7B,UAAU,iBAAiB;IACzB,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;CAC3C"}
@@ -0,0 +1,440 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ZeroDO = void 0;
4
+ // @ts-nocheck — cloudflare:workers types not available in orez
5
+ const cloudflare_workers_1 = require("cloudflare:workers");
6
+ // ── constants ─────────────────────────────────────────────────────────
7
+ const SCHEMA_VERSION = 1;
8
+ // ── Durable Object ────────────────────────────────────────────────────
9
+ class ZeroDO extends cloudflare_workers_1.DurableObject {
10
+ sql;
11
+ initialized = false;
12
+ // track which tables have been created from clientSchema
13
+ schemaTables = new Set();
14
+ constructor(ctx, env) {
15
+ super(ctx, env);
16
+ this.sql = ctx.storage.sql;
17
+ }
18
+ // ── fetch handler (HTTP + WebSocket upgrade) ────────────────────────
19
+ async fetch(request) {
20
+ this.ensureInternalTables();
21
+ const url = new URL(request.url);
22
+ if (request.method === 'OPTIONS') {
23
+ return new Response(null, {
24
+ headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', 'Access-Control-Allow-Headers': '*' },
25
+ });
26
+ }
27
+ // Zero sync protocol WebSocket endpoint
28
+ if (url.pathname === '/sync/v49/connect') {
29
+ return this.handleSyncConnect(request, url);
30
+ }
31
+ // Push mutations via HTTP (used by app server to proxy pushes)
32
+ if (url.pathname === '/zero/push' && request.method === 'POST') {
33
+ return this.handleHttpPush(request);
34
+ }
35
+ return new Response('not found', { status: 404 });
36
+ }
37
+ // ── Zero sync protocol WebSocket handler ────────────────────────────
38
+ handleSyncConnect(request, url) {
39
+ if (request.headers.get('upgrade')?.toLowerCase() !== 'websocket') {
40
+ return new Response('expected websocket upgrade', { status: 426 });
41
+ }
42
+ const pair = new WebSocketPair();
43
+ const client = pair[0];
44
+ const socket = pair[1];
45
+ const clientID = url.searchParams.get('clientID') || 'anon';
46
+ const clientGroupID = url.searchParams.get('clientGroupID') || 'default';
47
+ const userID = url.searchParams.get('userID') || 'anon';
48
+ const baseCookieParam = url.searchParams.get('baseCookie');
49
+ this.ctx.acceptWebSocket(socket);
50
+ const attachment = {
51
+ clientID,
52
+ clientGroupID,
53
+ userID,
54
+ cookie: baseCookieParam || null,
55
+ desiredTableNames: [],
56
+ };
57
+ socket.serializeAttachment(attachment);
58
+ // Send connected message
59
+ this.sendJSON(socket, ['connected', { wsid: url.searchParams.get('wsid') || crypto.randomUUID(), timestamp: Date.now() }]);
60
+ // Decode init connection from Sec-WebSocket-Protocol header
61
+ const secProtocol = request.headers.get('sec-websocket-protocol');
62
+ const initData = secProtocol ? this.decodeInitConnection(secProtocol) : null;
63
+ if (initData) {
64
+ const clientSchema = initData[1]?.clientSchema;
65
+ const patch = (initData[1]?.desiredQueriesPatch || []);
66
+ this.applyDesiredQueries(socket, patch, clientSchema);
67
+ }
68
+ return new Response(null, {
69
+ status: 101,
70
+ headers: secProtocol ? { 'Sec-WebSocket-Protocol': secProtocol } : undefined,
71
+ webSocket: client,
72
+ });
73
+ }
74
+ // ── WebSocket message handler ───────────────────────────────────────
75
+ async webSocketMessage(socket, messageData) {
76
+ this.ensureInternalTables();
77
+ const socketState = socket;
78
+ const attachment = socketState.deserializeAttachment();
79
+ if (!attachment)
80
+ return;
81
+ const message = this.parseMessage(messageData);
82
+ if (!message)
83
+ return;
84
+ switch (message[0]) {
85
+ case 'initConnection':
86
+ case 'changeDesiredQueries': {
87
+ const patch = (message[1]?.desiredQueriesPatch || []);
88
+ const clientSchema = message[1]?.clientSchema;
89
+ this.applyDesiredQueries(socketState, patch, clientSchema);
90
+ break;
91
+ }
92
+ case 'push':
93
+ this.handlePush(socketState, attachment, message[1]);
94
+ break;
95
+ case 'ping':
96
+ this.sendJSON(socketState, ['pong', {}]);
97
+ break;
98
+ case 'deleteClients':
99
+ case 'closeConnection':
100
+ case 'ackMutationResponses':
101
+ case 'updateAuth':
102
+ // no-op for experiment
103
+ break;
104
+ }
105
+ }
106
+ async webSocketClose(socket, code, reason, wasClean) {
107
+ // cleanup if needed
108
+ }
109
+ // ── handle desired queries (initial data / query change) ────────────
110
+ applyDesiredQueries(socket, patch, clientSchema) {
111
+ const attachment = socket.deserializeAttachment();
112
+ if (!attachment)
113
+ return;
114
+ // Create tables from client schema
115
+ if (clientSchema) {
116
+ this.ensureSchemaTables(clientSchema);
117
+ }
118
+ // Track desired tables from AST references
119
+ const tableNames = this.resolveTablesFromPatch(patch);
120
+ const allTableNames = [...new Set([...attachment.desiredTableNames, ...tableNames])];
121
+ const nextAttachment = {
122
+ ...attachment,
123
+ desiredTableNames: allTableNames,
124
+ };
125
+ socket.serializeAttachment(nextAttachment);
126
+ // Send initial poke with all rows for desired tables
127
+ if (allTableNames.length > 0) {
128
+ const rowsPatch = [];
129
+ for (const tableName of allTableNames) {
130
+ if (!this.tableExists(tableName))
131
+ continue;
132
+ const rows = this.readAllRows(tableName);
133
+ for (const row of rows) {
134
+ rowsPatch.push({ op: 'put', tableName, value: row });
135
+ }
136
+ }
137
+ if (rowsPatch.length > 0) {
138
+ this.sendSyncPoke(socket, attachment, { rowsPatch });
139
+ }
140
+ }
141
+ }
142
+ // ── resolve table names from query AST ──────────────────────────────
143
+ resolveTablesFromPatch(patch) {
144
+ const tableNames = [];
145
+ for (const op of patch) {
146
+ if (!op.ast)
147
+ continue;
148
+ // Extract table references from the AST
149
+ const tables = this.extractTableFromAST(op.ast);
150
+ tableNames.push(...tables);
151
+ }
152
+ return tableNames;
153
+ }
154
+ extractTableFromAST(ast) {
155
+ const tables = [];
156
+ if (ast?.table)
157
+ tables.push(ast.table);
158
+ if (ast?.related) {
159
+ for (const rel of ast.related) {
160
+ if (rel?.subquery?.table)
161
+ tables.push(rel.subquery.table);
162
+ // recurse into nested related
163
+ if (rel?.subquery?.related) {
164
+ tables.push(...this.extractTableFromAST(rel.subquery));
165
+ }
166
+ }
167
+ }
168
+ return tables;
169
+ }
170
+ // ── handle push mutations ──────────────────────────────────────────
171
+ handlePush(socket, attachment, body) {
172
+ const mutations = Array.isArray(body?.mutations) ? body.mutations : [];
173
+ const beforeWatermark = this.currentWatermark();
174
+ const mutationResults = [];
175
+ const lastMutationIDChanges = {};
176
+ for (const mutation of mutations) {
177
+ if (mutation.type === 'crud' && mutation.name === '_zero_crud') {
178
+ this.applyCrudMutation(mutation);
179
+ }
180
+ mutationResults.push({
181
+ id: { clientID: mutation.clientID, id: mutation.id },
182
+ result: {},
183
+ });
184
+ lastMutationIDChanges[mutation.clientID] = mutation.id;
185
+ }
186
+ // Send push response
187
+ this.sendJSON(socket, ['pushResponse', { mutations: mutationResults }]);
188
+ // Build poke from changes
189
+ const afterWatermark = this.currentWatermark();
190
+ if (afterWatermark > beforeWatermark) {
191
+ const changes = this.readChangesSince(beforeWatermark);
192
+ const rowsPatch = changes.map((change) => this.syncRowPatchFromChange(change));
193
+ if (rowsPatch.length > 0) {
194
+ this.broadcastPoke(attachment.clientGroupID, { lastMutationIDChanges, rowsPatch });
195
+ }
196
+ }
197
+ }
198
+ // ── HTTP push (used by app server proxy) ────────────────────────────
199
+ async handleHttpPush(request) {
200
+ try {
201
+ const body = await request.json();
202
+ const mutations = Array.isArray(body?.mutations) ? body.mutations : [];
203
+ const beforeWatermark = this.currentWatermark();
204
+ for (const mutation of mutations) {
205
+ if (mutation.type === 'crud' && mutation.name === '_zero_crud') {
206
+ this.applyCrudMutation(mutation);
207
+ }
208
+ }
209
+ const afterWatermark = this.currentWatermark();
210
+ if (afterWatermark > beforeWatermark) {
211
+ const changes = this.readChangesSince(beforeWatermark);
212
+ const rowsPatch = changes.map((change) => this.syncRowPatchFromChange(change));
213
+ if (rowsPatch.length > 0) {
214
+ this.broadcastPoke('default', { rowsPatch });
215
+ }
216
+ }
217
+ return Response.json({ ok: true });
218
+ }
219
+ catch (err) {
220
+ return Response.json({ error: err.message }, { status: 500 });
221
+ }
222
+ }
223
+ // ── apply a single CRUD mutation ────────────────────────────────────
224
+ applyCrudMutation(mutation) {
225
+ const arg = mutation.args[0];
226
+ const ops = Array.isArray(arg?.ops) ? arg.ops : [];
227
+ for (const crud of ops) {
228
+ if (!crud || typeof crud !== 'object')
229
+ continue;
230
+ const tableName = crud.tableName;
231
+ if (!tableName || !this.tableExists(tableName))
232
+ continue;
233
+ const value = crud.value || {};
234
+ if (crud.op === 'insert' || crud.op === 'upsert') {
235
+ this.upsertRow(tableName, value);
236
+ }
237
+ else if (crud.op === 'update') {
238
+ this.updateRow(tableName, value, crud.primaryKey || []);
239
+ }
240
+ else if (crud.op === 'delete') {
241
+ this.deleteRow(tableName, value, crud.primaryKey || []);
242
+ }
243
+ }
244
+ }
245
+ // ── row operations ──────────────────────────────────────────────────
246
+ upsertRow(tableName, value) {
247
+ const columns = Object.keys(value);
248
+ if (columns.length === 0)
249
+ return;
250
+ const quotedCols = columns.map((c) => `"${c}"`).join(', ');
251
+ const placeholders = columns.map(() => '?').join(', ');
252
+ const updateSet = columns.map((c) => `"${c}" = ?`).join(', ');
253
+ this.sql.exec(`INSERT INTO "${tableName}" (${quotedCols}) VALUES (${placeholders}) ON CONFLICT DO UPDATE SET ${updateSet}`, ...columns.map((c) => value[c]), ...columns.map((c) => value[c]));
254
+ this.appendChange(tableName, 'INSERT', value, null);
255
+ }
256
+ updateRow(tableName, value, primaryKey) {
257
+ const nonKeyCols = Object.keys(value).filter((c) => !primaryKey.includes(c));
258
+ if (nonKeyCols.length === 0)
259
+ return;
260
+ const updateSet = nonKeyCols.map((c) => `"${c}" = ?`).join(', ');
261
+ const whereClause = primaryKey.map((c) => `"${c}" = ?`).join(' AND ');
262
+ this.sql.exec(`UPDATE "${tableName}" SET ${updateSet} WHERE ${whereClause}`, ...nonKeyCols.map((c) => value[c]), ...primaryKey.map((c) => value[c]));
263
+ this.appendChange(tableName, 'UPDATE', value, null);
264
+ }
265
+ deleteRow(tableName, value, primaryKey) {
266
+ const whereClause = primaryKey.map((c) => `"${c}" = ?`).join(' AND ');
267
+ this.sql.exec(`DELETE FROM "${tableName}" WHERE ${whereClause}`, ...primaryKey.map((c) => value[c]));
268
+ this.appendChange(tableName, 'DELETE', null, value);
269
+ }
270
+ // ── change tracking ─────────────────────────────────────────────────
271
+ appendChange(tableName, op, rowData, oldData) {
272
+ this.sql.exec('INSERT INTO _zero_changes (table_name, op, row_data, old_data) VALUES (?, ?, ?, ?)', tableName, op, rowData ? JSON.stringify(rowData) : null, oldData ? JSON.stringify(oldData) : null);
273
+ }
274
+ readChangesSince(watermark) {
275
+ const cursor = this.sql.exec('SELECT watermark, table_name, op, row_data, old_data FROM _zero_changes WHERE watermark > ? ORDER BY watermark', watermark);
276
+ return cursor.toArray().map((row) => ({
277
+ watermark: Number(row.watermark),
278
+ tableName: String(row.table_name),
279
+ op: String(row.op),
280
+ rowData: row.row_data ? JSON.parse(String(row.row_data)) : null,
281
+ oldData: row.old_data ? JSON.parse(String(row.old_data)) : null,
282
+ }));
283
+ }
284
+ currentWatermark() {
285
+ const row = this.sql.exec('SELECT COALESCE(MAX(watermark), 0) AS watermark FROM _zero_changes').one();
286
+ return row?.watermark ?? 0;
287
+ }
288
+ // ── table management ────────────────────────────────────────────────
289
+ ensureInternalTables() {
290
+ if (this.initialized)
291
+ return;
292
+ this.initialized = true;
293
+ this.sql.exec(`
294
+ CREATE TABLE IF NOT EXISTS _zero_changes (
295
+ watermark INTEGER PRIMARY KEY AUTOINCREMENT,
296
+ table_name TEXT NOT NULL,
297
+ op TEXT NOT NULL CHECK (op IN ('INSERT', 'UPDATE', 'DELETE')),
298
+ row_data TEXT,
299
+ old_data TEXT,
300
+ created_at INTEGER NOT NULL DEFAULT (unixepoch())
301
+ )
302
+ `);
303
+ }
304
+ ensureSchemaTables(clientSchema) {
305
+ for (const [name, def] of Object.entries(clientSchema.tables)) {
306
+ if (this.schemaTables.has(name))
307
+ continue;
308
+ this.createTable(name, def);
309
+ this.schemaTables.add(name);
310
+ }
311
+ }
312
+ createTable(name, def) {
313
+ const pkColumns = def.primaryKey.map((c) => `"${c}"`);
314
+ const pkClause = pkColumns.length > 0 ? `, PRIMARY KEY (${pkColumns.join(', ')})` : '';
315
+ const columnDefs = Object.entries(def.columns).map(([colName, colDef]) => {
316
+ const sqlType = this.mapZeroTypeToSQLite(colDef.type);
317
+ return `"${colName}" ${sqlType}`;
318
+ });
319
+ this.sql.exec(`CREATE TABLE IF NOT EXISTS "${name}" (${columnDefs.join(', ')}${pkClause})`);
320
+ }
321
+ mapZeroTypeToSQLite(zeroType) {
322
+ switch (zeroType) {
323
+ case 'string': return 'TEXT';
324
+ case 'number': return 'REAL';
325
+ case 'boolean': return 'INTEGER';
326
+ case 'json': return 'TEXT';
327
+ case 'bigint': return 'TEXT';
328
+ default: return 'TEXT';
329
+ }
330
+ }
331
+ tableExists(name) {
332
+ try {
333
+ const row = this.sql.exec(`SELECT name FROM sqlite_master WHERE type='table' AND name=?`, name).one();
334
+ return !!row;
335
+ }
336
+ catch {
337
+ return false;
338
+ }
339
+ }
340
+ readAllRows(tableName) {
341
+ try {
342
+ const cursor = this.sql.exec(`SELECT * FROM "${tableName}"`);
343
+ return cursor.toArray().map((row) => {
344
+ const result = {};
345
+ // SQLite DO returns columns as properties
346
+ for (const key of Object.keys(row)) {
347
+ result[key] = row[key];
348
+ }
349
+ return result;
350
+ });
351
+ }
352
+ catch {
353
+ return [];
354
+ }
355
+ }
356
+ // ── poke helpers ────────────────────────────────────────────────────
357
+ sendSyncPoke(socket, attachment, part) {
358
+ const watermark = this.currentWatermark();
359
+ const cookie = String(watermark).padStart(20, '0');
360
+ const pokeID = crypto.randomUUID();
361
+ this.sendJSON(socket, ['pokeStart', {
362
+ pokeID,
363
+ baseCookie: attachment.cookie,
364
+ schemaVersions: { minSupportedVersion: SCHEMA_VERSION, maxSupportedVersion: SCHEMA_VERSION },
365
+ timestamp: Date.now(),
366
+ }]);
367
+ this.sendJSON(socket, ['pokePart', { pokeID, ...part }]);
368
+ this.sendJSON(socket, ['pokeEnd', { pokeID, cookie }]);
369
+ socket.serializeAttachment({ ...attachment, cookie });
370
+ }
371
+ broadcastPoke(clientGroupID, part) {
372
+ const websockets = this.ctx.getWebSockets();
373
+ for (const ws of websockets) {
374
+ const socket = ws;
375
+ const attachment = socket.deserializeAttachment();
376
+ if (!attachment)
377
+ continue;
378
+ this.sendSyncPoke(socket, attachment, part);
379
+ }
380
+ }
381
+ syncRowPatchFromChange(change) {
382
+ if (change.op === 'DELETE') {
383
+ const id = change.oldData ? this.extractID(change.oldData) : {};
384
+ return { op: 'del', tableName: change.tableName, id };
385
+ }
386
+ return { op: 'put', tableName: change.tableName, value: change.rowData };
387
+ }
388
+ extractID(row) {
389
+ // For a simple id column, use it directly
390
+ if (row.id !== undefined)
391
+ return { id: row.id };
392
+ // Otherwise use first key
393
+ const key = Object.keys(row)[0];
394
+ return key ? { [key]: row[key] } : {};
395
+ }
396
+ // ── message helpers ─────────────────────────────────────────────────
397
+ sendJSON(socket, message) {
398
+ socket.send(JSON.stringify(message));
399
+ }
400
+ parseMessage(data) {
401
+ try {
402
+ const text = typeof data === 'string' ? data : new TextDecoder().decode(data);
403
+ return JSON.parse(text);
404
+ }
405
+ catch {
406
+ return null;
407
+ }
408
+ }
409
+ decodeInitConnection(secProtocol) {
410
+ try {
411
+ const decoded = atob(secProtocol);
412
+ return JSON.parse(decoded);
413
+ }
414
+ catch {
415
+ return null;
416
+ }
417
+ }
418
+ }
419
+ exports.ZeroDO = ZeroDO;
420
+ // ── Worker entry ───────────────────────────────────────────────────────
421
+ exports.default = {
422
+ async fetch(request, env) {
423
+ const url = new URL(request.url);
424
+ // Route to DO: /sync/v49/connect → named DO instance
425
+ if (url.pathname === '/sync/v49/connect') {
426
+ const clientGroupID = url.searchParams.get('clientGroupID') || 'default';
427
+ const doId = env.ZERO_DO.idFromName(clientGroupID);
428
+ const stub = env.ZERO_DO.get(doId);
429
+ return stub.fetch(request);
430
+ }
431
+ // Push mutations via HTTP
432
+ if (url.pathname === '/zero/push' && request.method === 'POST') {
433
+ const doId = env.ZERO_DO.idFromName('default');
434
+ const stub = env.ZERO_DO.get(doId);
435
+ return stub.fetch(request);
436
+ }
437
+ return new Response('not found', { status: 404 });
438
+ },
439
+ };
440
+ //# sourceMappingURL=worker.js.map