orez 0.0.47 → 0.0.49

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 (65) hide show
  1. package/dist/admin/http-proxy.d.ts.map +1 -1
  2. package/dist/admin/http-proxy.js.map +1 -1
  3. package/dist/admin/log-store.d.ts.map +1 -1
  4. package/dist/admin/log-store.js.map +1 -1
  5. package/dist/admin/server.d.ts +2 -2
  6. package/dist/admin/server.d.ts.map +1 -1
  7. package/dist/admin/server.js.map +1 -1
  8. package/dist/admin/ui.d.ts.map +1 -1
  9. package/dist/admin/ui.js +2 -2
  10. package/dist/admin/ui.js.map +1 -1
  11. package/dist/cli.d.ts.map +1 -1
  12. package/dist/cli.js +6 -112
  13. package/dist/cli.js.map +1 -1
  14. package/dist/config.d.ts +0 -5
  15. package/dist/config.d.ts.map +1 -1
  16. package/dist/config.js +0 -5
  17. package/dist/config.js.map +1 -1
  18. package/dist/index.d.ts +0 -9
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +91 -249
  21. package/dist/index.js.map +1 -1
  22. package/dist/log.d.ts +0 -9
  23. package/dist/log.d.ts.map +1 -1
  24. package/dist/log.js +1 -24
  25. package/dist/log.js.map +1 -1
  26. package/dist/mutex.d.ts.map +1 -1
  27. package/dist/mutex.js +2 -13
  28. package/dist/mutex.js.map +1 -1
  29. package/dist/pg-proxy.d.ts +2 -3
  30. package/dist/pg-proxy.d.ts.map +1 -1
  31. package/dist/pg-proxy.js +167 -377
  32. package/dist/pg-proxy.js.map +1 -1
  33. package/dist/pglite-manager.d.ts +0 -1
  34. package/dist/pglite-manager.d.ts.map +1 -1
  35. package/dist/pglite-manager.js +1 -1
  36. package/dist/pglite-manager.js.map +1 -1
  37. package/dist/replication/change-tracker.d.ts +0 -6
  38. package/dist/replication/change-tracker.d.ts.map +1 -1
  39. package/dist/replication/change-tracker.js +0 -74
  40. package/dist/replication/change-tracker.js.map +1 -1
  41. package/dist/replication/handler.d.ts.map +1 -1
  42. package/dist/replication/handler.js +5 -47
  43. package/dist/replication/handler.js.map +1 -1
  44. package/dist/vite-plugin.d.ts +0 -3
  45. package/dist/vite-plugin.d.ts.map +1 -1
  46. package/dist/vite-plugin.js +0 -24
  47. package/dist/vite-plugin.js.map +1 -1
  48. package/package.json +5 -4
  49. package/src/admin/http-proxy.ts +5 -1
  50. package/src/admin/log-store.ts +4 -1
  51. package/src/admin/server.ts +7 -3
  52. package/src/admin/ui.ts +682 -680
  53. package/src/cli.ts +6 -111
  54. package/src/config.ts +0 -10
  55. package/src/index.ts +92 -262
  56. package/src/integration/integration.test.ts +264 -133
  57. package/src/log.ts +1 -25
  58. package/src/mutex.ts +2 -12
  59. package/src/pg-proxy.ts +187 -449
  60. package/src/pglite-manager.ts +1 -1
  61. package/src/replication/change-tracker.ts +0 -92
  62. package/src/replication/handler.ts +4 -50
  63. package/src/shim/hooks.mjs +34 -1
  64. package/src/vite-plugin.ts +0 -28
  65. package/src/wasm-sqlite.test.ts +1 -2
package/src/index.ts CHANGED
@@ -7,23 +7,19 @@
7
7
  */
8
8
 
9
9
  import { spawn, type ChildProcess } from 'node:child_process'
10
- import { existsSync, mkdirSync, readFileSync, rmSync, unlinkSync, writeFileSync } from 'node:fs'
10
+ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs'
11
11
  import { createRequire } from 'node:module'
12
- import { totalmem } from 'node:os'
13
12
  import { dirname, resolve } from 'node:path'
14
- import { fileURLToPath } from 'node:url'
15
13
 
16
14
  import { getConfig, getConnectionString } from './config.js'
17
- import { log, port, setLogLevel, addLogListener } from './log.js'
15
+ import { log, port, setLogLevel } from './log.js'
18
16
  import { startPgProxy } from './pg-proxy.js'
19
- import { createInstance, createPGliteInstances, runMigrations } from './pglite-manager.js'
17
+ import { createPGliteInstances, runMigrations } from './pglite-manager.js'
20
18
  import { findPort } from './port.js'
21
19
  import { installChangeTracking } from './replication/change-tracker.js'
22
20
 
23
21
  import type { ZeroLiteConfig } from './config.js'
24
22
  import type { PGlite } from '@electric-sql/pglite'
25
- import type { LogStore } from './admin/log-store.js'
26
- import type { HttpLogStore } from './admin/http-proxy.js'
27
23
 
28
24
  export { getConfig, getConnectionString } from './config.js'
29
25
  export type { LogLevel, ZeroLiteConfig } from './config.js'
@@ -45,21 +41,6 @@ export async function startZeroLite(overrides: Partial<ZeroLiteConfig> = {}) {
45
41
  const config = getConfig(overrides)
46
42
  setLogLevel(config.logLevel)
47
43
 
48
- // when admin ui enabled, create log store and capture all log output
49
- const SOURCE_MAP: Record<string, string> = {
50
- 'orez': 'orez', 'pglite': 'pglite', 'pg-proxy': 'proxy',
51
- 'zero': 'zero', 'zero-cache': 'zero', 'orez/s3': 's3',
52
- }
53
- let logStore: LogStore | null = null
54
- let removeLogListener: (() => void) | null = null
55
- if (config.admin) {
56
- const { createLogStore } = await import('./admin/log-store.js')
57
- logStore = createLogStore(config.dataDir, config.adminLogs)
58
- removeLogListener = addLogListener((source, level, msg) => {
59
- logStore!.push(SOURCE_MAP[source] || source, level, msg)
60
- })
61
- }
62
-
63
44
  // find available ports
64
45
  const pgPort = await findPort(config.pgPort)
65
46
  const zeroPort = config.skipZeroCache
@@ -90,7 +71,7 @@ export async function startZeroLite(overrides: Partial<ZeroLiteConfig> = {}) {
90
71
  // start tcp proxy (routes connections to correct instance by database name)
91
72
  const pgServer = await startPgProxy(instances, config)
92
73
 
93
- log.pglite(`postgres up ${port(pgPort, 'green')}`)
74
+ log.orez(`db up ${port(pgPort, 'green')}`)
94
75
  if (migrationsApplied > 0)
95
76
  log.orez(
96
77
  `${migrationsApplied} migration${migrationsApplied === 1 ? '' : 's'} applied`
@@ -134,132 +115,21 @@ export async function startZeroLite(overrides: Partial<ZeroLiteConfig> = {}) {
134
115
  await installChangeTracking(db)
135
116
  }
136
117
 
137
- // run beforeZero callback (e.g. create tables before zero-cache starts)
138
- if (config.beforeZero) {
139
- log.debug.orez('running beforeZero callback')
140
- await config.beforeZero(db)
141
- // re-install change tracking on tables created by the callback
142
- await installChangeTracking(db)
143
- }
118
+ // clean up stale sqlite replica from previous runs
119
+ cleanupStaleReplica(config)
144
120
 
145
- // clean up stale lock files from previous crash (keep replica for fast restart)
146
- cleanupStaleLockFiles(config)
147
-
148
- // http proxy for admin traffic logging
149
- let httpLogStore: HttpLogStore | null = null
150
- let httpProxyServer: import('node:http').Server | null = null
151
- let zeroInternalPort = zeroPort
152
- if (config.admin && !config.skipZeroCache) {
153
- const { createHttpLogStore } = await import('./admin/http-proxy.js')
154
- httpLogStore = createHttpLogStore()
155
- zeroInternalPort = await findPort(zeroPort + 100)
156
- }
157
-
158
- // start zero-cache with auto-recovery for stale change db
121
+ // start zero-cache
159
122
  let zeroCacheProcess: ChildProcess | null = null
160
- let zeroEnv: Record<string, string> = {}
161
- const cdbResets = { count: 0, lastReset: 0 }
162
- const MAX_CDB_RESETS = 10
163
- const MIN_RESET_INTERVAL_MS = 60_000
164
-
165
123
  if (!config.skipZeroCache) {
166
- let currentResult = await startZeroCache(config, zeroInternalPort)
167
- zeroCacheProcess = currentResult.child
168
- zeroEnv = currentResult.env
169
-
170
- // watch for stale changeLog crashes and auto-recover
171
- const attachCdbRecovery = (result: typeof currentResult) => {
172
- result.child.on('exit', async (code) => {
173
- if (code === 0 || code === null) return
174
- if (!result.stderrBuf.includes('changeLog_pkey')) return
175
-
176
- const now = Date.now()
177
- if (cdbResets.count >= MAX_CDB_RESETS) {
178
- log.zero('change db reset limit reached, not retrying')
179
- return
180
- }
181
- const elapsed = now - cdbResets.lastReset
182
- if (elapsed < MIN_RESET_INTERVAL_MS) {
183
- log.zero(`change db reset too soon (${Math.round(elapsed / 1000)}s ago), not retrying`)
184
- return
185
- }
186
-
187
- cdbResets.count++
188
- cdbResets.lastReset = now
189
- log.zero(`stale change db detected, resetting (${cdbResets.count}/${MAX_CDB_RESETS})`)
190
-
191
- try {
192
- await instances.cdb.close()
193
- const cdbPath = resolve(config.dataDir, 'pgdata-cdb')
194
- rmSync(cdbPath, { recursive: true, force: true })
195
- instances.cdb = await createInstance(config, 'cdb', false)
196
-
197
- currentResult = await startZeroCache(config, zeroInternalPort)
198
- zeroCacheProcess = currentResult.child
199
- attachCdbRecovery(currentResult)
200
- await waitForZeroCache(config, undefined, zeroInternalPort)
201
- log.zero(`recovered, ready ${port(config.zeroPort, 'magenta')}`)
202
- } catch (err) {
203
- log.zero(`recovery failed: ${err}`)
204
- }
205
- })
206
- }
207
-
208
- attachCdbRecovery(currentResult)
209
- await waitForZeroCache(config, undefined, zeroInternalPort)
124
+ zeroCacheProcess = await startZeroCache(config)
125
+ await waitForZeroCache(config)
210
126
  log.zero(`ready ${port(config.zeroPort, 'magenta')}`)
211
-
212
- // start http proxy for admin traffic logging
213
- if (httpLogStore) {
214
- const { startHttpProxy } = await import('./admin/http-proxy.js')
215
- httpProxyServer = await startHttpProxy({
216
- listenPort: zeroPort,
217
- targetPort: zeroInternalPort,
218
- httpLog: httpLogStore,
219
- })
220
- }
221
127
  } else {
222
128
  log.orez('skip zero-cache')
223
129
  }
224
130
 
225
- // admin action handlers
226
- const actions = {
227
- restartZero: config.skipZeroCache ? undefined : async () => {
228
- if (zeroCacheProcess && !zeroCacheProcess.killed) {
229
- zeroCacheProcess.kill('SIGTERM')
230
- await new Promise<void>((r) => {
231
- const t = setTimeout(() => { zeroCacheProcess?.kill('SIGKILL'); r() }, 3000)
232
- zeroCacheProcess!.on('exit', () => { clearTimeout(t); r() })
233
- })
234
- }
235
- const zc = await startZeroCache(config, zeroInternalPort)
236
- zeroCacheProcess = zc.child
237
- await waitForZeroCache(config, undefined, zeroInternalPort)
238
- log.zero(`restarted ${port(config.zeroPort, 'magenta')}`)
239
- },
240
- resetZero: config.skipZeroCache ? undefined : async () => {
241
- if (zeroCacheProcess && !zeroCacheProcess.killed) {
242
- zeroCacheProcess.kill('SIGTERM')
243
- await new Promise<void>((r) => {
244
- const t = setTimeout(() => { zeroCacheProcess?.kill('SIGKILL'); r() }, 3000)
245
- zeroCacheProcess!.on('exit', () => { clearTimeout(t); r() })
246
- })
247
- }
248
- const replicaPath = resolve(config.dataDir, 'zero-replica.db')
249
- for (const suffix of ['', '-wal', '-shm', '-wal2']) {
250
- try { if (existsSync(replicaPath + suffix)) unlinkSync(replicaPath + suffix) } catch {}
251
- }
252
- const zc = await startZeroCache(config, zeroInternalPort)
253
- zeroCacheProcess = zc.child
254
- await waitForZeroCache(config, undefined, zeroInternalPort)
255
- log.zero(`reset and restarted ${port(config.zeroPort, 'magenta')}`)
256
- },
257
- }
258
-
259
131
  const stop = async () => {
260
132
  log.debug.orez('shutting down')
261
- removeLogListener?.()
262
- httpProxyServer?.close()
263
133
  if (zeroCacheProcess && !zeroCacheProcess.killed) {
264
134
  zeroCacheProcess.kill('SIGTERM')
265
135
  // wait up to 3s for graceful exit, then force kill
@@ -285,21 +155,20 @@ export async function startZeroLite(overrides: Partial<ZeroLiteConfig> = {}) {
285
155
  log.debug.orez('stopped')
286
156
  }
287
157
 
288
- return { config, stop, db, instances, pgPort: config.pgPort, zeroPort: config.zeroPort, logStore, zeroEnv, actions, httpLogStore }
158
+ return { config, stop, db, instances, pgPort: config.pgPort, zeroPort: config.zeroPort }
289
159
  }
290
160
 
291
- function cleanupStaleLockFiles(config: ZeroLiteConfig): void {
161
+ function cleanupStaleReplica(config: ZeroLiteConfig): void {
292
162
  const replicaPath = resolve(config.dataDir, 'zero-replica.db')
293
- // only delete lock/wal files that prevent zero-cache from starting after a crash.
294
- // keep the replica db itself zero-cache catches up via replication, which is
295
- // nearly instant vs a full initial sync (COPY of all tables). if the replica is
296
- // too stale, ZERO_AUTO_RESET=true makes zero-cache wipe and resync automatically.
297
- for (const suffix of ['-wal', '-shm', '-wal2']) {
163
+ // delete replica + all lock/wal files so zero-cache does a fresh sync
164
+ // the replica is just a cache of pglite data, safe to recreate
165
+ for (const suffix of ['', '-wal', '-shm', '-wal2']) {
298
166
  const file = replicaPath + suffix
299
167
  try {
300
168
  if (existsSync(file)) {
301
169
  unlinkSync(file)
302
- log.debug.orez(`cleaned up stale ${suffix} file`)
170
+ if (suffix) log.debug.orez(`cleaned up stale ${suffix} file`)
171
+ else log.debug.orez('cleaned up stale replica (will re-sync)')
303
172
  }
304
173
  } catch {
305
174
  // ignore
@@ -339,32 +208,71 @@ async function seedIfNeeded(db: PGlite, config: ZeroLiteConfig): Promise<void> {
339
208
  log.orez('seeded')
340
209
  }
341
210
 
342
- // write esm loader hooks to tmpdir that intercept @rocicorp/zero-sqlite3
343
- // and redirect to bedrock-sqlite wasm. templates live in src/shim/.
344
- // returns the path to register.mjs (passed via --import in NODE_OPTIONS).
211
+ // create a fake @rocicorp/zero-sqlite3 package in tmpdir that redirects to
212
+ // bedrock-sqlite (wasm). uses NODE_PATH to make node resolve our shim first —
213
+ // no require hooks, no Module._resolveFilename monkey-patching, no .cjs files
214
+ // in the package (which all break vite).
345
215
  function writeSqliteShim(): string {
346
216
  const tmp = process.env.TMPDIR || process.env.TEMP || '/tmp'
347
- const dir = resolve(tmp, 'orez-sqlite')
217
+ const dir = resolve(tmp, 'orez-sqlite', 'node_modules', '@rocicorp', 'zero-sqlite3')
348
218
  mkdirSync(dir, { recursive: true })
349
219
 
350
220
  const bedrockEntry = resolvePackage('bedrock-sqlite')
351
- const shimDir = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'src', 'shim')
352
221
 
353
- const hooksPath = resolve(dir, 'hooks.mjs')
354
- const hooksTemplate = readFileSync(resolve(shimDir, 'hooks.mjs'), 'utf-8')
355
- writeFileSync(hooksPath, hooksTemplate.replace(/__BEDROCK_PATH__/g, bedrockEntry))
222
+ writeFileSync(
223
+ resolve(dir, 'package.json'),
224
+ '{"name":"@rocicorp/zero-sqlite3","main":"./index.js"}\n'
225
+ )
356
226
 
357
- const registerPath = resolve(dir, 'register.mjs')
358
- const registerTemplate = readFileSync(resolve(shimDir, 'register.mjs'), 'utf-8')
359
227
  writeFileSync(
360
- registerPath,
361
- registerTemplate.replace(/__HOOKS_URL__/g, `file://${hooksPath}`)
228
+ resolve(dir, 'index.js'),
229
+ `'use strict';
230
+ var mod = require('${bedrockEntry}');
231
+ var OrigDatabase = mod.Database;
232
+ var SqliteError = mod.SqliteError;
233
+ function Database() {
234
+ var db = new OrigDatabase(...arguments);
235
+ try {
236
+ db.pragma('journal_mode = delete');
237
+ db.pragma('busy_timeout = 30000');
238
+ db.pragma('synchronous = normal');
239
+ } catch(e) {}
240
+ return db;
241
+ }
242
+ Database.prototype = OrigDatabase.prototype;
243
+ Database.prototype.constructor = Database;
244
+ Object.keys(OrigDatabase).forEach(function(k) { Database[k] = OrigDatabase[k]; });
245
+ Database.prototype.unsafeMode = function() { return this; };
246
+ if (!Database.prototype.defaultSafeIntegers) Database.prototype.defaultSafeIntegers = function() { return this; };
247
+ if (!Database.prototype.serialize) Database.prototype.serialize = function() { throw new Error('not supported in wasm'); };
248
+ if (!Database.prototype.backup) Database.prototype.backup = function() { throw new Error('not supported in wasm'); };
249
+ var tmpDb = new OrigDatabase(':memory:');
250
+ var tmpStmt = tmpDb.prepare('SELECT 1');
251
+ var SP = Object.getPrototypeOf(tmpStmt);
252
+ if (!SP.safeIntegers) SP.safeIntegers = function() { return this; };
253
+ SP.scanStatus = function() { return undefined; };
254
+ SP.scanStatusV2 = function() { return []; };
255
+ SP.scanStatusReset = function() {};
256
+ tmpDb.close();
257
+ Database.SQLITE_SCANSTAT_NLOOP = 0;
258
+ Database.SQLITE_SCANSTAT_NVISIT = 1;
259
+ Database.SQLITE_SCANSTAT_EST = 2;
260
+ Database.SQLITE_SCANSTAT_NAME = 3;
261
+ Database.SQLITE_SCANSTAT_EXPLAIN = 4;
262
+ Database.SQLITE_SCANSTAT_SELECTID = 5;
263
+ Database.SQLITE_SCANSTAT_PARENTID = 6;
264
+ Database.SQLITE_SCANSTAT_NCYCLE = 7;
265
+ Database.SQLITE_SCANSTAT_COMPLEX = 8;
266
+ module.exports = Database;
267
+ module.exports.SqliteError = SqliteError;
268
+ `
362
269
  )
363
270
 
364
- return registerPath
271
+ // return the node_modules root so it can be prepended to NODE_PATH
272
+ return resolve(tmp, 'orez-sqlite', 'node_modules')
365
273
  }
366
274
 
367
- async function startZeroCache(config: ZeroLiteConfig, portOverride?: number): Promise<{ child: ChildProcess; env: Record<string, string>; stderrBuf: string }> {
275
+ async function startZeroCache(config: ZeroLiteConfig): Promise<ChildProcess> {
368
276
  // resolve @rocicorp/zero entry for finding zero-cache modules
369
277
  const zeroEntry = resolvePackage('@rocicorp/zero')
370
278
 
@@ -383,20 +291,12 @@ async function startZeroCache(config: ZeroLiteConfig, portOverride?: number): Pr
383
291
  // defaults that can be overridden by user env
384
292
  const defaults: Record<string, string> = {
385
293
  NODE_ENV: 'development',
386
- ZERO_LOG_LEVEL: 'info',
294
+ ZERO_LOG_LEVEL: config.logLevel,
387
295
  ZERO_NUM_SYNC_WORKERS: '1',
388
296
  // disable query planner — it relies on scanStatus which causes infinite
389
297
  // loops with wasm sqlite and has caused freezes with native too.
390
298
  // planner is an optimization, not required for correctness.
391
299
  ZERO_ENABLE_QUERY_PLANNER: 'false',
392
- // work around postgres.js bug: concurrent COPY TO STDOUT on a reused
393
- // connection causes .readable() to hang indefinitely. setting workers
394
- // high ensures each table gets its own connection (1 COPY per conn).
395
- // zero-cache already applies this workaround on windows (initial-sync.js).
396
- ZERO_INITIAL_SYNC_TABLE_COPY_WORKERS: '999',
397
- // auto-reset on replication errors (e.g. after pg_restore) instead of
398
- // crashing — zero-cache wipes its replica and resyncs from scratch.
399
- ZERO_AUTO_RESET: 'true',
400
300
  }
401
301
 
402
302
  const env: Record<string, string> = {
@@ -409,7 +309,7 @@ async function startZeroCache(config: ZeroLiteConfig, portOverride?: number): Pr
409
309
  ZERO_CVR_DB: cvrUrl,
410
310
  ZERO_CHANGE_DB: cdbUrl,
411
311
  ZERO_REPLICA_FILE: resolve(config.dataDir, 'zero-replica.db'),
412
- ZERO_PORT: String(portOverride || config.zeroPort),
312
+ ZERO_PORT: String(config.zeroPort),
413
313
  }
414
314
 
415
315
  const zeroCacheBin = resolve(zeroEntry, '..', 'cli.js')
@@ -417,135 +317,65 @@ async function startZeroCache(config: ZeroLiteConfig, portOverride?: number): Pr
417
317
  throw new Error('zero-cache cli.js not found. install @rocicorp/zero')
418
318
  }
419
319
 
420
- // calculate heap size: ~25% of system memory, min 4gb
421
- const memMB = Math.round(totalmem() / 1024 / 1024)
422
- const heapMB = Math.max(4096, Math.round(memMB * 0.25))
423
- const existing = process.env.NODE_OPTIONS || ''
424
-
425
- // wasm sqlite: write shim + ESM loader to tmpdir, pass --import to intercept
426
- // @rocicorp/zero-sqlite3 resolution with our bedrock-sqlite wasm build
320
+ // wasm sqlite: create a fake @rocicorp/zero-sqlite3 in tmpdir and prepend
321
+ // to NODE_PATH so node resolves our shim first. no require hooks, no
322
+ // Module._resolveFilename monkey-patching (which conflicts with vite).
427
323
  if (!config.disableWasmSqlite) {
428
- const registerPath = writeSqliteShim()
429
- const registerUrl = `file://${registerPath}`
430
- env.NODE_OPTIONS =
431
- `--import ${registerUrl} --max-old-space-size=${heapMB} ${existing}`.trim()
432
- } else {
433
- env.NODE_OPTIONS = `--max-old-space-size=${heapMB} ${existing}`.trim()
324
+ const shimNodeModules = writeSqliteShim()
325
+ const existingNodePath = process.env.NODE_PATH || ''
326
+ env.NODE_PATH = existingNodePath
327
+ ? `${shimNodeModules}:${existingNodePath}`
328
+ : shimNodeModules
434
329
  }
435
330
 
436
- // log env vars if --log-env was passed
437
- if (config.logEnv) {
438
- const zeroVars = Object.entries(env)
439
- .filter(([key]) => key.startsWith('ZERO_') || key === 'NODE_ENV')
440
- .sort(([a], [b]) => a.localeCompare(b))
441
- log.orez('zero-cache env:')
442
- for (const [key, value] of zeroVars) {
443
- log.orez(` ${key}=${value}`)
444
- }
445
- }
331
+ const nodeOptions = !config.disableWasmSqlite
332
+ ? `--max-old-space-size=16384 ${process.env.NODE_OPTIONS || ''}`
333
+ : process.env.NODE_OPTIONS || ''
334
+ if (nodeOptions.trim()) env.NODE_OPTIONS = nodeOptions.trim()
446
335
 
447
336
  const child = spawn(zeroCacheBin, [], {
448
337
  env,
449
338
  stdio: ['ignore', 'pipe', 'pipe'],
450
339
  })
451
340
 
452
- // zero-cache uses structured logging when piped (not a tty).
453
- // multiline format: timestamp + "[" on one line, context lines, "] message" on another.
454
- // single-line format: timestamp + [ context ] message, or timestamp + key=val,... message
455
- // we buffer multiline blocks and extract just the message.
456
- const timestampRe = /^\d{4}-\d{2}-\d{2}T[\d:.+\-Z]+\s*/
457
- let inBlock = false
458
- const zeroLog = (line: string) => {
459
- let stripped = line.replace(timestampRe, '')
460
-
461
- // start of multiline context block: line ends with "[" (possibly after timestamp)
462
- if (!inBlock && /^\[?\s*$/.test(stripped)) {
463
- inBlock = true
464
- return
465
- }
466
-
467
- // inside multiline block: skip context lines, look for "] message"
468
- if (inBlock) {
469
- const closeMatch = stripped.match(/^\]\s*(.*)$/)
470
- if (closeMatch) {
471
- inBlock = false
472
- const msg = closeMatch[1].trim()
473
- if (msg) log.zero(msg)
474
- }
475
- // context continuation lines like "'pid=8278'," — skip
476
- return
477
- }
478
-
479
- // single-line: strip inline [ context ] and key=val prefixes
480
- stripped = stripped.replace(/\[.*?\]\s*/g, '')
481
- stripped = stripped.replace(/^(?:\w+=\S+,)*\w+=\S+\s+/, '')
482
- stripped = stripped.trim()
483
-
484
- if (!stripped || /^[\[\]',\s]*$/.test(stripped)) return
485
-
486
- log.zero(stripped)
487
- }
488
-
489
341
  child.stdout?.on('data', (data: Buffer) => {
490
342
  const lines = data.toString().trim().split('\n')
491
343
  for (const line of lines) {
492
- zeroLog(line)
344
+ log.debug.zero(line)
493
345
  }
494
346
  })
495
347
 
496
- const result = { child, env, stderrBuf: '' }
497
-
498
348
  child.stderr?.on('data', (data: Buffer) => {
499
- const chunk = data.toString()
500
- result.stderrBuf += chunk
501
- const lines = chunk.trim().split('\n')
349
+ const lines = data.toString().trim().split('\n')
502
350
  for (const line of lines) {
503
- zeroLog(line)
351
+ log.debug.zero(line)
504
352
  }
505
353
  })
506
354
 
507
355
  child.on('exit', (code) => {
508
356
  if (code !== 0 && code !== null) {
509
- // changeLog_pkey errors are handled by the recovery logic in startZeroLite
510
- if (result.stderrBuf.includes('changeLog_pkey')) return
511
- if (result.stderrBuf.includes('Could not locate the bindings file')) {
512
- log.zero(
513
- 'native @rocicorp/zero-sqlite3 not found — native deps were not compiled.\n' +
514
- 'either:\n' +
515
- ' • remove --disable-wasm-sqlite to use the built-in wasm sqlite\n' +
516
- ' • install with native deps: bun install --trust @rocicorp/zero-sqlite3\n' +
517
- ' or add "trustedDependencies": ["@rocicorp/zero-sqlite3"] to package.json'
518
- )
519
- } else {
520
- const lastLines = result.stderrBuf.trim().split('\n').slice(-5).join('\n')
521
- if (lastLines) {
522
- log.zero(`exited with code ${code}:\n${lastLines}`)
523
- } else {
524
- log.zero(`exited with code ${code}`)
525
- }
526
- }
357
+ log.zero(`exited with code ${code}`)
527
358
  }
528
359
  })
529
360
 
530
- return result
361
+ return child
531
362
  }
532
363
 
533
364
  async function waitForZeroCache(
534
365
  config: ZeroLiteConfig,
535
- timeoutMs = 120000,
536
- portOverride?: number,
366
+ timeoutMs = 60000
537
367
  ): Promise<void> {
538
368
  const start = Date.now()
539
- const url = `http://127.0.0.1:${portOverride || config.zeroPort}/`
369
+ const url = `http://127.0.0.1:${config.zeroPort}/`
540
370
 
541
371
  while (Date.now() - start < timeoutMs) {
542
372
  try {
543
373
  const res = await fetch(url)
544
- if (res.ok || res.status === 404) return
374
+ if (res.ok) return
545
375
  } catch {
546
376
  // not ready yet
547
377
  }
548
- await new Promise((r) => setTimeout(r, 42))
378
+ await new Promise((r) => setTimeout(r, 500))
549
379
  }
550
380
 
551
381
  log.zero('health check timed out, continuing anyway')