orez 0.1.6 → 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 (69) hide show
  1. package/README.md +186 -225
  2. package/dist/admin/log-store.d.ts.map +1 -1
  3. package/dist/admin/log-store.js +17 -6
  4. package/dist/admin/log-store.js.map +1 -1
  5. package/dist/admin/server.d.ts +1 -0
  6. package/dist/admin/server.d.ts.map +1 -1
  7. package/dist/admin/server.js +10 -0
  8. package/dist/admin/server.js.map +1 -1
  9. package/dist/cli.d.ts.map +1 -1
  10. package/dist/cli.js +96 -46
  11. package/dist/cli.js.map +1 -1
  12. package/dist/config.d.ts +1 -0
  13. package/dist/config.d.ts.map +1 -1
  14. package/dist/config.js +1 -0
  15. package/dist/config.js.map +1 -1
  16. package/dist/index.d.ts +1 -0
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +158 -23
  19. package/dist/index.js.map +1 -1
  20. package/dist/integration/test-permissions.d.ts +7 -0
  21. package/dist/integration/test-permissions.d.ts.map +1 -0
  22. package/dist/integration/test-permissions.js +117 -0
  23. package/dist/integration/test-permissions.js.map +1 -0
  24. package/dist/pg-proxy.js +2 -2
  25. package/dist/pg-proxy.js.map +1 -1
  26. package/dist/replication/change-tracker.d.ts.map +1 -1
  27. package/dist/replication/change-tracker.js +15 -13
  28. package/dist/replication/change-tracker.js.map +1 -1
  29. package/dist/replication/handler.d.ts.map +1 -1
  30. package/dist/replication/handler.js +27 -2
  31. package/dist/replication/handler.js.map +1 -1
  32. package/dist/sqlite-mode/index.d.ts +1 -0
  33. package/dist/sqlite-mode/index.d.ts.map +1 -1
  34. package/dist/sqlite-mode/index.js +1 -0
  35. package/dist/sqlite-mode/index.js.map +1 -1
  36. package/dist/sqlite-mode/native-binary.d.ts +11 -0
  37. package/dist/sqlite-mode/native-binary.d.ts.map +1 -0
  38. package/dist/sqlite-mode/native-binary.js +67 -0
  39. package/dist/sqlite-mode/native-binary.js.map +1 -0
  40. package/dist/sqlite-mode/package-resolve.d.ts +6 -0
  41. package/dist/sqlite-mode/package-resolve.d.ts.map +1 -0
  42. package/dist/sqlite-mode/package-resolve.js +20 -0
  43. package/dist/sqlite-mode/package-resolve.js.map +1 -0
  44. package/dist/sqlite-mode/resolve-mode.d.ts +12 -7
  45. package/dist/sqlite-mode/resolve-mode.d.ts.map +1 -1
  46. package/dist/sqlite-mode/resolve-mode.js +27 -23
  47. package/dist/sqlite-mode/resolve-mode.js.map +1 -1
  48. package/package.json +8 -2
  49. package/src/admin/log-store.ts +19 -9
  50. package/src/admin/server.ts +12 -0
  51. package/src/cli.ts +99 -44
  52. package/src/config.ts +2 -0
  53. package/src/index.ts +186 -24
  54. package/src/integration/integration.test.ts +93 -15
  55. package/src/integration/native-binary.guard.test.ts +13 -0
  56. package/src/integration/native-startup.test.ts +44 -0
  57. package/src/integration/restore-live-stress.test.ts +433 -0
  58. package/src/integration/restore-reset.test.ts +136 -20
  59. package/src/integration/test-permissions.ts +147 -0
  60. package/src/pg-proxy.ts +2 -2
  61. package/src/replication/change-tracker.test.ts +1 -1
  62. package/src/replication/change-tracker.ts +16 -13
  63. package/src/replication/handler.test.ts +2 -2
  64. package/src/replication/handler.ts +30 -2
  65. package/src/sqlite-mode/index.ts +1 -0
  66. package/src/sqlite-mode/native-binary.ts +89 -0
  67. package/src/sqlite-mode/package-resolve.ts +17 -0
  68. package/src/sqlite-mode/resolve-mode.ts +31 -21
  69. package/src/sqlite-mode/sqlite-mode.test.ts +11 -5
@@ -29,20 +29,28 @@ export function createLogStore(dataDir: string, writeToDisk = true): LogStore {
29
29
  let nextId = 1
30
30
 
31
31
  const logsDir = join(dataDir, 'logs')
32
- const logFile = join(logsDir, 'orez.log')
33
- const backupFile = join(logsDir, 'orez.log.1')
34
32
 
35
33
  if (writeToDisk) {
36
34
  mkdirSync(logsDir, { recursive: true })
37
35
  }
38
36
 
39
- function rotateIfNeeded() {
37
+ // track file sizes to rotate per-source
38
+ const fileSizes: Record<string, number> = {}
39
+
40
+ function getLogFile(source: string): string {
41
+ return join(logsDir, `${source}.log`)
42
+ }
43
+
44
+ function rotateIfNeeded(source: string) {
40
45
  if (!writeToDisk) return
41
46
  try {
47
+ const logFile = getLogFile(source)
42
48
  if (!existsSync(logFile)) return
43
49
  const stat = statSync(logFile)
50
+ fileSizes[source] = stat.size
44
51
  if (stat.size > MAX_FILE_SIZE) {
45
- renameSync(logFile, backupFile)
52
+ renameSync(logFile, logFile + '.1')
53
+ fileSizes[source] = 0
46
54
  }
47
55
  } catch {}
48
56
  }
@@ -62,11 +70,13 @@ export function createLogStore(dataDir: string, writeToDisk = true): LogStore {
62
70
  if (writeToDisk) {
63
71
  try {
64
72
  const ts = new Date(entry.ts).toISOString()
65
- appendFileSync(
66
- logFile,
67
- '[' + ts + '] [' + source + '] [' + level + '] ' + entry.msg + '\n'
68
- )
69
- rotateIfNeeded()
73
+ const logFile = getLogFile(source)
74
+ appendFileSync(logFile, `[${ts}] [${level}] ${entry.msg}\n`)
75
+ // check rotation every ~100 writes to this source
76
+ fileSizes[source] = (fileSizes[source] || 0) + entry.msg.length + 50
77
+ if (fileSizes[source] > MAX_FILE_SIZE) {
78
+ rotateIfNeeded(source)
79
+ }
70
80
  } catch {}
71
81
  }
72
82
  }
@@ -14,6 +14,7 @@ import type { LogStore } from './log-store.js'
14
14
 
15
15
  export interface AdminActions {
16
16
  restartZero?: () => Promise<void>
17
+ stopZero?: () => Promise<void>
17
18
  resetZero?: () => Promise<void>
18
19
  resetZeroFull?: () => Promise<void>
19
20
  }
@@ -109,6 +110,17 @@ export function startAdminServer(opts: AdminServerOpts): Promise<Server> {
109
110
  return
110
111
  }
111
112
 
113
+ if (req.method === 'POST' && url.pathname === '/api/actions/stop-zero') {
114
+ if (!actions?.stopZero) {
115
+ json(res, { ok: false, message: 'zero-cache not running' }, 400)
116
+ return
117
+ }
118
+ log.orez('admin: stopping zero-cache for restore')
119
+ await actions.stopZero()
120
+ json(res, { ok: true, message: 'zero-cache stopped' })
121
+ return
122
+ }
123
+
112
124
  if (req.method === 'POST' && url.pathname === '/api/actions/reset-zero') {
113
125
  if (!actions?.resetZero) {
114
126
  json(res, { ok: false, message: 'zero-cache not running' }, 400)
package/src/cli.ts CHANGED
@@ -9,6 +9,34 @@ import { deparseSync, loadModule, parseSync } from 'pgsql-parser'
9
9
  import { startZeroLite } from './index.js'
10
10
  import { log, url } from './log.js'
11
11
 
12
+ // detect admin port from running orez instance
13
+ async function detectAdminPort(dataDir: string): Promise<number | null> {
14
+ const pidFile = resolve(dataDir, 'orez.pid')
15
+ const adminFile = resolve(dataDir, 'orez.admin')
16
+
17
+ if (!existsSync(pidFile)) return null
18
+
19
+ // check if admin port file exists
20
+ if (existsSync(adminFile)) {
21
+ try {
22
+ const port = parseInt(readFileSync(adminFile, 'utf-8').trim(), 10)
23
+ if (port > 0) return port
24
+ } catch {}
25
+ }
26
+
27
+ // fallback: try common admin ports
28
+ for (const port of [6477, 6478, 6479]) {
29
+ try {
30
+ const res = await fetch(`http://127.0.0.1:${port}/health`, {
31
+ signal: AbortSignal.timeout(500),
32
+ })
33
+ if (res.ok) return port
34
+ } catch {}
35
+ }
36
+
37
+ return null
38
+ }
39
+
12
40
  const s3Command = defineCommand({
13
41
  meta: {
14
42
  name: 's3',
@@ -539,6 +567,23 @@ async function tryWireRestore(opts: {
539
567
 
540
568
  // connected — restore errors should propagate, not fall back
541
569
  log.orez(`connected via wire protocol on port ${opts.port}`)
570
+
571
+ // automatically stop zero-cache before restore to prevent conflicts
572
+ const adminPort = await detectAdminPort(opts.dataDir)
573
+ if (adminPort) {
574
+ log.orez('stopping zero-cache for restore...')
575
+ try {
576
+ await fetch(`http://127.0.0.1:${adminPort}/api/actions/stop-zero`, {
577
+ method: 'POST',
578
+ signal: AbortSignal.timeout(10_000),
579
+ })
580
+ // give zero-cache time to stop
581
+ await new Promise((r) => setTimeout(r, 1000))
582
+ } catch {
583
+ log.orez('warning: could not stop zero-cache (may not be running)')
584
+ }
585
+ }
586
+
542
587
  try {
543
588
  const pubName = process.env.ZERO_APP_PUBLICATIONS?.trim()
544
589
  let pubTablesBeforeRestore: string[] = []
@@ -570,12 +615,12 @@ async function tryWireRestore(opts: {
570
615
  `restored ${opts.sqlFile} via wire protocol (${executed} statements, ${skipped} skipped)`
571
616
  )
572
617
 
573
- // clear zero replication state
574
- await sql.unsafe('TRUNCATE _zero_changes').catch(() => {})
575
- await sql.unsafe('TRUNCATE _zero_replication_slots').catch(() => {})
618
+ // clear zero replication state (in _orez schema)
619
+ await sql.unsafe('TRUNCATE _orez._zero_changes').catch(() => {})
620
+ await sql.unsafe('TRUNCATE _orez._zero_replication_slots').catch(() => {})
576
621
  log.orez('cleared zero replication state')
577
622
 
578
- // clear zero cdb change tracking state
623
+ // drop zero cdb cdc schemas so zero-cache can recreate them fresh
579
624
  const cdbSql = postgres({
580
625
  host: '127.0.0.1',
581
626
  port: opts.port,
@@ -587,14 +632,14 @@ async function tryWireRestore(opts: {
587
632
  onnotice: () => {},
588
633
  })
589
634
  try {
590
- const cdbTables = await cdbSql<{ schemaname: string; tablename: string }[]>`
591
- SELECT schemaname, tablename FROM pg_tables WHERE schemaname LIKE '%/cdc'
635
+ const cdcSchemas = await cdbSql<{ nspname: string }[]>`
636
+ SELECT DISTINCT nspname FROM pg_namespace WHERE nspname LIKE '%/cdc'
592
637
  `
593
- for (const { schemaname, tablename } of cdbTables) {
594
- await cdbSql.unsafe(`TRUNCATE "${schemaname}"."${tablename}"`).catch(() => {})
638
+ for (const { nspname } of cdcSchemas) {
639
+ await cdbSql.unsafe(`DROP SCHEMA IF EXISTS "${nspname}" CASCADE`).catch(() => {})
595
640
  }
596
- if (cdbTables.length > 0) {
597
- log.orez(`cleared ${cdbTables.length} cdc table(s)`)
641
+ if (cdcSchemas.length > 0) {
642
+ log.orez(`dropped ${cdcSchemas.length} cdc schema(s) from zero_cdb`)
598
643
  }
599
644
  } catch {
600
645
  // zero_cdb might not exist yet
@@ -617,19 +662,16 @@ async function tryWireRestore(opts: {
617
662
  const existingSet = new Set(existingPublicTables.map((r) => r.tablename))
618
663
 
619
664
  // Prefer pre-restore publication membership; if unavailable, fall back to
620
- // zero-marked tables (_0_version) from the restored schema.
665
+ // ALL public tables (prod dumps don't have _0_version columns yet).
621
666
  const desired = new Set<string>(
622
667
  pubTablesBeforeRestore.filter((t) => existingSet.has(t))
623
668
  )
624
669
  if (desired.size === 0) {
625
- const zeroMarked = await sql<{ tablename: string }[]>`
626
- SELECT DISTINCT table_name AS tablename
627
- FROM information_schema.columns
628
- WHERE table_schema = 'public'
629
- AND column_name = '_0_version'
630
- `
631
- for (const { tablename } of zeroMarked) {
632
- if (existingSet.has(tablename)) desired.add(tablename)
670
+ // Add all public tables except internal ones
671
+ for (const { tablename } of existingPublicTables) {
672
+ if (!tablename.startsWith('_')) {
673
+ desired.add(tablename)
674
+ }
633
675
  }
634
676
  }
635
677
 
@@ -661,34 +703,37 @@ async function tryWireRestore(opts: {
661
703
  log.orez(`publication "${pubName}" has ${count} table(s) after restore`)
662
704
  }
663
705
 
706
+ // drop zero shard schemas to prevent conflicts when zero restarts
707
+ const shardSchemas = await sql<{ nspname: string }[]>`
708
+ SELECT nspname FROM pg_namespace
709
+ WHERE nspname LIKE 'chat_%'
710
+ OR nspname LIKE 'zero_%'
711
+ OR nspname LIKE 'startchat_%'
712
+ `
713
+ for (const { nspname } of shardSchemas) {
714
+ await sql.unsafe(`DROP SCHEMA IF EXISTS "${nspname}" CASCADE`).catch(() => {})
715
+ }
716
+ if (shardSchemas.length > 0) {
717
+ log.orez(`dropped ${shardSchemas.length} shard schema(s)`)
718
+ }
719
+
664
720
  log.orez('restore complete')
665
721
  } finally {
666
722
  await sql.end({ timeout: 1 })
667
723
  }
668
724
 
669
- // signal orez to reset zero state (CVR/CDB + zero-cache)
670
- const pidFile = resolve(opts.dataDir, 'orez.pid')
671
- const resetFile = resolve(opts.dataDir, 'orez.resetting')
672
- if (existsSync(pidFile)) {
725
+ // restart zero-cache so it recreates shard schemas fresh
726
+ if (adminPort) {
727
+ log.orez('restarting zero-cache...')
673
728
  try {
674
- const pid = parseInt(readFileSync(pidFile, 'utf-8').trim(), 10)
675
- if (pid && pid > 0) {
676
- log.orez('signaling orez to reset...')
677
- process.kill(pid, 'SIGUSR1')
678
-
679
- // wait for reset to complete (orez writes .resetting file during reset)
680
- const deadline = Date.now() + 120_000
681
- await new Promise((r) => setTimeout(r, 500)) // give handler time to start
682
- while (existsSync(resetFile) && Date.now() < deadline) {
683
- await new Promise((r) => setTimeout(r, 500))
684
- }
685
- if (existsSync(resetFile)) {
686
- log.orez('warning: reset timed out, continuing anyway')
687
- } else {
688
- log.orez('reset complete')
689
- }
690
- }
691
- } catch {}
729
+ await fetch(`http://127.0.0.1:${adminPort}/api/actions/restart-zero`, {
730
+ method: 'POST',
731
+ signal: AbortSignal.timeout(10_000),
732
+ })
733
+ log.orez('zero-cache restarting')
734
+ } catch {
735
+ log.orez('warning: could not restart zero-cache')
736
+ }
692
737
  }
693
738
 
694
739
  return true
@@ -803,7 +848,10 @@ const pgRestoreCommand = defineCommand({
803
848
  sqlFile,
804
849
  dataDir: args['data-dir'],
805
850
  })
806
- if (restored) return
851
+ if (restored) {
852
+ // ensure clean exit - don't let any lingering handles keep process alive
853
+ process.exit(0)
854
+ }
807
855
  log.orez('wire protocol unavailable, falling back to direct PGlite')
808
856
  } catch (err: any) {
809
857
  // connected but restore failed — report error, don't fall back
@@ -882,7 +930,12 @@ const main = defineCommand({
882
930
  },
883
931
  'disable-wasm-sqlite': {
884
932
  type: 'boolean',
885
- 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',
886
939
  default: false,
887
940
  },
888
941
  'on-db-ready': {
@@ -920,6 +973,7 @@ const main = defineCommand({
920
973
  logStore,
921
974
  httpLog,
922
975
  restartZero,
976
+ stopZero,
923
977
  resetZero,
924
978
  resetZeroFull,
925
979
  } = await startZeroLite({
@@ -933,6 +987,7 @@ const main = defineCommand({
933
987
  pgPassword: args['pg-password'],
934
988
  skipZeroCache: args['skip-zero-cache'],
935
989
  disableWasmSqlite: args['disable-wasm-sqlite'],
990
+ forceWasmSqlite: args['force-wasm-sqlite'],
936
991
  logLevel: (args['log-level'] as 'error' | 'warn' | 'info' | 'debug') || undefined,
937
992
  onDbReady: args['on-db-ready'] || undefined,
938
993
  onHealthy: args['on-healthy'] || undefined,
@@ -956,7 +1011,7 @@ const main = defineCommand({
956
1011
  httpLog,
957
1012
  config,
958
1013
  zeroEnv,
959
- actions: { restartZero, resetZero, resetZeroFull },
1014
+ actions: { restartZero, stopZero, resetZero, resetZeroFull },
960
1015
  startTime: Date.now(),
961
1016
  })
962
1017
  log.orez(`admin: ${url(`http://localhost:${config.adminPort}`)}`)
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,