orez 0.1.7 → 0.1.8
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 +2 -1
- package/dist/cli.js +7 -1
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +54 -6
- package/dist/index.js.map +1 -1
- package/dist/integration/test-permissions.d.ts +2 -0
- package/dist/integration/test-permissions.d.ts.map +1 -1
- package/dist/integration/test-permissions.js +28 -0
- package/dist/integration/test-permissions.js.map +1 -1
- package/dist/sqlite-mode/native-binary.js +1 -1
- package/dist/sqlite-mode/native-binary.js.map +1 -1
- package/dist/sqlite-mode/package-resolve.d.ts +6 -0
- package/dist/sqlite-mode/package-resolve.d.ts.map +1 -0
- package/dist/sqlite-mode/package-resolve.js +20 -0
- package/dist/sqlite-mode/package-resolve.js.map +1 -0
- package/dist/sqlite-mode/resolve-mode.d.ts +12 -7
- package/dist/sqlite-mode/resolve-mode.d.ts.map +1 -1
- package/dist/sqlite-mode/resolve-mode.js +27 -23
- package/dist/sqlite-mode/resolve-mode.js.map +1 -1
- package/package.json +2 -2
- package/src/cli.ts +7 -1
- package/src/config.ts +2 -0
- package/src/index.ts +69 -6
- package/src/integration/integration.test.ts +49 -42
- package/src/integration/restore-live-stress.test.ts +61 -65
- package/src/integration/restore-reset.test.ts +63 -66
- package/src/integration/test-permissions.ts +36 -0
- package/src/sqlite-mode/native-binary.ts +1 -1
- package/src/sqlite-mode/package-resolve.ts +17 -0
- package/src/sqlite-mode/resolve-mode.ts +31 -21
- package/src/sqlite-mode/sqlite-mode.test.ts +11 -5
|
@@ -1,38 +1,42 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* mode resolution - canonical place to determine sqlite mode from config/env
|
|
3
|
+
*
|
|
4
|
+
* priority:
|
|
5
|
+
* 1. explicit --disable-wasm-sqlite flag → native
|
|
6
|
+
* 2. explicit --force-wasm-sqlite flag → wasm
|
|
7
|
+
* 3. native binary available → native (auto-detect)
|
|
8
|
+
* 4. fallback → wasm
|
|
3
9
|
*/
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
* import.meta.resolve doesn't work in vitest, so we fall back to require.resolve
|
|
8
|
-
*/
|
|
9
|
-
export function resolvePackage(pkg) {
|
|
10
|
-
try {
|
|
11
|
-
const resolved = import.meta.resolve(pkg);
|
|
12
|
-
if (resolved)
|
|
13
|
-
return resolved.replace('file://', '');
|
|
14
|
-
}
|
|
15
|
-
catch { }
|
|
16
|
-
try {
|
|
17
|
-
const require = createRequire(import.meta.url);
|
|
18
|
-
return require.resolve(pkg);
|
|
19
|
-
}
|
|
20
|
-
catch { }
|
|
21
|
-
return '';
|
|
22
|
-
}
|
|
10
|
+
import { inspectNativeSqliteBinary } from './native-binary.js';
|
|
11
|
+
import { resolvePackage } from './package-resolve.js';
|
|
12
|
+
export { resolvePackage } from './package-resolve.js';
|
|
23
13
|
/**
|
|
24
14
|
* resolve sqlite mode from config
|
|
25
15
|
* single source of truth for mode selection
|
|
16
|
+
*
|
|
17
|
+
* @param disableWasmSqlite - explicit flag to force native mode
|
|
18
|
+
* @param forceWasmSqlite - explicit flag to force wasm mode (overrides auto-detect)
|
|
26
19
|
*/
|
|
27
|
-
export function resolveSqliteMode(disableWasmSqlite) {
|
|
28
|
-
|
|
20
|
+
export function resolveSqliteMode(disableWasmSqlite, forceWasmSqlite = false) {
|
|
21
|
+
// explicit native request
|
|
22
|
+
if (disableWasmSqlite)
|
|
23
|
+
return 'native';
|
|
24
|
+
// explicit wasm request
|
|
25
|
+
if (forceWasmSqlite)
|
|
26
|
+
return 'wasm';
|
|
27
|
+
// auto-detect: prefer native if binary is available
|
|
28
|
+
const nativeCheck = inspectNativeSqliteBinary();
|
|
29
|
+
if (nativeCheck.found)
|
|
30
|
+
return 'native';
|
|
31
|
+
// fallback to wasm
|
|
32
|
+
return 'wasm';
|
|
29
33
|
}
|
|
30
34
|
/**
|
|
31
35
|
* resolve full sqlite mode config including paths
|
|
32
36
|
* returns null if required packages aren't installed
|
|
33
37
|
*/
|
|
34
|
-
export function resolveSqliteModeConfig(disableWasmSqlite) {
|
|
35
|
-
const mode = resolveSqliteMode(disableWasmSqlite);
|
|
38
|
+
export function resolveSqliteModeConfig(disableWasmSqlite, forceWasmSqlite = false) {
|
|
39
|
+
const mode = resolveSqliteMode(disableWasmSqlite, forceWasmSqlite);
|
|
36
40
|
const zeroSqlitePath = resolvePackage('@rocicorp/zero-sqlite3') || undefined;
|
|
37
41
|
// native mode may still need zero-sqlite3 path for restoring from a prior shim
|
|
38
42
|
if (mode === 'native') {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolve-mode.js","sourceRoot":"","sources":["../../src/sqlite-mode/resolve-mode.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"resolve-mode.js","sourceRoot":"","sources":["../../src/sqlite-mode/resolve-mode.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,yBAAyB,EAAE,MAAM,oBAAoB,CAAA;AAC9D,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAGrD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAErD;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAC/B,iBAA0B,EAC1B,kBAA2B,KAAK;IAEhC,0BAA0B;IAC1B,IAAI,iBAAiB;QAAE,OAAO,QAAQ,CAAA;IAEtC,wBAAwB;IACxB,IAAI,eAAe;QAAE,OAAO,MAAM,CAAA;IAElC,oDAAoD;IACpD,MAAM,WAAW,GAAG,yBAAyB,EAAE,CAAA;IAC/C,IAAI,WAAW,CAAC,KAAK;QAAE,OAAO,QAAQ,CAAA;IAEtC,mBAAmB;IACnB,OAAO,MAAM,CAAA;AACf,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,uBAAuB,CACrC,iBAA0B,EAC1B,kBAA2B,KAAK;IAEhC,MAAM,IAAI,GAAG,iBAAiB,CAAC,iBAAiB,EAAE,eAAe,CAAC,CAAA;IAClE,MAAM,cAAc,GAAG,cAAc,CAAC,wBAAwB,CAAC,IAAI,SAAS,CAAA;IAE5E,+EAA+E;IAC/E,IAAI,IAAI,KAAK,QAAQ,EAAE,CAAC;QACtB,OAAO,EAAE,IAAI,EAAE,cAAc,EAAE,CAAA;IACjC,CAAC;IAED,wDAAwD;IACxD,MAAM,WAAW,GAAG,cAAc,CAAC,gBAAgB,CAAC,CAAA;IAEpD,IAAI,CAAC,WAAW,EAAE,CAAC;QACjB,OAAO,IAAI,CAAA,CAAC,+BAA+B;IAC7C,CAAC;IAED,IAAI,CAAC,cAAc,EAAE,CAAC;QACpB,OAAO,IAAI,CAAA,CAAC,6BAA6B;IAC3C,CAAC;IAED,OAAO;QACL,IAAI;QACJ,WAAW;QACX,cAAc;KACf,CAAA;AACH,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,oBAAoB,CAAC,IAAgB;IACnD,OAAO,IAAI,CAAA;AACb,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "orez",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "PGlite-powered zero-sync development backend. No Docker required.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"@electric-sql/pglite": "^0.3.15",
|
|
53
53
|
"@electric-sql/pglite-tools": "^0.2.20",
|
|
54
54
|
"@rocicorp/zero": ">=0.1.0",
|
|
55
|
-
"bedrock-sqlite": "0.1.
|
|
55
|
+
"bedrock-sqlite": "0.1.9",
|
|
56
56
|
"citty": "^0.2.0",
|
|
57
57
|
"pg-gateway": "0.3.0-beta.4",
|
|
58
58
|
"pgsql-parser": "^17.9.11",
|
package/src/cli.ts
CHANGED
|
@@ -930,7 +930,12 @@ const main = defineCommand({
|
|
|
930
930
|
},
|
|
931
931
|
'disable-wasm-sqlite': {
|
|
932
932
|
type: 'boolean',
|
|
933
|
-
description: '
|
|
933
|
+
description: 'force native @rocicorp/zero-sqlite3 (fails if not available)',
|
|
934
|
+
default: false,
|
|
935
|
+
},
|
|
936
|
+
'force-wasm-sqlite': {
|
|
937
|
+
type: 'boolean',
|
|
938
|
+
description: 'force wasm bedrock-sqlite even if native is available',
|
|
934
939
|
default: false,
|
|
935
940
|
},
|
|
936
941
|
'on-db-ready': {
|
|
@@ -982,6 +987,7 @@ const main = defineCommand({
|
|
|
982
987
|
pgPassword: args['pg-password'],
|
|
983
988
|
skipZeroCache: args['skip-zero-cache'],
|
|
984
989
|
disableWasmSqlite: args['disable-wasm-sqlite'],
|
|
990
|
+
forceWasmSqlite: args['force-wasm-sqlite'],
|
|
985
991
|
logLevel: (args['log-level'] as 'error' | 'warn' | 'info' | 'debug') || undefined,
|
|
986
992
|
onDbReady: args['on-db-ready'] || undefined,
|
|
987
993
|
onHealthy: args['on-healthy'] || undefined,
|
package/src/config.ts
CHANGED
|
@@ -16,6 +16,7 @@ export interface ZeroLiteConfig {
|
|
|
16
16
|
seedFile: string
|
|
17
17
|
skipZeroCache: boolean
|
|
18
18
|
disableWasmSqlite: boolean
|
|
19
|
+
forceWasmSqlite: boolean
|
|
19
20
|
logLevel: LogLevel
|
|
20
21
|
pgliteOptions: Partial<PGliteOptions>
|
|
21
22
|
// lifecycle hooks
|
|
@@ -35,6 +36,7 @@ export function getConfig(overrides: Partial<ZeroLiteConfig> = {}): ZeroLiteConf
|
|
|
35
36
|
seedFile: overrides.seedFile || 'src/database/seed.sql',
|
|
36
37
|
skipZeroCache: overrides.skipZeroCache || false,
|
|
37
38
|
disableWasmSqlite: overrides.disableWasmSqlite ?? false,
|
|
39
|
+
forceWasmSqlite: overrides.forceWasmSqlite ?? false,
|
|
38
40
|
logLevel: overrides.logLevel || 'warn',
|
|
39
41
|
pgliteOptions: overrides.pgliteOptions || {},
|
|
40
42
|
onDbReady: overrides.onDbReady,
|
package/src/index.ts
CHANGED
|
@@ -75,6 +75,60 @@ async function runHook(
|
|
|
75
75
|
})
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
function getManagedPublicationConfig(): { names: string[]; managedByOrez: boolean } {
|
|
79
|
+
const existing = process.env.ZERO_APP_PUBLICATIONS?.trim()
|
|
80
|
+
if (existing) {
|
|
81
|
+
const names = existing
|
|
82
|
+
.split(',')
|
|
83
|
+
.map((s) => s.trim())
|
|
84
|
+
.filter(Boolean)
|
|
85
|
+
return { names, managedByOrez: false }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const appId = (process.env.ZERO_APP_ID || 'zero').trim() || 'zero'
|
|
89
|
+
const fallback = `orez_${appId}_public`
|
|
90
|
+
process.env.ZERO_APP_PUBLICATIONS = fallback
|
|
91
|
+
return { names: [fallback], managedByOrez: true }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function syncManagedPublications(
|
|
95
|
+
db: PGlite,
|
|
96
|
+
names: string[],
|
|
97
|
+
managedByOrez: boolean
|
|
98
|
+
): Promise<void> {
|
|
99
|
+
if (!managedByOrez || names.length === 0) return
|
|
100
|
+
|
|
101
|
+
const tables = await db.query<{ tablename: string }>(
|
|
102
|
+
`SELECT tablename
|
|
103
|
+
FROM pg_tables
|
|
104
|
+
WHERE schemaname = 'public'
|
|
105
|
+
AND tablename NOT LIKE '_zero_%'`
|
|
106
|
+
)
|
|
107
|
+
const publicTables = tables.rows
|
|
108
|
+
.map((r) => r.tablename)
|
|
109
|
+
.filter((t) => !t.startsWith('_'))
|
|
110
|
+
|
|
111
|
+
for (const pub of names) {
|
|
112
|
+
const quotedPub = '"' + pub.replace(/"/g, '""') + '"'
|
|
113
|
+
await db.exec(`CREATE PUBLICATION ${quotedPub}`).catch(() => {})
|
|
114
|
+
|
|
115
|
+
if (publicTables.length === 0) continue
|
|
116
|
+
const inPub = await db.query<{ tablename: string }>(
|
|
117
|
+
`SELECT tablename
|
|
118
|
+
FROM pg_publication_tables
|
|
119
|
+
WHERE pubname = $1
|
|
120
|
+
AND schemaname = 'public'`,
|
|
121
|
+
[pub]
|
|
122
|
+
)
|
|
123
|
+
const inPubSet = new Set(inPub.rows.map((r) => r.tablename))
|
|
124
|
+
const toAdd = publicTables.filter((t) => !inPubSet.has(t))
|
|
125
|
+
if (toAdd.length === 0) continue
|
|
126
|
+
const tableList = toAdd.map((t) => `"public"."${t.replace(/"/g, '""')}"`).join(', ')
|
|
127
|
+
await db.exec(`ALTER PUBLICATION ${quotedPub} ADD TABLE ${tableList}`)
|
|
128
|
+
log.debug.orez(`added ${toAdd.length} table(s) to publication "${pub}"`)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
78
132
|
// resolvePackage moved to sqlite-mode/resolve-mode.ts
|
|
79
133
|
import { resolvePackage } from './sqlite-mode/resolve-mode.js'
|
|
80
134
|
|
|
@@ -112,18 +166,20 @@ export async function startZeroLite(overrides: Partial<ZeroLiteConfig> = {}) {
|
|
|
112
166
|
log.debug.orez(`data dir: ${resolve(config.dataDir)}`)
|
|
113
167
|
|
|
114
168
|
// resolve sqlite mode config early (used for shim application and cleanup)
|
|
115
|
-
//
|
|
116
|
-
let sqliteMode = resolveSqliteMode(config.disableWasmSqlite)
|
|
117
|
-
let sqliteModeConfig = resolveSqliteModeConfig(
|
|
169
|
+
// auto-detects native if available, falls back to wasm
|
|
170
|
+
let sqliteMode = resolveSqliteMode(config.disableWasmSqlite, config.forceWasmSqlite)
|
|
171
|
+
let sqliteModeConfig = resolveSqliteModeConfig(
|
|
172
|
+
config.disableWasmSqlite,
|
|
173
|
+
config.forceWasmSqlite
|
|
174
|
+
)
|
|
118
175
|
if (sqliteMode === 'wasm' && !sqliteModeConfig) {
|
|
119
176
|
log.orez(
|
|
120
|
-
'warning: wasm sqlite requested but dependencies are missing, falling back to native
|
|
177
|
+
'warning: wasm sqlite requested but dependencies are missing, falling back to native'
|
|
121
178
|
)
|
|
122
179
|
sqliteMode = 'native'
|
|
123
180
|
config.disableWasmSqlite = true
|
|
124
|
-
sqliteModeConfig = resolveSqliteModeConfig(true)
|
|
181
|
+
sqliteModeConfig = resolveSqliteModeConfig(true, false)
|
|
125
182
|
}
|
|
126
|
-
log.orez(`sqlite: ${sqliteMode}`)
|
|
127
183
|
|
|
128
184
|
mkdirSync(config.dataDir, { recursive: true })
|
|
129
185
|
|
|
@@ -140,9 +196,14 @@ export async function startZeroLite(overrides: Partial<ZeroLiteConfig> = {}) {
|
|
|
140
196
|
// start pglite (separate instances for postgres, zero_cvr, zero_cdb)
|
|
141
197
|
const instances = await createPGliteInstances(config)
|
|
142
198
|
const db = instances.postgres
|
|
199
|
+
const managedPub = getManagedPublicationConfig()
|
|
200
|
+
if (managedPub.managedByOrez) {
|
|
201
|
+
log.debug.orez(`using managed publication: ${managedPub.names.join(', ')}`)
|
|
202
|
+
}
|
|
143
203
|
|
|
144
204
|
// run migrations (on postgres instance only)
|
|
145
205
|
const migrationsApplied = await runMigrations(db, config)
|
|
206
|
+
await syncManagedPublications(db, managedPub.names, managedPub.managedByOrez)
|
|
146
207
|
|
|
147
208
|
// install change tracking (on postgres instance only)
|
|
148
209
|
log.debug.orez('installing change tracking')
|
|
@@ -173,6 +234,7 @@ export async function startZeroLite(overrides: Partial<ZeroLiteConfig> = {}) {
|
|
|
173
234
|
})
|
|
174
235
|
|
|
175
236
|
// re-install change tracking on tables created by on-db-ready
|
|
237
|
+
await syncManagedPublications(db, managedPub.names, managedPub.managedByOrez)
|
|
176
238
|
log.debug.orez('re-installing change tracking after on-db-ready')
|
|
177
239
|
await installChangeTracking(db)
|
|
178
240
|
}
|
|
@@ -378,6 +440,7 @@ export async function startZeroLite(overrides: Partial<ZeroLiteConfig> = {}) {
|
|
|
378
440
|
|
|
379
441
|
// always re-install change tracking after a full reset so public table
|
|
380
442
|
// triggers reflect any schema changes introduced by restore.
|
|
443
|
+
await syncManagedPublications(db, managedPub.names, managedPub.managedByOrez)
|
|
381
444
|
log.debug.orez('re-installing change tracking after full reset')
|
|
382
445
|
await installChangeTracking(db)
|
|
383
446
|
|
|
@@ -12,11 +12,27 @@ import WebSocket from 'ws'
|
|
|
12
12
|
|
|
13
13
|
import { startZeroLite } from '../index.js'
|
|
14
14
|
import { installChangeTracking } from '../replication/change-tracker.js'
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
ensureTablesInPublications,
|
|
17
|
+
hasNonNullPermissions,
|
|
18
|
+
installAllowAllPermissions,
|
|
19
|
+
} from './test-permissions.js'
|
|
16
20
|
|
|
17
21
|
import type { PGlite } from '@electric-sql/pglite'
|
|
18
22
|
|
|
19
23
|
const SYNC_PROTOCOL_VERSION = 45
|
|
24
|
+
const CLIENT_SCHEMA = {
|
|
25
|
+
tables: {
|
|
26
|
+
foo: {
|
|
27
|
+
columns: {
|
|
28
|
+
id: { type: 'string' },
|
|
29
|
+
value: { type: 'string' },
|
|
30
|
+
num: { type: 'number' },
|
|
31
|
+
},
|
|
32
|
+
primaryKey: ['id'],
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
}
|
|
20
36
|
|
|
21
37
|
function encodeSecProtocols(
|
|
22
38
|
initConnectionMessage: unknown,
|
|
@@ -70,6 +86,7 @@ describe('orez integration', { timeout: 120000 }, () => {
|
|
|
70
86
|
let pgPort: number
|
|
71
87
|
let shutdown: () => Promise<void>
|
|
72
88
|
let restartZero: (() => Promise<void>) | undefined
|
|
89
|
+
let resetZeroFull: (() => Promise<void>) | undefined
|
|
73
90
|
let dataDir: string
|
|
74
91
|
|
|
75
92
|
beforeAll(async () => {
|
|
@@ -91,6 +108,7 @@ describe('orez integration', { timeout: 120000 }, () => {
|
|
|
91
108
|
pgPort = result.pgPort
|
|
92
109
|
shutdown = result.stop
|
|
93
110
|
restartZero = result.restartZero
|
|
111
|
+
resetZeroFull = result.resetZeroFull
|
|
94
112
|
|
|
95
113
|
console.log(`[test] orez started, creating tables`)
|
|
96
114
|
|
|
@@ -107,6 +125,7 @@ describe('orez integration', { timeout: 120000 }, () => {
|
|
|
107
125
|
foo_id TEXT
|
|
108
126
|
);
|
|
109
127
|
`)
|
|
128
|
+
await ensureTablesInPublications(db, ['foo', 'bar'])
|
|
110
129
|
const pubName = process.env.ZERO_APP_PUBLICATIONS?.trim()
|
|
111
130
|
if (pubName) {
|
|
112
131
|
const quotedPub = '"' + pubName.replace(/"/g, '""') + '"'
|
|
@@ -119,10 +138,30 @@ describe('orez integration', { timeout: 120000 }, () => {
|
|
|
119
138
|
await installChangeTracking(db)
|
|
120
139
|
}
|
|
121
140
|
await installAllowAllPermissions(db, ['foo', 'bar'])
|
|
122
|
-
|
|
141
|
+
expect(await hasNonNullPermissions(db)).toBe(true)
|
|
142
|
+
if (resetZeroFull) {
|
|
143
|
+
await resetZeroFull()
|
|
144
|
+
} else if (restartZero) {
|
|
123
145
|
await restartZero()
|
|
124
146
|
}
|
|
125
|
-
|
|
147
|
+
const pubNameAfterReset = process.env.ZERO_APP_PUBLICATIONS?.trim()
|
|
148
|
+
if (pubNameAfterReset) {
|
|
149
|
+
const pubRows = await db.query<{ tablename: string }>(
|
|
150
|
+
`SELECT tablename
|
|
151
|
+
FROM pg_publication_tables
|
|
152
|
+
WHERE pubname = $1
|
|
153
|
+
AND schemaname = 'public'`,
|
|
154
|
+
[pubNameAfterReset]
|
|
155
|
+
)
|
|
156
|
+
expect(pubRows.rows.map((r) => r.tablename)).toEqual(
|
|
157
|
+
expect.arrayContaining(['foo', 'bar'])
|
|
158
|
+
)
|
|
159
|
+
const shardCfg = await db.query<{ publications: string[] }>(
|
|
160
|
+
`SELECT publications FROM "zero_0"."shardConfig" WHERE lock = true`
|
|
161
|
+
)
|
|
162
|
+
expect(shardCfg.rows[0]?.publications || []).toContain(pubNameAfterReset)
|
|
163
|
+
}
|
|
164
|
+
expect(await hasNonNullPermissions(db)).toBe(true)
|
|
126
165
|
|
|
127
166
|
console.log(`[test] tables created, waiting for zero-cache`)
|
|
128
167
|
// wait for zero-cache to be ready
|
|
@@ -146,13 +185,15 @@ describe('orez integration', { timeout: 120000 }, () => {
|
|
|
146
185
|
})
|
|
147
186
|
|
|
148
187
|
test('zero-cache starts and accepts websocket connections', async () => {
|
|
188
|
+
const cg = `test-cg-${Date.now()}`
|
|
189
|
+
const cid = `test-client-${Date.now()}`
|
|
149
190
|
const secProtocol = encodeSecProtocols(
|
|
150
191
|
['initConnection', { desiredQueriesPatch: [] }],
|
|
151
192
|
undefined
|
|
152
193
|
)
|
|
153
194
|
const ws = new WebSocket(
|
|
154
195
|
`ws://localhost:${zeroPort}/sync/v${SYNC_PROTOCOL_VERSION}/connect` +
|
|
155
|
-
`?clientGroupID
|
|
196
|
+
`?clientGroupID=${cg}&clientID=${cid}&wsid=ws1&schemaVersion=1&baseCookie=&ts=${Date.now()}&lmid=0`,
|
|
156
197
|
secProtocol
|
|
157
198
|
)
|
|
158
199
|
|
|
@@ -358,18 +399,21 @@ describe('orez integration', { timeout: 120000 }, () => {
|
|
|
358
399
|
downstream: Queue<unknown>,
|
|
359
400
|
query: Record<string, unknown>
|
|
360
401
|
): WebSocket {
|
|
402
|
+
const cg = `test-cg-${Date.now()}`
|
|
403
|
+
const cid = `test-client-${Date.now()}`
|
|
361
404
|
const secProtocol = encodeSecProtocols(
|
|
362
405
|
[
|
|
363
406
|
'initConnection',
|
|
364
407
|
{
|
|
365
408
|
desiredQueriesPatch: [{ op: 'put', hash: 'q1', ast: query }],
|
|
409
|
+
clientSchema: CLIENT_SCHEMA,
|
|
366
410
|
},
|
|
367
411
|
],
|
|
368
412
|
undefined
|
|
369
413
|
)
|
|
370
414
|
const ws = new WebSocket(
|
|
371
415
|
`ws://localhost:${port}/sync/v${SYNC_PROTOCOL_VERSION}/connect` +
|
|
372
|
-
`?clientGroupID
|
|
416
|
+
`?clientGroupID=${cg}&clientID=${cid}&wsid=ws1&schemaVersion=1&baseCookie=&ts=${Date.now()}&lmid=0`,
|
|
373
417
|
secProtocol
|
|
374
418
|
)
|
|
375
419
|
|
|
@@ -457,40 +501,3 @@ async function waitForZero(port: number, timeoutMs = 30000) {
|
|
|
457
501
|
}
|
|
458
502
|
throw new Error(`zero-cache not ready on port ${port} after ${timeoutMs}ms`)
|
|
459
503
|
}
|
|
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
|
-
}
|
|
@@ -18,11 +18,26 @@ import WebSocket from 'ws'
|
|
|
18
18
|
import { execDumpFile } from '../cli.js'
|
|
19
19
|
import { startZeroLite } from '../index.js'
|
|
20
20
|
import { installChangeTracking } from '../replication/change-tracker.js'
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
ensureTablesInPublications,
|
|
23
|
+
hasNonNullPermissions,
|
|
24
|
+
installAllowAllPermissions,
|
|
25
|
+
} from './test-permissions.js'
|
|
22
26
|
|
|
23
27
|
import type { PGlite } from '@electric-sql/pglite'
|
|
24
28
|
|
|
25
29
|
const SYNC_PROTOCOL_VERSION = 45
|
|
30
|
+
const LIVE_CLIENT_SCHEMA = {
|
|
31
|
+
tables: {
|
|
32
|
+
restore_live_probe: {
|
|
33
|
+
columns: {
|
|
34
|
+
id: { type: 'string' },
|
|
35
|
+
value: { type: 'string' },
|
|
36
|
+
},
|
|
37
|
+
primaryKey: ['id'],
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
}
|
|
26
41
|
|
|
27
42
|
function encodeSecProtocols(
|
|
28
43
|
initConnectionMessage: unknown,
|
|
@@ -128,78 +143,52 @@ function connectAndSubscribe(
|
|
|
128
143
|
query: Record<string, unknown>
|
|
129
144
|
): Promise<WebSocket> {
|
|
130
145
|
return new Promise((resolve, reject) => {
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
146
|
+
const initConnectionMessage: [string, Record<string, unknown>] = [
|
|
147
|
+
'initConnection',
|
|
148
|
+
{
|
|
149
|
+
desiredQueriesPatch: [{ op: 'put', hash: 'q1', ast: query }],
|
|
150
|
+
clientSchema: LIVE_CLIENT_SCHEMA,
|
|
151
|
+
},
|
|
152
|
+
]
|
|
153
|
+
const secProtocol = encodeSecProtocols(initConnectionMessage, undefined)
|
|
154
|
+
const ws = new WebSocket(
|
|
134
155
|
`ws://127.0.0.1:${port}/sync/v${SYNC_PROTOCOL_VERSION}/connect` +
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const bootstrapProtocol = encodeSecProtocols(
|
|
140
|
-
['initConnection', { desiredQueriesPatch: [] }],
|
|
141
|
-
undefined
|
|
156
|
+
`?clientGroupID=restore-live-cg-${Date.now()}` +
|
|
157
|
+
`&clientID=restore-live-client` +
|
|
158
|
+
`&wsid=ws1&schemaVersion=1&baseCookie=&ts=${Date.now()}&lmid=0`,
|
|
159
|
+
secProtocol
|
|
142
160
|
)
|
|
143
|
-
const bootstrapWs = new WebSocket(`${urlBase}&wsid=bootstrap`, bootstrapProtocol)
|
|
144
161
|
|
|
145
|
-
|
|
146
|
-
|
|
162
|
+
let settled = false
|
|
163
|
+
const failTimer = setTimeout(() => {
|
|
164
|
+
if (settled) return
|
|
165
|
+
settled = true
|
|
147
166
|
try {
|
|
148
|
-
|
|
167
|
+
ws.close()
|
|
149
168
|
} catch {}
|
|
150
|
-
reject(
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const bootstrapTimer = setTimeout(() => {
|
|
154
|
-
fail(new Error('bootstrap websocket timeout'))
|
|
169
|
+
reject(new Error('websocket connected but no downstream messages'))
|
|
155
170
|
}, 7000)
|
|
156
|
-
bootstrapWs.once('error', fail)
|
|
157
|
-
bootstrapWs.once('message', () => {
|
|
158
|
-
clearTimeout(bootstrapTimer)
|
|
159
|
-
try {
|
|
160
|
-
bootstrapWs.close()
|
|
161
|
-
} catch {}
|
|
162
171
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
},
|
|
168
|
-
]
|
|
169
|
-
const secProtocol = encodeSecProtocols(initConnectionMessage, undefined)
|
|
170
|
-
const ws = new WebSocket(`${urlBase}&wsid=ws1`, secProtocol)
|
|
171
|
-
|
|
172
|
-
let settled = false
|
|
173
|
-
const failTimer = setTimeout(() => {
|
|
174
|
-
if (settled) return
|
|
175
|
-
settled = true
|
|
176
|
-
try {
|
|
177
|
-
ws.close()
|
|
178
|
-
} catch {}
|
|
179
|
-
reject(new Error('websocket connected but no downstream messages'))
|
|
180
|
-
}, 7000)
|
|
181
|
-
|
|
182
|
-
ws.on('message', (data) => {
|
|
183
|
-
const msg = JSON.parse(data.toString())
|
|
184
|
-
downstream.enqueue(msg)
|
|
185
|
-
if (!settled) {
|
|
186
|
-
settled = true
|
|
187
|
-
clearTimeout(failTimer)
|
|
188
|
-
resolve(ws)
|
|
189
|
-
}
|
|
190
|
-
})
|
|
191
|
-
ws.once('error', (err) => {
|
|
192
|
-
if (settled) return
|
|
172
|
+
ws.on('message', (data) => {
|
|
173
|
+
const msg = JSON.parse(data.toString())
|
|
174
|
+
downstream.enqueue(msg)
|
|
175
|
+
if (!settled) {
|
|
193
176
|
settled = true
|
|
194
177
|
clearTimeout(failTimer)
|
|
195
|
-
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
178
|
+
resolve(ws)
|
|
179
|
+
}
|
|
180
|
+
})
|
|
181
|
+
ws.once('error', (err) => {
|
|
182
|
+
if (settled) return
|
|
183
|
+
settled = true
|
|
184
|
+
clearTimeout(failTimer)
|
|
185
|
+
reject(err)
|
|
186
|
+
})
|
|
187
|
+
ws.once('close', () => {
|
|
188
|
+
if (settled) return
|
|
189
|
+
settled = true
|
|
190
|
+
clearTimeout(failTimer)
|
|
191
|
+
reject(new Error('websocket closed before initial downstream message'))
|
|
203
192
|
})
|
|
204
193
|
})
|
|
205
194
|
}
|
|
@@ -299,6 +288,7 @@ describe('live restore stress with connected frontend', { timeout: 360_000 }, ()
|
|
|
299
288
|
let zeroPort: number
|
|
300
289
|
let shutdown: () => Promise<void>
|
|
301
290
|
let restartZero: (() => Promise<void>) | undefined
|
|
291
|
+
let resetZeroFull: (() => Promise<void>) | undefined
|
|
302
292
|
let dataDir: string
|
|
303
293
|
let dumpFile: string
|
|
304
294
|
|
|
@@ -330,6 +320,7 @@ describe('live restore stress with connected frontend', { timeout: 360_000 }, ()
|
|
|
330
320
|
zeroPort = started.zeroPort
|
|
331
321
|
shutdown = started.stop
|
|
332
322
|
restartZero = started.restartZero
|
|
323
|
+
resetZeroFull = started.resetZeroFull
|
|
333
324
|
await waitForZero(zeroPort, 90_000)
|
|
334
325
|
}, 180_000)
|
|
335
326
|
|
|
@@ -352,8 +343,13 @@ describe('live restore stress with connected frontend', { timeout: 360_000 }, ()
|
|
|
352
343
|
value TEXT NOT NULL
|
|
353
344
|
)
|
|
354
345
|
`)
|
|
346
|
+
await ensureTablesInPublications(db, ['restore_live_probe'])
|
|
355
347
|
await installAllowAllPermissions(db, ['restore_live_probe'])
|
|
356
|
-
|
|
348
|
+
expect(await hasNonNullPermissions(db)).toBe(true)
|
|
349
|
+
if (resetZeroFull) {
|
|
350
|
+
await resetZeroFull()
|
|
351
|
+
await waitForZero(zeroPort, 90_000)
|
|
352
|
+
} else if (restartZero) {
|
|
357
353
|
await restartZero()
|
|
358
354
|
await waitForZero(zeroPort, 60_000)
|
|
359
355
|
}
|