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/dist/sql.d.ts ADDED
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Identifier-safety helpers. All values flow through parameterized queries;
3
+ * only identifiers (schema, channel) are interpolated, and only after they
4
+ * have been validated against a strict allowlist and double-quoted.
5
+ */
6
+ /**
7
+ * Validate a PostgreSQL schema name against `^[a-z_][a-z0-9_]*$`.
8
+ *
9
+ * @param schema - Candidate schema name.
10
+ * @returns The same schema name when valid.
11
+ * @throws {Error} When the name does not match the allowlist pattern.
12
+ */
13
+ export declare function assertValidSchema(schema: string): string;
14
+ /**
15
+ * Quote a validated identifier for safe interpolation into SQL text.
16
+ *
17
+ * @param identifier - An identifier already proven safe by validation.
18
+ * @returns The double-quoted identifier.
19
+ */
20
+ export declare function quoteIdentifier(identifier: string): string;
21
+ /**
22
+ * Derive the deterministic `LISTEN/NOTIFY` channel name for a schema. The
23
+ * schema is pre-validated, so the result is always a legal identifier.
24
+ *
25
+ * @param schema - The validated schema name.
26
+ * @returns The notify channel name.
27
+ */
28
+ export declare function notifyChannel(schema: string): string;
29
+ /**
30
+ * A stable 64-bit advisory lock key derived from a string, used to serialize
31
+ * lazy migrations across instances. FNV-1a folded into the signed BIGINT range
32
+ * accepted by `pg_advisory_lock`.
33
+ *
34
+ * @param input - Seed string (the schema name).
35
+ * @returns A bigint in the signed 64-bit range.
36
+ */
37
+ export declare function advisoryLockKey(input: string): bigint;
38
+ //# sourceMappingURL=sql.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sql.d.ts","sourceRoot":"","sources":["../src/sql.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAOxD;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CASrD"}
package/dist/sql.js ADDED
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Identifier-safety helpers. All values flow through parameterized queries;
3
+ * only identifiers (schema, channel) are interpolated, and only after they
4
+ * have been validated against a strict allowlist and double-quoted.
5
+ */
6
+ const SCHEMA_PATTERN = /^[a-z_][a-z0-9_]*$/;
7
+ /**
8
+ * Validate a PostgreSQL schema name against `^[a-z_][a-z0-9_]*$`.
9
+ *
10
+ * @param schema - Candidate schema name.
11
+ * @returns The same schema name when valid.
12
+ * @throws {Error} When the name does not match the allowlist pattern.
13
+ */
14
+ export function assertValidSchema(schema) {
15
+ if (!SCHEMA_PATTERN.test(schema)) {
16
+ throw new Error(`Invalid schema name ${JSON.stringify(schema)}: must match ${SCHEMA_PATTERN.source}`);
17
+ }
18
+ return schema;
19
+ }
20
+ /**
21
+ * Quote a validated identifier for safe interpolation into SQL text.
22
+ *
23
+ * @param identifier - An identifier already proven safe by validation.
24
+ * @returns The double-quoted identifier.
25
+ */
26
+ export function quoteIdentifier(identifier) {
27
+ return `"${identifier.replace(/"/g, '""')}"`;
28
+ }
29
+ /**
30
+ * Derive the deterministic `LISTEN/NOTIFY` channel name for a schema. The
31
+ * schema is pre-validated, so the result is always a legal identifier.
32
+ *
33
+ * @param schema - The validated schema name.
34
+ * @returns The notify channel name.
35
+ */
36
+ export function notifyChannel(schema) {
37
+ return `${schema}_events`;
38
+ }
39
+ /**
40
+ * A stable 64-bit advisory lock key derived from a string, used to serialize
41
+ * lazy migrations across instances. FNV-1a folded into the signed BIGINT range
42
+ * accepted by `pg_advisory_lock`.
43
+ *
44
+ * @param input - Seed string (the schema name).
45
+ * @returns A bigint in the signed 64-bit range.
46
+ */
47
+ export function advisoryLockKey(input) {
48
+ let hash = 1469598103934665603n;
49
+ const prime = 1099511628211n;
50
+ const mask = (1n << 64n) - 1n;
51
+ for (let i = 0; i < input.length; i++) {
52
+ hash ^= BigInt(input.charCodeAt(i));
53
+ hash = (hash * prime) & mask;
54
+ }
55
+ return hash >= 1n << 63n ? hash - (1n << 64n) : hash;
56
+ }
57
+ //# sourceMappingURL=sql.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sql.js","sourceRoot":"","sources":["../src/sql.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,cAAc,GAAG,oBAAoB,CAAC;AAE5C;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAAC,MAAc;IAC9C,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;QACjC,MAAM,IAAI,KAAK,CACb,uBAAuB,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,gBAAgB,cAAc,CAAC,MAAM,EAAE,CACrF,CAAC;IACJ,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,UAAkB;IAChD,OAAO,IAAI,UAAU,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC;AAC/C,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAAC,MAAc;IAC1C,OAAO,GAAG,MAAM,SAAS,CAAC;AAC5B,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAAC,KAAa;IAC3C,IAAI,IAAI,GAAG,oBAAoB,CAAC;IAChC,MAAM,KAAK,GAAG,cAAc,CAAC;IAC7B,MAAM,IAAI,GAAG,CAAC,EAAE,IAAI,GAAG,CAAC,GAAG,EAAE,CAAC;IAC9B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACtC,IAAI,IAAI,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;QACpC,IAAI,GAAG,CAAC,IAAI,GAAG,KAAK,CAAC,GAAG,IAAI,CAAC;IAC/B,CAAC;IACD,OAAO,IAAI,IAAI,EAAE,IAAI,GAAG,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,EAAE,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AACvD,CAAC"}
@@ -0,0 +1,119 @@
1
+ import type { Pool } from 'pg';
2
+ /**
3
+ * A minimal log function. Compatible with `console.log`-style signatures and
4
+ * with structured loggers that accept a message plus arbitrary context.
5
+ */
6
+ export type LogFn = (message: string, ...context: unknown[]) => void;
7
+ /**
8
+ * Optional logger injected into {@link PostgresPubSub}. Every method is
9
+ * optional; when the whole logger (or an individual method) is absent the
10
+ * adapter stays silent. Expected races (lost claims, redelivery) log at
11
+ * `debug`; recoverable problems at `warn`; unexpected failures at `error`.
12
+ */
13
+ export interface PubSubLogger {
14
+ /** Verbose, expected-path diagnostics (claim races, wakeups, polls). */
15
+ debug?: LogFn;
16
+ /** Recoverable anomalies (dropped events, exhausted retries, stale prune). */
17
+ warn?: LogFn;
18
+ /** Unexpected failures inside background loops. */
19
+ error?: LogFn;
20
+ }
21
+ /**
22
+ * Configuration for {@link PostgresPubSub}.
23
+ *
24
+ * Provide exactly one of {@link PostgresPubSubConfig.connectionString} or
25
+ * {@link PostgresPubSubConfig.pool}. A pool supplied here is owned by the
26
+ * caller and is never ended by {@link PostgresPubSub.close}.
27
+ */
28
+ export interface PostgresPubSubConfig {
29
+ /**
30
+ * PostgreSQL connection string. The adapter creates and owns the pool, and
31
+ * ends it on {@link PostgresPubSub.close}. Mutually exclusive with
32
+ * {@link PostgresPubSubConfig.pool}.
33
+ */
34
+ connectionString?: string;
35
+ /**
36
+ * Bring-your-own `pg.Pool`. The adapter never ends it on
37
+ * {@link PostgresPubSub.close}. Mutually exclusive with
38
+ * {@link PostgresPubSubConfig.connectionString}.
39
+ */
40
+ pool?: Pool;
41
+ /**
42
+ * PostgreSQL schema that holds every table. Must match
43
+ * `^[a-z_][a-z0-9_]*$`. Defaults to `mastra_pubsub`.
44
+ */
45
+ schema?: string;
46
+ /**
47
+ * Backstop polling interval in milliseconds for each consume loop. Also
48
+ * bounds how quickly visibility-timeout redeliveries are noticed.
49
+ * Defaults to `1000`.
50
+ */
51
+ pollIntervalMs?: number;
52
+ /**
53
+ * Visibility timeout in milliseconds. A claimed delivery becomes visible
54
+ * again this long after it was claimed unless acked or nacked, providing
55
+ * crash safety. Defaults to `30_000`.
56
+ */
57
+ ackDeadlineMs?: number;
58
+ /**
59
+ * Delay in milliseconds before a nacked delivery becomes visible again.
60
+ * Defaults to `0` (immediate redelivery).
61
+ */
62
+ nackDelayMs?: number;
63
+ /**
64
+ * Maximum delivery attempts before an event is dropped (and optionally
65
+ * dead-lettered). `Infinity` disables the cap. `0` is treated as `Infinity`
66
+ * with a one-time warning. Defaults to `5`.
67
+ */
68
+ maxDeliveryAttempts?: number;
69
+ /** Number of deliveries claimed per loop iteration. Defaults to `32`. */
70
+ batchSize?: number;
71
+ /**
72
+ * Retention cap per topic. Maintenance trims a topic's events down to this
73
+ * many, never deleting events that still have pending deliveries. `0` keeps
74
+ * everything. Defaults to `10_000`.
75
+ */
76
+ maxEventsPerTopic?: number;
77
+ /**
78
+ * Maintenance interval in milliseconds (retention trim + stale private
79
+ * subscription pruning). `0` disables maintenance. Defaults to `60_000`.
80
+ */
81
+ cleanupIntervalMs?: number;
82
+ /**
83
+ * Age in milliseconds after which a private subscription whose
84
+ * `last_seen_at` heartbeat has not advanced is pruned. Defaults to
85
+ * `300_000`.
86
+ */
87
+ staleSubscriptionMs?: number;
88
+ /**
89
+ * Enable `LISTEN/NOTIFY` wakeups for low latency. When `false`, the adapter
90
+ * relies purely on {@link PostgresPubSubConfig.pollIntervalMs} polling.
91
+ * Defaults to `true`.
92
+ */
93
+ listen?: boolean;
94
+ /**
95
+ * Copy events that exhaust their delivery attempts into a `dead_events`
96
+ * table instead of discarding them silently. Defaults to `false`.
97
+ */
98
+ deadLetter?: boolean;
99
+ /** Optional logger; silent when omitted. */
100
+ logger?: PubSubLogger;
101
+ }
102
+ /**
103
+ * Fully-resolved configuration with every default applied. Internal.
104
+ */
105
+ export interface ResolvedConfig {
106
+ schema: string;
107
+ pollIntervalMs: number;
108
+ ackDeadlineMs: number;
109
+ nackDelayMs: number;
110
+ maxDeliveryAttempts: number;
111
+ batchSize: number;
112
+ maxEventsPerTopic: number;
113
+ cleanupIntervalMs: number;
114
+ staleSubscriptionMs: number;
115
+ listen: boolean;
116
+ deadLetter: boolean;
117
+ logger: PubSubLogger;
118
+ }
119
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,IAAI,CAAC;AAE/B;;;GAGG;AACH,MAAM,MAAM,KAAK,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC;AAErE;;;;;GAKG;AACH,MAAM,WAAW,YAAY;IAC3B,wEAAwE;IACxE,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,8EAA8E;IAC9E,IAAI,CAAC,EAAE,KAAK,CAAC;IACb,mDAAmD;IACnD,KAAK,CAAC,EAAE,KAAK,CAAC;CACf;AAED;;;;;;GAMG;AACH,MAAM,WAAW,oBAAoB;IACnC;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;OAIG;IACH,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,yEAAyE;IACzE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B;;;;OAIG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,4CAA4C;IAC5C,MAAM,CAAC,EAAE,YAAY,CAAC;CACvB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,MAAM,EAAE,OAAO,CAAC;IAChB,UAAU,EAAE,OAAO,CAAC;IACpB,MAAM,EAAE,YAAY,CAAC;CACtB"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "mastra-pg-pubsub",
3
+ "version": "0.1.0",
4
+ "description": "PostgreSQL-backed PubSub adapter for Mastra: at-least-once delivery, consumer groups, replay from offset, dead-lettering, and low-latency LISTEN/NOTIFY wakeups.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Alan Hoffmeister",
8
+ "homepage": "https://github.com/alanhoff/mastra-pg-pubsub#readme",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/alanhoff/mastra-pg-pubsub.git"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/alanhoff/mastra-pg-pubsub/issues"
15
+ },
16
+ "keywords": [
17
+ "mastra",
18
+ "pubsub",
19
+ "postgres",
20
+ "postgresql",
21
+ "pg",
22
+ "events",
23
+ "message-queue",
24
+ "listen-notify",
25
+ "consumer-groups",
26
+ "at-least-once"
27
+ ],
28
+ "engines": {
29
+ "node": ">=22.13.0"
30
+ },
31
+ "sideEffects": false,
32
+ "files": [
33
+ "dist"
34
+ ],
35
+ "exports": {
36
+ ".": {
37
+ "types": "./dist/index.d.ts",
38
+ "import": "./dist/index.js"
39
+ }
40
+ },
41
+ "scripts": {
42
+ "build": "tsc -p tsconfig.build.json",
43
+ "typecheck": "tsc -p tsconfig.json --noEmit",
44
+ "lint": "biome check .",
45
+ "lint:fix": "biome check --write .",
46
+ "format": "biome format --write .",
47
+ "test": "node --test 'test/*.test.ts'",
48
+ "test:coverage": "node --test --experimental-test-coverage --test-coverage-include='src/**' --test-coverage-lines=95 --test-coverage-functions=95 --test-coverage-branches=90 'test/*.test.ts'",
49
+ "db:up": "docker compose up -d --wait",
50
+ "db:down": "docker compose down -v",
51
+ "test:e2e": "node --env-file-if-exists=.env --test 'test/e2e/*.test.ts'"
52
+ },
53
+ "dependencies": {
54
+ "pg": "^8.16.0",
55
+ "@types/pg": "^8.15.0"
56
+ },
57
+ "peerDependencies": {
58
+ "@mastra/core": ">=1.13.0-0 <2.0.0-0"
59
+ },
60
+ "devDependencies": {
61
+ "@biomejs/biome": "^2.0.0",
62
+ "@mastra/core": "^1.42.0",
63
+ "@mastra/memory": "^1.20.3",
64
+ "@mastra/pg": "^1.13.0",
65
+ "@types/node": "^22.13.0",
66
+ "typescript": "^5.7.0",
67
+ "zod": "^4.4.3"
68
+ }
69
+ }