orez 0.0.48 → 0.0.49
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/cli.d.ts.map +1 -1
- package/dist/cli.js +6 -112
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +0 -5
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +0 -5
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +0 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +91 -280
- package/dist/index.js.map +1 -1
- package/dist/log.d.ts +0 -9
- package/dist/log.d.ts.map +1 -1
- package/dist/log.js +1 -24
- package/dist/log.js.map +1 -1
- package/dist/mutex.d.ts.map +1 -1
- package/dist/mutex.js +2 -13
- package/dist/mutex.js.map +1 -1
- package/dist/pg-proxy.d.ts +2 -3
- package/dist/pg-proxy.d.ts.map +1 -1
- package/dist/pg-proxy.js +167 -377
- package/dist/pg-proxy.js.map +1 -1
- package/dist/pglite-manager.d.ts +0 -1
- package/dist/pglite-manager.d.ts.map +1 -1
- package/dist/pglite-manager.js +2 -8
- package/dist/pglite-manager.js.map +1 -1
- package/dist/replication/change-tracker.d.ts +0 -6
- package/dist/replication/change-tracker.d.ts.map +1 -1
- package/dist/replication/change-tracker.js +1 -62
- package/dist/replication/change-tracker.js.map +1 -1
- package/dist/replication/handler.d.ts.map +1 -1
- package/dist/replication/handler.js +7 -66
- package/dist/replication/handler.js.map +1 -1
- package/dist/vite-plugin.d.ts +0 -3
- package/dist/vite-plugin.d.ts.map +1 -1
- package/dist/vite-plugin.js +0 -24
- package/dist/vite-plugin.js.map +1 -1
- package/package.json +5 -4
- package/src/cli.ts +18 -124
- package/src/config.ts +0 -10
- package/src/index.ts +92 -309
- package/src/integration/integration.test.ts +264 -133
- package/src/log.ts +1 -25
- package/src/mutex.ts +2 -12
- package/src/pg-proxy.ts +187 -451
- package/src/pglite-manager.ts +2 -9
- package/src/replication/change-tracker.ts +1 -83
- package/src/replication/handler.ts +6 -79
- package/src/replication/pgoutput-encoder.test.ts +0 -217
- package/src/replication/zero-compat.test.ts +1 -232
- package/src/shim/hooks.mjs +1 -1
- package/src/vite-plugin.ts +0 -28
- package/src/wasm-sqlite.test.ts +1 -2
|
@@ -1,82 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* integration test
|
|
2
|
+
* integration test adapted from zero-cache's integration.pg.test.ts
|
|
3
3
|
*
|
|
4
|
-
* validates: pglite → change tracking → replication
|
|
5
|
-
* zero-cache → websocket poke messages to clients.
|
|
4
|
+
* validates the full sync pipeline: pglite → change tracking → replication
|
|
5
|
+
* protocol → zero-cache → websocket poke messages to clients.
|
|
6
6
|
*
|
|
7
|
-
* uses orez's startZeroLite()
|
|
8
|
-
* before zero-cache starts its initial sync. deploys ANYONE_CAN
|
|
9
|
-
* permissions after zero-cache creates its schema tables.
|
|
7
|
+
* uses orez's startZeroLite() instead of real postgres + manual zero-cache.
|
|
10
8
|
*/
|
|
11
9
|
|
|
12
|
-
import { describe, expect, test, beforeAll, afterAll } from 'vitest'
|
|
10
|
+
import { describe, expect, test, beforeAll, afterAll, beforeEach } from 'vitest'
|
|
13
11
|
import WebSocket from 'ws'
|
|
14
12
|
|
|
15
13
|
import { startZeroLite } from '../index.js'
|
|
16
14
|
|
|
17
15
|
import type { PGlite } from '@electric-sql/pglite'
|
|
18
16
|
|
|
19
|
-
// encode initConnectionMessage + authToken for sec-websocket-protocol header
|
|
20
|
-
// mirrors @rocicorp/zero's encodeSecProtocols
|
|
21
|
-
function encodeSecProtocol(
|
|
22
|
-
initConnectionMessage: unknown,
|
|
23
|
-
authToken: string = ''
|
|
24
|
-
): string {
|
|
25
|
-
const protocols = { initConnectionMessage, authToken }
|
|
26
|
-
const bytes = new TextEncoder().encode(JSON.stringify(protocols))
|
|
27
|
-
const s = Array.from(bytes, (byte: number) => String.fromCharCode(byte)).join('')
|
|
28
|
-
return encodeURIComponent(btoa(s))
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// zero v0.25 requires clientSchema for new client groups
|
|
32
|
-
const clientSchema = {
|
|
33
|
-
tables: {
|
|
34
|
-
foo: {
|
|
35
|
-
columns: {
|
|
36
|
-
id: { type: 'string' },
|
|
37
|
-
value: { type: 'string' },
|
|
38
|
-
num: { type: 'number' },
|
|
39
|
-
},
|
|
40
|
-
primaryKey: ['id'],
|
|
41
|
-
},
|
|
42
|
-
bar: {
|
|
43
|
-
columns: {
|
|
44
|
-
id: { type: 'string' },
|
|
45
|
-
foo_id: { type: 'string' },
|
|
46
|
-
},
|
|
47
|
-
primaryKey: ['id'],
|
|
48
|
-
},
|
|
49
|
-
},
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// ANYONE_CAN permissions — empty AND condition = always true
|
|
53
|
-
const anyoneCanPermissions = JSON.stringify({
|
|
54
|
-
tables: {
|
|
55
|
-
foo: {
|
|
56
|
-
row: {
|
|
57
|
-
select: [['allow', { type: 'and', conditions: [] }]],
|
|
58
|
-
insert: [['allow', { type: 'and', conditions: [] }]],
|
|
59
|
-
update: {
|
|
60
|
-
preMutation: [['allow', { type: 'and', conditions: [] }]],
|
|
61
|
-
postMutation: [['allow', { type: 'and', conditions: [] }]],
|
|
62
|
-
},
|
|
63
|
-
delete: [['allow', { type: 'and', conditions: [] }]],
|
|
64
|
-
},
|
|
65
|
-
},
|
|
66
|
-
bar: {
|
|
67
|
-
row: {
|
|
68
|
-
select: [['allow', { type: 'and', conditions: [] }]],
|
|
69
|
-
insert: [['allow', { type: 'and', conditions: [] }]],
|
|
70
|
-
update: {
|
|
71
|
-
preMutation: [['allow', { type: 'and', conditions: [] }]],
|
|
72
|
-
postMutation: [['allow', { type: 'and', conditions: [] }]],
|
|
73
|
-
},
|
|
74
|
-
delete: [['allow', { type: 'and', conditions: [] }]],
|
|
75
|
-
},
|
|
76
|
-
},
|
|
77
|
-
},
|
|
78
|
-
})
|
|
79
|
-
|
|
80
17
|
// simple async queue for collecting websocket messages
|
|
81
18
|
class Queue<T> {
|
|
82
19
|
private items: T[] = []
|
|
@@ -118,61 +55,49 @@ class Queue<T> {
|
|
|
118
55
|
describe('orez integration', { timeout: 120000 }, () => {
|
|
119
56
|
let db: PGlite
|
|
120
57
|
let zeroPort: number
|
|
58
|
+
let pgPort: number
|
|
121
59
|
let shutdown: () => Promise<void>
|
|
122
60
|
let dataDir: string
|
|
123
61
|
|
|
124
62
|
beforeAll(async () => {
|
|
125
63
|
const testPgPort = 23000 + Math.floor(Math.random() * 1000)
|
|
126
|
-
const testZeroPort = testPgPort +
|
|
64
|
+
const testZeroPort = testPgPort + 100
|
|
127
65
|
|
|
128
66
|
dataDir = `.orez-integration-test-${Date.now()}`
|
|
67
|
+
console.log(`[test] starting orez on pg:${testPgPort} zero:${testZeroPort}`)
|
|
129
68
|
const result = await startZeroLite({
|
|
130
69
|
pgPort: testPgPort,
|
|
131
70
|
zeroPort: testZeroPort,
|
|
132
71
|
dataDir,
|
|
133
72
|
logLevel: 'info',
|
|
134
73
|
skipZeroCache: false,
|
|
135
|
-
beforeZero: async (pglite) => {
|
|
136
|
-
await pglite.exec(`
|
|
137
|
-
CREATE TABLE IF NOT EXISTS foo (
|
|
138
|
-
id TEXT PRIMARY KEY,
|
|
139
|
-
value TEXT,
|
|
140
|
-
num INTEGER
|
|
141
|
-
);
|
|
142
|
-
CREATE TABLE IF NOT EXISTS bar (
|
|
143
|
-
id TEXT PRIMARY KEY,
|
|
144
|
-
foo_id TEXT
|
|
145
|
-
);
|
|
146
|
-
`)
|
|
147
|
-
// insert test data before zero-cache starts so initial sync includes it
|
|
148
|
-
await pglite.query(`INSERT INTO foo (id, value, num) VALUES ($1, $2, $3)`, [
|
|
149
|
-
'seed1',
|
|
150
|
-
'hello',
|
|
151
|
-
42,
|
|
152
|
-
])
|
|
153
|
-
await pglite.query(`INSERT INTO foo (id, value, num) VALUES ($1, $2, $3)`, [
|
|
154
|
-
'seed2',
|
|
155
|
-
'world',
|
|
156
|
-
99,
|
|
157
|
-
])
|
|
158
|
-
},
|
|
159
74
|
})
|
|
160
75
|
|
|
161
76
|
db = result.db
|
|
162
77
|
zeroPort = result.zeroPort
|
|
78
|
+
pgPort = result.pgPort
|
|
163
79
|
shutdown = result.stop
|
|
164
80
|
|
|
165
|
-
|
|
81
|
+
console.log(`[test] orez started, creating tables`)
|
|
166
82
|
|
|
167
|
-
//
|
|
168
|
-
await db.
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
83
|
+
// create test tables
|
|
84
|
+
await db.exec(`
|
|
85
|
+
CREATE TABLE IF NOT EXISTS foo (
|
|
86
|
+
id TEXT PRIMARY KEY,
|
|
87
|
+
value TEXT,
|
|
88
|
+
num INTEGER
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
CREATE TABLE IF NOT EXISTS bar (
|
|
92
|
+
id TEXT PRIMARY KEY,
|
|
93
|
+
foo_id TEXT
|
|
94
|
+
);
|
|
95
|
+
`)
|
|
96
|
+
|
|
97
|
+
console.log(`[test] tables created, waiting for zero-cache`)
|
|
98
|
+
// wait for zero-cache to be ready
|
|
99
|
+
await waitForZero(zeroPort, 90000)
|
|
100
|
+
console.log(`[test] zero-cache ready`)
|
|
176
101
|
}, 120000)
|
|
177
102
|
|
|
178
103
|
afterAll(async () => {
|
|
@@ -185,27 +110,96 @@ describe('orez integration', { timeout: 120000 }, () => {
|
|
|
185
110
|
}
|
|
186
111
|
})
|
|
187
112
|
|
|
113
|
+
beforeEach(async () => {
|
|
114
|
+
// clean tables between tests
|
|
115
|
+
await db.exec(`DELETE FROM foo; DELETE FROM bar;`)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
test('zero-cache starts and accepts websocket connections', async () => {
|
|
119
|
+
const ws = new WebSocket(
|
|
120
|
+
`ws://localhost:${zeroPort}/sync/v4/connect` +
|
|
121
|
+
`?clientGroupID=test-cg&clientID=test-client&wsid=ws1&schemaVersion=1&baseCookie=&ts=${Date.now()}&lmid=0`
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
const connected = new Promise<void>((resolve, reject) => {
|
|
125
|
+
ws.on('open', resolve)
|
|
126
|
+
ws.on('error', reject)
|
|
127
|
+
setTimeout(() => reject(new Error('ws connect timeout')), 5000)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
await connected
|
|
131
|
+
|
|
132
|
+
const firstMessage = await new Promise<unknown>((resolve) => {
|
|
133
|
+
ws.on('message', (data) => {
|
|
134
|
+
resolve(JSON.parse(data.toString()))
|
|
135
|
+
})
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
expect(firstMessage).toMatchObject(['connected', { wsid: 'ws1' }])
|
|
139
|
+
|
|
140
|
+
ws.close()
|
|
141
|
+
})
|
|
142
|
+
|
|
188
143
|
test('initial sync delivers existing rows via poke', async () => {
|
|
144
|
+
// insert data before connecting
|
|
145
|
+
await db.query(`INSERT INTO foo (id, value, num) VALUES ($1, $2, $3)`, [
|
|
146
|
+
'row1',
|
|
147
|
+
'hello',
|
|
148
|
+
42,
|
|
149
|
+
])
|
|
150
|
+
|
|
189
151
|
const downstream = new Queue<unknown>()
|
|
190
152
|
const ws = connectAndSubscribe(zeroPort, downstream, {
|
|
191
153
|
table: 'foo',
|
|
192
154
|
orderBy: [['id', 'asc']],
|
|
193
155
|
})
|
|
194
156
|
|
|
195
|
-
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
.
|
|
199
|
-
|
|
157
|
+
// drain until we get a pokePart with rowsPatch containing our data
|
|
158
|
+
const poke = await waitForPokePart(downstream, 30000)
|
|
159
|
+
expect(poke.rowsPatch).toEqual(
|
|
160
|
+
expect.arrayContaining([
|
|
161
|
+
expect.objectContaining({
|
|
162
|
+
op: 'put',
|
|
163
|
+
tableName: 'foo',
|
|
164
|
+
value: expect.objectContaining({
|
|
165
|
+
id: 'row1',
|
|
166
|
+
value: 'hello',
|
|
167
|
+
}),
|
|
168
|
+
}),
|
|
169
|
+
])
|
|
170
|
+
)
|
|
200
171
|
|
|
201
|
-
|
|
202
|
-
|
|
172
|
+
ws.close()
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
test('live replication: insert triggers poke', async () => {
|
|
176
|
+
const downstream = new Queue<unknown>()
|
|
177
|
+
const ws = connectAndSubscribe(zeroPort, downstream, {
|
|
178
|
+
table: 'foo',
|
|
179
|
+
orderBy: [['id', 'asc']],
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
// drain initial connection + sync pokes
|
|
183
|
+
await drainInitialPokes(downstream)
|
|
184
|
+
|
|
185
|
+
// now insert data - this should trigger a replication poke
|
|
186
|
+
await db.query(`INSERT INTO foo (id, value, num) VALUES ($1, $2, $3)`, [
|
|
187
|
+
'live-row',
|
|
188
|
+
'live-value',
|
|
189
|
+
99,
|
|
190
|
+
])
|
|
191
|
+
|
|
192
|
+
// wait for the replication poke
|
|
193
|
+
const poke = await waitForPokePart(downstream, 30000)
|
|
203
194
|
expect(poke.rowsPatch).toEqual(
|
|
204
195
|
expect.arrayContaining([
|
|
205
196
|
expect.objectContaining({
|
|
206
197
|
op: 'put',
|
|
207
198
|
tableName: 'foo',
|
|
208
|
-
value: expect.objectContaining({
|
|
199
|
+
value: expect.objectContaining({
|
|
200
|
+
id: 'live-row',
|
|
201
|
+
value: 'live-value',
|
|
202
|
+
}),
|
|
209
203
|
}),
|
|
210
204
|
])
|
|
211
205
|
)
|
|
@@ -213,57 +207,165 @@ describe('orez integration', { timeout: 120000 }, () => {
|
|
|
213
207
|
ws.close()
|
|
214
208
|
})
|
|
215
209
|
|
|
216
|
-
test('
|
|
210
|
+
test('live replication: update triggers poke', async () => {
|
|
211
|
+
// insert initial data
|
|
212
|
+
await db.query(`INSERT INTO foo (id, value, num) VALUES ($1, $2, $3)`, [
|
|
213
|
+
'upd-row',
|
|
214
|
+
'original',
|
|
215
|
+
1,
|
|
216
|
+
])
|
|
217
|
+
|
|
217
218
|
const downstream = new Queue<unknown>()
|
|
218
219
|
const ws = connectAndSubscribe(zeroPort, downstream, {
|
|
219
220
|
table: 'foo',
|
|
220
221
|
orderBy: [['id', 'asc']],
|
|
221
222
|
})
|
|
222
223
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
224
|
+
await drainInitialPokes(downstream)
|
|
225
|
+
|
|
226
|
+
// update the row
|
|
227
|
+
await db.query(`UPDATE foo SET value = $1, num = $2 WHERE id = $3`, [
|
|
228
|
+
'updated',
|
|
229
|
+
2,
|
|
230
|
+
'upd-row',
|
|
231
|
+
])
|
|
232
|
+
|
|
233
|
+
const poke = await waitForPokePart(downstream, 30000)
|
|
234
|
+
expect(poke.rowsPatch).toEqual(
|
|
235
|
+
expect.arrayContaining([
|
|
236
|
+
expect.objectContaining({
|
|
237
|
+
op: 'put',
|
|
238
|
+
tableName: 'foo',
|
|
239
|
+
value: expect.objectContaining({
|
|
240
|
+
id: 'upd-row',
|
|
241
|
+
value: 'updated',
|
|
242
|
+
}),
|
|
243
|
+
}),
|
|
244
|
+
])
|
|
226
245
|
)
|
|
227
246
|
|
|
228
|
-
|
|
229
|
-
|
|
247
|
+
ws.close()
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
test('live replication: delete triggers poke', async () => {
|
|
251
|
+
await db.query(`INSERT INTO foo (id, value, num) VALUES ($1, $2, $3)`, [
|
|
252
|
+
'del-row',
|
|
253
|
+
'to-delete',
|
|
254
|
+
1,
|
|
255
|
+
])
|
|
256
|
+
|
|
257
|
+
const downstream = new Queue<unknown>()
|
|
258
|
+
const ws = connectAndSubscribe(zeroPort, downstream, {
|
|
259
|
+
table: 'foo',
|
|
260
|
+
orderBy: [['id', 'asc']],
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
await drainInitialPokes(downstream)
|
|
264
|
+
|
|
265
|
+
// delete the row
|
|
266
|
+
await db.query(`DELETE FROM foo WHERE id = $1`, ['del-row'])
|
|
267
|
+
|
|
268
|
+
const poke = await waitForPokePart(downstream, 30000)
|
|
269
|
+
expect(poke.rowsPatch).toEqual(
|
|
270
|
+
expect.arrayContaining([
|
|
271
|
+
expect.objectContaining({
|
|
272
|
+
op: 'del',
|
|
273
|
+
tableName: 'foo',
|
|
274
|
+
}),
|
|
275
|
+
])
|
|
276
|
+
)
|
|
230
277
|
|
|
231
278
|
ws.close()
|
|
232
279
|
})
|
|
233
280
|
|
|
234
|
-
|
|
281
|
+
test('concurrent inserts all replicate', async () => {
|
|
282
|
+
const downstream = new Queue<unknown>()
|
|
283
|
+
const ws = connectAndSubscribe(zeroPort, downstream, {
|
|
284
|
+
table: 'foo',
|
|
285
|
+
orderBy: [['id', 'asc']],
|
|
286
|
+
})
|
|
235
287
|
|
|
236
|
-
|
|
288
|
+
await drainInitialPokes(downstream)
|
|
289
|
+
|
|
290
|
+
// insert 5 rows concurrently
|
|
291
|
+
await Promise.all(
|
|
292
|
+
Array.from({ length: 5 }, (_, i) =>
|
|
293
|
+
db.query(`INSERT INTO foo (id, value, num) VALUES ($1, $2, $3)`, [
|
|
294
|
+
`concurrent-${i}`,
|
|
295
|
+
`value-${i}`,
|
|
296
|
+
i,
|
|
297
|
+
])
|
|
298
|
+
)
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
// collect all poke parts within a window
|
|
302
|
+
const allRows = await collectPokeRows(downstream, 30000)
|
|
303
|
+
const ids = allRows
|
|
304
|
+
.filter((r: any) => r.op === 'put' && r.tableName === 'foo')
|
|
305
|
+
.map((r: any) => r.value.id)
|
|
306
|
+
.sort()
|
|
307
|
+
|
|
308
|
+
expect(ids).toEqual([
|
|
309
|
+
'concurrent-0',
|
|
310
|
+
'concurrent-1',
|
|
311
|
+
'concurrent-2',
|
|
312
|
+
'concurrent-3',
|
|
313
|
+
'concurrent-4',
|
|
314
|
+
])
|
|
315
|
+
|
|
316
|
+
ws.close()
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
// --- helpers ---
|
|
237
320
|
|
|
238
321
|
function connectAndSubscribe(
|
|
239
322
|
port: number,
|
|
240
323
|
downstream: Queue<unknown>,
|
|
241
324
|
query: Record<string, unknown>
|
|
242
325
|
): WebSocket {
|
|
243
|
-
const cid = `test-${++clientCounter}-${Date.now()}`
|
|
244
|
-
const wsid = `ws-${clientCounter}-${Date.now()}`
|
|
245
|
-
const initMsg = [
|
|
246
|
-
'initConnection',
|
|
247
|
-
{
|
|
248
|
-
desiredQueriesPatch: [{ op: 'put', hash: 'q1', ast: query }],
|
|
249
|
-
clientSchema,
|
|
250
|
-
},
|
|
251
|
-
]
|
|
252
|
-
const secProtocol = encodeSecProtocol(initMsg)
|
|
253
|
-
|
|
254
326
|
const ws = new WebSocket(
|
|
255
|
-
`ws://localhost:${port}/sync/
|
|
256
|
-
`?clientGroupID
|
|
257
|
-
[secProtocol]
|
|
327
|
+
`ws://localhost:${port}/sync/v4/connect` +
|
|
328
|
+
`?clientGroupID=test-cg-${Date.now()}&clientID=test-client&wsid=ws1&schemaVersion=1&baseCookie=&ts=${Date.now()}&lmid=0`
|
|
258
329
|
)
|
|
259
330
|
|
|
260
331
|
ws.on('message', (data) => {
|
|
261
332
|
downstream.enqueue(JSON.parse(data.toString()))
|
|
262
333
|
})
|
|
263
334
|
|
|
335
|
+
ws.on('open', () => {
|
|
336
|
+
ws.send(
|
|
337
|
+
JSON.stringify([
|
|
338
|
+
'initConnection',
|
|
339
|
+
{
|
|
340
|
+
desiredQueriesPatch: [{ op: 'put', hash: 'q1', ast: query }],
|
|
341
|
+
},
|
|
342
|
+
])
|
|
343
|
+
)
|
|
344
|
+
})
|
|
345
|
+
|
|
264
346
|
return ws
|
|
265
347
|
}
|
|
266
348
|
|
|
349
|
+
async function drainInitialPokes(downstream: Queue<unknown>) {
|
|
350
|
+
// drain messages until we've seen the initial data sync complete
|
|
351
|
+
// pattern: connected → pokeStart/End → pokeStart/pokePart(queries)/pokeEnd → pokeStart/pokePart(data)/pokeEnd
|
|
352
|
+
let settled = false
|
|
353
|
+
const timeout = Date.now() + 30000
|
|
354
|
+
|
|
355
|
+
while (!settled && Date.now() < timeout) {
|
|
356
|
+
const msg = (await downstream.dequeue('timeout' as any, 3000)) as any
|
|
357
|
+
if (msg === 'timeout') {
|
|
358
|
+
settled = true
|
|
359
|
+
} else if (Array.isArray(msg) && msg[0] === 'pokeEnd') {
|
|
360
|
+
// after a pokeEnd, check if another poke comes quickly
|
|
361
|
+
const next = (await downstream.dequeue('timeout' as any, 2000)) as any
|
|
362
|
+
if (next === 'timeout') {
|
|
363
|
+
settled = true
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
267
369
|
async function waitForPokePart(
|
|
268
370
|
downstream: Queue<unknown>,
|
|
269
371
|
timeoutMs = 10000
|
|
@@ -279,6 +381,35 @@ describe('orez integration', { timeout: 120000 }, () => {
|
|
|
279
381
|
}
|
|
280
382
|
throw new Error('timed out waiting for pokePart')
|
|
281
383
|
}
|
|
384
|
+
|
|
385
|
+
async function collectPokeRows(
|
|
386
|
+
downstream: Queue<unknown>,
|
|
387
|
+
windowMs = 5000
|
|
388
|
+
): Promise<any[]> {
|
|
389
|
+
const rows: any[] = []
|
|
390
|
+
const deadline = Date.now() + windowMs
|
|
391
|
+
// first wait for the pokePart with data
|
|
392
|
+
while (Date.now() < deadline) {
|
|
393
|
+
const remaining = Math.max(1000, deadline - Date.now())
|
|
394
|
+
const msg = (await downstream.dequeue('timeout' as any, remaining)) as any
|
|
395
|
+
if (msg === 'timeout') break
|
|
396
|
+
if (Array.isArray(msg) && msg[0] === 'pokePart' && msg[1]?.rowsPatch) {
|
|
397
|
+
rows.push(...msg[1].rowsPatch)
|
|
398
|
+
// check if more poke parts come quickly
|
|
399
|
+
const more = (await downstream.dequeue('timeout' as any, 2000)) as any
|
|
400
|
+
if (
|
|
401
|
+
more !== 'timeout' &&
|
|
402
|
+
Array.isArray(more) &&
|
|
403
|
+
more[0] === 'pokePart' &&
|
|
404
|
+
more[1]?.rowsPatch
|
|
405
|
+
) {
|
|
406
|
+
rows.push(...more[1].rowsPatch)
|
|
407
|
+
}
|
|
408
|
+
break
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
return rows
|
|
412
|
+
}
|
|
282
413
|
})
|
|
283
414
|
|
|
284
415
|
async function waitForZero(port: number, timeoutMs = 30000) {
|
package/src/log.ts
CHANGED
|
@@ -25,17 +25,6 @@ export function setLogLevel(level: LogLevel) {
|
|
|
25
25
|
currentLevel = level
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
type LogListener = (source: string, level: LogLevel, msg: string) => void
|
|
29
|
-
const listeners: LogListener[] = []
|
|
30
|
-
|
|
31
|
-
export function addLogListener(fn: LogListener) {
|
|
32
|
-
listeners.push(fn)
|
|
33
|
-
return () => {
|
|
34
|
-
const idx = listeners.indexOf(fn)
|
|
35
|
-
if (idx !== -1) listeners.splice(idx, 1)
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
28
|
function prefix(label: string, color: string): string {
|
|
40
29
|
return `${BOLD}${color}[${label}]${RESET}`
|
|
41
30
|
}
|
|
@@ -48,12 +37,6 @@ export function port(n: number, color: keyof typeof COLORS): string {
|
|
|
48
37
|
function makeLogger(label: string, color: string, level: LogLevel = 'info') {
|
|
49
38
|
const p = prefix(label, color)
|
|
50
39
|
return (...args: unknown[]) => {
|
|
51
|
-
// always notify listeners (they capture everything for admin ui)
|
|
52
|
-
if (listeners.length > 0) {
|
|
53
|
-
const msg = args.map((a) => (typeof a === 'string' ? a : String(a))).join(' ')
|
|
54
|
-
for (const fn of listeners) fn(label, level, msg)
|
|
55
|
-
}
|
|
56
|
-
// only print to terminal if level passes filter
|
|
57
40
|
if (LEVEL_PRIORITY[level] <= LEVEL_PRIORITY[currentLevel]) {
|
|
58
41
|
console.info(p, ...args)
|
|
59
42
|
}
|
|
@@ -64,15 +47,8 @@ export const log = {
|
|
|
64
47
|
orez: makeLogger('orez', COLORS.cyan, 'warn'),
|
|
65
48
|
pglite: makeLogger('pglite', COLORS.green, 'warn'),
|
|
66
49
|
proxy: makeLogger('pg-proxy', COLORS.yellow, 'warn'),
|
|
67
|
-
zero: makeLogger('zero', COLORS.magenta, 'warn'),
|
|
50
|
+
zero: makeLogger('zero-cache', COLORS.magenta, 'warn'),
|
|
68
51
|
s3: makeLogger('orez/s3', COLORS.blue, 'warn'),
|
|
69
|
-
info: {
|
|
70
|
-
orez: makeLogger('orez', COLORS.cyan, 'info'),
|
|
71
|
-
pglite: makeLogger('pglite', COLORS.green, 'info'),
|
|
72
|
-
proxy: makeLogger('pg-proxy', COLORS.yellow, 'info'),
|
|
73
|
-
zero: makeLogger('zero', COLORS.magenta, 'info'),
|
|
74
|
-
s3: makeLogger('orez/s3', COLORS.blue, 'info'),
|
|
75
|
-
},
|
|
76
52
|
debug: {
|
|
77
53
|
orez: makeLogger('orez', COLORS.cyan, 'debug'),
|
|
78
54
|
pglite: makeLogger('pglite', COLORS.green, 'debug'),
|
package/src/mutex.ts
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
// simple mutex for serializing pglite access
|
|
2
|
-
// uses setImmediate/setTimeout between releases to prevent event loop
|
|
3
|
-
// starvation when multiple connections queue up — without this, releasing
|
|
4
|
-
// the mutex resolves the next waiter as a microtask, which causes a chain
|
|
5
|
-
// of synchronous pglite executions that blocks all I/O processing.
|
|
1
|
+
// simple mutex for serializing pglite access
|
|
6
2
|
export class Mutex {
|
|
7
3
|
private locked = false
|
|
8
4
|
private queue: Array<() => void> = []
|
|
@@ -20,13 +16,7 @@ export class Mutex {
|
|
|
20
16
|
release(): void {
|
|
21
17
|
const next = this.queue.shift()
|
|
22
18
|
if (next) {
|
|
23
|
-
|
|
24
|
-
// before the next waiter acquires the mutex
|
|
25
|
-
if (typeof setImmediate !== 'undefined') {
|
|
26
|
-
setImmediate(next)
|
|
27
|
-
} else {
|
|
28
|
-
setTimeout(next, 0)
|
|
29
|
-
}
|
|
19
|
+
next()
|
|
30
20
|
} else {
|
|
31
21
|
this.locked = false
|
|
32
22
|
}
|