@rotorsoft/act-pg 0.21.1 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/@types/postgres-store.d.ts +26 -3
- package/dist/@types/postgres-store.d.ts.map +1 -1
- package/dist/index.cjs +86 -28
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +86 -28
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/postgres-store.ts","../src/utils.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport type {\n BlockedLease,\n Committed,\n EventMeta,\n Lease,\n Logger,\n Message,\n NotifyDisposer,\n PrioritizeFilter,\n Query,\n QueryStreams,\n QueryStreamsResult,\n Schema,\n Schemas,\n Store,\n StoreNotification,\n StreamPosition,\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 ) 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\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\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 ): 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 retry: number;\n lagging: boolean;\n }>(\n `\n WITH\n available AS (\n SELECT stream, source, at, priority\n FROM ${this._fqs} s\n WHERE blocked = false\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, 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, 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, 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\n `,\n [lagging, leading, by, millis]\n );\n await client.query(\"COMMIT\");\n\n return rows.map(({ stream, source, at, retry, lagging }) => ({\n stream,\n source: source ?? undefined,\n at,\n by,\n retry,\n lagging,\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<{ stream: string; source?: string; priority?: number }>\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 // Two 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 const { rowCount: inserted } = await client.query(\n `\n INSERT INTO ${this._fqs} (stream, source, priority)\n SELECT s->>'stream',\n s->>'source',\n COALESCE((s->>'priority')::int, 0)\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 }\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 }>(\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\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 }));\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 }>(\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\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 }));\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 async reset(streams: string[]): Promise<number> {\n if (!streams.length) return 0;\n const { rowCount } = await this._pool.query(\n `UPDATE ${this._fqs}\n SET at = -1, retry = 0, blocked = false, error = NULL,\n leased_by = NULL, leased_until = NULL\n WHERE stream = ANY($1)`,\n [streams]\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(\n filter: PrioritizeFilter,\n priority: number\n ): Promise<number> {\n const conditions: string[] = [\"priority <> $1\"];\n const values: unknown[] = [priority];\n\n if (filter.stream !== undefined) {\n values.push(filter.stream);\n conditions.push(\n filter.stream_exact\n ? `stream = $${values.length}`\n : `stream ~ $${values.length}`\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 = $${values.length}`\n : `source ~ $${values.length}`\n );\n }\n if (filter.blocked !== undefined) {\n values.push(filter.blocked);\n conditions.push(`blocked = $${values.length}`);\n }\n const sql = `UPDATE ${this._fqs} SET priority = $1 WHERE ${conditions.join(\" AND \")}`;\n const { rowCount } = await this._pool.query(sql, 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?.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 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 }>(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 });\n count++;\n }\n\n return { maxEventId: Number(maxResult.rows[0].m), count };\n } finally {\n client.release();\n }\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":["lagging"]}
|
|
1
|
+
{"version":3,"sources":["../src/postgres-store.ts","../src/utils.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport type {\n BlockedLease,\n Committed,\n EventMeta,\n Lease,\n Logger,\n Message,\n NotifyDisposer,\n Query,\n QueryStreams,\n QueryStreamsResult,\n Schema,\n Schemas,\n Store,\n StoreNotification,\n StreamFilter,\n StreamPosition,\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 ) 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\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\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 ): 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 retry: number;\n lagging: boolean;\n }>(\n `\n WITH\n available AS (\n SELECT stream, source, at, priority\n FROM ${this._fqs} s\n WHERE blocked = false\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, 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, 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, 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\n `,\n [lagging, leading, by, millis]\n );\n await client.query(\"COMMIT\");\n\n return rows.map(({ stream, source, at, retry, lagging }) => ({\n stream,\n source: source ?? undefined,\n at,\n by,\n retry,\n lagging,\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<{ stream: string; source?: string; priority?: number }>\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 // Two 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 const { rowCount: inserted } = await client.query(\n `\n INSERT INTO ${this._fqs} (stream, source, priority)\n SELECT s->>'stream',\n s->>'source',\n COALESCE((s->>'priority')::int, 0)\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 }\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 }>(\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\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 }));\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 }>(\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\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 }));\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 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?.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 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 }>(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 });\n count++;\n }\n\n return { maxEventId: Number(maxResult.rows[0].m), count };\n } finally {\n client.release();\n }\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":["lagging","rowCount"]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rotorsoft/act-pg",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.23.0",
|
|
5
5
|
"description": "act pg adapters",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"typescript",
|
|
@@ -49,7 +49,8 @@
|
|
|
49
49
|
"zod": "^4.4.3"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
|
-
"@types/pg": "^8.20.0"
|
|
52
|
+
"@types/pg": "^8.20.0",
|
|
53
|
+
"@rotorsoft/act-tck": "^0.1.0"
|
|
53
54
|
},
|
|
54
55
|
"scripts": {
|
|
55
56
|
"clean": "rm -rf dist",
|