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.
Files changed (36) hide show
  1. package/README.md +2 -1
  2. package/dist/cli.js +7 -1
  3. package/dist/cli.js.map +1 -1
  4. package/dist/config.d.ts +1 -0
  5. package/dist/config.d.ts.map +1 -1
  6. package/dist/config.js +1 -0
  7. package/dist/config.js.map +1 -1
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +54 -6
  10. package/dist/index.js.map +1 -1
  11. package/dist/integration/test-permissions.d.ts +2 -0
  12. package/dist/integration/test-permissions.d.ts.map +1 -1
  13. package/dist/integration/test-permissions.js +28 -0
  14. package/dist/integration/test-permissions.js.map +1 -1
  15. package/dist/sqlite-mode/native-binary.js +1 -1
  16. package/dist/sqlite-mode/native-binary.js.map +1 -1
  17. package/dist/sqlite-mode/package-resolve.d.ts +6 -0
  18. package/dist/sqlite-mode/package-resolve.d.ts.map +1 -0
  19. package/dist/sqlite-mode/package-resolve.js +20 -0
  20. package/dist/sqlite-mode/package-resolve.js.map +1 -0
  21. package/dist/sqlite-mode/resolve-mode.d.ts +12 -7
  22. package/dist/sqlite-mode/resolve-mode.d.ts.map +1 -1
  23. package/dist/sqlite-mode/resolve-mode.js +27 -23
  24. package/dist/sqlite-mode/resolve-mode.js.map +1 -1
  25. package/package.json +2 -2
  26. package/src/cli.ts +7 -1
  27. package/src/config.ts +2 -0
  28. package/src/index.ts +69 -6
  29. package/src/integration/integration.test.ts +49 -42
  30. package/src/integration/restore-live-stress.test.ts +61 -65
  31. package/src/integration/restore-reset.test.ts +63 -66
  32. package/src/integration/test-permissions.ts +36 -0
  33. package/src/sqlite-mode/native-binary.ts +1 -1
  34. package/src/sqlite-mode/package-resolve.ts +17 -0
  35. package/src/sqlite-mode/resolve-mode.ts +31 -21
  36. 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 { createRequire } from 'node:module';
5
- /**
6
- * resolve a package entry path
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
- return disableWasmSqlite ? 'native' : 'wasm';
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;;GAEG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAI3C;;;GAGG;AACH,MAAM,UAAU,cAAc,CAAC,GAAW;IACxC,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;QACzC,IAAI,QAAQ;YAAE,OAAO,QAAQ,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAA;IACtD,CAAC;IAAC,MAAM,CAAC,CAAA,CAAC;IACV,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;QAC9C,OAAO,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;IAC7B,CAAC;IAAC,MAAM,CAAC,CAAA,CAAC;IACV,OAAO,EAAE,CAAA;AACX,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAAC,iBAA0B;IAC1D,OAAO,iBAAiB,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAA;AAC9C,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,uBAAuB,CACrC,iBAA0B;IAE1B,MAAM,IAAI,GAAG,iBAAiB,CAAC,iBAAiB,CAAC,CAAA;IACjD,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"}
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.7",
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.8",
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: 'use native @rocicorp/zero-sqlite3 instead of wasm bedrock-sqlite',
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
- // requested wasm can fall back to native if required packages are missing.
116
- let sqliteMode = resolveSqliteMode(config.disableWasmSqlite)
117
- let sqliteModeConfig = resolveSqliteModeConfig(config.disableWasmSqlite)
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 sqlite'
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 { installAllowAllPermissions } from './test-permissions.js'
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
- if (restartZero) {
141
+ expect(await hasNonNullPermissions(db)).toBe(true)
142
+ if (resetZeroFull) {
143
+ await resetZeroFull()
144
+ } else if (restartZero) {
123
145
  await restartZero()
124
146
  }
125
- await ensureClientGroup(zeroPort, 'test-cg')
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=test-cg&clientID=test-client&wsid=ws1&schemaVersion=1&baseCookie=&ts=${Date.now()}&lmid=0`,
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=test-cg&clientID=test-client&wsid=ws1&schemaVersion=1&baseCookie=&ts=${Date.now()}&lmid=0`,
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 { installAllowAllPermissions } from './test-permissions.js'
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 ts = Date.now()
132
- const clientGroupID = `restore-live-cg-${ts}`
133
- const urlBase =
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
- `?clientGroupID=${clientGroupID}` +
136
- `&clientID=restore-live-client` +
137
- `&schemaVersion=1&baseCookie=&ts=${Date.now()}&lmid=0`
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
- const fail = (err: unknown) => {
146
- clearTimeout(bootstrapTimer)
162
+ let settled = false
163
+ const failTimer = setTimeout(() => {
164
+ if (settled) return
165
+ settled = true
147
166
  try {
148
- bootstrapWs.close()
167
+ ws.close()
149
168
  } catch {}
150
- reject(err)
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
- const initConnectionMessage: [string, Record<string, unknown>] = [
164
- 'initConnection',
165
- {
166
- desiredQueriesPatch: [{ op: 'put', hash: 'q1', ast: query }],
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
- reject(err)
196
- })
197
- ws.once('close', () => {
198
- if (settled) return
199
- settled = true
200
- clearTimeout(failTimer)
201
- reject(new Error('websocket closed before initial downstream message'))
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
- if (restartZero) {
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
  }