@rotorsoft/act-pg 1.0.0 → 1.1.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.cjs CHANGED
@@ -1125,6 +1125,73 @@ var PostgresStore = class {
1125
1125
  client.release();
1126
1126
  }
1127
1127
  }
1128
+ /**
1129
+ * Atomically rebuild the store from a stream of {@link RestoreRow}.
1130
+ *
1131
+ * Wraps the entire rebuild in a single `BEGIN`/`COMMIT` transaction
1132
+ * — on any throw the transaction rolls back and the store ends
1133
+ * byte-for-byte unchanged. `TRUNCATE ... RESTART IDENTITY CASCADE`
1134
+ * wipes events + resets the serial sequence to 1; the streams
1135
+ * table is cleared in the same statement via `CASCADE`-like
1136
+ * `DELETE`. Rows are inserted one at a time with explicit columns
1137
+ * (skipping `id`) so the serial assigns dense ids from 1.
1138
+ *
1139
+ * Causation refs in `meta.causation.event.id` are remapped through
1140
+ * the `old → new` table built as rows land. References to ids not
1141
+ * in the source pass through unchanged.
1142
+ *
1143
+ * `created` is preserved verbatim from the source.
1144
+ */
1145
+ async restore(source, _opts = {}) {
1146
+ const started = Date.now();
1147
+ const client = await this._pool.connect();
1148
+ try {
1149
+ await client.query("BEGIN");
1150
+ await client.query(
1151
+ `TRUNCATE TABLE ${this._fqt} RESTART IDENTITY CASCADE`
1152
+ );
1153
+ await client.query(`TRUNCATE TABLE ${this._fqs}`);
1154
+ const idMap = /* @__PURE__ */ new Map();
1155
+ let kept = 0;
1156
+ for await (const row of source) {
1157
+ let meta = row.meta;
1158
+ const causedBy = meta.causation.event?.id;
1159
+ if (causedBy !== void 0) {
1160
+ const remapped = idMap.get(causedBy);
1161
+ if (remapped !== void 0 && remapped !== causedBy) {
1162
+ meta = {
1163
+ ...meta,
1164
+ causation: {
1165
+ ...meta.causation,
1166
+ event: { ...meta.causation.event, id: remapped }
1167
+ }
1168
+ };
1169
+ }
1170
+ }
1171
+ const created = row.created instanceof Date ? row.created : new Date(row.created);
1172
+ const { rows } = await client.query(
1173
+ `INSERT INTO ${this._fqt}(name, data, stream, version, created, meta)
1174
+ VALUES($1, $2, $3, $4, $5, $6) RETURNING id`,
1175
+ [row.name, row.data, row.stream, row.version, created, meta]
1176
+ );
1177
+ idMap.set(row.id, rows[0].id);
1178
+ kept++;
1179
+ }
1180
+ await client.query("COMMIT");
1181
+ return {
1182
+ kept,
1183
+ duration_ms: Date.now() - started,
1184
+ dropped: { closed_streams: 0, snapshots: 0, empty_streams: 0 },
1185
+ dry_run: false
1186
+ };
1187
+ } catch (error) {
1188
+ await client.query("ROLLBACK").catch(() => {
1189
+ });
1190
+ throw error;
1191
+ } finally {
1192
+ client.release();
1193
+ }
1194
+ }
1128
1195
  };
1129
1196
  // Annotate the CommonJS export names for ESM import in node:
1130
1197
  0 && (module.exports = {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/postgres-store.ts","../src/utils.ts"],"sourcesContent":["/**\n * @packageDocumentation\n * @module act-pg\n * Main entry point for the Act-PG framework. Re-exports all core APIs\n */\nexport * from \"./postgres-store.js\";\n","import { randomUUID } from \"node:crypto\";\nimport type {\n BlockedLease,\n Committed,\n EventMeta,\n Lease,\n Logger,\n Message,\n NotifyDisposer,\n Query,\n QueryStatsOptions,\n QueryStreams,\n QueryStreamsResult,\n Schema,\n Schemas,\n Store,\n StoreNotification,\n StreamFilter,\n StreamPosition,\n StreamStats,\n} from \"@rotorsoft/act\";\nimport {\n ConcurrencyError,\n log,\n SNAP_EVENT,\n TOMBSTONE_EVENT,\n} from \"@rotorsoft/act\";\nimport pg from \"pg\";\nimport { dateReviver } from \"./utils.js\";\n\nconst logger: Logger = log();\n\nconst { Pool, types } = pg;\ntypes.setTypeParser(types.builtins.JSONB, (val) =>\n JSON.parse(val, dateReviver)\n);\n\ntype Config = Readonly<{\n schema: string;\n table: string;\n /**\n * Opt in to cross-process commit notifications via `LISTEN`/`NOTIFY`.\n * Optional — defaults to `false` so existing callers keep their\n * current behavior. Setting it to `true` is the only behavior change\n * an upgrading deployment needs to make to enable cross-process\n * reaction wakeup.\n *\n * When `true`:\n * - `commit()` issues `pg_notify` after each successful insert.\n * - `notify(handler)` checks out a dedicated long-lived `LISTEN`\n * client from the pool and delivers cross-process notifications.\n *\n * When `false` (default):\n * - `commit()` skips the notify SQL entirely — zero per-write\n * overhead.\n * - The `notify` method is **not present on the instance**, so the\n * orchestrator's `if (store.notify)` auto-wire short-circuits and\n * no LISTEN client is allocated.\n *\n * Single-instance deployments should leave this off. Multi-process\n * deployments that need sub-poll reaction latency turn it on\n * **on every store instance** (writers and listeners both).\n */\n notify?: boolean;\n}> &\n pg.PoolConfig;\n\nconst SAFE_IDENTIFIER = /^[a-zA-Z_][a-zA-Z0-9_]*$/;\n\n// PostgreSQL SQLSTATE for `unique_violation` — surfaces when a concurrent\n// commit beats us between the version SELECT and the INSERT, hitting the\n// unique index on (stream, version). Stable across PG versions per the\n// SQL standard. See: https://www.postgresql.org/docs/current/errcodes-appendix.html\nconst PG_UNIQUE_VIOLATION = \"23505\";\n\n// Channel-name prefix for cross-process commit notifications. The\n// effective channel is namespaced per `(schema, table)` so two\n// PostgresStores pointed at distinct event tables in the same database\n// don't cross-talk. PG channel names are case-folded unless quoted; we\n// stick to lowercase identifiers so a future `LISTEN act_commit_*` from\n// any client (psql, scripts, alternative consumers) matches without\n// surprises.\nconst NOTIFY_CHANNEL_PREFIX = \"act_commit\";\n\nfunction notifyChannel(schema: string, table: string): string {\n return `${NOTIFY_CHANNEL_PREFIX}_${schema}_${table}`;\n}\nfunction assertSafeIdentifier(value: string, label: string) {\n if (!SAFE_IDENTIFIER.test(value))\n throw new Error(`Unsafe SQL identifier for ${label}: \"${value}\"`);\n}\n\nconst DEFAULT_CONFIG: Config = {\n host: \"localhost\",\n port: 5432,\n database: \"postgres\",\n user: \"postgres\",\n password: \"postgres\",\n schema: \"public\",\n table: \"events\",\n notify: false,\n};\n\n/**\n * Production-ready PostgreSQL event store implementation.\n *\n * PostgresStore provides persistent, scalable event storage using PostgreSQL.\n * It implements the full {@link Store} interface with production-grade features:\n *\n * **Features:**\n * - Persistent event storage with ACID guarantees\n * - Optimistic concurrency control via version numbers\n * - Distributed stream processing with leasing\n * - Snapshot support for performance optimization\n * - Connection pooling for scalability\n * - Automatic table and index creation\n *\n * **Database Schema:**\n * - Events table: Stores all committed events\n * - Streams table: Tracks stream metadata and leases\n * - Indexes on stream, version, and timestamps for fast queries\n *\n * @example Basic setup\n * ```typescript\n * import { store } from \"@rotorsoft/act\";\n * import { PostgresStore } from \"@rotorsoft/act-pg\";\n *\n * store(new PostgresStore({\n * host: \"localhost\",\n * port: 5432,\n * database: \"myapp\",\n * user: \"postgres\",\n * password: \"secret\"\n * }));\n *\n * const app = act()\n * .withState(Counter)\n * .build();\n * ```\n *\n * @example With custom schema and table\n * ```typescript\n * import { PostgresStore } from \"@rotorsoft/act-pg\";\n *\n * const pgStore = new PostgresStore({\n * host: process.env.DB_HOST || \"localhost\",\n * port: parseInt(process.env.DB_PORT || \"5432\"),\n * database: process.env.DB_NAME || \"myapp\",\n * user: process.env.DB_USER || \"postgres\",\n * password: process.env.DB_PASSWORD,\n * schema: \"events\", // Custom schema\n * table: \"act_events\" // Custom table name\n * });\n *\n * // Initialize tables\n * await pgStore.seed();\n * ```\n *\n * @example Connection pooling configuration\n * ```typescript\n * // PostgresStore uses node-postgres (pg) connection pooling\n * // Pool is created automatically with default settings\n * // For custom pool config, use environment variables:\n * // PGHOST, PGPORT, PGDATABASE, PGUSER, PGPASSWORD\n * // PGMAXCONNECTIONS, PGIDLETIMEOUT, etc.\n *\n * const pgStore = new PostgresStore({\n * host: \"db.example.com\",\n * port: 5432,\n * database: \"production\",\n * user: \"app_user\",\n * password: process.env.DB_PASSWORD\n * });\n * ```\n *\n * @example Multi-tenant setup\n * ```typescript\n * // Use separate schemas per tenant\n * const tenants = [\"tenant1\", \"tenant2\", \"tenant3\"];\n *\n * for (const tenant of tenants) {\n * const tenantStore = new PostgresStore({\n * host: \"localhost\",\n * database: \"multitenant\",\n * schema: tenant, // Each tenant gets own schema\n * table: \"events\"\n * });\n * await tenantStore.seed();\n * }\n * ```\n *\n * @example Querying PostgreSQL directly\n * ```typescript\n * // For advanced queries, you can access pg client\n * const pgStore = new PostgresStore(config);\n * await pgStore.seed();\n *\n * // Use the store's query method for standard queries\n * await pgStore.query(\n * (event) => console.log(event),\n * { stream: \"user-123\", limit: 100 }\n * );\n * ```\n *\n * @see {@link Store} for the interface definition\n * @see {@link InMemoryStore} for development/testing\n * @see {@link store} for injecting stores\n * @see {@link https://node-postgres.com/ | node-postgres documentation}\n *\n * @category Adapters\n */\nexport class PostgresStore implements Store {\n private _pool;\n readonly config: Config;\n private _fqt: string;\n private _fqs: string;\n /**\n * Per-instance writer identifier embedded in every NOTIFY payload. The\n * `notify()` LISTEN handler skips payloads where `by === this._by`,\n * giving the `\"notified\"` lifecycle event a clean cross-process\n * semantic — local commits never echo back through this channel.\n */\n private readonly _by: string = randomUUID();\n /**\n * Effective NOTIFY channel for this store. Computed from `(schema,\n * table)` at construction so multiple stores in the same database\n * stay isolated.\n */\n private readonly _channel: string;\n /** Active LISTEN client (one per `notify()` subscription). */\n private _listenClient: pg.PoolClient | undefined;\n /**\n * Notification listener attached to the active LISTEN client. Tracked\n * separately so the re-subscribe / dispose paths can detach it before\n * destroying the client — without this, a pool that reused the\n * connection would re-fire the stale handler.\n */\n private _listenHandler: ((msg: pg.Notification) => void) | undefined;\n /**\n * Cross-process commit subscription. **Present only when\n * `config.notify === true`** — the orchestrator's auto-wire path\n * checks `if (store.notify)`, so omitting the method keeps\n * single-instance deployments free of any LISTEN/NOTIFY overhead\n * (no dedicated client, no per-commit `pg_notify`).\n *\n * @see {@link Config.notify} for the rationale and the multi-process\n * contract.\n */\n notify?: (\n handler: (notification: StoreNotification) => void\n ) => Promise<NotifyDisposer>;\n\n /**\n * Create a new PostgresStore instance.\n * @param config Partial configuration (host, port, user, password, schema, table, etc.)\n */\n constructor(config: Partial<Config> = {}) {\n this.config = { ...DEFAULT_CONFIG, ...config };\n assertSafeIdentifier(this.config.schema, \"schema\");\n assertSafeIdentifier(this.config.table, \"table\");\n const { schema: _, table: __, ...poolConfig } = this.config;\n this._pool = new Pool(poolConfig);\n this._fqt = `\"${this.config.schema}\".\"${this.config.table}\"`;\n this._fqs = `\"${this.config.schema}\".\"${this.config.table}_streams\"`;\n this._channel = notifyChannel(this.config.schema, this.config.table);\n // Attach the notify subscriber only when the user opted in. With\n // notify off, `this.notify` is `undefined`, the orchestrator skips\n // its auto-wire, and no LISTEN client is ever allocated.\n if (this.config.notify) {\n this.notify = this._subscribeNotifications.bind(this);\n }\n }\n\n /**\n * Dispose of the store and close all database connections.\n * Releases any active LISTEN client first so the pool can drain cleanly.\n * @returns Promise that resolves when all connections are closed\n */\n async dispose() {\n await this._teardownListen();\n await this._pool.end();\n }\n\n /**\n * Tear down the active LISTEN subscription if any: detach the\n * notification listener, run UNLISTEN, and destroy the dedicated\n * client (do not return it to the pool — its listener is removed but\n * destroying belt-and-braces guards against any future change in\n * pg-pool semantics that could re-issue a half-clean client).\n */\n private async _teardownListen() {\n if (!this._listenClient) return;\n // _listenHandler is set in lockstep with _listenClient in notify(),\n // so if the client is present, the handler is too.\n this._listenClient.removeListener(\"notification\", this._listenHandler!);\n this._listenHandler = undefined;\n try {\n await this._listenClient.query(`UNLISTEN ${this._channel}`);\n } catch {\n // best-effort — pool end (or destroy) tears the connection down\n }\n this._listenClient.release(true);\n this._listenClient = undefined;\n }\n\n /**\n * Seed the database with required tables, indexes, and schema for event storage.\n * @returns Promise that resolves when seeding is complete\n * @throws Error if seeding fails\n */\n async seed() {\n const client = await this._pool.connect();\n\n try {\n await client.query(\"BEGIN\");\n\n // Create schema\n await client.query(\n `CREATE SCHEMA IF NOT EXISTS \"${this.config.schema}\";`\n );\n\n // Events table\n await client.query(\n `CREATE TABLE IF NOT EXISTS ${this._fqt} (\n id serial PRIMARY KEY,\n name varchar(100) COLLATE pg_catalog.\"default\" NOT NULL,\n data jsonb,\n stream varchar(100) COLLATE pg_catalog.\"default\" NOT NULL,\n version int NOT NULL,\n created timestamptz NOT NULL DEFAULT now(),\n meta jsonb\n ) TABLESPACE pg_default;`\n );\n\n // Indexes on events\n await client.query(\n `CREATE UNIQUE INDEX IF NOT EXISTS \"${this.config.table}_stream_ix\" \n ON ${this._fqt} (stream COLLATE pg_catalog.\"default\", version);`\n );\n await client.query(\n `CREATE INDEX IF NOT EXISTS \"${this.config.table}_name_ix\" \n ON ${this._fqt} (name COLLATE pg_catalog.\"default\");`\n );\n await client.query(\n `CREATE INDEX IF NOT EXISTS \"${this.config.table}_created_id_ix\" \n ON ${this._fqt} (created, id);`\n );\n await client.query(\n `CREATE INDEX IF NOT EXISTS \"${this.config.table}_correlation_ix\" \n ON ${this._fqt} ((meta ->> 'correlation') COLLATE pg_catalog.\"default\");`\n );\n\n // Streams table\n await client.query(\n `CREATE TABLE IF NOT EXISTS ${this._fqs} (\n stream varchar(100) COLLATE pg_catalog.\"default\" PRIMARY KEY,\n source varchar(100) COLLATE pg_catalog.\"default\",\n at int NOT NULL DEFAULT -1,\n retry smallint NOT NULL DEFAULT 0,\n blocked boolean NOT NULL DEFAULT false,\n error text,\n leased_by text,\n leased_until timestamptz,\n priority int NOT NULL DEFAULT 0,\n lane text NOT NULL DEFAULT 'default'\n ) TABLESPACE pg_default;`\n );\n // Migration for tables created before priority lanes (ACT-102).\n // `ADD COLUMN IF NOT EXISTS` is a no-op when the column is\n // already present, so this is safe on every seed call.\n await client.query(\n `ALTER TABLE ${this._fqs}\n ADD COLUMN IF NOT EXISTS priority int NOT NULL DEFAULT 0;`\n );\n // Migration for tables created before drain lanes (ACT-1103).\n await client.query(\n `ALTER TABLE ${this._fqs}\n ADD COLUMN IF NOT EXISTS lane text NOT NULL DEFAULT 'default';`\n );\n\n // Composite index for `claim()` — `(blocked, priority DESC, at)`\n // matches the lagging-frontier ORDER BY exactly so the planner\n // can serve the lag CTE from the index without a sort. The\n // `_streams_fetch_ix` index is dropped because the new one\n // supersedes it (`(blocked, at)` is a prefix of the new key\n // when the planner reads `priority` as fixed).\n await client.query(\n `DROP INDEX IF EXISTS \"${this.config.schema}\".\"${this.config.table}_streams_fetch_ix\"`\n );\n await client.query(\n `CREATE INDEX IF NOT EXISTS \"${this.config.table}_streams_claim_ix\"\n ON ${this._fqs} (blocked, priority DESC, at);`\n );\n // Lane filter index (ACT-1103).\n await client.query(\n `CREATE INDEX IF NOT EXISTS \"${this.config.table}_streams_lane_ix\"\n ON ${this._fqs} (lane);`\n );\n\n await client.query(\"COMMIT\");\n logger.info(\n `Seeded schema \"${this.config.schema}\" with table \"${this.config.table}\"`\n );\n } catch (error) {\n await client.query(\"ROLLBACK\");\n logger.error(error);\n throw error;\n } finally {\n client.release();\n }\n }\n\n /**\n * Drop all tables and schema created by the store (for testing or cleanup).\n * @returns Promise that resolves when the schema is dropped\n */\n async drop() {\n await this._pool.query(\n `\n DO $$\n BEGIN\n IF EXISTS (SELECT 1 FROM information_schema.schemata\n WHERE schema_name = '${this.config.schema}'\n ) THEN\n EXECUTE 'DROP TABLE IF EXISTS ${this._fqt}';\n EXECUTE 'DROP TABLE IF EXISTS ${this._fqs}';\n IF '${this.config.schema}' <> 'public' THEN\n EXECUTE 'DROP SCHEMA \"${this.config.schema}\" CASCADE';\n END IF;\n END IF;\n END\n $$;\n `\n );\n }\n\n /**\n * Query events from the store, optionally filtered by stream, event name, time, etc.\n *\n * @param callback Function called for each event found\n * @param query (Optional) Query filter (stream, names, before, after, etc.)\n * @returns The number of events found\n *\n * @example\n * await store.query((event) => console.log(event), { stream: \"A\" });\n */\n async query<E extends Schemas>(\n callback: (event: Committed<E, keyof E>) => void,\n query?: Query\n ) {\n const {\n stream,\n names,\n before,\n after,\n limit,\n created_before,\n created_after,\n backward,\n correlation,\n with_snaps = false,\n } = query || {};\n\n let sql = `SELECT * FROM ${this._fqt}`;\n const conditions: string[] = [];\n const values: any[] = [];\n\n if (query) {\n if (typeof after !== \"undefined\") {\n values.push(after);\n conditions.push(`id>$${values.length}`);\n } else {\n conditions.push(\"id>-1\");\n }\n if (stream) {\n values.push(stream);\n conditions.push(\n query.stream_exact\n ? `stream = $${values.length}`\n : `stream ~ $${values.length}`\n );\n }\n if (names?.length) {\n values.push(names);\n conditions.push(`name = ANY($${values.length})`);\n }\n if (before) {\n values.push(before);\n conditions.push(`id<$${values.length}`);\n }\n if (created_after) {\n values.push(created_after.toISOString());\n conditions.push(`created>$${values.length}`);\n }\n if (created_before) {\n values.push(created_before.toISOString());\n conditions.push(`created<$${values.length}`);\n }\n if (correlation) {\n values.push(correlation);\n conditions.push(`meta->>'correlation'=$${values.length}`);\n }\n if (!with_snaps) {\n conditions.push(`name <> '${SNAP_EVENT}'`);\n }\n }\n if (conditions.length) {\n sql += \" WHERE \" + conditions.join(\" AND \");\n }\n sql += ` ORDER BY id ${backward ? \"DESC\" : \"ASC\"}`;\n if (limit) {\n values.push(limit);\n sql += ` LIMIT $${values.length}`;\n }\n\n const result = await this._pool.query<Committed<E, keyof E>>(sql, values);\n for (const row of result.rows) callback(row);\n\n return result.rowCount ?? 0;\n }\n\n /**\n * Commit new events to the store for a given stream, with concurrency control.\n *\n * @param stream The stream name\n * @param msgs Array of messages (event name and data)\n * @param meta Event metadata (correlation, causation, etc.)\n * @param expectedVersion (Optional) Expected stream version for concurrency control\n * @returns Array of committed events\n * @throws ConcurrencyError if the expected version does not match\n */\n async commit<E extends Schemas>(\n stream: string,\n msgs: Message<E, keyof E>[],\n meta: EventMeta,\n expectedVersion?: number\n ) {\n if (msgs.length === 0) return [];\n const client = await this._pool.connect();\n let version = -1;\n try {\n await client.query(\"BEGIN\");\n\n const last = await client.query<Committed<E, keyof E>>(\n `SELECT version\n FROM ${this._fqt}\n WHERE stream=$1 ORDER BY version DESC LIMIT 1`,\n [stream]\n );\n version = last.rowCount ? last.rows[0].version : -1;\n if (typeof expectedVersion === \"number\" && version !== expectedVersion)\n throw new ConcurrencyError(\n stream,\n version,\n msgs as unknown as Message<Schemas, string>[],\n expectedVersion\n );\n\n const committed: Committed<E, keyof E>[] = [];\n for (const { name, data } of msgs) {\n version++;\n const sql = `\n INSERT INTO ${this._fqt}(name, data, stream, version, meta)\n VALUES($1, $2, $3, $4, $5) RETURNING *`;\n const vals = [name, data, stream, version, meta];\n try {\n const { rows } = await client.query<Committed<E, keyof E>>(sql, vals);\n committed.push(rows.at(0)!);\n } catch (error) {\n // PG unique-violation on (stream, version) — a concurrent commit\n // beat us between the version SELECT and this INSERT. Surface as\n // ConcurrencyError so callers retry on the framework signal\n // instead of an adapter-specific error.\n if ((error as { code?: string })?.code === PG_UNIQUE_VIOLATION) {\n throw new ConcurrencyError(\n stream,\n version - 1,\n msgs as unknown as Message<Schemas, string>[],\n expectedVersion ?? -1\n );\n }\n throw error;\n }\n }\n\n // One NOTIFY per commit transaction, payload carries the full event\n // batch so listeners reason about atomic groups (matches reaction\n // semantics in the rest of the framework). `by` lets other\n // PostgresStore instances self-filter their own writes — see\n // `_subscribeNotifications()`. PG NOTIFY payloads cap at 8000\n // bytes; for typical commits (1–10 events) this is comfortably\n // under, and the polling fallback path handles the rare overflow\n // case correctly. Skipped entirely when `config.notify === false`\n // (the default) so single-instance deployments pay zero\n // per-write overhead.\n if (this.config.notify) {\n const payload = JSON.stringify({\n stream,\n events: committed.map((c) => ({ id: c.id, name: c.name as string })),\n by: this._by,\n });\n await client.query(`SELECT pg_notify($1, $2)`, [\n this._channel,\n payload,\n ]);\n }\n\n await client.query(\"COMMIT\");\n return committed;\n } catch (error) {\n await client.query(\"ROLLBACK\").catch(() => {});\n throw error;\n } finally {\n client.release();\n }\n }\n\n /**\n * Atomically discovers and leases streams for reaction processing.\n *\n * Uses `FOR UPDATE SKIP LOCKED` to implement zero-contention competing consumers:\n * - Workers never block each other — locked rows are silently skipped\n * - Discovery and locking happen in a single atomic transaction\n * - No wasted polls — every returned stream is exclusively owned\n *\n * @param lagging - Max streams from lagging frontier (ascending watermark)\n * @param leading - Max streams from leading frontier (descending watermark)\n * @param by - Lease holder identifier (UUID)\n * @param millis - Lease duration in milliseconds\n * @returns Leased streams with metadata\n */\n async claim(\n lagging: number,\n leading: number,\n by: string,\n millis: number,\n lane?: string\n ): Promise<Lease[]> {\n const client = await this._pool.connect();\n try {\n await client.query(\"BEGIN\");\n const laneClause = lane !== undefined ? `AND s.lane = $5` : \"\";\n const params: unknown[] =\n lane !== undefined\n ? [lagging, leading, by, millis, lane]\n : [lagging, leading, by, millis];\n const { rows } = await client.query<{\n stream: string;\n source: string | null;\n at: number;\n retry: number;\n lagging: boolean;\n lane: string;\n }>(\n `\n WITH\n available AS (\n SELECT stream, source, at, priority, lane\n FROM ${this._fqs} s\n WHERE blocked = false\n ${laneClause}\n AND (leased_by IS NULL OR leased_until <= NOW())\n AND (s.at < 0 OR EXISTS (\n SELECT 1 FROM ${this._fqt} e\n WHERE e.id > s.at\n AND e.name <> '${SNAP_EVENT}'\n AND (s.source IS NULL OR e.stream = COALESCE(s.source, s.stream))\n LIMIT 1\n ))\n FOR UPDATE SKIP LOCKED\n ),\n -- Priority lanes (ACT-102): higher priority first, then\n -- lagging-watermark order. With everyone at priority=0 the\n -- ORDER BY collapses to plain at ASC so existing workloads\n -- see no behavior change.\n lag AS (\n SELECT stream, source, at, lane, TRUE AS lagging\n FROM available\n ORDER BY priority DESC, at ASC\n LIMIT $1\n ),\n lead AS (\n SELECT stream, source, at, lane, FALSE AS lagging\n FROM available\n ORDER BY at DESC\n LIMIT $2\n ),\n combined AS (\n SELECT DISTINCT ON (stream) stream, source, at, lane, lagging\n FROM (SELECT * FROM lag UNION ALL SELECT * FROM lead) t\n ORDER BY stream, at\n )\n UPDATE ${this._fqs} s\n SET\n leased_by = $3,\n leased_until = NOW() + ($4::integer || ' milliseconds')::interval,\n retry = s.retry + 1\n FROM combined c\n WHERE s.stream = c.stream\n RETURNING s.stream, s.source, s.at, s.retry, c.lagging, s.lane\n `,\n params\n );\n await client.query(\"COMMIT\");\n\n return rows.map(({ stream, source, at, retry, lagging, lane }) => ({\n stream,\n source: source ?? undefined,\n at,\n by,\n retry,\n lagging,\n lane,\n }));\n } catch (error) {\n await client.query(\"ROLLBACK\").catch(() => {});\n logger.error(error);\n return [];\n } finally {\n client.release();\n }\n }\n\n /**\n * Registers streams for event processing.\n * Upserts stream entries so they become visible to claim().\n * Also returns the current max watermark across all subscriptions.\n * @param streams - Streams to register with optional source.\n * @returns subscribed count and current max watermark.\n */\n async subscribe(\n streams: Array<{\n stream: string;\n source?: string;\n priority?: number;\n lane?: string;\n }>\n ): Promise<{ subscribed: number; watermark: number }> {\n const client = await this._pool.connect();\n try {\n await client.query(\"BEGIN\");\n let subscribed = 0;\n if (streams.length) {\n // Three statements to keep `subscribed` meaning \"newly\n // registered streams\" (not \"rows touched\"):\n // 1. INSERT ... ON CONFLICT DO NOTHING — rowCount = inserts.\n // 2. UPDATE priority on the existing rows whose new value is\n // higher than the stored one (ACT-102: keep the max so the\n // highest-priority registered reaction wins). Operator\n // overrides (which may *decrease*) go through `prioritize()`.\n // 3. UPDATE lane unconditionally — current subscribe wins (ACT-1103).\n const { rowCount: inserted } = await client.query(\n `\n INSERT INTO ${this._fqs} (stream, source, priority, lane)\n SELECT s->>'stream',\n s->>'source',\n COALESCE((s->>'priority')::int, 0),\n COALESCE(s->>'lane', 'default')\n FROM jsonb_array_elements($1::jsonb) AS s\n ON CONFLICT (stream) DO NOTHING\n `,\n [JSON.stringify(streams)]\n );\n subscribed = inserted ?? 0;\n await client.query(\n `\n UPDATE ${this._fqs} t\n SET priority = COALESCE((s->>'priority')::int, 0)\n FROM jsonb_array_elements($1::jsonb) AS s\n WHERE t.stream = s->>'stream'\n AND COALESCE((s->>'priority')::int, 0) > t.priority\n `,\n [JSON.stringify(streams)]\n );\n await client.query(\n `\n UPDATE ${this._fqs} t\n SET lane = COALESCE(s->>'lane', 'default')\n FROM jsonb_array_elements($1::jsonb) AS s\n WHERE t.stream = s->>'stream'\n AND t.lane <> COALESCE(s->>'lane', 'default')\n `,\n [JSON.stringify(streams)]\n );\n }\n const { rows } = await client.query<{ max: number | null }>(\n `SELECT COALESCE(MAX(at), -1) AS max FROM ${this._fqs}`\n );\n await client.query(\"COMMIT\");\n return { subscribed, watermark: rows[0]?.max ?? -1 };\n } catch (error) {\n await client.query(\"ROLLBACK\").catch(() => {});\n logger.error(error);\n return { subscribed: 0, watermark: -1 };\n } finally {\n client.release();\n }\n }\n\n /**\n * Acknowledge and release leases after processing, updating stream positions.\n *\n * @param leases - Leases to acknowledge, including last processed watermark and lease holder.\n * @returns Acked leases.\n */\n async ack(leases: Lease[]): Promise<Lease[]> {\n const client = await this._pool.connect();\n try {\n await client.query(\"BEGIN\");\n const { rows } = await client.query<{\n stream: string;\n source: string | null;\n at: number;\n by: string;\n retry: number;\n lagging: boolean;\n lane: string;\n }>(\n `\n WITH input AS (\n SELECT * FROM jsonb_to_recordset($1::jsonb)\n AS x(stream text, by text, at int, lagging boolean)\n )\n UPDATE ${this._fqs} AS s\n SET\n at = i.at,\n retry = -1,\n leased_by = NULL,\n leased_until = NULL\n FROM input i\n WHERE s.stream = i.stream AND s.leased_by = i.by\n RETURNING s.stream, s.source, s.at, i.by, s.retry, i.lagging, s.lane\n `,\n [JSON.stringify(leases)]\n );\n await client.query(\"COMMIT\");\n\n return rows.map((row) => ({\n stream: row.stream,\n source: row.source ?? undefined,\n at: row.at,\n by: row.by,\n retry: row.retry,\n lagging: row.lagging,\n lane: row.lane,\n }));\n } catch (error) {\n await client.query(\"ROLLBACK\").catch(() => {});\n logger.error(error);\n return [];\n } finally {\n client.release();\n }\n }\n\n /**\n * Block a stream for processing after failing to process and reaching max retries with blocking enabled.\n * @param leases - Leases to block, including lease holder and last error message.\n * @returns Blocked leases.\n */\n async block(leases: BlockedLease[]): Promise<BlockedLease[]> {\n const client = await this._pool.connect();\n try {\n await client.query(\"BEGIN\");\n const { rows } = await client.query<{\n stream: string;\n source: string | null;\n at: number;\n by: string;\n retry: number;\n lagging: boolean;\n error: string;\n lane: string;\n }>(\n `\n WITH input AS (\n SELECT * FROM jsonb_to_recordset($1::jsonb)\n AS x(stream text, by text, error text, lagging boolean)\n )\n UPDATE ${this._fqs} AS s\n SET blocked = true, error = i.error\n FROM input i\n WHERE s.stream = i.stream AND s.leased_by = i.by AND s.blocked = false\n RETURNING s.stream, s.source, s.at, i.by, s.retry, s.error, i.lagging, s.lane\n `,\n [JSON.stringify(leases)]\n );\n await client.query(\"COMMIT\");\n\n return rows.map((row) => ({\n stream: row.stream,\n source: row.source ?? undefined,\n at: row.at,\n by: row.by,\n retry: row.retry,\n lagging: row.lagging,\n error: row.error,\n lane: row.lane,\n }));\n } catch (error) {\n await client.query(\"ROLLBACK\").catch(() => {});\n logger.error(error);\n return [];\n } finally {\n client.release();\n }\n }\n\n /**\n * Reset watermarks for the given streams to -1, clearing retry, blocked,\n * error, and lease state so they can be replayed from the beginning.\n * @param streams - Stream names to reset.\n * @returns Count of streams that were actually reset.\n */\n /**\n * Translate a {@link StreamFilter} to a `WHERE` clause fragment and\n * the corresponding parameter values. The fragment never starts with\n * `WHERE` — callers compose it with any other predicates they need.\n * Returns an always-true clause (`true`) when the filter is empty.\n */\n private _filterClause(\n filter: StreamFilter,\n start: number\n ): { clause: string; values: unknown[] } {\n const conditions: string[] = [];\n const values: unknown[] = [];\n if (filter.stream !== undefined) {\n values.push(filter.stream);\n conditions.push(\n filter.stream_exact\n ? `stream = $${start + values.length - 1}`\n : `stream ~ $${start + values.length - 1}`\n );\n }\n if (filter.source !== undefined) {\n conditions.push(`source IS NOT NULL`);\n values.push(filter.source);\n conditions.push(\n filter.source_exact\n ? `source = $${start + values.length - 1}`\n : `source ~ $${start + values.length - 1}`\n );\n }\n if (filter.blocked !== undefined) {\n values.push(filter.blocked);\n conditions.push(`blocked = $${start + values.length - 1}`);\n }\n if (filter.lane !== undefined) {\n values.push(filter.lane);\n conditions.push(`lane = $${start + values.length - 1}`);\n }\n return {\n clause: conditions.length ? conditions.join(\" AND \") : \"TRUE\",\n values,\n };\n }\n\n async reset(input: string[] | StreamFilter): Promise<number> {\n const setClause = `SET at = -1, retry = 0, blocked = false, error = NULL,\n leased_by = NULL, leased_until = NULL`;\n if (Array.isArray(input)) {\n if (!input.length) return 0;\n const { rowCount } = await this._pool.query(\n `UPDATE ${this._fqs} ${setClause} WHERE stream = ANY($1)`,\n [input]\n );\n return rowCount ?? 0;\n }\n const { clause, values } = this._filterClause(input, 1);\n const { rowCount } = await this._pool.query(\n `UPDATE ${this._fqs} ${setClause} WHERE ${clause}`,\n values\n );\n return rowCount ?? 0;\n }\n\n /**\n * Clear blocked flag (and retry / error / lease state) on streams\n * without touching the `at` watermark. `blocked = true` is always\n * applied, so the return count reflects only streams that were\n * actually flipped — already-unblocked rows, unknown streams, and\n * filter matches that aren't blocked are silently skipped.\n *\n * `retry = -1` matches the InMemoryStore convention: claim() bumps\n * retry on every acquisition, so storing -1 means the first claim\n * after unblock returns retry=0 (\"first attempt\"). Storing 0 would\n * mis-report the post-recovery attempt as a continuation of the\n * failed sequence. See {@link Store.unblock}.\n *\n * @returns Count of streams that were actually flipped (were blocked).\n */\n async unblock(input: string[] | StreamFilter): Promise<number> {\n const setClause = `SET retry = -1, blocked = false, error = NULL,\n leased_by = NULL, leased_until = NULL`;\n if (Array.isArray(input)) {\n if (!input.length) return 0;\n const { rowCount } = await this._pool.query(\n `UPDATE ${this._fqs} ${setClause}\n WHERE stream = ANY($1) AND blocked = true`,\n [input]\n );\n return rowCount ?? 0;\n }\n // Filter form: force `blocked = true` regardless of what the\n // caller passed — there is no use case for \"unblock unblocked\n // streams.\" A no-op overlay is the right shape here.\n const { clause, values } = this._filterClause(\n { ...input, blocked: true },\n 1\n );\n const { rowCount } = await this._pool.query(\n `UPDATE ${this._fqs} ${setClause} WHERE ${clause}`,\n values\n );\n return rowCount ?? 0;\n }\n\n /**\n * Bulk-update priority of streams matching `filter` (ACT-102).\n *\n * Filter semantics mirror {@link query_streams}: regex on `stream` /\n * `source` by default, exact match with the `_exact` flags,\n * `blocked` restricts to blocked or unblocked rows. Empty filter\n * (`{}`) updates every registered stream.\n *\n * Unlike {@link subscribe} (which keeps `max()` of registered\n * priorities), this sets the priority outright — operator override\n * for the build-time scheduling policy.\n *\n * @returns Count of streams whose priority changed.\n */\n async prioritize(filter: StreamFilter, priority: number): Promise<number> {\n const { clause, values } = this._filterClause(filter, 2);\n const sql = `UPDATE ${this._fqs} SET priority = $1\n WHERE priority <> $1 AND ${clause}`;\n const { rowCount } = await this._pool.query(sql, [priority, ...values]);\n return rowCount ?? 0;\n }\n\n /**\n * Streams subscription positions to a callback, ordered by stream name,\n * along with the highest event id in the store.\n *\n * Filters (`stream`, `source`, `blocked`, `after`, `limit`) are applied\n * server-side. `stream`/`source` are regex by default (`~`), or exact\n * with `*_exact: true` — same convention as {@link Store.query}.\n *\n * @returns `maxEventId` and the `count` of positions emitted.\n */\n async query_streams(\n callback: (position: StreamPosition) => void,\n query?: QueryStreams\n ): Promise<QueryStreamsResult> {\n const limit = query?.limit ?? 100;\n const conditions: string[] = [];\n const values: unknown[] = [];\n\n if (query?.stream !== undefined) {\n values.push(query.stream);\n conditions.push(\n query.stream_exact\n ? `stream = $${values.length}`\n : `stream ~ $${values.length}`\n );\n }\n if (query?.source !== undefined) {\n conditions.push(`source IS NOT NULL`);\n values.push(query.source);\n conditions.push(\n query.source_exact\n ? `source = $${values.length}`\n : `source ~ $${values.length}`\n );\n }\n if (query?.blocked !== undefined) {\n values.push(query.blocked);\n conditions.push(`blocked = $${values.length}`);\n }\n if (query?.lane !== undefined) {\n values.push(query.lane);\n conditions.push(`lane = $${values.length}`);\n }\n if (query?.after !== undefined) {\n values.push(query.after);\n conditions.push(`stream > $${values.length}`);\n }\n let sql = `SELECT stream, source, at, retry, blocked, error, leased_by, leased_until, priority, lane FROM ${this._fqs}`;\n if (conditions.length) sql += \" WHERE \" + conditions.join(\" AND \");\n values.push(limit);\n sql += ` ORDER BY stream LIMIT $${values.length}`;\n\n const client = await this._pool.connect();\n try {\n const [streamsResult, maxResult] = await Promise.all([\n client.query<{\n stream: string;\n source: string | null;\n at: number;\n retry: number;\n blocked: boolean;\n error: string | null;\n leased_by: string | null;\n leased_until: Date | null;\n priority: number;\n lane: string;\n }>(sql, values),\n client.query<{ m: number | null }>(\n `SELECT COALESCE(MAX(id), -1) AS m FROM ${this._fqt}`\n ),\n ]);\n\n let count = 0;\n for (const row of streamsResult.rows) {\n callback({\n stream: row.stream,\n source: row.source ?? undefined,\n at: row.at,\n retry: row.retry,\n blocked: row.blocked,\n error: row.error ?? \"\",\n priority: row.priority,\n leased_by: row.leased_by ?? undefined,\n leased_until: row.leased_until ?? undefined,\n lane: row.lane,\n });\n count++;\n }\n\n return { maxEventId: Number(maxResult.rows[0].m), count };\n } finally {\n client.release();\n }\n }\n\n /**\n * Per-stream aggregated stats — see {@link Store.query_stats}.\n *\n * Two code paths chosen by the requested stats:\n *\n * - **Heads-only path** (no `count`, no `names`): one or two\n * `SELECT DISTINCT ON (stream) ... ORDER BY stream, version DESC|ASC`\n * queries, executed in parallel when `tail: true`. The\n * `(stream, version)` unique index gives index-only access — K rows\n * touched per query (K = matched streams), not N (events).\n * Ordering by `version` (not `id`) is equivalent within a stream\n * (versions are monotonic per stream and events are committed\n * sequentially) and is the column actually indexed.\n *\n * - **Full-scan path** (`count` or `names` set): one CTE materializes\n * the filtered events, then `GROUP BY stream, name` →\n * `jsonb_object_agg(name, n)` for the `names` map plus per-stream\n * `COUNT(*)` for `count`. Heads (and `tails` when requested) come\n * from `DISTINCT ON` over the same CTE — they ride free on the\n * already-paid scan.\n *\n * The stream universe is derived from the events table: filter form\n * matches event-bearing streams (not subscription rows). When the\n * filter sets `source` or `blocked`, the events table is joined\n * against the streams subscription table since those concepts only\n * exist for subscribed streams.\n */\n async query_stats<E extends Schemas>(\n input: string[] | Pick<StreamFilter, \"stream\" | \"stream_exact\">,\n options?: QueryStatsOptions<E>\n ): Promise<Map<string, StreamStats<E>>> {\n const exclude = options?.exclude ?? [];\n const wantTail = options?.tail ?? false;\n const wantCount = options?.count ?? false;\n const wantNames = options?.names ?? false;\n const before = options?.before;\n const fullScan = wantCount || wantNames;\n\n // Empty array short-circuit — saves a round trip on a no-op.\n if (Array.isArray(input) && input.length === 0) {\n return new Map<string, StreamStats<E>>();\n }\n\n // Build WHERE clause + parameter list. Subscription-level filters\n // (source, blocked) are intentionally not accepted — events live in\n // the events table; subscription state in the streams table. For\n // \"stats for blocked subscriptions\" callers compose with\n // query_streams. So no JOIN here.\n const where: string[] = [];\n const params: unknown[] = [];\n\n if (Array.isArray(input)) {\n params.push(input);\n where.push(`e.stream = ANY($${params.length})`);\n } else if (input.stream !== undefined) {\n params.push(input.stream);\n where.push(\n input.stream_exact\n ? `e.stream = $${params.length}`\n : `e.stream ~ $${params.length}`\n );\n }\n if (exclude.length) {\n params.push(exclude);\n where.push(`e.name <> ALL($${params.length})`);\n }\n if (before !== undefined) {\n params.push(before);\n where.push(`e.id < $${params.length}`);\n }\n\n const fromClause = `${this._fqt} e`;\n // Always emit a WHERE clause — `WHERE TRUE` short-circuits the\n // empty-filter case without a conditional branch on the generation\n // side. PG optimizes the trivial predicate out.\n const whereClause = `WHERE ${where.length ? where.join(\" AND \") : \"TRUE\"}`;\n\n return fullScan\n ? this._queryStatsFullScan<E>(\n fromClause,\n whereClause,\n params,\n wantTail,\n wantCount,\n wantNames\n )\n : this._queryStatsHeadsOnly<E>(fromClause, whereClause, params, wantTail);\n }\n\n /**\n * Cheap path: index-only DISTINCT ON for the head per stream, plus an\n * optional second query (in parallel) for the tail. K rows touched\n * per query, not N events.\n */\n private async _queryStatsHeadsOnly<E extends Schemas>(\n fromClause: string,\n whereClause: string,\n params: unknown[],\n wantTail: boolean\n ): Promise<Map<string, StreamStats<E>>> {\n const cols = `e.id, e.stream, e.version, e.name, e.data, e.created, e.meta`;\n const headSql = `SELECT DISTINCT ON (e.stream) ${cols} FROM ${fromClause} ${whereClause} ORDER BY e.stream, e.version DESC`;\n const tailSql = wantTail\n ? `SELECT DISTINCT ON (e.stream) ${cols} FROM ${fromClause} ${whereClause} ORDER BY e.stream, e.version ASC`\n : null;\n\n const [headRes, tailRes] = await Promise.all([\n this._pool.query<Committed<E, keyof E>>(headSql, params),\n tailSql\n ? this._pool.query<Committed<E, keyof E>>(tailSql, params)\n : Promise.resolve(null),\n ]);\n\n const out = new Map<string, StreamStats<E>>();\n for (const row of headRes.rows) {\n out.set(row.stream, { head: row });\n }\n if (tailRes) {\n for (const row of tailRes.rows) {\n // Head and tail share the same WHERE, so any stream returning a\n // tail must also have returned a head — no null check needed.\n (\n out.get(row.stream) as {\n head: Committed<E, keyof E>;\n tail?: Committed<E, keyof E>;\n }\n ).tail = row;\n }\n }\n return out;\n }\n\n /**\n * Full-scan path: one CTE-based query computes the per-stream\n * `COUNT(*)` and `jsonb_object_agg(name, n)` map alongside the head\n * (and tail when requested). All extras share the single events scan.\n */\n private async _queryStatsFullScan<E extends Schemas>(\n fromClause: string,\n whereClause: string,\n params: unknown[],\n wantTail: boolean,\n wantCount: boolean,\n wantNames: boolean\n ): Promise<Map<string, StreamStats<E>>> {\n const tailCte = wantTail\n ? `, tails AS (SELECT DISTINCT ON (stream) * FROM ef ORDER BY stream, version ASC)`\n : \"\";\n const tailJoin = wantTail ? `LEFT JOIN tails t ON t.stream = h.stream` : \"\";\n const tailCols = wantTail\n ? `, t.id AS t_id, t.stream AS t_stream, t.version AS t_version,\n t.name AS t_name, t.data AS t_data, t.created AS t_created, t.meta AS t_meta`\n : \"\";\n\n const sql = `\n WITH ef AS (\n SELECT e.id, e.stream, e.version, e.name, e.data, e.created, e.meta\n FROM ${fromClause}\n ${whereClause}\n ),\n agg AS (\n SELECT stream,\n SUM(n)::int AS cnt,\n jsonb_object_agg(name, n) AS names\n FROM (\n SELECT stream, name, COUNT(*)::int AS n\n FROM ef\n GROUP BY stream, name\n ) t\n GROUP BY stream\n ),\n heads AS (\n SELECT DISTINCT ON (stream) * FROM ef ORDER BY stream, version DESC\n )\n ${tailCte}\n SELECT\n h.id, h.stream, h.version, h.name, h.data, h.created, h.meta,\n a.cnt AS agg_count,\n a.names AS agg_names\n ${tailCols}\n FROM heads h\n LEFT JOIN agg a ON a.stream = h.stream\n ${tailJoin}\n `;\n\n const res = await this._pool.query<\n Committed<E, keyof E> & {\n agg_count: number;\n agg_names: Record<string, number> | null;\n t_id?: number;\n t_stream?: string;\n t_version?: number;\n t_name?: string;\n t_data?: object;\n t_created?: Date;\n t_meta?: object;\n }\n >(sql, params);\n\n const out = new Map<string, StreamStats<E>>();\n for (const row of res.rows) {\n const stats: {\n head: Committed<E, keyof E>;\n tail?: Committed<E, keyof E>;\n count?: number;\n names?: Record<string, number>;\n } = {\n head: {\n id: row.id,\n stream: row.stream,\n version: row.version,\n name: row.name,\n data: row.data,\n created: row.created,\n meta: row.meta,\n } as Committed<E, keyof E>,\n };\n if (wantTail && row.t_id !== undefined && row.t_id !== null) {\n stats.tail = {\n id: row.t_id,\n stream: row.t_stream,\n version: row.t_version,\n name: row.t_name,\n data: row.t_data,\n created: row.t_created,\n meta: row.t_meta,\n } as unknown as Committed<E, keyof E>;\n }\n if (wantCount) stats.count = row.agg_count;\n // `agg_names` is non-null when this row exists: heads and agg are\n // both built from the same `ef` CTE, so any stream in heads has\n // at least one matching event and `jsonb_object_agg` returns an\n // object (never null) for that group.\n if (wantNames) stats.names = row.agg_names as Record<string, number>;\n out.set(row.stream, stats as StreamStats<E>);\n }\n return out;\n }\n\n /**\n * Implementation of the optional `Store.notify` hook. Bound onto\n * `this.notify` in the constructor when `config.notify === true`,\n * left detached otherwise — see {@link Config.notify}.\n *\n * Checks out a dedicated long-lived client from the pool, runs\n * `LISTEN act_commit_<schema>_<table>`, and parses each incoming\n * notification payload. The handler is invoked exactly once per\n * **remote** commit — payloads originating from this same store\n * instance (matched by the per-instance `_by` UUID) are silently\n * skipped, giving callers a clean cross-process semantic.\n *\n * Multiple subscriptions on the same store instance are not supported —\n * this method releases any prior LISTEN client before opening a new one.\n * The returned disposer cleanly UNLISTENs and releases the dedicated\n * client; pool disposal also tears the subscription down as a safety\n * net.\n *\n * @param handler Called for each cross-process commit notification.\n * @returns Disposer that releases the LISTEN client.\n */\n private async _subscribeNotifications(\n handler: (notification: StoreNotification) => void\n ): Promise<NotifyDisposer> {\n // Close any prior subscription so callers don't silently double-listen.\n await this._teardownListen();\n\n const client = await this._pool.connect();\n const onNotification = (msg: pg.Notification) => {\n // Channel filter: this client only `LISTEN`s on `this._channel`,\n // but pg-pool can in theory deliver buffered notifications when a\n // connection is reused — guard rather than trust.\n if (msg.channel !== this._channel) return;\n if (!msg.payload) return;\n let parsed: {\n stream?: unknown;\n events?: unknown;\n by?: unknown;\n };\n try {\n parsed = JSON.parse(msg.payload);\n } catch (err) {\n // A malformed payload is a bug somewhere upstream — log and skip\n // instead of tearing down the listener.\n logger.error(\n { err, payload: msg.payload },\n \"act_commit: malformed payload, skipping\"\n );\n return;\n }\n // Self-filter: skip notifications that originated from this same\n // store instance. This is what gives `notified` its cross-process\n // semantic — local commits already arm the drain via `do()`.\n if (parsed.by === this._by) return;\n if (typeof parsed.stream !== \"string\" || !Array.isArray(parsed.events)) {\n logger.error(\n { payload: msg.payload },\n \"act_commit: payload missing required fields, skipping\"\n );\n return;\n }\n const events: Array<{ id: number; name: string }> = [];\n for (const raw of parsed.events) {\n if (\n raw &&\n typeof raw === \"object\" &&\n typeof (raw as { id?: unknown }).id === \"number\" &&\n typeof (raw as { name?: unknown }).name === \"string\"\n ) {\n events.push({\n id: (raw as { id: number }).id,\n name: (raw as { name: string }).name,\n });\n }\n }\n if (events.length === 0) return;\n // Adapter-level robustness: a throwing handler must not tear\n // down the dedicated LISTEN client. The orchestrator wraps its\n // own `notified` emit + drain wakeup separately\n // (`Act._wireNotify`) — defense in depth, with each layer\n // protecting its own resources. Direct callers of\n // `store.notify(handler)` (tests, custom integrations) inherit\n // the adapter wrap.\n try {\n handler({ stream: parsed.stream, events });\n } catch (err) {\n logger.error(err, \"act_commit: handler threw, listener preserved\");\n }\n };\n client.on(\"notification\", onNotification);\n try {\n await client.query(`LISTEN ${this._channel}`);\n } catch (err) {\n client.removeListener(\"notification\", onNotification);\n client.release(true);\n throw err;\n }\n this._listenClient = client;\n this._listenHandler = onNotification;\n\n return async () => {\n // No-op when this disposer is stale (a later notify() call already\n // tore the subscription down).\n if (this._listenClient !== client) return;\n await this._teardownListen();\n };\n }\n\n /**\n * Atomically truncates streams and seeds each with a snapshot or tombstone.\n * @param targets - Streams to truncate with optional snapshot state and meta.\n * @returns Map keyed by stream name, each entry with `deleted` count and `committed` event.\n */\n async truncate(\n targets: Array<{\n stream: string;\n snapshot?: Schema;\n meta?: EventMeta;\n }>\n ): Promise<\n Map<\n string,\n { deleted: number; committed: Committed<Schemas, keyof Schemas> }\n >\n > {\n if (!targets.length) return new Map();\n const streams = targets.map((t) => t.stream);\n const client = await this._pool.connect();\n try {\n await client.query(\"BEGIN\");\n await client.query(`DELETE FROM ${this._fqs} WHERE stream = ANY($1)`, [\n streams,\n ]);\n const result = new Map<\n string,\n { deleted: number; committed: Committed<Schemas, keyof Schemas> }\n >();\n for (const { stream, snapshot, meta } of targets) {\n const { rowCount } = await client.query(\n `DELETE FROM ${this._fqt} WHERE stream = $1`,\n [stream]\n );\n const name = snapshot !== undefined ? SNAP_EVENT : TOMBSTONE_EVENT;\n const { rows } = await client.query(\n `INSERT INTO ${this._fqt}(name, data, stream, version, created, meta)\n VALUES($1, $2, $3, 0, now(), $4) RETURNING *`,\n [\n name,\n snapshot ?? {},\n stream,\n meta ?? { correlation: \"\", causation: {} },\n ]\n );\n result.set(stream, {\n deleted: rowCount ?? 0,\n committed: rows[0] as Committed<Schemas, keyof Schemas>,\n });\n }\n await client.query(\"COMMIT\");\n return result;\n } catch (error) {\n await client.query(\"ROLLBACK\").catch(() => {});\n throw error;\n } finally {\n client.release();\n }\n }\n}\n","/**\n * @module act-pg\n * Date reviver for JSON.parse to automatically convert ISO 8601 date strings to Date objects.\n *\n * Recognizes the following formats:\n * - YYYY-MM-DDTHH:MM:SS.sssZ\n * - YYYY-MM-DDTHH:MM:SS.sss+HH:MM\n * - YYYY-MM-DDTHH:MM:SS.sss-HH:MM\n *\n * @param key The key being parsed\n * @param value The value being parsed\n * @returns A Date object if the value matches ISO 8601, otherwise the original value\n *\n * @example\n * const obj = JSON.parse(jsonString, dateReviver);\n */\nconst ISO_8601 =\n /^(\\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])T([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])(\\.\\d+)?(Z|[+-][0-2][0-9]:[0-5][0-9])?$/;\nexport const dateReviver = (_key: string, value: string): string | Date => {\n if (typeof value === \"string\" && ISO_8601.test(value)) {\n return new Date(value);\n }\n return value;\n};\n"],"mappings":"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","names":["pg","lagging","lane","rowCount"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/postgres-store.ts","../src/utils.ts"],"sourcesContent":["/**\n * @packageDocumentation\n * @module act-pg\n * Main entry point for the Act-PG framework. Re-exports all core APIs\n */\nexport * from \"./postgres-store.js\";\n","import { randomUUID } from \"node:crypto\";\nimport type {\n BlockedLease,\n Committed,\n EventMeta,\n Lease,\n Logger,\n Message,\n NotifyDisposer,\n Query,\n QueryStatsOptions,\n QueryStreams,\n QueryStreamsResult,\n RestoreOptions,\n RestoreResult,\n RestoreRow,\n Schema,\n Schemas,\n Store,\n StoreNotification,\n StreamFilter,\n StreamPosition,\n StreamStats,\n} from \"@rotorsoft/act\";\nimport {\n ConcurrencyError,\n log,\n SNAP_EVENT,\n TOMBSTONE_EVENT,\n} from \"@rotorsoft/act\";\nimport pg from \"pg\";\nimport { dateReviver } from \"./utils.js\";\n\nconst logger: Logger = log();\n\nconst { Pool, types } = pg;\ntypes.setTypeParser(types.builtins.JSONB, (val) =>\n JSON.parse(val, dateReviver)\n);\n\ntype Config = Readonly<{\n schema: string;\n table: string;\n /**\n * Opt in to cross-process commit notifications via `LISTEN`/`NOTIFY`.\n * Optional — defaults to `false` so existing callers keep their\n * current behavior. Setting it to `true` is the only behavior change\n * an upgrading deployment needs to make to enable cross-process\n * reaction wakeup.\n *\n * When `true`:\n * - `commit()` issues `pg_notify` after each successful insert.\n * - `notify(handler)` checks out a dedicated long-lived `LISTEN`\n * client from the pool and delivers cross-process notifications.\n *\n * When `false` (default):\n * - `commit()` skips the notify SQL entirely — zero per-write\n * overhead.\n * - The `notify` method is **not present on the instance**, so the\n * orchestrator's `if (store.notify)` auto-wire short-circuits and\n * no LISTEN client is allocated.\n *\n * Single-instance deployments should leave this off. Multi-process\n * deployments that need sub-poll reaction latency turn it on\n * **on every store instance** (writers and listeners both).\n */\n notify?: boolean;\n}> &\n pg.PoolConfig;\n\nconst SAFE_IDENTIFIER = /^[a-zA-Z_][a-zA-Z0-9_]*$/;\n\n// PostgreSQL SQLSTATE for `unique_violation` — surfaces when a concurrent\n// commit beats us between the version SELECT and the INSERT, hitting the\n// unique index on (stream, version). Stable across PG versions per the\n// SQL standard. See: https://www.postgresql.org/docs/current/errcodes-appendix.html\nconst PG_UNIQUE_VIOLATION = \"23505\";\n\n// Channel-name prefix for cross-process commit notifications. The\n// effective channel is namespaced per `(schema, table)` so two\n// PostgresStores pointed at distinct event tables in the same database\n// don't cross-talk. PG channel names are case-folded unless quoted; we\n// stick to lowercase identifiers so a future `LISTEN act_commit_*` from\n// any client (psql, scripts, alternative consumers) matches without\n// surprises.\nconst NOTIFY_CHANNEL_PREFIX = \"act_commit\";\n\nfunction notifyChannel(schema: string, table: string): string {\n return `${NOTIFY_CHANNEL_PREFIX}_${schema}_${table}`;\n}\nfunction assertSafeIdentifier(value: string, label: string) {\n if (!SAFE_IDENTIFIER.test(value))\n throw new Error(`Unsafe SQL identifier for ${label}: \"${value}\"`);\n}\n\nconst DEFAULT_CONFIG: Config = {\n host: \"localhost\",\n port: 5432,\n database: \"postgres\",\n user: \"postgres\",\n password: \"postgres\",\n schema: \"public\",\n table: \"events\",\n notify: false,\n};\n\n/**\n * Production-ready PostgreSQL event store implementation.\n *\n * PostgresStore provides persistent, scalable event storage using PostgreSQL.\n * It implements the full {@link Store} interface with production-grade features:\n *\n * **Features:**\n * - Persistent event storage with ACID guarantees\n * - Optimistic concurrency control via version numbers\n * - Distributed stream processing with leasing\n * - Snapshot support for performance optimization\n * - Connection pooling for scalability\n * - Automatic table and index creation\n *\n * **Database Schema:**\n * - Events table: Stores all committed events\n * - Streams table: Tracks stream metadata and leases\n * - Indexes on stream, version, and timestamps for fast queries\n *\n * @example Basic setup\n * ```typescript\n * import { store } from \"@rotorsoft/act\";\n * import { PostgresStore } from \"@rotorsoft/act-pg\";\n *\n * store(new PostgresStore({\n * host: \"localhost\",\n * port: 5432,\n * database: \"myapp\",\n * user: \"postgres\",\n * password: \"secret\"\n * }));\n *\n * const app = act()\n * .withState(Counter)\n * .build();\n * ```\n *\n * @example With custom schema and table\n * ```typescript\n * import { PostgresStore } from \"@rotorsoft/act-pg\";\n *\n * const pgStore = new PostgresStore({\n * host: process.env.DB_HOST || \"localhost\",\n * port: parseInt(process.env.DB_PORT || \"5432\"),\n * database: process.env.DB_NAME || \"myapp\",\n * user: process.env.DB_USER || \"postgres\",\n * password: process.env.DB_PASSWORD,\n * schema: \"events\", // Custom schema\n * table: \"act_events\" // Custom table name\n * });\n *\n * // Initialize tables\n * await pgStore.seed();\n * ```\n *\n * @example Connection pooling configuration\n * ```typescript\n * // PostgresStore uses node-postgres (pg) connection pooling\n * // Pool is created automatically with default settings\n * // For custom pool config, use environment variables:\n * // PGHOST, PGPORT, PGDATABASE, PGUSER, PGPASSWORD\n * // PGMAXCONNECTIONS, PGIDLETIMEOUT, etc.\n *\n * const pgStore = new PostgresStore({\n * host: \"db.example.com\",\n * port: 5432,\n * database: \"production\",\n * user: \"app_user\",\n * password: process.env.DB_PASSWORD\n * });\n * ```\n *\n * @example Multi-tenant setup\n * ```typescript\n * // Use separate schemas per tenant\n * const tenants = [\"tenant1\", \"tenant2\", \"tenant3\"];\n *\n * for (const tenant of tenants) {\n * const tenantStore = new PostgresStore({\n * host: \"localhost\",\n * database: \"multitenant\",\n * schema: tenant, // Each tenant gets own schema\n * table: \"events\"\n * });\n * await tenantStore.seed();\n * }\n * ```\n *\n * @example Querying PostgreSQL directly\n * ```typescript\n * // For advanced queries, you can access pg client\n * const pgStore = new PostgresStore(config);\n * await pgStore.seed();\n *\n * // Use the store's query method for standard queries\n * await pgStore.query(\n * (event) => console.log(event),\n * { stream: \"user-123\", limit: 100 }\n * );\n * ```\n *\n * @see {@link Store} for the interface definition\n * @see {@link InMemoryStore} for development/testing\n * @see {@link store} for injecting stores\n * @see {@link https://node-postgres.com/ | node-postgres documentation}\n *\n * @category Adapters\n */\nexport class PostgresStore implements Store {\n private _pool;\n readonly config: Config;\n private _fqt: string;\n private _fqs: string;\n /**\n * Per-instance writer identifier embedded in every NOTIFY payload. The\n * `notify()` LISTEN handler skips payloads where `by === this._by`,\n * giving the `\"notified\"` lifecycle event a clean cross-process\n * semantic — local commits never echo back through this channel.\n */\n private readonly _by: string = randomUUID();\n /**\n * Effective NOTIFY channel for this store. Computed from `(schema,\n * table)` at construction so multiple stores in the same database\n * stay isolated.\n */\n private readonly _channel: string;\n /** Active LISTEN client (one per `notify()` subscription). */\n private _listenClient: pg.PoolClient | undefined;\n /**\n * Notification listener attached to the active LISTEN client. Tracked\n * separately so the re-subscribe / dispose paths can detach it before\n * destroying the client — without this, a pool that reused the\n * connection would re-fire the stale handler.\n */\n private _listenHandler: ((msg: pg.Notification) => void) | undefined;\n /**\n * Cross-process commit subscription. **Present only when\n * `config.notify === true`** — the orchestrator's auto-wire path\n * checks `if (store.notify)`, so omitting the method keeps\n * single-instance deployments free of any LISTEN/NOTIFY overhead\n * (no dedicated client, no per-commit `pg_notify`).\n *\n * @see {@link Config.notify} for the rationale and the multi-process\n * contract.\n */\n notify?: (\n handler: (notification: StoreNotification) => void\n ) => Promise<NotifyDisposer>;\n\n /**\n * Create a new PostgresStore instance.\n * @param config Partial configuration (host, port, user, password, schema, table, etc.)\n */\n constructor(config: Partial<Config> = {}) {\n this.config = { ...DEFAULT_CONFIG, ...config };\n assertSafeIdentifier(this.config.schema, \"schema\");\n assertSafeIdentifier(this.config.table, \"table\");\n const { schema: _, table: __, ...poolConfig } = this.config;\n this._pool = new Pool(poolConfig);\n this._fqt = `\"${this.config.schema}\".\"${this.config.table}\"`;\n this._fqs = `\"${this.config.schema}\".\"${this.config.table}_streams\"`;\n this._channel = notifyChannel(this.config.schema, this.config.table);\n // Attach the notify subscriber only when the user opted in. With\n // notify off, `this.notify` is `undefined`, the orchestrator skips\n // its auto-wire, and no LISTEN client is ever allocated.\n if (this.config.notify) {\n this.notify = this._subscribeNotifications.bind(this);\n }\n }\n\n /**\n * Dispose of the store and close all database connections.\n * Releases any active LISTEN client first so the pool can drain cleanly.\n * @returns Promise that resolves when all connections are closed\n */\n async dispose() {\n await this._teardownListen();\n await this._pool.end();\n }\n\n /**\n * Tear down the active LISTEN subscription if any: detach the\n * notification listener, run UNLISTEN, and destroy the dedicated\n * client (do not return it to the pool — its listener is removed but\n * destroying belt-and-braces guards against any future change in\n * pg-pool semantics that could re-issue a half-clean client).\n */\n private async _teardownListen() {\n if (!this._listenClient) return;\n // _listenHandler is set in lockstep with _listenClient in notify(),\n // so if the client is present, the handler is too.\n this._listenClient.removeListener(\"notification\", this._listenHandler!);\n this._listenHandler = undefined;\n try {\n await this._listenClient.query(`UNLISTEN ${this._channel}`);\n } catch {\n // best-effort — pool end (or destroy) tears the connection down\n }\n this._listenClient.release(true);\n this._listenClient = undefined;\n }\n\n /**\n * Seed the database with required tables, indexes, and schema for event storage.\n * @returns Promise that resolves when seeding is complete\n * @throws Error if seeding fails\n */\n async seed() {\n const client = await this._pool.connect();\n\n try {\n await client.query(\"BEGIN\");\n\n // Create schema\n await client.query(\n `CREATE SCHEMA IF NOT EXISTS \"${this.config.schema}\";`\n );\n\n // Events table\n await client.query(\n `CREATE TABLE IF NOT EXISTS ${this._fqt} (\n id serial PRIMARY KEY,\n name varchar(100) COLLATE pg_catalog.\"default\" NOT NULL,\n data jsonb,\n stream varchar(100) COLLATE pg_catalog.\"default\" NOT NULL,\n version int NOT NULL,\n created timestamptz NOT NULL DEFAULT now(),\n meta jsonb\n ) TABLESPACE pg_default;`\n );\n\n // Indexes on events\n await client.query(\n `CREATE UNIQUE INDEX IF NOT EXISTS \"${this.config.table}_stream_ix\" \n ON ${this._fqt} (stream COLLATE pg_catalog.\"default\", version);`\n );\n await client.query(\n `CREATE INDEX IF NOT EXISTS \"${this.config.table}_name_ix\" \n ON ${this._fqt} (name COLLATE pg_catalog.\"default\");`\n );\n await client.query(\n `CREATE INDEX IF NOT EXISTS \"${this.config.table}_created_id_ix\" \n ON ${this._fqt} (created, id);`\n );\n await client.query(\n `CREATE INDEX IF NOT EXISTS \"${this.config.table}_correlation_ix\" \n ON ${this._fqt} ((meta ->> 'correlation') COLLATE pg_catalog.\"default\");`\n );\n\n // Streams table\n await client.query(\n `CREATE TABLE IF NOT EXISTS ${this._fqs} (\n stream varchar(100) COLLATE pg_catalog.\"default\" PRIMARY KEY,\n source varchar(100) COLLATE pg_catalog.\"default\",\n at int NOT NULL DEFAULT -1,\n retry smallint NOT NULL DEFAULT 0,\n blocked boolean NOT NULL DEFAULT false,\n error text,\n leased_by text,\n leased_until timestamptz,\n priority int NOT NULL DEFAULT 0,\n lane text NOT NULL DEFAULT 'default'\n ) TABLESPACE pg_default;`\n );\n // Migration for tables created before priority lanes (ACT-102).\n // `ADD COLUMN IF NOT EXISTS` is a no-op when the column is\n // already present, so this is safe on every seed call.\n await client.query(\n `ALTER TABLE ${this._fqs}\n ADD COLUMN IF NOT EXISTS priority int NOT NULL DEFAULT 0;`\n );\n // Migration for tables created before drain lanes (ACT-1103).\n await client.query(\n `ALTER TABLE ${this._fqs}\n ADD COLUMN IF NOT EXISTS lane text NOT NULL DEFAULT 'default';`\n );\n\n // Composite index for `claim()` — `(blocked, priority DESC, at)`\n // matches the lagging-frontier ORDER BY exactly so the planner\n // can serve the lag CTE from the index without a sort. The\n // `_streams_fetch_ix` index is dropped because the new one\n // supersedes it (`(blocked, at)` is a prefix of the new key\n // when the planner reads `priority` as fixed).\n await client.query(\n `DROP INDEX IF EXISTS \"${this.config.schema}\".\"${this.config.table}_streams_fetch_ix\"`\n );\n await client.query(\n `CREATE INDEX IF NOT EXISTS \"${this.config.table}_streams_claim_ix\"\n ON ${this._fqs} (blocked, priority DESC, at);`\n );\n // Lane filter index (ACT-1103).\n await client.query(\n `CREATE INDEX IF NOT EXISTS \"${this.config.table}_streams_lane_ix\"\n ON ${this._fqs} (lane);`\n );\n\n await client.query(\"COMMIT\");\n logger.info(\n `Seeded schema \"${this.config.schema}\" with table \"${this.config.table}\"`\n );\n } catch (error) {\n await client.query(\"ROLLBACK\");\n logger.error(error);\n throw error;\n } finally {\n client.release();\n }\n }\n\n /**\n * Drop all tables and schema created by the store (for testing or cleanup).\n * @returns Promise that resolves when the schema is dropped\n */\n async drop() {\n await this._pool.query(\n `\n DO $$\n BEGIN\n IF EXISTS (SELECT 1 FROM information_schema.schemata\n WHERE schema_name = '${this.config.schema}'\n ) THEN\n EXECUTE 'DROP TABLE IF EXISTS ${this._fqt}';\n EXECUTE 'DROP TABLE IF EXISTS ${this._fqs}';\n IF '${this.config.schema}' <> 'public' THEN\n EXECUTE 'DROP SCHEMA \"${this.config.schema}\" CASCADE';\n END IF;\n END IF;\n END\n $$;\n `\n );\n }\n\n /**\n * Query events from the store, optionally filtered by stream, event name, time, etc.\n *\n * @param callback Function called for each event found\n * @param query (Optional) Query filter (stream, names, before, after, etc.)\n * @returns The number of events found\n *\n * @example\n * await store.query((event) => console.log(event), { stream: \"A\" });\n */\n async query<E extends Schemas>(\n callback: (event: Committed<E, keyof E>) => void,\n query?: Query\n ) {\n const {\n stream,\n names,\n before,\n after,\n limit,\n created_before,\n created_after,\n backward,\n correlation,\n with_snaps = false,\n } = query || {};\n\n let sql = `SELECT * FROM ${this._fqt}`;\n const conditions: string[] = [];\n const values: any[] = [];\n\n if (query) {\n if (typeof after !== \"undefined\") {\n values.push(after);\n conditions.push(`id>$${values.length}`);\n } else {\n conditions.push(\"id>-1\");\n }\n if (stream) {\n values.push(stream);\n conditions.push(\n query.stream_exact\n ? `stream = $${values.length}`\n : `stream ~ $${values.length}`\n );\n }\n if (names?.length) {\n values.push(names);\n conditions.push(`name = ANY($${values.length})`);\n }\n if (before) {\n values.push(before);\n conditions.push(`id<$${values.length}`);\n }\n if (created_after) {\n values.push(created_after.toISOString());\n conditions.push(`created>$${values.length}`);\n }\n if (created_before) {\n values.push(created_before.toISOString());\n conditions.push(`created<$${values.length}`);\n }\n if (correlation) {\n values.push(correlation);\n conditions.push(`meta->>'correlation'=$${values.length}`);\n }\n if (!with_snaps) {\n conditions.push(`name <> '${SNAP_EVENT}'`);\n }\n }\n if (conditions.length) {\n sql += \" WHERE \" + conditions.join(\" AND \");\n }\n sql += ` ORDER BY id ${backward ? \"DESC\" : \"ASC\"}`;\n if (limit) {\n values.push(limit);\n sql += ` LIMIT $${values.length}`;\n }\n\n const result = await this._pool.query<Committed<E, keyof E>>(sql, values);\n for (const row of result.rows) callback(row);\n\n return result.rowCount ?? 0;\n }\n\n /**\n * Commit new events to the store for a given stream, with concurrency control.\n *\n * @param stream The stream name\n * @param msgs Array of messages (event name and data)\n * @param meta Event metadata (correlation, causation, etc.)\n * @param expectedVersion (Optional) Expected stream version for concurrency control\n * @returns Array of committed events\n * @throws ConcurrencyError if the expected version does not match\n */\n async commit<E extends Schemas>(\n stream: string,\n msgs: Message<E, keyof E>[],\n meta: EventMeta,\n expectedVersion?: number\n ) {\n if (msgs.length === 0) return [];\n const client = await this._pool.connect();\n let version = -1;\n try {\n await client.query(\"BEGIN\");\n\n const last = await client.query<Committed<E, keyof E>>(\n `SELECT version\n FROM ${this._fqt}\n WHERE stream=$1 ORDER BY version DESC LIMIT 1`,\n [stream]\n );\n version = last.rowCount ? last.rows[0].version : -1;\n if (typeof expectedVersion === \"number\" && version !== expectedVersion)\n throw new ConcurrencyError(\n stream,\n version,\n msgs as unknown as Message<Schemas, string>[],\n expectedVersion\n );\n\n const committed: Committed<E, keyof E>[] = [];\n for (const { name, data } of msgs) {\n version++;\n const sql = `\n INSERT INTO ${this._fqt}(name, data, stream, version, meta)\n VALUES($1, $2, $3, $4, $5) RETURNING *`;\n const vals = [name, data, stream, version, meta];\n try {\n const { rows } = await client.query<Committed<E, keyof E>>(sql, vals);\n committed.push(rows.at(0)!);\n } catch (error) {\n // PG unique-violation on (stream, version) — a concurrent commit\n // beat us between the version SELECT and this INSERT. Surface as\n // ConcurrencyError so callers retry on the framework signal\n // instead of an adapter-specific error.\n if ((error as { code?: string })?.code === PG_UNIQUE_VIOLATION) {\n throw new ConcurrencyError(\n stream,\n version - 1,\n msgs as unknown as Message<Schemas, string>[],\n expectedVersion ?? -1\n );\n }\n throw error;\n }\n }\n\n // One NOTIFY per commit transaction, payload carries the full event\n // batch so listeners reason about atomic groups (matches reaction\n // semantics in the rest of the framework). `by` lets other\n // PostgresStore instances self-filter their own writes — see\n // `_subscribeNotifications()`. PG NOTIFY payloads cap at 8000\n // bytes; for typical commits (1–10 events) this is comfortably\n // under, and the polling fallback path handles the rare overflow\n // case correctly. Skipped entirely when `config.notify === false`\n // (the default) so single-instance deployments pay zero\n // per-write overhead.\n if (this.config.notify) {\n const payload = JSON.stringify({\n stream,\n events: committed.map((c) => ({ id: c.id, name: c.name as string })),\n by: this._by,\n });\n await client.query(`SELECT pg_notify($1, $2)`, [\n this._channel,\n payload,\n ]);\n }\n\n await client.query(\"COMMIT\");\n return committed;\n } catch (error) {\n await client.query(\"ROLLBACK\").catch(() => {});\n throw error;\n } finally {\n client.release();\n }\n }\n\n /**\n * Atomically discovers and leases streams for reaction processing.\n *\n * Uses `FOR UPDATE SKIP LOCKED` to implement zero-contention competing consumers:\n * - Workers never block each other — locked rows are silently skipped\n * - Discovery and locking happen in a single atomic transaction\n * - No wasted polls — every returned stream is exclusively owned\n *\n * @param lagging - Max streams from lagging frontier (ascending watermark)\n * @param leading - Max streams from leading frontier (descending watermark)\n * @param by - Lease holder identifier (UUID)\n * @param millis - Lease duration in milliseconds\n * @returns Leased streams with metadata\n */\n async claim(\n lagging: number,\n leading: number,\n by: string,\n millis: number,\n lane?: string\n ): Promise<Lease[]> {\n const client = await this._pool.connect();\n try {\n await client.query(\"BEGIN\");\n const laneClause = lane !== undefined ? `AND s.lane = $5` : \"\";\n const params: unknown[] =\n lane !== undefined\n ? [lagging, leading, by, millis, lane]\n : [lagging, leading, by, millis];\n const { rows } = await client.query<{\n stream: string;\n source: string | null;\n at: number;\n retry: number;\n lagging: boolean;\n lane: string;\n }>(\n `\n WITH\n available AS (\n SELECT stream, source, at, priority, lane\n FROM ${this._fqs} s\n WHERE blocked = false\n ${laneClause}\n AND (leased_by IS NULL OR leased_until <= NOW())\n AND (s.at < 0 OR EXISTS (\n SELECT 1 FROM ${this._fqt} e\n WHERE e.id > s.at\n AND e.name <> '${SNAP_EVENT}'\n AND (s.source IS NULL OR e.stream = COALESCE(s.source, s.stream))\n LIMIT 1\n ))\n FOR UPDATE SKIP LOCKED\n ),\n -- Priority lanes (ACT-102): higher priority first, then\n -- lagging-watermark order. With everyone at priority=0 the\n -- ORDER BY collapses to plain at ASC so existing workloads\n -- see no behavior change.\n lag AS (\n SELECT stream, source, at, lane, TRUE AS lagging\n FROM available\n ORDER BY priority DESC, at ASC\n LIMIT $1\n ),\n lead AS (\n SELECT stream, source, at, lane, FALSE AS lagging\n FROM available\n ORDER BY at DESC\n LIMIT $2\n ),\n combined AS (\n SELECT DISTINCT ON (stream) stream, source, at, lane, lagging\n FROM (SELECT * FROM lag UNION ALL SELECT * FROM lead) t\n ORDER BY stream, at\n )\n UPDATE ${this._fqs} s\n SET\n leased_by = $3,\n leased_until = NOW() + ($4::integer || ' milliseconds')::interval,\n retry = s.retry + 1\n FROM combined c\n WHERE s.stream = c.stream\n RETURNING s.stream, s.source, s.at, s.retry, c.lagging, s.lane\n `,\n params\n );\n await client.query(\"COMMIT\");\n\n return rows.map(({ stream, source, at, retry, lagging, lane }) => ({\n stream,\n source: source ?? undefined,\n at,\n by,\n retry,\n lagging,\n lane,\n }));\n } catch (error) {\n await client.query(\"ROLLBACK\").catch(() => {});\n logger.error(error);\n return [];\n } finally {\n client.release();\n }\n }\n\n /**\n * Registers streams for event processing.\n * Upserts stream entries so they become visible to claim().\n * Also returns the current max watermark across all subscriptions.\n * @param streams - Streams to register with optional source.\n * @returns subscribed count and current max watermark.\n */\n async subscribe(\n streams: Array<{\n stream: string;\n source?: string;\n priority?: number;\n lane?: string;\n }>\n ): Promise<{ subscribed: number; watermark: number }> {\n const client = await this._pool.connect();\n try {\n await client.query(\"BEGIN\");\n let subscribed = 0;\n if (streams.length) {\n // Three statements to keep `subscribed` meaning \"newly\n // registered streams\" (not \"rows touched\"):\n // 1. INSERT ... ON CONFLICT DO NOTHING — rowCount = inserts.\n // 2. UPDATE priority on the existing rows whose new value is\n // higher than the stored one (ACT-102: keep the max so the\n // highest-priority registered reaction wins). Operator\n // overrides (which may *decrease*) go through `prioritize()`.\n // 3. UPDATE lane unconditionally — current subscribe wins (ACT-1103).\n const { rowCount: inserted } = await client.query(\n `\n INSERT INTO ${this._fqs} (stream, source, priority, lane)\n SELECT s->>'stream',\n s->>'source',\n COALESCE((s->>'priority')::int, 0),\n COALESCE(s->>'lane', 'default')\n FROM jsonb_array_elements($1::jsonb) AS s\n ON CONFLICT (stream) DO NOTHING\n `,\n [JSON.stringify(streams)]\n );\n subscribed = inserted ?? 0;\n await client.query(\n `\n UPDATE ${this._fqs} t\n SET priority = COALESCE((s->>'priority')::int, 0)\n FROM jsonb_array_elements($1::jsonb) AS s\n WHERE t.stream = s->>'stream'\n AND COALESCE((s->>'priority')::int, 0) > t.priority\n `,\n [JSON.stringify(streams)]\n );\n await client.query(\n `\n UPDATE ${this._fqs} t\n SET lane = COALESCE(s->>'lane', 'default')\n FROM jsonb_array_elements($1::jsonb) AS s\n WHERE t.stream = s->>'stream'\n AND t.lane <> COALESCE(s->>'lane', 'default')\n `,\n [JSON.stringify(streams)]\n );\n }\n const { rows } = await client.query<{ max: number | null }>(\n `SELECT COALESCE(MAX(at), -1) AS max FROM ${this._fqs}`\n );\n await client.query(\"COMMIT\");\n return { subscribed, watermark: rows[0]?.max ?? -1 };\n } catch (error) {\n await client.query(\"ROLLBACK\").catch(() => {});\n logger.error(error);\n return { subscribed: 0, watermark: -1 };\n } finally {\n client.release();\n }\n }\n\n /**\n * Acknowledge and release leases after processing, updating stream positions.\n *\n * @param leases - Leases to acknowledge, including last processed watermark and lease holder.\n * @returns Acked leases.\n */\n async ack(leases: Lease[]): Promise<Lease[]> {\n const client = await this._pool.connect();\n try {\n await client.query(\"BEGIN\");\n const { rows } = await client.query<{\n stream: string;\n source: string | null;\n at: number;\n by: string;\n retry: number;\n lagging: boolean;\n lane: string;\n }>(\n `\n WITH input AS (\n SELECT * FROM jsonb_to_recordset($1::jsonb)\n AS x(stream text, by text, at int, lagging boolean)\n )\n UPDATE ${this._fqs} AS s\n SET\n at = i.at,\n retry = -1,\n leased_by = NULL,\n leased_until = NULL\n FROM input i\n WHERE s.stream = i.stream AND s.leased_by = i.by\n RETURNING s.stream, s.source, s.at, i.by, s.retry, i.lagging, s.lane\n `,\n [JSON.stringify(leases)]\n );\n await client.query(\"COMMIT\");\n\n return rows.map((row) => ({\n stream: row.stream,\n source: row.source ?? undefined,\n at: row.at,\n by: row.by,\n retry: row.retry,\n lagging: row.lagging,\n lane: row.lane,\n }));\n } catch (error) {\n await client.query(\"ROLLBACK\").catch(() => {});\n logger.error(error);\n return [];\n } finally {\n client.release();\n }\n }\n\n /**\n * Block a stream for processing after failing to process and reaching max retries with blocking enabled.\n * @param leases - Leases to block, including lease holder and last error message.\n * @returns Blocked leases.\n */\n async block(leases: BlockedLease[]): Promise<BlockedLease[]> {\n const client = await this._pool.connect();\n try {\n await client.query(\"BEGIN\");\n const { rows } = await client.query<{\n stream: string;\n source: string | null;\n at: number;\n by: string;\n retry: number;\n lagging: boolean;\n error: string;\n lane: string;\n }>(\n `\n WITH input AS (\n SELECT * FROM jsonb_to_recordset($1::jsonb)\n AS x(stream text, by text, error text, lagging boolean)\n )\n UPDATE ${this._fqs} AS s\n SET blocked = true, error = i.error\n FROM input i\n WHERE s.stream = i.stream AND s.leased_by = i.by AND s.blocked = false\n RETURNING s.stream, s.source, s.at, i.by, s.retry, s.error, i.lagging, s.lane\n `,\n [JSON.stringify(leases)]\n );\n await client.query(\"COMMIT\");\n\n return rows.map((row) => ({\n stream: row.stream,\n source: row.source ?? undefined,\n at: row.at,\n by: row.by,\n retry: row.retry,\n lagging: row.lagging,\n error: row.error,\n lane: row.lane,\n }));\n } catch (error) {\n await client.query(\"ROLLBACK\").catch(() => {});\n logger.error(error);\n return [];\n } finally {\n client.release();\n }\n }\n\n /**\n * Reset watermarks for the given streams to -1, clearing retry, blocked,\n * error, and lease state so they can be replayed from the beginning.\n * @param streams - Stream names to reset.\n * @returns Count of streams that were actually reset.\n */\n /**\n * Translate a {@link StreamFilter} to a `WHERE` clause fragment and\n * the corresponding parameter values. The fragment never starts with\n * `WHERE` — callers compose it with any other predicates they need.\n * Returns an always-true clause (`true`) when the filter is empty.\n */\n private _filterClause(\n filter: StreamFilter,\n start: number\n ): { clause: string; values: unknown[] } {\n const conditions: string[] = [];\n const values: unknown[] = [];\n if (filter.stream !== undefined) {\n values.push(filter.stream);\n conditions.push(\n filter.stream_exact\n ? `stream = $${start + values.length - 1}`\n : `stream ~ $${start + values.length - 1}`\n );\n }\n if (filter.source !== undefined) {\n conditions.push(`source IS NOT NULL`);\n values.push(filter.source);\n conditions.push(\n filter.source_exact\n ? `source = $${start + values.length - 1}`\n : `source ~ $${start + values.length - 1}`\n );\n }\n if (filter.blocked !== undefined) {\n values.push(filter.blocked);\n conditions.push(`blocked = $${start + values.length - 1}`);\n }\n if (filter.lane !== undefined) {\n values.push(filter.lane);\n conditions.push(`lane = $${start + values.length - 1}`);\n }\n return {\n clause: conditions.length ? conditions.join(\" AND \") : \"TRUE\",\n values,\n };\n }\n\n async reset(input: string[] | StreamFilter): Promise<number> {\n const setClause = `SET at = -1, retry = 0, blocked = false, error = NULL,\n leased_by = NULL, leased_until = NULL`;\n if (Array.isArray(input)) {\n if (!input.length) return 0;\n const { rowCount } = await this._pool.query(\n `UPDATE ${this._fqs} ${setClause} WHERE stream = ANY($1)`,\n [input]\n );\n return rowCount ?? 0;\n }\n const { clause, values } = this._filterClause(input, 1);\n const { rowCount } = await this._pool.query(\n `UPDATE ${this._fqs} ${setClause} WHERE ${clause}`,\n values\n );\n return rowCount ?? 0;\n }\n\n /**\n * Clear blocked flag (and retry / error / lease state) on streams\n * without touching the `at` watermark. `blocked = true` is always\n * applied, so the return count reflects only streams that were\n * actually flipped — already-unblocked rows, unknown streams, and\n * filter matches that aren't blocked are silently skipped.\n *\n * `retry = -1` matches the InMemoryStore convention: claim() bumps\n * retry on every acquisition, so storing -1 means the first claim\n * after unblock returns retry=0 (\"first attempt\"). Storing 0 would\n * mis-report the post-recovery attempt as a continuation of the\n * failed sequence. See {@link Store.unblock}.\n *\n * @returns Count of streams that were actually flipped (were blocked).\n */\n async unblock(input: string[] | StreamFilter): Promise<number> {\n const setClause = `SET retry = -1, blocked = false, error = NULL,\n leased_by = NULL, leased_until = NULL`;\n if (Array.isArray(input)) {\n if (!input.length) return 0;\n const { rowCount } = await this._pool.query(\n `UPDATE ${this._fqs} ${setClause}\n WHERE stream = ANY($1) AND blocked = true`,\n [input]\n );\n return rowCount ?? 0;\n }\n // Filter form: force `blocked = true` regardless of what the\n // caller passed — there is no use case for \"unblock unblocked\n // streams.\" A no-op overlay is the right shape here.\n const { clause, values } = this._filterClause(\n { ...input, blocked: true },\n 1\n );\n const { rowCount } = await this._pool.query(\n `UPDATE ${this._fqs} ${setClause} WHERE ${clause}`,\n values\n );\n return rowCount ?? 0;\n }\n\n /**\n * Bulk-update priority of streams matching `filter` (ACT-102).\n *\n * Filter semantics mirror {@link query_streams}: regex on `stream` /\n * `source` by default, exact match with the `_exact` flags,\n * `blocked` restricts to blocked or unblocked rows. Empty filter\n * (`{}`) updates every registered stream.\n *\n * Unlike {@link subscribe} (which keeps `max()` of registered\n * priorities), this sets the priority outright — operator override\n * for the build-time scheduling policy.\n *\n * @returns Count of streams whose priority changed.\n */\n async prioritize(filter: StreamFilter, priority: number): Promise<number> {\n const { clause, values } = this._filterClause(filter, 2);\n const sql = `UPDATE ${this._fqs} SET priority = $1\n WHERE priority <> $1 AND ${clause}`;\n const { rowCount } = await this._pool.query(sql, [priority, ...values]);\n return rowCount ?? 0;\n }\n\n /**\n * Streams subscription positions to a callback, ordered by stream name,\n * along with the highest event id in the store.\n *\n * Filters (`stream`, `source`, `blocked`, `after`, `limit`) are applied\n * server-side. `stream`/`source` are regex by default (`~`), or exact\n * with `*_exact: true` — same convention as {@link Store.query}.\n *\n * @returns `maxEventId` and the `count` of positions emitted.\n */\n async query_streams(\n callback: (position: StreamPosition) => void,\n query?: QueryStreams\n ): Promise<QueryStreamsResult> {\n const limit = query?.limit ?? 100;\n const conditions: string[] = [];\n const values: unknown[] = [];\n\n if (query?.stream !== undefined) {\n values.push(query.stream);\n conditions.push(\n query.stream_exact\n ? `stream = $${values.length}`\n : `stream ~ $${values.length}`\n );\n }\n if (query?.source !== undefined) {\n conditions.push(`source IS NOT NULL`);\n values.push(query.source);\n conditions.push(\n query.source_exact\n ? `source = $${values.length}`\n : `source ~ $${values.length}`\n );\n }\n if (query?.blocked !== undefined) {\n values.push(query.blocked);\n conditions.push(`blocked = $${values.length}`);\n }\n if (query?.lane !== undefined) {\n values.push(query.lane);\n conditions.push(`lane = $${values.length}`);\n }\n if (query?.after !== undefined) {\n values.push(query.after);\n conditions.push(`stream > $${values.length}`);\n }\n let sql = `SELECT stream, source, at, retry, blocked, error, leased_by, leased_until, priority, lane FROM ${this._fqs}`;\n if (conditions.length) sql += \" WHERE \" + conditions.join(\" AND \");\n values.push(limit);\n sql += ` ORDER BY stream LIMIT $${values.length}`;\n\n const client = await this._pool.connect();\n try {\n const [streamsResult, maxResult] = await Promise.all([\n client.query<{\n stream: string;\n source: string | null;\n at: number;\n retry: number;\n blocked: boolean;\n error: string | null;\n leased_by: string | null;\n leased_until: Date | null;\n priority: number;\n lane: string;\n }>(sql, values),\n client.query<{ m: number | null }>(\n `SELECT COALESCE(MAX(id), -1) AS m FROM ${this._fqt}`\n ),\n ]);\n\n let count = 0;\n for (const row of streamsResult.rows) {\n callback({\n stream: row.stream,\n source: row.source ?? undefined,\n at: row.at,\n retry: row.retry,\n blocked: row.blocked,\n error: row.error ?? \"\",\n priority: row.priority,\n leased_by: row.leased_by ?? undefined,\n leased_until: row.leased_until ?? undefined,\n lane: row.lane,\n });\n count++;\n }\n\n return { maxEventId: Number(maxResult.rows[0].m), count };\n } finally {\n client.release();\n }\n }\n\n /**\n * Per-stream aggregated stats — see {@link Store.query_stats}.\n *\n * Two code paths chosen by the requested stats:\n *\n * - **Heads-only path** (no `count`, no `names`): one or two\n * `SELECT DISTINCT ON (stream) ... ORDER BY stream, version DESC|ASC`\n * queries, executed in parallel when `tail: true`. The\n * `(stream, version)` unique index gives index-only access — K rows\n * touched per query (K = matched streams), not N (events).\n * Ordering by `version` (not `id`) is equivalent within a stream\n * (versions are monotonic per stream and events are committed\n * sequentially) and is the column actually indexed.\n *\n * - **Full-scan path** (`count` or `names` set): one CTE materializes\n * the filtered events, then `GROUP BY stream, name` →\n * `jsonb_object_agg(name, n)` for the `names` map plus per-stream\n * `COUNT(*)` for `count`. Heads (and `tails` when requested) come\n * from `DISTINCT ON` over the same CTE — they ride free on the\n * already-paid scan.\n *\n * The stream universe is derived from the events table: filter form\n * matches event-bearing streams (not subscription rows). When the\n * filter sets `source` or `blocked`, the events table is joined\n * against the streams subscription table since those concepts only\n * exist for subscribed streams.\n */\n async query_stats<E extends Schemas>(\n input: string[] | Pick<StreamFilter, \"stream\" | \"stream_exact\">,\n options?: QueryStatsOptions<E>\n ): Promise<Map<string, StreamStats<E>>> {\n const exclude = options?.exclude ?? [];\n const wantTail = options?.tail ?? false;\n const wantCount = options?.count ?? false;\n const wantNames = options?.names ?? false;\n const before = options?.before;\n const fullScan = wantCount || wantNames;\n\n // Empty array short-circuit — saves a round trip on a no-op.\n if (Array.isArray(input) && input.length === 0) {\n return new Map<string, StreamStats<E>>();\n }\n\n // Build WHERE clause + parameter list. Subscription-level filters\n // (source, blocked) are intentionally not accepted — events live in\n // the events table; subscription state in the streams table. For\n // \"stats for blocked subscriptions\" callers compose with\n // query_streams. So no JOIN here.\n const where: string[] = [];\n const params: unknown[] = [];\n\n if (Array.isArray(input)) {\n params.push(input);\n where.push(`e.stream = ANY($${params.length})`);\n } else if (input.stream !== undefined) {\n params.push(input.stream);\n where.push(\n input.stream_exact\n ? `e.stream = $${params.length}`\n : `e.stream ~ $${params.length}`\n );\n }\n if (exclude.length) {\n params.push(exclude);\n where.push(`e.name <> ALL($${params.length})`);\n }\n if (before !== undefined) {\n params.push(before);\n where.push(`e.id < $${params.length}`);\n }\n\n const fromClause = `${this._fqt} e`;\n // Always emit a WHERE clause — `WHERE TRUE` short-circuits the\n // empty-filter case without a conditional branch on the generation\n // side. PG optimizes the trivial predicate out.\n const whereClause = `WHERE ${where.length ? where.join(\" AND \") : \"TRUE\"}`;\n\n return fullScan\n ? this._queryStatsFullScan<E>(\n fromClause,\n whereClause,\n params,\n wantTail,\n wantCount,\n wantNames\n )\n : this._queryStatsHeadsOnly<E>(fromClause, whereClause, params, wantTail);\n }\n\n /**\n * Cheap path: index-only DISTINCT ON for the head per stream, plus an\n * optional second query (in parallel) for the tail. K rows touched\n * per query, not N events.\n */\n private async _queryStatsHeadsOnly<E extends Schemas>(\n fromClause: string,\n whereClause: string,\n params: unknown[],\n wantTail: boolean\n ): Promise<Map<string, StreamStats<E>>> {\n const cols = `e.id, e.stream, e.version, e.name, e.data, e.created, e.meta`;\n const headSql = `SELECT DISTINCT ON (e.stream) ${cols} FROM ${fromClause} ${whereClause} ORDER BY e.stream, e.version DESC`;\n const tailSql = wantTail\n ? `SELECT DISTINCT ON (e.stream) ${cols} FROM ${fromClause} ${whereClause} ORDER BY e.stream, e.version ASC`\n : null;\n\n const [headRes, tailRes] = await Promise.all([\n this._pool.query<Committed<E, keyof E>>(headSql, params),\n tailSql\n ? this._pool.query<Committed<E, keyof E>>(tailSql, params)\n : Promise.resolve(null),\n ]);\n\n const out = new Map<string, StreamStats<E>>();\n for (const row of headRes.rows) {\n out.set(row.stream, { head: row });\n }\n if (tailRes) {\n for (const row of tailRes.rows) {\n // Head and tail share the same WHERE, so any stream returning a\n // tail must also have returned a head — no null check needed.\n (\n out.get(row.stream) as {\n head: Committed<E, keyof E>;\n tail?: Committed<E, keyof E>;\n }\n ).tail = row;\n }\n }\n return out;\n }\n\n /**\n * Full-scan path: one CTE-based query computes the per-stream\n * `COUNT(*)` and `jsonb_object_agg(name, n)` map alongside the head\n * (and tail when requested). All extras share the single events scan.\n */\n private async _queryStatsFullScan<E extends Schemas>(\n fromClause: string,\n whereClause: string,\n params: unknown[],\n wantTail: boolean,\n wantCount: boolean,\n wantNames: boolean\n ): Promise<Map<string, StreamStats<E>>> {\n const tailCte = wantTail\n ? `, tails AS (SELECT DISTINCT ON (stream) * FROM ef ORDER BY stream, version ASC)`\n : \"\";\n const tailJoin = wantTail ? `LEFT JOIN tails t ON t.stream = h.stream` : \"\";\n const tailCols = wantTail\n ? `, t.id AS t_id, t.stream AS t_stream, t.version AS t_version,\n t.name AS t_name, t.data AS t_data, t.created AS t_created, t.meta AS t_meta`\n : \"\";\n\n const sql = `\n WITH ef AS (\n SELECT e.id, e.stream, e.version, e.name, e.data, e.created, e.meta\n FROM ${fromClause}\n ${whereClause}\n ),\n agg AS (\n SELECT stream,\n SUM(n)::int AS cnt,\n jsonb_object_agg(name, n) AS names\n FROM (\n SELECT stream, name, COUNT(*)::int AS n\n FROM ef\n GROUP BY stream, name\n ) t\n GROUP BY stream\n ),\n heads AS (\n SELECT DISTINCT ON (stream) * FROM ef ORDER BY stream, version DESC\n )\n ${tailCte}\n SELECT\n h.id, h.stream, h.version, h.name, h.data, h.created, h.meta,\n a.cnt AS agg_count,\n a.names AS agg_names\n ${tailCols}\n FROM heads h\n LEFT JOIN agg a ON a.stream = h.stream\n ${tailJoin}\n `;\n\n const res = await this._pool.query<\n Committed<E, keyof E> & {\n agg_count: number;\n agg_names: Record<string, number> | null;\n t_id?: number;\n t_stream?: string;\n t_version?: number;\n t_name?: string;\n t_data?: object;\n t_created?: Date;\n t_meta?: object;\n }\n >(sql, params);\n\n const out = new Map<string, StreamStats<E>>();\n for (const row of res.rows) {\n const stats: {\n head: Committed<E, keyof E>;\n tail?: Committed<E, keyof E>;\n count?: number;\n names?: Record<string, number>;\n } = {\n head: {\n id: row.id,\n stream: row.stream,\n version: row.version,\n name: row.name,\n data: row.data,\n created: row.created,\n meta: row.meta,\n } as Committed<E, keyof E>,\n };\n if (wantTail && row.t_id !== undefined && row.t_id !== null) {\n stats.tail = {\n id: row.t_id,\n stream: row.t_stream,\n version: row.t_version,\n name: row.t_name,\n data: row.t_data,\n created: row.t_created,\n meta: row.t_meta,\n } as unknown as Committed<E, keyof E>;\n }\n if (wantCount) stats.count = row.agg_count;\n // `agg_names` is non-null when this row exists: heads and agg are\n // both built from the same `ef` CTE, so any stream in heads has\n // at least one matching event and `jsonb_object_agg` returns an\n // object (never null) for that group.\n if (wantNames) stats.names = row.agg_names as Record<string, number>;\n out.set(row.stream, stats as StreamStats<E>);\n }\n return out;\n }\n\n /**\n * Implementation of the optional `Store.notify` hook. Bound onto\n * `this.notify` in the constructor when `config.notify === true`,\n * left detached otherwise — see {@link Config.notify}.\n *\n * Checks out a dedicated long-lived client from the pool, runs\n * `LISTEN act_commit_<schema>_<table>`, and parses each incoming\n * notification payload. The handler is invoked exactly once per\n * **remote** commit — payloads originating from this same store\n * instance (matched by the per-instance `_by` UUID) are silently\n * skipped, giving callers a clean cross-process semantic.\n *\n * Multiple subscriptions on the same store instance are not supported —\n * this method releases any prior LISTEN client before opening a new one.\n * The returned disposer cleanly UNLISTENs and releases the dedicated\n * client; pool disposal also tears the subscription down as a safety\n * net.\n *\n * @param handler Called for each cross-process commit notification.\n * @returns Disposer that releases the LISTEN client.\n */\n private async _subscribeNotifications(\n handler: (notification: StoreNotification) => void\n ): Promise<NotifyDisposer> {\n // Close any prior subscription so callers don't silently double-listen.\n await this._teardownListen();\n\n const client = await this._pool.connect();\n const onNotification = (msg: pg.Notification) => {\n // Channel filter: this client only `LISTEN`s on `this._channel`,\n // but pg-pool can in theory deliver buffered notifications when a\n // connection is reused — guard rather than trust.\n if (msg.channel !== this._channel) return;\n if (!msg.payload) return;\n let parsed: {\n stream?: unknown;\n events?: unknown;\n by?: unknown;\n };\n try {\n parsed = JSON.parse(msg.payload);\n } catch (err) {\n // A malformed payload is a bug somewhere upstream — log and skip\n // instead of tearing down the listener.\n logger.error(\n { err, payload: msg.payload },\n \"act_commit: malformed payload, skipping\"\n );\n return;\n }\n // Self-filter: skip notifications that originated from this same\n // store instance. This is what gives `notified` its cross-process\n // semantic — local commits already arm the drain via `do()`.\n if (parsed.by === this._by) return;\n if (typeof parsed.stream !== \"string\" || !Array.isArray(parsed.events)) {\n logger.error(\n { payload: msg.payload },\n \"act_commit: payload missing required fields, skipping\"\n );\n return;\n }\n const events: Array<{ id: number; name: string }> = [];\n for (const raw of parsed.events) {\n if (\n raw &&\n typeof raw === \"object\" &&\n typeof (raw as { id?: unknown }).id === \"number\" &&\n typeof (raw as { name?: unknown }).name === \"string\"\n ) {\n events.push({\n id: (raw as { id: number }).id,\n name: (raw as { name: string }).name,\n });\n }\n }\n if (events.length === 0) return;\n // Adapter-level robustness: a throwing handler must not tear\n // down the dedicated LISTEN client. The orchestrator wraps its\n // own `notified` emit + drain wakeup separately\n // (`Act._wireNotify`) — defense in depth, with each layer\n // protecting its own resources. Direct callers of\n // `store.notify(handler)` (tests, custom integrations) inherit\n // the adapter wrap.\n try {\n handler({ stream: parsed.stream, events });\n } catch (err) {\n logger.error(err, \"act_commit: handler threw, listener preserved\");\n }\n };\n client.on(\"notification\", onNotification);\n try {\n await client.query(`LISTEN ${this._channel}`);\n } catch (err) {\n client.removeListener(\"notification\", onNotification);\n client.release(true);\n throw err;\n }\n this._listenClient = client;\n this._listenHandler = onNotification;\n\n return async () => {\n // No-op when this disposer is stale (a later notify() call already\n // tore the subscription down).\n if (this._listenClient !== client) return;\n await this._teardownListen();\n };\n }\n\n /**\n * Atomically truncates streams and seeds each with a snapshot or tombstone.\n * @param targets - Streams to truncate with optional snapshot state and meta.\n * @returns Map keyed by stream name, each entry with `deleted` count and `committed` event.\n */\n async truncate(\n targets: Array<{\n stream: string;\n snapshot?: Schema;\n meta?: EventMeta;\n }>\n ): Promise<\n Map<\n string,\n { deleted: number; committed: Committed<Schemas, keyof Schemas> }\n >\n > {\n if (!targets.length) return new Map();\n const streams = targets.map((t) => t.stream);\n const client = await this._pool.connect();\n try {\n await client.query(\"BEGIN\");\n await client.query(`DELETE FROM ${this._fqs} WHERE stream = ANY($1)`, [\n streams,\n ]);\n const result = new Map<\n string,\n { deleted: number; committed: Committed<Schemas, keyof Schemas> }\n >();\n for (const { stream, snapshot, meta } of targets) {\n const { rowCount } = await client.query(\n `DELETE FROM ${this._fqt} WHERE stream = $1`,\n [stream]\n );\n const name = snapshot !== undefined ? SNAP_EVENT : TOMBSTONE_EVENT;\n const { rows } = await client.query(\n `INSERT INTO ${this._fqt}(name, data, stream, version, created, meta)\n VALUES($1, $2, $3, 0, now(), $4) RETURNING *`,\n [\n name,\n snapshot ?? {},\n stream,\n meta ?? { correlation: \"\", causation: {} },\n ]\n );\n result.set(stream, {\n deleted: rowCount ?? 0,\n committed: rows[0] as Committed<Schemas, keyof Schemas>,\n });\n }\n await client.query(\"COMMIT\");\n return result;\n } catch (error) {\n await client.query(\"ROLLBACK\").catch(() => {});\n throw error;\n } finally {\n client.release();\n }\n }\n\n /**\n * Atomically rebuild the store from a stream of {@link RestoreRow}.\n *\n * Wraps the entire rebuild in a single `BEGIN`/`COMMIT` transaction\n * — on any throw the transaction rolls back and the store ends\n * byte-for-byte unchanged. `TRUNCATE ... RESTART IDENTITY CASCADE`\n * wipes events + resets the serial sequence to 1; the streams\n * table is cleared in the same statement via `CASCADE`-like\n * `DELETE`. Rows are inserted one at a time with explicit columns\n * (skipping `id`) so the serial assigns dense ids from 1.\n *\n * Causation refs in `meta.causation.event.id` are remapped through\n * the `old → new` table built as rows land. References to ids not\n * in the source pass through unchanged.\n *\n * `created` is preserved verbatim from the source.\n */\n async restore(\n source: AsyncIterable<RestoreRow>,\n _opts: RestoreOptions = {}\n ): Promise<RestoreResult> {\n const started = Date.now();\n const client = await this._pool.connect();\n try {\n await client.query(\"BEGIN\");\n // RESTART IDENTITY resets the id sequence; CASCADE handles any\n // future FK refs (none today, but cheap insurance).\n await client.query(\n `TRUNCATE TABLE ${this._fqt} RESTART IDENTITY CASCADE`\n );\n await client.query(`TRUNCATE TABLE ${this._fqs}`);\n const idMap = new Map<number, number>();\n let kept = 0;\n for await (const row of source) {\n // Rewrite causation refs to the new id space before insert.\n let meta = row.meta;\n const causedBy = meta.causation.event?.id;\n if (causedBy !== undefined) {\n const remapped = idMap.get(causedBy);\n if (remapped !== undefined && remapped !== causedBy) {\n meta = {\n ...meta,\n causation: {\n ...meta.causation,\n event: { ...meta.causation.event!, id: remapped },\n },\n };\n }\n }\n const created =\n row.created instanceof Date ? row.created : new Date(row.created);\n const { rows } = await client.query<{ id: number }>(\n `INSERT INTO ${this._fqt}(name, data, stream, version, created, meta)\n VALUES($1, $2, $3, $4, $5, $6) RETURNING id`,\n [row.name, row.data, row.stream, row.version, created, meta]\n );\n idMap.set(row.id, rows[0]!.id);\n kept++;\n }\n await client.query(\"COMMIT\");\n return {\n kept,\n duration_ms: Date.now() - started,\n dropped: { closed_streams: 0, snapshots: 0, empty_streams: 0 },\n dry_run: false,\n };\n } catch (error) {\n await client.query(\"ROLLBACK\").catch(() => {});\n throw error;\n } finally {\n client.release();\n }\n }\n}\n","/**\n * @module act-pg\n * Date reviver for JSON.parse to automatically convert ISO 8601 date strings to Date objects.\n *\n * Recognizes the following formats:\n * - YYYY-MM-DDTHH:MM:SS.sssZ\n * - YYYY-MM-DDTHH:MM:SS.sss+HH:MM\n * - YYYY-MM-DDTHH:MM:SS.sss-HH:MM\n *\n * @param key The key being parsed\n * @param value The value being parsed\n * @returns A Date object if the value matches ISO 8601, otherwise the original value\n *\n * @example\n * const obj = JSON.parse(jsonString, dateReviver);\n */\nconst ISO_8601 =\n /^(\\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])T([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])(\\.\\d+)?(Z|[+-][0-2][0-9]:[0-5][0-9])?$/;\nexport const dateReviver = (_key: string, value: string): string | Date => {\n if (typeof value === \"string\" && ISO_8601.test(value)) {\n return new Date(value);\n }\n return value;\n};\n"],"mappings":"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","names":["pg","lagging","lane","rowCount"]}
package/dist/index.js CHANGED
@@ -1094,6 +1094,73 @@ var PostgresStore = class {
1094
1094
  client.release();
1095
1095
  }
1096
1096
  }
1097
+ /**
1098
+ * Atomically rebuild the store from a stream of {@link RestoreRow}.
1099
+ *
1100
+ * Wraps the entire rebuild in a single `BEGIN`/`COMMIT` transaction
1101
+ * — on any throw the transaction rolls back and the store ends
1102
+ * byte-for-byte unchanged. `TRUNCATE ... RESTART IDENTITY CASCADE`
1103
+ * wipes events + resets the serial sequence to 1; the streams
1104
+ * table is cleared in the same statement via `CASCADE`-like
1105
+ * `DELETE`. Rows are inserted one at a time with explicit columns
1106
+ * (skipping `id`) so the serial assigns dense ids from 1.
1107
+ *
1108
+ * Causation refs in `meta.causation.event.id` are remapped through
1109
+ * the `old → new` table built as rows land. References to ids not
1110
+ * in the source pass through unchanged.
1111
+ *
1112
+ * `created` is preserved verbatim from the source.
1113
+ */
1114
+ async restore(source, _opts = {}) {
1115
+ const started = Date.now();
1116
+ const client = await this._pool.connect();
1117
+ try {
1118
+ await client.query("BEGIN");
1119
+ await client.query(
1120
+ `TRUNCATE TABLE ${this._fqt} RESTART IDENTITY CASCADE`
1121
+ );
1122
+ await client.query(`TRUNCATE TABLE ${this._fqs}`);
1123
+ const idMap = /* @__PURE__ */ new Map();
1124
+ let kept = 0;
1125
+ for await (const row of source) {
1126
+ let meta = row.meta;
1127
+ const causedBy = meta.causation.event?.id;
1128
+ if (causedBy !== void 0) {
1129
+ const remapped = idMap.get(causedBy);
1130
+ if (remapped !== void 0 && remapped !== causedBy) {
1131
+ meta = {
1132
+ ...meta,
1133
+ causation: {
1134
+ ...meta.causation,
1135
+ event: { ...meta.causation.event, id: remapped }
1136
+ }
1137
+ };
1138
+ }
1139
+ }
1140
+ const created = row.created instanceof Date ? row.created : new Date(row.created);
1141
+ const { rows } = await client.query(
1142
+ `INSERT INTO ${this._fqt}(name, data, stream, version, created, meta)
1143
+ VALUES($1, $2, $3, $4, $5, $6) RETURNING id`,
1144
+ [row.name, row.data, row.stream, row.version, created, meta]
1145
+ );
1146
+ idMap.set(row.id, rows[0].id);
1147
+ kept++;
1148
+ }
1149
+ await client.query("COMMIT");
1150
+ return {
1151
+ kept,
1152
+ duration_ms: Date.now() - started,
1153
+ dropped: { closed_streams: 0, snapshots: 0, empty_streams: 0 },
1154
+ dry_run: false
1155
+ };
1156
+ } catch (error) {
1157
+ await client.query("ROLLBACK").catch(() => {
1158
+ });
1159
+ throw error;
1160
+ } finally {
1161
+ client.release();
1162
+ }
1163
+ }
1097
1164
  };
1098
1165
  export {
1099
1166
  PostgresStore