@rotorsoft/act-pg 1.9.3 → 1.10.1
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/README.md +24 -2
- package/dist/.tsbuildinfo +1 -1
- package/dist/@types/postgres-store.d.ts +26 -6
- package/dist/@types/postgres-store.d.ts.map +1 -1
- package/dist/index.cjs +93 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +93 -16
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -48,7 +48,35 @@ var DEFAULT_CONFIG = {
|
|
|
48
48
|
password: "postgres",
|
|
49
49
|
schema: "public",
|
|
50
50
|
table: "events",
|
|
51
|
-
notify: false
|
|
51
|
+
notify: false,
|
|
52
|
+
// Opinionated pool defaults (#1119). node-postgres ships `max: 10`
|
|
53
|
+
// with no acquisition timeout and no statement timeout — a saturated
|
|
54
|
+
// pool makes every caller hang indefinitely instead of failing with
|
|
55
|
+
// a diagnosable error. Nearly every store method holds a client for
|
|
56
|
+
// a multi-statement transaction, so multi-lane drains plus API
|
|
57
|
+
// traffic can exhaust a 10-client pool quickly. All four values are
|
|
58
|
+
// plain `pg.PoolConfig` fields — caller config overrides any of them
|
|
59
|
+
// via the constructor spread.
|
|
60
|
+
//
|
|
61
|
+
// - `max: 20` — floor for the default lane's parallel handler budget
|
|
62
|
+
// (streamLimit 10, each commit holds a client) plus API commits,
|
|
63
|
+
// the optional LISTEN client, and headroom. Sizing rule in the
|
|
64
|
+
// README: Σ per-lane streamLimit + API concurrency + notify + 2–4.
|
|
65
|
+
// - `connectionTimeoutMillis: 10_000` — fail acquisition fast (the
|
|
66
|
+
// pg default of 0 waits forever); surfaces as StoreError via
|
|
67
|
+
// `_client()` so operators see *which* operation starved.
|
|
68
|
+
// - `idleTimeoutMillis: 30_000` — keep idle clients warm across
|
|
69
|
+
// drain cycles (cycleMs can exceed the pg default of 10s in
|
|
70
|
+
// low-traffic deployments) without pinning connections for long.
|
|
71
|
+
// - `statement_timeout: 60_000` — per-statement (not per-transaction)
|
|
72
|
+
// ceiling; every statement the store issues (claim CTE, seed DDL,
|
|
73
|
+
// truncate, restore's per-event inserts) legitimately completes
|
|
74
|
+
// orders of magnitude faster, so 60s only fires on a wedged server
|
|
75
|
+
// or a lost lock instead of holding the client hostage.
|
|
76
|
+
max: 20,
|
|
77
|
+
connectionTimeoutMillis: 1e4,
|
|
78
|
+
idleTimeoutMillis: 3e4,
|
|
79
|
+
statement_timeout: 6e4
|
|
52
80
|
};
|
|
53
81
|
var PostgresStore = class {
|
|
54
82
|
_pool;
|
|
@@ -119,6 +147,21 @@ var PostgresStore = class {
|
|
|
119
147
|
}
|
|
120
148
|
this._resolve_pii_key = this.config.pii_encryption ? makeKeyResolver(this.config.pii_encryption) : void 0;
|
|
121
149
|
}
|
|
150
|
+
/**
|
|
151
|
+
* Acquire a pooled client, translating acquisition failures into
|
|
152
|
+
* {@link StoreError} with the calling operation as context. With the
|
|
153
|
+
* default `connectionTimeoutMillis`, a saturated pool fails here
|
|
154
|
+
* after 10s with `Store operation "<operation>" failed` (driver
|
|
155
|
+
* error preserved as `cause`) instead of hanging indefinitely.
|
|
156
|
+
* Every method that checks out a client routes through this helper.
|
|
157
|
+
*/
|
|
158
|
+
async _client(operation) {
|
|
159
|
+
try {
|
|
160
|
+
return await this._pool.connect();
|
|
161
|
+
} catch (error) {
|
|
162
|
+
throw new StoreError(operation, { cause: error });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
122
165
|
/**
|
|
123
166
|
* Dispose of the store and close all database connections.
|
|
124
167
|
* Releases any active LISTEN client first so the pool can drain cleanly.
|
|
@@ -152,7 +195,7 @@ var PostgresStore = class {
|
|
|
152
195
|
* @throws Error if seeding fails
|
|
153
196
|
*/
|
|
154
197
|
async seed() {
|
|
155
|
-
const client = await this.
|
|
198
|
+
const client = await this._client("seed");
|
|
156
199
|
try {
|
|
157
200
|
await client.query("BEGIN");
|
|
158
201
|
await client.query(
|
|
@@ -205,7 +248,8 @@ var PostgresStore = class {
|
|
|
205
248
|
leased_by text,
|
|
206
249
|
leased_until timestamptz,
|
|
207
250
|
priority int NOT NULL DEFAULT 0,
|
|
208
|
-
lane text NOT NULL DEFAULT 'default'
|
|
251
|
+
lane text NOT NULL DEFAULT 'default',
|
|
252
|
+
deferred_at timestamptz
|
|
209
253
|
) TABLESPACE pg_default;`
|
|
210
254
|
);
|
|
211
255
|
await client.query(
|
|
@@ -216,6 +260,10 @@ var PostgresStore = class {
|
|
|
216
260
|
`ALTER TABLE ${this._fqs}
|
|
217
261
|
ADD COLUMN IF NOT EXISTS lane text NOT NULL DEFAULT 'default';`
|
|
218
262
|
);
|
|
263
|
+
await client.query(
|
|
264
|
+
`ALTER TABLE ${this._fqs}
|
|
265
|
+
ADD COLUMN IF NOT EXISTS deferred_at timestamptz;`
|
|
266
|
+
);
|
|
219
267
|
await client.query(
|
|
220
268
|
`DROP INDEX IF EXISTS "${this.config.schema}"."${this.config.table}_streams_fetch_ix"`
|
|
221
269
|
);
|
|
@@ -360,7 +408,7 @@ var PostgresStore = class {
|
|
|
360
408
|
*/
|
|
361
409
|
async commit(stream, msgs, meta, expectedVersion) {
|
|
362
410
|
if (msgs.length === 0) return [];
|
|
363
|
-
const client = await this.
|
|
411
|
+
const client = await this._client("commit");
|
|
364
412
|
let version = -1;
|
|
365
413
|
try {
|
|
366
414
|
await client.query("BEGIN");
|
|
@@ -442,7 +490,7 @@ var PostgresStore = class {
|
|
|
442
490
|
* @returns Leased streams with metadata
|
|
443
491
|
*/
|
|
444
492
|
async claim(lagging, leading, by, millis, lane) {
|
|
445
|
-
const client = await this.
|
|
493
|
+
const client = await this._client("claim");
|
|
446
494
|
try {
|
|
447
495
|
await client.query("BEGIN");
|
|
448
496
|
const lane_clause = lane !== void 0 ? `AND s.lane = $5` : "";
|
|
@@ -456,6 +504,7 @@ var PostgresStore = class {
|
|
|
456
504
|
WHERE blocked = false
|
|
457
505
|
${lane_clause}
|
|
458
506
|
AND (leased_by IS NULL OR leased_until <= NOW())
|
|
507
|
+
AND (deferred_at IS NULL OR deferred_at <= NOW())
|
|
459
508
|
AND (s.at < 0 OR EXISTS (
|
|
460
509
|
SELECT 1 FROM ${this._fqt} e
|
|
461
510
|
WHERE e.id > s.at
|
|
@@ -523,7 +572,7 @@ var PostgresStore = class {
|
|
|
523
572
|
* @returns subscribed count and current max watermark.
|
|
524
573
|
*/
|
|
525
574
|
async subscribe(streams) {
|
|
526
|
-
const client = await this.
|
|
575
|
+
const client = await this._client("subscribe");
|
|
527
576
|
try {
|
|
528
577
|
await client.query("BEGIN");
|
|
529
578
|
let subscribed = 0;
|
|
@@ -583,7 +632,7 @@ var PostgresStore = class {
|
|
|
583
632
|
* @returns Acked leases.
|
|
584
633
|
*/
|
|
585
634
|
async ack(leases) {
|
|
586
|
-
const client = await this.
|
|
635
|
+
const client = await this._client("ack");
|
|
587
636
|
try {
|
|
588
637
|
await client.query("BEGIN");
|
|
589
638
|
const { rows } = await client.query(
|
|
@@ -597,7 +646,8 @@ var PostgresStore = class {
|
|
|
597
646
|
at = i.at,
|
|
598
647
|
retry = -1,
|
|
599
648
|
leased_by = NULL,
|
|
600
|
-
leased_until = NULL
|
|
649
|
+
leased_until = NULL,
|
|
650
|
+
deferred_at = NULL
|
|
601
651
|
FROM input i
|
|
602
652
|
WHERE s.stream = i.stream AND s.leased_by = i.by
|
|
603
653
|
RETURNING s.stream, s.source, s.at, i.by, s.retry, i.lagging, s.lane
|
|
@@ -628,7 +678,7 @@ var PostgresStore = class {
|
|
|
628
678
|
* @returns Blocked leases.
|
|
629
679
|
*/
|
|
630
680
|
async block(leases) {
|
|
631
|
-
const client = await this.
|
|
681
|
+
const client = await this._client("block");
|
|
632
682
|
try {
|
|
633
683
|
await client.query("BEGIN");
|
|
634
684
|
const { rows } = await client.query(
|
|
@@ -638,7 +688,7 @@ var PostgresStore = class {
|
|
|
638
688
|
AS x(stream text, by text, error text, lagging boolean)
|
|
639
689
|
)
|
|
640
690
|
UPDATE ${this._fqs} AS s
|
|
641
|
-
SET blocked = true, error = i.error
|
|
691
|
+
SET blocked = true, error = i.error, deferred_at = NULL
|
|
642
692
|
FROM input i
|
|
643
693
|
WHERE s.stream = i.stream AND s.leased_by = i.by AND s.blocked = false
|
|
644
694
|
RETURNING s.stream, s.source, s.at, i.by, s.retry, s.error, i.lagging, s.lane
|
|
@@ -664,6 +714,33 @@ var PostgresStore = class {
|
|
|
664
714
|
client.release();
|
|
665
715
|
}
|
|
666
716
|
}
|
|
717
|
+
/**
|
|
718
|
+
* Hold the matched streams out of {@link claim} until `deferred_at`
|
|
719
|
+
* (ms since epoch) — see {@link Store.defer}. Persists `deferred_at`
|
|
720
|
+
* (as a `timestamptz`) so the skip is honored by every competing
|
|
721
|
+
* worker; `claim` filters on `deferred_at <= NOW()`. Accepts an
|
|
722
|
+
* explicit list of names or a {@link StreamFilter}, mirroring
|
|
723
|
+
* {@link reset}/{@link prioritize}. Cleared by ack/block/reset/unblock.
|
|
724
|
+
*
|
|
725
|
+
* @returns Count of streams whose `deferred_at` was set.
|
|
726
|
+
*/
|
|
727
|
+
async defer(input, deferred_at) {
|
|
728
|
+
const set_clause = `SET deferred_at = to_timestamp($1 / 1000.0), retry = -1`;
|
|
729
|
+
if (Array.isArray(input)) {
|
|
730
|
+
if (!input.length) return 0;
|
|
731
|
+
const { rowCount: rowCount2 } = await this._pool.query(
|
|
732
|
+
`UPDATE ${this._fqs} ${set_clause} WHERE stream = ANY($2)`,
|
|
733
|
+
[deferred_at, input]
|
|
734
|
+
);
|
|
735
|
+
return rowCount2 ?? 0;
|
|
736
|
+
}
|
|
737
|
+
const { clause, values } = this._filter_clause(input, 2);
|
|
738
|
+
const { rowCount } = await this._pool.query(
|
|
739
|
+
`UPDATE ${this._fqs} ${set_clause} WHERE ${clause}`,
|
|
740
|
+
[deferred_at, ...values]
|
|
741
|
+
);
|
|
742
|
+
return rowCount ?? 0;
|
|
743
|
+
}
|
|
667
744
|
/**
|
|
668
745
|
* Reset watermarks for the given streams to -1, clearing retry, blocked,
|
|
669
746
|
* error, and lease state so they can be replayed from the beginning.
|
|
@@ -707,7 +784,7 @@ var PostgresStore = class {
|
|
|
707
784
|
}
|
|
708
785
|
async reset(input) {
|
|
709
786
|
const set_clause = `SET at = -1, retry = -1, blocked = false, error = NULL,
|
|
710
|
-
leased_by = NULL, leased_until = NULL`;
|
|
787
|
+
leased_by = NULL, leased_until = NULL, deferred_at = NULL`;
|
|
711
788
|
if (Array.isArray(input)) {
|
|
712
789
|
if (!input.length) return 0;
|
|
713
790
|
const { rowCount: rowCount2 } = await this._pool.query(
|
|
@@ -740,7 +817,7 @@ var PostgresStore = class {
|
|
|
740
817
|
*/
|
|
741
818
|
async unblock(input) {
|
|
742
819
|
const set_clause = `SET retry = -1, blocked = false, error = NULL,
|
|
743
|
-
leased_by = NULL, leased_until = NULL`;
|
|
820
|
+
leased_by = NULL, leased_until = NULL, deferred_at = NULL`;
|
|
744
821
|
if (Array.isArray(input)) {
|
|
745
822
|
if (!input.length) return 0;
|
|
746
823
|
const { rowCount: rowCount2 } = await this._pool.query(
|
|
@@ -832,7 +909,7 @@ var PostgresStore = class {
|
|
|
832
909
|
if (conditions.length) sql += " WHERE " + conditions.join(" AND ");
|
|
833
910
|
values.push(limit);
|
|
834
911
|
sql += ` ORDER BY stream LIMIT $${values.length}`;
|
|
835
|
-
const client = await this.
|
|
912
|
+
const client = await this._client("query_streams");
|
|
836
913
|
try {
|
|
837
914
|
const [streamsResult, maxResult] = await Promise.all([
|
|
838
915
|
client.query(sql, values),
|
|
@@ -1062,7 +1139,7 @@ var PostgresStore = class {
|
|
|
1062
1139
|
*/
|
|
1063
1140
|
async _subscribe_notifications(handler) {
|
|
1064
1141
|
await this._teardown_listen();
|
|
1065
|
-
const client = await this.
|
|
1142
|
+
const client = await this._client("notify");
|
|
1066
1143
|
const on_notification = (msg) => {
|
|
1067
1144
|
if (msg.channel !== this._channel) return;
|
|
1068
1145
|
if (!msg.payload) return;
|
|
@@ -1123,7 +1200,7 @@ var PostgresStore = class {
|
|
|
1123
1200
|
async truncate(targets) {
|
|
1124
1201
|
if (!targets.length) return /* @__PURE__ */ new Map();
|
|
1125
1202
|
const streams = targets.map((t) => t.stream);
|
|
1126
|
-
const client = await this.
|
|
1203
|
+
const client = await this._client("truncate");
|
|
1127
1204
|
try {
|
|
1128
1205
|
await client.query("BEGIN");
|
|
1129
1206
|
await client.query(`DELETE FROM ${this._fqs} WHERE stream = ANY($1)`, [
|
|
@@ -1174,7 +1251,7 @@ var PostgresStore = class {
|
|
|
1174
1251
|
* from 1. `created` is preserved verbatim from the source.
|
|
1175
1252
|
*/
|
|
1176
1253
|
async restore(driver) {
|
|
1177
|
-
const client = await this.
|
|
1254
|
+
const client = await this._client("restore");
|
|
1178
1255
|
try {
|
|
1179
1256
|
await client.query("BEGIN");
|
|
1180
1257
|
await client.query(
|