@rotorsoft/act-pg 1.10.0 → 1.10.2

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/index.js CHANGED
@@ -33,6 +33,7 @@ types.setTypeParser(
33
33
  var SAFE_IDENTIFIER = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
34
34
  var PG_UNIQUE_VIOLATION = "23505";
35
35
  var NOTIFY_CHANNEL_PREFIX = "act_commit";
36
+ var NOTIFY_MAX_PAYLOAD_BYTES = 8e3;
36
37
  function notify_channel(schema, table) {
37
38
  return `${NOTIFY_CHANNEL_PREFIX}_${schema}_${table}`;
38
39
  }
@@ -48,7 +49,35 @@ var DEFAULT_CONFIG = {
48
49
  password: "postgres",
49
50
  schema: "public",
50
51
  table: "events",
51
- notify: false
52
+ notify: false,
53
+ // Opinionated pool defaults (#1119). node-postgres ships `max: 10`
54
+ // with no acquisition timeout and no statement timeout — a saturated
55
+ // pool makes every caller hang indefinitely instead of failing with
56
+ // a diagnosable error. Nearly every store method holds a client for
57
+ // a multi-statement transaction, so multi-lane drains plus API
58
+ // traffic can exhaust a 10-client pool quickly. All four values are
59
+ // plain `pg.PoolConfig` fields — caller config overrides any of them
60
+ // via the constructor spread.
61
+ //
62
+ // - `max: 20` — floor for the default lane's parallel handler budget
63
+ // (streamLimit 10, each commit holds a client) plus API commits,
64
+ // the optional LISTEN client, and headroom. Sizing rule in the
65
+ // README: Σ per-lane streamLimit + API concurrency + notify + 2–4.
66
+ // - `connectionTimeoutMillis: 10_000` — fail acquisition fast (the
67
+ // pg default of 0 waits forever); surfaces as StoreError via
68
+ // `_client()` so operators see *which* operation starved.
69
+ // - `idleTimeoutMillis: 30_000` — keep idle clients warm across
70
+ // drain cycles (cycleMs can exceed the pg default of 10s in
71
+ // low-traffic deployments) without pinning connections for long.
72
+ // - `statement_timeout: 60_000` — per-statement (not per-transaction)
73
+ // ceiling; every statement the store issues (claim CTE, seed DDL,
74
+ // truncate, restore's per-event inserts) legitimately completes
75
+ // orders of magnitude faster, so 60s only fires on a wedged server
76
+ // or a lost lock instead of holding the client hostage.
77
+ max: 20,
78
+ connectionTimeoutMillis: 1e4,
79
+ idleTimeoutMillis: 3e4,
80
+ statement_timeout: 6e4
52
81
  };
53
82
  var PostgresStore = class {
54
83
  _pool;
@@ -119,6 +148,21 @@ var PostgresStore = class {
119
148
  }
120
149
  this._resolve_pii_key = this.config.pii_encryption ? makeKeyResolver(this.config.pii_encryption) : void 0;
121
150
  }
151
+ /**
152
+ * Acquire a pooled client, translating acquisition failures into
153
+ * {@link StoreError} with the calling operation as context. With the
154
+ * default `connectionTimeoutMillis`, a saturated pool fails here
155
+ * after 10s with `Store operation "<operation>" failed` (driver
156
+ * error preserved as `cause`) instead of hanging indefinitely.
157
+ * Every method that checks out a client routes through this helper.
158
+ */
159
+ async _client(operation) {
160
+ try {
161
+ return await this._pool.connect();
162
+ } catch (error) {
163
+ throw new StoreError(operation, { cause: error });
164
+ }
165
+ }
122
166
  /**
123
167
  * Dispose of the store and close all database connections.
124
168
  * Releases any active LISTEN client first so the pool can drain cleanly.
@@ -152,7 +196,7 @@ var PostgresStore = class {
152
196
  * @throws Error if seeding fails
153
197
  */
154
198
  async seed() {
155
- const client = await this._pool.connect();
199
+ const client = await this._client("seed");
156
200
  try {
157
201
  await client.query("BEGIN");
158
202
  await client.query(
@@ -365,7 +409,7 @@ var PostgresStore = class {
365
409
  */
366
410
  async commit(stream, msgs, meta, expectedVersion) {
367
411
  if (msgs.length === 0) return [];
368
- const client = await this._pool.connect();
412
+ const client = await this._client("commit");
369
413
  let version = -1;
370
414
  try {
371
415
  await client.query("BEGIN");
@@ -417,10 +461,11 @@ var PostgresStore = class {
417
461
  events: committed.map((c) => ({ id: c.id, name: c.name })),
418
462
  by: this._by
419
463
  });
420
- await client.query(`SELECT pg_notify($1, $2)`, [
421
- this._channel,
422
- payload
423
- ]);
464
+ if (Buffer.byteLength(payload, "utf8") < NOTIFY_MAX_PAYLOAD_BYTES)
465
+ await client.query(`SELECT pg_notify($1, $2)`, [
466
+ this._channel,
467
+ payload
468
+ ]);
424
469
  }
425
470
  await client.query("COMMIT");
426
471
  return committed;
@@ -447,7 +492,7 @@ var PostgresStore = class {
447
492
  * @returns Leased streams with metadata
448
493
  */
449
494
  async claim(lagging, leading, by, millis, lane) {
450
- const client = await this._pool.connect();
495
+ const client = await this._client("claim");
451
496
  try {
452
497
  await client.query("BEGIN");
453
498
  const lane_clause = lane !== void 0 ? `AND s.lane = $5` : "";
@@ -529,7 +574,7 @@ var PostgresStore = class {
529
574
  * @returns subscribed count and current max watermark.
530
575
  */
531
576
  async subscribe(streams) {
532
- const client = await this._pool.connect();
577
+ const client = await this._client("subscribe");
533
578
  try {
534
579
  await client.query("BEGIN");
535
580
  let subscribed = 0;
@@ -589,30 +634,31 @@ var PostgresStore = class {
589
634
  * @returns Acked leases.
590
635
  */
591
636
  async ack(leases) {
592
- const client = await this._pool.connect();
637
+ const client = await this._client("ack");
593
638
  try {
594
639
  await client.query("BEGIN");
595
640
  const { rows } = await client.query(
596
641
  `
597
642
  WITH input AS (
598
643
  SELECT * FROM jsonb_to_recordset($1::jsonb)
599
- AS x(stream text, by text, at int, lagging boolean)
644
+ AS x(stream text, by text, at int, lagging boolean, due bigint)
600
645
  )
601
646
  UPDATE ${this._fqs} AS s
602
647
  SET
603
- at = i.at,
648
+ at = CASE WHEN i.due IS NULL THEN i.at ELSE s.at END,
604
649
  retry = -1,
605
650
  leased_by = NULL,
606
651
  leased_until = NULL,
607
- deferred_at = NULL
652
+ deferred_at = CASE WHEN i.due IS NULL THEN NULL
653
+ ELSE to_timestamp(i.due / 1000.0) END
608
654
  FROM input i
609
655
  WHERE s.stream = i.stream AND s.leased_by = i.by
610
- RETURNING s.stream, s.source, s.at, i.by, s.retry, i.lagging, s.lane
656
+ RETURNING s.stream, s.source, s.at, i.by, s.retry, i.lagging, s.lane, i.due
611
657
  `,
612
658
  [JSON.stringify(leases)]
613
659
  );
614
660
  await client.query("COMMIT");
615
- return rows.map((row) => ({
661
+ return rows.filter((row) => row.due === null).map((row) => ({
616
662
  stream: row.stream,
617
663
  source: row.source ?? void 0,
618
664
  at: row.at,
@@ -635,7 +681,7 @@ var PostgresStore = class {
635
681
  * @returns Blocked leases.
636
682
  */
637
683
  async block(leases) {
638
- const client = await this._pool.connect();
684
+ const client = await this._client("block");
639
685
  try {
640
686
  await client.query("BEGIN");
641
687
  const { rows } = await client.query(
@@ -866,7 +912,7 @@ var PostgresStore = class {
866
912
  if (conditions.length) sql += " WHERE " + conditions.join(" AND ");
867
913
  values.push(limit);
868
914
  sql += ` ORDER BY stream LIMIT $${values.length}`;
869
- const client = await this._pool.connect();
915
+ const client = await this._client("query_streams");
870
916
  try {
871
917
  const [streamsResult, maxResult] = await Promise.all([
872
918
  client.query(sql, values),
@@ -1096,7 +1142,7 @@ var PostgresStore = class {
1096
1142
  */
1097
1143
  async _subscribe_notifications(handler) {
1098
1144
  await this._teardown_listen();
1099
- const client = await this._pool.connect();
1145
+ const client = await this._client("notify");
1100
1146
  const on_notification = (msg) => {
1101
1147
  if (msg.channel !== this._channel) return;
1102
1148
  if (!msg.payload) return;
@@ -1157,7 +1203,7 @@ var PostgresStore = class {
1157
1203
  async truncate(targets) {
1158
1204
  if (!targets.length) return /* @__PURE__ */ new Map();
1159
1205
  const streams = targets.map((t) => t.stream);
1160
- const client = await this._pool.connect();
1206
+ const client = await this._client("truncate");
1161
1207
  try {
1162
1208
  await client.query("BEGIN");
1163
1209
  await client.query(`DELETE FROM ${this._fqs} WHERE stream = ANY($1)`, [
@@ -1208,7 +1254,7 @@ var PostgresStore = class {
1208
1254
  * from 1. `created` is preserved verbatim from the source.
1209
1255
  */
1210
1256
  async restore(driver) {
1211
- const client = await this._pool.connect();
1257
+ const client = await this._client("restore");
1212
1258
  try {
1213
1259
  await client.query("BEGIN");
1214
1260
  await client.query(