orez 0.1.6 → 0.1.7
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/README.md +185 -225
- package/dist/admin/log-store.d.ts.map +1 -1
- package/dist/admin/log-store.js +17 -6
- package/dist/admin/log-store.js.map +1 -1
- package/dist/admin/server.d.ts +1 -0
- package/dist/admin/server.d.ts.map +1 -1
- package/dist/admin/server.js +10 -0
- package/dist/admin/server.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +89 -45
- 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 +104 -17
- package/dist/index.js.map +1 -1
- package/dist/integration/test-permissions.d.ts +5 -0
- package/dist/integration/test-permissions.d.ts.map +1 -0
- package/dist/integration/test-permissions.js +89 -0
- package/dist/integration/test-permissions.js.map +1 -0
- package/dist/pg-proxy.js +2 -2
- package/dist/pg-proxy.js.map +1 -1
- package/dist/replication/change-tracker.d.ts.map +1 -1
- package/dist/replication/change-tracker.js +15 -13
- package/dist/replication/change-tracker.js.map +1 -1
- package/dist/replication/handler.d.ts.map +1 -1
- package/dist/replication/handler.js +27 -2
- package/dist/replication/handler.js.map +1 -1
- package/dist/sqlite-mode/index.d.ts +1 -0
- package/dist/sqlite-mode/index.d.ts.map +1 -1
- package/dist/sqlite-mode/index.js +1 -0
- package/dist/sqlite-mode/index.js.map +1 -1
- package/dist/sqlite-mode/native-binary.d.ts +11 -0
- package/dist/sqlite-mode/native-binary.d.ts.map +1 -0
- package/dist/sqlite-mode/native-binary.js +67 -0
- package/dist/sqlite-mode/native-binary.js.map +1 -0
- package/package.json +8 -2
- package/src/admin/log-store.ts +19 -9
- package/src/admin/server.ts +12 -0
- package/src/cli.ts +92 -43
- package/src/index.ts +117 -18
- package/src/integration/integration.test.ts +86 -15
- package/src/integration/native-binary.guard.test.ts +13 -0
- package/src/integration/native-startup.test.ts +44 -0
- package/src/integration/restore-live-stress.test.ts +437 -0
- package/src/integration/restore-reset.test.ts +135 -16
- package/src/integration/test-permissions.ts +111 -0
- package/src/pg-proxy.ts +2 -2
- package/src/replication/change-tracker.test.ts +1 -1
- package/src/replication/change-tracker.ts +16 -13
- package/src/replication/handler.test.ts +2 -2
- package/src/replication/handler.ts +30 -2
- package/src/sqlite-mode/index.ts +1 -0
- package/src/sqlite-mode/native-binary.ts +89 -0
package/src/index.ts
CHANGED
|
@@ -23,6 +23,9 @@ import { createPGliteInstances, runMigrations } from './pglite-manager.js'
|
|
|
23
23
|
import { findPort } from './port.js'
|
|
24
24
|
import { installChangeTracking } from './replication/change-tracker.js'
|
|
25
25
|
import {
|
|
26
|
+
formatNativeBootstrapInstructions,
|
|
27
|
+
hasMissingNativeBinarySignature,
|
|
28
|
+
inspectNativeSqliteBinary,
|
|
26
29
|
resolveSqliteMode,
|
|
27
30
|
resolveSqliteModeConfig,
|
|
28
31
|
type SqliteMode,
|
|
@@ -32,6 +35,8 @@ import {
|
|
|
32
35
|
import type { ZeroLiteConfig } from './config.js'
|
|
33
36
|
import type { PGlite } from '@electric-sql/pglite'
|
|
34
37
|
|
|
38
|
+
type ZeroChildProcess = ChildProcess & { __orezTail?: string[] }
|
|
39
|
+
|
|
35
40
|
export { getConfig, getConnectionString } from './config.js'
|
|
36
41
|
export type { Hook, LogLevel, ZeroLiteConfig } from './config.js'
|
|
37
42
|
|
|
@@ -126,6 +131,12 @@ export async function startZeroLite(overrides: Partial<ZeroLiteConfig> = {}) {
|
|
|
126
131
|
const pidFile = resolve(config.dataDir, 'orez.pid')
|
|
127
132
|
writeFileSync(pidFile, String(process.pid))
|
|
128
133
|
|
|
134
|
+
// write admin port file so pg_restore can find it
|
|
135
|
+
const adminFile = resolve(config.dataDir, 'orez.admin')
|
|
136
|
+
if (adminPort > 0) {
|
|
137
|
+
writeFileSync(adminFile, String(adminPort))
|
|
138
|
+
}
|
|
139
|
+
|
|
129
140
|
// start pglite (separate instances for postgres, zero_cvr, zero_cdb)
|
|
130
141
|
const instances = await createPGliteInstances(config)
|
|
131
142
|
const db = instances.postgres
|
|
@@ -191,7 +202,7 @@ export async function startZeroLite(overrides: Partial<ZeroLiteConfig> = {}) {
|
|
|
191
202
|
)
|
|
192
203
|
zeroCacheProcess = result.process
|
|
193
204
|
zeroEnv = result.env
|
|
194
|
-
await waitForZeroCache(zeroConfig)
|
|
205
|
+
await waitForZeroCache(zeroConfig, zeroCacheProcess, 60000, sqliteMode)
|
|
195
206
|
|
|
196
207
|
// start http proxy in front of zero-cache when admin is enabled
|
|
197
208
|
if (httpLog) {
|
|
@@ -247,7 +258,7 @@ export async function startZeroLite(overrides: Partial<ZeroLiteConfig> = {}) {
|
|
|
247
258
|
)
|
|
248
259
|
zeroCacheProcess = result.process
|
|
249
260
|
zeroEnv = result.env
|
|
250
|
-
await waitForZeroCache(zeroConfig)
|
|
261
|
+
await waitForZeroCache(zeroConfig, zeroCacheProcess, 60000, sqliteMode)
|
|
251
262
|
}
|
|
252
263
|
|
|
253
264
|
// unified reset function for zero state
|
|
@@ -312,6 +323,38 @@ export async function startZeroLite(overrides: Partial<ZeroLiteConfig> = {}) {
|
|
|
312
323
|
await instances.cvr.waitReady
|
|
313
324
|
await instances.cdb.waitReady
|
|
314
325
|
log.orez('CVR/CDB recreated')
|
|
326
|
+
|
|
327
|
+
// remove stale zero shard schemas from upstream; these can outlive CVR/CDB
|
|
328
|
+
// and cause dispatcher errors after full reset.
|
|
329
|
+
const shardSchemas = await db.query<{ schemaname: string }>(
|
|
330
|
+
`SELECT DISTINCT schemaname
|
|
331
|
+
FROM pg_tables
|
|
332
|
+
WHERE tablename IN ('clients', 'replicas', 'mutations')
|
|
333
|
+
AND schemaname NOT IN (
|
|
334
|
+
'pg_catalog',
|
|
335
|
+
'information_schema',
|
|
336
|
+
'pg_toast',
|
|
337
|
+
'public',
|
|
338
|
+
'_orez'
|
|
339
|
+
)
|
|
340
|
+
AND schemaname NOT LIKE 'pg_%'`
|
|
341
|
+
)
|
|
342
|
+
for (const { schemaname } of shardSchemas.rows) {
|
|
343
|
+
const quoted = '"' + schemaname.replace(/"/g, '""') + '"'
|
|
344
|
+
await db.exec(`DROP SCHEMA IF EXISTS ${quoted} CASCADE`)
|
|
345
|
+
}
|
|
346
|
+
if (shardSchemas.rows.length > 0) {
|
|
347
|
+
log.orez(`dropped ${shardSchemas.rows.length} stale shard schema(s)`)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// clear upstream replication tracking so zero-cache starts from a
|
|
351
|
+
// clean change stream baseline after full reset.
|
|
352
|
+
await db.exec(`TRUNCATE _orez._zero_changes`).catch(() => {})
|
|
353
|
+
await db.exec(`TRUNCATE _orez._zero_replication_slots`).catch(() => {})
|
|
354
|
+
await db
|
|
355
|
+
.exec(`ALTER SEQUENCE _orez._zero_watermark RESTART WITH 1`)
|
|
356
|
+
.catch(() => {})
|
|
357
|
+
log.orez('cleared upstream replication tracking state')
|
|
315
358
|
}
|
|
316
359
|
|
|
317
360
|
// always clean up replica file
|
|
@@ -331,12 +374,13 @@ export async function startZeroLite(overrides: Partial<ZeroLiteConfig> = {}) {
|
|
|
331
374
|
DATABASE_URL: upstreamUrl,
|
|
332
375
|
OREZ_PG_PORT: String(config.pgPort),
|
|
333
376
|
})
|
|
334
|
-
|
|
335
|
-
// re-install change tracking on any tables created/modified by on-db-ready
|
|
336
|
-
log.debug.orez('re-installing change tracking after on-db-ready')
|
|
337
|
-
await installChangeTracking(db)
|
|
338
377
|
}
|
|
339
378
|
|
|
379
|
+
// always re-install change tracking after a full reset so public table
|
|
380
|
+
// triggers reflect any schema changes introduced by restore.
|
|
381
|
+
log.debug.orez('re-installing change tracking after full reset')
|
|
382
|
+
await installChangeTracking(db)
|
|
383
|
+
|
|
340
384
|
// restart zero-cache
|
|
341
385
|
log.orez('starting zero-cache...')
|
|
342
386
|
// use internal port when http proxy is enabled
|
|
@@ -350,7 +394,7 @@ export async function startZeroLite(overrides: Partial<ZeroLiteConfig> = {}) {
|
|
|
350
394
|
zeroCacheProcess = result.process
|
|
351
395
|
zeroEnv = result.env
|
|
352
396
|
|
|
353
|
-
await waitForZeroCache(zeroConfig)
|
|
397
|
+
await waitForZeroCache(zeroConfig, zeroCacheProcess, 60000, sqliteMode)
|
|
354
398
|
log.orez(`zero state reset complete (${mode})`)
|
|
355
399
|
log.zero(`ready ${port(config.zeroPort, 'magenta')}`)
|
|
356
400
|
} catch (err: any) {
|
|
@@ -365,14 +409,22 @@ export async function startZeroLite(overrides: Partial<ZeroLiteConfig> = {}) {
|
|
|
365
409
|
}
|
|
366
410
|
}
|
|
367
411
|
|
|
368
|
-
// handle SIGUSR1 to reset zero state (sent by pg_restore)
|
|
412
|
+
// handle SIGUSR1 to reset zero state (sent by pg_restore after restore completes)
|
|
369
413
|
if (!config.skipZeroCache) {
|
|
370
414
|
process.on('SIGUSR1', () => {
|
|
371
|
-
log.orez('received SIGUSR1')
|
|
415
|
+
log.orez('received SIGUSR1 - full reset')
|
|
372
416
|
resetZeroState('full').catch((err) => {
|
|
373
417
|
log.orez(`SIGUSR1 reset failed: ${err?.message || err}`)
|
|
374
418
|
})
|
|
375
419
|
})
|
|
420
|
+
|
|
421
|
+
// handle SIGUSR2 to quiesce zero-cache (sent by pg_restore before restore starts)
|
|
422
|
+
process.on('SIGUSR2', () => {
|
|
423
|
+
log.orez('received SIGUSR2 - stopping zero-cache for restore')
|
|
424
|
+
killZeroCache().catch((err) => {
|
|
425
|
+
log.orez(`SIGUSR2 stop failed: ${err?.message || err}`)
|
|
426
|
+
})
|
|
427
|
+
})
|
|
376
428
|
}
|
|
377
429
|
|
|
378
430
|
const stop = async () => {
|
|
@@ -388,6 +440,9 @@ export async function startZeroLite(overrides: Partial<ZeroLiteConfig> = {}) {
|
|
|
388
440
|
try {
|
|
389
441
|
unlinkSync(pidFile)
|
|
390
442
|
} catch {}
|
|
443
|
+
try {
|
|
444
|
+
unlinkSync(adminFile)
|
|
445
|
+
} catch {}
|
|
391
446
|
log.debug.orez('stopped')
|
|
392
447
|
}
|
|
393
448
|
|
|
@@ -402,6 +457,8 @@ export async function startZeroLite(overrides: Partial<ZeroLiteConfig> = {}) {
|
|
|
402
457
|
httpLog,
|
|
403
458
|
zeroEnv,
|
|
404
459
|
restartZero: config.skipZeroCache ? undefined : restartZeroCache,
|
|
460
|
+
// stop zero-cache without restart (for pg_restore to safely modify schema)
|
|
461
|
+
stopZero: config.skipZeroCache ? undefined : killZeroCache,
|
|
405
462
|
// cache-only reset: just replica file (fast, for minor sync issues)
|
|
406
463
|
resetZero: config.skipZeroCache ? undefined : () => resetZeroState('cache-only'),
|
|
407
464
|
// full reset: CVR/CDB + replica (for schema changes, used by pg_restore via SIGUSR1)
|
|
@@ -594,7 +651,14 @@ async function startZeroCache(
|
|
|
594
651
|
const child = spawn(zeroCacheBin, [], {
|
|
595
652
|
env,
|
|
596
653
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
597
|
-
})
|
|
654
|
+
}) as ZeroChildProcess
|
|
655
|
+
child.__orezTail = []
|
|
656
|
+
|
|
657
|
+
const pushTail = (line: string) => {
|
|
658
|
+
const tail = child.__orezTail!
|
|
659
|
+
tail.push(line)
|
|
660
|
+
if (tail.length > 80) tail.splice(0, tail.length - 80)
|
|
661
|
+
}
|
|
598
662
|
|
|
599
663
|
// detect log level from zero-cache output
|
|
600
664
|
const detectLevel = (line: string, fallback: string): string => {
|
|
@@ -618,21 +682,28 @@ async function startZeroCache(
|
|
|
618
682
|
child.stdout?.on('data', (data: Buffer) => {
|
|
619
683
|
const lines = data.toString().trim().split('\n')
|
|
620
684
|
for (const line of lines) {
|
|
621
|
-
|
|
622
|
-
|
|
685
|
+
pushTail(`stdout: ${line}`)
|
|
686
|
+
const level = detectLevel(line, 'info')
|
|
687
|
+
if (level === 'warn' || level === 'error') log.zero(line)
|
|
688
|
+
else log.debug.zero(line)
|
|
689
|
+
logStore?.push('zero', level, line)
|
|
623
690
|
}
|
|
624
691
|
})
|
|
625
692
|
|
|
626
693
|
child.stderr?.on('data', (data: Buffer) => {
|
|
627
694
|
const lines = data.toString().trim().split('\n')
|
|
628
695
|
for (const line of lines) {
|
|
629
|
-
|
|
630
|
-
|
|
696
|
+
pushTail(`stderr: ${line}`)
|
|
697
|
+
const level = detectLevel(line, 'error')
|
|
698
|
+
if (level === 'warn' || level === 'error') log.zero(line)
|
|
699
|
+
else log.debug.zero(line)
|
|
700
|
+
logStore?.push('zero', level, line)
|
|
631
701
|
}
|
|
632
702
|
})
|
|
633
703
|
|
|
634
704
|
child.on('exit', (code) => {
|
|
635
705
|
if (code !== 0 && code !== null) {
|
|
706
|
+
pushTail(`exit: code ${code}`)
|
|
636
707
|
log.zero(`exited with code ${code}`)
|
|
637
708
|
logStore?.push('zero', 'error', `exited with code ${code}`)
|
|
638
709
|
}
|
|
@@ -643,20 +714,48 @@ async function startZeroCache(
|
|
|
643
714
|
|
|
644
715
|
async function waitForZeroCache(
|
|
645
716
|
config: ZeroLiteConfig,
|
|
646
|
-
|
|
717
|
+
zeroProcess?: ChildProcess | null,
|
|
718
|
+
timeoutMs = 60000,
|
|
719
|
+
sqliteMode: SqliteMode = resolveSqliteMode(config.disableWasmSqlite)
|
|
647
720
|
): Promise<void> {
|
|
648
721
|
const start = Date.now()
|
|
649
722
|
const url = `http://127.0.0.1:${config.zeroPort}/`
|
|
650
723
|
|
|
651
724
|
while (Date.now() - start < timeoutMs) {
|
|
725
|
+
if (zeroProcess && zeroProcess.exitCode !== null) {
|
|
726
|
+
const tail = (zeroProcess as ZeroChildProcess).__orezTail
|
|
727
|
+
const details = tail?.length ? `\n${tail.slice(-20).join('\n')}` : ''
|
|
728
|
+
throw new Error(
|
|
729
|
+
`zero-cache exited with code ${zeroProcess.exitCode}${details}${nativeStartupDiagnostics(details, sqliteMode)}`
|
|
730
|
+
)
|
|
731
|
+
}
|
|
732
|
+
|
|
652
733
|
try {
|
|
653
|
-
const
|
|
654
|
-
|
|
734
|
+
const controller = new AbortController()
|
|
735
|
+
const timer = setTimeout(() => controller.abort(), 1000)
|
|
736
|
+
const res = await fetch(url, { signal: controller.signal })
|
|
737
|
+
clearTimeout(timer)
|
|
738
|
+
// zero may return 404 on "/" while still being healthy.
|
|
739
|
+
if (res.ok || res.status === 404) return
|
|
655
740
|
} catch {
|
|
656
741
|
// not ready yet
|
|
657
742
|
}
|
|
658
743
|
await new Promise((r) => setTimeout(r, 500))
|
|
659
744
|
}
|
|
660
745
|
|
|
661
|
-
|
|
746
|
+
const tail = (zeroProcess as ZeroChildProcess | null | undefined)?.__orezTail
|
|
747
|
+
const details = tail?.length ? `\n${tail.slice(-20).join('\n')}` : ''
|
|
748
|
+
throw new Error(
|
|
749
|
+
`zero-cache health check timed out after ${timeoutMs}ms${details}${nativeStartupDiagnostics(details, sqliteMode)}`
|
|
750
|
+
)
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function nativeStartupDiagnostics(details: string, sqliteMode: SqliteMode): string {
|
|
754
|
+
if (sqliteMode !== 'native') return ''
|
|
755
|
+
if (!details) return ''
|
|
756
|
+
if (!hasMissingNativeBinarySignature(details)) return ''
|
|
757
|
+
|
|
758
|
+
const check = inspectNativeSqliteBinary()
|
|
759
|
+
const instructions = formatNativeBootstrapInstructions(check)
|
|
760
|
+
return `\n\nnative sqlite startup diagnostics:\n${instructions}`
|
|
662
761
|
}
|
|
@@ -11,9 +11,21 @@ import { describe, expect, test, beforeAll, afterAll, beforeEach } from 'vitest'
|
|
|
11
11
|
import WebSocket from 'ws'
|
|
12
12
|
|
|
13
13
|
import { startZeroLite } from '../index.js'
|
|
14
|
+
import { installChangeTracking } from '../replication/change-tracker.js'
|
|
15
|
+
import { installAllowAllPermissions } from './test-permissions.js'
|
|
14
16
|
|
|
15
17
|
import type { PGlite } from '@electric-sql/pglite'
|
|
16
18
|
|
|
19
|
+
const SYNC_PROTOCOL_VERSION = 45
|
|
20
|
+
|
|
21
|
+
function encodeSecProtocols(
|
|
22
|
+
initConnectionMessage: unknown,
|
|
23
|
+
authToken: string | undefined
|
|
24
|
+
): string {
|
|
25
|
+
const payload = JSON.stringify({ initConnectionMessage, authToken })
|
|
26
|
+
return encodeURIComponent(Buffer.from(payload, 'utf-8').toString('base64'))
|
|
27
|
+
}
|
|
28
|
+
|
|
17
29
|
// simple async queue for collecting websocket messages
|
|
18
30
|
class Queue<T> {
|
|
19
31
|
private items: T[] = []
|
|
@@ -57,6 +69,7 @@ describe('orez integration', { timeout: 120000 }, () => {
|
|
|
57
69
|
let zeroPort: number
|
|
58
70
|
let pgPort: number
|
|
59
71
|
let shutdown: () => Promise<void>
|
|
72
|
+
let restartZero: (() => Promise<void>) | undefined
|
|
60
73
|
let dataDir: string
|
|
61
74
|
|
|
62
75
|
beforeAll(async () => {
|
|
@@ -77,6 +90,7 @@ describe('orez integration', { timeout: 120000 }, () => {
|
|
|
77
90
|
zeroPort = result.zeroPort
|
|
78
91
|
pgPort = result.pgPort
|
|
79
92
|
shutdown = result.stop
|
|
93
|
+
restartZero = result.restartZero
|
|
80
94
|
|
|
81
95
|
console.log(`[test] orez started, creating tables`)
|
|
82
96
|
|
|
@@ -93,6 +107,22 @@ describe('orez integration', { timeout: 120000 }, () => {
|
|
|
93
107
|
foo_id TEXT
|
|
94
108
|
);
|
|
95
109
|
`)
|
|
110
|
+
const pubName = process.env.ZERO_APP_PUBLICATIONS?.trim()
|
|
111
|
+
if (pubName) {
|
|
112
|
+
const quotedPub = '"' + pubName.replace(/"/g, '""') + '"'
|
|
113
|
+
await db
|
|
114
|
+
.exec(`ALTER PUBLICATION ${quotedPub} ADD TABLE "public"."foo"`)
|
|
115
|
+
.catch(() => {})
|
|
116
|
+
await db
|
|
117
|
+
.exec(`ALTER PUBLICATION ${quotedPub} ADD TABLE "public"."bar"`)
|
|
118
|
+
.catch(() => {})
|
|
119
|
+
await installChangeTracking(db)
|
|
120
|
+
}
|
|
121
|
+
await installAllowAllPermissions(db, ['foo', 'bar'])
|
|
122
|
+
if (restartZero) {
|
|
123
|
+
await restartZero()
|
|
124
|
+
}
|
|
125
|
+
await ensureClientGroup(zeroPort, 'test-cg')
|
|
96
126
|
|
|
97
127
|
console.log(`[test] tables created, waiting for zero-cache`)
|
|
98
128
|
// wait for zero-cache to be ready
|
|
@@ -116,9 +146,14 @@ describe('orez integration', { timeout: 120000 }, () => {
|
|
|
116
146
|
})
|
|
117
147
|
|
|
118
148
|
test('zero-cache starts and accepts websocket connections', async () => {
|
|
149
|
+
const secProtocol = encodeSecProtocols(
|
|
150
|
+
['initConnection', { desiredQueriesPatch: [] }],
|
|
151
|
+
undefined
|
|
152
|
+
)
|
|
119
153
|
const ws = new WebSocket(
|
|
120
|
-
`ws://localhost:${zeroPort}/sync/
|
|
121
|
-
`?clientGroupID=test-cg&clientID=test-client&wsid=ws1&schemaVersion=1&baseCookie=&ts=${Date.now()}&lmid=0
|
|
154
|
+
`ws://localhost:${zeroPort}/sync/v${SYNC_PROTOCOL_VERSION}/connect` +
|
|
155
|
+
`?clientGroupID=test-cg&clientID=test-client&wsid=ws1&schemaVersion=1&baseCookie=&ts=${Date.now()}&lmid=0`,
|
|
156
|
+
secProtocol
|
|
122
157
|
)
|
|
123
158
|
|
|
124
159
|
const connected = new Promise<void>((resolve, reject) => {
|
|
@@ -323,26 +358,25 @@ describe('orez integration', { timeout: 120000 }, () => {
|
|
|
323
358
|
downstream: Queue<unknown>,
|
|
324
359
|
query: Record<string, unknown>
|
|
325
360
|
): WebSocket {
|
|
361
|
+
const secProtocol = encodeSecProtocols(
|
|
362
|
+
[
|
|
363
|
+
'initConnection',
|
|
364
|
+
{
|
|
365
|
+
desiredQueriesPatch: [{ op: 'put', hash: 'q1', ast: query }],
|
|
366
|
+
},
|
|
367
|
+
],
|
|
368
|
+
undefined
|
|
369
|
+
)
|
|
326
370
|
const ws = new WebSocket(
|
|
327
|
-
`ws://localhost:${port}/sync/
|
|
328
|
-
`?clientGroupID=test-cg
|
|
371
|
+
`ws://localhost:${port}/sync/v${SYNC_PROTOCOL_VERSION}/connect` +
|
|
372
|
+
`?clientGroupID=test-cg&clientID=test-client&wsid=ws1&schemaVersion=1&baseCookie=&ts=${Date.now()}&lmid=0`,
|
|
373
|
+
secProtocol
|
|
329
374
|
)
|
|
330
375
|
|
|
331
376
|
ws.on('message', (data) => {
|
|
332
377
|
downstream.enqueue(JSON.parse(data.toString()))
|
|
333
378
|
})
|
|
334
379
|
|
|
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
|
-
|
|
346
380
|
return ws
|
|
347
381
|
}
|
|
348
382
|
|
|
@@ -423,3 +457,40 @@ async function waitForZero(port: number, timeoutMs = 30000) {
|
|
|
423
457
|
}
|
|
424
458
|
throw new Error(`zero-cache not ready on port ${port} after ${timeoutMs}ms`)
|
|
425
459
|
}
|
|
460
|
+
|
|
461
|
+
async function ensureClientGroup(port: number, clientGroupID: string): Promise<void> {
|
|
462
|
+
const secProtocol = encodeSecProtocols(
|
|
463
|
+
['initConnection', { desiredQueriesPatch: [], clientSchema: { tables: {} } }],
|
|
464
|
+
undefined
|
|
465
|
+
)
|
|
466
|
+
await new Promise<void>((resolve, reject) => {
|
|
467
|
+
const ws = new WebSocket(
|
|
468
|
+
`ws://localhost:${port}/sync/v${SYNC_PROTOCOL_VERSION}/connect` +
|
|
469
|
+
`?clientGroupID=${clientGroupID}&clientID=test-client&wsid=ws-bootstrap&schemaVersion=1&baseCookie=&ts=${Date.now()}&lmid=0`,
|
|
470
|
+
secProtocol
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
const timer = setTimeout(() => {
|
|
474
|
+
try {
|
|
475
|
+
ws.close()
|
|
476
|
+
} catch {}
|
|
477
|
+
reject(new Error('client-group bootstrap timeout'))
|
|
478
|
+
}, 7000)
|
|
479
|
+
|
|
480
|
+
ws.once('message', (data) => {
|
|
481
|
+
clearTimeout(timer)
|
|
482
|
+
const msg = JSON.parse(data.toString())
|
|
483
|
+
const isError = Array.isArray(msg) && msg[0] === 'error'
|
|
484
|
+
ws.close()
|
|
485
|
+
if (isError) {
|
|
486
|
+
reject(new Error(`client-group bootstrap failed: ${JSON.stringify(msg)}`))
|
|
487
|
+
} else {
|
|
488
|
+
resolve()
|
|
489
|
+
}
|
|
490
|
+
})
|
|
491
|
+
ws.once('error', (err) => {
|
|
492
|
+
clearTimeout(timer)
|
|
493
|
+
reject(err)
|
|
494
|
+
})
|
|
495
|
+
})
|
|
496
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
formatNativeBootstrapInstructions,
|
|
5
|
+
inspectNativeSqliteBinary,
|
|
6
|
+
} from '../sqlite-mode/native-binary.js'
|
|
7
|
+
|
|
8
|
+
describe('native sqlite binary guard', () => {
|
|
9
|
+
test('better_sqlite3.node is present before native integration tests', () => {
|
|
10
|
+
const check = inspectNativeSqliteBinary()
|
|
11
|
+
expect(check.found, formatNativeBootstrapInstructions(check)).toBe(true)
|
|
12
|
+
})
|
|
13
|
+
})
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { rmSync } from 'node:fs'
|
|
2
|
+
|
|
3
|
+
import { afterAll, beforeAll, describe, expect, test } from 'vitest'
|
|
4
|
+
|
|
5
|
+
import { startZeroLite } from '../index.js'
|
|
6
|
+
|
|
7
|
+
describe('native sqlite startup integration', { timeout: 120_000 }, () => {
|
|
8
|
+
let shutdown: (() => Promise<void>) | undefined
|
|
9
|
+
let zeroPort = 0
|
|
10
|
+
let dataDir = ''
|
|
11
|
+
|
|
12
|
+
beforeAll(async () => {
|
|
13
|
+
const basePort = 29000 + Math.floor(Math.random() * 1000)
|
|
14
|
+
dataDir = `.orez-native-startup-test-${Date.now()}`
|
|
15
|
+
|
|
16
|
+
const started = await startZeroLite({
|
|
17
|
+
pgPort: basePort,
|
|
18
|
+
zeroPort: basePort + 100,
|
|
19
|
+
dataDir,
|
|
20
|
+
logLevel: 'warn',
|
|
21
|
+
skipZeroCache: false,
|
|
22
|
+
disableWasmSqlite: true,
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
shutdown = started.stop
|
|
26
|
+
zeroPort = started.zeroPort
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
afterAll(async () => {
|
|
30
|
+
if (shutdown) await shutdown()
|
|
31
|
+
if (dataDir) {
|
|
32
|
+
try {
|
|
33
|
+
rmSync(dataDir, { recursive: true, force: true })
|
|
34
|
+
} catch {
|
|
35
|
+
// ignore cleanup failures in test teardown
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('zero-cache responds in native mode', async () => {
|
|
41
|
+
const response = await fetch(`http://127.0.0.1:${zeroPort}/`)
|
|
42
|
+
expect([200, 404]).toContain(response.status)
|
|
43
|
+
})
|
|
44
|
+
})
|