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
@@ -16,7 +16,7 @@ export interface PGliteInstances {
16
16
  }
17
17
 
18
18
  // create a single pglite instance with given dataDir suffix
19
- export async function createInstance(
19
+ async function createInstance(
20
20
  config: ZeroLiteConfig,
21
21
  name: string,
22
22
  withExtensions: boolean
@@ -67,35 +67,6 @@ export async function installChangeTracking(db: PGlite): Promise<void> {
67
67
  $$ LANGUAGE plpgsql;
68
68
  `)
69
69
 
70
- // auto-install change tracking on tables created after startup (e.g. via restore
71
- // or wire protocol). uses a DDL event trigger that fires on CREATE TABLE.
72
- await db.exec(`
73
- CREATE OR REPLACE FUNCTION public._zero_auto_track() RETURNS event_trigger AS $$
74
- DECLARE
75
- obj record;
76
- BEGIN
77
- FOR obj IN SELECT * FROM pg_event_trigger_ddl_commands()
78
- WHERE command_tag = 'CREATE TABLE'
79
- LOOP
80
- IF obj.schema_name = 'public'
81
- AND obj.object_identity NOT LIKE '%._zero_%'
82
- AND obj.object_identity NOT LIKE '%.migrations'
83
- THEN
84
- EXECUTE format(
85
- 'CREATE TRIGGER _zero_change_trigger AFTER INSERT OR UPDATE OR DELETE ON %s FOR EACH ROW EXECUTE FUNCTION public._zero_track_change()',
86
- obj.object_identity
87
- );
88
- END IF;
89
- END LOOP;
90
- END;
91
- $$ LANGUAGE plpgsql;
92
-
93
- DROP EVENT TRIGGER IF EXISTS _zero_auto_track_trigger;
94
- CREATE EVENT TRIGGER _zero_auto_track_trigger ON ddl_command_end
95
- WHEN TAG IN ('CREATE TABLE')
96
- EXECUTE FUNCTION public._zero_auto_track();
97
- `)
98
-
99
70
  // install triggers on all public tables
100
71
  await installTriggersOnAllTables(db)
101
72
  }
@@ -169,58 +140,6 @@ async function installTriggersOnAllTables(db: PGlite): Promise<void> {
169
140
  log.debug.pglite(`installed change tracking triggers on ${count} tables`)
170
141
  }
171
142
 
172
- /**
173
- * re-install change tracking triggers on any public tables that don't have them.
174
- * catches tables created between startup and replication start.
175
- */
176
- export async function ensureChangeTrackingOnAllTables(db: PGlite): Promise<void> {
177
- const pubName = process.env.ZERO_APP_PUBLICATIONS
178
- let tables: { tablename: string }[]
179
-
180
- if (pubName) {
181
- const result = await db.query<{ tablename: string }>(
182
- `SELECT tablename FROM pg_publication_tables
183
- WHERE pubname = $1
184
- AND schemaname = 'public'
185
- AND tablename NOT LIKE '_zero_%'`,
186
- [pubName]
187
- )
188
- tables = result.rows
189
- } else {
190
- const result = await db.query<{ tablename: string }>(
191
- `SELECT tablename FROM pg_tables
192
- WHERE schemaname = 'public'
193
- AND tablename NOT IN ('migrations', '_zero_changes')
194
- AND tablename NOT LIKE '_zero_%'`
195
- )
196
- tables = result.rows
197
- }
198
-
199
- // find tables missing the change trigger
200
- const triggered = await db.query<{ event_object_table: string }>(
201
- `SELECT DISTINCT event_object_table FROM information_schema.triggers
202
- WHERE trigger_name = '_zero_change_trigger'
203
- AND event_object_schema = 'public'`
204
- )
205
- const hasTracker = new Set(triggered.rows.map((r) => r.event_object_table))
206
-
207
- let count = 0
208
- for (const { tablename } of tables) {
209
- if (hasTracker.has(tablename)) continue
210
- const quoted = quoteIdent(tablename)
211
- await db.exec(`
212
- CREATE TRIGGER _zero_change_trigger
213
- AFTER INSERT OR UPDATE OR DELETE ON public.${quoted}
214
- FOR EACH ROW EXECUTE FUNCTION public._zero_track_change();
215
- `)
216
- count++
217
- }
218
-
219
- if (count > 0) {
220
- log.debug.pglite(`installed change tracking on ${count} new tables`)
221
- }
222
- }
223
-
224
143
  /**
225
144
  * install change tracking triggers on tables in shard schemas.
226
145
  * zero-cache creates shard schemas (e.g. chat_0) with clients/mutations
@@ -276,17 +195,6 @@ export async function getChangesSince(
276
195
  return result.rows
277
196
  }
278
197
 
279
- export async function purgeConsumedChanges(
280
- db: PGlite,
281
- watermark: number
282
- ): Promise<number> {
283
- const result = await db.query<{ count: string }>(
284
- 'WITH deleted AS (DELETE FROM public._zero_changes WHERE watermark <= $1 RETURNING 1) SELECT count(*)::text AS count FROM deleted',
285
- [watermark]
286
- )
287
- return Number(result.rows[0]?.count || 0)
288
- }
289
-
290
198
  export async function getCurrentWatermark(db: PGlite): Promise<number> {
291
199
  const result = await db.query<{ last_value: string; is_called: boolean }>(
292
200
  'SELECT last_value, is_called FROM public._zero_watermark'
@@ -10,9 +10,7 @@ import { log } from '../log.js'
10
10
  import {
11
11
  getChangesSince,
12
12
  getCurrentWatermark,
13
- purgeConsumedChanges,
14
13
  installTriggersOnShardTables,
15
- ensureChangeTrackingOnAllTables,
16
14
  type ChangeRecord,
17
15
  } from './change-tracker.js'
18
16
  import {
@@ -33,9 +31,6 @@ import {
33
31
  import type { Mutex } from '../mutex.js'
34
32
  import type { PGlite } from '@electric-sql/pglite'
35
33
 
36
- // track concurrent replication handlers to detect reconnect-purge race
37
- let activeHandlerCount = 0
38
-
39
34
  export interface ReplicationWriter {
40
35
  write(data: Uint8Array): void
41
36
  }
@@ -266,9 +261,6 @@ export async function handleStartReplication(
266
261
  db: PGlite,
267
262
  mutex: Mutex
268
263
  ): Promise<void> {
269
- activeHandlerCount++
270
- const handlerId = activeHandlerCount
271
- console.info(`[orez-repl#${handlerId}] START_REPLICATION (active handlers: ${activeHandlerCount})`)
272
264
  log.debug.proxy('replication: entering streaming mode')
273
265
 
274
266
  // send CopyBothResponse to enter streaming mode
@@ -292,9 +284,6 @@ export async function handleStartReplication(
292
284
  // "already in transaction" errors when they interleave.
293
285
  await mutex.acquire()
294
286
  try {
295
- // install change tracking triggers on any tables created after startup
296
- await ensureChangeTrackingOnAllTables(db)
297
-
298
287
  // install change tracking triggers on shard schema tables (e.g. chat_0.clients)
299
288
  // these track zero-cache's lastMutationID for .server promise resolution
300
289
  await installTriggersOnShardTables(db)
@@ -463,21 +452,13 @@ export async function handleStartReplication(
463
452
  mutex.release()
464
453
  }
465
454
 
466
- console.info(`[orez-repl#${handlerId}] setup complete, starting poll (lastWatermark=${lastWatermark})`)
467
-
468
455
  // track which tables we've sent RELATION messages for
469
456
  const sentRelations = new Set<string>()
470
457
  let txCounter = 1
471
458
 
472
459
  // polling + notification loop
473
- // adaptive: poll fast when catching up, slow when idle
474
- const pollIntervalIdle = 500
475
- const pollIntervalCatchUp = 20
476
- const batchSize = 2000
477
- const purgeEveryN = 10
460
+ const pollInterval = 500
478
461
  let running = true
479
- let pollsSincePurge = 0
480
- let lastIdleLog = 0
481
462
 
482
463
  const poll = async () => {
483
464
  while (running) {
@@ -486,14 +467,12 @@ export async function handleStartReplication(
486
467
  await mutex.acquire()
487
468
  let changes: Awaited<ReturnType<typeof getChangesSince>>
488
469
  try {
489
- changes = await getChangesSince(db, lastWatermark, batchSize)
470
+ changes = await getChangesSince(db, lastWatermark, 100)
490
471
  } finally {
491
472
  mutex.release()
492
473
  }
493
474
 
494
475
  if (changes.length > 0) {
495
- const tables = [...new Set(changes.map(c => c.table_name))].join(',')
496
- console.info(`[orez-repl#${handlerId}] found ${changes.length} changes [${tables}] (wm ${lastWatermark}→${changes[changes.length - 1].watermark}, type=${typeof changes[0].watermark})`)
497
476
  await streamChanges(
498
477
  changes,
499
478
  writer,
@@ -504,37 +483,13 @@ export async function handleStartReplication(
504
483
  columnTypeOids
505
484
  )
506
485
  lastWatermark = changes[changes.length - 1].watermark
507
-
508
- // purge consumed changes periodically to free wasm memory
509
- pollsSincePurge++
510
- if (pollsSincePurge >= purgeEveryN) {
511
- pollsSincePurge = 0
512
- await mutex.acquire()
513
- try {
514
- const purged = await purgeConsumedChanges(db, lastWatermark)
515
- if (purged > 0) {
516
- console.info(`[orez-repl#${handlerId}] purged ${purged} changes (wm<=${lastWatermark})`)
517
- }
518
- } finally {
519
- mutex.release()
520
- }
521
- }
522
- } else {
523
- // throttled idle logging (every 10s)
524
- const now = Date.now()
525
- if (now - lastIdleLog > 10000) {
526
- lastIdleLog = now
527
- console.info(`[orez-repl#${handlerId}] idle (lastWatermark=${lastWatermark}, type=${typeof lastWatermark})`)
528
- }
529
486
  }
530
487
 
531
488
  // send keepalive
532
489
  const ts = nowMicros()
533
490
  writer.write(encodeKeepalive(currentLsn, ts, false))
534
491
 
535
- // if we got a full batch, there's likely more - poll fast
536
- const delay = changes.length >= batchSize ? pollIntervalCatchUp : pollIntervalIdle
537
- await new Promise((resolve) => setTimeout(resolve, delay))
492
+ await new Promise((resolve) => setTimeout(resolve, pollInterval))
538
493
  } catch (err: unknown) {
539
494
  const msg = err instanceof Error ? err.message : String(err)
540
495
  log.debug.proxy(`replication poll error: ${msg}`)
@@ -549,8 +504,7 @@ export async function handleStartReplication(
549
504
 
550
505
  log.debug.proxy('replication: starting poll loop')
551
506
  await poll()
552
- activeHandlerCount--
553
- console.info(`[orez-repl#${handlerId}] poll loop exited (remaining handlers: ${activeHandlerCount})`)
507
+ log.debug.proxy('replication: poll loop exited')
554
508
  }
555
509
 
556
510
  async function streamChanges(
@@ -20,6 +20,13 @@ export function load(url, context, nextLoad) {
20
20
  format: 'module',
21
21
  shortCircuit: true,
22
22
  source: `
23
+ // catch uncaught exceptions from bedrock-sqlite wasm clearly
24
+ process.on('uncaughtException', (err) => {
25
+ console.error('[orez-shim] UNCAUGHT EXCEPTION:', err?.message || err);
26
+ console.error('[orez-shim] code:', err?.code, 'name:', err?.name);
27
+ console.error('[orez-shim] stack:', err?.stack?.split('\\n').slice(0, 5).join('\\n'));
28
+ process.exit(1);
29
+ });
23
30
  import { createRequire } from 'node:module';
24
31
  const require = createRequire('${BEDROCK_PATH}');
25
32
  const mod = require('${BEDROCK_PATH}');
@@ -27,13 +34,39 @@ const OrigDatabase = mod.Database;
27
34
  const SqliteError = mod.SqliteError;
28
35
  function Database(...args) {
29
36
  const db = new OrigDatabase(...args);
30
- try { db.pragma('busy_timeout = 30000'); db.pragma('synchronous = normal'); } catch(e) {}
37
+ try { db.pragma('journal_mode = delete'); db.pragma('busy_timeout = 30000'); db.pragma('synchronous = normal'); } catch(e) {}
31
38
  return db;
32
39
  }
33
40
  Database.prototype = OrigDatabase.prototype;
34
41
  Database.prototype.constructor = Database;
35
42
  Object.keys(OrigDatabase).forEach(k => { Database[k] = OrigDatabase[k]; });
36
43
  Database.prototype.unsafeMode = function() { return this; };
44
+ // wrap pragma to skip optimize (corrupts wasm vfs) and swallow sqlite errors
45
+ const origPragma = OrigDatabase.prototype.pragma;
46
+ Database.prototype.pragma = function(str, opts) {
47
+ if (str && str.trim().toLowerCase().startsWith('optimize')) return [];
48
+ try { return origPragma.call(this, str, opts); }
49
+ catch(e) { if (e && (e.code === 'SQLITE_CORRUPT' || e.code === 'SQLITE_IOERR')) return []; throw e; }
50
+ };
51
+ // wrap close to swallow wasm errors during shutdown
52
+ const origClose = OrigDatabase.prototype.close;
53
+ Database.prototype.close = function() {
54
+ try { return origClose.call(this); }
55
+ catch(e) { console.error('[orez-shim] close error (swallowed):', e?.message || e); }
56
+ };
57
+ // trace writes to _zero.changeLog and _zero.replicationState to debug view-syncer
58
+ const origRun = OrigDatabase.prototype.run;
59
+ Database.prototype.run = function(sql, ...args) {
60
+ if (typeof sql === 'string') {
61
+ if (sql.includes('_zero.changeLog')) {
62
+ console.info('[orez-shim] changeLog write:', sql.slice(0, 120), args.length ? JSON.stringify(args[0]).slice(0, 80) : '');
63
+ }
64
+ if (sql.includes('_zero.replicationState') && (sql.includes('UPDATE') || sql.includes('INSERT'))) {
65
+ console.info('[orez-shim] replicationState update:', sql.slice(0, 120), args.length ? JSON.stringify(args[0]).slice(0, 80) : '');
66
+ }
67
+ }
68
+ return origRun.call(this, sql, ...args);
69
+ };
37
70
  if (!Database.prototype.defaultSafeIntegers) Database.prototype.defaultSafeIntegers = function() { return this; };
38
71
  if (!Database.prototype.serialize) Database.prototype.serialize = function() { throw new Error('not supported in wasm'); };
39
72
  if (!Database.prototype.backup) Database.prototype.backup = function() { throw new Error('not supported in wasm'); };
@@ -7,21 +7,16 @@ import type { Plugin } from 'vite'
7
7
  export interface OrezPluginOptions extends Partial<ZeroLiteConfig> {
8
8
  s3?: boolean
9
9
  s3Port?: number
10
- admin?: boolean
11
- adminPort?: number
12
- adminLogs?: boolean
13
10
  }
14
11
 
15
12
  export default function orez(options?: OrezPluginOptions): Plugin {
16
13
  let stop: (() => Promise<void>) | null = null
17
14
  let s3Server: Server | null = null
18
- let adminServer: Server | null = null
19
15
 
20
16
  return {
21
17
  name: 'orez',
22
18
 
23
19
  async configureServer(server) {
24
- const startTime = Date.now()
25
20
  const result = await startZeroLite(options)
26
21
  stop = result.stop
27
22
 
@@ -33,30 +28,7 @@ export default function orez(options?: OrezPluginOptions): Plugin {
33
28
  })
34
29
  }
35
30
 
36
- if (options?.admin && result.logStore) {
37
- const { findPort } = await import('./port.js')
38
- const { log } = await import('./log.js')
39
- const adminPort = options.adminPort || (result.config.zeroPort + 2)
40
- const resolvedPort = await findPort(adminPort)
41
- const { startAdminServer } = await import('./admin/server.js')
42
- adminServer = await startAdminServer({
43
- port: resolvedPort,
44
- logStore: result.logStore,
45
- config: result.config,
46
- zeroEnv: result.zeroEnv,
47
- actions: result.actions,
48
- startTime,
49
- httpLog: result.httpLogStore || undefined,
50
- })
51
- log.orez(`admin: http://127.0.0.1:${resolvedPort}`)
52
- if (result.config.adminLogs) {
53
- const { resolve } = await import('node:path')
54
- log.orez(`logs: ${resolve(result.config.dataDir, 'logs', 'orez.log')}`)
55
- }
56
- }
57
-
58
31
  server.httpServer?.on('close', async () => {
59
- adminServer?.close()
60
32
  s3Server?.close()
61
33
  if (stop) {
62
34
  await stop()
@@ -19,8 +19,7 @@ import { resolve } from 'node:path'
19
19
 
20
20
  // import bedrock-sqlite directly (our wasm build)
21
21
  // @ts-expect-error - CJS module
22
- import bedrockSqlite from 'bedrock-sqlite'
23
- const { Database } = bedrockSqlite
22
+ import { Database } from 'bedrock-sqlite'
24
23
  import { describe, test, expect, beforeEach, afterEach } from 'vitest'
25
24
 
26
25
  // helper: temp db file