@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/README.md +24 -2
- package/dist/.tsbuildinfo +1 -1
- package/dist/@types/postgres-store.d.ts +15 -6
- package/dist/@types/postgres-store.d.ts.map +1 -1
- package/dist/index.cjs +66 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +66 -20
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
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.
|
|
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.
|
|
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
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
1257
|
+
const client = await this._client("restore");
|
|
1212
1258
|
try {
|
|
1213
1259
|
await client.query("BEGIN");
|
|
1214
1260
|
await client.query(
|