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