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