orez 0.2.27 → 0.2.30

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 (157) hide show
  1. package/dist/cf-do/worker.d.ts +3 -0
  2. package/dist/cf-do/worker.d.ts.map +1 -1
  3. package/dist/cf-do/worker.js +37 -15
  4. package/dist/cf-do/worker.js.map +1 -1
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +8 -0
  7. package/dist/index.js.map +1 -1
  8. package/package.json +3 -4
  9. package/src/admin/admin-data.test.ts +0 -348
  10. package/src/admin/http-proxy.ts +0 -252
  11. package/src/admin/log-store.ts +0 -192
  12. package/src/admin/server.ts +0 -471
  13. package/src/admin/ui.ts +0 -1322
  14. package/src/bench/proxy-throughput.bench.ts +0 -343
  15. package/src/bench/serial-mutations.bench.ts +0 -270
  16. package/src/browser.ts +0 -203
  17. package/src/cf-do/.wrangler/cache/cf.json +0 -1
  18. package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite +0 -0
  19. package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-shm +0 -0
  20. package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-wal +0 -0
  21. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/0ffaabee41a60e04dd0eb7db3073f0a40139e6a97ccd26823967acb652b89a7b.sqlite +0 -0
  22. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite +0 -0
  23. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite-shm +0 -0
  24. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite-wal +0 -0
  25. package/src/cf-do/.wrangler/tmp/bundle-0z4CpE/middleware-insertion-facade.js +0 -11
  26. package/src/cf-do/.wrangler/tmp/bundle-0z4CpE/middleware-loader.entry.ts +0 -134
  27. package/src/cf-do/.wrangler/tmp/bundle-vYmw0E/middleware-insertion-facade.js +0 -11
  28. package/src/cf-do/.wrangler/tmp/bundle-vYmw0E/middleware-loader.entry.ts +0 -134
  29. package/src/cf-do/.wrangler/tmp/dev-cbILNo/worker.js +0 -1059
  30. package/src/cf-do/.wrangler/tmp/dev-cbILNo/worker.js.map +0 -8
  31. package/src/cf-do/.wrangler/tmp/dev-qbho19/worker.js +0 -1059
  32. package/src/cf-do/.wrangler/tmp/dev-qbho19/worker.js.map +0 -8
  33. package/src/cf-do/ARCHITECTURE.md +0 -93
  34. package/src/cf-do/CHAT_E2E.md +0 -213
  35. package/src/cf-do/watermark.test.ts +0 -103
  36. package/src/cf-do/watermark.ts +0 -118
  37. package/src/cf-do/worker.ts +0 -1041
  38. package/src/cf-do/wrangler.toml +0 -11
  39. package/src/cf-pglite/README.md +0 -19
  40. package/src/change-tracking.ts +0 -25
  41. package/src/child-process.test.ts +0 -147
  42. package/src/child-process.ts +0 -90
  43. package/src/cli-entry.ts +0 -72
  44. package/src/cli.test.ts +0 -40
  45. package/src/cli.ts +0 -1214
  46. package/src/config.ts +0 -150
  47. package/src/do-sql-tracking.test.ts +0 -19
  48. package/src/do-sql-tracking.ts +0 -19
  49. package/src/index.ts +0 -1215
  50. package/src/integration/integration.test.ts +0 -517
  51. package/src/integration/native-binary.guard.test.ts +0 -13
  52. package/src/integration/native-startup.test.ts +0 -44
  53. package/src/integration/replication-latency.test.ts +0 -428
  54. package/src/integration/restore-live-stress.test.ts +0 -433
  55. package/src/integration/restore-reset.test.ts +0 -400
  56. package/src/integration/restore.test.ts +0 -274
  57. package/src/integration/test-permissions.ts +0 -147
  58. package/src/load-config.ts +0 -46
  59. package/src/log.ts +0 -96
  60. package/src/mutex.ts +0 -47
  61. package/src/pg-proxy-browser.singledb.test.ts +0 -233
  62. package/src/pg-proxy-browser.ts +0 -2022
  63. package/src/pg-proxy-do-backend.test.ts +0 -3890
  64. package/src/pg-proxy-do-backend.ts +0 -7191
  65. package/src/pg-proxy.ts +0 -1087
  66. package/src/pg-sqlite-compiler/README.md +0 -53
  67. package/src/pg-sqlite-compiler/catalog/seed.ts +0 -524
  68. package/src/pg-sqlite-compiler/fixtures/pgsqlite/arithmetic.json +0 -307
  69. package/src/pg-sqlite-compiler/fixtures/pgsqlite/array.json +0 -377
  70. package/src/pg-sqlite-compiler/fixtures/pgsqlite/cast.json +0 -12
  71. package/src/pg-sqlite-compiler/fixtures/pgsqlite/catalog.json +0 -447
  72. package/src/pg-sqlite-compiler/fixtures/pgsqlite/create-table.json +0 -32
  73. package/src/pg-sqlite-compiler/fixtures/pgsqlite/datetime.json +0 -397
  74. package/src/pg-sqlite-compiler/fixtures/pgsqlite/enum.json +0 -337
  75. package/src/pg-sqlite-compiler/fixtures/pgsqlite/insert.json +0 -337
  76. package/src/pg-sqlite-compiler/fixtures/pgsqlite/json.json +0 -537
  77. package/src/pg-sqlite-compiler/fixtures/pgsqlite/misc.json +0 -1837
  78. package/src/pg-sqlite-compiler/index.ts +0 -73
  79. package/src/pg-sqlite-compiler/integration.test.ts +0 -136
  80. package/src/pg-sqlite-compiler/passes/ast-utils.ts +0 -113
  81. package/src/pg-sqlite-compiler/passes/catalog.ts +0 -65
  82. package/src/pg-sqlite-compiler/passes/datetime.ts +0 -74
  83. package/src/pg-sqlite-compiler/passes/index.ts +0 -49
  84. package/src/pg-sqlite-compiler/passes/types.ts +0 -156
  85. package/src/pg-sqlite-compiler/smoke.test.ts +0 -69
  86. package/src/pg-sqlite-compiler/test/catalog.test.ts +0 -171
  87. package/src/pg-sqlite-compiler/test/corpus.test.ts +0 -161
  88. package/src/pg-sqlite-compiler/test/datetime.oracle.test.ts +0 -102
  89. package/src/pg-sqlite-compiler/test/oracle.ts +0 -237
  90. package/src/pg-sqlite-compiler/test/types.test.ts +0 -109
  91. package/src/pg-sqlite-compiler/types.ts +0 -63
  92. package/src/pglite-ipc.test.ts +0 -116
  93. package/src/pglite-ipc.ts +0 -266
  94. package/src/pglite-manager.ts +0 -557
  95. package/src/pglite-web-proxy.test.ts +0 -57
  96. package/src/pglite-web-proxy.ts +0 -221
  97. package/src/pglite-web-worker.ts +0 -152
  98. package/src/pglite-worker-thread.ts +0 -253
  99. package/src/port.ts +0 -25
  100. package/src/process-title.ts +0 -9
  101. package/src/recovery.ts +0 -155
  102. package/src/replication/change-tracker.test.ts +0 -357
  103. package/src/replication/change-tracker.ts +0 -279
  104. package/src/replication/handler.test.ts +0 -511
  105. package/src/replication/handler.ts +0 -1190
  106. package/src/replication/pgoutput-encoder.test.ts +0 -697
  107. package/src/replication/pgoutput-encoder.ts +0 -373
  108. package/src/replication/tcp-replication.test.ts +0 -876
  109. package/src/replication/zero-compat.test.ts +0 -1150
  110. package/src/restore-stress.test.ts +0 -188
  111. package/src/s3-local.ts +0 -203
  112. package/src/shim/hooks.mjs +0 -120
  113. package/src/shim/register.mjs +0 -4
  114. package/src/sqlite-mode/apply-mode.ts +0 -224
  115. package/src/sqlite-mode/index.ts +0 -15
  116. package/src/sqlite-mode/native-binary.ts +0 -89
  117. package/src/sqlite-mode/package-resolve.ts +0 -17
  118. package/src/sqlite-mode/resolve-mode.ts +0 -80
  119. package/src/sqlite-mode/shim-template.ts +0 -159
  120. package/src/sqlite-mode/sqlite-mode.test.ts +0 -427
  121. package/src/sqlite-mode/types.ts +0 -30
  122. package/src/vite-plugin.ts +0 -67
  123. package/src/wasm-sqlite.test.ts +0 -537
  124. package/src/worker/browser-admin.ts +0 -52
  125. package/src/worker/browser-build-config.test.ts +0 -71
  126. package/src/worker/browser-build-config.ts +0 -109
  127. package/src/worker/browser-embed-admin.test.ts +0 -75
  128. package/src/worker/browser-embed.ts +0 -345
  129. package/src/worker/cf-patches.ts +0 -384
  130. package/src/worker/embed-integration.test.ts +0 -321
  131. package/src/worker/index.ts +0 -138
  132. package/src/worker/shims/fastify.test.ts +0 -255
  133. package/src/worker/shims/fastify.ts +0 -306
  134. package/src/worker/shims/http-service.test.ts +0 -355
  135. package/src/worker/shims/http-service.ts +0 -293
  136. package/src/worker/shims/node-stub.ts +0 -290
  137. package/src/worker/shims/oxfmt.ts +0 -3
  138. package/src/worker/shims/postgres-browser.ts +0 -59
  139. package/src/worker/shims/postgres-socket.test.ts +0 -576
  140. package/src/worker/shims/postgres-socket.ts +0 -310
  141. package/src/worker/shims/postgres.test.ts +0 -364
  142. package/src/worker/shims/postgres.ts +0 -1454
  143. package/src/worker/shims/sqlite-browser.test.ts +0 -233
  144. package/src/worker/shims/sqlite-browser.ts +0 -175
  145. package/src/worker/shims/sqlite.test.ts +0 -786
  146. package/src/worker/shims/sqlite.ts +0 -978
  147. package/src/worker/shims/stream-browser.ts +0 -15
  148. package/src/worker/shims/ws-browser.test.ts +0 -205
  149. package/src/worker/shims/ws-browser.ts +0 -248
  150. package/src/worker/shims/ws.test.ts +0 -288
  151. package/src/worker/shims/ws.ts +0 -467
  152. package/src/worker/shims/zero-process-env.ts +0 -11
  153. package/src/worker/types.ts +0 -75
  154. package/src/worker/worker-integration.test.ts +0 -223
  155. package/src/worker/worker.test.ts +0 -136
  156. package/src/worker/zero-cache-embed-cf.ts +0 -463
  157. 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.