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.
- 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 -280
- 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 +2 -8
- 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 +1 -62
- package/dist/replication/change-tracker.js.map +1 -1
- package/dist/replication/handler.d.ts.map +1 -1
- package/dist/replication/handler.js +7 -66
- 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/cli.ts +18 -124
- package/src/config.ts +0 -10
- package/src/index.ts +92 -309
- 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 -451
- package/src/pglite-manager.ts +2 -9
- package/src/replication/change-tracker.ts +1 -83
- package/src/replication/handler.ts +6 -79
- package/src/replication/pgoutput-encoder.test.ts +0 -217
- package/src/replication/zero-compat.test.ts +1 -232
- package/src/shim/hooks.mjs +1 -1
- package/src/vite-plugin.ts +0 -28
- 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
|
|
15
|
+
import { log, port, setLogLevel } from './log.js'
|
|
25
16
|
import { startPgProxy } from './pg-proxy.js'
|
|
26
|
-
import {
|
|
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.
|
|
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
|
-
//
|
|
149
|
-
|
|
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
|
|
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
|
-
|
|
178
|
-
|
|
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
|
|
161
|
+
function cleanupStaleReplica(config: ZeroLiteConfig): void {
|
|
336
162
|
const replicaPath = resolve(config.dataDir, 'zero-replica.db')
|
|
337
|
-
//
|
|
338
|
-
//
|
|
339
|
-
|
|
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
|
-
//
|
|
387
|
-
//
|
|
388
|
-
//
|
|
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
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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
|
-
|
|
405
|
-
|
|
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
|
|
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:
|
|
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(
|
|
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
|
-
//
|
|
468
|
-
|
|
469
|
-
|
|
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
|
|
476
|
-
const
|
|
477
|
-
env.
|
|
478
|
-
|
|
479
|
-
|
|
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
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
361
|
+
return child
|
|
578
362
|
}
|
|
579
363
|
|
|
580
364
|
async function waitForZeroCache(
|
|
581
365
|
config: ZeroLiteConfig,
|
|
582
|
-
timeoutMs =
|
|
583
|
-
portOverride?: number
|
|
366
|
+
timeoutMs = 60000
|
|
584
367
|
): Promise<void> {
|
|
585
368
|
const start = Date.now()
|
|
586
|
-
const url = `http://127.0.0.1:${
|
|
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
|
|
374
|
+
if (res.ok) return
|
|
592
375
|
} catch {
|
|
593
376
|
// not ready yet
|
|
594
377
|
}
|
|
595
|
-
await new Promise((r) => setTimeout(r,
|
|
378
|
+
await new Promise((r) => setTimeout(r, 500))
|
|
596
379
|
}
|
|
597
380
|
|
|
598
381
|
log.zero('health check timed out, continuing anyway')
|