mastra-pg-pubsub 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alan Hoffmeister
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,168 @@
1
+ # mastra-pg-pubsub
2
+
3
+ [![CI](https://github.com/alanhoff/mastra-pg-pubsub/actions/workflows/ci.yml/badge.svg)](https://github.com/alanhoff/mastra-pg-pubsub/actions/workflows/ci.yml)
4
+ [![npm version](https://img.shields.io/npm/v/mastra-pg-pubsub.svg)](https://www.npmjs.com/package/mastra-pg-pubsub)
5
+ [![license](https://img.shields.io/npm/l/mastra-pg-pubsub.svg)](./LICENSE)
6
+
7
+ PostgreSQL-backed [`PubSub`](https://mastra.ai/reference/pubsub/base) for Mastra. It gives Mastra apps at-least-once delivery, consumer groups, replay by offset, optional dead-lettering, and low-latency `LISTEN/NOTIFY` wakeups using a database you already operate.
8
+
9
+ ## Why
10
+
11
+ Use this when you want Mastra agent/workflow events to survive process restarts and coordinate across multiple Node processes without adding Redis, NATS, or a cloud queue.
12
+
13
+ - **At-least-once delivery** with ack/nack and visibility timeouts.
14
+ - **Consumer groups** for competing workers (`subscribe(..., { group })`).
15
+ - **Fan-out** for groupless subscribers.
16
+ - **Replay** via `getHistory`, `subscribeWithReplay`, and `subscribeFromOffset`.
17
+ - **Crash recovery** through durable delivery rows and visibility timeout redelivery.
18
+ - **Low latency** through Postgres `LISTEN/NOTIFY`, with polling as the correctness backstop.
19
+ - **Small runtime surface**: one runtime dependency (`pg`), ESM, strict TypeScript, Node-native tests.
20
+
21
+ ## Install
22
+
23
+ ```sh
24
+ npm install mastra-pg-pubsub @mastra/core
25
+ ```
26
+
27
+ `@mastra/core` is a peer dependency. `pg` is installed as this package's runtime dependency. If your app already owns a `pg.Pool`, pass it in instead of a connection string.
28
+
29
+ ## Quickstart
30
+
31
+ ```ts
32
+ import { Agent } from '@mastra/core/agent';
33
+ import { Mastra } from '@mastra/core/mastra';
34
+ import { PostgresPubSub } from 'mastra-pg-pubsub';
35
+
36
+ const pubsub = new PostgresPubSub({
37
+ connectionString: process.env.DATABASE_URL,
38
+ schema: 'mastra_pubsub',
39
+ });
40
+
41
+ await pubsub.migrate(); // optional; methods migrate lazily too
42
+
43
+ export const mastra = new Mastra({
44
+ pubsub,
45
+ agents: {
46
+ assistant: new Agent({
47
+ id: 'assistant',
48
+ name: 'Assistant',
49
+ instructions: 'You are helpful.',
50
+ model: 'openai/gpt-4o-mini',
51
+ }),
52
+ },
53
+ });
54
+ ```
55
+
56
+ You can also use it directly through the Mastra PubSub contract:
57
+
58
+ ```ts
59
+ await pubsub.subscribe('agent.stream.run-123', (event, ack, nack) => {
60
+ try {
61
+ console.log(event.type, event.index, event.data);
62
+ ack?.();
63
+ } catch {
64
+ nack?.();
65
+ }
66
+ });
67
+
68
+ await pubsub.publish('agent.stream.run-123', {
69
+ type: 'chunk',
70
+ data: { text: 'hello' },
71
+ runId: 'run-123',
72
+ });
73
+ ```
74
+
75
+ ## Replay examples
76
+
77
+ ```ts
78
+ const history = await pubsub.getHistory('agent.stream.run-123', 10);
79
+
80
+ await pubsub.subscribeWithReplay('agent.stream.run-123', (event, ack) => {
81
+ ack?.();
82
+ });
83
+
84
+ await pubsub.subscribeFromOffset('agent.stream.run-123', 42, (event, ack) => {
85
+ ack?.();
86
+ });
87
+ ```
88
+
89
+ Replay registers the live subscription first, then replays history, deduping the boundary by event `index` so no event is missed or delivered twice at the transition.
90
+
91
+ ## Configuration
92
+
93
+ Provide exactly one of `connectionString` or `pool`.
94
+
95
+ | Option | Default | Description |
96
+ | --- | ---: | --- |
97
+ | `connectionString` | — | PostgreSQL connection string. The adapter owns and closes its pool. |
98
+ | `pool` | — | Bring-your-own `pg.Pool`; never closed by `PostgresPubSub.close()`. |
99
+ | `schema` | `mastra_pubsub` | Schema for all tables. Must match `^[a-z_][a-z0-9_]*$`. |
100
+ | `pollIntervalMs` | `1000` | Backstop polling interval and redelivery detection bound. |
101
+ | `ackDeadlineMs` | `30000` | Visibility timeout before unacked deliveries can be reclaimed. |
102
+ | `nackDelayMs` | `0` | Delay before a nacked delivery becomes visible again. |
103
+ | `maxDeliveryAttempts` | `5` | Attempts before drop/dead-letter. `Infinity` disables the cap; `0` is treated as `Infinity`. |
104
+ | `batchSize` | `32` | Deliveries claimed per consume-loop tick. |
105
+ | `maxEventsPerTopic` | `10000` | Retention cap per topic. `0` keeps everything. |
106
+ | `cleanupIntervalMs` | `60000` | Maintenance interval. `0` disables maintenance. |
107
+ | `staleSubscriptionMs` | `300000` | Age before stale private subscriptions are pruned. |
108
+ | `listen` | `true` | Enable `LISTEN/NOTIFY` wakeups. `false` uses polling only. |
109
+ | `deadLetter` | `false` | Copy exhausted events to `dead_events`. |
110
+ | `logger` | silent | Optional `debug`, `warn`, and `error` functions. |
111
+
112
+ ## Delivery guarantees
113
+
114
+ | Property | Guarantee |
115
+ | --- | --- |
116
+ | Delivery | At least once; `ack()` settles, missing ack redelivers after `ackDeadlineMs`. |
117
+ | Ordering | Per-topic `index` order for normal delivery; retries can interleave with newer events. |
118
+ | Groups | Each event is delivered to one member per group. |
119
+ | Fan-out | Each groupless subscriber receives every event published after it subscribes. |
120
+ | Replay | Historical events are ordered by per-topic `index` and available until retention trims them. |
121
+ | Idempotency | Event `id` is stable across redeliveries for consumer-side dedupe. |
122
+ | Lifecycle | `flush()` drains in-flight local work; `close()` is idempotent and cleans private subscriptions. |
123
+
124
+ This is intentionally **not exactly-once** delivery. Consumers that perform side effects should dedupe by `event.id` or a domain idempotency key.
125
+
126
+ ## Architecture
127
+
128
+ ```mermaid
129
+ flowchart LR
130
+ P[publish] -->|tx: bump topic counter, insert event + deliveries, NOTIFY| DB[(PostgreSQL)]
131
+ DB -->|NOTIFY wakeup / poll| L[consume loop per subscription]
132
+ L -->|claim batch: FOR UPDATE SKIP LOCKED + visibility timeout| DB
133
+ L -->|event, ack, nack| CB[EventCallback]
134
+ CB -->|ack: DELETE delivery| DB
135
+ CB -->|nack: visible_at = now()+nackDelay| DB
136
+ ```
137
+
138
+ The schema is created lazily under a Postgres advisory lock, or explicitly with `await pubsub.migrate()`.
139
+
140
+ ## Local development
141
+
142
+ ```sh
143
+ npm install
144
+ npm run db:up
145
+ npm test
146
+ npm run test:coverage
147
+ npm run typecheck
148
+ npm run lint
149
+ npm run build
150
+ ```
151
+
152
+ `npm test` is key-free and uses the pinned Postgres service from `docker-compose.yml` on port `5544`.
153
+
154
+ ### Real e2e tests
155
+
156
+ The e2e suite includes one real Mastra durable-agent stream backed by OpenAI and Postgres memory, plus no-OpenAI delivery semantics tests. The real agent test intentionally validates the durable-agent stream API and topic shape for the locked `@mastra/core` version; refresh it when upgrading Mastra.
157
+
158
+ ```sh
159
+ OPENAI_API_KEY=... # or put it in .env
160
+ npm run db:up
161
+ npm run test:e2e
162
+ ```
163
+
164
+ The script loads `.env` when present with Node's `--env-file-if-exists=.env`, so an exported `OPENAI_API_KEY` also works. Keep `.env` out of git.
165
+
166
+ ## Package contents
167
+
168
+ `npm pack --dry-run` should include only the built `dist/` files plus package metadata, README, and license. Source, tests, local research notes, and `.env` are not published.
@@ -0,0 +1,46 @@
1
+ import type { EventCallback } from '@mastra/core/events';
2
+ import type { Pool } from 'pg';
3
+ import type { ResolvedConfig } from './types.ts';
4
+ /**
5
+ * Round-robin set of local callbacks bound to one (topic, subscription) pair.
6
+ * Group subscriptions round-robin a claimed event across local members;
7
+ * private (fan-out) subscriptions deliver every event to their single member.
8
+ */
9
+ export interface CallbackRegistry {
10
+ /** Ordered callbacks; group loops round-robin, private loops use index 0. */
11
+ readonly callbacks: EventCallback[];
12
+ /** Rotating cursor for round-robin group dispatch. */
13
+ cursor: number;
14
+ }
15
+ /**
16
+ * One consume loop per (topic, subscription id). Claims batches with
17
+ * `FOR UPDATE SKIP LOCKED`, extends visibility, delivers sequentially, and
18
+ * settles deliveries via ack/nack or visibility timeout. Woken by
19
+ * `LISTEN/NOTIFY` and by a polling backstop that also reclaims expired
20
+ * deliveries.
21
+ */
22
+ export declare class ConsumeLoop {
23
+ #private;
24
+ /**
25
+ * @param pool - Connection pool.
26
+ * @param schema - Validated schema name.
27
+ * @param config - Resolved configuration.
28
+ * @param topic - Subscribed topic.
29
+ * @param subscriptionId - Subscription row id (group name or private id).
30
+ * @param isGroup - Whether this is a competing-consumer group.
31
+ * @param registry - Shared local callback registry for this subscription.
32
+ */
33
+ constructor(pool: Pool, schema: string, config: ResolvedConfig, topic: string, subscriptionId: string, isGroup: boolean, registry: CallbackRegistry);
34
+ /** Start the background loop. Safe to call once. */
35
+ start(): void;
36
+ /** Request an immediate poll (used by NOTIFY wakeups). */
37
+ wake(): void;
38
+ /**
39
+ * Resolve once the loop is idle with no claimable work and no in-flight
40
+ * deliveries — the basis for `flush()`.
41
+ */
42
+ drain(): Promise<void>;
43
+ /** Stop the loop and wait for it to settle. Idempotent. */
44
+ stop(): Promise<void>;
45
+ }
46
+ //# sourceMappingURL=consume-loop.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"consume-loop.d.ts","sourceRoot":"","sources":["../src/consume-loop.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAS,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAChE,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,IAAI,CAAC;AAE/B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAajD;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,6EAA6E;IAC7E,QAAQ,CAAC,SAAS,EAAE,aAAa,EAAE,CAAC;IACpC,sDAAsD;IACtD,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;GAMG;AACH,qBAAa,WAAW;;IAiBtB;;;;;;;;OAQG;gBAED,IAAI,EAAE,IAAI,EACV,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,cAAc,EACtB,KAAK,EAAE,MAAM,EACb,cAAc,EAAE,MAAM,EACtB,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAE,gBAAgB;IAgB5B,oDAAoD;IACpD,KAAK,IAAI,IAAI;IAOb,0DAA0D;IAC1D,IAAI,IAAI,IAAI;IASZ;;;OAGG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAkN5B,2DAA2D;IACrD,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAQ5B"}
@@ -0,0 +1,262 @@
1
+ import { quoteIdentifier } from "./sql.js";
2
+ /**
3
+ * One consume loop per (topic, subscription id). Claims batches with
4
+ * `FOR UPDATE SKIP LOCKED`, extends visibility, delivers sequentially, and
5
+ * settles deliveries via ack/nack or visibility timeout. Woken by
6
+ * `LISTEN/NOTIFY` and by a polling backstop that also reclaims expired
7
+ * deliveries.
8
+ */
9
+ export class ConsumeLoop {
10
+ #pool;
11
+ #schema;
12
+ #config;
13
+ #topic;
14
+ #subscriptionId;
15
+ #isGroup;
16
+ #registry;
17
+ #stopped = false;
18
+ #wakeRequested = false;
19
+ #wake;
20
+ #idle;
21
+ #resolveIdle;
22
+ #inFlight = 0;
23
+ #loopPromise;
24
+ /**
25
+ * @param pool - Connection pool.
26
+ * @param schema - Validated schema name.
27
+ * @param config - Resolved configuration.
28
+ * @param topic - Subscribed topic.
29
+ * @param subscriptionId - Subscription row id (group name or private id).
30
+ * @param isGroup - Whether this is a competing-consumer group.
31
+ * @param registry - Shared local callback registry for this subscription.
32
+ */
33
+ constructor(pool, schema, config, topic, subscriptionId, isGroup, registry) {
34
+ this.#pool = pool;
35
+ this.#schema = schema;
36
+ this.#config = config;
37
+ this.#topic = topic;
38
+ this.#subscriptionId = subscriptionId;
39
+ this.#isGroup = isGroup;
40
+ this.#registry = registry;
41
+ this.#idle = Promise.resolve();
42
+ }
43
+ #q(table) {
44
+ return `${quoteIdentifier(this.#schema)}.${quoteIdentifier(table)}`;
45
+ }
46
+ /** Start the background loop. Safe to call once. */
47
+ start() {
48
+ if (this.#loopPromise) {
49
+ return;
50
+ }
51
+ this.#loopPromise = this.#run();
52
+ }
53
+ /** Request an immediate poll (used by NOTIFY wakeups). */
54
+ wake() {
55
+ this.#wakeRequested = true;
56
+ if (this.#wake) {
57
+ const fn = this.#wake;
58
+ this.#wake = undefined;
59
+ fn();
60
+ }
61
+ }
62
+ /**
63
+ * Resolve once the loop is idle with no claimable work and no in-flight
64
+ * deliveries — the basis for `flush()`.
65
+ */
66
+ async drain() {
67
+ this.wake();
68
+ await this.#idle;
69
+ }
70
+ async #run() {
71
+ while (!this.#stopped) {
72
+ this.#wakeRequested = false;
73
+ let delivered = 0;
74
+ try {
75
+ delivered = await this.#tick();
76
+ }
77
+ catch (error) {
78
+ if (!this.#stopped) {
79
+ this.#config.logger.error?.('consume loop tick failed', error);
80
+ }
81
+ }
82
+ if (this.#stopped) {
83
+ break;
84
+ }
85
+ if (delivered > 0 || this.#wakeRequested) {
86
+ continue;
87
+ }
88
+ this.#markIdle();
89
+ await this.#sleep();
90
+ }
91
+ this.#markIdle();
92
+ }
93
+ #markIdle() {
94
+ if (this.#inFlight === 0 && this.#resolveIdle) {
95
+ const resolve = this.#resolveIdle;
96
+ this.#resolveIdle = undefined;
97
+ resolve();
98
+ }
99
+ }
100
+ #armIdle() {
101
+ if (!this.#resolveIdle) {
102
+ this.#idle = new Promise((resolve) => {
103
+ this.#resolveIdle = resolve;
104
+ });
105
+ }
106
+ }
107
+ async #sleep() {
108
+ this.#armIdle();
109
+ if (this.#wakeRequested || this.#stopped) {
110
+ return;
111
+ }
112
+ await new Promise((resolve) => {
113
+ const timer = setTimeout(() => {
114
+ this.#wake = undefined;
115
+ resolve();
116
+ }, this.#config.pollIntervalMs);
117
+ this.#wake = () => {
118
+ clearTimeout(timer);
119
+ resolve();
120
+ };
121
+ });
122
+ }
123
+ async #tick() {
124
+ if (this.#registry.callbacks.length === 0) {
125
+ return 0;
126
+ }
127
+ const rows = await this.#claim();
128
+ if (rows.length === 0) {
129
+ return 0;
130
+ }
131
+ this.#armIdle();
132
+ for (const row of rows) {
133
+ if (this.#stopped) {
134
+ break;
135
+ }
136
+ await this.#deliver(row);
137
+ }
138
+ return rows.length;
139
+ }
140
+ async #claim() {
141
+ const visibility = this.#config.ackDeadlineMs;
142
+ const sql = `
143
+ WITH claimed AS (
144
+ SELECT d.event_seq
145
+ FROM ${this.#q('deliveries')} d
146
+ WHERE d.subscription_id = $1 AND d.visible_at <= now()
147
+ ORDER BY d.event_seq
148
+ FOR UPDATE SKIP LOCKED
149
+ LIMIT $2
150
+ )
151
+ UPDATE ${this.#q('deliveries')} d
152
+ SET delivery_attempt = d.delivery_attempt + 1,
153
+ visible_at = now() + ($3::double precision * interval '1 millisecond')
154
+ FROM claimed, ${this.#q('events')} e
155
+ WHERE d.subscription_id = $1
156
+ AND d.event_seq = claimed.event_seq
157
+ AND e.seq = d.event_seq
158
+ RETURNING d.event_seq AS seq, e.id AS event_id, e.index, e.type,
159
+ e.run_id, e.data, e.created_at, d.delivery_attempt`;
160
+ const result = await this.#pool.query(sql, [
161
+ this.#subscriptionId,
162
+ this.#config.batchSize,
163
+ visibility,
164
+ ]);
165
+ return [...result.rows].sort((a, b) => Number(BigInt(a.seq) - BigInt(b.seq)));
166
+ }
167
+ async #deliver(row) {
168
+ if (row.delivery_attempt > this.#config.maxDeliveryAttempts) {
169
+ await this.#dropExhausted(row);
170
+ return;
171
+ }
172
+ const callback = this.#nextCallback();
173
+ if (!callback) {
174
+ return;
175
+ }
176
+ const event = {
177
+ id: row.event_id,
178
+ type: row.type,
179
+ data: row.data,
180
+ runId: row.run_id,
181
+ createdAt: row.created_at,
182
+ index: Number(row.index),
183
+ deliveryAttempt: row.delivery_attempt,
184
+ };
185
+ this.#inFlight++;
186
+ let settled = false;
187
+ const ack = async () => {
188
+ if (settled) {
189
+ return;
190
+ }
191
+ settled = true;
192
+ await this.#ack(row.seq);
193
+ };
194
+ const nack = async () => {
195
+ if (settled) {
196
+ return;
197
+ }
198
+ settled = true;
199
+ await this.#nack(row.seq);
200
+ };
201
+ try {
202
+ await callback(event, ack, nack);
203
+ }
204
+ catch (error) {
205
+ this.#config.logger.error?.('subscriber callback threw', error);
206
+ }
207
+ finally {
208
+ this.#inFlight--;
209
+ this.#markIdle();
210
+ }
211
+ }
212
+ #nextCallback() {
213
+ const callbacks = this.#registry.callbacks;
214
+ if (callbacks.length === 0) {
215
+ return undefined;
216
+ }
217
+ if (!this.#isGroup) {
218
+ return callbacks[0];
219
+ }
220
+ const index = this.#registry.cursor % callbacks.length;
221
+ this.#registry.cursor = (this.#registry.cursor + 1) % callbacks.length;
222
+ return callbacks[index];
223
+ }
224
+ async #ack(seq) {
225
+ await this.#pool.query(`DELETE FROM ${this.#q('deliveries')} WHERE subscription_id = $1 AND event_seq = $2`, [this.#subscriptionId, seq]);
226
+ }
227
+ async #nack(seq) {
228
+ await this.#pool.query(`UPDATE ${this.#q('deliveries')}
229
+ SET visible_at = now() + ($3::double precision * interval '1 millisecond')
230
+ WHERE subscription_id = $1 AND event_seq = $2`, [this.#subscriptionId, seq, this.#config.nackDelayMs]);
231
+ this.wake();
232
+ }
233
+ async #dropExhausted(row) {
234
+ this.#config.logger.warn?.(`dropping event ${row.event_id} on subscription ${this.#subscriptionId} after ${row.delivery_attempt - 1} attempts`);
235
+ if (this.#config.deadLetter) {
236
+ await this.#pool.query(`INSERT INTO ${this.#q('dead_events')}
237
+ (event_id, subscription_id, topic, index, type, run_id, data, created_at, delivery_attempt)
238
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`, [
239
+ row.event_id,
240
+ this.#subscriptionId,
241
+ this.#topic,
242
+ row.index,
243
+ row.type,
244
+ row.run_id,
245
+ row.data === null || row.data === undefined ? null : JSON.stringify(row.data),
246
+ row.created_at,
247
+ row.delivery_attempt - 1,
248
+ ]);
249
+ }
250
+ await this.#ack(row.seq);
251
+ }
252
+ /** Stop the loop and wait for it to settle. Idempotent. */
253
+ async stop() {
254
+ this.#stopped = true;
255
+ this.wake();
256
+ if (this.#loopPromise) {
257
+ // #run() swallows tick errors internally, so this never rejects.
258
+ await this.#loopPromise;
259
+ }
260
+ }
261
+ }
262
+ //# sourceMappingURL=consume-loop.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"consume-loop.js","sourceRoot":"","sources":["../src/consume-loop.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ export { PostgresPubSub } from './postgres-pubsub.ts';
2
+ export type { LogFn, PostgresPubSubConfig, PubSubLogger, } from './types.ts';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,YAAY,EACV,KAAK,EACL,oBAAoB,EACpB,YAAY,GACb,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { PostgresPubSub } from "./postgres-pubsub.js";
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC"}
@@ -0,0 +1,31 @@
1
+ import type { Pool } from 'pg';
2
+ import type { PubSubLogger } from './types.ts';
3
+ /**
4
+ * Owns a single dedicated `LISTEN` connection and dispatches `NOTIFY` payloads
5
+ * (topic names) to registered per-topic wakeup handlers. Reconnects on
6
+ * connection loss so wakeups survive transient database blips; polling remains
7
+ * the correctness backstop in the meantime.
8
+ */
9
+ export declare class NotifyListener {
10
+ #private;
11
+ /**
12
+ * @param pool - Pool used to acquire the dedicated listen connection.
13
+ * @param schema - Validated schema name; determines the channel.
14
+ * @param logger - Logger for connection diagnostics.
15
+ */
16
+ constructor(pool: Pool, schema: string, logger: PubSubLogger);
17
+ /**
18
+ * Register a wakeup handler for a topic, ensuring the listen connection is
19
+ * established. Returns an unregister function.
20
+ *
21
+ * @param topic - The topic to wake on.
22
+ * @param handler - Invoked when a `NOTIFY` for the topic arrives.
23
+ * @returns A function that removes this handler.
24
+ */
25
+ register(topic: string, handler: () => void): Promise<() => void>;
26
+ /**
27
+ * Release the listen connection and stop dispatching. Idempotent.
28
+ */
29
+ close(): Promise<void>;
30
+ }
31
+ //# sourceMappingURL=listener.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"listener.d.ts","sourceRoot":"","sources":["../src/listener.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAc,MAAM,IAAI,CAAC;AAE3C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C;;;;;GAKG;AACH,qBAAa,cAAc;;IASzB;;;;OAIG;gBACS,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY;IAM5D;;;;;;;OAOG;IACG,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,MAAM,IAAI,CAAC;IAwEvE;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAmB7B"}