@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/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.source IS NULL
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.source IS NOT NULL
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.source IS NOT NULL
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) AS max FROM ${this._fqs}`
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 { subscribed, watermark: rows[0]?.max ?? -1 };
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
  });