@rotorsoft/act-pg 0.24.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -119
- package/dist/.tsbuildinfo +1 -1
- package/dist/@types/postgres-store.d.ts +2 -1
- package/dist/@types/postgres-store.d.ts.map +1 -1
- package/dist/index.cjs +53 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +53 -18
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -175,13 +175,18 @@ var PostgresStore = class {
|
|
|
175
175
|
error text,
|
|
176
176
|
leased_by text,
|
|
177
177
|
leased_until timestamptz,
|
|
178
|
-
priority int NOT NULL DEFAULT 0
|
|
178
|
+
priority int NOT NULL DEFAULT 0,
|
|
179
|
+
lane text NOT NULL DEFAULT 'default'
|
|
179
180
|
) TABLESPACE pg_default;`
|
|
180
181
|
);
|
|
181
182
|
await client.query(
|
|
182
183
|
`ALTER TABLE ${this._fqs}
|
|
183
184
|
ADD COLUMN IF NOT EXISTS priority int NOT NULL DEFAULT 0;`
|
|
184
185
|
);
|
|
186
|
+
await client.query(
|
|
187
|
+
`ALTER TABLE ${this._fqs}
|
|
188
|
+
ADD COLUMN IF NOT EXISTS lane text NOT NULL DEFAULT 'default';`
|
|
189
|
+
);
|
|
185
190
|
await client.query(
|
|
186
191
|
`DROP INDEX IF EXISTS "${this.config.schema}"."${this.config.table}_streams_fetch_ix"`
|
|
187
192
|
);
|
|
@@ -189,6 +194,10 @@ var PostgresStore = class {
|
|
|
189
194
|
`CREATE INDEX IF NOT EXISTS "${this.config.table}_streams_claim_ix"
|
|
190
195
|
ON ${this._fqs} (blocked, priority DESC, at);`
|
|
191
196
|
);
|
|
197
|
+
await client.query(
|
|
198
|
+
`CREATE INDEX IF NOT EXISTS "${this.config.table}_streams_lane_ix"
|
|
199
|
+
ON ${this._fqs} (lane);`
|
|
200
|
+
);
|
|
192
201
|
await client.query("COMMIT");
|
|
193
202
|
logger.info(
|
|
194
203
|
`Seeded schema "${this.config.schema}" with table "${this.config.table}"`
|
|
@@ -386,17 +395,20 @@ var PostgresStore = class {
|
|
|
386
395
|
* @param millis - Lease duration in milliseconds
|
|
387
396
|
* @returns Leased streams with metadata
|
|
388
397
|
*/
|
|
389
|
-
async claim(lagging, leading, by, millis) {
|
|
398
|
+
async claim(lagging, leading, by, millis, lane) {
|
|
390
399
|
const client = await this._pool.connect();
|
|
391
400
|
try {
|
|
392
401
|
await client.query("BEGIN");
|
|
402
|
+
const laneClause = lane !== void 0 ? `AND s.lane = $5` : "";
|
|
403
|
+
const params = lane !== void 0 ? [lagging, leading, by, millis, lane] : [lagging, leading, by, millis];
|
|
393
404
|
const { rows } = await client.query(
|
|
394
405
|
`
|
|
395
406
|
WITH
|
|
396
407
|
available AS (
|
|
397
|
-
SELECT stream, source, at, priority
|
|
408
|
+
SELECT stream, source, at, priority, lane
|
|
398
409
|
FROM ${this._fqs} s
|
|
399
410
|
WHERE blocked = false
|
|
411
|
+
${laneClause}
|
|
400
412
|
AND (leased_by IS NULL OR leased_until <= NOW())
|
|
401
413
|
AND (s.at < 0 OR EXISTS (
|
|
402
414
|
SELECT 1 FROM ${this._fqt} e
|
|
@@ -412,19 +424,19 @@ var PostgresStore = class {
|
|
|
412
424
|
-- ORDER BY collapses to plain at ASC so existing workloads
|
|
413
425
|
-- see no behavior change.
|
|
414
426
|
lag AS (
|
|
415
|
-
SELECT stream, source, at, TRUE AS lagging
|
|
427
|
+
SELECT stream, source, at, lane, TRUE AS lagging
|
|
416
428
|
FROM available
|
|
417
429
|
ORDER BY priority DESC, at ASC
|
|
418
430
|
LIMIT $1
|
|
419
431
|
),
|
|
420
432
|
lead AS (
|
|
421
|
-
SELECT stream, source, at, FALSE AS lagging
|
|
433
|
+
SELECT stream, source, at, lane, FALSE AS lagging
|
|
422
434
|
FROM available
|
|
423
435
|
ORDER BY at DESC
|
|
424
436
|
LIMIT $2
|
|
425
437
|
),
|
|
426
438
|
combined AS (
|
|
427
|
-
SELECT DISTINCT ON (stream) stream, source, at, lagging
|
|
439
|
+
SELECT DISTINCT ON (stream) stream, source, at, lane, lagging
|
|
428
440
|
FROM (SELECT * FROM lag UNION ALL SELECT * FROM lead) t
|
|
429
441
|
ORDER BY stream, at
|
|
430
442
|
)
|
|
@@ -435,18 +447,19 @@ var PostgresStore = class {
|
|
|
435
447
|
retry = s.retry + 1
|
|
436
448
|
FROM combined c
|
|
437
449
|
WHERE s.stream = c.stream
|
|
438
|
-
RETURNING s.stream, s.source, s.at, s.retry, c.lagging
|
|
450
|
+
RETURNING s.stream, s.source, s.at, s.retry, c.lagging, s.lane
|
|
439
451
|
`,
|
|
440
|
-
|
|
452
|
+
params
|
|
441
453
|
);
|
|
442
454
|
await client.query("COMMIT");
|
|
443
|
-
return rows.map(({ stream, source, at, retry, lagging: lagging2 }) => ({
|
|
455
|
+
return rows.map(({ stream, source, at, retry, lagging: lagging2, lane: lane2 }) => ({
|
|
444
456
|
stream,
|
|
445
457
|
source: source ?? void 0,
|
|
446
458
|
at,
|
|
447
459
|
by,
|
|
448
460
|
retry,
|
|
449
|
-
lagging: lagging2
|
|
461
|
+
lagging: lagging2,
|
|
462
|
+
lane: lane2
|
|
450
463
|
}));
|
|
451
464
|
} catch (error) {
|
|
452
465
|
await client.query("ROLLBACK").catch(() => {
|
|
@@ -472,10 +485,11 @@ var PostgresStore = class {
|
|
|
472
485
|
if (streams.length) {
|
|
473
486
|
const { rowCount: inserted } = await client.query(
|
|
474
487
|
`
|
|
475
|
-
INSERT INTO ${this._fqs} (stream, source, priority)
|
|
488
|
+
INSERT INTO ${this._fqs} (stream, source, priority, lane)
|
|
476
489
|
SELECT s->>'stream',
|
|
477
490
|
s->>'source',
|
|
478
|
-
COALESCE((s->>'priority')::int, 0)
|
|
491
|
+
COALESCE((s->>'priority')::int, 0),
|
|
492
|
+
COALESCE(s->>'lane', 'default')
|
|
479
493
|
FROM jsonb_array_elements($1::jsonb) AS s
|
|
480
494
|
ON CONFLICT (stream) DO NOTHING
|
|
481
495
|
`,
|
|
@@ -492,6 +506,16 @@ var PostgresStore = class {
|
|
|
492
506
|
`,
|
|
493
507
|
[JSON.stringify(streams)]
|
|
494
508
|
);
|
|
509
|
+
await client.query(
|
|
510
|
+
`
|
|
511
|
+
UPDATE ${this._fqs} t
|
|
512
|
+
SET lane = COALESCE(s->>'lane', 'default')
|
|
513
|
+
FROM jsonb_array_elements($1::jsonb) AS s
|
|
514
|
+
WHERE t.stream = s->>'stream'
|
|
515
|
+
AND t.lane <> COALESCE(s->>'lane', 'default')
|
|
516
|
+
`,
|
|
517
|
+
[JSON.stringify(streams)]
|
|
518
|
+
);
|
|
495
519
|
}
|
|
496
520
|
const { rows } = await client.query(
|
|
497
521
|
`SELECT COALESCE(MAX(at), -1) AS max FROM ${this._fqs}`
|
|
@@ -531,7 +555,7 @@ var PostgresStore = class {
|
|
|
531
555
|
leased_until = NULL
|
|
532
556
|
FROM input i
|
|
533
557
|
WHERE s.stream = i.stream AND s.leased_by = i.by
|
|
534
|
-
RETURNING s.stream, s.source, s.at, i.by, s.retry, i.lagging
|
|
558
|
+
RETURNING s.stream, s.source, s.at, i.by, s.retry, i.lagging, s.lane
|
|
535
559
|
`,
|
|
536
560
|
[JSON.stringify(leases)]
|
|
537
561
|
);
|
|
@@ -542,7 +566,8 @@ var PostgresStore = class {
|
|
|
542
566
|
at: row.at,
|
|
543
567
|
by: row.by,
|
|
544
568
|
retry: row.retry,
|
|
545
|
-
lagging: row.lagging
|
|
569
|
+
lagging: row.lagging,
|
|
570
|
+
lane: row.lane
|
|
546
571
|
}));
|
|
547
572
|
} catch (error) {
|
|
548
573
|
await client.query("ROLLBACK").catch(() => {
|
|
@@ -572,7 +597,7 @@ var PostgresStore = class {
|
|
|
572
597
|
SET blocked = true, error = i.error
|
|
573
598
|
FROM input i
|
|
574
599
|
WHERE s.stream = i.stream AND s.leased_by = i.by AND s.blocked = false
|
|
575
|
-
RETURNING s.stream, s.source, s.at, i.by, s.retry, s.error, i.lagging
|
|
600
|
+
RETURNING s.stream, s.source, s.at, i.by, s.retry, s.error, i.lagging, s.lane
|
|
576
601
|
`,
|
|
577
602
|
[JSON.stringify(leases)]
|
|
578
603
|
);
|
|
@@ -584,7 +609,8 @@ var PostgresStore = class {
|
|
|
584
609
|
by: row.by,
|
|
585
610
|
retry: row.retry,
|
|
586
611
|
lagging: row.lagging,
|
|
587
|
-
error: row.error
|
|
612
|
+
error: row.error,
|
|
613
|
+
lane: row.lane
|
|
588
614
|
}));
|
|
589
615
|
} catch (error) {
|
|
590
616
|
await client.query("ROLLBACK").catch(() => {
|
|
@@ -627,6 +653,10 @@ var PostgresStore = class {
|
|
|
627
653
|
values.push(filter.blocked);
|
|
628
654
|
conditions.push(`blocked = $${start + values.length - 1}`);
|
|
629
655
|
}
|
|
656
|
+
if (filter.lane !== void 0) {
|
|
657
|
+
values.push(filter.lane);
|
|
658
|
+
conditions.push(`lane = $${start + values.length - 1}`);
|
|
659
|
+
}
|
|
630
660
|
return {
|
|
631
661
|
clause: conditions.length ? conditions.join(" AND ") : "TRUE",
|
|
632
662
|
values
|
|
@@ -739,11 +769,15 @@ var PostgresStore = class {
|
|
|
739
769
|
values.push(query.blocked);
|
|
740
770
|
conditions.push(`blocked = $${values.length}`);
|
|
741
771
|
}
|
|
772
|
+
if (query?.lane !== void 0) {
|
|
773
|
+
values.push(query.lane);
|
|
774
|
+
conditions.push(`lane = $${values.length}`);
|
|
775
|
+
}
|
|
742
776
|
if (query?.after !== void 0) {
|
|
743
777
|
values.push(query.after);
|
|
744
778
|
conditions.push(`stream > $${values.length}`);
|
|
745
779
|
}
|
|
746
|
-
let sql = `SELECT stream, source, at, retry, blocked, error, leased_by, leased_until, priority FROM ${this._fqs}`;
|
|
780
|
+
let sql = `SELECT stream, source, at, retry, blocked, error, leased_by, leased_until, priority, lane FROM ${this._fqs}`;
|
|
747
781
|
if (conditions.length) sql += " WHERE " + conditions.join(" AND ");
|
|
748
782
|
values.push(limit);
|
|
749
783
|
sql += ` ORDER BY stream LIMIT $${values.length}`;
|
|
@@ -766,7 +800,8 @@ var PostgresStore = class {
|
|
|
766
800
|
error: row.error ?? "",
|
|
767
801
|
priority: row.priority,
|
|
768
802
|
leased_by: row.leased_by ?? void 0,
|
|
769
|
-
leased_until: row.leased_until ?? void 0
|
|
803
|
+
leased_until: row.leased_until ?? void 0,
|
|
804
|
+
lane: row.lane
|
|
770
805
|
});
|
|
771
806
|
count++;
|
|
772
807
|
}
|
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 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 ) 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 * 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":["lagging","rowCount"]}
|
|
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 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":["lagging","lane","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": "1.0.0",
|
|
5
5
|
"description": "act pg adapters",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"typescript",
|
|
@@ -45,12 +45,12 @@
|
|
|
45
45
|
"pg": "^8.20.0"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
|
-
"
|
|
49
|
-
"
|
|
48
|
+
"zod": "^4.4.3",
|
|
49
|
+
"@rotorsoft/act": "^1.0.0"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@types/pg": "^8.20.0",
|
|
53
|
-
"@rotorsoft/act-tck": "^0.
|
|
53
|
+
"@rotorsoft/act-tck": "^0.4.0"
|
|
54
54
|
},
|
|
55
55
|
"scripts": {
|
|
56
56
|
"clean": "rm -rf dist",
|