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.
- package/dist/cf-do/test-protocol.d.ts +11 -0
- package/dist/cf-do/test-protocol.d.ts.map +1 -0
- package/dist/cf-do/test-protocol.js +137 -0
- package/dist/cf-do/test-protocol.js.map +1 -0
- package/dist/cf-do/worker.d.ts +65 -0
- package/dist/cf-do/worker.d.ts.map +1 -0
- package/dist/cf-do/worker.js +440 -0
- package/dist/cf-do/worker.js.map +1 -0
- package/dist/config.d.ts +4 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +2 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +60 -28
- package/dist/index.js.map +1 -1
- package/dist/pg-proxy-do-backend.d.ts +49 -0
- package/dist/pg-proxy-do-backend.d.ts.map +1 -0
- package/dist/pg-proxy-do-backend.js +713 -0
- package/dist/pg-proxy-do-backend.js.map +1 -0
- package/dist/pglite-ipc.d.ts +3 -0
- package/dist/pglite-ipc.d.ts.map +1 -1
- package/dist/pglite-ipc.js +34 -12
- package/dist/pglite-ipc.js.map +1 -1
- package/dist/pglite-web-proxy.d.ts +3 -0
- package/dist/pglite-web-proxy.d.ts.map +1 -1
- package/dist/pglite-web-proxy.js +50 -7
- package/dist/pglite-web-proxy.js.map +1 -1
- package/dist/query-rewrites.d.ts +2 -0
- package/dist/query-rewrites.d.ts.map +1 -0
- package/dist/query-rewrites.js +140 -0
- package/dist/query-rewrites.js.map +1 -0
- package/package.json +2 -2
- package/src/config.ts +5 -0
- package/src/index.ts +66 -33
- package/src/pg-proxy-do-backend.ts +840 -0
- package/src/pglite-ipc.test.ts +17 -0
- package/src/pglite-ipc.ts +31 -12
- package/src/pglite-web-proxy.test.ts +57 -0
- package/src/pglite-web-proxy.ts +48 -7
- package/src/query-rewrites.test.ts +30 -0
- 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
|