orez 0.5.11 → 0.5.13

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 (77) hide show
  1. package/dist/cf-do/tx-journal.d.ts +6 -5
  2. package/dist/cf-do/tx-journal.d.ts.map +1 -1
  3. package/dist/cf-do/tx-journal.js +116 -27
  4. package/dist/cf-do/tx-journal.js.map +1 -1
  5. package/dist/pg-proxy-browser.d.ts +2 -0
  6. package/dist/pg-proxy-browser.d.ts.map +1 -1
  7. package/dist/pg-proxy-browser.js +56 -102
  8. package/dist/pg-proxy-browser.js.map +1 -1
  9. package/dist/pg-proxy-do-backend.d.ts +8 -0
  10. package/dist/pg-proxy-do-backend.d.ts.map +1 -1
  11. package/dist/pg-proxy-do-backend.js +27 -9
  12. package/dist/pg-proxy-do-backend.js.map +1 -1
  13. package/dist/pg-sqlite-compiler/compiler.d.ts +17 -0
  14. package/dist/pg-sqlite-compiler/compiler.d.ts.map +1 -0
  15. package/dist/pg-sqlite-compiler/compiler.js +80 -0
  16. package/dist/pg-sqlite-compiler/compiler.js.map +1 -0
  17. package/dist/pg-sqlite-compiler/index.d.ts +1 -16
  18. package/dist/pg-sqlite-compiler/index.d.ts.map +1 -1
  19. package/dist/pg-sqlite-compiler/index.js +2 -79
  20. package/dist/pg-sqlite-compiler/index.js.map +1 -1
  21. package/dist/replication/handler.d.ts +8 -7
  22. package/dist/replication/handler.d.ts.map +1 -1
  23. package/dist/replication/handler.js +147 -116
  24. package/dist/replication/handler.js.map +1 -1
  25. package/dist/worker/browser-build-config.d.ts.map +1 -1
  26. package/dist/worker/browser-build-config.js +1 -0
  27. package/dist/worker/browser-build-config.js.map +1 -1
  28. package/dist/worker/browser-embed.d.ts +3 -2
  29. package/dist/worker/browser-embed.d.ts.map +1 -1
  30. package/dist/worker/browser-embed.js +187 -150
  31. package/dist/worker/browser-embed.js.map +1 -1
  32. package/dist/worker/cf-instance-runtime.d.ts +62 -0
  33. package/dist/worker/cf-instance-runtime.d.ts.map +1 -0
  34. package/dist/worker/cf-instance-runtime.js +260 -0
  35. package/dist/worker/cf-instance-runtime.js.map +1 -0
  36. package/dist/worker/cf-patches.d.ts +9 -6
  37. package/dist/worker/cf-patches.d.ts.map +1 -1
  38. package/dist/worker/cf-patches.js +117 -60
  39. package/dist/worker/cf-patches.js.map +1 -1
  40. package/dist/worker/durable-object-websocket-handoff.d.ts +2 -0
  41. package/dist/worker/durable-object-websocket-handoff.d.ts.map +1 -1
  42. package/dist/worker/durable-object-websocket-handoff.js +22 -41
  43. package/dist/worker/durable-object-websocket-handoff.js.map +1 -1
  44. package/dist/worker/embed-generation.d.ts +9 -6
  45. package/dist/worker/embed-generation.d.ts.map +1 -1
  46. package/dist/worker/embed-generation.js +27 -6
  47. package/dist/worker/embed-generation.js.map +1 -1
  48. package/dist/worker/shims/fastify-websocket.d.ts +3 -0
  49. package/dist/worker/shims/fastify-websocket.d.ts.map +1 -0
  50. package/dist/worker/shims/fastify-websocket.js +3 -0
  51. package/dist/worker/shims/fastify-websocket.js.map +1 -0
  52. package/dist/worker/shims/fastify.d.ts +2 -10
  53. package/dist/worker/shims/fastify.d.ts.map +1 -1
  54. package/dist/worker/shims/fastify.js +14 -21
  55. package/dist/worker/shims/fastify.js.map +1 -1
  56. package/dist/worker/shims/postgres-browser.d.ts +1 -1
  57. package/dist/worker/shims/postgres-browser.d.ts.map +1 -1
  58. package/dist/worker/shims/postgres-browser.js +29 -20
  59. package/dist/worker/shims/postgres-browser.js.map +1 -1
  60. package/dist/worker/shims/sqlite-browser.d.ts +2 -3
  61. package/dist/worker/shims/sqlite-browser.d.ts.map +1 -1
  62. package/dist/worker/shims/sqlite-browser.js +2 -3
  63. package/dist/worker/shims/sqlite-browser.js.map +1 -1
  64. package/dist/worker/shims/sqlite.d.ts.map +1 -1
  65. package/dist/worker/shims/sqlite.js +56 -35
  66. package/dist/worker/shims/sqlite.js.map +1 -1
  67. package/dist/worker/shims/ws.d.ts.map +1 -1
  68. package/dist/worker/shims/ws.js +54 -39
  69. package/dist/worker/shims/ws.js.map +1 -1
  70. package/dist/worker/shims/zero-process-env.d.ts.map +1 -1
  71. package/dist/worker/shims/zero-process-env.js +25 -4
  72. package/dist/worker/shims/zero-process-env.js.map +1 -1
  73. package/dist/worker/zero-cache-embed-cf.d.ts +5 -1
  74. package/dist/worker/zero-cache-embed-cf.d.ts.map +1 -1
  75. package/dist/worker/zero-cache-embed-cf.js +104 -137
  76. package/dist/worker/zero-cache-embed-cf.js.map +1 -1
  77. package/package.json +6 -2
@@ -65,55 +65,65 @@ function lsnFloorFromTime() {
65
65
  const timeLsn = BigInt(Date.now()) << LSN_TIME_SHIFT;
66
66
  return timeLsn > MIN_LSN ? timeLsn : MIN_LSN;
67
67
  }
68
- // current lsn counter. seed from wall time so a restarted browser proxy never
69
- // emits stream lsns behind zero-cache's persisted initial-sync watermark.
70
- // use a large stride because zero-cache's initial backfill can reserve
71
- // watermarks after slot creation while app writes are already queued; the next
72
- // streamed transaction still has to land beyond that warmup range.
73
- let currentLsn = lsnFloorFromTime();
74
- // persistent watermark across handler restarts so new handlers
75
- // don't replay already-streamed changes
76
- let lastStreamedWatermark = 0;
77
- // confirmed-flush feedback from the consumer (standby status updates).
78
- // the poll loop purges _zero_changes for confirmed batches; until then the
79
- // table is the durable record so a consumer death mid-store re-streams the
80
- // transaction instead of silently losing it.
81
- let pendingConfirmLsn = 0n;
82
- let processedConfirmLsn = 0n;
83
- // performance.now() of the last standby status update received from the
84
- // consumer (0 = never). liveness signal for the stale-unconfirmed reconnect.
85
- let lastFeedbackAtPerf = 0;
86
- // ── replication pipeline health gauges ──────────────────────────────────────
87
- // plain module state read by the supervisor's bankruptcy monitor
88
- // (isReplicationBankrupt in recovery.ts). deliberately no db access: the
89
- // monitor must be able to see a wedged pipeline without queueing on the very
90
- // mutex that is wedged. timestamps are Date.now() ms; 0 = never.
91
- let lastWriteSignalAt = 0; // a proxy write signaled new changes
92
- let lastConfirmProgressAt = 0; // consumer durably confirmed (or a fresh slot reset the log)
93
- let lastStreamActivityAt = 0; // a replication stream started or streamed a batch
94
- export function getReplicationHealth() {
95
- return { lastWriteSignalAt, lastConfirmProgressAt, lastStreamActivityAt };
68
+ const DEFAULT_REPLICATION_INSTANCE = '__orez_default_replication__';
69
+ const replicationStates = new Map();
70
+ function newReplicationState(instanceId, instanceLog) {
71
+ return {
72
+ cachedColumnTypeOids: null,
73
+ cachedColumns: new Map(),
74
+ cachedExcludedColumns: null,
75
+ cachedTableKeyColumns: null,
76
+ currentLsn: lsnFloorFromTime(),
77
+ instanceId,
78
+ lastConfirmProgressAt: 0,
79
+ lastFeedbackAtPerf: 0,
80
+ lastStreamActivityAt: 0,
81
+ lastStreamedWatermark: 0,
82
+ lastWriteSignalAt: 0,
83
+ log: instanceLog,
84
+ pendingConfirmLsn: 0n,
85
+ processedConfirmLsn: 0n,
86
+ replicationWakeup: null,
87
+ };
88
+ }
89
+ function replicationState(instanceId) {
90
+ const key = instanceId ?? DEFAULT_REPLICATION_INSTANCE;
91
+ let state = replicationStates.get(key);
92
+ if (!state) {
93
+ state = newReplicationState(instanceId);
94
+ replicationStates.set(key, state);
95
+ }
96
+ return state;
97
+ }
98
+ export function getReplicationHealth(instanceId) {
99
+ const state = replicationState(instanceId);
100
+ return {
101
+ lastWriteSignalAt: state.lastWriteSignalAt,
102
+ lastConfirmProgressAt: state.lastConfirmProgressAt,
103
+ lastStreamActivityAt: state.lastStreamActivityAt,
104
+ };
96
105
  }
97
106
  /** record durable consumer progress (confirm purge, fresh slot, or a
98
107
  * completed zero reset — all mean the backlog is not stuck). */
99
- export function markReplicationProgress() {
100
- lastConfirmProgressAt = Date.now();
108
+ export function markReplicationProgress(instanceId) {
109
+ replicationState(instanceId).lastConfirmProgressAt = Date.now();
101
110
  }
102
111
  /**
103
112
  * note a confirmed-flush LSN from the consumer's standby status update and
104
113
  * wake the replication loop so it purges the confirmed batches.
105
114
  */
106
- export function noteConfirmedFlushLsn(lsn) {
115
+ export function noteConfirmedFlushLsn(lsn, instanceId) {
116
+ const state = replicationState(instanceId);
107
117
  // any standby status update — even one that doesn't advance the LSN — is
108
118
  // proof the consumer is alive on the wire. the stale-unconfirmed reconnect
109
119
  // must not kill a stream whose consumer is still chewing a large backlog
110
120
  // (2026-07 CF incident: the 60s reconnect kept beating the consumer's first
111
121
  // ack, so a 10k-row backlog re-streamed on every cycle, forever).
112
- lastFeedbackAtPerf = performance.now();
113
- if (lsn <= pendingConfirmLsn)
122
+ state.lastFeedbackAtPerf = performance.now();
123
+ if (lsn <= state.pendingConfirmLsn)
114
124
  return;
115
- pendingConfirmLsn = lsn;
116
- _replicationWakeup?.();
125
+ state.pendingConfirmLsn = lsn;
126
+ state.replicationWakeup?.();
117
127
  }
118
128
  /**
119
129
  * incremental parser for client→server traffic on a replication connection.
@@ -157,14 +167,14 @@ export function createReplicationFeedbackParser(onFlushedLsn = noteConfirmedFlus
157
167
  };
158
168
  }
159
169
  // direct wakeup from proxy — bypasses pg_notify for instant replication
160
- let _replicationWakeup = null;
161
170
  /** signal the replication handler that changes may be available.
162
171
  * called by the proxy after executing writes on the postgres instance. */
163
- export function signalReplicationChange() {
164
- lastWriteSignalAt = Date.now();
165
- _replicationWakeup?.();
166
- const globalWakeup = globalThis.__orez_signal_replication;
167
- if (typeof globalWakeup === 'function' && globalWakeup !== _replicationWakeup) {
172
+ export function signalReplicationChange(instanceId) {
173
+ const state = replicationState(instanceId);
174
+ state.lastWriteSignalAt = Date.now();
175
+ state.replicationWakeup?.();
176
+ const globalWakeup = instanceId === undefined ? globalThis.__orez_signal_replication : undefined;
177
+ if (typeof globalWakeup === 'function' && globalWakeup !== state.replicationWakeup) {
168
178
  globalWakeup();
169
179
  }
170
180
  }
@@ -172,37 +182,30 @@ export function signalReplicationChange() {
172
182
  // zero-cache reconnects the replication stream after initial sync, and if setup
173
183
  // takes too long (holding the mutex, blocking proxy queries), zero-cache's
174
184
  // queries timeout and it kills the connection.
175
- let cachedTableKeyColumns = null;
176
- let cachedExcludedColumns = null;
177
- let cachedColumnTypeOids = null;
178
- function unconfirmedReconnectMs() {
179
- const raw = process.env.OREZ_REPLICATION_UNCONFIRMED_RECONNECT_MS;
185
+ function unconfirmedReconnectMs(state) {
186
+ const raw = state.unconfirmedReconnectMs ?? process.env.OREZ_REPLICATION_UNCONFIRMED_RECONNECT_MS;
180
187
  if (!raw)
181
188
  return DEFAULT_UNCONFIRMED_RECONNECT_MS;
182
189
  const parsed = Number(raw);
183
190
  return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_UNCONFIRMED_RECONNECT_MS;
184
191
  }
185
192
  /** reset module state (for tests) */
186
- export function resetReplicationState() {
187
- currentLsn = lsnFloorFromTime();
188
- lastStreamedWatermark = 0;
189
- pendingConfirmLsn = 0n;
190
- processedConfirmLsn = 0n;
191
- lastFeedbackAtPerf = 0;
192
- lastWriteSignalAt = 0;
193
- lastConfirmProgressAt = 0;
194
- lastStreamActivityAt = 0;
195
- cachedTableKeyColumns = null;
196
- cachedExcludedColumns = null;
197
- cachedColumnTypeOids = null;
198
- cachedColumns.clear();
193
+ export function resetReplicationState(instanceId, env, instanceLog) {
194
+ const state = newReplicationState(instanceId, instanceLog);
195
+ const runtimeEnv = env ?? (instanceId === undefined ? process.env : undefined);
196
+ state.publications = runtimeEnv?.ZERO_APP_PUBLICATIONS?.trim();
197
+ state.unconfirmedReconnectMs = runtimeEnv?.OREZ_REPLICATION_UNCONFIRMED_RECONNECT_MS;
198
+ replicationStates.set(instanceId ?? DEFAULT_REPLICATION_INSTANCE, state);
199
+ }
200
+ export function deleteReplicationState(instanceId) {
201
+ replicationStates.delete(instanceId);
199
202
  }
200
- function nextLsn() {
203
+ function nextLsn(state) {
201
204
  const floor = lsnFloorFromTime();
202
- if (currentLsn < floor)
203
- currentLsn = floor;
204
- currentLsn += LSN_INCREMENT;
205
- return currentLsn;
205
+ if (state.currentLsn < floor)
206
+ state.currentLsn = floor;
207
+ state.currentLsn += LSN_INCREMENT;
208
+ return state.currentLsn;
206
209
  }
207
210
  function lsnToString(lsn) {
208
211
  const high = Number(lsn >> 32n);
@@ -356,17 +359,18 @@ function buildErrorResponse(message) {
356
359
  * handle a replication query. returns response bytes or null if not handled.
357
360
  * async because slot operations need to write to pglite.
358
361
  */
359
- export async function handleReplicationQuery(query, db) {
362
+ export async function handleReplicationQuery(query, db, instanceId) {
363
+ const state = replicationState(instanceId);
360
364
  const trimmed = query.trim().replace(/;$/, '').trim();
361
365
  const upper = trimmed.toUpperCase();
362
366
  if (upper === 'IDENTIFY_SYSTEM') {
363
- const lsn = lsnToString(currentLsn);
367
+ const lsn = lsnToString(state.currentLsn);
364
368
  return buildSimpleResponse(['systemid', 'timeline', 'xlogpos', 'dbname'], ['1234567890', '1', lsn, 'postgres']);
365
369
  }
366
370
  if (upper.startsWith('CREATE_REPLICATION_SLOT')) {
367
371
  const match = trimmed.match(/CREATE_REPLICATION_SLOT\s+(?:"([^"]+)"|'([^']+)'|(\S+))/i);
368
372
  const slotName = match?.[1] || match?.[2] || match?.[3] || 'zero_slot';
369
- const lsn = lsnToString(nextLsn());
373
+ const lsn = lsnToString(nextLsn(state));
370
374
  const snapshotName = `00000003-00000001-1`;
371
375
  // set watermark to current DB state so replication only delivers changes
372
376
  // that happen AFTER this point. this mirrors real postgres behavior where
@@ -375,8 +379,8 @@ export async function handleReplicationQuery(query, db) {
375
379
  // on reconnect this is effectively a no-op since the watermark is already
376
380
  // at or past the current DB state.
377
381
  const currentWm = await getCurrentWatermark(db);
378
- if (currentWm > lastStreamedWatermark) {
379
- lastStreamedWatermark = currentWm;
382
+ if (currentWm > state.lastStreamedWatermark) {
383
+ state.lastStreamedWatermark = currentWm;
380
384
  }
381
385
  // a new slot means the consumer re-initializes from a fresh snapshot:
382
386
  // rows up to the snapshot point are covered by the initial copy, and any
@@ -384,7 +388,7 @@ export async function handleReplicationQuery(query, db) {
384
388
  await purgeConsumedChanges(db, currentWm);
385
389
  await clearStreamedBatches(db);
386
390
  // the log was just cut to the snapshot point — the backlog is not stuck.
387
- markReplicationProgress();
391
+ markReplicationProgress(instanceId);
388
392
  // persist slot so pg_replication_slots queries find it
389
393
  await db.query(`INSERT INTO _orez._zero_replication_slots (slot_name, restart_lsn, confirmed_flush_lsn)
390
394
  VALUES ($1, $2, $2)
@@ -425,9 +429,10 @@ export async function handleReplicationQuery(query, db) {
425
429
  * start streaming replication changes to the client.
426
430
  * this runs indefinitely until the connection is closed.
427
431
  */
428
- export async function handleStartReplication(query, writer, db, mutex) {
432
+ export async function handleStartReplication(query, writer, db, mutex, instanceId) {
433
+ const state = replicationState(instanceId);
429
434
  log.debug.repl('entering streaming mode');
430
- lastStreamActivityAt = Date.now();
435
+ state.lastStreamActivityAt = Date.now();
431
436
  // honor zero-cache's resume LSN. without this, after a page reload the
432
437
  // in-memory currentLsn / lastStreamedWatermark are reset to defaults but
433
438
  // changeLog persists with prior LSNs — re-streaming from BIGINT 0 makes
@@ -438,9 +443,9 @@ export async function handleStartReplication(query, writer, db, mutex) {
438
443
  // lastStreamedWatermark to the current sequence value we skip already-
439
444
  // streamed _zero_changes rows.
440
445
  const clientStartLsn = extractStartLsn(query);
441
- if (clientStartLsn !== null && clientStartLsn > currentLsn) {
442
- log.debug.repl(`advancing currentLsn ${lsnToString(currentLsn)} → ${lsnToString(clientStartLsn)} from client START_REPLICATION`);
443
- currentLsn = clientStartLsn;
446
+ if (clientStartLsn !== null && clientStartLsn > state.currentLsn) {
447
+ log.debug.repl(`advancing currentLsn ${lsnToString(state.currentLsn)} → ${lsnToString(clientStartLsn)} from client START_REPLICATION`);
448
+ state.currentLsn = clientStartLsn;
444
449
  }
445
450
  // send CopyBothResponse to enter streaming mode
446
451
  const copyBoth = new Uint8Array(1 + 4 + 1 + 2);
@@ -482,12 +487,12 @@ export async function handleStartReplication(query, writer, db, mutex) {
482
487
  // feed the bankruptcy monitor's confirm gauge, or a reconnect livelock
483
488
  // (stream → no confirm → reconnect → repeat) looks healthy forever.
484
489
  if (purged > 0)
485
- markReplicationProgress();
490
+ markReplicationProgress(instanceId);
486
491
  await clearStreamedBatches(db);
487
492
  const resumeWm = await getStreamResumeWatermark(db);
488
- if (resumeWm !== lastStreamedWatermark) {
489
- log.debug.repl(`moving lastStreamedWatermark ${lastStreamedWatermark} → ${resumeWm} on reconnect`);
490
- lastStreamedWatermark = resumeWm;
493
+ if (resumeWm !== state.lastStreamedWatermark) {
494
+ log.debug.repl(`moving lastStreamedWatermark ${state.lastStreamedWatermark} → ${resumeWm} on reconnect`);
495
+ state.lastStreamedWatermark = resumeWm;
491
496
  }
492
497
  }
493
498
  catch (err) {
@@ -497,18 +502,20 @@ export async function handleStartReplication(query, writer, db, mutex) {
497
502
  mutex.release();
498
503
  }
499
504
  }
500
- let lastWatermark = lastStreamedWatermark;
505
+ let lastWatermark = state.lastStreamedWatermark;
501
506
  // use cached setup results on reconnect to avoid holding the mutex
502
507
  // for seconds doing trigger installation + schema queries. zero-cache
503
508
  // disconnects if its proxy queries are blocked too long by the mutex.
504
509
  let tableKeyColumns;
505
510
  let excludedColumns;
506
511
  let columnTypeOids;
507
- if (cachedTableKeyColumns && cachedExcludedColumns && cachedColumnTypeOids) {
512
+ if (state.cachedTableKeyColumns &&
513
+ state.cachedExcludedColumns &&
514
+ state.cachedColumnTypeOids) {
508
515
  log.debug.repl('reconnect: using cached setup (skipping mutex)');
509
- tableKeyColumns = cachedTableKeyColumns;
510
- excludedColumns = cachedExcludedColumns;
511
- columnTypeOids = cachedColumnTypeOids;
516
+ tableKeyColumns = state.cachedTableKeyColumns;
517
+ excludedColumns = state.cachedExcludedColumns;
518
+ columnTypeOids = state.cachedColumnTypeOids;
512
519
  }
513
520
  else {
514
521
  tableKeyColumns = new Map();
@@ -526,7 +533,7 @@ export async function handleStartReplication(query, writer, db, mutex) {
526
533
  // install change tracking triggers on shard schema tables (e.g. chat_0.clients)
527
534
  await installTriggersOnShardTables(db);
528
535
  // set up LISTEN + install notify triggers in one batch
529
- const pubName = process.env.ZERO_APP_PUBLICATIONS?.trim();
536
+ const pubName = state.publications;
530
537
  let tables;
531
538
  if (pubName) {
532
539
  const result = await db.query(`SELECT tablename FROM pg_publication_tables
@@ -647,9 +654,9 @@ export async function handleStartReplication(query, writer, db, mutex) {
647
654
  log.debug.proxy(`excluding unsupported columns: ${[...excludedColumns.entries()].map(([t, c]) => `${t}(${[...c].join(',')})`).join(', ')}`);
648
655
  }
649
656
  // cache for subsequent reconnects
650
- cachedTableKeyColumns = tableKeyColumns;
651
- cachedExcludedColumns = excludedColumns;
652
- cachedColumnTypeOids = columnTypeOids;
657
+ state.cachedTableKeyColumns = tableKeyColumns;
658
+ state.cachedExcludedColumns = excludedColumns;
659
+ state.cachedColumnTypeOids = columnTypeOids;
653
660
  }
654
661
  finally {
655
662
  mutex.release();
@@ -697,14 +704,14 @@ export async function handleStartReplication(query, writer, db, mutex) {
697
704
  });
698
705
  };
699
706
  const flushConfirmedBatches = async () => {
700
- if (pendingConfirmLsn <= processedConfirmLsn)
707
+ if (state.pendingConfirmLsn <= state.processedConfirmLsn)
701
708
  return;
702
709
  await mutex.acquire();
703
- const target = pendingConfirmLsn;
710
+ const target = state.pendingConfirmLsn;
704
711
  try {
705
712
  const purged = await confirmStreamedBatches(db, target);
706
- processedConfirmLsn = target;
707
- markReplicationProgress();
713
+ state.processedConfirmLsn = target;
714
+ markReplicationProgress(instanceId);
708
715
  if (newestUnconfirmedLsn > 0n && target >= newestUnconfirmedLsn) {
709
716
  oldestUnconfirmedAt = 0;
710
717
  oldestUnconfirmedLsn = 0n;
@@ -741,8 +748,8 @@ export async function handleStartReplication(query, writer, db, mutex) {
741
748
  // discards the batch mappings and restarts the chew from zero — the
742
749
  // reconnect livelock that burned $50-65/day of DO rows-written on the
743
750
  // 2026-07 CF incident. reconnect is for a dead wire, not a slow consumer.
744
- const sinceSignal = performance.now() - Math.max(oldestUnconfirmedAt, lastFeedbackAtPerf);
745
- const timeoutMs = unconfirmedReconnectMs();
751
+ const sinceSignal = performance.now() - Math.max(oldestUnconfirmedAt, state.lastFeedbackAtPerf);
752
+ const timeoutMs = unconfirmedReconnectMs(state);
746
753
  if (sinceSignal < timeoutMs)
747
754
  return false;
748
755
  log.repl(`unconfirmed streamed batch stale for ${sinceSignal.toFixed(0)}ms with no consumer feedback; closing replication stream for reconnect`);
@@ -751,8 +758,11 @@ export async function handleStartReplication(query, writer, db, mutex) {
751
758
  return true;
752
759
  };
753
760
  // register direct wakeup so the proxy can signal us immediately
754
- _replicationWakeup = wakeup;
755
- globalThis.__orez_signal_replication = wakeup;
761
+ state.replicationWakeup = wakeup;
762
+ // expose on globalThis so external code (e.g. pglite-pool) can signal
763
+ // without importing from this module (works across separate bundles)
764
+ if (instanceId === undefined)
765
+ globalThis.__orez_signal_replication = wakeup;
756
766
  // also set up LISTEN as secondary signal
757
767
  let unsubscribe = null;
758
768
  try {
@@ -793,7 +803,7 @@ export async function handleStartReplication(query, writer, db, mutex) {
793
803
  // as its downstream commits, instead of at its own leisurely
794
804
  // cadence — without this the first ack routinely lost the race
795
805
  // against the stale-unconfirmed reconnect (2026-07 CF incident).
796
- writer.write(encodeKeepalive(currentLsn, nowMicros(), newestUnconfirmedLsn > 0n));
806
+ writer.write(encodeKeepalive(state.currentLsn, nowMicros(), newestUnconfirmedLsn > 0n));
797
807
  log.debug.repl(`idle keepalive (lastWatermark=${lastWatermark})`);
798
808
  // re-scan for new shard schemas during idle
799
809
  if (performance.now() - lastShardRescan > shardRescanIntervalMs) {
@@ -925,7 +935,7 @@ export async function handleStartReplication(query, writer, db, mutex) {
925
935
  log.debug.repl(`filter: ${preFilterCount} → ${changes.length} changes`);
926
936
  if (changes.length === 0) {
927
937
  lastWatermark = batchEnd;
928
- lastStreamedWatermark = batchEnd;
938
+ state.lastStreamedWatermark = batchEnd;
929
939
  // all changes were filtered out (e.g. shard internal tables).
930
940
  // they never reach the consumer by design, so no confirmation
931
941
  // will ever cover them — purge directly.
@@ -942,14 +952,14 @@ export async function handleStartReplication(query, writer, db, mutex) {
942
952
  queryPending = true;
943
953
  continue;
944
954
  }
945
- await ensureMetadataForChangedTables(db, mutex, changes, tableKeyColumns, excludedColumns, columnTypeOids);
955
+ await ensureMetadataForChangedTables(db, mutex, changes, tableKeyColumns, excludedColumns, columnTypeOids, state);
946
956
  // allocate the batch's commit LSN and durably record the
947
957
  // lsn → batch-end mapping BEFORE the rows hit the wire: if the
948
958
  // consumer commits the batch and this process dies before
949
959
  // persisting the mapping, the reconnect reconciliation would
950
960
  // re-stream an already-committed transaction (duplicate apply).
951
- const batchLsn = nextLsn();
952
- const batchEndLsn = nextLsn();
961
+ const batchLsn = nextLsn(state);
962
+ const batchEndLsn = nextLsn(state);
953
963
  await mutex.acquire();
954
964
  try {
955
965
  await recordStreamedBatch(db, batchLsn, batchEnd);
@@ -958,11 +968,11 @@ export async function handleStartReplication(query, writer, db, mutex) {
958
968
  mutex.release();
959
969
  }
960
970
  log.debug.repl(`streaming ${changes.length} changes to writer`);
961
- await streamChanges(changes, writer, sentRelations, txCounter++, batchLsn, batchEndLsn, tableKeyColumns, excludedColumns, columnTypeOids);
971
+ await streamChanges(changes, writer, sentRelations, txCounter++, batchLsn, batchEndLsn, tableKeyColumns, excludedColumns, columnTypeOids, state);
962
972
  markBatchAwaitingConfirmation(batchLsn);
963
- lastStreamActivityAt = Date.now();
973
+ state.lastStreamActivityAt = Date.now();
964
974
  lastWatermark = batchEnd;
965
- lastStreamedWatermark = batchEnd;
975
+ state.lastStreamedWatermark = batchEnd;
966
976
  log.debug.repl(`streamed ok, watermark=${batchEnd}`);
967
977
  hasStreamedOnce = true;
968
978
  // got changes - continue immediately to check for more
@@ -972,7 +982,7 @@ export async function handleStartReplication(query, writer, db, mutex) {
972
982
  // no changes: send keepalive (reply requested while batches await
973
983
  // confirmation — see the idle-timeout keepalive above)
974
984
  const ts = nowMicros();
975
- writer.write(encodeKeepalive(currentLsn, ts, newestUnconfirmedLsn > 0n));
985
+ writer.write(encodeKeepalive(state.currentLsn, ts, newestUnconfirmedLsn > 0n));
976
986
  log.debug.repl(`idle (lastWatermark=${lastWatermark})`);
977
987
  // next iteration will wait for signal at the top
978
988
  }
@@ -996,8 +1006,8 @@ export async function handleStartReplication(query, writer, db, mutex) {
996
1006
  }
997
1007
  finally {
998
1008
  // only clear if still pointing to our wakeup (a new handler may have replaced it)
999
- if (_replicationWakeup === wakeup) {
1000
- _replicationWakeup = null;
1009
+ if (state.replicationWakeup === wakeup) {
1010
+ state.replicationWakeup = null;
1001
1011
  }
1002
1012
  if (unsubscribe) {
1003
1013
  await unsubscribe().catch(() => { });
@@ -1005,7 +1015,7 @@ export async function handleStartReplication(query, writer, db, mutex) {
1005
1015
  }
1006
1016
  log.repl('poll loop exited');
1007
1017
  }
1008
- async function ensureMetadataForChangedTables(db, mutex, changes, tableKeyColumns, excludedColumns, columnTypeOids) {
1018
+ async function ensureMetadataForChangedTables(db, mutex, changes, tableKeyColumns, excludedColumns, columnTypeOids, state) {
1009
1019
  const missing = new Map();
1010
1020
  for (const change of changes) {
1011
1021
  if (tableKeyColumns.has(change.table_name) ||
@@ -1074,18 +1084,16 @@ async function ensureMetadataForChangedTables(db, mutex, changes, tableKeyColumn
1074
1084
  }
1075
1085
  log.debug.repl(`refreshed metadata for ${missing.size} late table(s): ${[...missing.keys()].join(',')}`);
1076
1086
  for (const key of missing.keys())
1077
- cachedColumns.delete(key);
1087
+ state.cachedColumns.delete(key);
1078
1088
  }
1079
1089
  finally {
1080
1090
  mutex.release();
1081
1091
  }
1082
1092
  }
1083
- // cache column info per table to avoid per-change allocation
1084
- const cachedColumns = new Map();
1085
1093
  async function streamChanges(changes, writer, sentRelations, txId,
1086
1094
  // the batch's commit LSN and commit-end LSN are allocated by the caller so
1087
1095
  // the lsn → batch-end mapping can be durably recorded before streaming.
1088
- lsn, endLsn, tableKeyColumns, excludedColumns, columnTypeOids) {
1096
+ lsn, endLsn, tableKeyColumns, excludedColumns, columnTypeOids, state) {
1089
1097
  const ts = nowMicros();
1090
1098
  // collect all encoded messages into a list, then batch-write
1091
1099
  // to minimize syscalls (each writer.write → socket.write is a syscall)
@@ -1120,7 +1128,7 @@ lsn, endLsn, tableKeyColumns, excludedColumns, columnTypeOids) {
1120
1128
  if (!row)
1121
1129
  continue;
1122
1130
  // use cached columns or build and cache them
1123
- let columns = cachedColumns.get(qualifiedKey);
1131
+ let columns = state.cachedColumns.get(qualifiedKey);
1124
1132
  if (!columns) {
1125
1133
  const keySet = tableKeyColumns.get(qualifiedKey);
1126
1134
  const typeOids = columnTypeOids.get(qualifiedKey);
@@ -1129,7 +1137,7 @@ lsn, endLsn, tableKeyColumns, excludedColumns, columnTypeOids) {
1129
1137
  typeOid: typeOids?.get(col.name) ?? col.typeOid,
1130
1138
  isKey: keySet?.has(col.name) ?? false,
1131
1139
  }));
1132
- cachedColumns.set(qualifiedKey, columns);
1140
+ state.cachedColumns.set(qualifiedKey, columns);
1133
1141
  }
1134
1142
  // send RELATION if not yet sent
1135
1143
  if (!sentRelations.has(qualifiedKey)) {
@@ -1174,9 +1182,32 @@ lsn, endLsn, tableKeyColumns, excludedColumns, columnTypeOids) {
1174
1182
  }
1175
1183
  // hook for arch instrumentation (soot-arch sq-write events)
1176
1184
  const hook = globalThis.__orez_on_repl_commit;
1185
+ const tables = new Set(changes.map((change) => change.table_name));
1186
+ try {
1187
+ state.log?.({
1188
+ changes: changes.length,
1189
+ component: 'replication',
1190
+ event: 'commit',
1191
+ instanceId: state.instanceId,
1192
+ tables: [...tables],
1193
+ txId,
1194
+ });
1195
+ }
1196
+ catch {
1197
+ // diagnostics cannot break replication
1198
+ }
1177
1199
  if (hook) {
1178
- const tables = new Set(changes.map((c) => c.table_name));
1179
- hook({ changes: changes.length, tables: [...tables], txId });
1200
+ try {
1201
+ hook({
1202
+ changes: changes.length,
1203
+ instanceId: state.instanceId,
1204
+ tables: [...tables],
1205
+ txId,
1206
+ });
1207
+ }
1208
+ catch {
1209
+ // instrumentation cannot break replication
1210
+ }
1180
1211
  }
1181
1212
  }
1182
1213
  function normalizeShardClientsRow(row) {