@rotorsoft/act-pg 1.4.0 → 1.4.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 +7 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/@types/postgres-store.d.ts +24 -7
- package/dist/@types/postgres-store.d.ts.map +1 -1
- package/dist/index.cjs +107 -74
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +107 -74
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
|
@@ -155,14 +155,14 @@ export declare class PostgresStore implements Store {
|
|
|
155
155
|
*/
|
|
156
156
|
private readonly _channel;
|
|
157
157
|
/** Active LISTEN client (one per `notify()` subscription). */
|
|
158
|
-
private
|
|
158
|
+
private _listen_client;
|
|
159
159
|
/**
|
|
160
160
|
* Notification listener attached to the active LISTEN client. Tracked
|
|
161
161
|
* separately so the re-subscribe / dispose paths can detach it before
|
|
162
162
|
* destroying the client — without this, a pool that reused the
|
|
163
163
|
* connection would re-fire the stale handler.
|
|
164
164
|
*/
|
|
165
|
-
private
|
|
165
|
+
private _listen_handler;
|
|
166
166
|
/**
|
|
167
167
|
* Cross-process commit subscription. **Present only when
|
|
168
168
|
* `config.notify === true`** — the orchestrator's auto-wire path
|
|
@@ -192,7 +192,7 @@ export declare class PostgresStore implements Store {
|
|
|
192
192
|
* destroying belt-and-braces guards against any future change in
|
|
193
193
|
* pg-pool semantics that could re-issue a half-clean client).
|
|
194
194
|
*/
|
|
195
|
-
private
|
|
195
|
+
private _teardown_listen;
|
|
196
196
|
/**
|
|
197
197
|
* Seed the database with required tables, indexes, and schema for event storage.
|
|
198
198
|
* @returns Promise that resolves when seeding is complete
|
|
@@ -282,7 +282,7 @@ export declare class PostgresStore implements Store {
|
|
|
282
282
|
* `WHERE` — callers compose it with any other predicates they need.
|
|
283
283
|
* Returns an always-true clause (`true`) when the filter is empty.
|
|
284
284
|
*/
|
|
285
|
-
private
|
|
285
|
+
private _filter_clause;
|
|
286
286
|
reset(input: string[] | StreamFilter): Promise<number>;
|
|
287
287
|
/**
|
|
288
288
|
* Clear blocked flag (and retry / error / lease state) on streams
|
|
@@ -359,13 +359,13 @@ export declare class PostgresStore implements Store {
|
|
|
359
359
|
* optional second query (in parallel) for the tail. K rows touched
|
|
360
360
|
* per query, not N events.
|
|
361
361
|
*/
|
|
362
|
-
private
|
|
362
|
+
private _query_stats_heads_only;
|
|
363
363
|
/**
|
|
364
364
|
* Full-scan path: one CTE-based query computes the per-stream
|
|
365
365
|
* `COUNT(*)` and `jsonb_object_agg(name, n)` map alongside the head
|
|
366
366
|
* (and tail when requested). All extras share the single events scan.
|
|
367
367
|
*/
|
|
368
|
-
private
|
|
368
|
+
private _query_stats_full_scan;
|
|
369
369
|
/**
|
|
370
370
|
* Implementation of the optional `Store.notify` hook. Bound onto
|
|
371
371
|
* `this.notify` in the constructor when `config.notify === true`,
|
|
@@ -387,7 +387,7 @@ export declare class PostgresStore implements Store {
|
|
|
387
387
|
* @param handler Called for each cross-process commit notification.
|
|
388
388
|
* @returns Disposer that releases the LISTEN client.
|
|
389
389
|
*/
|
|
390
|
-
private
|
|
390
|
+
private _subscribe_notifications;
|
|
391
391
|
/**
|
|
392
392
|
* Atomically truncates streams and seeds each with a snapshot or tombstone.
|
|
393
393
|
* @param targets - Streams to truncate with optional snapshot state and meta.
|
|
@@ -414,6 +414,23 @@ export declare class PostgresStore implements Store {
|
|
|
414
414
|
* from 1. `created` is preserved verbatim from the source.
|
|
415
415
|
*/
|
|
416
416
|
restore(driver: (callback: (event: Committed<Schemas, keyof Schemas>) => Promise<number>) => Promise<void>): Promise<void>;
|
|
417
|
+
/**
|
|
418
|
+
* Wipe the sensitive-data payload for every event on the stream — the
|
|
419
|
+
* physical-erasure side of the sensitive-data epic (#566). Sets
|
|
420
|
+
* `events.pii` to `NULL` for the stream's events; `events.data` and
|
|
421
|
+
* the rest of the row are never touched.
|
|
422
|
+
*
|
|
423
|
+
* Row-level locks (no table lock), bounded by events-per-stream.
|
|
424
|
+
* Idempotent — a second call on an already-wiped stream returns `0`.
|
|
425
|
+
*
|
|
426
|
+
* Disk reclamation is autovacuum-driven; for strict-deletion
|
|
427
|
+
* jurisdictions the production checklist documents `VACUUM FULL` as
|
|
428
|
+
* the operator step.
|
|
429
|
+
*
|
|
430
|
+
* @param stream Target stream
|
|
431
|
+
* @returns Count of events whose `pii` was set to `NULL`
|
|
432
|
+
*/
|
|
433
|
+
forget_pii(stream: string): Promise<number>;
|
|
417
434
|
}
|
|
418
435
|
export {};
|
|
419
436
|
//# sourceMappingURL=postgres-store.d.ts.map
|
|
@@ -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,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,
|
|
1
|
+
{"version":3,"file":"postgres-store.d.ts","sourceRoot":"","sources":["../../src/postgres-store.ts"],"names":[],"mappings":"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"}
|
package/dist/index.cjs
CHANGED
|
@@ -58,10 +58,10 @@ types.setTypeParser(
|
|
|
58
58
|
var SAFE_IDENTIFIER = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
59
59
|
var PG_UNIQUE_VIOLATION = "23505";
|
|
60
60
|
var NOTIFY_CHANNEL_PREFIX = "act_commit";
|
|
61
|
-
function
|
|
61
|
+
function notify_channel(schema, table) {
|
|
62
62
|
return `${NOTIFY_CHANNEL_PREFIX}_${schema}_${table}`;
|
|
63
63
|
}
|
|
64
|
-
function
|
|
64
|
+
function assert_safe_identifier(value, label) {
|
|
65
65
|
if (!SAFE_IDENTIFIER.test(value))
|
|
66
66
|
throw new Error(`Unsafe SQL identifier for ${label}: "${value}"`);
|
|
67
67
|
}
|
|
@@ -94,14 +94,14 @@ var PostgresStore = class {
|
|
|
94
94
|
*/
|
|
95
95
|
_channel;
|
|
96
96
|
/** Active LISTEN client (one per `notify()` subscription). */
|
|
97
|
-
|
|
97
|
+
_listen_client;
|
|
98
98
|
/**
|
|
99
99
|
* Notification listener attached to the active LISTEN client. Tracked
|
|
100
100
|
* separately so the re-subscribe / dispose paths can detach it before
|
|
101
101
|
* destroying the client — without this, a pool that reused the
|
|
102
102
|
* connection would re-fire the stale handler.
|
|
103
103
|
*/
|
|
104
|
-
|
|
104
|
+
_listen_handler;
|
|
105
105
|
/**
|
|
106
106
|
* Cross-process commit subscription. **Present only when
|
|
107
107
|
* `config.notify === true`** — the orchestrator's auto-wire path
|
|
@@ -119,15 +119,15 @@ var PostgresStore = class {
|
|
|
119
119
|
*/
|
|
120
120
|
constructor(config = {}) {
|
|
121
121
|
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
122
|
-
|
|
123
|
-
|
|
122
|
+
assert_safe_identifier(this.config.schema, "schema");
|
|
123
|
+
assert_safe_identifier(this.config.table, "table");
|
|
124
124
|
const { schema: _, table: __, ...poolConfig } = this.config;
|
|
125
125
|
this._pool = new Pool(poolConfig);
|
|
126
126
|
this._fqt = `"${this.config.schema}"."${this.config.table}"`;
|
|
127
127
|
this._fqs = `"${this.config.schema}"."${this.config.table}_streams"`;
|
|
128
|
-
this._channel =
|
|
128
|
+
this._channel = notify_channel(this.config.schema, this.config.table);
|
|
129
129
|
if (this.config.notify) {
|
|
130
|
-
this.notify = this.
|
|
130
|
+
this.notify = this._subscribe_notifications.bind(this);
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
133
|
/**
|
|
@@ -136,7 +136,7 @@ var PostgresStore = class {
|
|
|
136
136
|
* @returns Promise that resolves when all connections are closed
|
|
137
137
|
*/
|
|
138
138
|
async dispose() {
|
|
139
|
-
await this.
|
|
139
|
+
await this._teardown_listen();
|
|
140
140
|
await this._pool.end();
|
|
141
141
|
}
|
|
142
142
|
/**
|
|
@@ -146,16 +146,16 @@ var PostgresStore = class {
|
|
|
146
146
|
* destroying belt-and-braces guards against any future change in
|
|
147
147
|
* pg-pool semantics that could re-issue a half-clean client).
|
|
148
148
|
*/
|
|
149
|
-
async
|
|
150
|
-
if (!this.
|
|
151
|
-
this.
|
|
152
|
-
this.
|
|
149
|
+
async _teardown_listen() {
|
|
150
|
+
if (!this._listen_client) return;
|
|
151
|
+
this._listen_client.removeListener("notification", this._listen_handler);
|
|
152
|
+
this._listen_handler = void 0;
|
|
153
153
|
try {
|
|
154
|
-
await this.
|
|
154
|
+
await this._listen_client.query(`UNLISTEN ${this._channel}`);
|
|
155
155
|
} catch {
|
|
156
156
|
}
|
|
157
|
-
this.
|
|
158
|
-
this.
|
|
157
|
+
this._listen_client.release(true);
|
|
158
|
+
this._listen_client = void 0;
|
|
159
159
|
}
|
|
160
160
|
/**
|
|
161
161
|
* Seed the database with required tables, indexes, and schema for event storage.
|
|
@@ -177,9 +177,13 @@ var PostgresStore = class {
|
|
|
177
177
|
stream varchar(100) COLLATE pg_catalog."default" NOT NULL,
|
|
178
178
|
version int NOT NULL,
|
|
179
179
|
created timestamptz NOT NULL DEFAULT now(),
|
|
180
|
-
meta jsonb
|
|
180
|
+
meta jsonb,
|
|
181
|
+
pii jsonb
|
|
181
182
|
) TABLESPACE pg_default;`
|
|
182
183
|
);
|
|
184
|
+
await client.query(
|
|
185
|
+
`ALTER TABLE ${this._fqt} ADD COLUMN IF NOT EXISTS pii jsonb;`
|
|
186
|
+
);
|
|
183
187
|
await client.query(
|
|
184
188
|
`CREATE UNIQUE INDEX IF NOT EXISTS "${this.config.table}_stream_ix"
|
|
185
189
|
ON ${this._fqt} (stream COLLATE pg_catalog."default", version);`
|
|
@@ -370,12 +374,12 @@ var PostgresStore = class {
|
|
|
370
374
|
expectedVersion
|
|
371
375
|
);
|
|
372
376
|
const committed = [];
|
|
373
|
-
for (const { name, data } of msgs) {
|
|
377
|
+
for (const { name, data, pii } of msgs) {
|
|
374
378
|
version++;
|
|
375
379
|
const sql = `
|
|
376
|
-
INSERT INTO ${this._fqt}(name, data, stream, version, meta)
|
|
377
|
-
VALUES($1, $2, $3, $4, $5) RETURNING *`;
|
|
378
|
-
const vals = [name, data, stream, version, meta];
|
|
380
|
+
INSERT INTO ${this._fqt}(name, data, pii, stream, version, meta)
|
|
381
|
+
VALUES($1, $2, $3, $4, $5, $6) RETURNING *`;
|
|
382
|
+
const vals = [name, data, pii ?? null, stream, version, meta];
|
|
379
383
|
try {
|
|
380
384
|
const { rows } = await client.query(sql, vals);
|
|
381
385
|
committed.push(rows.at(0));
|
|
@@ -430,7 +434,7 @@ var PostgresStore = class {
|
|
|
430
434
|
const client = await this._pool.connect();
|
|
431
435
|
try {
|
|
432
436
|
await client.query("BEGIN");
|
|
433
|
-
const
|
|
437
|
+
const lane_clause = lane !== void 0 ? `AND s.lane = $5` : "";
|
|
434
438
|
const params = lane !== void 0 ? [lagging, leading, by, millis, lane] : [lagging, leading, by, millis];
|
|
435
439
|
const { rows } = await client.query(
|
|
436
440
|
`
|
|
@@ -439,7 +443,7 @@ var PostgresStore = class {
|
|
|
439
443
|
SELECT stream, source, at, priority, lane
|
|
440
444
|
FROM ${this._fqs} s
|
|
441
445
|
WHERE blocked = false
|
|
442
|
-
${
|
|
446
|
+
${lane_clause}
|
|
443
447
|
AND (leased_by IS NULL OR leased_until <= NOW())
|
|
444
448
|
AND (s.at < 0 OR EXISTS (
|
|
445
449
|
SELECT 1 FROM ${this._fqt} e
|
|
@@ -664,7 +668,7 @@ var PostgresStore = class {
|
|
|
664
668
|
* `WHERE` — callers compose it with any other predicates they need.
|
|
665
669
|
* Returns an always-true clause (`true`) when the filter is empty.
|
|
666
670
|
*/
|
|
667
|
-
|
|
671
|
+
_filter_clause(filter, start) {
|
|
668
672
|
const conditions = [];
|
|
669
673
|
const values = [];
|
|
670
674
|
if (filter.stream !== void 0) {
|
|
@@ -694,19 +698,19 @@ var PostgresStore = class {
|
|
|
694
698
|
};
|
|
695
699
|
}
|
|
696
700
|
async reset(input) {
|
|
697
|
-
const
|
|
701
|
+
const set_clause = `SET at = -1, retry = 0, blocked = false, error = NULL,
|
|
698
702
|
leased_by = NULL, leased_until = NULL`;
|
|
699
703
|
if (Array.isArray(input)) {
|
|
700
704
|
if (!input.length) return 0;
|
|
701
705
|
const { rowCount: rowCount2 } = await this._pool.query(
|
|
702
|
-
`UPDATE ${this._fqs} ${
|
|
706
|
+
`UPDATE ${this._fqs} ${set_clause} WHERE stream = ANY($1)`,
|
|
703
707
|
[input]
|
|
704
708
|
);
|
|
705
709
|
return rowCount2 ?? 0;
|
|
706
710
|
}
|
|
707
|
-
const { clause, values } = this.
|
|
711
|
+
const { clause, values } = this._filter_clause(input, 1);
|
|
708
712
|
const { rowCount } = await this._pool.query(
|
|
709
|
-
`UPDATE ${this._fqs} ${
|
|
713
|
+
`UPDATE ${this._fqs} ${set_clause} WHERE ${clause}`,
|
|
710
714
|
values
|
|
711
715
|
);
|
|
712
716
|
return rowCount ?? 0;
|
|
@@ -727,23 +731,23 @@ var PostgresStore = class {
|
|
|
727
731
|
* @returns Count of streams that were actually flipped (were blocked).
|
|
728
732
|
*/
|
|
729
733
|
async unblock(input) {
|
|
730
|
-
const
|
|
734
|
+
const set_clause = `SET retry = -1, blocked = false, error = NULL,
|
|
731
735
|
leased_by = NULL, leased_until = NULL`;
|
|
732
736
|
if (Array.isArray(input)) {
|
|
733
737
|
if (!input.length) return 0;
|
|
734
738
|
const { rowCount: rowCount2 } = await this._pool.query(
|
|
735
|
-
`UPDATE ${this._fqs} ${
|
|
739
|
+
`UPDATE ${this._fqs} ${set_clause}
|
|
736
740
|
WHERE stream = ANY($1) AND blocked = true`,
|
|
737
741
|
[input]
|
|
738
742
|
);
|
|
739
743
|
return rowCount2 ?? 0;
|
|
740
744
|
}
|
|
741
|
-
const { clause, values } = this.
|
|
745
|
+
const { clause, values } = this._filter_clause(
|
|
742
746
|
{ ...input, blocked: true },
|
|
743
747
|
1
|
|
744
748
|
);
|
|
745
749
|
const { rowCount } = await this._pool.query(
|
|
746
|
-
`UPDATE ${this._fqs} ${
|
|
750
|
+
`UPDATE ${this._fqs} ${set_clause} WHERE ${clause}`,
|
|
747
751
|
values
|
|
748
752
|
);
|
|
749
753
|
return rowCount ?? 0;
|
|
@@ -763,7 +767,7 @@ var PostgresStore = class {
|
|
|
763
767
|
* @returns Count of streams whose priority changed.
|
|
764
768
|
*/
|
|
765
769
|
async prioritize(filter, priority) {
|
|
766
|
-
const { clause, values } = this.
|
|
770
|
+
const { clause, values } = this._filter_clause(filter, 2);
|
|
767
771
|
const sql = `UPDATE ${this._fqs} SET priority = $1
|
|
768
772
|
WHERE priority <> $1 AND ${clause}`;
|
|
769
773
|
const { rowCount } = await this._pool.query(sql, [priority, ...values]);
|
|
@@ -870,11 +874,11 @@ var PostgresStore = class {
|
|
|
870
874
|
*/
|
|
871
875
|
async query_stats(input, options) {
|
|
872
876
|
const exclude = options?.exclude ?? [];
|
|
873
|
-
const
|
|
874
|
-
const
|
|
875
|
-
const
|
|
877
|
+
const want_tail = options?.tail ?? false;
|
|
878
|
+
const want_count = options?.count ?? false;
|
|
879
|
+
const want_names = options?.names ?? false;
|
|
876
880
|
const before = options?.before;
|
|
877
|
-
const
|
|
881
|
+
const full_scan = want_count || want_names;
|
|
878
882
|
if (Array.isArray(input) && input.length === 0) {
|
|
879
883
|
return /* @__PURE__ */ new Map();
|
|
880
884
|
}
|
|
@@ -897,29 +901,34 @@ var PostgresStore = class {
|
|
|
897
901
|
params.push(before);
|
|
898
902
|
where.push(`e.id < $${params.length}`);
|
|
899
903
|
}
|
|
900
|
-
const
|
|
901
|
-
const
|
|
902
|
-
return
|
|
903
|
-
|
|
904
|
-
|
|
904
|
+
const from_clause = `${this._fqt} e`;
|
|
905
|
+
const where_clause = `WHERE ${where.length ? where.join(" AND ") : "TRUE"}`;
|
|
906
|
+
return full_scan ? this._query_stats_full_scan(
|
|
907
|
+
from_clause,
|
|
908
|
+
where_clause,
|
|
909
|
+
params,
|
|
910
|
+
want_tail,
|
|
911
|
+
want_count,
|
|
912
|
+
want_names
|
|
913
|
+
) : this._query_stats_heads_only(
|
|
914
|
+
from_clause,
|
|
915
|
+
where_clause,
|
|
905
916
|
params,
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
wantNames
|
|
909
|
-
) : this._queryStatsHeadsOnly(fromClause, whereClause, params, wantTail);
|
|
917
|
+
want_tail
|
|
918
|
+
);
|
|
910
919
|
}
|
|
911
920
|
/**
|
|
912
921
|
* Cheap path: index-only DISTINCT ON for the head per stream, plus an
|
|
913
922
|
* optional second query (in parallel) for the tail. K rows touched
|
|
914
923
|
* per query, not N events.
|
|
915
924
|
*/
|
|
916
|
-
async
|
|
925
|
+
async _query_stats_heads_only(from_clause, where_clause, params, want_tail) {
|
|
917
926
|
const cols = `e.id, e.stream, e.version, e.name, e.data, e.created, e.meta`;
|
|
918
|
-
const
|
|
919
|
-
const
|
|
927
|
+
const head_sql = `SELECT DISTINCT ON (e.stream) ${cols} FROM ${from_clause} ${where_clause} ORDER BY e.stream, e.version DESC`;
|
|
928
|
+
const tail_sql = want_tail ? `SELECT DISTINCT ON (e.stream) ${cols} FROM ${from_clause} ${where_clause} ORDER BY e.stream, e.version ASC` : null;
|
|
920
929
|
const [headRes, tailRes] = await Promise.all([
|
|
921
|
-
this._pool.query(
|
|
922
|
-
|
|
930
|
+
this._pool.query(head_sql, params),
|
|
931
|
+
tail_sql ? this._pool.query(tail_sql, params) : Promise.resolve(null)
|
|
923
932
|
]);
|
|
924
933
|
const out = /* @__PURE__ */ new Map();
|
|
925
934
|
for (const row of headRes.rows) {
|
|
@@ -937,16 +946,16 @@ var PostgresStore = class {
|
|
|
937
946
|
* `COUNT(*)` and `jsonb_object_agg(name, n)` map alongside the head
|
|
938
947
|
* (and tail when requested). All extras share the single events scan.
|
|
939
948
|
*/
|
|
940
|
-
async
|
|
941
|
-
const
|
|
942
|
-
const
|
|
943
|
-
const
|
|
949
|
+
async _query_stats_full_scan(from_clause, where_clause, params, want_tail, want_count, want_names) {
|
|
950
|
+
const tail_cte = want_tail ? `, tails AS (SELECT DISTINCT ON (stream) * FROM ef ORDER BY stream, version ASC)` : "";
|
|
951
|
+
const tail_join = want_tail ? `LEFT JOIN tails t ON t.stream = h.stream` : "";
|
|
952
|
+
const tail_cols = want_tail ? `, t.id AS t_id, t.stream AS t_stream, t.version AS t_version,
|
|
944
953
|
t.name AS t_name, t.data AS t_data, t.created AS t_created, t.meta AS t_meta` : "";
|
|
945
954
|
const sql = `
|
|
946
955
|
WITH ef AS (
|
|
947
956
|
SELECT e.id, e.stream, e.version, e.name, e.data, e.created, e.meta
|
|
948
|
-
FROM ${
|
|
949
|
-
${
|
|
957
|
+
FROM ${from_clause}
|
|
958
|
+
${where_clause}
|
|
950
959
|
),
|
|
951
960
|
agg AS (
|
|
952
961
|
SELECT stream,
|
|
@@ -962,15 +971,15 @@ var PostgresStore = class {
|
|
|
962
971
|
heads AS (
|
|
963
972
|
SELECT DISTINCT ON (stream) * FROM ef ORDER BY stream, version DESC
|
|
964
973
|
)
|
|
965
|
-
${
|
|
974
|
+
${tail_cte}
|
|
966
975
|
SELECT
|
|
967
976
|
h.id, h.stream, h.version, h.name, h.data, h.created, h.meta,
|
|
968
977
|
a.cnt AS agg_count,
|
|
969
978
|
a.names AS agg_names
|
|
970
|
-
${
|
|
979
|
+
${tail_cols}
|
|
971
980
|
FROM heads h
|
|
972
981
|
LEFT JOIN agg a ON a.stream = h.stream
|
|
973
|
-
${
|
|
982
|
+
${tail_join}
|
|
974
983
|
`;
|
|
975
984
|
const res = await this._pool.query(sql, params);
|
|
976
985
|
const out = /* @__PURE__ */ new Map();
|
|
@@ -986,7 +995,7 @@ var PostgresStore = class {
|
|
|
986
995
|
meta: row.meta
|
|
987
996
|
}
|
|
988
997
|
};
|
|
989
|
-
if (
|
|
998
|
+
if (want_tail && row.t_id !== void 0 && row.t_id !== null) {
|
|
990
999
|
stats.tail = {
|
|
991
1000
|
id: row.t_id,
|
|
992
1001
|
stream: row.t_stream,
|
|
@@ -997,8 +1006,8 @@ var PostgresStore = class {
|
|
|
997
1006
|
meta: row.t_meta
|
|
998
1007
|
};
|
|
999
1008
|
}
|
|
1000
|
-
if (
|
|
1001
|
-
if (
|
|
1009
|
+
if (want_count) stats.count = row.agg_count;
|
|
1010
|
+
if (want_names) stats.names = row.agg_names;
|
|
1002
1011
|
out.set(row.stream, stats);
|
|
1003
1012
|
}
|
|
1004
1013
|
return out;
|
|
@@ -1024,10 +1033,10 @@ var PostgresStore = class {
|
|
|
1024
1033
|
* @param handler Called for each cross-process commit notification.
|
|
1025
1034
|
* @returns Disposer that releases the LISTEN client.
|
|
1026
1035
|
*/
|
|
1027
|
-
async
|
|
1028
|
-
await this.
|
|
1036
|
+
async _subscribe_notifications(handler) {
|
|
1037
|
+
await this._teardown_listen();
|
|
1029
1038
|
const client = await this._pool.connect();
|
|
1030
|
-
const
|
|
1039
|
+
const on_notification = (msg) => {
|
|
1031
1040
|
if (msg.channel !== this._channel) return;
|
|
1032
1041
|
if (!msg.payload) return;
|
|
1033
1042
|
let parsed;
|
|
@@ -1064,19 +1073,19 @@ var PostgresStore = class {
|
|
|
1064
1073
|
logger.error(err, "act_commit: handler threw, listener preserved");
|
|
1065
1074
|
}
|
|
1066
1075
|
};
|
|
1067
|
-
client.on("notification",
|
|
1076
|
+
client.on("notification", on_notification);
|
|
1068
1077
|
try {
|
|
1069
1078
|
await client.query(`LISTEN ${this._channel}`);
|
|
1070
1079
|
} catch (err) {
|
|
1071
|
-
client.removeListener("notification",
|
|
1080
|
+
client.removeListener("notification", on_notification);
|
|
1072
1081
|
client.release(true);
|
|
1073
1082
|
throw err;
|
|
1074
1083
|
}
|
|
1075
|
-
this.
|
|
1076
|
-
this.
|
|
1084
|
+
this._listen_client = client;
|
|
1085
|
+
this._listen_handler = on_notification;
|
|
1077
1086
|
return async () => {
|
|
1078
|
-
if (this.
|
|
1079
|
-
await this.
|
|
1087
|
+
if (this._listen_client !== client) return;
|
|
1088
|
+
await this._teardown_listen();
|
|
1080
1089
|
};
|
|
1081
1090
|
}
|
|
1082
1091
|
/**
|
|
@@ -1147,11 +1156,12 @@ var PostgresStore = class {
|
|
|
1147
1156
|
await client.query(`TRUNCATE TABLE ${this._fqs}`);
|
|
1148
1157
|
await driver(async (event) => {
|
|
1149
1158
|
const { rows } = await client.query(
|
|
1150
|
-
`INSERT INTO ${this._fqt}(name, data, stream, version, created, meta)
|
|
1151
|
-
VALUES($1, $2, $3, $4, $5, $6) RETURNING id`,
|
|
1159
|
+
`INSERT INTO ${this._fqt}(name, data, pii, stream, version, created, meta)
|
|
1160
|
+
VALUES($1, $2, $3, $4, $5, $6, $7) RETURNING id`,
|
|
1152
1161
|
[
|
|
1153
1162
|
event.name,
|
|
1154
1163
|
event.data,
|
|
1164
|
+
event.pii ?? null,
|
|
1155
1165
|
event.stream,
|
|
1156
1166
|
event.version,
|
|
1157
1167
|
event.created,
|
|
@@ -1169,6 +1179,29 @@ var PostgresStore = class {
|
|
|
1169
1179
|
client.release();
|
|
1170
1180
|
}
|
|
1171
1181
|
}
|
|
1182
|
+
/**
|
|
1183
|
+
* Wipe the sensitive-data payload for every event on the stream — the
|
|
1184
|
+
* physical-erasure side of the sensitive-data epic (#566). Sets
|
|
1185
|
+
* `events.pii` to `NULL` for the stream's events; `events.data` and
|
|
1186
|
+
* the rest of the row are never touched.
|
|
1187
|
+
*
|
|
1188
|
+
* Row-level locks (no table lock), bounded by events-per-stream.
|
|
1189
|
+
* Idempotent — a second call on an already-wiped stream returns `0`.
|
|
1190
|
+
*
|
|
1191
|
+
* Disk reclamation is autovacuum-driven; for strict-deletion
|
|
1192
|
+
* jurisdictions the production checklist documents `VACUUM FULL` as
|
|
1193
|
+
* the operator step.
|
|
1194
|
+
*
|
|
1195
|
+
* @param stream Target stream
|
|
1196
|
+
* @returns Count of events whose `pii` was set to `NULL`
|
|
1197
|
+
*/
|
|
1198
|
+
async forget_pii(stream) {
|
|
1199
|
+
const r = await this._pool.query(
|
|
1200
|
+
`UPDATE ${this._fqt} SET pii = NULL WHERE stream = $1 AND pii IS NOT NULL`,
|
|
1201
|
+
[stream]
|
|
1202
|
+
);
|
|
1203
|
+
return r.rowCount ?? 0;
|
|
1204
|
+
}
|
|
1172
1205
|
};
|
|
1173
1206
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1174
1207
|
0 && (module.exports = {
|