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