@rotorsoft/act-pg 0.23.0 → 0.25.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/README.md +91 -119
- package/dist/.tsbuildinfo +1 -1
- package/dist/@types/postgres-store.d.ts +43 -2
- package/dist/@types/postgres-store.d.ts.map +1 -1
- package/dist/index.cjs +215 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +215 -18
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"postgres-store.d.ts","sourceRoot":"","sources":["../../src/postgres-store.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,YAAY,EACZ,SAAS,EACT,SAAS,EACT,KAAK,EAEL,OAAO,EACP,cAAc,EACd,KAAK,EACL,YAAY,EACZ,kBAAkB,EAClB,MAAM,EACN,OAAO,EACP,KAAK,EACL,iBAAiB,EACjB,YAAY,EACZ,cAAc,
|
|
1
|
+
{"version":3,"file":"postgres-store.d.ts","sourceRoot":"","sources":["../../src/postgres-store.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,YAAY,EACZ,SAAS,EACT,SAAS,EACT,KAAK,EAEL,OAAO,EACP,cAAc,EACd,KAAK,EACL,iBAAiB,EACjB,YAAY,EACZ,kBAAkB,EAClB,MAAM,EACN,OAAO,EACP,KAAK,EACL,iBAAiB,EACjB,YAAY,EACZ,cAAc,EACd,WAAW,EACZ,MAAM,gBAAgB,CAAC;AAOxB,OAAO,EAAE,MAAM,IAAI,CAAC;AAUpB,KAAK,MAAM,GAAG,QAAQ,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB,CAAC,GACA,EAAE,CAAC,UAAU,CAAC;AAsChB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2GG;AACH,qBAAa,aAAc,YAAW,KAAK;IACzC,OAAO,CAAC,KAAK,CAAC;IACd,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,OAAO,CAAC,IAAI,CAAS;IACrB,OAAO,CAAC,IAAI,CAAS;IACrB;;;;;OAKG;IACH,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAwB;IAC5C;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,8DAA8D;IAC9D,OAAO,CAAC,aAAa,CAA4B;IACjD;;;;;OAKG;IACH,OAAO,CAAC,cAAc,CAA+C;IACrE;;;;;;;;;OASG;IACH,MAAM,CAAC,EAAE,CACP,OAAO,EAAE,CAAC,YAAY,EAAE,iBAAiB,KAAK,IAAI,KAC/C,OAAO,CAAC,cAAc,CAAC,CAAC;IAE7B;;;OAGG;gBACS,MAAM,GAAE,OAAO,CAAC,MAAM,CAAM;IAiBxC;;;;OAIG;IACG,OAAO;IAKb;;;;;;OAMG;YACW,eAAe;IAe7B;;;;OAIG;IACG,IAAI;IAsGV;;;OAGG;IACG,IAAI;IAoBV;;;;;;;;;OASG;IACG,KAAK,CAAC,CAAC,SAAS,OAAO,EAC3B,QAAQ,EAAE,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,KAAK,IAAI,EAChD,KAAK,CAAC,EAAE,KAAK;IAyEf;;;;;;;;;OASG;IACG,MAAM,CAAC,CAAC,SAAS,OAAO,EAC5B,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,OAAO,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,EAAE,EAC3B,IAAI,EAAE,SAAS,EACf,eAAe,CAAC,EAAE,MAAM;IAkF1B;;;;;;;;;;;;;OAaG;IACG,KAAK,CACT,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,EAAE,EAAE,MAAM,EACV,MAAM,EAAE,MAAM,EACd,IAAI,CAAC,EAAE,MAAM,GACZ,OAAO,CAAC,KAAK,EAAE,CAAC;IAsFnB;;;;;;OAMG;IACG,SAAS,CACb,OAAO,EAAE,KAAK,CAAC;QACb,MAAM,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,IAAI,CAAC,EAAE,MAAM,CAAC;KACf,CAAC,GACD,OAAO,CAAC;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;IA8DrD;;;;;OAKG;IACG,GAAG,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC;IAkD5C;;;;OAIG;IACG,KAAK,CAAC,MAAM,EAAE,YAAY,EAAE,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;IAgD5D;;;;;OAKG;IACH;;;;;OAKG;IACH,OAAO,CAAC,aAAa;IAqCf,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC;IAmB5D;;;;;;;;;;;;;;OAcG;IACG,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC;IA0B9D;;;;;;;;;;;;;OAaG;IACG,UAAU,CAAC,MAAM,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAQzE;;;;;;;;;OASG;IACG,aAAa,CACjB,QAAQ,EAAE,CAAC,QAAQ,EAAE,cAAc,KAAK,IAAI,EAC5C,KAAK,CAAC,EAAE,YAAY,GACnB,OAAO,CAAC,kBAAkB,CAAC;IAkF9B;;;;;;;;;;;;;;;;;;;;;;;;;;OA0BG;IACG,WAAW,CAAC,CAAC,SAAS,OAAO,EACjC,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC,YAAY,EAAE,QAAQ,GAAG,cAAc,CAAC,EAC/D,OAAO,CAAC,EAAE,iBAAiB,CAAC,CAAC,CAAC,GAC7B,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC;IA2DvC;;;;OAIG;YACW,oBAAoB;IAsClC;;;;OAIG;YACW,mBAAmB;IAsGjC;;;;;;;;;;;;;;;;;;;;OAoBG;YACW,uBAAuB;IAuFrC;;;;OAIG;IACG,QAAQ,CACZ,OAAO,EAAE,KAAK,CAAC;QACb,MAAM,EAAE,MAAM,CAAC;QACf,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,IAAI,CAAC,EAAE,SAAS,CAAC;KAClB,CAAC,GACD,OAAO,CACR,GAAG,CACD,MAAM,EACN;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,SAAS,CAAC,OAAO,EAAE,MAAM,OAAO,CAAC,CAAA;KAAE,CAClE,CACF;CA2CF"}
|
package/dist/index.cjs
CHANGED
|
@@ -206,13 +206,18 @@ var PostgresStore = class {
|
|
|
206
206
|
error text,
|
|
207
207
|
leased_by text,
|
|
208
208
|
leased_until timestamptz,
|
|
209
|
-
priority int NOT NULL DEFAULT 0
|
|
209
|
+
priority int NOT NULL DEFAULT 0,
|
|
210
|
+
lane text NOT NULL DEFAULT 'default'
|
|
210
211
|
) TABLESPACE pg_default;`
|
|
211
212
|
);
|
|
212
213
|
await client.query(
|
|
213
214
|
`ALTER TABLE ${this._fqs}
|
|
214
215
|
ADD COLUMN IF NOT EXISTS priority int NOT NULL DEFAULT 0;`
|
|
215
216
|
);
|
|
217
|
+
await client.query(
|
|
218
|
+
`ALTER TABLE ${this._fqs}
|
|
219
|
+
ADD COLUMN IF NOT EXISTS lane text NOT NULL DEFAULT 'default';`
|
|
220
|
+
);
|
|
216
221
|
await client.query(
|
|
217
222
|
`DROP INDEX IF EXISTS "${this.config.schema}"."${this.config.table}_streams_fetch_ix"`
|
|
218
223
|
);
|
|
@@ -220,6 +225,10 @@ var PostgresStore = class {
|
|
|
220
225
|
`CREATE INDEX IF NOT EXISTS "${this.config.table}_streams_claim_ix"
|
|
221
226
|
ON ${this._fqs} (blocked, priority DESC, at);`
|
|
222
227
|
);
|
|
228
|
+
await client.query(
|
|
229
|
+
`CREATE INDEX IF NOT EXISTS "${this.config.table}_streams_lane_ix"
|
|
230
|
+
ON ${this._fqs} (lane);`
|
|
231
|
+
);
|
|
223
232
|
await client.query("COMMIT");
|
|
224
233
|
logger.info(
|
|
225
234
|
`Seeded schema "${this.config.schema}" with table "${this.config.table}"`
|
|
@@ -417,17 +426,20 @@ var PostgresStore = class {
|
|
|
417
426
|
* @param millis - Lease duration in milliseconds
|
|
418
427
|
* @returns Leased streams with metadata
|
|
419
428
|
*/
|
|
420
|
-
async claim(lagging, leading, by, millis) {
|
|
429
|
+
async claim(lagging, leading, by, millis, lane) {
|
|
421
430
|
const client = await this._pool.connect();
|
|
422
431
|
try {
|
|
423
432
|
await client.query("BEGIN");
|
|
433
|
+
const laneClause = lane !== void 0 ? `AND s.lane = $5` : "";
|
|
434
|
+
const params = lane !== void 0 ? [lagging, leading, by, millis, lane] : [lagging, leading, by, millis];
|
|
424
435
|
const { rows } = await client.query(
|
|
425
436
|
`
|
|
426
437
|
WITH
|
|
427
438
|
available AS (
|
|
428
|
-
SELECT stream, source, at, priority
|
|
439
|
+
SELECT stream, source, at, priority, lane
|
|
429
440
|
FROM ${this._fqs} s
|
|
430
441
|
WHERE blocked = false
|
|
442
|
+
${laneClause}
|
|
431
443
|
AND (leased_by IS NULL OR leased_until <= NOW())
|
|
432
444
|
AND (s.at < 0 OR EXISTS (
|
|
433
445
|
SELECT 1 FROM ${this._fqt} e
|
|
@@ -443,19 +455,19 @@ var PostgresStore = class {
|
|
|
443
455
|
-- ORDER BY collapses to plain at ASC so existing workloads
|
|
444
456
|
-- see no behavior change.
|
|
445
457
|
lag AS (
|
|
446
|
-
SELECT stream, source, at, TRUE AS lagging
|
|
458
|
+
SELECT stream, source, at, lane, TRUE AS lagging
|
|
447
459
|
FROM available
|
|
448
460
|
ORDER BY priority DESC, at ASC
|
|
449
461
|
LIMIT $1
|
|
450
462
|
),
|
|
451
463
|
lead AS (
|
|
452
|
-
SELECT stream, source, at, FALSE AS lagging
|
|
464
|
+
SELECT stream, source, at, lane, FALSE AS lagging
|
|
453
465
|
FROM available
|
|
454
466
|
ORDER BY at DESC
|
|
455
467
|
LIMIT $2
|
|
456
468
|
),
|
|
457
469
|
combined AS (
|
|
458
|
-
SELECT DISTINCT ON (stream) stream, source, at, lagging
|
|
470
|
+
SELECT DISTINCT ON (stream) stream, source, at, lane, lagging
|
|
459
471
|
FROM (SELECT * FROM lag UNION ALL SELECT * FROM lead) t
|
|
460
472
|
ORDER BY stream, at
|
|
461
473
|
)
|
|
@@ -466,18 +478,19 @@ var PostgresStore = class {
|
|
|
466
478
|
retry = s.retry + 1
|
|
467
479
|
FROM combined c
|
|
468
480
|
WHERE s.stream = c.stream
|
|
469
|
-
RETURNING s.stream, s.source, s.at, s.retry, c.lagging
|
|
481
|
+
RETURNING s.stream, s.source, s.at, s.retry, c.lagging, s.lane
|
|
470
482
|
`,
|
|
471
|
-
|
|
483
|
+
params
|
|
472
484
|
);
|
|
473
485
|
await client.query("COMMIT");
|
|
474
|
-
return rows.map(({ stream, source, at, retry, lagging: lagging2 }) => ({
|
|
486
|
+
return rows.map(({ stream, source, at, retry, lagging: lagging2, lane: lane2 }) => ({
|
|
475
487
|
stream,
|
|
476
488
|
source: source ?? void 0,
|
|
477
489
|
at,
|
|
478
490
|
by,
|
|
479
491
|
retry,
|
|
480
|
-
lagging: lagging2
|
|
492
|
+
lagging: lagging2,
|
|
493
|
+
lane: lane2
|
|
481
494
|
}));
|
|
482
495
|
} catch (error) {
|
|
483
496
|
await client.query("ROLLBACK").catch(() => {
|
|
@@ -503,10 +516,11 @@ var PostgresStore = class {
|
|
|
503
516
|
if (streams.length) {
|
|
504
517
|
const { rowCount: inserted } = await client.query(
|
|
505
518
|
`
|
|
506
|
-
INSERT INTO ${this._fqs} (stream, source, priority)
|
|
519
|
+
INSERT INTO ${this._fqs} (stream, source, priority, lane)
|
|
507
520
|
SELECT s->>'stream',
|
|
508
521
|
s->>'source',
|
|
509
|
-
COALESCE((s->>'priority')::int, 0)
|
|
522
|
+
COALESCE((s->>'priority')::int, 0),
|
|
523
|
+
COALESCE(s->>'lane', 'default')
|
|
510
524
|
FROM jsonb_array_elements($1::jsonb) AS s
|
|
511
525
|
ON CONFLICT (stream) DO NOTHING
|
|
512
526
|
`,
|
|
@@ -523,6 +537,16 @@ var PostgresStore = class {
|
|
|
523
537
|
`,
|
|
524
538
|
[JSON.stringify(streams)]
|
|
525
539
|
);
|
|
540
|
+
await client.query(
|
|
541
|
+
`
|
|
542
|
+
UPDATE ${this._fqs} t
|
|
543
|
+
SET lane = COALESCE(s->>'lane', 'default')
|
|
544
|
+
FROM jsonb_array_elements($1::jsonb) AS s
|
|
545
|
+
WHERE t.stream = s->>'stream'
|
|
546
|
+
AND t.lane <> COALESCE(s->>'lane', 'default')
|
|
547
|
+
`,
|
|
548
|
+
[JSON.stringify(streams)]
|
|
549
|
+
);
|
|
526
550
|
}
|
|
527
551
|
const { rows } = await client.query(
|
|
528
552
|
`SELECT COALESCE(MAX(at), -1) AS max FROM ${this._fqs}`
|
|
@@ -562,7 +586,7 @@ var PostgresStore = class {
|
|
|
562
586
|
leased_until = NULL
|
|
563
587
|
FROM input i
|
|
564
588
|
WHERE s.stream = i.stream AND s.leased_by = i.by
|
|
565
|
-
RETURNING s.stream, s.source, s.at, i.by, s.retry, i.lagging
|
|
589
|
+
RETURNING s.stream, s.source, s.at, i.by, s.retry, i.lagging, s.lane
|
|
566
590
|
`,
|
|
567
591
|
[JSON.stringify(leases)]
|
|
568
592
|
);
|
|
@@ -573,7 +597,8 @@ var PostgresStore = class {
|
|
|
573
597
|
at: row.at,
|
|
574
598
|
by: row.by,
|
|
575
599
|
retry: row.retry,
|
|
576
|
-
lagging: row.lagging
|
|
600
|
+
lagging: row.lagging,
|
|
601
|
+
lane: row.lane
|
|
577
602
|
}));
|
|
578
603
|
} catch (error) {
|
|
579
604
|
await client.query("ROLLBACK").catch(() => {
|
|
@@ -603,7 +628,7 @@ var PostgresStore = class {
|
|
|
603
628
|
SET blocked = true, error = i.error
|
|
604
629
|
FROM input i
|
|
605
630
|
WHERE s.stream = i.stream AND s.leased_by = i.by AND s.blocked = false
|
|
606
|
-
RETURNING s.stream, s.source, s.at, i.by, s.retry, s.error, i.lagging
|
|
631
|
+
RETURNING s.stream, s.source, s.at, i.by, s.retry, s.error, i.lagging, s.lane
|
|
607
632
|
`,
|
|
608
633
|
[JSON.stringify(leases)]
|
|
609
634
|
);
|
|
@@ -615,7 +640,8 @@ var PostgresStore = class {
|
|
|
615
640
|
by: row.by,
|
|
616
641
|
retry: row.retry,
|
|
617
642
|
lagging: row.lagging,
|
|
618
|
-
error: row.error
|
|
643
|
+
error: row.error,
|
|
644
|
+
lane: row.lane
|
|
619
645
|
}));
|
|
620
646
|
} catch (error) {
|
|
621
647
|
await client.query("ROLLBACK").catch(() => {
|
|
@@ -658,6 +684,10 @@ var PostgresStore = class {
|
|
|
658
684
|
values.push(filter.blocked);
|
|
659
685
|
conditions.push(`blocked = $${start + values.length - 1}`);
|
|
660
686
|
}
|
|
687
|
+
if (filter.lane !== void 0) {
|
|
688
|
+
values.push(filter.lane);
|
|
689
|
+
conditions.push(`lane = $${start + values.length - 1}`);
|
|
690
|
+
}
|
|
661
691
|
return {
|
|
662
692
|
clause: conditions.length ? conditions.join(" AND ") : "TRUE",
|
|
663
693
|
values
|
|
@@ -770,11 +800,15 @@ var PostgresStore = class {
|
|
|
770
800
|
values.push(query.blocked);
|
|
771
801
|
conditions.push(`blocked = $${values.length}`);
|
|
772
802
|
}
|
|
803
|
+
if (query?.lane !== void 0) {
|
|
804
|
+
values.push(query.lane);
|
|
805
|
+
conditions.push(`lane = $${values.length}`);
|
|
806
|
+
}
|
|
773
807
|
if (query?.after !== void 0) {
|
|
774
808
|
values.push(query.after);
|
|
775
809
|
conditions.push(`stream > $${values.length}`);
|
|
776
810
|
}
|
|
777
|
-
let sql = `SELECT stream, source, at, retry, blocked, error, leased_by, leased_until, priority FROM ${this._fqs}`;
|
|
811
|
+
let sql = `SELECT stream, source, at, retry, blocked, error, leased_by, leased_until, priority, lane FROM ${this._fqs}`;
|
|
778
812
|
if (conditions.length) sql += " WHERE " + conditions.join(" AND ");
|
|
779
813
|
values.push(limit);
|
|
780
814
|
sql += ` ORDER BY stream LIMIT $${values.length}`;
|
|
@@ -797,7 +831,8 @@ var PostgresStore = class {
|
|
|
797
831
|
error: row.error ?? "",
|
|
798
832
|
priority: row.priority,
|
|
799
833
|
leased_by: row.leased_by ?? void 0,
|
|
800
|
-
leased_until: row.leased_until ?? void 0
|
|
834
|
+
leased_until: row.leased_until ?? void 0,
|
|
835
|
+
lane: row.lane
|
|
801
836
|
});
|
|
802
837
|
count++;
|
|
803
838
|
}
|
|
@@ -806,6 +841,168 @@ var PostgresStore = class {
|
|
|
806
841
|
client.release();
|
|
807
842
|
}
|
|
808
843
|
}
|
|
844
|
+
/**
|
|
845
|
+
* Per-stream aggregated stats — see {@link Store.query_stats}.
|
|
846
|
+
*
|
|
847
|
+
* Two code paths chosen by the requested stats:
|
|
848
|
+
*
|
|
849
|
+
* - **Heads-only path** (no `count`, no `names`): one or two
|
|
850
|
+
* `SELECT DISTINCT ON (stream) ... ORDER BY stream, version DESC|ASC`
|
|
851
|
+
* queries, executed in parallel when `tail: true`. The
|
|
852
|
+
* `(stream, version)` unique index gives index-only access — K rows
|
|
853
|
+
* touched per query (K = matched streams), not N (events).
|
|
854
|
+
* Ordering by `version` (not `id`) is equivalent within a stream
|
|
855
|
+
* (versions are monotonic per stream and events are committed
|
|
856
|
+
* sequentially) and is the column actually indexed.
|
|
857
|
+
*
|
|
858
|
+
* - **Full-scan path** (`count` or `names` set): one CTE materializes
|
|
859
|
+
* the filtered events, then `GROUP BY stream, name` →
|
|
860
|
+
* `jsonb_object_agg(name, n)` for the `names` map plus per-stream
|
|
861
|
+
* `COUNT(*)` for `count`. Heads (and `tails` when requested) come
|
|
862
|
+
* from `DISTINCT ON` over the same CTE — they ride free on the
|
|
863
|
+
* already-paid scan.
|
|
864
|
+
*
|
|
865
|
+
* The stream universe is derived from the events table: filter form
|
|
866
|
+
* matches event-bearing streams (not subscription rows). When the
|
|
867
|
+
* filter sets `source` or `blocked`, the events table is joined
|
|
868
|
+
* against the streams subscription table since those concepts only
|
|
869
|
+
* exist for subscribed streams.
|
|
870
|
+
*/
|
|
871
|
+
async query_stats(input, options) {
|
|
872
|
+
const exclude = options?.exclude ?? [];
|
|
873
|
+
const wantTail = options?.tail ?? false;
|
|
874
|
+
const wantCount = options?.count ?? false;
|
|
875
|
+
const wantNames = options?.names ?? false;
|
|
876
|
+
const before = options?.before;
|
|
877
|
+
const fullScan = wantCount || wantNames;
|
|
878
|
+
if (Array.isArray(input) && input.length === 0) {
|
|
879
|
+
return /* @__PURE__ */ new Map();
|
|
880
|
+
}
|
|
881
|
+
const where = [];
|
|
882
|
+
const params = [];
|
|
883
|
+
if (Array.isArray(input)) {
|
|
884
|
+
params.push(input);
|
|
885
|
+
where.push(`e.stream = ANY($${params.length})`);
|
|
886
|
+
} else if (input.stream !== void 0) {
|
|
887
|
+
params.push(input.stream);
|
|
888
|
+
where.push(
|
|
889
|
+
input.stream_exact ? `e.stream = $${params.length}` : `e.stream ~ $${params.length}`
|
|
890
|
+
);
|
|
891
|
+
}
|
|
892
|
+
if (exclude.length) {
|
|
893
|
+
params.push(exclude);
|
|
894
|
+
where.push(`e.name <> ALL($${params.length})`);
|
|
895
|
+
}
|
|
896
|
+
if (before !== void 0) {
|
|
897
|
+
params.push(before);
|
|
898
|
+
where.push(`e.id < $${params.length}`);
|
|
899
|
+
}
|
|
900
|
+
const fromClause = `${this._fqt} e`;
|
|
901
|
+
const whereClause = `WHERE ${where.length ? where.join(" AND ") : "TRUE"}`;
|
|
902
|
+
return fullScan ? this._queryStatsFullScan(
|
|
903
|
+
fromClause,
|
|
904
|
+
whereClause,
|
|
905
|
+
params,
|
|
906
|
+
wantTail,
|
|
907
|
+
wantCount,
|
|
908
|
+
wantNames
|
|
909
|
+
) : this._queryStatsHeadsOnly(fromClause, whereClause, params, wantTail);
|
|
910
|
+
}
|
|
911
|
+
/**
|
|
912
|
+
* Cheap path: index-only DISTINCT ON for the head per stream, plus an
|
|
913
|
+
* optional second query (in parallel) for the tail. K rows touched
|
|
914
|
+
* per query, not N events.
|
|
915
|
+
*/
|
|
916
|
+
async _queryStatsHeadsOnly(fromClause, whereClause, params, wantTail) {
|
|
917
|
+
const cols = `e.id, e.stream, e.version, e.name, e.data, e.created, e.meta`;
|
|
918
|
+
const headSql = `SELECT DISTINCT ON (e.stream) ${cols} FROM ${fromClause} ${whereClause} ORDER BY e.stream, e.version DESC`;
|
|
919
|
+
const tailSql = wantTail ? `SELECT DISTINCT ON (e.stream) ${cols} FROM ${fromClause} ${whereClause} ORDER BY e.stream, e.version ASC` : null;
|
|
920
|
+
const [headRes, tailRes] = await Promise.all([
|
|
921
|
+
this._pool.query(headSql, params),
|
|
922
|
+
tailSql ? this._pool.query(tailSql, params) : Promise.resolve(null)
|
|
923
|
+
]);
|
|
924
|
+
const out = /* @__PURE__ */ new Map();
|
|
925
|
+
for (const row of headRes.rows) {
|
|
926
|
+
out.set(row.stream, { head: row });
|
|
927
|
+
}
|
|
928
|
+
if (tailRes) {
|
|
929
|
+
for (const row of tailRes.rows) {
|
|
930
|
+
out.get(row.stream).tail = row;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
return out;
|
|
934
|
+
}
|
|
935
|
+
/**
|
|
936
|
+
* Full-scan path: one CTE-based query computes the per-stream
|
|
937
|
+
* `COUNT(*)` and `jsonb_object_agg(name, n)` map alongside the head
|
|
938
|
+
* (and tail when requested). All extras share the single events scan.
|
|
939
|
+
*/
|
|
940
|
+
async _queryStatsFullScan(fromClause, whereClause, params, wantTail, wantCount, wantNames) {
|
|
941
|
+
const tailCte = wantTail ? `, tails AS (SELECT DISTINCT ON (stream) * FROM ef ORDER BY stream, version ASC)` : "";
|
|
942
|
+
const tailJoin = wantTail ? `LEFT JOIN tails t ON t.stream = h.stream` : "";
|
|
943
|
+
const tailCols = wantTail ? `, t.id AS t_id, t.stream AS t_stream, t.version AS t_version,
|
|
944
|
+
t.name AS t_name, t.data AS t_data, t.created AS t_created, t.meta AS t_meta` : "";
|
|
945
|
+
const sql = `
|
|
946
|
+
WITH ef AS (
|
|
947
|
+
SELECT e.id, e.stream, e.version, e.name, e.data, e.created, e.meta
|
|
948
|
+
FROM ${fromClause}
|
|
949
|
+
${whereClause}
|
|
950
|
+
),
|
|
951
|
+
agg AS (
|
|
952
|
+
SELECT stream,
|
|
953
|
+
SUM(n)::int AS cnt,
|
|
954
|
+
jsonb_object_agg(name, n) AS names
|
|
955
|
+
FROM (
|
|
956
|
+
SELECT stream, name, COUNT(*)::int AS n
|
|
957
|
+
FROM ef
|
|
958
|
+
GROUP BY stream, name
|
|
959
|
+
) t
|
|
960
|
+
GROUP BY stream
|
|
961
|
+
),
|
|
962
|
+
heads AS (
|
|
963
|
+
SELECT DISTINCT ON (stream) * FROM ef ORDER BY stream, version DESC
|
|
964
|
+
)
|
|
965
|
+
${tailCte}
|
|
966
|
+
SELECT
|
|
967
|
+
h.id, h.stream, h.version, h.name, h.data, h.created, h.meta,
|
|
968
|
+
a.cnt AS agg_count,
|
|
969
|
+
a.names AS agg_names
|
|
970
|
+
${tailCols}
|
|
971
|
+
FROM heads h
|
|
972
|
+
LEFT JOIN agg a ON a.stream = h.stream
|
|
973
|
+
${tailJoin}
|
|
974
|
+
`;
|
|
975
|
+
const res = await this._pool.query(sql, params);
|
|
976
|
+
const out = /* @__PURE__ */ new Map();
|
|
977
|
+
for (const row of res.rows) {
|
|
978
|
+
const stats = {
|
|
979
|
+
head: {
|
|
980
|
+
id: row.id,
|
|
981
|
+
stream: row.stream,
|
|
982
|
+
version: row.version,
|
|
983
|
+
name: row.name,
|
|
984
|
+
data: row.data,
|
|
985
|
+
created: row.created,
|
|
986
|
+
meta: row.meta
|
|
987
|
+
}
|
|
988
|
+
};
|
|
989
|
+
if (wantTail && row.t_id !== void 0 && row.t_id !== null) {
|
|
990
|
+
stats.tail = {
|
|
991
|
+
id: row.t_id,
|
|
992
|
+
stream: row.t_stream,
|
|
993
|
+
version: row.t_version,
|
|
994
|
+
name: row.t_name,
|
|
995
|
+
data: row.t_data,
|
|
996
|
+
created: row.t_created,
|
|
997
|
+
meta: row.t_meta
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
if (wantCount) stats.count = row.agg_count;
|
|
1001
|
+
if (wantNames) stats.names = row.agg_names;
|
|
1002
|
+
out.set(row.stream, stats);
|
|
1003
|
+
}
|
|
1004
|
+
return out;
|
|
1005
|
+
}
|
|
809
1006
|
/**
|
|
810
1007
|
* Implementation of the optional `Store.notify` hook. Bound onto
|
|
811
1008
|
* `this.notify` in the constructor when `config.notify === true`,
|