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.
- package/README.md +186 -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 +96 -46
- 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 +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +158 -23
- package/dist/index.js.map +1 -1
- package/dist/integration/test-permissions.d.ts +7 -0
- package/dist/integration/test-permissions.d.ts.map +1 -0
- package/dist/integration/test-permissions.js +117 -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/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 +8 -2
- package/src/admin/log-store.ts +19 -9
- package/src/admin/server.ts +12 -0
- package/src/cli.ts +99 -44
- package/src/config.ts +2 -0
- package/src/index.ts +186 -24
- package/src/integration/integration.test.ts +93 -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 +433 -0
- package/src/integration/restore-reset.test.ts +136 -20
- package/src/integration/test-permissions.ts +147 -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/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
package/src/admin/log-store.ts
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
)
|
|
69
|
-
|
|
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
|
}
|
package/src/admin/server.ts
CHANGED
|
@@ -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
|
-
//
|
|
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
|
|
591
|
-
SELECT
|
|
635
|
+
const cdcSchemas = await cdbSql<{ nspname: string }[]>`
|
|
636
|
+
SELECT DISTINCT nspname FROM pg_namespace WHERE nspname LIKE '%/cdc'
|
|
592
637
|
`
|
|
593
|
-
for (const {
|
|
594
|
-
await cdbSql.unsafe(`
|
|
638
|
+
for (const { nspname } of cdcSchemas) {
|
|
639
|
+
await cdbSql.unsafe(`DROP SCHEMA IF EXISTS "${nspname}" CASCADE`).catch(() => {})
|
|
595
640
|
}
|
|
596
|
-
if (
|
|
597
|
-
log.orez(`
|
|
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
|
-
//
|
|
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
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
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
|
-
//
|
|
670
|
-
|
|
671
|
-
|
|
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
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
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)
|
|
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: '
|
|
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,
|