@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/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 notifyChannel(schema, table) {
30
+ function notify_channel(schema, table) {
31
31
  return `${NOTIFY_CHANNEL_PREFIX}_${schema}_${table}`;
32
32
  }
33
- function assertSafeIdentifier(value, label) {
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
- _listenClient;
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
- _listenHandler;
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
- assertSafeIdentifier(this.config.schema, "schema");
92
- assertSafeIdentifier(this.config.table, "table");
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 = notifyChannel(this.config.schema, this.config.table);
97
+ this._channel = notify_channel(this.config.schema, this.config.table);
98
98
  if (this.config.notify) {
99
- this.notify = this._subscribeNotifications.bind(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._teardownListen();
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 _teardownListen() {
119
- if (!this._listenClient) return;
120
- this._listenClient.removeListener("notification", this._listenHandler);
121
- this._listenHandler = void 0;
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._listenClient.query(`UNLISTEN ${this._channel}`);
123
+ await this._listen_client.query(`UNLISTEN ${this._channel}`);
124
124
  } catch {
125
125
  }
126
- this._listenClient.release(true);
127
- this._listenClient = void 0;
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.
@@ -146,9 +146,13 @@ var PostgresStore = class {
146
146
  stream varchar(100) COLLATE pg_catalog."default" NOT NULL,
147
147
  version int NOT NULL,
148
148
  created timestamptz NOT NULL DEFAULT now(),
149
- meta jsonb
149
+ meta jsonb,
150
+ pii jsonb
150
151
  ) TABLESPACE pg_default;`
151
152
  );
153
+ await client.query(
154
+ `ALTER TABLE ${this._fqt} ADD COLUMN IF NOT EXISTS pii jsonb;`
155
+ );
152
156
  await client.query(
153
157
  `CREATE UNIQUE INDEX IF NOT EXISTS "${this.config.table}_stream_ix"
154
158
  ON ${this._fqt} (stream COLLATE pg_catalog."default", version);`
@@ -339,12 +343,12 @@ var PostgresStore = class {
339
343
  expectedVersion
340
344
  );
341
345
  const committed = [];
342
- for (const { name, data } of msgs) {
346
+ for (const { name, data, pii } of msgs) {
343
347
  version++;
344
348
  const sql = `
345
- INSERT INTO ${this._fqt}(name, data, stream, version, meta)
346
- VALUES($1, $2, $3, $4, $5) RETURNING *`;
347
- const vals = [name, data, stream, version, meta];
349
+ INSERT INTO ${this._fqt}(name, data, pii, stream, version, meta)
350
+ VALUES($1, $2, $3, $4, $5, $6) RETURNING *`;
351
+ const vals = [name, data, pii ?? null, stream, version, meta];
348
352
  try {
349
353
  const { rows } = await client.query(sql, vals);
350
354
  committed.push(rows.at(0));
@@ -399,7 +403,7 @@ var PostgresStore = class {
399
403
  const client = await this._pool.connect();
400
404
  try {
401
405
  await client.query("BEGIN");
402
- const laneClause = lane !== void 0 ? `AND s.lane = $5` : "";
406
+ const lane_clause = lane !== void 0 ? `AND s.lane = $5` : "";
403
407
  const params = lane !== void 0 ? [lagging, leading, by, millis, lane] : [lagging, leading, by, millis];
404
408
  const { rows } = await client.query(
405
409
  `
@@ -408,7 +412,7 @@ var PostgresStore = class {
408
412
  SELECT stream, source, at, priority, lane
409
413
  FROM ${this._fqs} s
410
414
  WHERE blocked = false
411
- ${laneClause}
415
+ ${lane_clause}
412
416
  AND (leased_by IS NULL OR leased_until <= NOW())
413
417
  AND (s.at < 0 OR EXISTS (
414
418
  SELECT 1 FROM ${this._fqt} e
@@ -633,7 +637,7 @@ var PostgresStore = class {
633
637
  * `WHERE` — callers compose it with any other predicates they need.
634
638
  * Returns an always-true clause (`true`) when the filter is empty.
635
639
  */
636
- _filterClause(filter, start) {
640
+ _filter_clause(filter, start) {
637
641
  const conditions = [];
638
642
  const values = [];
639
643
  if (filter.stream !== void 0) {
@@ -663,19 +667,19 @@ var PostgresStore = class {
663
667
  };
664
668
  }
665
669
  async reset(input) {
666
- const setClause = `SET at = -1, retry = 0, blocked = false, error = NULL,
670
+ const set_clause = `SET at = -1, retry = 0, blocked = false, error = NULL,
667
671
  leased_by = NULL, leased_until = NULL`;
668
672
  if (Array.isArray(input)) {
669
673
  if (!input.length) return 0;
670
674
  const { rowCount: rowCount2 } = await this._pool.query(
671
- `UPDATE ${this._fqs} ${setClause} WHERE stream = ANY($1)`,
675
+ `UPDATE ${this._fqs} ${set_clause} WHERE stream = ANY($1)`,
672
676
  [input]
673
677
  );
674
678
  return rowCount2 ?? 0;
675
679
  }
676
- const { clause, values } = this._filterClause(input, 1);
680
+ const { clause, values } = this._filter_clause(input, 1);
677
681
  const { rowCount } = await this._pool.query(
678
- `UPDATE ${this._fqs} ${setClause} WHERE ${clause}`,
682
+ `UPDATE ${this._fqs} ${set_clause} WHERE ${clause}`,
679
683
  values
680
684
  );
681
685
  return rowCount ?? 0;
@@ -696,23 +700,23 @@ var PostgresStore = class {
696
700
  * @returns Count of streams that were actually flipped (were blocked).
697
701
  */
698
702
  async unblock(input) {
699
- const setClause = `SET retry = -1, blocked = false, error = NULL,
703
+ const set_clause = `SET retry = -1, blocked = false, error = NULL,
700
704
  leased_by = NULL, leased_until = NULL`;
701
705
  if (Array.isArray(input)) {
702
706
  if (!input.length) return 0;
703
707
  const { rowCount: rowCount2 } = await this._pool.query(
704
- `UPDATE ${this._fqs} ${setClause}
708
+ `UPDATE ${this._fqs} ${set_clause}
705
709
  WHERE stream = ANY($1) AND blocked = true`,
706
710
  [input]
707
711
  );
708
712
  return rowCount2 ?? 0;
709
713
  }
710
- const { clause, values } = this._filterClause(
714
+ const { clause, values } = this._filter_clause(
711
715
  { ...input, blocked: true },
712
716
  1
713
717
  );
714
718
  const { rowCount } = await this._pool.query(
715
- `UPDATE ${this._fqs} ${setClause} WHERE ${clause}`,
719
+ `UPDATE ${this._fqs} ${set_clause} WHERE ${clause}`,
716
720
  values
717
721
  );
718
722
  return rowCount ?? 0;
@@ -732,7 +736,7 @@ var PostgresStore = class {
732
736
  * @returns Count of streams whose priority changed.
733
737
  */
734
738
  async prioritize(filter, priority) {
735
- const { clause, values } = this._filterClause(filter, 2);
739
+ const { clause, values } = this._filter_clause(filter, 2);
736
740
  const sql = `UPDATE ${this._fqs} SET priority = $1
737
741
  WHERE priority <> $1 AND ${clause}`;
738
742
  const { rowCount } = await this._pool.query(sql, [priority, ...values]);
@@ -839,11 +843,11 @@ var PostgresStore = class {
839
843
  */
840
844
  async query_stats(input, options) {
841
845
  const exclude = options?.exclude ?? [];
842
- const wantTail = options?.tail ?? false;
843
- const wantCount = options?.count ?? false;
844
- const wantNames = options?.names ?? false;
846
+ const want_tail = options?.tail ?? false;
847
+ const want_count = options?.count ?? false;
848
+ const want_names = options?.names ?? false;
845
849
  const before = options?.before;
846
- const fullScan = wantCount || wantNames;
850
+ const full_scan = want_count || want_names;
847
851
  if (Array.isArray(input) && input.length === 0) {
848
852
  return /* @__PURE__ */ new Map();
849
853
  }
@@ -866,29 +870,34 @@ var PostgresStore = class {
866
870
  params.push(before);
867
871
  where.push(`e.id < $${params.length}`);
868
872
  }
869
- const fromClause = `${this._fqt} e`;
870
- const whereClause = `WHERE ${where.length ? where.join(" AND ") : "TRUE"}`;
871
- return fullScan ? this._queryStatsFullScan(
872
- fromClause,
873
- whereClause,
873
+ const from_clause = `${this._fqt} e`;
874
+ const where_clause = `WHERE ${where.length ? where.join(" AND ") : "TRUE"}`;
875
+ return full_scan ? this._query_stats_full_scan(
876
+ from_clause,
877
+ where_clause,
878
+ params,
879
+ want_tail,
880
+ want_count,
881
+ want_names
882
+ ) : this._query_stats_heads_only(
883
+ from_clause,
884
+ where_clause,
874
885
  params,
875
- wantTail,
876
- wantCount,
877
- wantNames
878
- ) : this._queryStatsHeadsOnly(fromClause, whereClause, params, wantTail);
886
+ want_tail
887
+ );
879
888
  }
880
889
  /**
881
890
  * Cheap path: index-only DISTINCT ON for the head per stream, plus an
882
891
  * optional second query (in parallel) for the tail. K rows touched
883
892
  * per query, not N events.
884
893
  */
885
- async _queryStatsHeadsOnly(fromClause, whereClause, params, wantTail) {
894
+ async _query_stats_heads_only(from_clause, where_clause, params, want_tail) {
886
895
  const cols = `e.id, e.stream, e.version, e.name, e.data, e.created, e.meta`;
887
- const headSql = `SELECT DISTINCT ON (e.stream) ${cols} FROM ${fromClause} ${whereClause} ORDER BY e.stream, e.version DESC`;
888
- const tailSql = wantTail ? `SELECT DISTINCT ON (e.stream) ${cols} FROM ${fromClause} ${whereClause} ORDER BY e.stream, e.version ASC` : null;
896
+ const head_sql = `SELECT DISTINCT ON (e.stream) ${cols} FROM ${from_clause} ${where_clause} ORDER BY e.stream, e.version DESC`;
897
+ 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
898
  const [headRes, tailRes] = await Promise.all([
890
- this._pool.query(headSql, params),
891
- tailSql ? this._pool.query(tailSql, params) : Promise.resolve(null)
899
+ this._pool.query(head_sql, params),
900
+ tail_sql ? this._pool.query(tail_sql, params) : Promise.resolve(null)
892
901
  ]);
893
902
  const out = /* @__PURE__ */ new Map();
894
903
  for (const row of headRes.rows) {
@@ -906,16 +915,16 @@ var PostgresStore = class {
906
915
  * `COUNT(*)` and `jsonb_object_agg(name, n)` map alongside the head
907
916
  * (and tail when requested). All extras share the single events scan.
908
917
  */
909
- async _queryStatsFullScan(fromClause, whereClause, params, wantTail, wantCount, wantNames) {
910
- const tailCte = wantTail ? `, tails AS (SELECT DISTINCT ON (stream) * FROM ef ORDER BY stream, version ASC)` : "";
911
- const tailJoin = wantTail ? `LEFT JOIN tails t ON t.stream = h.stream` : "";
912
- const tailCols = wantTail ? `, t.id AS t_id, t.stream AS t_stream, t.version AS t_version,
918
+ async _query_stats_full_scan(from_clause, where_clause, params, want_tail, want_count, want_names) {
919
+ const tail_cte = want_tail ? `, tails AS (SELECT DISTINCT ON (stream) * FROM ef ORDER BY stream, version ASC)` : "";
920
+ const tail_join = want_tail ? `LEFT JOIN tails t ON t.stream = h.stream` : "";
921
+ const tail_cols = want_tail ? `, t.id AS t_id, t.stream AS t_stream, t.version AS t_version,
913
922
  t.name AS t_name, t.data AS t_data, t.created AS t_created, t.meta AS t_meta` : "";
914
923
  const sql = `
915
924
  WITH ef AS (
916
925
  SELECT e.id, e.stream, e.version, e.name, e.data, e.created, e.meta
917
- FROM ${fromClause}
918
- ${whereClause}
926
+ FROM ${from_clause}
927
+ ${where_clause}
919
928
  ),
920
929
  agg AS (
921
930
  SELECT stream,
@@ -931,15 +940,15 @@ var PostgresStore = class {
931
940
  heads AS (
932
941
  SELECT DISTINCT ON (stream) * FROM ef ORDER BY stream, version DESC
933
942
  )
934
- ${tailCte}
943
+ ${tail_cte}
935
944
  SELECT
936
945
  h.id, h.stream, h.version, h.name, h.data, h.created, h.meta,
937
946
  a.cnt AS agg_count,
938
947
  a.names AS agg_names
939
- ${tailCols}
948
+ ${tail_cols}
940
949
  FROM heads h
941
950
  LEFT JOIN agg a ON a.stream = h.stream
942
- ${tailJoin}
951
+ ${tail_join}
943
952
  `;
944
953
  const res = await this._pool.query(sql, params);
945
954
  const out = /* @__PURE__ */ new Map();
@@ -955,7 +964,7 @@ var PostgresStore = class {
955
964
  meta: row.meta
956
965
  }
957
966
  };
958
- if (wantTail && row.t_id !== void 0 && row.t_id !== null) {
967
+ if (want_tail && row.t_id !== void 0 && row.t_id !== null) {
959
968
  stats.tail = {
960
969
  id: row.t_id,
961
970
  stream: row.t_stream,
@@ -966,8 +975,8 @@ var PostgresStore = class {
966
975
  meta: row.t_meta
967
976
  };
968
977
  }
969
- if (wantCount) stats.count = row.agg_count;
970
- if (wantNames) stats.names = row.agg_names;
978
+ if (want_count) stats.count = row.agg_count;
979
+ if (want_names) stats.names = row.agg_names;
971
980
  out.set(row.stream, stats);
972
981
  }
973
982
  return out;
@@ -993,10 +1002,10 @@ var PostgresStore = class {
993
1002
  * @param handler Called for each cross-process commit notification.
994
1003
  * @returns Disposer that releases the LISTEN client.
995
1004
  */
996
- async _subscribeNotifications(handler) {
997
- await this._teardownListen();
1005
+ async _subscribe_notifications(handler) {
1006
+ await this._teardown_listen();
998
1007
  const client = await this._pool.connect();
999
- const onNotification = (msg) => {
1008
+ const on_notification = (msg) => {
1000
1009
  if (msg.channel !== this._channel) return;
1001
1010
  if (!msg.payload) return;
1002
1011
  let parsed;
@@ -1033,19 +1042,19 @@ var PostgresStore = class {
1033
1042
  logger.error(err, "act_commit: handler threw, listener preserved");
1034
1043
  }
1035
1044
  };
1036
- client.on("notification", onNotification);
1045
+ client.on("notification", on_notification);
1037
1046
  try {
1038
1047
  await client.query(`LISTEN ${this._channel}`);
1039
1048
  } catch (err) {
1040
- client.removeListener("notification", onNotification);
1049
+ client.removeListener("notification", on_notification);
1041
1050
  client.release(true);
1042
1051
  throw err;
1043
1052
  }
1044
- this._listenClient = client;
1045
- this._listenHandler = onNotification;
1053
+ this._listen_client = client;
1054
+ this._listen_handler = on_notification;
1046
1055
  return async () => {
1047
- if (this._listenClient !== client) return;
1048
- await this._teardownListen();
1056
+ if (this._listen_client !== client) return;
1057
+ await this._teardown_listen();
1049
1058
  };
1050
1059
  }
1051
1060
  /**
@@ -1116,11 +1125,12 @@ var PostgresStore = class {
1116
1125
  await client.query(`TRUNCATE TABLE ${this._fqs}`);
1117
1126
  await driver(async (event) => {
1118
1127
  const { rows } = await client.query(
1119
- `INSERT INTO ${this._fqt}(name, data, stream, version, created, meta)
1120
- VALUES($1, $2, $3, $4, $5, $6) RETURNING id`,
1128
+ `INSERT INTO ${this._fqt}(name, data, pii, stream, version, created, meta)
1129
+ VALUES($1, $2, $3, $4, $5, $6, $7) RETURNING id`,
1121
1130
  [
1122
1131
  event.name,
1123
1132
  event.data,
1133
+ event.pii ?? null,
1124
1134
  event.stream,
1125
1135
  event.version,
1126
1136
  event.created,
@@ -1138,6 +1148,29 @@ var PostgresStore = class {
1138
1148
  client.release();
1139
1149
  }
1140
1150
  }
1151
+ /**
1152
+ * Wipe the sensitive-data payload for every event on the stream — the
1153
+ * physical-erasure side of the sensitive-data epic (#566). Sets
1154
+ * `events.pii` to `NULL` for the stream's events; `events.data` and
1155
+ * the rest of the row are never touched.
1156
+ *
1157
+ * Row-level locks (no table lock), bounded by events-per-stream.
1158
+ * Idempotent — a second call on an already-wiped stream returns `0`.
1159
+ *
1160
+ * Disk reclamation is autovacuum-driven; for strict-deletion
1161
+ * jurisdictions the production checklist documents `VACUUM FULL` as
1162
+ * the operator step.
1163
+ *
1164
+ * @param stream Target stream
1165
+ * @returns Count of events whose `pii` was set to `NULL`
1166
+ */
1167
+ async forget_pii(stream) {
1168
+ const r = await this._pool.query(
1169
+ `UPDATE ${this._fqt} SET pii = NULL WHERE stream = $1 AND pii IS NOT NULL`,
1170
+ [stream]
1171
+ );
1172
+ return r.rowCount ?? 0;
1173
+ }
1141
1174
  };
1142
1175
  export {
1143
1176
  PostgresStore