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.
- package/dist/admin/http-proxy.d.ts.map +1 -1
- package/dist/admin/http-proxy.js.map +1 -1
- package/dist/admin/log-store.d.ts.map +1 -1
- package/dist/admin/log-store.js.map +1 -1
- package/dist/admin/server.d.ts +2 -2
- package/dist/admin/server.d.ts.map +1 -1
- package/dist/admin/server.js.map +1 -1
- package/dist/admin/ui.d.ts.map +1 -1
- package/dist/admin/ui.js +2 -2
- package/dist/admin/ui.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +6 -112
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +0 -5
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +0 -5
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +0 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +91 -249
- package/dist/index.js.map +1 -1
- package/dist/log.d.ts +0 -9
- package/dist/log.d.ts.map +1 -1
- package/dist/log.js +1 -24
- package/dist/log.js.map +1 -1
- package/dist/mutex.d.ts.map +1 -1
- package/dist/mutex.js +2 -13
- package/dist/mutex.js.map +1 -1
- package/dist/pg-proxy.d.ts +2 -3
- package/dist/pg-proxy.d.ts.map +1 -1
- package/dist/pg-proxy.js +167 -377
- package/dist/pg-proxy.js.map +1 -1
- package/dist/pglite-manager.d.ts +0 -1
- package/dist/pglite-manager.d.ts.map +1 -1
- package/dist/pglite-manager.js +1 -1
- package/dist/pglite-manager.js.map +1 -1
- package/dist/replication/change-tracker.d.ts +0 -6
- package/dist/replication/change-tracker.d.ts.map +1 -1
- package/dist/replication/change-tracker.js +0 -74
- package/dist/replication/change-tracker.js.map +1 -1
- package/dist/replication/handler.d.ts.map +1 -1
- package/dist/replication/handler.js +5 -47
- package/dist/replication/handler.js.map +1 -1
- package/dist/vite-plugin.d.ts +0 -3
- package/dist/vite-plugin.d.ts.map +1 -1
- package/dist/vite-plugin.js +0 -24
- package/dist/vite-plugin.js.map +1 -1
- package/package.json +5 -4
- package/src/admin/http-proxy.ts +5 -1
- package/src/admin/log-store.ts +4 -1
- package/src/admin/server.ts +7 -3
- package/src/admin/ui.ts +682 -680
- package/src/cli.ts +6 -111
- package/src/config.ts +0 -10
- package/src/index.ts +92 -262
- package/src/integration/integration.test.ts +264 -133
- package/src/log.ts +1 -25
- package/src/mutex.ts +2 -12
- package/src/pg-proxy.ts +187 -449
- package/src/pglite-manager.ts +1 -1
- package/src/replication/change-tracker.ts +0 -92
- package/src/replication/handler.ts +4 -50
- package/src/shim/hooks.mjs +34 -1
- package/src/vite-plugin.ts +0 -28
- 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,
|
|
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
|
|
15
|
+
import { log, port, setLogLevel } from './log.js'
|
|
18
16
|
import { startPgProxy } from './pg-proxy.js'
|
|
19
|
-
import {
|
|
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.
|
|
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
|
-
//
|
|
138
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
167
|
-
|
|
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
|
|
158
|
+
return { config, stop, db, instances, pgPort: config.pgPort, zeroPort: config.zeroPort }
|
|
289
159
|
}
|
|
290
160
|
|
|
291
|
-
function
|
|
161
|
+
function cleanupStaleReplica(config: ZeroLiteConfig): void {
|
|
292
162
|
const replicaPath = resolve(config.dataDir, 'zero-replica.db')
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
|
|
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
|
-
//
|
|
343
|
-
//
|
|
344
|
-
//
|
|
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
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
-
|
|
361
|
-
|
|
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
|
|
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
|
|
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:
|
|
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(
|
|
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
|
-
//
|
|
421
|
-
|
|
422
|
-
|
|
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
|
|
429
|
-
const
|
|
430
|
-
env.
|
|
431
|
-
|
|
432
|
-
|
|
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
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
361
|
+
return child
|
|
531
362
|
}
|
|
532
363
|
|
|
533
364
|
async function waitForZeroCache(
|
|
534
365
|
config: ZeroLiteConfig,
|
|
535
|
-
timeoutMs =
|
|
536
|
-
portOverride?: number,
|
|
366
|
+
timeoutMs = 60000
|
|
537
367
|
): Promise<void> {
|
|
538
368
|
const start = Date.now()
|
|
539
|
-
const url = `http://127.0.0.1:${
|
|
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
|
|
374
|
+
if (res.ok) return
|
|
545
375
|
} catch {
|
|
546
376
|
// not ready yet
|
|
547
377
|
}
|
|
548
|
-
await new Promise((r) => setTimeout(r,
|
|
378
|
+
await new Promise((r) => setTimeout(r, 500))
|
|
549
379
|
}
|
|
550
380
|
|
|
551
381
|
log.zero('health check timed out, continuing anyway')
|