@rotorsoft/act-pg 1.13.1 → 1.13.3

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.
@@ -188,6 +188,34 @@ export declare class PostgresStore implements Store {
188
188
  * connection would re-fire the stale handler.
189
189
  */
190
190
  private _listen_handler;
191
+ /**
192
+ * Error listener attached to the active LISTEN client. node-postgres
193
+ * removes its idle-error guard on checkout, so a checked-out client
194
+ * that emits `error` (backend restart, failover, network drop) with no
195
+ * listener is an uncaught exception — a process crash (#1189). Tracked
196
+ * alongside `_listen_handler` so teardown detaches it in lockstep.
197
+ */
198
+ private _listen_error_handler;
199
+ /**
200
+ * The caller's notification handler for the active subscription, kept
201
+ * so the self-healing reconnect path (#1189) can re-establish LISTEN
202
+ * on a fresh client after the dedicated one emits `error`. Cleared by
203
+ * `_teardown_listen`, which is what makes disposal cancel any pending
204
+ * reconnect.
205
+ */
206
+ private _notify_handler;
207
+ /**
208
+ * Pending reconnect timer, if a LISTEN client error scheduled one.
209
+ * Tracked so `_teardown_listen` (and therefore `dispose()`) can cancel
210
+ * it — a reconnect must never fire after teardown.
211
+ */
212
+ private _reconnect_timer;
213
+ /**
214
+ * Consecutive reconnect attempts since the last healthy LISTEN, used to
215
+ * grow the capped exponential backoff. Reset to 0 once a re-LISTEN
216
+ * succeeds.
217
+ */
218
+ private _reconnect_attempts;
191
219
  /**
192
220
  * Cross-process commit subscription. **Present only when
193
221
  * `config.notify === true`** — the orchestrator's auto-wire path
@@ -228,11 +256,17 @@ export declare class PostgresStore implements Store {
228
256
  */
229
257
  dispose(): Promise<void>;
230
258
  /**
231
- * Tear down the active LISTEN subscription if any: detach the
232
- * notification listener, run UNLISTEN, and destroy the dedicated
233
- * client (do not return it to the pool its listener is removed but
234
- * destroying belt-and-braces guards against any future change in
235
- * pg-pool semantics that could re-issue a half-clean client).
259
+ * Tear down the active LISTEN subscription if any: cancel any pending
260
+ * reconnect, forget the caller's handler (so no reconnect can fire
261
+ * after teardown), detach the notification + error listeners, run
262
+ * UNLISTEN, and destroy the dedicated client (do not return it to the
263
+ * pool its listeners are removed but destroying belt-and-braces
264
+ * guards against any future change in pg-pool semantics that could
265
+ * re-issue a half-clean client).
266
+ *
267
+ * Clearing `_notify_handler` and the reconnect timer here is what makes
268
+ * `dispose()` safe during a pending reconnect (#1189): a scheduled
269
+ * `_reconnect` bails the moment it finds no handler.
236
270
  */
237
271
  private _teardown_listen;
238
272
  /**
@@ -437,10 +471,35 @@ export declare class PostgresStore implements Store {
437
471
  * client; pool disposal also tears the subscription down as a safety
438
472
  * net.
439
473
  *
474
+ * The subscription is **self-healing** (#1189): the dedicated client
475
+ * has an `error` listener that, on a connection blip (backend restart,
476
+ * failover, network drop), tears the dead client down and re-LISTENs
477
+ * on a fresh one with capped exponential backoff — degrading to the
478
+ * poll path in between. A pending reconnect is cancelled by disposal,
479
+ * so no reconnect fires after teardown.
480
+ *
440
481
  * @param handler Called for each cross-process commit notification.
441
482
  * @returns Disposer that releases the LISTEN client.
442
483
  */
443
484
  private _subscribe_notifications;
485
+ /**
486
+ * Check out a dedicated client, attach the notification + error
487
+ * listeners, and run `LISTEN`. Shared by the initial subscription and
488
+ * every reconnect (#1189). On any failure before `LISTEN` succeeds the
489
+ * client is detached and destroyed so nothing leaks — the caller
490
+ * decides whether to propagate (initial subscribe) or reschedule
491
+ * (reconnect).
492
+ */
493
+ private _open_listen;
494
+ /**
495
+ * Self-heal the LISTEN subscription after the dedicated client emitted
496
+ * `error` (#1189). Detaches and destroys the dead client, then
497
+ * reconnects on a fresh one with capped exponential backoff. Bails
498
+ * immediately if the subscription was disposed while a reconnect was
499
+ * pending (`_notify_handler` cleared by `_teardown_listen`), so no
500
+ * reconnect ever fires after teardown.
501
+ */
502
+ private _reconnect;
444
503
  /**
445
504
  * Atomically truncates streams and seeds each with a snapshot or tombstone.
446
505
  * Windowed targets (`before` set) prune the prefix below the closest safe
@@ -1 +1 @@
1
- {"version":3,"file":"postgres-store.d.ts","sourceRoot":"","sources":["../../src/postgres-store.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"postgres-store.d.ts","sourceRoot":"","sources":["../../src/postgres-store.ts"],"names":[],"mappings":"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"}
package/dist/index.cjs CHANGED
@@ -51,6 +51,7 @@ var dateReviver = (_key, value) => {
51
51
 
52
52
  // src/postgres-store.ts
53
53
  var logger = (0, import_act.log)();
54
+ var SOURCE_METACHARACTER_CLASS = "[]^$.*+?()[{}|\\\\]";
54
55
  var { Pool, types } = import_pg.default;
55
56
  types.setTypeParser(
56
57
  types.builtins.JSONB,
@@ -60,6 +61,8 @@ var SAFE_IDENTIFIER = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
60
61
  var PG_UNIQUE_VIOLATION = "23505";
61
62
  var NOTIFY_CHANNEL_PREFIX = "act_commit";
62
63
  var NOTIFY_MAX_PAYLOAD_BYTES = 8e3;
64
+ var NOTIFY_RECONNECT_BASE_MS = 250;
65
+ var NOTIFY_RECONNECT_MAX_MS = 3e4;
63
66
  function notify_channel(schema, table) {
64
67
  return `${NOTIFY_CHANNEL_PREFIX}_${schema}_${table}`;
65
68
  }
@@ -132,6 +135,34 @@ var PostgresStore = class {
132
135
  * connection would re-fire the stale handler.
133
136
  */
134
137
  _listen_handler;
138
+ /**
139
+ * Error listener attached to the active LISTEN client. node-postgres
140
+ * removes its idle-error guard on checkout, so a checked-out client
141
+ * that emits `error` (backend restart, failover, network drop) with no
142
+ * listener is an uncaught exception — a process crash (#1189). Tracked
143
+ * alongside `_listen_handler` so teardown detaches it in lockstep.
144
+ */
145
+ _listen_error_handler;
146
+ /**
147
+ * The caller's notification handler for the active subscription, kept
148
+ * so the self-healing reconnect path (#1189) can re-establish LISTEN
149
+ * on a fresh client after the dedicated one emits `error`. Cleared by
150
+ * `_teardown_listen`, which is what makes disposal cancel any pending
151
+ * reconnect.
152
+ */
153
+ _notify_handler;
154
+ /**
155
+ * Pending reconnect timer, if a LISTEN client error scheduled one.
156
+ * Tracked so `_teardown_listen` (and therefore `dispose()`) can cancel
157
+ * it — a reconnect must never fire after teardown.
158
+ */
159
+ _reconnect_timer;
160
+ /**
161
+ * Consecutive reconnect attempts since the last healthy LISTEN, used to
162
+ * grow the capped exponential backoff. Reset to 0 once a re-LISTEN
163
+ * succeeds.
164
+ */
165
+ _reconnect_attempts = 0;
135
166
  /**
136
167
  * Cross-process commit subscription. **Present only when
137
168
  * `config.notify === true`** — the orchestrator's auto-wire path
@@ -199,16 +230,30 @@ var PostgresStore = class {
199
230
  await this._pool.end();
200
231
  }
201
232
  /**
202
- * Tear down the active LISTEN subscription if any: detach the
203
- * notification listener, run UNLISTEN, and destroy the dedicated
204
- * client (do not return it to the pool its listener is removed but
205
- * destroying belt-and-braces guards against any future change in
206
- * pg-pool semantics that could re-issue a half-clean client).
233
+ * Tear down the active LISTEN subscription if any: cancel any pending
234
+ * reconnect, forget the caller's handler (so no reconnect can fire
235
+ * after teardown), detach the notification + error listeners, run
236
+ * UNLISTEN, and destroy the dedicated client (do not return it to the
237
+ * pool its listeners are removed but destroying belt-and-braces
238
+ * guards against any future change in pg-pool semantics that could
239
+ * re-issue a half-clean client).
240
+ *
241
+ * Clearing `_notify_handler` and the reconnect timer here is what makes
242
+ * `dispose()` safe during a pending reconnect (#1189): a scheduled
243
+ * `_reconnect` bails the moment it finds no handler.
207
244
  */
208
245
  async _teardown_listen() {
246
+ if (this._reconnect_timer) {
247
+ clearTimeout(this._reconnect_timer);
248
+ this._reconnect_timer = void 0;
249
+ }
250
+ this._notify_handler = void 0;
251
+ this._reconnect_attempts = 0;
209
252
  if (!this._listen_client) return;
210
253
  this._listen_client.removeListener("notification", this._listen_handler);
254
+ this._listen_client.removeListener("error", this._listen_error_handler);
211
255
  this._listen_handler = void 0;
256
+ this._listen_error_handler = void 0;
212
257
  try {
213
258
  await this._listen_client.query(`UNLISTEN ${this._channel}`);
214
259
  } catch {
@@ -272,7 +317,7 @@ var PostgresStore = class {
272
317
  stream varchar(100) COLLATE pg_catalog."default" PRIMARY KEY,
273
318
  source varchar(100) COLLATE pg_catalog."default",
274
319
  at int NOT NULL DEFAULT -1,
275
- retry smallint NOT NULL DEFAULT -1,
320
+ retry int NOT NULL DEFAULT -1,
276
321
  blocked boolean NOT NULL DEFAULT false,
277
322
  error text,
278
323
  leased_by text,
@@ -294,6 +339,21 @@ var PostgresStore = class {
294
339
  `ALTER TABLE ${this._fqs}
295
340
  ADD COLUMN IF NOT EXISTS deferred_at timestamptz;`
296
341
  );
342
+ await client.query(
343
+ `DO $$
344
+ BEGIN
345
+ IF EXISTS (
346
+ SELECT 1 FROM information_schema.columns
347
+ WHERE table_schema = '${this.config.schema}'
348
+ AND table_name = '${this.config.table}_streams'
349
+ AND column_name = 'retry'
350
+ AND data_type = 'smallint'
351
+ ) THEN
352
+ EXECUTE 'ALTER TABLE ${this._fqs} ALTER COLUMN retry TYPE integer';
353
+ END IF;
354
+ END
355
+ $$;`
356
+ );
297
357
  await client.query(
298
358
  `DROP INDEX IF EXISTS "${this.config.schema}"."${this.config.table}_streams_fetch_ix"`
299
359
  );
@@ -554,7 +614,16 @@ var PostgresStore = class {
554
614
  SELECT 1 FROM ${this._fqt} e
555
615
  WHERE e.id > s.at
556
616
  AND e.name <> '${import_act.SNAP_EVENT}'
557
- AND (s.source IS NULL OR e.stream = COALESCE(s.source, s.stream))
617
+ -- Literal source (no regex metacharacter) matches by
618
+ -- equality \u2014 index-friendly, and exact so "s1" never
619
+ -- claims "s12". A pattern source (e.g. '^(A|B)$') matches
620
+ -- with the POSIX regex operator so the calculator's static
621
+ -- regex reaction is claimed for every stream it anchors.
622
+ AND (
623
+ s.source IS NULL
624
+ OR (s.source !~ '${SOURCE_METACHARACTER_CLASS}' AND e.stream = s.source)
625
+ OR (s.source ~ '${SOURCE_METACHARACTER_CLASS}' AND e.stream ~ s.source)
626
+ )
558
627
  LIMIT 1
559
628
  ))
560
629
  FOR UPDATE SKIP LOCKED
@@ -1180,11 +1249,39 @@ var PostgresStore = class {
1180
1249
  * client; pool disposal also tears the subscription down as a safety
1181
1250
  * net.
1182
1251
  *
1252
+ * The subscription is **self-healing** (#1189): the dedicated client
1253
+ * has an `error` listener that, on a connection blip (backend restart,
1254
+ * failover, network drop), tears the dead client down and re-LISTENs
1255
+ * on a fresh one with capped exponential backoff — degrading to the
1256
+ * poll path in between. A pending reconnect is cancelled by disposal,
1257
+ * so no reconnect fires after teardown.
1258
+ *
1183
1259
  * @param handler Called for each cross-process commit notification.
1184
1260
  * @returns Disposer that releases the LISTEN client.
1185
1261
  */
1186
1262
  async _subscribe_notifications(handler) {
1187
1263
  await this._teardown_listen();
1264
+ this._notify_handler = handler;
1265
+ try {
1266
+ await this._open_listen(handler);
1267
+ } catch (err) {
1268
+ this._notify_handler = void 0;
1269
+ throw err;
1270
+ }
1271
+ return async () => {
1272
+ if (this._notify_handler !== handler) return;
1273
+ await this._teardown_listen();
1274
+ };
1275
+ }
1276
+ /**
1277
+ * Check out a dedicated client, attach the notification + error
1278
+ * listeners, and run `LISTEN`. Shared by the initial subscription and
1279
+ * every reconnect (#1189). On any failure before `LISTEN` succeeds the
1280
+ * client is detached and destroyed so nothing leaks — the caller
1281
+ * decides whether to propagate (initial subscribe) or reschedule
1282
+ * (reconnect).
1283
+ */
1284
+ async _open_listen(handler) {
1188
1285
  const client = await this._client("notify");
1189
1286
  const on_notification = (msg) => {
1190
1287
  if (msg.channel !== this._channel) return;
@@ -1223,20 +1320,59 @@ var PostgresStore = class {
1223
1320
  logger.error(err, "act_commit: handler threw, listener preserved");
1224
1321
  }
1225
1322
  };
1323
+ const on_error = (err) => {
1324
+ logger.error(err, "act_commit: LISTEN client errored, reconnecting");
1325
+ this._reconnect();
1326
+ };
1226
1327
  client.on("notification", on_notification);
1328
+ client.on("error", on_error);
1227
1329
  try {
1228
1330
  await client.query(`LISTEN ${this._channel}`);
1229
1331
  } catch (err) {
1230
1332
  client.removeListener("notification", on_notification);
1333
+ client.removeListener("error", on_error);
1231
1334
  client.release(true);
1232
1335
  throw err;
1233
1336
  }
1234
1337
  this._listen_client = client;
1235
1338
  this._listen_handler = on_notification;
1236
- return async () => {
1237
- if (this._listen_client !== client) return;
1238
- await this._teardown_listen();
1239
- };
1339
+ this._listen_error_handler = on_error;
1340
+ this._reconnect_attempts = 0;
1341
+ }
1342
+ /**
1343
+ * Self-heal the LISTEN subscription after the dedicated client emitted
1344
+ * `error` (#1189). Detaches and destroys the dead client, then
1345
+ * reconnects on a fresh one with capped exponential backoff. Bails
1346
+ * immediately if the subscription was disposed while a reconnect was
1347
+ * pending (`_notify_handler` cleared by `_teardown_listen`), so no
1348
+ * reconnect ever fires after teardown.
1349
+ */
1350
+ _reconnect() {
1351
+ const handler = this._notify_handler;
1352
+ if (!handler) return;
1353
+ if (this._listen_client) {
1354
+ this._listen_client.removeListener("notification", this._listen_handler);
1355
+ this._listen_client.removeListener("error", this._listen_error_handler);
1356
+ this._listen_handler = void 0;
1357
+ this._listen_error_handler = void 0;
1358
+ this._listen_client.release(true);
1359
+ this._listen_client = void 0;
1360
+ }
1361
+ const delay = Math.min(
1362
+ NOTIFY_RECONNECT_MAX_MS,
1363
+ NOTIFY_RECONNECT_BASE_MS * 2 ** this._reconnect_attempts
1364
+ );
1365
+ this._reconnect_attempts++;
1366
+ this._reconnect_timer = setTimeout(() => {
1367
+ this._reconnect_timer = void 0;
1368
+ const current = this._notify_handler;
1369
+ if (!current) return;
1370
+ this._open_listen(current).catch((err) => {
1371
+ logger.error(err, "act_commit: LISTEN reconnect failed, retrying");
1372
+ this._reconnect();
1373
+ });
1374
+ }, delay);
1375
+ this._reconnect_timer.unref?.();
1240
1376
  }
1241
1377
  /**
1242
1378
  * Atomically truncates streams and seeds each with a snapshot or tombstone.