orez 0.1.43 → 0.1.45
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/admin/http-proxy.d.ts.map +1 -1
- package/dist/admin/http-proxy.js +3 -1
- package/dist/admin/http-proxy.js.map +1 -1
- package/dist/admin/log-store.d.ts.map +1 -1
- package/dist/admin/log-store.js +5 -1
- package/dist/admin/log-store.js.map +1 -1
- package/dist/admin/server.d.ts.map +1 -1
- package/dist/admin/server.js +25 -25
- package/dist/admin/server.js.map +1 -1
- package/dist/browser.d.ts +54 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +110 -0
- package/dist/browser.js.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/pg-proxy-browser.d.ts +26 -0
- package/dist/pg-proxy-browser.d.ts.map +1 -0
- package/dist/pg-proxy-browser.js +1460 -0
- package/dist/pg-proxy-browser.js.map +1 -0
- package/dist/pg-proxy.d.ts.map +1 -1
- package/dist/pg-proxy.js +48 -34
- package/dist/pg-proxy.js.map +1 -1
- package/dist/pglite-ipc.d.ts.map +1 -1
- package/dist/pglite-ipc.js +3 -2
- package/dist/pglite-ipc.js.map +1 -1
- package/dist/pglite-manager.d.ts.map +1 -1
- package/dist/pglite-manager.js +101 -111
- package/dist/pglite-manager.js.map +1 -1
- package/dist/pglite-web-proxy.d.ts +38 -0
- package/dist/pglite-web-proxy.d.ts.map +1 -0
- package/dist/pglite-web-proxy.js +155 -0
- package/dist/pglite-web-proxy.js.map +1 -0
- package/dist/pglite-web-worker.d.ts +24 -0
- package/dist/pglite-web-worker.d.ts.map +1 -0
- package/dist/pglite-web-worker.js +139 -0
- package/dist/pglite-web-worker.js.map +1 -0
- package/dist/pglite-worker-thread.js +65 -24
- package/dist/pglite-worker-thread.js.map +1 -1
- package/dist/recovery.js +2 -2
- package/dist/recovery.js.map +1 -1
- package/dist/replication/change-tracker.js +9 -9
- package/dist/replication/change-tracker.js.map +1 -1
- package/dist/replication/handler.d.ts.map +1 -1
- package/dist/replication/handler.js +34 -26
- package/dist/replication/handler.js.map +1 -1
- package/dist/worker/browser-build-config.d.ts.map +1 -1
- package/dist/worker/browser-build-config.js +5 -2
- package/dist/worker/browser-build-config.js.map +1 -1
- package/dist/worker/browser-embed.d.ts.map +1 -1
- package/dist/worker/browser-embed.js +31 -26
- package/dist/worker/browser-embed.js.map +1 -1
- package/dist/worker/shims/fastify.d.ts +1 -0
- package/dist/worker/shims/fastify.d.ts.map +1 -1
- package/dist/worker/shims/fastify.js +31 -20
- package/dist/worker/shims/fastify.js.map +1 -1
- package/dist/worker/shims/postgres-browser.d.ts +12 -0
- package/dist/worker/shims/postgres-browser.d.ts.map +1 -0
- package/dist/worker/shims/postgres-browser.js +52 -0
- package/dist/worker/shims/postgres-browser.js.map +1 -0
- package/dist/worker/shims/postgres-socket.d.ts +83 -0
- package/dist/worker/shims/postgres-socket.d.ts.map +1 -0
- package/dist/worker/shims/postgres-socket.js +278 -0
- package/dist/worker/shims/postgres-socket.js.map +1 -0
- package/dist/worker/shims/postgres.d.ts.map +1 -1
- package/dist/worker/shims/postgres.js +18 -9
- package/dist/worker/shims/postgres.js.map +1 -1
- package/dist/worker/shims/stream-browser.d.ts +5 -4
- package/dist/worker/shims/stream-browser.d.ts.map +1 -1
- package/dist/worker/shims/stream-browser.js +7 -6
- package/dist/worker/shims/stream-browser.js.map +1 -1
- package/dist/worker/shims/ws-browser.d.ts.map +1 -1
- package/dist/worker/shims/ws-browser.js +43 -21
- package/dist/worker/shims/ws-browser.js.map +1 -1
- package/dist/worker/shims/ws.d.ts.map +1 -1
- package/dist/worker/shims/ws.js +81 -17
- package/dist/worker/shims/ws.js.map +1 -1
- package/package.json +11 -58
- package/src/admin/http-proxy.ts +4 -1
- package/src/admin/log-store.ts +5 -1
- package/src/admin/server.ts +26 -25
- package/src/browser.ts +195 -0
- package/src/cli.ts +1 -1
- package/src/index.ts +5 -2
- package/src/integration/integration.test.ts +1 -1
- package/src/integration/restore-live-stress.test.ts +2 -2
- package/src/pg-proxy-browser.ts +1673 -0
- package/src/pg-proxy.ts +48 -40
- package/src/pglite-ipc.ts +3 -2
- package/src/pglite-manager.ts +115 -133
- package/src/pglite-web-proxy.ts +180 -0
- package/src/pglite-web-worker.ts +152 -0
- package/src/pglite-worker-thread.ts +66 -24
- package/src/recovery.ts +2 -2
- package/src/replication/change-tracker.test.ts +1 -1
- package/src/replication/change-tracker.ts +9 -9
- package/src/replication/handler.ts +37 -26
- package/src/worker/browser-build-config.test.ts +1 -1
- package/src/worker/browser-build-config.ts +5 -2
- package/src/worker/browser-embed.ts +33 -30
- package/src/worker/shims/fastify.ts +37 -24
- package/src/worker/shims/postgres-browser.ts +59 -0
- package/src/worker/shims/postgres-socket.test.ts +576 -0
- package/src/worker/shims/postgres-socket.ts +310 -0
- package/src/worker/shims/postgres.ts +30 -15
- package/src/worker/shims/stream-browser.ts +15 -0
- package/src/worker/shims/ws-browser.ts +38 -20
- package/src/worker/shims/ws.ts +76 -21
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PGlite Web Worker — browser equivalent of pglite-worker-thread.ts.
|
|
3
|
+
*
|
|
4
|
+
* runs a single PGlite instance in a Web Worker. receives commands via
|
|
5
|
+
* postMessage, executes on PGlite, sends results back. ArrayBuffers
|
|
6
|
+
* are transferred (not copied) for execProtocolRaw.
|
|
7
|
+
*
|
|
8
|
+
* message protocol (same as pglite-worker-thread.ts):
|
|
9
|
+
* init: { type: 'init', dataDir, name, withExtensions, pgliteOptions }
|
|
10
|
+
* → { type: 'ready' }
|
|
11
|
+
*
|
|
12
|
+
* execProtocolRaw: { type: 'execProtocolRaw', id, data: ArrayBuffer, options }
|
|
13
|
+
* → { type: 'result', id, data: ArrayBuffer }
|
|
14
|
+
*
|
|
15
|
+
* query: { type: 'query', id, sql, params }
|
|
16
|
+
* → { type: 'result', id, rows, affectedRows }
|
|
17
|
+
*
|
|
18
|
+
* exec: { type: 'exec', id, sql }
|
|
19
|
+
* → { type: 'result', id, results: [{ affectedRows }] }
|
|
20
|
+
*
|
|
21
|
+
* listen/unlisten/close: same as pglite-worker-thread.ts
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
// NOTE: this file is meant to be bundled with PGlite as external
|
|
25
|
+
// the consumer provides PGlite via importScripts or ESM import
|
|
26
|
+
|
|
27
|
+
declare const self: any
|
|
28
|
+
|
|
29
|
+
const listeners = new Map<number, () => Promise<void>>()
|
|
30
|
+
let db: any // PGlite instance — type depends on how it's loaded
|
|
31
|
+
|
|
32
|
+
self.onmessage = async (ev: MessageEvent) => {
|
|
33
|
+
const msg = ev.data
|
|
34
|
+
if (!msg || typeof msg !== 'object') return
|
|
35
|
+
const { type, id } = msg
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
switch (type) {
|
|
39
|
+
case 'init': {
|
|
40
|
+
// dynamically import PGlite (external, provided by consumer's bundler)
|
|
41
|
+
const { PGlite } = await import('@electric-sql/pglite')
|
|
42
|
+
db = new PGlite({
|
|
43
|
+
dataDir: msg.dataDir || 'idb://orez-pglite',
|
|
44
|
+
relaxedDurability: true,
|
|
45
|
+
initialMemory: 32 * 1024 * 1024,
|
|
46
|
+
startParams: [
|
|
47
|
+
'--single',
|
|
48
|
+
'-F',
|
|
49
|
+
'-O',
|
|
50
|
+
'-j',
|
|
51
|
+
'-c',
|
|
52
|
+
'search_path=public',
|
|
53
|
+
'-c',
|
|
54
|
+
'exit_on_error=false',
|
|
55
|
+
'-c',
|
|
56
|
+
'log_checkpoints=false',
|
|
57
|
+
'-c',
|
|
58
|
+
'shared_buffers=4MB',
|
|
59
|
+
'-c',
|
|
60
|
+
'wal_buffers=1MB',
|
|
61
|
+
],
|
|
62
|
+
...(msg.pgliteOptions || {}),
|
|
63
|
+
// extensions loaded by consumer if needed
|
|
64
|
+
})
|
|
65
|
+
await db.waitReady
|
|
66
|
+
|
|
67
|
+
// tune postgres internals — modest values for embedded use
|
|
68
|
+
await db.exec(`
|
|
69
|
+
SET work_mem = '4MB';
|
|
70
|
+
SET maintenance_work_mem = '16MB';
|
|
71
|
+
SET effective_cache_size = '64MB';
|
|
72
|
+
SET random_page_cost = 1.1;
|
|
73
|
+
SET jit = off;
|
|
74
|
+
`)
|
|
75
|
+
|
|
76
|
+
self.postMessage({ type: 'ready' })
|
|
77
|
+
break
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
case 'execProtocolRaw': {
|
|
81
|
+
const input = new Uint8Array(msg.data as ArrayBuffer)
|
|
82
|
+
const result = await db.execProtocolRaw(input, msg.options)
|
|
83
|
+
const buf = new ArrayBuffer(result.byteLength)
|
|
84
|
+
new Uint8Array(buf).set(result)
|
|
85
|
+
self.postMessage({ type: 'result', id, data: buf }, [buf])
|
|
86
|
+
break
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
case 'query': {
|
|
90
|
+
const result = await db.query(msg.sql, msg.params)
|
|
91
|
+
self.postMessage({
|
|
92
|
+
type: 'result',
|
|
93
|
+
id,
|
|
94
|
+
rows: result.rows,
|
|
95
|
+
affectedRows: result.affectedRows,
|
|
96
|
+
})
|
|
97
|
+
break
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
case 'exec': {
|
|
101
|
+
const result = await db.exec(msg.sql)
|
|
102
|
+
const results = result.map((r: any) => ({ affectedRows: r.affectedRows ?? 0 }))
|
|
103
|
+
self.postMessage({ type: 'result', id, results })
|
|
104
|
+
break
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
case 'listen': {
|
|
108
|
+
const unsub = await db.listen(msg.channel, (payload: string) => {
|
|
109
|
+
self.postMessage({ type: 'notification', channel: msg.channel, payload })
|
|
110
|
+
})
|
|
111
|
+
listeners.set(id, unsub)
|
|
112
|
+
self.postMessage({ type: 'result', id })
|
|
113
|
+
break
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
case 'unlisten': {
|
|
117
|
+
const unsub = listeners.get(msg.listenId)
|
|
118
|
+
if (unsub) {
|
|
119
|
+
await unsub()
|
|
120
|
+
listeners.delete(msg.listenId)
|
|
121
|
+
}
|
|
122
|
+
self.postMessage({ type: 'result', id })
|
|
123
|
+
break
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
case 'close': {
|
|
127
|
+
for (const unsub of listeners.values()) {
|
|
128
|
+
await unsub().catch(() => {})
|
|
129
|
+
}
|
|
130
|
+
listeners.clear()
|
|
131
|
+
await db.close()
|
|
132
|
+
self.postMessage({ type: 'result', id })
|
|
133
|
+
break
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
default:
|
|
137
|
+
self.postMessage({
|
|
138
|
+
type: 'error',
|
|
139
|
+
id,
|
|
140
|
+
message: `unknown message type: ${type}`,
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
} catch (err: unknown) {
|
|
144
|
+
const error = err as { message?: string; code?: string }
|
|
145
|
+
self.postMessage({
|
|
146
|
+
type: 'error',
|
|
147
|
+
id,
|
|
148
|
+
message: error?.message || String(err),
|
|
149
|
+
code: error?.code,
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -38,42 +38,84 @@ const listeners = new Map<number, () => Promise<void>>()
|
|
|
38
38
|
|
|
39
39
|
let db: PGlite
|
|
40
40
|
|
|
41
|
+
const PGLITE_BASE_FLAGS = [
|
|
42
|
+
'--single',
|
|
43
|
+
'-F',
|
|
44
|
+
'-O',
|
|
45
|
+
'-j',
|
|
46
|
+
'-c',
|
|
47
|
+
'search_path=public',
|
|
48
|
+
'-c',
|
|
49
|
+
'exit_on_error=false',
|
|
50
|
+
'-c',
|
|
51
|
+
'log_checkpoints=false',
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
const ZERO_START_PARAMS = [
|
|
55
|
+
...PGLITE_BASE_FLAGS,
|
|
56
|
+
'-c',
|
|
57
|
+
'shared_buffers=128kB',
|
|
58
|
+
'-c',
|
|
59
|
+
'wal_buffers=64kB',
|
|
60
|
+
'-c',
|
|
61
|
+
'work_mem=64kB',
|
|
62
|
+
'-c',
|
|
63
|
+
'maintenance_work_mem=1MB',
|
|
64
|
+
'-c',
|
|
65
|
+
'temp_buffers=800kB',
|
|
66
|
+
]
|
|
67
|
+
|
|
41
68
|
async function init() {
|
|
42
69
|
const { dataDir: _userDataDir, debug: _dbg, ...userOpts } = config.pgliteOptions || {}
|
|
70
|
+
const isMain = config.withExtensions
|
|
43
71
|
|
|
44
72
|
db = new PGlite({
|
|
45
73
|
dataDir: config.dataDir,
|
|
46
74
|
debug: config.debug,
|
|
47
75
|
relaxedDurability: true,
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
76
|
+
initialMemory: isMain ? 32 * 1024 * 1024 : 16 * 1024 * 1024,
|
|
77
|
+
...(isMain ? {} : { startParams: ZERO_START_PARAMS }),
|
|
78
|
+
...(isMain
|
|
79
|
+
? {
|
|
80
|
+
startParams: [
|
|
81
|
+
...PGLITE_BASE_FLAGS,
|
|
82
|
+
'-c',
|
|
83
|
+
'shared_buffers=4MB',
|
|
84
|
+
'-c',
|
|
85
|
+
'wal_buffers=1MB',
|
|
86
|
+
],
|
|
87
|
+
...userOpts,
|
|
88
|
+
extensions: userOpts.extensions || {
|
|
89
|
+
vector,
|
|
90
|
+
pg_trgm,
|
|
91
|
+
pgcrypto,
|
|
92
|
+
uuid_ossp,
|
|
93
|
+
citext,
|
|
94
|
+
hstore,
|
|
95
|
+
ltree,
|
|
96
|
+
fuzzystrmatch,
|
|
97
|
+
btree_gin,
|
|
98
|
+
btree_gist,
|
|
99
|
+
cube,
|
|
100
|
+
earthdistance,
|
|
101
|
+
},
|
|
63
102
|
}
|
|
64
|
-
: {},
|
|
103
|
+
: { extensions: {} }),
|
|
65
104
|
} as any)
|
|
66
105
|
|
|
67
106
|
await db.waitReady
|
|
68
107
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
108
|
+
if (isMain) {
|
|
109
|
+
await db.exec(`
|
|
110
|
+
SET work_mem = '4MB';
|
|
111
|
+
SET maintenance_work_mem = '16MB';
|
|
112
|
+
SET effective_cache_size = '64MB';
|
|
113
|
+
SET random_page_cost = 1.1;
|
|
114
|
+
SET jit = off;
|
|
115
|
+
`)
|
|
116
|
+
} else {
|
|
117
|
+
await db.exec(`SET jit = off;`)
|
|
118
|
+
}
|
|
77
119
|
|
|
78
120
|
port.postMessage({ type: 'ready' })
|
|
79
121
|
}
|
package/src/recovery.ts
CHANGED
|
@@ -100,9 +100,9 @@ export async function recoverFromCdcCorruption(ctx: RecoveryContext): Promise<vo
|
|
|
100
100
|
|
|
101
101
|
// clear upstream replication tracking
|
|
102
102
|
const db = instances.postgres
|
|
103
|
-
await db.exec(`TRUNCATE _orez.
|
|
103
|
+
await db.exec(`TRUNCATE _orez._zero_changes`).catch(() => {})
|
|
104
104
|
await db.exec(`TRUNCATE _orez._zero_replication_slots`).catch(() => {})
|
|
105
|
-
await db.exec(`ALTER SEQUENCE _orez.
|
|
105
|
+
await db.exec(`ALTER SEQUENCE _orez._zero_watermark RESTART WITH 1`).catch(() => {})
|
|
106
106
|
|
|
107
107
|
// drop stale shard schemas
|
|
108
108
|
const shardSchemas = await db.query<{ schemaname: string }>(
|
|
@@ -151,7 +151,7 @@ describe('change-tracker', () => {
|
|
|
151
151
|
try {
|
|
152
152
|
await db.exec(`CREATE PUBLICATION "zero_scope"`)
|
|
153
153
|
await installChangeTracking(db) // reinstall picks up publication scope
|
|
154
|
-
await db.exec(`TRUNCATE _orez.
|
|
154
|
+
await db.exec(`TRUNCATE _orez._zero_changes`)
|
|
155
155
|
|
|
156
156
|
await db.exec(`INSERT INTO public.items (name, value) VALUES ('x', 1)`)
|
|
157
157
|
const changes = await getChangesSince(db, 0)
|
|
@@ -17,10 +17,10 @@ export async function installChangeTracking(db: PGlite): Promise<void> {
|
|
|
17
17
|
// create changes table and watermark sequence
|
|
18
18
|
// watermark is the primary key - monotonically increasing, no separate id needed
|
|
19
19
|
await db.exec(`
|
|
20
|
-
CREATE SEQUENCE IF NOT EXISTS _orez.
|
|
20
|
+
CREATE SEQUENCE IF NOT EXISTS _orez._zero_watermark;
|
|
21
21
|
|
|
22
|
-
CREATE TABLE IF NOT EXISTS _orez.
|
|
23
|
-
watermark BIGINT NOT NULL DEFAULT nextval('_orez.
|
|
22
|
+
CREATE TABLE IF NOT EXISTS _orez._zero_changes (
|
|
23
|
+
watermark BIGINT NOT NULL DEFAULT nextval('_orez._zero_watermark') PRIMARY KEY,
|
|
24
24
|
table_name TEXT NOT NULL,
|
|
25
25
|
op TEXT NOT NULL CHECK (op IN ('INSERT', 'UPDATE', 'DELETE')),
|
|
26
26
|
row_data JSONB,
|
|
@@ -47,7 +47,7 @@ export async function installChangeTracking(db: PGlite): Promise<void> {
|
|
|
47
47
|
CREATE OR REPLACE FUNCTION public._zero_track_change() RETURNS TRIGGER AS $$
|
|
48
48
|
BEGIN
|
|
49
49
|
IF TG_OP = 'DELETE' THEN
|
|
50
|
-
INSERT INTO _orez.
|
|
50
|
+
INSERT INTO _orez._zero_changes (table_name, op, old_data)
|
|
51
51
|
VALUES (TG_TABLE_SCHEMA || '.' || TG_TABLE_NAME, 'DELETE', to_jsonb(OLD));
|
|
52
52
|
RETURN OLD;
|
|
53
53
|
ELSIF TG_OP = 'UPDATE' THEN
|
|
@@ -55,11 +55,11 @@ export async function installChangeTracking(db: PGlite): Promise<void> {
|
|
|
55
55
|
IF to_jsonb(NEW) = to_jsonb(OLD) THEN
|
|
56
56
|
RETURN NEW;
|
|
57
57
|
END IF;
|
|
58
|
-
INSERT INTO _orez.
|
|
58
|
+
INSERT INTO _orez._zero_changes (table_name, op, row_data, old_data)
|
|
59
59
|
VALUES (TG_TABLE_SCHEMA || '.' || TG_TABLE_NAME, 'UPDATE', to_jsonb(NEW), to_jsonb(OLD));
|
|
60
60
|
RETURN NEW;
|
|
61
61
|
ELSE
|
|
62
|
-
INSERT INTO _orez.
|
|
62
|
+
INSERT INTO _orez._zero_changes (table_name, op, row_data)
|
|
63
63
|
VALUES (TG_TABLE_SCHEMA || '.' || TG_TABLE_NAME, 'INSERT', to_jsonb(NEW));
|
|
64
64
|
RETURN NEW;
|
|
65
65
|
END IF;
|
|
@@ -228,7 +228,7 @@ export async function getChangesSince(
|
|
|
228
228
|
limit = 50000
|
|
229
229
|
): Promise<ChangeRecord[]> {
|
|
230
230
|
const result = await db.query<ChangeRecord>(
|
|
231
|
-
'SELECT watermark, table_name, op, row_data, old_data FROM _orez.
|
|
231
|
+
'SELECT watermark, table_name, op, row_data, old_data FROM _orez._zero_changes WHERE watermark > $1 ORDER BY watermark LIMIT $2',
|
|
232
232
|
[watermark, limit]
|
|
233
233
|
)
|
|
234
234
|
return result.rows
|
|
@@ -239,14 +239,14 @@ export async function purgeConsumedChanges(
|
|
|
239
239
|
watermark: number
|
|
240
240
|
): Promise<number> {
|
|
241
241
|
const result = await db.exec(
|
|
242
|
-
`DELETE FROM _orez.
|
|
242
|
+
`DELETE FROM _orez._zero_changes WHERE watermark <= ${Number(watermark)}`
|
|
243
243
|
)
|
|
244
244
|
return result[0]?.affectedRows ?? 0
|
|
245
245
|
}
|
|
246
246
|
|
|
247
247
|
export async function getCurrentWatermark(db: PGlite): Promise<number> {
|
|
248
248
|
const result = await db.query<{ last_value: string; is_called: boolean }>(
|
|
249
|
-
'SELECT last_value, is_called FROM _orez.
|
|
249
|
+
'SELECT last_value, is_called FROM _orez._zero_watermark'
|
|
250
250
|
)
|
|
251
251
|
const { last_value, is_called } = result.rows[0]
|
|
252
252
|
if (!is_called) return 0
|
|
@@ -32,6 +32,32 @@ import {
|
|
|
32
32
|
import type { Mutex } from '../mutex.js'
|
|
33
33
|
import type { PGlite } from '@electric-sql/pglite'
|
|
34
34
|
|
|
35
|
+
// types pglite can't replicate — excluded from change tracking columns
|
|
36
|
+
const UNSUPPORTED_TYPES = new Set(['tsvector', 'tsquery', 'USER-DEFINED'])
|
|
37
|
+
|
|
38
|
+
// pg data_type string → wire protocol oid mapping
|
|
39
|
+
const PG_DATA_TYPE_OIDS: Record<string, number> = {
|
|
40
|
+
boolean: 16,
|
|
41
|
+
bytea: 17,
|
|
42
|
+
bigint: 20,
|
|
43
|
+
smallint: 21,
|
|
44
|
+
integer: 23,
|
|
45
|
+
text: 25,
|
|
46
|
+
json: 114,
|
|
47
|
+
real: 700,
|
|
48
|
+
'double precision': 701,
|
|
49
|
+
character: 1042,
|
|
50
|
+
'character varying': 1043,
|
|
51
|
+
date: 1082,
|
|
52
|
+
'time without time zone': 1083,
|
|
53
|
+
'timestamp without time zone': 1114,
|
|
54
|
+
'timestamp with time zone': 1184,
|
|
55
|
+
'time with time zone': 1266,
|
|
56
|
+
numeric: 1700,
|
|
57
|
+
uuid: 2950,
|
|
58
|
+
jsonb: 3802,
|
|
59
|
+
}
|
|
60
|
+
|
|
35
61
|
export interface ReplicationWriter {
|
|
36
62
|
write(data: Uint8Array): void
|
|
37
63
|
readonly closed?: boolean
|
|
@@ -393,7 +419,7 @@ export async function handleStartReplication(
|
|
|
393
419
|
const all = await db.query<{ tablename: string }>(
|
|
394
420
|
`SELECT tablename FROM pg_tables
|
|
395
421
|
WHERE schemaname = 'public'
|
|
396
|
-
AND tablename NOT IN ('migrations', '
|
|
422
|
+
AND tablename NOT IN ('migrations', '_zero_changes')
|
|
397
423
|
AND tablename NOT LIKE '_zero_%'`
|
|
398
424
|
)
|
|
399
425
|
tables = all.rows
|
|
@@ -403,7 +429,7 @@ export async function handleStartReplication(
|
|
|
403
429
|
const ddlParts: string[] = [
|
|
404
430
|
`CREATE OR REPLACE FUNCTION public._zero_notify_change() RETURNS TRIGGER AS $$
|
|
405
431
|
BEGIN
|
|
406
|
-
PERFORM pg_notify('
|
|
432
|
+
PERFORM pg_notify('_zero_changes', TG_TABLE_NAME);
|
|
407
433
|
RETURN NULL;
|
|
408
434
|
END;
|
|
409
435
|
$$ LANGUAGE plpgsql;`,
|
|
@@ -499,28 +525,6 @@ export async function handleStartReplication(
|
|
|
499
525
|
}
|
|
500
526
|
keys.add(row.column_name)
|
|
501
527
|
} else {
|
|
502
|
-
const UNSUPPORTED_TYPES = new Set(['tsvector', 'tsquery', 'USER-DEFINED'])
|
|
503
|
-
const PG_DATA_TYPE_OIDS: Record<string, number> = {
|
|
504
|
-
boolean: 16,
|
|
505
|
-
bytea: 17,
|
|
506
|
-
bigint: 20,
|
|
507
|
-
smallint: 21,
|
|
508
|
-
integer: 23,
|
|
509
|
-
text: 25,
|
|
510
|
-
json: 114,
|
|
511
|
-
real: 700,
|
|
512
|
-
'double precision': 701,
|
|
513
|
-
character: 1042,
|
|
514
|
-
'character varying': 1043,
|
|
515
|
-
date: 1082,
|
|
516
|
-
'time without time zone': 1083,
|
|
517
|
-
'timestamp without time zone': 1114,
|
|
518
|
-
'timestamp with time zone': 1184,
|
|
519
|
-
'time with time zone': 1266,
|
|
520
|
-
numeric: 1700,
|
|
521
|
-
uuid: 2950,
|
|
522
|
-
jsonb: 3802,
|
|
523
|
-
}
|
|
524
528
|
if (row.data_type && UNSUPPORTED_TYPES.has(row.data_type)) {
|
|
525
529
|
let cols = excludedColumns.get(key)
|
|
526
530
|
if (!cols) {
|
|
@@ -612,8 +616,8 @@ export async function handleStartReplication(
|
|
|
612
616
|
// also set up LISTEN as secondary signal
|
|
613
617
|
let unsubscribe: (() => Promise<void>) | null = null
|
|
614
618
|
try {
|
|
615
|
-
unsubscribe = await db.listen('
|
|
616
|
-
log.debug.proxy('replication: listening for
|
|
619
|
+
unsubscribe = await db.listen('_zero_changes', wakeup)
|
|
620
|
+
log.debug.proxy('replication: listening for _zero_changes notifications')
|
|
617
621
|
} catch {
|
|
618
622
|
log.debug.proxy('replication: LISTEN not available')
|
|
619
623
|
}
|
|
@@ -962,6 +966,13 @@ async function streamChanges(
|
|
|
962
966
|
for (const msg of messages) {
|
|
963
967
|
writer.write(msg)
|
|
964
968
|
}
|
|
969
|
+
|
|
970
|
+
// hook for arch instrumentation (soot-arch sq-write events)
|
|
971
|
+
const hook = (globalThis as any).__orez_on_repl_commit
|
|
972
|
+
if (hook) {
|
|
973
|
+
const tables = new Set(changes.map((c) => c.table_name))
|
|
974
|
+
hook({ changes: changes.length, tables: [...tables], txId })
|
|
975
|
+
}
|
|
965
976
|
}
|
|
966
977
|
|
|
967
978
|
function normalizeShardClientsRow(
|
|
@@ -24,7 +24,7 @@ describe('browser build config', () => {
|
|
|
24
24
|
it('includes Node.js polyfills', () => {
|
|
25
25
|
const aliases = getBrowserAliases()
|
|
26
26
|
expect(aliases['node:events']).toBe('events')
|
|
27
|
-
expect(aliases['node:stream']).toBe('stream-
|
|
27
|
+
expect(aliases['node:stream']).toBe('orez/worker/shims/stream-browser')
|
|
28
28
|
expect(aliases['node:path']).toBe('path-browserify')
|
|
29
29
|
})
|
|
30
30
|
|
|
@@ -42,7 +42,10 @@
|
|
|
42
42
|
export function getBrowserAliases(): Record<string, string> {
|
|
43
43
|
return {
|
|
44
44
|
// -- orez shims for zero-cache dependencies --
|
|
45
|
-
postgres
|
|
45
|
+
// postgres-browser uses the real postgres package with MessagePort transport
|
|
46
|
+
// to pg-proxy-browser, matching orez-node's wire protocol architecture.
|
|
47
|
+
// falls back to old PGlite-wrapping shim if postgres-browser isn't available.
|
|
48
|
+
postgres: 'orez/worker/shims/postgres-browser',
|
|
46
49
|
'@rocicorp/zero-sqlite3': 'orez/worker/shims/sqlite',
|
|
47
50
|
fastify: 'orez/worker/shims/fastify',
|
|
48
51
|
ws: 'orez/worker/shims/ws',
|
|
@@ -55,7 +58,7 @@ export function getBrowserAliases(): Record<string, string> {
|
|
|
55
58
|
'node:process': 'process/browser',
|
|
56
59
|
'node:crypto': 'orez/worker/shims/node-stub',
|
|
57
60
|
'crypto-browserify': 'orez/worker/shims/node-stub',
|
|
58
|
-
'node:stream': 'stream-
|
|
61
|
+
'node:stream': 'orez/worker/shims/stream-browser',
|
|
59
62
|
'node:path': 'path-browserify',
|
|
60
63
|
'node:os': 'os-browserify/browser',
|
|
61
64
|
|
|
@@ -171,9 +171,9 @@ export async function startZeroCacheEmbedBrowser(
|
|
|
171
171
|
...((globalThis as any).process.env as Record<string, string>),
|
|
172
172
|
SINGLE_PROCESS: '1',
|
|
173
173
|
NODE_ENV: 'development',
|
|
174
|
-
ZERO_UPSTREAM_DB: 'pglite://
|
|
175
|
-
ZERO_CVR_DB: 'pglite://
|
|
176
|
-
ZERO_CHANGE_DB: 'pglite://
|
|
174
|
+
ZERO_UPSTREAM_DB: 'pglite://localhost/postgres',
|
|
175
|
+
ZERO_CVR_DB: 'pglite://localhost/zero_cvr',
|
|
176
|
+
ZERO_CHANGE_DB: 'pglite://localhost/zero_cdb',
|
|
177
177
|
ZERO_REPLICA_FILE: ':browser-sqlite:',
|
|
178
178
|
ZERO_PORT: '0',
|
|
179
179
|
ZERO_APP_ID: appId,
|
|
@@ -263,40 +263,43 @@ export async function startZeroCacheEmbedBrowser(
|
|
|
263
263
|
},
|
|
264
264
|
|
|
265
265
|
async handleWebSocket(ws: WsLike, url = '/', headers?: Record<string, string>) {
|
|
266
|
-
// lazily resolve
|
|
267
|
-
// ready signal fires, so
|
|
268
|
-
//
|
|
269
|
-
|
|
270
|
-
//
|
|
271
|
-
// check: the dispatcher's server will have 'message' listeners (from
|
|
272
|
-
// installWebSocketHandoff). poll until we find an instance with listeners.
|
|
266
|
+
// lazily resolve fastify instances — ZeroDispatcher is constructed AFTER the
|
|
267
|
+
// ready signal fires, so instances may not all be registered yet.
|
|
268
|
+
// poll until we have instances with ws routes (tryHandoff will check).
|
|
269
|
+
let instances: any[] = []
|
|
273
270
|
for (let i = 0; i < 100; i++) {
|
|
271
|
+
instances = (globalThis as any).__orez_fastify_instances || []
|
|
274
272
|
fastifyInstance = (globalThis as any).__orez_fastify_instance
|
|
275
|
-
//
|
|
276
|
-
//
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
if (fastifyInstance?.server?.listenerCount?.('message') >= 2) break
|
|
273
|
+
// wait until we have at least one instance with 2+ message listeners
|
|
274
|
+
// (the dispatcher's instance has both the shim handler + installWebSocketHandoff)
|
|
275
|
+
if (instances.some((inst: any) => inst?.server?.listenerCount?.('message') >= 2))
|
|
276
|
+
break
|
|
280
277
|
await new Promise((r) => setTimeout(r, 50))
|
|
281
278
|
}
|
|
282
|
-
if (!isReady
|
|
283
|
-
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
279
|
+
if (!isReady) return
|
|
280
|
+
|
|
281
|
+
const handoffMsg = {
|
|
282
|
+
message: {
|
|
283
|
+
url,
|
|
284
|
+
headers: headers || {},
|
|
285
|
+
method: 'GET',
|
|
286
|
+
},
|
|
287
|
+
head: new Uint8Array(0),
|
|
288
288
|
}
|
|
289
289
|
|
|
290
|
-
//
|
|
290
|
+
// try all fastify instances via tryHandoff, stop at first match
|
|
291
|
+
let handled = false
|
|
292
|
+
for (const inst of instances) {
|
|
293
|
+
if (inst?.tryHandoff?.(handoffMsg, ws)) {
|
|
294
|
+
handled = true
|
|
295
|
+
break
|
|
296
|
+
}
|
|
297
|
+
}
|
|
291
298
|
|
|
292
|
-
//
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
'message',
|
|
297
|
-
['handoff', { message, head: new Uint8Array(0) }],
|
|
298
|
-
ws // the WebSocket-like object as sendHandle
|
|
299
|
-
)
|
|
299
|
+
// fallback: emit directly on the last instance's server
|
|
300
|
+
if (!handled && fastifyInstance?.server) {
|
|
301
|
+
fastifyInstance.server.emit('message', ['handoff', handoffMsg], ws)
|
|
302
|
+
}
|
|
300
303
|
},
|
|
301
304
|
|
|
302
305
|
async handleHttp(request: HttpRequest): Promise<HttpResponse> {
|
|
@@ -124,32 +124,41 @@ class FastifyShim {
|
|
|
124
124
|
// and call the handler with the socket.
|
|
125
125
|
#installWsHandoffHandler() {
|
|
126
126
|
this.server.onMessageType('handoff', (msg: any, socket?: any) => {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
148
|
-
)
|
|
149
|
-
return
|
|
127
|
+
this.tryHandoff(msg, socket)
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// try to match a handoff message against registered websocket routes.
|
|
132
|
+
// returns true if a route matched, false otherwise.
|
|
133
|
+
// this is public so callers (ws shim, browser-embed) can iterate
|
|
134
|
+
// all fastify instances and stop at the first match.
|
|
135
|
+
tryHandoff(msg: any, socket?: any): boolean {
|
|
136
|
+
if (!socket || !msg?.message?.url) return false
|
|
137
|
+
const url = msg.message.url
|
|
138
|
+
const parsedUrl = new URL(url, 'http://localhost')
|
|
139
|
+
const pathname = parsedUrl.pathname
|
|
140
|
+
|
|
141
|
+
for (const route of this.#wsRoutes) {
|
|
142
|
+
if (route.pattern.test(pathname)) {
|
|
143
|
+
const req = {
|
|
144
|
+
url,
|
|
145
|
+
headers: msg.message.headers || {},
|
|
146
|
+
method: msg.message.method || 'GET',
|
|
150
147
|
}
|
|
148
|
+
// wrap socket through handleUpgrade so it gets the full WS API
|
|
149
|
+
// (ping, on, once, terminate, etc.) needed by zero-cache's streamOut
|
|
150
|
+
this.websocketServer.handleUpgrade(
|
|
151
|
+
req,
|
|
152
|
+
socket,
|
|
153
|
+
Buffer.from(new Uint8Array(0)),
|
|
154
|
+
(ws: any) => {
|
|
155
|
+
route.handler(ws, req)
|
|
156
|
+
}
|
|
157
|
+
)
|
|
158
|
+
return true
|
|
151
159
|
}
|
|
152
|
-
}
|
|
160
|
+
}
|
|
161
|
+
return false
|
|
153
162
|
}
|
|
154
163
|
|
|
155
164
|
// route registration — supports optional { websocket: true } option
|
|
@@ -286,6 +295,10 @@ function Fastify(_opts?: unknown): FastifyShim {
|
|
|
286
295
|
// always overwrite — the ZeroDispatcher (which has the WS handoff routes)
|
|
287
296
|
// is created LAST, so the final instance is the one handleWebSocket needs.
|
|
288
297
|
;(globalThis as any).__orez_fastify_instance = instance
|
|
298
|
+
// track all instances so callers can try handoff against each one
|
|
299
|
+
;(globalThis as any).__orez_fastify_instances =
|
|
300
|
+
(globalThis as any).__orez_fastify_instances || []
|
|
301
|
+
;(globalThis as any).__orez_fastify_instances.push(instance)
|
|
289
302
|
return instance
|
|
290
303
|
}
|
|
291
304
|
|