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
@@ -18,10 +18,25 @@ import WebSocket from 'ws'
18
18
 
19
19
  import { execDumpFile } from '../cli.js'
20
20
  import { startZeroLite } from '../index.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
  // zero-cache protocol version (from @rocicorp/zero/out/zero-protocol/src/protocol-version.js)
24
28
  const PROTOCOL_VERSION = 45
29
+ const RESET_CLIENT_SCHEMA = {
30
+ tables: {
31
+ reset_probe: {
32
+ columns: {
33
+ id: { type: 'string' },
34
+ value: { type: 'string' },
35
+ },
36
+ primaryKey: ['id'],
37
+ },
38
+ },
39
+ }
25
40
 
26
41
  // encode initConnection message for sec-websocket-protocol header
27
42
  // matches zero-protocol's encodeSecProtocols implementation
@@ -117,6 +132,7 @@ describe('restore/reset integration regression', { timeout: 150_000 }, () => {
117
132
  let zeroPort: number
118
133
  let shutdown: () => Promise<void>
119
134
  let restartZero: (() => Promise<void>) | undefined
135
+ let resetZeroFull: (() => Promise<void>) | undefined
120
136
  let dataDir: string
121
137
  let dumpFile: string
122
138
  let dumpFileIsTemp = false
@@ -143,6 +159,7 @@ describe('restore/reset integration regression', { timeout: 150_000 }, () => {
143
159
  zeroPort = started.zeroPort
144
160
  shutdown = started.stop
145
161
  restartZero = started.restartZero
162
+ resetZeroFull = started.resetZeroFull
146
163
 
147
164
  await waitForZero(zeroPort, 90_000)
148
165
  }, 120_000)
@@ -205,6 +222,7 @@ describe('restore/reset integration regression', { timeout: 150_000 }, () => {
205
222
  AFTER INSERT OR UPDATE OR DELETE ON public.reset_probe
206
223
  FOR EACH STATEMENT EXECUTE FUNCTION public._zero_notify_change();
207
224
  `)
225
+ await ensureTablesInPublications(db, ['reset_probe'])
208
226
  const pubName = process.env.ZERO_APP_PUBLICATIONS?.trim()
209
227
  if (pubName) {
210
228
  const quotedPub = '"' + pubName.replace(/"/g, '""') + '"'
@@ -213,7 +231,11 @@ describe('restore/reset integration regression', { timeout: 150_000 }, () => {
213
231
  .catch(() => {})
214
232
  }
215
233
  await installAllowAllPermissions(db, ['reset_probe'])
216
- if (restartZero) {
234
+ expect(await hasNonNullPermissions(db)).toBe(true)
235
+ if (resetZeroFull) {
236
+ await resetZeroFull()
237
+ await waitForZero(zeroPort, 90_000)
238
+ } else if (restartZero) {
217
239
  await restartZero()
218
240
  await waitForZero(zeroPort, 60_000)
219
241
  }
@@ -259,79 +281,54 @@ function connectAndSubscribe(
259
281
  const ts = Date.now()
260
282
  const clientGroupID = `restore-reset-cg-${ts}`
261
283
  const clientID = 'restore-reset-client'
262
- const urlBase =
284
+ const initConnectionMessage: [string, Record<string, unknown>] = [
285
+ 'initConnection',
286
+ {
287
+ desiredQueriesPatch: [{ op: 'put', hash: 'q1', ast: query }],
288
+ clientSchema: RESET_CLIENT_SCHEMA,
289
+ },
290
+ ]
291
+ const secProtocol = encodeSecProtocols(initConnectionMessage, undefined)
292
+ const ws = new WebSocket(
263
293
  `ws://127.0.0.1:${port}/sync/v${PROTOCOL_VERSION}/connect` +
264
- `?clientGroupID=${clientGroupID}&clientID=${clientID}&schemaVersion=1&baseCookie=&ts=${ts}&lmid=0`
265
-
266
- // bootstrap the client group first so the query connection is not "new group"
267
- const bootstrapProtocol = encodeSecProtocols(
268
- ['initConnection', { desiredQueriesPatch: [] }],
269
- undefined
294
+ `?clientGroupID=${clientGroupID}&clientID=${clientID}&wsid=ws1&schemaVersion=1&baseCookie=&ts=${ts}&lmid=0`,
295
+ secProtocol
270
296
  )
271
- const bootstrapWs = new WebSocket(`${urlBase}&wsid=bootstrap`, bootstrapProtocol)
272
- const bootstrapTimer = setTimeout(() => {
273
- fail(new Error('bootstrap websocket timeout'))
274
- }, 7000)
275
-
276
- const fail = (err: unknown) => {
277
- clearTimeout(bootstrapTimer)
278
- try {
279
- bootstrapWs.close()
280
- } catch {}
281
- reject(err)
282
- }
283
297
 
284
- bootstrapWs.once('error', fail)
285
- bootstrapWs.once('message', () => {
286
- clearTimeout(bootstrapTimer)
298
+ let settled = false
299
+ let sawMessage = false
300
+ const failTimer = setTimeout(() => {
301
+ if (settled) return
302
+ settled = true
287
303
  try {
288
- bootstrapWs.close()
304
+ ws.close()
289
305
  } catch {}
306
+ reject(new Error('websocket connected but no downstream messages'))
307
+ }, 7000)
290
308
 
291
- const initConnectionMessage: [string, Record<string, unknown>] = [
292
- 'initConnection',
293
- {
294
- desiredQueriesPatch: [{ op: 'put', hash: 'q1', ast: query }],
295
- },
296
- ]
297
- const secProtocol = encodeSecProtocols(initConnectionMessage, undefined)
298
- const ws = new WebSocket(`${urlBase}&wsid=ws1`, secProtocol)
299
-
300
- let settled = false
301
- let sawMessage = false
302
- const failTimer = setTimeout(() => {
303
- if (settled) return
304
- settled = true
305
- try {
306
- ws.close()
307
- } catch {}
308
- reject(new Error('websocket connected but no downstream messages'))
309
- }, 7000)
310
-
311
- ws.on('message', (data) => {
312
- const msg = JSON.parse(data.toString())
313
- downstream.enqueue(msg)
314
- if (!sawMessage && !settled) {
315
- sawMessage = true
316
- settled = true
317
- clearTimeout(failTimer)
318
- resolve(ws)
319
- }
320
- })
321
-
322
- ws.once('error', (err) => {
323
- if (settled) return
309
+ ws.on('message', (data) => {
310
+ const msg = JSON.parse(data.toString())
311
+ downstream.enqueue(msg)
312
+ if (!sawMessage && !settled) {
313
+ sawMessage = true
324
314
  settled = true
325
315
  clearTimeout(failTimer)
326
- reject(err)
327
- })
316
+ resolve(ws)
317
+ }
318
+ })
328
319
 
329
- ws.once('close', () => {
330
- if (settled) return
331
- settled = true
332
- clearTimeout(failTimer)
333
- reject(new Error('websocket closed before initial downstream message'))
334
- })
320
+ ws.once('error', (err) => {
321
+ if (settled) return
322
+ settled = true
323
+ clearTimeout(failTimer)
324
+ reject(err)
325
+ })
326
+
327
+ ws.once('close', () => {
328
+ if (settled) return
329
+ settled = true
330
+ clearTimeout(failTimer)
331
+ reject(new Error('websocket closed before initial downstream message'))
335
332
  })
336
333
  })
337
334
  }
@@ -84,6 +84,42 @@ export async function installAllowAllPermissions(
84
84
  }
85
85
  }
86
86
 
87
+ export async function hasNonNullPermissions(db: DbLike): Promise<boolean> {
88
+ const schemas = await findPermissionsSchemas(db)
89
+ for (const schema of schemas) {
90
+ const quotedSchema = '"' + schema.replace(/"/g, '""') + '"'
91
+ const result = await db.query<{ has_permissions: boolean }>(
92
+ `SELECT (permissions IS NOT NULL) AS has_permissions
93
+ FROM ${quotedSchema}.permissions
94
+ WHERE lock = true
95
+ LIMIT 1`
96
+ )
97
+ if (result.rows[0]?.has_permissions) return true
98
+ }
99
+ return false
100
+ }
101
+
102
+ export async function ensureTablesInPublications(
103
+ db: DbLike,
104
+ tables: string[]
105
+ ): Promise<void> {
106
+ const pubs = await db.query<{ pubname: string }>(
107
+ `SELECT pubname
108
+ FROM pg_publication
109
+ WHERE pubname NOT LIKE '%metadata%'
110
+ ORDER BY pubname`
111
+ )
112
+ for (const { pubname } of pubs.rows) {
113
+ const quotedPub = '"' + pubname.replace(/"/g, '""') + '"'
114
+ for (const table of tables) {
115
+ const quotedTable = '"' + table.replace(/"/g, '""') + '"'
116
+ await db
117
+ .exec(`ALTER PUBLICATION ${quotedPub} ADD TABLE "public".${quotedTable}`)
118
+ .catch(() => {})
119
+ }
120
+ }
121
+ }
122
+
87
123
  function parsePermissions(value: unknown): { tables?: Record<string, unknown> } {
88
124
  if (!value) return {}
89
125
  if (typeof value === 'string') {
@@ -1,7 +1,7 @@
1
1
  import { existsSync, readFileSync } from 'node:fs'
2
2
  import { dirname, resolve } from 'node:path'
3
3
 
4
- import { resolvePackage } from './resolve-mode.js'
4
+ import { resolvePackage } from './package-resolve.js'
5
5
 
6
6
  const NATIVE_BINARY_RELATIVE_PATHS = ['build/Release/better_sqlite3.node']
7
7
 
@@ -0,0 +1,17 @@
1
+ import { createRequire } from 'node:module'
2
+
3
+ /**
4
+ * resolve a package entry path
5
+ * import.meta.resolve doesn't work in vitest, so we fall back to require.resolve
6
+ */
7
+ export function resolvePackage(pkg: string): string {
8
+ try {
9
+ const resolved = import.meta.resolve(pkg)
10
+ if (resolved) return resolved.replace('file://', '')
11
+ } catch {}
12
+ try {
13
+ const require = createRequire(import.meta.url)
14
+ return require.resolve(pkg)
15
+ } catch {}
16
+ return ''
17
+ }
@@ -1,33 +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
10
 
5
- import { createRequire } from 'node:module'
11
+ import { inspectNativeSqliteBinary } from './native-binary.js'
12
+ import { resolvePackage } from './package-resolve.js'
6
13
 
7
14
  import type { SqliteMode, SqliteModeConfig } from './types.js'
8
-
9
- /**
10
- * resolve a package entry path
11
- * import.meta.resolve doesn't work in vitest, so we fall back to require.resolve
12
- */
13
- export function resolvePackage(pkg: string): string {
14
- try {
15
- const resolved = import.meta.resolve(pkg)
16
- if (resolved) return resolved.replace('file://', '')
17
- } catch {}
18
- try {
19
- const require = createRequire(import.meta.url)
20
- return require.resolve(pkg)
21
- } catch {}
22
- return ''
23
- }
15
+ export { resolvePackage } from './package-resolve.js'
24
16
 
25
17
  /**
26
18
  * resolve sqlite mode from config
27
19
  * single source of truth for mode selection
20
+ *
21
+ * @param disableWasmSqlite - explicit flag to force native mode
22
+ * @param forceWasmSqlite - explicit flag to force wasm mode (overrides auto-detect)
28
23
  */
29
- export function resolveSqliteMode(disableWasmSqlite: boolean): SqliteMode {
30
- return disableWasmSqlite ? 'native' : 'wasm'
24
+ export function resolveSqliteMode(
25
+ disableWasmSqlite: boolean,
26
+ forceWasmSqlite: boolean = false
27
+ ): SqliteMode {
28
+ // explicit native request
29
+ if (disableWasmSqlite) return 'native'
30
+
31
+ // explicit wasm request
32
+ if (forceWasmSqlite) return 'wasm'
33
+
34
+ // auto-detect: prefer native if binary is available
35
+ const nativeCheck = inspectNativeSqliteBinary()
36
+ if (nativeCheck.found) return 'native'
37
+
38
+ // fallback to wasm
39
+ return 'wasm'
31
40
  }
32
41
 
33
42
  /**
@@ -35,9 +44,10 @@ export function resolveSqliteMode(disableWasmSqlite: boolean): SqliteMode {
35
44
  * returns null if required packages aren't installed
36
45
  */
37
46
  export function resolveSqliteModeConfig(
38
- disableWasmSqlite: boolean
47
+ disableWasmSqlite: boolean,
48
+ forceWasmSqlite: boolean = false
39
49
  ): SqliteModeConfig | null {
40
- const mode = resolveSqliteMode(disableWasmSqlite)
50
+ const mode = resolveSqliteMode(disableWasmSqlite, forceWasmSqlite)
41
51
  const zeroSqlitePath = resolvePackage('@rocicorp/zero-sqlite3') || undefined
42
52
 
43
53
  // native mode may still need zero-sqlite3 path for restoring from a prior shim
@@ -28,16 +28,22 @@ describe('sqlite mode types', () => {
28
28
  })
29
29
 
30
30
  describe('sqlite mode resolution', () => {
31
- it('resolves wasm mode when disableWasmSqlite is false', () => {
32
- expect(resolveSqliteMode(false)).toBe('wasm')
31
+ it('resolves native mode when disableWasmSqlite is true', () => {
32
+ expect(resolveSqliteMode(true, false)).toBe('native')
33
33
  })
34
34
 
35
- it('resolves native mode when disableWasmSqlite is true', () => {
36
- expect(resolveSqliteMode(true)).toBe('native')
35
+ it('resolves wasm mode when forceWasmSqlite is true', () => {
36
+ expect(resolveSqliteMode(false, true)).toBe('wasm')
37
+ })
38
+
39
+ it('auto-detects mode based on native binary availability', () => {
40
+ // when neither flag is set, mode depends on whether native binary exists
41
+ const mode = resolveSqliteMode(false, false)
42
+ expect(['wasm', 'native']).toContain(mode)
37
43
  })
38
44
 
39
45
  it('returns config with mode for native', () => {
40
- const config = resolveSqliteModeConfig(true)
46
+ const config = resolveSqliteModeConfig(true, false)
41
47
  expect(config).not.toBeNull()
42
48
  expect(config?.mode).toBe('native')
43
49
  })