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 +21 -0
- package/README.md +168 -0
- package/dist/consume-loop.d.ts +46 -0
- package/dist/consume-loop.d.ts.map +1 -0
- package/dist/consume-loop.js +262 -0
- package/dist/consume-loop.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/listener.d.ts +31 -0
- package/dist/listener.d.ts.map +1 -0
- package/dist/listener.js +126 -0
- package/dist/listener.js.map +1 -0
- package/dist/postgres-pubsub.d.ts +99 -0
- package/dist/postgres-pubsub.d.ts.map +1 -0
- package/dist/postgres-pubsub.js +499 -0
- package/dist/postgres-pubsub.js.map +1 -0
- package/dist/schema.d.ts +21 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +97 -0
- package/dist/schema.js.map +1 -0
- package/dist/sql.d.ts +38 -0
- package/dist/sql.d.ts.map +1 -0
- package/dist/sql.js +57 -0
- package/dist/sql.js.map +1 -0
- package/dist/types.d.ts +119 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +69 -0
package/dist/listener.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { notifyChannel, quoteIdentifier } from "./sql.js";
|
|
2
|
+
/**
|
|
3
|
+
* Owns a single dedicated `LISTEN` connection and dispatches `NOTIFY` payloads
|
|
4
|
+
* (topic names) to registered per-topic wakeup handlers. Reconnects on
|
|
5
|
+
* connection loss so wakeups survive transient database blips; polling remains
|
|
6
|
+
* the correctness backstop in the meantime.
|
|
7
|
+
*/
|
|
8
|
+
export class NotifyListener {
|
|
9
|
+
#pool;
|
|
10
|
+
#channel;
|
|
11
|
+
#logger;
|
|
12
|
+
#handlers = new Map();
|
|
13
|
+
#client;
|
|
14
|
+
#connecting;
|
|
15
|
+
#closed = false;
|
|
16
|
+
/**
|
|
17
|
+
* @param pool - Pool used to acquire the dedicated listen connection.
|
|
18
|
+
* @param schema - Validated schema name; determines the channel.
|
|
19
|
+
* @param logger - Logger for connection diagnostics.
|
|
20
|
+
*/
|
|
21
|
+
constructor(pool, schema, logger) {
|
|
22
|
+
this.#pool = pool;
|
|
23
|
+
this.#channel = notifyChannel(schema);
|
|
24
|
+
this.#logger = logger;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Register a wakeup handler for a topic, ensuring the listen connection is
|
|
28
|
+
* established. Returns an unregister function.
|
|
29
|
+
*
|
|
30
|
+
* @param topic - The topic to wake on.
|
|
31
|
+
* @param handler - Invoked when a `NOTIFY` for the topic arrives.
|
|
32
|
+
* @returns A function that removes this handler.
|
|
33
|
+
*/
|
|
34
|
+
async register(topic, handler) {
|
|
35
|
+
let set = this.#handlers.get(topic);
|
|
36
|
+
if (!set) {
|
|
37
|
+
set = new Set();
|
|
38
|
+
this.#handlers.set(topic, set);
|
|
39
|
+
}
|
|
40
|
+
set.add(handler);
|
|
41
|
+
await this.#ensureConnected();
|
|
42
|
+
return () => {
|
|
43
|
+
const current = this.#handlers.get(topic);
|
|
44
|
+
if (current) {
|
|
45
|
+
current.delete(handler);
|
|
46
|
+
if (current.size === 0) {
|
|
47
|
+
this.#handlers.delete(topic);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
async #ensureConnected() {
|
|
53
|
+
if (this.#closed || this.#client) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (!this.#connecting) {
|
|
57
|
+
this.#connecting = this.#connect().finally(() => {
|
|
58
|
+
this.#connecting = undefined;
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return this.#connecting;
|
|
62
|
+
}
|
|
63
|
+
async #connect() {
|
|
64
|
+
const client = await this.#pool.connect();
|
|
65
|
+
client.on('notification', (msg) => {
|
|
66
|
+
if (msg.payload === undefined) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const handlers = this.#handlers.get(msg.payload);
|
|
70
|
+
if (handlers) {
|
|
71
|
+
for (const handler of handlers) {
|
|
72
|
+
handler();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
client.on('error', (error) => {
|
|
77
|
+
this.#logger.debug?.('listen connection error', error);
|
|
78
|
+
this.#handleDisconnect(client);
|
|
79
|
+
});
|
|
80
|
+
await client.query(`LISTEN ${quoteIdentifier(this.#channel)}`);
|
|
81
|
+
if (this.#closed) {
|
|
82
|
+
client.removeAllListeners('notification');
|
|
83
|
+
client.release();
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
this.#client = client;
|
|
87
|
+
}
|
|
88
|
+
#handleDisconnect(client) {
|
|
89
|
+
if (this.#client !== client) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
this.#client = undefined;
|
|
93
|
+
client.removeAllListeners('notification');
|
|
94
|
+
client.release(true);
|
|
95
|
+
if (this.#closed || this.#handlers.size === 0) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
this.#ensureConnected().catch((error) => {
|
|
99
|
+
this.#logger.warn?.('listen reconnect failed', error);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Release the listen connection and stop dispatching. Idempotent.
|
|
104
|
+
*/
|
|
105
|
+
async close() {
|
|
106
|
+
this.#closed = true;
|
|
107
|
+
this.#handlers.clear();
|
|
108
|
+
if (this.#connecting) {
|
|
109
|
+
await this.#connecting.catch(() => undefined);
|
|
110
|
+
}
|
|
111
|
+
const client = this.#client;
|
|
112
|
+
this.#client = undefined;
|
|
113
|
+
if (client) {
|
|
114
|
+
client.removeAllListeners('notification');
|
|
115
|
+
client.removeAllListeners('error');
|
|
116
|
+
try {
|
|
117
|
+
await client.query(`UNLISTEN ${quoteIdentifier(this.#channel)}`);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
// best effort; releasing the client drops the listen anyway
|
|
121
|
+
}
|
|
122
|
+
client.release();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=listener.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"listener.js","sourceRoot":"","sources":["../src/listener.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { Event, EventCallback, SubscribeOptions } from '@mastra/core/events';
|
|
2
|
+
import { PubSub } from '@mastra/core/events';
|
|
3
|
+
import type { PostgresPubSubConfig } from './types.ts';
|
|
4
|
+
/**
|
|
5
|
+
* A PostgreSQL-backed {@link PubSub} implementation for Mastra.
|
|
6
|
+
*
|
|
7
|
+
* Provides at-least-once delivery with ack/nack and visibility timeouts,
|
|
8
|
+
* competing-consumer groups and groupless fan-out, replay addressed by
|
|
9
|
+
* per-topic `index`, optional dead-lettering, and low-latency `LISTEN/NOTIFY`
|
|
10
|
+
* wakeups with polling as the correctness backstop. The only runtime
|
|
11
|
+
* dependency is `pg`; `@mastra/core` is a peer dependency.
|
|
12
|
+
*/
|
|
13
|
+
export declare class PostgresPubSub extends PubSub {
|
|
14
|
+
#private;
|
|
15
|
+
/**
|
|
16
|
+
* @param config - Adapter configuration. Provide exactly one of
|
|
17
|
+
* `connectionString` or `pool`.
|
|
18
|
+
*/
|
|
19
|
+
constructor(config: PostgresPubSubConfig);
|
|
20
|
+
/**
|
|
21
|
+
* Create the schema and tables explicitly. Idempotent and safe to call
|
|
22
|
+
* concurrently across instances (serialized by an advisory lock). When not
|
|
23
|
+
* called, migration happens lazily on first use.
|
|
24
|
+
*/
|
|
25
|
+
migrate(): Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* Publish an event to a topic. Assigns `id`, `createdAt`, and a per-topic
|
|
28
|
+
* `index` inside a single transaction, fans out one delivery row per active
|
|
29
|
+
* subscription, and emits a `NOTIFY` wakeup.
|
|
30
|
+
*
|
|
31
|
+
* @param topic - The topic to publish to.
|
|
32
|
+
* @param event - Event without `id`/`createdAt` (assigned here).
|
|
33
|
+
* @param _options - Accepted for interface compatibility; `localOnly` is
|
|
34
|
+
* ignored because delivery is always database-mediated.
|
|
35
|
+
*/
|
|
36
|
+
publish(topic: string, event: Omit<Event, 'id' | 'createdAt'>, _options?: {
|
|
37
|
+
localOnly?: boolean;
|
|
38
|
+
}): Promise<void>;
|
|
39
|
+
/**
|
|
40
|
+
* Subscribe to a topic. With `options.group`, members compete and each event
|
|
41
|
+
* reaches exactly one member (round-robin across local callbacks). Without a
|
|
42
|
+
* group, the subscription is private to this instance and receives every
|
|
43
|
+
* event (fan-out).
|
|
44
|
+
*
|
|
45
|
+
* @param topic - The topic to subscribe to.
|
|
46
|
+
* @param cb - Callback invoked per delivered event.
|
|
47
|
+
* @param options - Subscribe options; `group` selects competing-consumer
|
|
48
|
+
* semantics. `batch` is ignored (no native batching).
|
|
49
|
+
*/
|
|
50
|
+
subscribe(topic: string, cb: EventCallback, options?: SubscribeOptions): Promise<void>;
|
|
51
|
+
/**
|
|
52
|
+
* Remove a callback from a topic. Tears down the underlying subscription
|
|
53
|
+
* (and, for private subscriptions, deletes its database rows) once no local
|
|
54
|
+
* callbacks remain.
|
|
55
|
+
*
|
|
56
|
+
* @param topic - The topic the callback was subscribed to.
|
|
57
|
+
* @param cb - The callback to remove.
|
|
58
|
+
*/
|
|
59
|
+
unsubscribe(topic: string, cb: EventCallback): Promise<void>;
|
|
60
|
+
/**
|
|
61
|
+
* Resolve once all in-flight publishes and local deliveries have settled.
|
|
62
|
+
* Per-event callback errors are logged, never thrown. If locally-owned
|
|
63
|
+
* deliveries remain unsettled after the bounded drain window, reject so
|
|
64
|
+
* shutdown callers do not mistake a stuck subscriber for a clean drain.
|
|
65
|
+
*/
|
|
66
|
+
flush(): Promise<void>;
|
|
67
|
+
/**
|
|
68
|
+
* Fetch historical events for a topic with `index >= offset`, ordered by
|
|
69
|
+
* `index`.
|
|
70
|
+
*
|
|
71
|
+
* @param topic - The topic to read history for.
|
|
72
|
+
* @param offset - Inclusive starting `index`. Defaults to `0`.
|
|
73
|
+
* @returns Events in ascending `index` order.
|
|
74
|
+
*/
|
|
75
|
+
getHistory(topic: string, offset?: number): Promise<Event[]>;
|
|
76
|
+
/**
|
|
77
|
+
* Subscribe and replay all history first, then continue live, deduping the
|
|
78
|
+
* boundary by event `index` so no event is delivered twice.
|
|
79
|
+
*
|
|
80
|
+
* @param topic - The topic to subscribe to.
|
|
81
|
+
* @param cb - Callback for replayed and live events.
|
|
82
|
+
*/
|
|
83
|
+
subscribeWithReplay(topic: string, cb: EventCallback): Promise<void>;
|
|
84
|
+
/**
|
|
85
|
+
* Subscribe and replay history starting at `offset`, then continue live,
|
|
86
|
+
* deduping the boundary by event `index`.
|
|
87
|
+
*
|
|
88
|
+
* @param topic - The topic to subscribe to.
|
|
89
|
+
* @param offset - Inclusive starting `index` for replay.
|
|
90
|
+
* @param cb - Callback for replayed and live events.
|
|
91
|
+
*/
|
|
92
|
+
subscribeFromOffset(topic: string, offset: number, cb: EventCallback): Promise<void>;
|
|
93
|
+
/**
|
|
94
|
+
* Stop all loops, release the listener, delete this instance's private
|
|
95
|
+
* subscriptions, and end the pool when the adapter created it. Idempotent.
|
|
96
|
+
*/
|
|
97
|
+
close(): Promise<void>;
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=postgres-pubsub.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgres-pubsub.d.ts","sourceRoot":"","sources":["../src/postgres-pubsub.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAClF,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAM7C,OAAO,KAAK,EAAE,oBAAoB,EAAgC,MAAM,YAAY,CAAC;AAqErF;;;;;;;;GAQG;AACH,qBAAa,cAAe,SAAQ,MAAM;;IAiBxC;;;OAGG;gBACS,MAAM,EAAE,oBAAoB;IA0BxC;;;;OAIG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAe9B;;;;;;;;;OASG;IACY,OAAO,CACpB,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,GAAG,WAAW,CAAC,EACtC,QAAQ,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,OAAO,CAAA;KAAE,GACjC,OAAO,CAAC,IAAI,CAAC;IAkEhB;;;;;;;;;;OAUG;IACY,SAAS,CACtB,KAAK,EAAE,MAAM,EACb,EAAE,EAAE,aAAa,EACjB,OAAO,CAAC,EAAE,gBAAgB,GACzB,OAAO,CAAC,IAAI,CAAC;IAyFhB;;;;;;;OAOG;IACY,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAsC3E;;;;;OAKG;IACY,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IA2BrC;;;;;;;OAOG;IACY,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,SAAI,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC;IAYtE;;;;;;OAMG;IACY,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAInF;;;;;;;OAOG;IACY,mBAAmB,CAChC,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,EAAE,EAAE,aAAa,GAChB,OAAO,CAAC,IAAI,CAAC;IA+HhB;;;OAGG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAmC7B"}
|