@rotorsoft/act-pg 1.13.19 → 1.15.0
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/.tsbuildinfo +1 -1
- package/dist/@types/postgres-store.d.ts +5 -7
- package/dist/@types/postgres-store.d.ts.map +1 -1
- package/dist/index.cjs +78 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +78 -9
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -84,6 +84,8 @@ var PostgresStore = class {
|
|
|
84
84
|
config;
|
|
85
85
|
_fqt;
|
|
86
86
|
_fqs;
|
|
87
|
+
/** Correlate checkpoint table (#1484) — one row, id 0. */
|
|
88
|
+
_fqc;
|
|
87
89
|
/**
|
|
88
90
|
* Per-instance writer identifier embedded in every NOTIFY payload. The
|
|
89
91
|
* `notify()` LISTEN handler skips payloads where `by === this._by`,
|
|
@@ -170,6 +172,7 @@ var PostgresStore = class {
|
|
|
170
172
|
this._pool = new Pool({ ...poolConfig, types: scopedTypes });
|
|
171
173
|
this._fqt = `"${this.config.schema}"."${this.config.table}"`;
|
|
172
174
|
this._fqs = `"${this.config.schema}"."${this.config.table}_streams"`;
|
|
175
|
+
this._fqc = `"${this.config.schema}"."${this.config.table}_correlated"`;
|
|
173
176
|
this._channel = notify_channel(this.config.schema, this.config.table);
|
|
174
177
|
if (this.config.notify) {
|
|
175
178
|
this.notify = this._subscribe_notifications.bind(this);
|
|
@@ -303,9 +306,20 @@ var PostgresStore = class {
|
|
|
303
306
|
leased_until timestamptz,
|
|
304
307
|
priority int NOT NULL DEFAULT 0,
|
|
305
308
|
lane text NOT NULL DEFAULT 'default',
|
|
306
|
-
deferred_at timestamptz
|
|
309
|
+
deferred_at timestamptz,
|
|
310
|
+
correlated_at int
|
|
307
311
|
) TABLESPACE pg_default;`
|
|
308
312
|
);
|
|
313
|
+
await client.query(
|
|
314
|
+
`CREATE TABLE IF NOT EXISTS ${this._fqc} (
|
|
315
|
+
id int PRIMARY KEY DEFAULT 0,
|
|
316
|
+
at int NOT NULL DEFAULT -1,
|
|
317
|
+
CONSTRAINT ${this.config.table}_correlated_singleton CHECK (id = 0)
|
|
318
|
+
) TABLESPACE pg_default;`
|
|
319
|
+
);
|
|
320
|
+
await client.query(
|
|
321
|
+
`INSERT INTO ${this._fqc} (id) VALUES (0) ON CONFLICT (id) DO NOTHING;`
|
|
322
|
+
);
|
|
309
323
|
await client.query(
|
|
310
324
|
`ALTER TABLE ${this._fqs}
|
|
311
325
|
ADD COLUMN IF NOT EXISTS priority int NOT NULL DEFAULT 0;`
|
|
@@ -318,6 +332,10 @@ var PostgresStore = class {
|
|
|
318
332
|
`ALTER TABLE ${this._fqs}
|
|
319
333
|
ADD COLUMN IF NOT EXISTS deferred_at timestamptz;`
|
|
320
334
|
);
|
|
335
|
+
await client.query(
|
|
336
|
+
`ALTER TABLE ${this._fqs}
|
|
337
|
+
ADD COLUMN IF NOT EXISTS correlated_at int;`
|
|
338
|
+
);
|
|
321
339
|
await client.query(
|
|
322
340
|
`DO $$
|
|
323
341
|
BEGIN
|
|
@@ -366,6 +384,11 @@ var PostgresStore = class {
|
|
|
366
384
|
`CREATE INDEX IF NOT EXISTS "${this.config.table}_streams_lane_ix"
|
|
367
385
|
ON ${this._fqs} (lane);`
|
|
368
386
|
);
|
|
387
|
+
await client.query(
|
|
388
|
+
`CREATE INDEX IF NOT EXISTS "${this.config.table}_streams_correlated_at_ix"
|
|
389
|
+
ON ${this._fqs} (lane, priority DESC, at)
|
|
390
|
+
WHERE blocked = false AND at < correlated_at;`
|
|
391
|
+
);
|
|
369
392
|
await client.query("COMMIT");
|
|
370
393
|
logger.info(
|
|
371
394
|
`Seeded schema "${this.config.schema}" with table "${this.config.table}"`
|
|
@@ -391,7 +414,7 @@ var PostgresStore = class {
|
|
|
391
414
|
WHERE schema_name = '${this.config.schema}'
|
|
392
415
|
) THEN
|
|
393
416
|
EXECUTE 'DROP TABLE IF EXISTS ${this._fqt}';
|
|
394
|
-
EXECUTE 'DROP TABLE IF EXISTS ${this._fqs}';
|
|
417
|
+
EXECUTE 'DROP TABLE IF EXISTS ${this._fqc}, ${this._fqs}';
|
|
395
418
|
IF '${this.config.schema}' <> 'public' THEN
|
|
396
419
|
EXECUTE 'DROP SCHEMA "${this.config.schema}" CASCADE';
|
|
397
420
|
END IF;
|
|
@@ -642,7 +665,7 @@ var PostgresStore = class {
|
|
|
642
665
|
-- Measured at 10k subscribed streams with 10 pending: 5,792 ms
|
|
643
666
|
-- before, 10.3 ms after. See libs/act-pg/PERFORMANCE.md for #1448.
|
|
644
667
|
eligible AS (
|
|
645
|
-
SELECT stream, source, at, priority, lane
|
|
668
|
+
SELECT stream, source, at, priority, lane, correlated_at
|
|
646
669
|
FROM ${this._fqs} s
|
|
647
670
|
WHERE blocked = false
|
|
648
671
|
${lane_clause}
|
|
@@ -650,6 +673,26 @@ var PostgresStore = class {
|
|
|
650
673
|
AND (deferred_at IS NULL OR deferred_at <= NOW())
|
|
651
674
|
),
|
|
652
675
|
available AS (
|
|
676
|
+
-- Fast arm (#1485): a marked stream answers from the subscription
|
|
677
|
+
-- row alone. Read from the base table, NOT from the eligible CTE:
|
|
678
|
+
-- it is referenced several times so PG materializes it, and a
|
|
679
|
+
-- materialized scan cannot use the partial index this predicate was
|
|
680
|
+
-- built for (WHERE blocked = false AND at < correlated_at).
|
|
681
|
+
-- The comparison is NULL-safe: an unmarked row compares
|
|
682
|
+
-- unknown and is excluded here, falling through to the legacy arms.
|
|
683
|
+
SELECT stream, source, at, priority, lane
|
|
684
|
+
FROM ${this._fqs} s
|
|
685
|
+
WHERE s.blocked = false
|
|
686
|
+
AND s.at < s.correlated_at
|
|
687
|
+
${lane_clause}
|
|
688
|
+
AND (s.leased_by IS NULL OR s.leased_until <= NOW())
|
|
689
|
+
AND (s.deferred_at IS NULL OR s.deferred_at <= NOW())
|
|
690
|
+
UNION ALL
|
|
691
|
+
-- Legacy arms, gated on an absent mark. NULL means "unknown", so
|
|
692
|
+
-- an install that predates the column probes the event log exactly
|
|
693
|
+
-- as it did before, and each row migrates to the fast arm the first
|
|
694
|
+
-- time correlate marks it. Deleted once correlate marks universally.
|
|
695
|
+
--
|
|
653
696
|
-- Every arm below is watermark-agnostic (#1446): a fresh
|
|
654
697
|
-- subscription sits at -1, and e.id > s.at already answers
|
|
655
698
|
-- correctly there, since the first event has a greater id.
|
|
@@ -659,7 +702,8 @@ var PostgresStore = class {
|
|
|
659
702
|
-- Source-less subscription: any non-snapshot event past the
|
|
660
703
|
-- watermark counts, so the id index alone answers it.
|
|
661
704
|
SELECT stream, source, at, priority, lane FROM eligible s
|
|
662
|
-
WHERE s.
|
|
705
|
+
WHERE s.correlated_at IS NULL
|
|
706
|
+
AND s.source IS NULL
|
|
663
707
|
AND EXISTS (
|
|
664
708
|
SELECT 1 FROM ${this._fqt} e
|
|
665
709
|
WHERE e.id > s.at AND e.name <> '${SNAP_EVENT}' LIMIT 1
|
|
@@ -668,7 +712,8 @@ var PostgresStore = class {
|
|
|
668
712
|
-- Literal source (no regex metacharacter): exact equality, so
|
|
669
713
|
-- "s1" never claims "s12", and (stream, id) is usable.
|
|
670
714
|
SELECT stream, source, at, priority, lane FROM eligible s
|
|
671
|
-
WHERE s.
|
|
715
|
+
WHERE s.correlated_at IS NULL
|
|
716
|
+
AND s.source IS NOT NULL
|
|
672
717
|
AND s.source !~ '${SOURCE_METACHARACTER_CLASS}'
|
|
673
718
|
AND EXISTS (
|
|
674
719
|
SELECT 1 FROM ${this._fqt} e
|
|
@@ -681,7 +726,8 @@ var PostgresStore = class {
|
|
|
681
726
|
-- it anchors. Unavoidably a scan \u2014 but only for the subscriptions
|
|
682
727
|
-- that actually declare a pattern.
|
|
683
728
|
SELECT stream, source, at, priority, lane FROM eligible s
|
|
684
|
-
WHERE s.
|
|
729
|
+
WHERE s.correlated_at IS NULL
|
|
730
|
+
AND s.source IS NOT NULL
|
|
685
731
|
AND s.source ~ '${SOURCE_METACHARACTER_CLASS}'
|
|
686
732
|
AND EXISTS (
|
|
687
733
|
SELECT 1 FROM ${this._fqt} e
|
|
@@ -783,7 +829,7 @@ var PostgresStore = class {
|
|
|
783
829
|
* @param streams - Streams to register with optional source.
|
|
784
830
|
* @returns subscribed count and current max watermark.
|
|
785
831
|
*/
|
|
786
|
-
async subscribe(streams) {
|
|
832
|
+
async subscribe(streams, correlated_at) {
|
|
787
833
|
const client = await this._client("subscribe");
|
|
788
834
|
try {
|
|
789
835
|
await client.query("BEGIN");
|
|
@@ -823,12 +869,35 @@ var PostgresStore = class {
|
|
|
823
869
|
`,
|
|
824
870
|
[JSON.stringify(streams)]
|
|
825
871
|
);
|
|
872
|
+
if (streams.some((s) => s.correlated_at !== void 0))
|
|
873
|
+
await client.query(
|
|
874
|
+
`
|
|
875
|
+
UPDATE ${this._fqs} t
|
|
876
|
+
SET correlated_at = (s->>'correlated_at')::int
|
|
877
|
+
FROM jsonb_array_elements($1::jsonb) AS s
|
|
878
|
+
WHERE t.stream = s->>'stream'
|
|
879
|
+
AND s->>'correlated_at' IS NOT NULL
|
|
880
|
+
AND (t.correlated_at IS NULL
|
|
881
|
+
OR t.correlated_at < (s->>'correlated_at')::int)
|
|
882
|
+
`,
|
|
883
|
+
[JSON.stringify(streams)]
|
|
884
|
+
);
|
|
826
885
|
}
|
|
886
|
+
if (correlated_at !== void 0)
|
|
887
|
+
await client.query(
|
|
888
|
+
`UPDATE ${this._fqc} SET at = GREATEST(at, $1::int) WHERE id = 0`,
|
|
889
|
+
[correlated_at]
|
|
890
|
+
);
|
|
827
891
|
const { rows } = await client.query(
|
|
828
|
-
`SELECT COALESCE(MAX(at), -1)
|
|
892
|
+
`SELECT (SELECT COALESCE(MAX(at), -1) FROM ${this._fqs}) AS max,
|
|
893
|
+
(SELECT at FROM ${this._fqc} WHERE id = 0) AS correlated_at`
|
|
829
894
|
);
|
|
830
895
|
await client.query("COMMIT");
|
|
831
|
-
return {
|
|
896
|
+
return {
|
|
897
|
+
subscribed,
|
|
898
|
+
watermark: rows[0]?.max ?? -1,
|
|
899
|
+
correlated_at: Number(rows[0]?.correlated_at ?? -1)
|
|
900
|
+
};
|
|
832
901
|
} catch (error) {
|
|
833
902
|
await client.query("ROLLBACK").catch(() => {
|
|
834
903
|
});
|