@rotorsoft/act-pg 1.3.0 → 1.4.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 +7 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/@types/postgres-store.d.ts +7 -7
- package/dist/@types/postgres-store.d.ts.map +1 -1
- package/dist/index.cjs +72 -67
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +72 -67
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -27,10 +27,10 @@ types.setTypeParser(
|
|
|
27
27
|
var SAFE_IDENTIFIER = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
28
28
|
var PG_UNIQUE_VIOLATION = "23505";
|
|
29
29
|
var NOTIFY_CHANNEL_PREFIX = "act_commit";
|
|
30
|
-
function
|
|
30
|
+
function notify_channel(schema, table) {
|
|
31
31
|
return `${NOTIFY_CHANNEL_PREFIX}_${schema}_${table}`;
|
|
32
32
|
}
|
|
33
|
-
function
|
|
33
|
+
function assert_safe_identifier(value, label) {
|
|
34
34
|
if (!SAFE_IDENTIFIER.test(value))
|
|
35
35
|
throw new Error(`Unsafe SQL identifier for ${label}: "${value}"`);
|
|
36
36
|
}
|
|
@@ -63,14 +63,14 @@ var PostgresStore = class {
|
|
|
63
63
|
*/
|
|
64
64
|
_channel;
|
|
65
65
|
/** Active LISTEN client (one per `notify()` subscription). */
|
|
66
|
-
|
|
66
|
+
_listen_client;
|
|
67
67
|
/**
|
|
68
68
|
* Notification listener attached to the active LISTEN client. Tracked
|
|
69
69
|
* separately so the re-subscribe / dispose paths can detach it before
|
|
70
70
|
* destroying the client — without this, a pool that reused the
|
|
71
71
|
* connection would re-fire the stale handler.
|
|
72
72
|
*/
|
|
73
|
-
|
|
73
|
+
_listen_handler;
|
|
74
74
|
/**
|
|
75
75
|
* Cross-process commit subscription. **Present only when
|
|
76
76
|
* `config.notify === true`** — the orchestrator's auto-wire path
|
|
@@ -88,15 +88,15 @@ var PostgresStore = class {
|
|
|
88
88
|
*/
|
|
89
89
|
constructor(config = {}) {
|
|
90
90
|
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
91
|
-
|
|
92
|
-
|
|
91
|
+
assert_safe_identifier(this.config.schema, "schema");
|
|
92
|
+
assert_safe_identifier(this.config.table, "table");
|
|
93
93
|
const { schema: _, table: __, ...poolConfig } = this.config;
|
|
94
94
|
this._pool = new Pool(poolConfig);
|
|
95
95
|
this._fqt = `"${this.config.schema}"."${this.config.table}"`;
|
|
96
96
|
this._fqs = `"${this.config.schema}"."${this.config.table}_streams"`;
|
|
97
|
-
this._channel =
|
|
97
|
+
this._channel = notify_channel(this.config.schema, this.config.table);
|
|
98
98
|
if (this.config.notify) {
|
|
99
|
-
this.notify = this.
|
|
99
|
+
this.notify = this._subscribe_notifications.bind(this);
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
102
|
/**
|
|
@@ -105,7 +105,7 @@ var PostgresStore = class {
|
|
|
105
105
|
* @returns Promise that resolves when all connections are closed
|
|
106
106
|
*/
|
|
107
107
|
async dispose() {
|
|
108
|
-
await this.
|
|
108
|
+
await this._teardown_listen();
|
|
109
109
|
await this._pool.end();
|
|
110
110
|
}
|
|
111
111
|
/**
|
|
@@ -115,16 +115,16 @@ var PostgresStore = class {
|
|
|
115
115
|
* destroying belt-and-braces guards against any future change in
|
|
116
116
|
* pg-pool semantics that could re-issue a half-clean client).
|
|
117
117
|
*/
|
|
118
|
-
async
|
|
119
|
-
if (!this.
|
|
120
|
-
this.
|
|
121
|
-
this.
|
|
118
|
+
async _teardown_listen() {
|
|
119
|
+
if (!this._listen_client) return;
|
|
120
|
+
this._listen_client.removeListener("notification", this._listen_handler);
|
|
121
|
+
this._listen_handler = void 0;
|
|
122
122
|
try {
|
|
123
|
-
await this.
|
|
123
|
+
await this._listen_client.query(`UNLISTEN ${this._channel}`);
|
|
124
124
|
} catch {
|
|
125
125
|
}
|
|
126
|
-
this.
|
|
127
|
-
this.
|
|
126
|
+
this._listen_client.release(true);
|
|
127
|
+
this._listen_client = void 0;
|
|
128
128
|
}
|
|
129
129
|
/**
|
|
130
130
|
* Seed the database with required tables, indexes, and schema for event storage.
|
|
@@ -399,7 +399,7 @@ var PostgresStore = class {
|
|
|
399
399
|
const client = await this._pool.connect();
|
|
400
400
|
try {
|
|
401
401
|
await client.query("BEGIN");
|
|
402
|
-
const
|
|
402
|
+
const lane_clause = lane !== void 0 ? `AND s.lane = $5` : "";
|
|
403
403
|
const params = lane !== void 0 ? [lagging, leading, by, millis, lane] : [lagging, leading, by, millis];
|
|
404
404
|
const { rows } = await client.query(
|
|
405
405
|
`
|
|
@@ -408,7 +408,7 @@ var PostgresStore = class {
|
|
|
408
408
|
SELECT stream, source, at, priority, lane
|
|
409
409
|
FROM ${this._fqs} s
|
|
410
410
|
WHERE blocked = false
|
|
411
|
-
${
|
|
411
|
+
${lane_clause}
|
|
412
412
|
AND (leased_by IS NULL OR leased_until <= NOW())
|
|
413
413
|
AND (s.at < 0 OR EXISTS (
|
|
414
414
|
SELECT 1 FROM ${this._fqt} e
|
|
@@ -633,7 +633,7 @@ var PostgresStore = class {
|
|
|
633
633
|
* `WHERE` — callers compose it with any other predicates they need.
|
|
634
634
|
* Returns an always-true clause (`true`) when the filter is empty.
|
|
635
635
|
*/
|
|
636
|
-
|
|
636
|
+
_filter_clause(filter, start) {
|
|
637
637
|
const conditions = [];
|
|
638
638
|
const values = [];
|
|
639
639
|
if (filter.stream !== void 0) {
|
|
@@ -663,19 +663,19 @@ var PostgresStore = class {
|
|
|
663
663
|
};
|
|
664
664
|
}
|
|
665
665
|
async reset(input) {
|
|
666
|
-
const
|
|
666
|
+
const set_clause = `SET at = -1, retry = 0, blocked = false, error = NULL,
|
|
667
667
|
leased_by = NULL, leased_until = NULL`;
|
|
668
668
|
if (Array.isArray(input)) {
|
|
669
669
|
if (!input.length) return 0;
|
|
670
670
|
const { rowCount: rowCount2 } = await this._pool.query(
|
|
671
|
-
`UPDATE ${this._fqs} ${
|
|
671
|
+
`UPDATE ${this._fqs} ${set_clause} WHERE stream = ANY($1)`,
|
|
672
672
|
[input]
|
|
673
673
|
);
|
|
674
674
|
return rowCount2 ?? 0;
|
|
675
675
|
}
|
|
676
|
-
const { clause, values } = this.
|
|
676
|
+
const { clause, values } = this._filter_clause(input, 1);
|
|
677
677
|
const { rowCount } = await this._pool.query(
|
|
678
|
-
`UPDATE ${this._fqs} ${
|
|
678
|
+
`UPDATE ${this._fqs} ${set_clause} WHERE ${clause}`,
|
|
679
679
|
values
|
|
680
680
|
);
|
|
681
681
|
return rowCount ?? 0;
|
|
@@ -696,23 +696,23 @@ var PostgresStore = class {
|
|
|
696
696
|
* @returns Count of streams that were actually flipped (were blocked).
|
|
697
697
|
*/
|
|
698
698
|
async unblock(input) {
|
|
699
|
-
const
|
|
699
|
+
const set_clause = `SET retry = -1, blocked = false, error = NULL,
|
|
700
700
|
leased_by = NULL, leased_until = NULL`;
|
|
701
701
|
if (Array.isArray(input)) {
|
|
702
702
|
if (!input.length) return 0;
|
|
703
703
|
const { rowCount: rowCount2 } = await this._pool.query(
|
|
704
|
-
`UPDATE ${this._fqs} ${
|
|
704
|
+
`UPDATE ${this._fqs} ${set_clause}
|
|
705
705
|
WHERE stream = ANY($1) AND blocked = true`,
|
|
706
706
|
[input]
|
|
707
707
|
);
|
|
708
708
|
return rowCount2 ?? 0;
|
|
709
709
|
}
|
|
710
|
-
const { clause, values } = this.
|
|
710
|
+
const { clause, values } = this._filter_clause(
|
|
711
711
|
{ ...input, blocked: true },
|
|
712
712
|
1
|
|
713
713
|
);
|
|
714
714
|
const { rowCount } = await this._pool.query(
|
|
715
|
-
`UPDATE ${this._fqs} ${
|
|
715
|
+
`UPDATE ${this._fqs} ${set_clause} WHERE ${clause}`,
|
|
716
716
|
values
|
|
717
717
|
);
|
|
718
718
|
return rowCount ?? 0;
|
|
@@ -732,7 +732,7 @@ var PostgresStore = class {
|
|
|
732
732
|
* @returns Count of streams whose priority changed.
|
|
733
733
|
*/
|
|
734
734
|
async prioritize(filter, priority) {
|
|
735
|
-
const { clause, values } = this.
|
|
735
|
+
const { clause, values } = this._filter_clause(filter, 2);
|
|
736
736
|
const sql = `UPDATE ${this._fqs} SET priority = $1
|
|
737
737
|
WHERE priority <> $1 AND ${clause}`;
|
|
738
738
|
const { rowCount } = await this._pool.query(sql, [priority, ...values]);
|
|
@@ -839,11 +839,11 @@ var PostgresStore = class {
|
|
|
839
839
|
*/
|
|
840
840
|
async query_stats(input, options) {
|
|
841
841
|
const exclude = options?.exclude ?? [];
|
|
842
|
-
const
|
|
843
|
-
const
|
|
844
|
-
const
|
|
842
|
+
const want_tail = options?.tail ?? false;
|
|
843
|
+
const want_count = options?.count ?? false;
|
|
844
|
+
const want_names = options?.names ?? false;
|
|
845
845
|
const before = options?.before;
|
|
846
|
-
const
|
|
846
|
+
const full_scan = want_count || want_names;
|
|
847
847
|
if (Array.isArray(input) && input.length === 0) {
|
|
848
848
|
return /* @__PURE__ */ new Map();
|
|
849
849
|
}
|
|
@@ -866,29 +866,34 @@ var PostgresStore = class {
|
|
|
866
866
|
params.push(before);
|
|
867
867
|
where.push(`e.id < $${params.length}`);
|
|
868
868
|
}
|
|
869
|
-
const
|
|
870
|
-
const
|
|
871
|
-
return
|
|
872
|
-
|
|
873
|
-
|
|
869
|
+
const from_clause = `${this._fqt} e`;
|
|
870
|
+
const where_clause = `WHERE ${where.length ? where.join(" AND ") : "TRUE"}`;
|
|
871
|
+
return full_scan ? this._query_stats_full_scan(
|
|
872
|
+
from_clause,
|
|
873
|
+
where_clause,
|
|
874
874
|
params,
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
) : this.
|
|
875
|
+
want_tail,
|
|
876
|
+
want_count,
|
|
877
|
+
want_names
|
|
878
|
+
) : this._query_stats_heads_only(
|
|
879
|
+
from_clause,
|
|
880
|
+
where_clause,
|
|
881
|
+
params,
|
|
882
|
+
want_tail
|
|
883
|
+
);
|
|
879
884
|
}
|
|
880
885
|
/**
|
|
881
886
|
* Cheap path: index-only DISTINCT ON for the head per stream, plus an
|
|
882
887
|
* optional second query (in parallel) for the tail. K rows touched
|
|
883
888
|
* per query, not N events.
|
|
884
889
|
*/
|
|
885
|
-
async
|
|
890
|
+
async _query_stats_heads_only(from_clause, where_clause, params, want_tail) {
|
|
886
891
|
const cols = `e.id, e.stream, e.version, e.name, e.data, e.created, e.meta`;
|
|
887
|
-
const
|
|
888
|
-
const
|
|
892
|
+
const head_sql = `SELECT DISTINCT ON (e.stream) ${cols} FROM ${from_clause} ${where_clause} ORDER BY e.stream, e.version DESC`;
|
|
893
|
+
const tail_sql = want_tail ? `SELECT DISTINCT ON (e.stream) ${cols} FROM ${from_clause} ${where_clause} ORDER BY e.stream, e.version ASC` : null;
|
|
889
894
|
const [headRes, tailRes] = await Promise.all([
|
|
890
|
-
this._pool.query(
|
|
891
|
-
|
|
895
|
+
this._pool.query(head_sql, params),
|
|
896
|
+
tail_sql ? this._pool.query(tail_sql, params) : Promise.resolve(null)
|
|
892
897
|
]);
|
|
893
898
|
const out = /* @__PURE__ */ new Map();
|
|
894
899
|
for (const row of headRes.rows) {
|
|
@@ -906,16 +911,16 @@ var PostgresStore = class {
|
|
|
906
911
|
* `COUNT(*)` and `jsonb_object_agg(name, n)` map alongside the head
|
|
907
912
|
* (and tail when requested). All extras share the single events scan.
|
|
908
913
|
*/
|
|
909
|
-
async
|
|
910
|
-
const
|
|
911
|
-
const
|
|
912
|
-
const
|
|
914
|
+
async _query_stats_full_scan(from_clause, where_clause, params, want_tail, want_count, want_names) {
|
|
915
|
+
const tail_cte = want_tail ? `, tails AS (SELECT DISTINCT ON (stream) * FROM ef ORDER BY stream, version ASC)` : "";
|
|
916
|
+
const tail_join = want_tail ? `LEFT JOIN tails t ON t.stream = h.stream` : "";
|
|
917
|
+
const tail_cols = want_tail ? `, t.id AS t_id, t.stream AS t_stream, t.version AS t_version,
|
|
913
918
|
t.name AS t_name, t.data AS t_data, t.created AS t_created, t.meta AS t_meta` : "";
|
|
914
919
|
const sql = `
|
|
915
920
|
WITH ef AS (
|
|
916
921
|
SELECT e.id, e.stream, e.version, e.name, e.data, e.created, e.meta
|
|
917
|
-
FROM ${
|
|
918
|
-
${
|
|
922
|
+
FROM ${from_clause}
|
|
923
|
+
${where_clause}
|
|
919
924
|
),
|
|
920
925
|
agg AS (
|
|
921
926
|
SELECT stream,
|
|
@@ -931,15 +936,15 @@ var PostgresStore = class {
|
|
|
931
936
|
heads AS (
|
|
932
937
|
SELECT DISTINCT ON (stream) * FROM ef ORDER BY stream, version DESC
|
|
933
938
|
)
|
|
934
|
-
${
|
|
939
|
+
${tail_cte}
|
|
935
940
|
SELECT
|
|
936
941
|
h.id, h.stream, h.version, h.name, h.data, h.created, h.meta,
|
|
937
942
|
a.cnt AS agg_count,
|
|
938
943
|
a.names AS agg_names
|
|
939
|
-
${
|
|
944
|
+
${tail_cols}
|
|
940
945
|
FROM heads h
|
|
941
946
|
LEFT JOIN agg a ON a.stream = h.stream
|
|
942
|
-
${
|
|
947
|
+
${tail_join}
|
|
943
948
|
`;
|
|
944
949
|
const res = await this._pool.query(sql, params);
|
|
945
950
|
const out = /* @__PURE__ */ new Map();
|
|
@@ -955,7 +960,7 @@ var PostgresStore = class {
|
|
|
955
960
|
meta: row.meta
|
|
956
961
|
}
|
|
957
962
|
};
|
|
958
|
-
if (
|
|
963
|
+
if (want_tail && row.t_id !== void 0 && row.t_id !== null) {
|
|
959
964
|
stats.tail = {
|
|
960
965
|
id: row.t_id,
|
|
961
966
|
stream: row.t_stream,
|
|
@@ -966,8 +971,8 @@ var PostgresStore = class {
|
|
|
966
971
|
meta: row.t_meta
|
|
967
972
|
};
|
|
968
973
|
}
|
|
969
|
-
if (
|
|
970
|
-
if (
|
|
974
|
+
if (want_count) stats.count = row.agg_count;
|
|
975
|
+
if (want_names) stats.names = row.agg_names;
|
|
971
976
|
out.set(row.stream, stats);
|
|
972
977
|
}
|
|
973
978
|
return out;
|
|
@@ -993,10 +998,10 @@ var PostgresStore = class {
|
|
|
993
998
|
* @param handler Called for each cross-process commit notification.
|
|
994
999
|
* @returns Disposer that releases the LISTEN client.
|
|
995
1000
|
*/
|
|
996
|
-
async
|
|
997
|
-
await this.
|
|
1001
|
+
async _subscribe_notifications(handler) {
|
|
1002
|
+
await this._teardown_listen();
|
|
998
1003
|
const client = await this._pool.connect();
|
|
999
|
-
const
|
|
1004
|
+
const on_notification = (msg) => {
|
|
1000
1005
|
if (msg.channel !== this._channel) return;
|
|
1001
1006
|
if (!msg.payload) return;
|
|
1002
1007
|
let parsed;
|
|
@@ -1033,19 +1038,19 @@ var PostgresStore = class {
|
|
|
1033
1038
|
logger.error(err, "act_commit: handler threw, listener preserved");
|
|
1034
1039
|
}
|
|
1035
1040
|
};
|
|
1036
|
-
client.on("notification",
|
|
1041
|
+
client.on("notification", on_notification);
|
|
1037
1042
|
try {
|
|
1038
1043
|
await client.query(`LISTEN ${this._channel}`);
|
|
1039
1044
|
} catch (err) {
|
|
1040
|
-
client.removeListener("notification",
|
|
1045
|
+
client.removeListener("notification", on_notification);
|
|
1041
1046
|
client.release(true);
|
|
1042
1047
|
throw err;
|
|
1043
1048
|
}
|
|
1044
|
-
this.
|
|
1045
|
-
this.
|
|
1049
|
+
this._listen_client = client;
|
|
1050
|
+
this._listen_handler = on_notification;
|
|
1046
1051
|
return async () => {
|
|
1047
|
-
if (this.
|
|
1048
|
-
await this.
|
|
1052
|
+
if (this._listen_client !== client) return;
|
|
1053
|
+
await this._teardown_listen();
|
|
1049
1054
|
};
|
|
1050
1055
|
}
|
|
1051
1056
|
/**
|