orez 0.2.27 → 0.2.29
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/package.json +3 -4
- package/src/admin/admin-data.test.ts +0 -348
- package/src/admin/http-proxy.ts +0 -252
- package/src/admin/log-store.ts +0 -192
- package/src/admin/server.ts +0 -471
- package/src/admin/ui.ts +0 -1322
- package/src/bench/proxy-throughput.bench.ts +0 -343
- package/src/bench/serial-mutations.bench.ts +0 -270
- package/src/browser.ts +0 -203
- package/src/cf-do/.wrangler/cache/cf.json +0 -1
- package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-shm +0 -0
- package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-wal +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/0ffaabee41a60e04dd0eb7db3073f0a40139e6a97ccd26823967acb652b89a7b.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite-shm +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite-wal +0 -0
- package/src/cf-do/.wrangler/tmp/bundle-0z4CpE/middleware-insertion-facade.js +0 -11
- package/src/cf-do/.wrangler/tmp/bundle-0z4CpE/middleware-loader.entry.ts +0 -134
- package/src/cf-do/.wrangler/tmp/bundle-vYmw0E/middleware-insertion-facade.js +0 -11
- package/src/cf-do/.wrangler/tmp/bundle-vYmw0E/middleware-loader.entry.ts +0 -134
- package/src/cf-do/.wrangler/tmp/dev-cbILNo/worker.js +0 -1059
- package/src/cf-do/.wrangler/tmp/dev-cbILNo/worker.js.map +0 -8
- package/src/cf-do/.wrangler/tmp/dev-qbho19/worker.js +0 -1059
- package/src/cf-do/.wrangler/tmp/dev-qbho19/worker.js.map +0 -8
- package/src/cf-do/ARCHITECTURE.md +0 -93
- package/src/cf-do/CHAT_E2E.md +0 -213
- package/src/cf-do/watermark.test.ts +0 -103
- package/src/cf-do/watermark.ts +0 -118
- package/src/cf-do/worker.ts +0 -1041
- package/src/cf-do/wrangler.toml +0 -11
- package/src/cf-pglite/README.md +0 -19
- package/src/change-tracking.ts +0 -25
- package/src/child-process.test.ts +0 -147
- package/src/child-process.ts +0 -90
- package/src/cli-entry.ts +0 -72
- package/src/cli.test.ts +0 -40
- package/src/cli.ts +0 -1214
- package/src/config.ts +0 -150
- package/src/do-sql-tracking.test.ts +0 -19
- package/src/do-sql-tracking.ts +0 -19
- package/src/index.ts +0 -1215
- package/src/integration/integration.test.ts +0 -517
- package/src/integration/native-binary.guard.test.ts +0 -13
- package/src/integration/native-startup.test.ts +0 -44
- package/src/integration/replication-latency.test.ts +0 -428
- package/src/integration/restore-live-stress.test.ts +0 -433
- package/src/integration/restore-reset.test.ts +0 -400
- package/src/integration/restore.test.ts +0 -274
- package/src/integration/test-permissions.ts +0 -147
- package/src/load-config.ts +0 -46
- package/src/log.ts +0 -96
- package/src/mutex.ts +0 -47
- package/src/pg-proxy-browser.singledb.test.ts +0 -233
- package/src/pg-proxy-browser.ts +0 -2022
- package/src/pg-proxy-do-backend.test.ts +0 -3890
- package/src/pg-proxy-do-backend.ts +0 -7191
- package/src/pg-proxy.ts +0 -1087
- package/src/pg-sqlite-compiler/README.md +0 -53
- package/src/pg-sqlite-compiler/catalog/seed.ts +0 -524
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/arithmetic.json +0 -307
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/array.json +0 -377
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/cast.json +0 -12
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/catalog.json +0 -447
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/create-table.json +0 -32
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/datetime.json +0 -397
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/enum.json +0 -337
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/insert.json +0 -337
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/json.json +0 -537
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/misc.json +0 -1837
- package/src/pg-sqlite-compiler/index.ts +0 -73
- package/src/pg-sqlite-compiler/integration.test.ts +0 -136
- package/src/pg-sqlite-compiler/passes/ast-utils.ts +0 -113
- package/src/pg-sqlite-compiler/passes/catalog.ts +0 -65
- package/src/pg-sqlite-compiler/passes/datetime.ts +0 -74
- package/src/pg-sqlite-compiler/passes/index.ts +0 -49
- package/src/pg-sqlite-compiler/passes/types.ts +0 -156
- package/src/pg-sqlite-compiler/smoke.test.ts +0 -69
- package/src/pg-sqlite-compiler/test/catalog.test.ts +0 -171
- package/src/pg-sqlite-compiler/test/corpus.test.ts +0 -161
- package/src/pg-sqlite-compiler/test/datetime.oracle.test.ts +0 -102
- package/src/pg-sqlite-compiler/test/oracle.ts +0 -237
- package/src/pg-sqlite-compiler/test/types.test.ts +0 -109
- package/src/pg-sqlite-compiler/types.ts +0 -63
- package/src/pglite-ipc.test.ts +0 -116
- package/src/pglite-ipc.ts +0 -266
- package/src/pglite-manager.ts +0 -557
- package/src/pglite-web-proxy.test.ts +0 -57
- package/src/pglite-web-proxy.ts +0 -221
- package/src/pglite-web-worker.ts +0 -152
- package/src/pglite-worker-thread.ts +0 -253
- package/src/port.ts +0 -25
- package/src/process-title.ts +0 -9
- package/src/recovery.ts +0 -155
- package/src/replication/change-tracker.test.ts +0 -357
- package/src/replication/change-tracker.ts +0 -279
- package/src/replication/handler.test.ts +0 -511
- package/src/replication/handler.ts +0 -1190
- package/src/replication/pgoutput-encoder.test.ts +0 -697
- package/src/replication/pgoutput-encoder.ts +0 -373
- package/src/replication/tcp-replication.test.ts +0 -876
- package/src/replication/zero-compat.test.ts +0 -1150
- package/src/restore-stress.test.ts +0 -188
- package/src/s3-local.ts +0 -203
- package/src/shim/hooks.mjs +0 -120
- package/src/shim/register.mjs +0 -4
- package/src/sqlite-mode/apply-mode.ts +0 -224
- package/src/sqlite-mode/index.ts +0 -15
- package/src/sqlite-mode/native-binary.ts +0 -89
- package/src/sqlite-mode/package-resolve.ts +0 -17
- package/src/sqlite-mode/resolve-mode.ts +0 -80
- package/src/sqlite-mode/shim-template.ts +0 -159
- package/src/sqlite-mode/sqlite-mode.test.ts +0 -427
- package/src/sqlite-mode/types.ts +0 -30
- package/src/vite-plugin.ts +0 -67
- package/src/wasm-sqlite.test.ts +0 -537
- package/src/worker/browser-admin.ts +0 -52
- package/src/worker/browser-build-config.test.ts +0 -71
- package/src/worker/browser-build-config.ts +0 -109
- package/src/worker/browser-embed-admin.test.ts +0 -75
- package/src/worker/browser-embed.ts +0 -345
- package/src/worker/cf-patches.ts +0 -384
- package/src/worker/embed-integration.test.ts +0 -321
- package/src/worker/index.ts +0 -138
- package/src/worker/shims/fastify.test.ts +0 -255
- package/src/worker/shims/fastify.ts +0 -306
- package/src/worker/shims/http-service.test.ts +0 -355
- package/src/worker/shims/http-service.ts +0 -293
- package/src/worker/shims/node-stub.ts +0 -290
- package/src/worker/shims/oxfmt.ts +0 -3
- package/src/worker/shims/postgres-browser.ts +0 -59
- package/src/worker/shims/postgres-socket.test.ts +0 -576
- package/src/worker/shims/postgres-socket.ts +0 -310
- package/src/worker/shims/postgres.test.ts +0 -364
- package/src/worker/shims/postgres.ts +0 -1454
- package/src/worker/shims/sqlite-browser.test.ts +0 -233
- package/src/worker/shims/sqlite-browser.ts +0 -175
- package/src/worker/shims/sqlite.test.ts +0 -786
- package/src/worker/shims/sqlite.ts +0 -978
- package/src/worker/shims/stream-browser.ts +0 -15
- package/src/worker/shims/ws-browser.test.ts +0 -205
- package/src/worker/shims/ws-browser.ts +0 -248
- package/src/worker/shims/ws.test.ts +0 -288
- package/src/worker/shims/ws.ts +0 -467
- package/src/worker/shims/zero-process-env.ts +0 -11
- package/src/worker/types.ts +0 -75
- package/src/worker/worker-integration.test.ts +0 -223
- package/src/worker/worker.test.ts +0 -136
- package/src/worker/zero-cache-embed-cf.ts +0 -463
- package/src/worker/zero-cache-embed.ts +0 -277
package/src/cli.ts
DELETED
|
@@ -1,1214 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { spawn } from 'node:child_process'
|
|
3
|
-
import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs'
|
|
4
|
-
import { resolve } from 'node:path'
|
|
5
|
-
|
|
6
|
-
import { isPidRunning } from './child-process.js'
|
|
7
|
-
import { orezTitle } from './process-title.js'
|
|
8
|
-
|
|
9
|
-
process.title = orezTitle()
|
|
10
|
-
|
|
11
|
-
import { defineCommand } from 'citty'
|
|
12
|
-
import { deparseSync, loadModule, parseSync } from 'pgsql-parser'
|
|
13
|
-
|
|
14
|
-
import { startZeroLite } from './index.js'
|
|
15
|
-
import { loadConfigFile, resolveOrezConfig } from './load-config.js'
|
|
16
|
-
import { log, url } from './log.js'
|
|
17
|
-
|
|
18
|
-
// detect admin port from running orez instance
|
|
19
|
-
async function detectAdminPort(dataDir: string): Promise<number | null> {
|
|
20
|
-
const pidFile = resolve(dataDir, 'orez.pid')
|
|
21
|
-
const adminFile = resolve(dataDir, 'orez.admin')
|
|
22
|
-
|
|
23
|
-
if (!existsSync(pidFile)) return null
|
|
24
|
-
|
|
25
|
-
let pid: number | null = null
|
|
26
|
-
try {
|
|
27
|
-
const value = Number.parseInt(readFileSync(pidFile, 'utf-8').trim(), 10)
|
|
28
|
-
if (Number.isInteger(value) && value > 0) {
|
|
29
|
-
pid = value
|
|
30
|
-
}
|
|
31
|
-
} catch {}
|
|
32
|
-
|
|
33
|
-
if (!isPidRunning(pid)) {
|
|
34
|
-
try {
|
|
35
|
-
unlinkSync(pidFile)
|
|
36
|
-
} catch {}
|
|
37
|
-
try {
|
|
38
|
-
unlinkSync(adminFile)
|
|
39
|
-
} catch {}
|
|
40
|
-
return null
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// check if admin port file exists
|
|
44
|
-
if (existsSync(adminFile)) {
|
|
45
|
-
try {
|
|
46
|
-
const port = parseInt(readFileSync(adminFile, 'utf-8').trim(), 10)
|
|
47
|
-
if (port > 0) return port
|
|
48
|
-
} catch {}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// fallback: try common admin ports
|
|
52
|
-
for (const port of [6477, 6478, 6479]) {
|
|
53
|
-
try {
|
|
54
|
-
const res = await fetch(`http://127.0.0.1:${port}/health`, {
|
|
55
|
-
signal: AbortSignal.timeout(500),
|
|
56
|
-
})
|
|
57
|
-
if (res.ok) return port
|
|
58
|
-
} catch {}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return null
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const s3Command = defineCommand({
|
|
65
|
-
meta: {
|
|
66
|
-
name: 's3',
|
|
67
|
-
description: 'start a local s3-compatible server',
|
|
68
|
-
},
|
|
69
|
-
args: {
|
|
70
|
-
port: {
|
|
71
|
-
type: 'string',
|
|
72
|
-
description: 'port to listen on',
|
|
73
|
-
default: '9200',
|
|
74
|
-
},
|
|
75
|
-
'data-dir': {
|
|
76
|
-
type: 'string',
|
|
77
|
-
description: 'data directory for stored files',
|
|
78
|
-
default: '.orez',
|
|
79
|
-
},
|
|
80
|
-
},
|
|
81
|
-
async run({ args }) {
|
|
82
|
-
const { startS3Local } = await import('./s3-local.js')
|
|
83
|
-
const server = await startS3Local({
|
|
84
|
-
port: Number(args.port),
|
|
85
|
-
dataDir: args['data-dir'],
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
process.on('SIGINT', () => {
|
|
89
|
-
server.close()
|
|
90
|
-
process.exit(0)
|
|
91
|
-
})
|
|
92
|
-
process.on('SIGTERM', () => {
|
|
93
|
-
server.close()
|
|
94
|
-
process.exit(0)
|
|
95
|
-
})
|
|
96
|
-
},
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
const pgDumpCommand = defineCommand({
|
|
100
|
-
meta: {
|
|
101
|
-
name: 'pg_dump',
|
|
102
|
-
description: 'dump the pglite postgres database to a SQL file',
|
|
103
|
-
},
|
|
104
|
-
args: {
|
|
105
|
-
'data-dir': {
|
|
106
|
-
type: 'string',
|
|
107
|
-
description: 'data directory',
|
|
108
|
-
default: '.orez',
|
|
109
|
-
},
|
|
110
|
-
output: {
|
|
111
|
-
type: 'string',
|
|
112
|
-
description: 'output file path (default: stdout)',
|
|
113
|
-
alias: 'o',
|
|
114
|
-
},
|
|
115
|
-
},
|
|
116
|
-
async run({ args }) {
|
|
117
|
-
const { PGlite } = await import('@electric-sql/pglite')
|
|
118
|
-
const { vector } = await import('@electric-sql/pglite/vector')
|
|
119
|
-
const { pg_trgm } = await import('@electric-sql/pglite/contrib/pg_trgm')
|
|
120
|
-
const { pgcrypto } = await import('@electric-sql/pglite/contrib/pgcrypto')
|
|
121
|
-
const { pgDump } = await import('@electric-sql/pglite-tools/pg_dump')
|
|
122
|
-
|
|
123
|
-
const dataPath = resolve(args['data-dir'], 'pgdata-postgres')
|
|
124
|
-
if (!existsSync(dataPath)) {
|
|
125
|
-
console.error(`error: no database found at ${dataPath}`)
|
|
126
|
-
process.exit(1)
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
let db: InstanceType<typeof PGlite> | undefined
|
|
130
|
-
try {
|
|
131
|
-
db = new PGlite({
|
|
132
|
-
dataDir: dataPath,
|
|
133
|
-
extensions: { vector, pg_trgm, pgcrypto },
|
|
134
|
-
})
|
|
135
|
-
await db.waitReady
|
|
136
|
-
|
|
137
|
-
const file = await pgDump({ pg: db })
|
|
138
|
-
const sql = await file.text()
|
|
139
|
-
|
|
140
|
-
if (args.output) {
|
|
141
|
-
writeFileSync(args.output, sql)
|
|
142
|
-
log.orez(`dump written to ${args.output}`)
|
|
143
|
-
} else {
|
|
144
|
-
process.stdout.write(sql)
|
|
145
|
-
}
|
|
146
|
-
} catch (err: any) {
|
|
147
|
-
if (err?.message?.includes('lock')) {
|
|
148
|
-
console.error(
|
|
149
|
-
'error: database is locked — stop orez first before running pg_dump'
|
|
150
|
-
)
|
|
151
|
-
} else {
|
|
152
|
-
console.error(`error: ${err?.message ?? err}`)
|
|
153
|
-
}
|
|
154
|
-
process.exit(1)
|
|
155
|
-
} finally {
|
|
156
|
-
await db?.close()
|
|
157
|
-
}
|
|
158
|
-
},
|
|
159
|
-
})
|
|
160
|
-
|
|
161
|
-
// extensions that don't exist in pglite — skip during restore
|
|
162
|
-
const UNSUPPORTED_EXTENSIONS = new Set([
|
|
163
|
-
'pg_stat_statements',
|
|
164
|
-
'pg_buffercache',
|
|
165
|
-
'pg_freespacemap',
|
|
166
|
-
'pg_prewarm',
|
|
167
|
-
'pg_stat_kcache',
|
|
168
|
-
'pg_wait_sampling',
|
|
169
|
-
'auto_explain',
|
|
170
|
-
'pg_cron',
|
|
171
|
-
])
|
|
172
|
-
|
|
173
|
-
// check if a statement should be skipped during restore
|
|
174
|
-
function shouldSkipStatement(stmt: string): boolean {
|
|
175
|
-
const trimmed = stmt.trimStart()
|
|
176
|
-
// skip psql meta-commands like \restrict (can't be parsed)
|
|
177
|
-
if (trimmed.startsWith('\\')) return true
|
|
178
|
-
|
|
179
|
-
let parsed
|
|
180
|
-
try {
|
|
181
|
-
parsed = parseSync(trimmed)
|
|
182
|
-
} catch {
|
|
183
|
-
return false // if parser can't handle it, let pglite try
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
for (const entry of parsed.stmts) {
|
|
187
|
-
const nodeType = Object.keys(entry.stmt)[0]
|
|
188
|
-
const node = entry.stmt[nodeType]
|
|
189
|
-
|
|
190
|
-
// skip SET transaction_timeout (pg 18+ artifact)
|
|
191
|
-
if (nodeType === 'VariableSetStmt' && node.name === 'transaction_timeout') return true
|
|
192
|
-
|
|
193
|
-
// skip CREATE EXTENSION for unsupported extensions
|
|
194
|
-
if (nodeType === 'CreateExtensionStmt' && UNSUPPORTED_EXTENSIONS.has(node.extname))
|
|
195
|
-
return true
|
|
196
|
-
|
|
197
|
-
// skip DROP EXTENSION for unsupported extensions
|
|
198
|
-
if (nodeType === 'DropStmt' && node.removeType === 'OBJECT_EXTENSION') {
|
|
199
|
-
const extName = node.objects?.[0]?.String?.sval
|
|
200
|
-
if (extName && UNSUPPORTED_EXTENSIONS.has(extName)) return true
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// skip COMMENT ON EXTENSION for unsupported extensions
|
|
204
|
-
if (nodeType === 'CommentStmt' && node.objtype === 'OBJECT_EXTENSION') {
|
|
205
|
-
const extName = node.object?.String?.sval
|
|
206
|
-
if (extName && UNSUPPORTED_EXTENSIONS.has(extName)) return true
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// skip CREATE/ALTER/DROP PUBLICATION — pglite doesn't support wal_level=logical
|
|
210
|
-
// internally, so CREATE PUBLICATION errors and can roll back the transaction.
|
|
211
|
-
// orez handles replication via its own change tracker, not publications.
|
|
212
|
-
if (nodeType === 'CreatePublicationStmt' || nodeType === 'AlterPublicationStmt')
|
|
213
|
-
return true
|
|
214
|
-
if (nodeType === 'DropStmt' && node.removeType === 'OBJECT_PUBLICATION') return true
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
return false
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// how many data statements to batch into a single transaction
|
|
221
|
-
const BATCH_SIZE = 200
|
|
222
|
-
// run CHECKPOINT every N batches to flush WAL and reclaim wasm memory
|
|
223
|
-
const CHECKPOINT_INTERVAL = 3
|
|
224
|
-
|
|
225
|
-
// true for statements that are data manipulation (INSERT/UPDATE/DELETE)
|
|
226
|
-
// these get batched into transactions. DDL runs outside batches.
|
|
227
|
-
// note: COPY FROM stdin is handled separately by the copy-data converter
|
|
228
|
-
function isDataStatement(stmt: string): boolean {
|
|
229
|
-
try {
|
|
230
|
-
const parsed = parseSync(stmt)
|
|
231
|
-
if (parsed.stmts.length === 0) return false
|
|
232
|
-
const nodeType = Object.keys(parsed.stmts[0].stmt)[0]
|
|
233
|
-
return (
|
|
234
|
-
nodeType === 'InsertStmt' || nodeType === 'UpdateStmt' || nodeType === 'DeleteStmt'
|
|
235
|
-
)
|
|
236
|
-
} catch {
|
|
237
|
-
return false
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// detect COPY ... FROM stdin and extract table + columns from AST
|
|
242
|
-
function parseCopyFromStdin(stmt: string): { table: string; columns: string[] } | null {
|
|
243
|
-
try {
|
|
244
|
-
const parsed = parseSync(stmt)
|
|
245
|
-
if (parsed.stmts.length === 0) return null
|
|
246
|
-
const node = parsed.stmts[0].stmt.CopyStmt
|
|
247
|
-
if (!node || !node.is_from) return null
|
|
248
|
-
const schema = node.relation.schemaname
|
|
249
|
-
const table = schema
|
|
250
|
-
? `"${schema}"."${node.relation.relname}"`
|
|
251
|
-
: `"${node.relation.relname}"`
|
|
252
|
-
const columns = node.attlist ? node.attlist.map((a: any) => `"${a.String.sval}"`) : []
|
|
253
|
-
return { table, columns }
|
|
254
|
-
} catch {
|
|
255
|
-
return null
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// convert a COPY text-format value to a SQL literal
|
|
260
|
-
// handles: \N → NULL, \\ → \, \t \n \r escapes, and single-quote escaping
|
|
261
|
-
function copyValueToLiteral(val: string): string {
|
|
262
|
-
if (val === '\\N') return 'NULL'
|
|
263
|
-
let result = ''
|
|
264
|
-
for (let i = 0; i < val.length; i++) {
|
|
265
|
-
if (val[i] === '\\' && i + 1 < val.length) {
|
|
266
|
-
const next = val[i + 1]
|
|
267
|
-
if (next === '\\') {
|
|
268
|
-
result += '\\'
|
|
269
|
-
i++
|
|
270
|
-
} else if (next === 'n') {
|
|
271
|
-
result += '\n'
|
|
272
|
-
i++
|
|
273
|
-
} else if (next === 'r') {
|
|
274
|
-
result += '\r'
|
|
275
|
-
i++
|
|
276
|
-
} else if (next === 't') {
|
|
277
|
-
result += '\t'
|
|
278
|
-
i++
|
|
279
|
-
} else {
|
|
280
|
-
result += val[i]
|
|
281
|
-
}
|
|
282
|
-
} else {
|
|
283
|
-
result += val[i]
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
return "'" + result.replace(/'/g, "''") + "'"
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// stream a sql dump file statement-by-statement with transaction batching
|
|
290
|
-
export async function execDumpFile(
|
|
291
|
-
db: { exec: (sql: string) => Promise<unknown> },
|
|
292
|
-
filePath: string
|
|
293
|
-
): Promise<{ executed: number; skipped: number }> {
|
|
294
|
-
const { createReadStream } = await import('node:fs')
|
|
295
|
-
const { createInterface } = await import('node:readline')
|
|
296
|
-
|
|
297
|
-
const rl = createInterface({
|
|
298
|
-
input: createReadStream(filePath, 'utf-8'),
|
|
299
|
-
crlfDelay: Infinity,
|
|
300
|
-
})
|
|
301
|
-
|
|
302
|
-
let buf = ''
|
|
303
|
-
let executed = 0
|
|
304
|
-
let skipped = 0
|
|
305
|
-
let batchCount = 0
|
|
306
|
-
let batchesSinceCheckpoint = 0
|
|
307
|
-
let inBatch = false
|
|
308
|
-
let dollarTag: string | null = null // tracks $tag$ quoting
|
|
309
|
-
|
|
310
|
-
// copy-data mode: when we hit COPY ... FROM stdin, we read data lines until \.
|
|
311
|
-
let copyTarget: { table: string; columns: string[] } | null = null
|
|
312
|
-
// accumulate COPY rows for multi-row INSERT (reduces statement count ~50x)
|
|
313
|
-
const COPY_ROWS_PER_INSERT = 50
|
|
314
|
-
// flush early if accumulated SQL exceeds this (prevents WASM OOM on huge rows)
|
|
315
|
-
const COPY_BATCH_MAX_BYTES = 1_000_000
|
|
316
|
-
// skip individual rows larger than this — PGlite WASM crashes around 24MB
|
|
317
|
-
const MAX_ROW_BYTES = 16_000_000
|
|
318
|
-
let copyRows: string[] = []
|
|
319
|
-
let copyRowsBytes = 0
|
|
320
|
-
|
|
321
|
-
async function flushCopyRows() {
|
|
322
|
-
if (copyRows.length === 0 || !copyTarget) return
|
|
323
|
-
const colList =
|
|
324
|
-
copyTarget.columns.length > 0 ? ` (${copyTarget.columns.join(', ')})` : ''
|
|
325
|
-
const insert = `INSERT INTO ${copyTarget.table}${colList} VALUES ${copyRows.join(', ')}`
|
|
326
|
-
try {
|
|
327
|
-
await db.exec(insert)
|
|
328
|
-
executed += copyRows.length
|
|
329
|
-
batchCount += copyRows.length
|
|
330
|
-
} catch (err: any) {
|
|
331
|
-
log.orez(`warning: ${err?.message?.split('\n')[0] ?? err}`)
|
|
332
|
-
skipped += copyRows.length
|
|
333
|
-
// transaction is aborted, rollback and start fresh
|
|
334
|
-
if (inBatch) {
|
|
335
|
-
try {
|
|
336
|
-
await db.exec('ROLLBACK')
|
|
337
|
-
} catch {}
|
|
338
|
-
inBatch = false
|
|
339
|
-
batchCount = 0
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
copyRows = []
|
|
343
|
-
copyRowsBytes = 0
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
async function flushBatch() {
|
|
347
|
-
if (inBatch) {
|
|
348
|
-
await db.exec('COMMIT')
|
|
349
|
-
inBatch = false
|
|
350
|
-
batchesSinceCheckpoint++
|
|
351
|
-
if (batchesSinceCheckpoint >= CHECKPOINT_INTERVAL) {
|
|
352
|
-
await db.exec('CHECKPOINT')
|
|
353
|
-
batchesSinceCheckpoint = 0
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
batchCount = 0
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
for await (const line of rl) {
|
|
360
|
-
// in copy-data mode: read tab-delimited rows until \.
|
|
361
|
-
if (copyTarget) {
|
|
362
|
-
if (line === '\\.') {
|
|
363
|
-
if (copyRows.length > 0) {
|
|
364
|
-
if (!inBatch) {
|
|
365
|
-
await db.exec('BEGIN')
|
|
366
|
-
inBatch = true
|
|
367
|
-
}
|
|
368
|
-
await flushCopyRows()
|
|
369
|
-
}
|
|
370
|
-
copyTarget = null
|
|
371
|
-
continue
|
|
372
|
-
}
|
|
373
|
-
const values = line.split('\t').map(copyValueToLiteral)
|
|
374
|
-
const row = `(${values.join(', ')})`
|
|
375
|
-
|
|
376
|
-
// skip rows that exceed WASM memory limits (~24MB crashes PGlite)
|
|
377
|
-
if (row.length > MAX_ROW_BYTES) {
|
|
378
|
-
log.orez(
|
|
379
|
-
`skipping oversized row (${(row.length / 1_000_000).toFixed(1)}MB) in ${copyTarget.table}`
|
|
380
|
-
)
|
|
381
|
-
skipped++
|
|
382
|
-
continue
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
// flush accumulated rows before adding if this would exceed size limit
|
|
386
|
-
if (copyRows.length > 0 && copyRowsBytes + row.length > COPY_BATCH_MAX_BYTES) {
|
|
387
|
-
if (!inBatch) {
|
|
388
|
-
await db.exec('BEGIN')
|
|
389
|
-
inBatch = true
|
|
390
|
-
}
|
|
391
|
-
await flushCopyRows()
|
|
392
|
-
if (batchCount >= BATCH_SIZE) {
|
|
393
|
-
await flushBatch()
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
copyRows.push(row)
|
|
398
|
-
copyRowsBytes += row.length
|
|
399
|
-
if (
|
|
400
|
-
copyRows.length >= COPY_ROWS_PER_INSERT ||
|
|
401
|
-
copyRowsBytes >= COPY_BATCH_MAX_BYTES
|
|
402
|
-
) {
|
|
403
|
-
if (!inBatch) {
|
|
404
|
-
await db.exec('BEGIN')
|
|
405
|
-
inBatch = true
|
|
406
|
-
}
|
|
407
|
-
await flushCopyRows()
|
|
408
|
-
if (batchCount >= BATCH_SIZE) {
|
|
409
|
-
await flushBatch()
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
continue
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
// skip empty lines and sql comments (only outside dollar-quoted blocks)
|
|
416
|
-
if (!dollarTag && (line === '' || line.startsWith('--'))) continue
|
|
417
|
-
|
|
418
|
-
buf += (buf ? '\n' : '') + line
|
|
419
|
-
|
|
420
|
-
// track dollar-quoting: $$ or $tag$
|
|
421
|
-
const dollarMatches = line.matchAll(/(\$[a-zA-Z_]*\$)/g)
|
|
422
|
-
for (const m of dollarMatches) {
|
|
423
|
-
if (dollarTag === null) {
|
|
424
|
-
dollarTag = m[1]
|
|
425
|
-
} else if (m[1] === dollarTag) {
|
|
426
|
-
dollarTag = null
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
// can't end a statement while inside a dollar-quoted block
|
|
431
|
-
if (dollarTag) continue
|
|
432
|
-
|
|
433
|
-
// statements end with ; at end of line (pg_dump always formats this way)
|
|
434
|
-
if (!line.trimEnd().endsWith(';')) continue
|
|
435
|
-
|
|
436
|
-
const stmt = buf
|
|
437
|
-
buf = ''
|
|
438
|
-
|
|
439
|
-
if (shouldSkipStatement(stmt)) {
|
|
440
|
-
skipped++
|
|
441
|
-
continue
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
// check for COPY ... FROM stdin → convert to INSERTs
|
|
445
|
-
const copyInfo = parseCopyFromStdin(stmt)
|
|
446
|
-
if (copyInfo) {
|
|
447
|
-
copyTarget = copyInfo
|
|
448
|
-
continue
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
// rewrite statements to be idempotent so restores don't crash on "already exists"
|
|
452
|
-
let rewritten = stmt
|
|
453
|
-
try {
|
|
454
|
-
const parsed = parseSync(stmt)
|
|
455
|
-
if (parsed.stmts.length > 0) {
|
|
456
|
-
const nodeType = Object.keys(parsed.stmts[0].stmt)[0]
|
|
457
|
-
const node = parsed.stmts[0].stmt[nodeType]
|
|
458
|
-
let modified = false
|
|
459
|
-
|
|
460
|
-
// CREATE SCHEMA → CREATE SCHEMA IF NOT EXISTS
|
|
461
|
-
if (nodeType === 'CreateSchemaStmt' && !node.if_not_exists) {
|
|
462
|
-
node.if_not_exists = true
|
|
463
|
-
modified = true
|
|
464
|
-
}
|
|
465
|
-
// CREATE FUNCTION/PROCEDURE → CREATE OR REPLACE
|
|
466
|
-
if (nodeType === 'CreateFunctionStmt' && !node.replace) {
|
|
467
|
-
node.replace = true
|
|
468
|
-
modified = true
|
|
469
|
-
}
|
|
470
|
-
// CREATE VIEW → CREATE OR REPLACE VIEW
|
|
471
|
-
if (nodeType === 'ViewStmt' && !node.replace) {
|
|
472
|
-
node.replace = true
|
|
473
|
-
modified = true
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
if (modified) rewritten = deparseSync(parsed)
|
|
477
|
-
}
|
|
478
|
-
} catch {
|
|
479
|
-
// if parse/deparse fails, use original
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
if (isDataStatement(rewritten)) {
|
|
483
|
-
// batch data statements into transactions
|
|
484
|
-
if (!inBatch) {
|
|
485
|
-
await db.exec('BEGIN')
|
|
486
|
-
inBatch = true
|
|
487
|
-
}
|
|
488
|
-
try {
|
|
489
|
-
await db.exec(rewritten)
|
|
490
|
-
executed++
|
|
491
|
-
batchCount++
|
|
492
|
-
if (batchCount >= BATCH_SIZE) {
|
|
493
|
-
await flushBatch()
|
|
494
|
-
}
|
|
495
|
-
} catch (err: any) {
|
|
496
|
-
// non-fatal data errors (duplicate keys from internal tables, etc.)
|
|
497
|
-
log.orez(`warning: ${err?.message?.split('\n')[0] ?? err}`)
|
|
498
|
-
skipped++
|
|
499
|
-
// transaction is aborted, rollback and start fresh
|
|
500
|
-
try {
|
|
501
|
-
await db.exec('ROLLBACK')
|
|
502
|
-
} catch {}
|
|
503
|
-
inBatch = false
|
|
504
|
-
batchCount = 0
|
|
505
|
-
}
|
|
506
|
-
} else {
|
|
507
|
-
// DDL runs outside batches
|
|
508
|
-
await flushBatch()
|
|
509
|
-
try {
|
|
510
|
-
await db.exec(rewritten)
|
|
511
|
-
executed++
|
|
512
|
-
} catch (err: any) {
|
|
513
|
-
// non-fatal DDL errors (missing tables from filtered dumps, etc.)
|
|
514
|
-
log.orez(`warning: ${err?.message?.split('\n')[0] ?? err}`)
|
|
515
|
-
skipped++
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
// flush remaining batch + buffer
|
|
521
|
-
await flushBatch()
|
|
522
|
-
if (buf.trim()) {
|
|
523
|
-
if (!shouldSkipStatement(buf)) {
|
|
524
|
-
await db.exec(buf)
|
|
525
|
-
executed++
|
|
526
|
-
} else {
|
|
527
|
-
skipped++
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
return { executed, skipped }
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
// after restore, drop triggers whose backing functions no longer exist.
|
|
535
|
-
// this happens when a filtered dump includes triggers on public-schema tables
|
|
536
|
-
// that reference functions from excluded schemas.
|
|
537
|
-
async function cleanupBrokenTriggers(db: { exec: (q: string) => Promise<unknown> }) {
|
|
538
|
-
try {
|
|
539
|
-
const result = (await db.exec(`
|
|
540
|
-
SELECT tgname, relname, nspname, proname, pronamespace
|
|
541
|
-
FROM pg_trigger t
|
|
542
|
-
JOIN pg_class c ON c.oid = t.tgrelid
|
|
543
|
-
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
544
|
-
LEFT JOIN pg_proc p ON p.oid = t.tgfoid
|
|
545
|
-
WHERE NOT t.tgisinternal
|
|
546
|
-
AND n.nspname = 'public'
|
|
547
|
-
AND (p.oid IS NULL OR p.pronamespace != n.oid)
|
|
548
|
-
`)) as any
|
|
549
|
-
|
|
550
|
-
const rows = result?.rows || result?.[0]?.rows || []
|
|
551
|
-
for (const row of rows) {
|
|
552
|
-
const trigger = row.tgname
|
|
553
|
-
const table = row.relname
|
|
554
|
-
try {
|
|
555
|
-
await db.exec(`DROP TRIGGER IF EXISTS "${trigger}" ON "public"."${table}"`)
|
|
556
|
-
log.orez(`dropped broken trigger "${trigger}" on "${table}"`)
|
|
557
|
-
} catch {}
|
|
558
|
-
}
|
|
559
|
-
} catch {
|
|
560
|
-
// best-effort cleanup
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
// try restoring via wire protocol (postgres running on given port)
|
|
565
|
-
// returns true if connected and restored, false if connection unavailable
|
|
566
|
-
async function tryWireRestore(opts: {
|
|
567
|
-
port: number
|
|
568
|
-
user: string
|
|
569
|
-
password: string
|
|
570
|
-
clean: boolean
|
|
571
|
-
sqlFile: string
|
|
572
|
-
dataDir: string
|
|
573
|
-
}): Promise<boolean> {
|
|
574
|
-
const postgres = (await import('postgres')).default
|
|
575
|
-
const sql = postgres({
|
|
576
|
-
host: '127.0.0.1',
|
|
577
|
-
port: opts.port,
|
|
578
|
-
user: opts.user,
|
|
579
|
-
password: opts.password,
|
|
580
|
-
database: 'postgres',
|
|
581
|
-
connect_timeout: 3,
|
|
582
|
-
max: 1, // single connection so BEGIN/COMMIT work correctly
|
|
583
|
-
onnotice: () => {}, // suppress pglite transaction warnings
|
|
584
|
-
})
|
|
585
|
-
|
|
586
|
-
try {
|
|
587
|
-
await sql`SELECT 1`
|
|
588
|
-
} catch {
|
|
589
|
-
await sql.end({ timeout: 0 }).catch(() => {})
|
|
590
|
-
return false
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
// connected — restore errors should propagate, not fall back
|
|
594
|
-
log.orez(`connected via wire protocol on port ${opts.port}`)
|
|
595
|
-
|
|
596
|
-
// automatically stop zero-cache before restore to prevent conflicts
|
|
597
|
-
const adminPort = await detectAdminPort(opts.dataDir)
|
|
598
|
-
if (adminPort) {
|
|
599
|
-
log.orez('stopping zero-cache for restore...')
|
|
600
|
-
try {
|
|
601
|
-
await fetch(`http://127.0.0.1:${adminPort}/api/actions/stop-zero`, {
|
|
602
|
-
method: 'POST',
|
|
603
|
-
signal: AbortSignal.timeout(10_000),
|
|
604
|
-
})
|
|
605
|
-
// give zero-cache time to stop
|
|
606
|
-
await new Promise((r) => setTimeout(r, 1000))
|
|
607
|
-
} catch {
|
|
608
|
-
log.orez('warning: could not stop zero-cache (may not be running)')
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
try {
|
|
613
|
-
const pubName = process.env.ZERO_APP_PUBLICATIONS?.trim()
|
|
614
|
-
let pubTablesBeforeRestore: string[] = []
|
|
615
|
-
if (pubName) {
|
|
616
|
-
try {
|
|
617
|
-
const existing = await sql<{ tablename: string }[]>`
|
|
618
|
-
SELECT tablename
|
|
619
|
-
FROM pg_publication_tables
|
|
620
|
-
WHERE pubname = ${pubName}
|
|
621
|
-
AND schemaname = 'public'
|
|
622
|
-
`
|
|
623
|
-
pubTablesBeforeRestore = existing.map((r) => r.tablename)
|
|
624
|
-
} catch {
|
|
625
|
-
// publication might not exist yet
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
if (opts.clean) {
|
|
630
|
-
log.orez('dropping and recreating public schema')
|
|
631
|
-
await sql.unsafe('DROP SCHEMA public CASCADE')
|
|
632
|
-
await sql.unsafe('CREATE SCHEMA public')
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
const db = { exec: (query: string) => sql.unsafe(query) as Promise<unknown> }
|
|
636
|
-
const { executed, skipped } = await execDumpFile(db, opts.sqlFile)
|
|
637
|
-
await cleanupBrokenTriggers(db)
|
|
638
|
-
await db.exec('SET search_path TO public')
|
|
639
|
-
log.orez(
|
|
640
|
-
`restored ${opts.sqlFile} via wire protocol (${executed} statements, ${skipped} skipped)`
|
|
641
|
-
)
|
|
642
|
-
|
|
643
|
-
// clear zero replication state (in _orez schema)
|
|
644
|
-
await sql.unsafe('TRUNCATE _orez._zero_changes').catch(() => {})
|
|
645
|
-
await sql.unsafe('TRUNCATE _orez._zero_replication_slots').catch(() => {})
|
|
646
|
-
log.orez('cleared zero replication state')
|
|
647
|
-
|
|
648
|
-
// drop zero cdb cdc schemas so zero-cache can recreate them fresh
|
|
649
|
-
const cdbSql = postgres({
|
|
650
|
-
host: '127.0.0.1',
|
|
651
|
-
port: opts.port,
|
|
652
|
-
user: opts.user,
|
|
653
|
-
password: opts.password,
|
|
654
|
-
database: 'zero_cdb',
|
|
655
|
-
connect_timeout: 3,
|
|
656
|
-
max: 1,
|
|
657
|
-
onnotice: () => {},
|
|
658
|
-
})
|
|
659
|
-
try {
|
|
660
|
-
const cdcSchemas = await cdbSql<{ nspname: string }[]>`
|
|
661
|
-
SELECT DISTINCT nspname FROM pg_namespace WHERE nspname LIKE '%/cdc'
|
|
662
|
-
`
|
|
663
|
-
for (const { nspname } of cdcSchemas) {
|
|
664
|
-
await cdbSql.unsafe(`DROP SCHEMA IF EXISTS "${nspname}" CASCADE`).catch(() => {})
|
|
665
|
-
}
|
|
666
|
-
if (cdcSchemas.length > 0) {
|
|
667
|
-
log.orez(`dropped ${cdcSchemas.length} cdc schema(s) from zero_cdb`)
|
|
668
|
-
}
|
|
669
|
-
} catch {
|
|
670
|
-
// zero_cdb might not exist yet
|
|
671
|
-
} finally {
|
|
672
|
-
await cdbSql.end({ timeout: 1 }).catch(() => {})
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
if (pubName) {
|
|
676
|
-
const quoted = '"' + pubName.replace(/"/g, '""') + '"'
|
|
677
|
-
await sql.unsafe(`CREATE PUBLICATION ${quoted}`).catch(() => {})
|
|
678
|
-
|
|
679
|
-
// Rebuild publication membership after restore so replication resumes
|
|
680
|
-
// without requiring an app restart or migration rerun.
|
|
681
|
-
const existingPublicTables = await sql<{ tablename: string }[]>`
|
|
682
|
-
SELECT tablename
|
|
683
|
-
FROM pg_tables
|
|
684
|
-
WHERE schemaname = 'public'
|
|
685
|
-
AND tablename NOT LIKE '_zero_%'
|
|
686
|
-
`
|
|
687
|
-
const existingSet = new Set(existingPublicTables.map((r) => r.tablename))
|
|
688
|
-
|
|
689
|
-
// Prefer pre-restore publication membership; if unavailable, fall back to
|
|
690
|
-
// ALL public tables (prod dumps don't have _0_version columns yet).
|
|
691
|
-
const desired = new Set<string>(
|
|
692
|
-
pubTablesBeforeRestore.filter((t) => existingSet.has(t))
|
|
693
|
-
)
|
|
694
|
-
if (desired.size === 0) {
|
|
695
|
-
// Add all public tables except internal ones
|
|
696
|
-
for (const { tablename } of existingPublicTables) {
|
|
697
|
-
if (!tablename.startsWith('_')) {
|
|
698
|
-
desired.add(tablename)
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
if (desired.size > 0) {
|
|
704
|
-
const inPub = await sql<{ tablename: string }[]>`
|
|
705
|
-
SELECT tablename
|
|
706
|
-
FROM pg_publication_tables
|
|
707
|
-
WHERE pubname = ${pubName}
|
|
708
|
-
AND schemaname = 'public'
|
|
709
|
-
`
|
|
710
|
-
const inPubSet = new Set(inPub.map((r) => r.tablename))
|
|
711
|
-
const toAdd = [...desired].filter((t) => !inPubSet.has(t))
|
|
712
|
-
if (toAdd.length > 0) {
|
|
713
|
-
const tableList = toAdd
|
|
714
|
-
.map((t) => `"public"."${t.replace(/"/g, '""')}"`)
|
|
715
|
-
.join(', ')
|
|
716
|
-
await sql.unsafe(`ALTER PUBLICATION ${quoted} ADD TABLE ${tableList}`)
|
|
717
|
-
log.orez(`added ${toAdd.length} table(s) to publication "${pubName}"`)
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
const countRows = await sql<{ count: string }[]>`
|
|
722
|
-
SELECT count(*)::text AS count
|
|
723
|
-
FROM pg_publication_tables
|
|
724
|
-
WHERE pubname = ${pubName}
|
|
725
|
-
AND schemaname = 'public'
|
|
726
|
-
`
|
|
727
|
-
const count = Number(countRows[0]?.count || '0')
|
|
728
|
-
log.orez(`publication "${pubName}" has ${count} table(s) after restore`)
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
// drop zero shard schemas to prevent conflicts when zero restarts
|
|
732
|
-
const shardSchemas = await sql<{ nspname: string }[]>`
|
|
733
|
-
SELECT nspname FROM pg_namespace
|
|
734
|
-
WHERE nspname LIKE 'chat_%'
|
|
735
|
-
OR nspname LIKE 'zero_%'
|
|
736
|
-
OR nspname LIKE 'startchat_%'
|
|
737
|
-
`
|
|
738
|
-
for (const { nspname } of shardSchemas) {
|
|
739
|
-
await sql.unsafe(`DROP SCHEMA IF EXISTS "${nspname}" CASCADE`).catch(() => {})
|
|
740
|
-
}
|
|
741
|
-
if (shardSchemas.length > 0) {
|
|
742
|
-
log.orez(`dropped ${shardSchemas.length} shard schema(s)`)
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
log.orez('restore complete')
|
|
746
|
-
} finally {
|
|
747
|
-
await sql.end({ timeout: 1 })
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
// restart zero-cache so it recreates shard schemas fresh
|
|
751
|
-
if (adminPort) {
|
|
752
|
-
log.orez('restarting zero-cache...')
|
|
753
|
-
try {
|
|
754
|
-
await fetch(`http://127.0.0.1:${adminPort}/api/actions/restart-zero`, {
|
|
755
|
-
method: 'POST',
|
|
756
|
-
signal: AbortSignal.timeout(10_000),
|
|
757
|
-
})
|
|
758
|
-
log.orez('zero-cache restarting')
|
|
759
|
-
} catch {
|
|
760
|
-
log.orez('warning: could not restart zero-cache')
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
return true
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
// restore by opening PGlite directly (requires no other process holding the lock)
|
|
768
|
-
async function directRestore(opts: {
|
|
769
|
-
dataDir: string
|
|
770
|
-
clean: boolean
|
|
771
|
-
sqlFile: string
|
|
772
|
-
}): Promise<void> {
|
|
773
|
-
const { PGlite } = await import('@electric-sql/pglite')
|
|
774
|
-
const { vector } = await import('@electric-sql/pglite/vector')
|
|
775
|
-
const { pg_trgm } = await import('@electric-sql/pglite/contrib/pg_trgm')
|
|
776
|
-
const { pgcrypto } = await import('@electric-sql/pglite/contrib/pgcrypto')
|
|
777
|
-
|
|
778
|
-
const dataPath = resolve(opts.dataDir, 'pgdata-postgres')
|
|
779
|
-
|
|
780
|
-
let db: InstanceType<typeof PGlite> | undefined
|
|
781
|
-
try {
|
|
782
|
-
db = new PGlite({
|
|
783
|
-
dataDir: dataPath,
|
|
784
|
-
extensions: { vector, pg_trgm, pgcrypto },
|
|
785
|
-
relaxedDurability: true,
|
|
786
|
-
})
|
|
787
|
-
await db.waitReady
|
|
788
|
-
|
|
789
|
-
if (opts.clean) {
|
|
790
|
-
log.orez('dropping and recreating public schema')
|
|
791
|
-
await db.exec('DROP SCHEMA public CASCADE')
|
|
792
|
-
await db.exec('CREATE SCHEMA public')
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
const { executed, skipped } = await execDumpFile(db, opts.sqlFile)
|
|
796
|
-
await cleanupBrokenTriggers(db)
|
|
797
|
-
await db.exec('SET search_path TO public')
|
|
798
|
-
log.orez(
|
|
799
|
-
`restored ${opts.sqlFile} into ${dataPath} (${executed} statements, ${skipped} skipped)`
|
|
800
|
-
)
|
|
801
|
-
} catch (err: any) {
|
|
802
|
-
if (err?.message?.includes('lock')) {
|
|
803
|
-
console.error(
|
|
804
|
-
'error: database is locked — stop orez first before running pg_restore'
|
|
805
|
-
)
|
|
806
|
-
} else {
|
|
807
|
-
console.error(`error: ${err?.message ?? err}`)
|
|
808
|
-
}
|
|
809
|
-
process.exit(1)
|
|
810
|
-
} finally {
|
|
811
|
-
await db?.close()
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
const pgRestoreCommand = defineCommand({
|
|
816
|
-
meta: {
|
|
817
|
-
name: 'pg_restore',
|
|
818
|
-
description: 'restore a SQL dump into the pglite postgres database',
|
|
819
|
-
},
|
|
820
|
-
args: {
|
|
821
|
-
file: {
|
|
822
|
-
type: 'positional',
|
|
823
|
-
description: 'SQL file to restore',
|
|
824
|
-
required: true,
|
|
825
|
-
},
|
|
826
|
-
'data-dir': {
|
|
827
|
-
type: 'string',
|
|
828
|
-
description: 'data directory',
|
|
829
|
-
default: '.orez',
|
|
830
|
-
},
|
|
831
|
-
clean: {
|
|
832
|
-
type: 'boolean',
|
|
833
|
-
description: 'drop and recreate public schema before restoring',
|
|
834
|
-
default: false,
|
|
835
|
-
},
|
|
836
|
-
'pg-port': {
|
|
837
|
-
type: 'string',
|
|
838
|
-
description: 'postgresql port for wire protocol connection',
|
|
839
|
-
default: '6434',
|
|
840
|
-
},
|
|
841
|
-
'pg-user': {
|
|
842
|
-
type: 'string',
|
|
843
|
-
description: 'postgresql user',
|
|
844
|
-
default: 'user',
|
|
845
|
-
},
|
|
846
|
-
'pg-password': {
|
|
847
|
-
type: 'string',
|
|
848
|
-
description: 'postgresql password',
|
|
849
|
-
default: 'password',
|
|
850
|
-
},
|
|
851
|
-
direct: {
|
|
852
|
-
type: 'boolean',
|
|
853
|
-
description: 'force direct PGlite access, skip wire protocol auto-detection',
|
|
854
|
-
default: false,
|
|
855
|
-
},
|
|
856
|
-
},
|
|
857
|
-
async run({ args }) {
|
|
858
|
-
await loadModule() // initialize pgsql-parser WASM
|
|
859
|
-
|
|
860
|
-
const sqlFile = args.file
|
|
861
|
-
if (!existsSync(sqlFile)) {
|
|
862
|
-
console.error(`error: file not found: ${sqlFile}`)
|
|
863
|
-
process.exit(1)
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
// try wire protocol first (unless --direct)
|
|
867
|
-
if (!args.direct) {
|
|
868
|
-
try {
|
|
869
|
-
const restored = await tryWireRestore({
|
|
870
|
-
port: Number(args['pg-port']),
|
|
871
|
-
user: args['pg-user'],
|
|
872
|
-
password: args['pg-password'],
|
|
873
|
-
clean: args.clean,
|
|
874
|
-
sqlFile,
|
|
875
|
-
dataDir: args['data-dir'],
|
|
876
|
-
})
|
|
877
|
-
if (restored) {
|
|
878
|
-
// ensure clean exit - don't let any lingering handles keep process alive
|
|
879
|
-
process.exit(0)
|
|
880
|
-
}
|
|
881
|
-
log.orez('wire protocol unavailable, falling back to direct PGlite')
|
|
882
|
-
} catch (err: any) {
|
|
883
|
-
// connected but restore failed — report error, don't fall back
|
|
884
|
-
console.error(`error: ${err?.message ?? err}`)
|
|
885
|
-
process.exit(1)
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
await directRestore({
|
|
890
|
-
dataDir: args['data-dir'],
|
|
891
|
-
clean: args.clean,
|
|
892
|
-
sqlFile,
|
|
893
|
-
})
|
|
894
|
-
},
|
|
895
|
-
})
|
|
896
|
-
|
|
897
|
-
export const main = defineCommand({
|
|
898
|
-
meta: {
|
|
899
|
-
name: 'orez',
|
|
900
|
-
description: 'pglite-powered zero-sync development backend',
|
|
901
|
-
},
|
|
902
|
-
args: {
|
|
903
|
-
'pg-port': {
|
|
904
|
-
type: 'string',
|
|
905
|
-
description: 'postgresql proxy port',
|
|
906
|
-
default: '6434',
|
|
907
|
-
},
|
|
908
|
-
'zero-port': {
|
|
909
|
-
type: 'string',
|
|
910
|
-
description: 'zero-cache port',
|
|
911
|
-
default: '5849',
|
|
912
|
-
},
|
|
913
|
-
'data-dir': {
|
|
914
|
-
type: 'string',
|
|
915
|
-
description: 'data directory',
|
|
916
|
-
default: '.orez',
|
|
917
|
-
},
|
|
918
|
-
migrations: {
|
|
919
|
-
type: 'string',
|
|
920
|
-
description: 'migrations directory',
|
|
921
|
-
default: '',
|
|
922
|
-
},
|
|
923
|
-
seed: {
|
|
924
|
-
type: 'string',
|
|
925
|
-
description: 'seed file path',
|
|
926
|
-
default: '',
|
|
927
|
-
},
|
|
928
|
-
'pg-user': {
|
|
929
|
-
type: 'string',
|
|
930
|
-
description: 'postgresql user',
|
|
931
|
-
default: 'user',
|
|
932
|
-
},
|
|
933
|
-
'pg-password': {
|
|
934
|
-
type: 'string',
|
|
935
|
-
description: 'postgresql password',
|
|
936
|
-
default: 'password',
|
|
937
|
-
},
|
|
938
|
-
'skip-zero-cache': {
|
|
939
|
-
type: 'boolean',
|
|
940
|
-
description: 'run pglite + proxy only, skip zero-cache',
|
|
941
|
-
default: false,
|
|
942
|
-
},
|
|
943
|
-
'log-level': {
|
|
944
|
-
type: 'string',
|
|
945
|
-
description: 'log level: error, warn, info, debug (default: warn)',
|
|
946
|
-
},
|
|
947
|
-
s3: {
|
|
948
|
-
type: 'boolean',
|
|
949
|
-
description: 'also start a local s3-compatible server',
|
|
950
|
-
default: false,
|
|
951
|
-
},
|
|
952
|
-
's3-port': {
|
|
953
|
-
type: 'string',
|
|
954
|
-
description: 's3 server port',
|
|
955
|
-
default: '9200',
|
|
956
|
-
},
|
|
957
|
-
'disable-wasm-sqlite': {
|
|
958
|
-
type: 'boolean',
|
|
959
|
-
description: 'force native @rocicorp/zero-sqlite3 (fails if not available)',
|
|
960
|
-
default: false,
|
|
961
|
-
},
|
|
962
|
-
'force-wasm-sqlite': {
|
|
963
|
-
type: 'boolean',
|
|
964
|
-
description: 'force wasm bedrock-sqlite even if native is available',
|
|
965
|
-
default: false,
|
|
966
|
-
},
|
|
967
|
-
'no-worker-threads': {
|
|
968
|
-
type: 'boolean',
|
|
969
|
-
description: 'run pglite in-process instead of worker threads',
|
|
970
|
-
default: false,
|
|
971
|
-
},
|
|
972
|
-
'single-db': {
|
|
973
|
-
type: 'boolean',
|
|
974
|
-
description:
|
|
975
|
-
'use a single pglite instance for all databases (lighter for constrained environments)',
|
|
976
|
-
default: false,
|
|
977
|
-
},
|
|
978
|
-
'read-replicas': {
|
|
979
|
-
type: 'string',
|
|
980
|
-
description:
|
|
981
|
-
'number of pglite read replicas for postgres (0 to disable, default: auto)',
|
|
982
|
-
default: '',
|
|
983
|
-
},
|
|
984
|
-
'on-db-ready': {
|
|
985
|
-
type: 'string',
|
|
986
|
-
description: 'command to run after db+proxy are ready, before zero-cache starts',
|
|
987
|
-
default: '',
|
|
988
|
-
},
|
|
989
|
-
'on-healthy': {
|
|
990
|
-
type: 'string',
|
|
991
|
-
description: 'command to run once all services are healthy',
|
|
992
|
-
default: '',
|
|
993
|
-
},
|
|
994
|
-
'disable-admin': {
|
|
995
|
-
type: 'boolean',
|
|
996
|
-
description: 'disable admin dashboard',
|
|
997
|
-
default: false,
|
|
998
|
-
},
|
|
999
|
-
'admin-port': {
|
|
1000
|
-
type: 'string',
|
|
1001
|
-
description: 'admin dashboard port',
|
|
1002
|
-
default: '6477',
|
|
1003
|
-
},
|
|
1004
|
-
'checkpoint-interval': {
|
|
1005
|
-
type: 'string',
|
|
1006
|
-
description: 'WAL checkpoint interval in seconds (0 to disable, default: 300)',
|
|
1007
|
-
default: '',
|
|
1008
|
-
},
|
|
1009
|
-
'max-log-size': {
|
|
1010
|
-
type: 'string',
|
|
1011
|
-
description: 'max log file size in MB before rotation (default: 2)',
|
|
1012
|
-
default: '',
|
|
1013
|
-
},
|
|
1014
|
-
'disable-disk-logs': {
|
|
1015
|
-
type: 'boolean',
|
|
1016
|
-
description: 'disable writing logs to disk',
|
|
1017
|
-
default: false,
|
|
1018
|
-
},
|
|
1019
|
-
},
|
|
1020
|
-
subCommands: {
|
|
1021
|
-
s3: s3Command,
|
|
1022
|
-
pg_dump: pgDumpCommand,
|
|
1023
|
-
pg_restore: pgRestoreCommand,
|
|
1024
|
-
},
|
|
1025
|
-
async run({ args }) {
|
|
1026
|
-
// load orez.config.ts/js/mjs if present
|
|
1027
|
-
const fileConfig = await loadConfigFile()
|
|
1028
|
-
|
|
1029
|
-
// build cli overrides — only include values the user actually passed
|
|
1030
|
-
// (citty fills defaults for all args, so we detect "user passed" by
|
|
1031
|
-
// comparing against the declared defaults above)
|
|
1032
|
-
const cliDefaults: Record<string, unknown> = {
|
|
1033
|
-
'pg-port': '6434',
|
|
1034
|
-
'zero-port': '5849',
|
|
1035
|
-
'data-dir': '.orez',
|
|
1036
|
-
migrations: '',
|
|
1037
|
-
seed: '',
|
|
1038
|
-
'pg-user': 'user',
|
|
1039
|
-
'pg-password': 'password',
|
|
1040
|
-
'skip-zero-cache': false,
|
|
1041
|
-
'log-level': undefined,
|
|
1042
|
-
s3: false,
|
|
1043
|
-
's3-port': '9200',
|
|
1044
|
-
'disable-wasm-sqlite': false,
|
|
1045
|
-
'force-wasm-sqlite': false,
|
|
1046
|
-
'no-worker-threads': false,
|
|
1047
|
-
'single-db': false,
|
|
1048
|
-
'read-replicas': '',
|
|
1049
|
-
'on-db-ready': '',
|
|
1050
|
-
'on-healthy': '',
|
|
1051
|
-
'disable-admin': false,
|
|
1052
|
-
'admin-port': '6477',
|
|
1053
|
-
'checkpoint-interval': '',
|
|
1054
|
-
'max-log-size': '',
|
|
1055
|
-
'disable-disk-logs': false,
|
|
1056
|
-
}
|
|
1057
|
-
const wasSet = (key: string) => {
|
|
1058
|
-
const val = (args as Record<string, unknown>)[key]
|
|
1059
|
-
const def = cliDefaults[key]
|
|
1060
|
-
return val !== def
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
|
-
const cliOverrides = resolveOrezConfig(fileConfig, {
|
|
1064
|
-
...(wasSet('pg-port') && { pgPort: Number(args['pg-port']) }),
|
|
1065
|
-
...(wasSet('zero-port') && { zeroPort: Number(args['zero-port']) }),
|
|
1066
|
-
...(wasSet('admin-port') && { adminPort: Number(args['admin-port']) }),
|
|
1067
|
-
...(wasSet('data-dir') && { dataDir: args['data-dir'] }),
|
|
1068
|
-
...(wasSet('migrations') && { migrations: args.migrations }),
|
|
1069
|
-
...(wasSet('seed') && { seed: args.seed }),
|
|
1070
|
-
...(wasSet('pg-user') && { pgUser: args['pg-user'] }),
|
|
1071
|
-
...(wasSet('pg-password') && { pgPassword: args['pg-password'] }),
|
|
1072
|
-
...(wasSet('skip-zero-cache') && { skipZeroCache: args['skip-zero-cache'] }),
|
|
1073
|
-
...(wasSet('log-level') && {
|
|
1074
|
-
logLevel: args['log-level'] as 'error' | 'warn' | 'info' | 'debug',
|
|
1075
|
-
}),
|
|
1076
|
-
...(wasSet('s3') && { s3: args.s3 }),
|
|
1077
|
-
...(wasSet('s3-port') && { s3Port: Number(args['s3-port']) }),
|
|
1078
|
-
...(wasSet('disable-wasm-sqlite') && {
|
|
1079
|
-
disableWasmSqlite: args['disable-wasm-sqlite'],
|
|
1080
|
-
}),
|
|
1081
|
-
...(wasSet('force-wasm-sqlite') && { forceWasmSqlite: args['force-wasm-sqlite'] }),
|
|
1082
|
-
...(wasSet('no-worker-threads') && { noWorkerThreads: args['no-worker-threads'] }),
|
|
1083
|
-
...(wasSet('single-db') && { singleDb: args['single-db'] }),
|
|
1084
|
-
...(wasSet('read-replicas') && { readReplicas: Number(args['read-replicas']) }),
|
|
1085
|
-
...(wasSet('on-db-ready') && { onDbReady: args['on-db-ready'] }),
|
|
1086
|
-
...(wasSet('on-healthy') && { onHealthy: args['on-healthy'] }),
|
|
1087
|
-
...(wasSet('disable-admin') && { disableAdmin: args['disable-admin'] }),
|
|
1088
|
-
...(wasSet('checkpoint-interval') && {
|
|
1089
|
-
checkpointIntervalMs: Number(args['checkpoint-interval']) * 1000,
|
|
1090
|
-
}),
|
|
1091
|
-
...(wasSet('max-log-size') && {
|
|
1092
|
-
maxLogFileSize: Number(args['max-log-size']) * 1024 * 1024,
|
|
1093
|
-
}),
|
|
1094
|
-
...(wasSet('disable-disk-logs') && { disableDiskLogs: args['disable-disk-logs'] }),
|
|
1095
|
-
})
|
|
1096
|
-
|
|
1097
|
-
// resolve aliases and compute final values
|
|
1098
|
-
const resolvedMigrations = cliOverrides.migrations ?? cliOverrides.migrationsDir ?? ''
|
|
1099
|
-
const resolvedSeed = cliOverrides.seed ?? cliOverrides.seedFile ?? ''
|
|
1100
|
-
const resolvedUseWorkerThreads =
|
|
1101
|
-
cliOverrides.noWorkerThreads != null
|
|
1102
|
-
? !cliOverrides.noWorkerThreads
|
|
1103
|
-
: (cliOverrides.useWorkerThreads ?? true)
|
|
1104
|
-
const resolvedDisableAdmin = cliOverrides.disableAdmin ?? false
|
|
1105
|
-
const resolvedAdminPort = resolvedDisableAdmin ? 0 : (cliOverrides.adminPort ?? 6477)
|
|
1106
|
-
|
|
1107
|
-
const {
|
|
1108
|
-
config,
|
|
1109
|
-
stop,
|
|
1110
|
-
instances,
|
|
1111
|
-
zeroEnv,
|
|
1112
|
-
logStore,
|
|
1113
|
-
httpLog,
|
|
1114
|
-
restartZero,
|
|
1115
|
-
stopZero,
|
|
1116
|
-
resetZero,
|
|
1117
|
-
resetZeroFull,
|
|
1118
|
-
} = await startZeroLite({
|
|
1119
|
-
pgPort: cliOverrides.pgPort,
|
|
1120
|
-
zeroPort: cliOverrides.zeroPort,
|
|
1121
|
-
adminPort: resolvedAdminPort,
|
|
1122
|
-
dataDir: cliOverrides.dataDir,
|
|
1123
|
-
migrationsDir: resolvedMigrations,
|
|
1124
|
-
seedFile: resolvedSeed,
|
|
1125
|
-
pgUser: cliOverrides.pgUser,
|
|
1126
|
-
pgPassword: cliOverrides.pgPassword,
|
|
1127
|
-
skipZeroCache: cliOverrides.skipZeroCache,
|
|
1128
|
-
disableWasmSqlite: cliOverrides.disableWasmSqlite,
|
|
1129
|
-
forceWasmSqlite: cliOverrides.forceWasmSqlite,
|
|
1130
|
-
useWorkerThreads: resolvedUseWorkerThreads,
|
|
1131
|
-
singleDb: cliOverrides.singleDb,
|
|
1132
|
-
readReplicas: cliOverrides.readReplicas,
|
|
1133
|
-
logLevel: cliOverrides.logLevel,
|
|
1134
|
-
onDbReady: cliOverrides.onDbReady || undefined,
|
|
1135
|
-
onHealthy: cliOverrides.onHealthy || undefined,
|
|
1136
|
-
pgliteOptions: cliOverrides.pgliteOptions,
|
|
1137
|
-
zeroPublications: cliOverrides.zeroPublications,
|
|
1138
|
-
zeroMutateUrl: cliOverrides.zeroMutateUrl,
|
|
1139
|
-
zeroQueryUrl: cliOverrides.zeroQueryUrl,
|
|
1140
|
-
checkpointIntervalMs: cliOverrides.checkpointIntervalMs,
|
|
1141
|
-
maxLogFileSize: cliOverrides.maxLogFileSize,
|
|
1142
|
-
disableDiskLogs: cliOverrides.disableDiskLogs,
|
|
1143
|
-
})
|
|
1144
|
-
|
|
1145
|
-
const s3Enabled = cliOverrides.s3 ?? false
|
|
1146
|
-
let s3Server: import('node:http').Server | null = null
|
|
1147
|
-
if (s3Enabled) {
|
|
1148
|
-
const { startS3Local } = await import('./s3-local.js')
|
|
1149
|
-
s3Server = await startS3Local({
|
|
1150
|
-
port: cliOverrides.s3Port ?? 9200,
|
|
1151
|
-
dataDir: config.dataDir,
|
|
1152
|
-
})
|
|
1153
|
-
}
|
|
1154
|
-
|
|
1155
|
-
let adminServer: import('node:http').Server | null = null
|
|
1156
|
-
if (!resolvedDisableAdmin && logStore && zeroEnv) {
|
|
1157
|
-
const { startAdminServer } = await import('./admin/server.js')
|
|
1158
|
-
adminServer = await startAdminServer({
|
|
1159
|
-
port: config.adminPort,
|
|
1160
|
-
logStore,
|
|
1161
|
-
httpLog,
|
|
1162
|
-
config,
|
|
1163
|
-
zeroEnv,
|
|
1164
|
-
actions: { restartZero, stopZero, resetZero, resetZeroFull },
|
|
1165
|
-
startTime: Date.now(),
|
|
1166
|
-
db: instances,
|
|
1167
|
-
})
|
|
1168
|
-
log.orez(`admin: ${url(`http://localhost:${config.adminPort}`)}`)
|
|
1169
|
-
}
|
|
1170
|
-
|
|
1171
|
-
log.pg(
|
|
1172
|
-
`ready ${url(`postgresql://${config.pgUser}:${config.pgPassword}@127.0.0.1:${config.pgPort}/postgres`)}`
|
|
1173
|
-
)
|
|
1174
|
-
|
|
1175
|
-
let stopping = false
|
|
1176
|
-
const shutdown = async (reason: string, exitCode = 0) => {
|
|
1177
|
-
if (stopping) return
|
|
1178
|
-
stopping = true
|
|
1179
|
-
log.debug.orez(`shutdown requested: ${reason}`)
|
|
1180
|
-
adminServer?.close()
|
|
1181
|
-
s3Server?.close()
|
|
1182
|
-
await stop()
|
|
1183
|
-
process.exit(exitCode)
|
|
1184
|
-
}
|
|
1185
|
-
|
|
1186
|
-
process.on('SIGINT', () => shutdown('SIGINT'))
|
|
1187
|
-
process.on('SIGTERM', () => shutdown('SIGTERM'))
|
|
1188
|
-
|
|
1189
|
-
// handle crashes - try to clean up so next startup isn't corrupted
|
|
1190
|
-
// EPIPE/ECONNRESET are transient socket errors (e.g. client disconnect) — not crashes
|
|
1191
|
-
const isSocketError = (err: unknown): boolean => {
|
|
1192
|
-
const code = (err as NodeJS.ErrnoException)?.code
|
|
1193
|
-
if (code === 'EPIPE' || code === 'ECONNRESET') return true
|
|
1194
|
-
const msg = err instanceof Error ? err.message : String(err)
|
|
1195
|
-
return msg.includes('ended by the other party')
|
|
1196
|
-
}
|
|
1197
|
-
process.on('uncaughtException', async (err) => {
|
|
1198
|
-
if (isSocketError(err)) return
|
|
1199
|
-
log.orez(`uncaught exception: ${err.message}`)
|
|
1200
|
-
await shutdown('uncaughtException', 1)
|
|
1201
|
-
})
|
|
1202
|
-
process.on('unhandledRejection', async (reason) => {
|
|
1203
|
-
if (isSocketError(reason)) return
|
|
1204
|
-
log.orez(`unhandled rejection: ${reason}`)
|
|
1205
|
-
await shutdown('unhandledRejection', 1)
|
|
1206
|
-
})
|
|
1207
|
-
},
|
|
1208
|
-
})
|
|
1209
|
-
|
|
1210
|
-
// note: runMain is invoked from cli-entry.ts, not here. attempts to detect
|
|
1211
|
-
// "is this the entry" via import.meta.main / process.argv[1] are fragile
|
|
1212
|
-
// because bin symlinks land argv[1] at ".../.bin/orez" and nested imports
|
|
1213
|
-
// flip import.meta.main. keeping the call in cli-entry (a dedicated entry)
|
|
1214
|
-
// is the only reliable signal.
|