@agendajs/postgres-backend 1.0.0-alpha.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.
@@ -0,0 +1,170 @@
1
+ import debug from 'debug';
2
+ import { Pool } from 'pg';
3
+ import { BaseNotificationChannel, toJobId } from 'agenda';
4
+ const log = debug('agenda:postgres:notification');
5
+ /**
6
+ * PostgreSQL notification channel using LISTEN/NOTIFY
7
+ *
8
+ * This implementation uses PostgreSQL's built-in pub/sub mechanism
9
+ * for real-time job notifications across multiple processes.
10
+ */
11
+ export class PostgresNotificationChannel extends BaseNotificationChannel {
12
+ pool;
13
+ ownPool = false;
14
+ connectionString;
15
+ listenClient;
16
+ constructor(config = {}) {
17
+ super(config);
18
+ if (config.pool) {
19
+ this.pool = config.pool;
20
+ this.ownPool = false;
21
+ }
22
+ else if (config.connectionString) {
23
+ this.connectionString = config.connectionString;
24
+ this.ownPool = true;
25
+ }
26
+ }
27
+ /**
28
+ * Set the pool (used when created by PostgresBackend)
29
+ */
30
+ setPool(pool) {
31
+ if (this.pool && this.ownPool) {
32
+ throw new Error('Cannot set pool on channel with own pool');
33
+ }
34
+ this.pool = pool;
35
+ this.ownPool = false;
36
+ }
37
+ async connect() {
38
+ log('connecting notification channel');
39
+ this.clearReconnect();
40
+ try {
41
+ // Create own pool if needed
42
+ if (!this.pool && this.connectionString) {
43
+ this.pool = new Pool({ connectionString: this.connectionString });
44
+ this.ownPool = true;
45
+ }
46
+ if (!this.pool) {
47
+ throw new Error('PostgresNotificationChannel requires pool or connectionString');
48
+ }
49
+ // Get a dedicated connection for LISTEN
50
+ this.listenClient = await this.pool.connect();
51
+ // Set up notification handler
52
+ this.listenClient.on('notification', msg => {
53
+ if (msg.channel === this.config.channelName && msg.payload) {
54
+ try {
55
+ const notification = this.parseNotification(msg.payload);
56
+ log('received notification: %O', notification);
57
+ this.notifyHandlers(notification).catch(err => {
58
+ this.emit('error', err);
59
+ });
60
+ }
61
+ catch (error) {
62
+ log('error parsing notification: %O', error);
63
+ this.emit('error', error);
64
+ }
65
+ }
66
+ });
67
+ // Handle connection errors
68
+ this.listenClient.on('error', error => {
69
+ log('listen client error: %O', error);
70
+ this.emit('error', error);
71
+ // Try to reconnect if the connection was lost
72
+ if (this._state === 'connected') {
73
+ this.handleConnectionLoss();
74
+ }
75
+ });
76
+ // Start listening
77
+ await this.listenClient.query(`LISTEN "${this.config.channelName}"`);
78
+ log('listening on channel: %s', this.config.channelName);
79
+ this.setState('connected');
80
+ }
81
+ catch (error) {
82
+ log('connection failed: %O', error);
83
+ this.emit('error', error);
84
+ this.scheduleReconnect();
85
+ throw error;
86
+ }
87
+ }
88
+ handleConnectionLoss() {
89
+ log('handling connection loss');
90
+ // Clean up the listen client
91
+ if (this.listenClient) {
92
+ try {
93
+ this.listenClient.release(true); // Destroy the client
94
+ }
95
+ catch {
96
+ // Ignore release errors
97
+ }
98
+ this.listenClient = undefined;
99
+ }
100
+ this.scheduleReconnect();
101
+ }
102
+ async disconnect() {
103
+ log('disconnecting notification channel');
104
+ this.clearReconnect();
105
+ if (this.listenClient) {
106
+ try {
107
+ await this.listenClient.query(`UNLISTEN "${this.config.channelName}"`);
108
+ }
109
+ catch {
110
+ // Ignore UNLISTEN errors (connection might be dead)
111
+ }
112
+ try {
113
+ this.listenClient.release();
114
+ }
115
+ catch {
116
+ // Ignore release errors
117
+ }
118
+ this.listenClient = undefined;
119
+ }
120
+ // Only close pool if we created it
121
+ if (this.ownPool && this.pool) {
122
+ await this.pool.end();
123
+ this.pool = undefined;
124
+ }
125
+ this.handlers.clear();
126
+ this.setState('disconnected');
127
+ }
128
+ async publish(notification) {
129
+ if (this._state !== 'connected') {
130
+ throw new Error('Cannot publish: channel not connected');
131
+ }
132
+ if (!this.pool) {
133
+ throw new Error('Cannot publish: no pool available');
134
+ }
135
+ const payload = this.serializeNotification(notification);
136
+ log('publishing notification: %s', payload);
137
+ // NOTIFY doesn't support parameterized queries, so we must escape the payload
138
+ // PostgreSQL escapes single quotes by doubling them
139
+ const escapedPayload = payload.replace(/'/g, "''");
140
+ await this.pool.query(`NOTIFY "${this.config.channelName}", '${escapedPayload}'`);
141
+ }
142
+ /**
143
+ * Serialize notification to JSON string for NOTIFY payload
144
+ */
145
+ serializeNotification(notification) {
146
+ return JSON.stringify({
147
+ jobId: notification.jobId,
148
+ jobName: notification.jobName,
149
+ nextRunAt: notification.nextRunAt?.toISOString() || null,
150
+ priority: notification.priority,
151
+ timestamp: notification.timestamp.toISOString(),
152
+ source: notification.source
153
+ });
154
+ }
155
+ /**
156
+ * Parse notification from JSON string received via LISTEN
157
+ */
158
+ parseNotification(payload) {
159
+ const data = JSON.parse(payload);
160
+ return {
161
+ jobId: toJobId(data.jobId),
162
+ jobName: data.jobName,
163
+ nextRunAt: data.nextRunAt ? new Date(data.nextRunAt) : null,
164
+ priority: data.priority,
165
+ timestamp: new Date(data.timestamp),
166
+ source: data.source
167
+ };
168
+ }
169
+ }
170
+ //# sourceMappingURL=PostgresNotificationChannel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PostgresNotificationChannel.js","sourceRoot":"","sources":["../src/PostgresNotificationChannel.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * @agendajs/postgres-backend
3
+ *
4
+ * PostgreSQL backend for Agenda job scheduler with LISTEN/NOTIFY support.
5
+ *
6
+ * @example
7
+ * ```typescript
8
+ * import { Agenda } from 'agenda';
9
+ * import { PostgresBackend } from '@agendajs/postgres-backend';
10
+ *
11
+ * // Create agenda with PostgreSQL backend
12
+ * const agenda = new Agenda({
13
+ * backend: new PostgresBackend({
14
+ * connectionString: 'postgresql://user:pass@localhost:5432/mydb'
15
+ * })
16
+ * });
17
+ *
18
+ * // Define and run jobs
19
+ * agenda.define('myJob', async (job) => {
20
+ * console.log('Running job:', job.attrs.name);
21
+ * });
22
+ *
23
+ * await agenda.start();
24
+ * await agenda.every('5 minutes', 'myJob');
25
+ * ```
26
+ */
27
+ export { PostgresBackend } from './PostgresBackend.js';
28
+ export { PostgresJobRepository } from './PostgresJobRepository.js';
29
+ export { PostgresNotificationChannel } from './PostgresNotificationChannel.js';
30
+ export type { PostgresBackendConfig, PostgresJobRow } from './types.js';
31
+ export type { PostgresNotificationChannelConfig } from './PostgresNotificationChannel.js';
32
+ export { getCreateTableSQL, getCreateIndexesSQL, getDropTableSQL, getUpdateTimestampTriggerSQL } from './schema.js';
package/dist/index.js ADDED
@@ -0,0 +1,32 @@
1
+ /**
2
+ * @agendajs/postgres-backend
3
+ *
4
+ * PostgreSQL backend for Agenda job scheduler with LISTEN/NOTIFY support.
5
+ *
6
+ * @example
7
+ * ```typescript
8
+ * import { Agenda } from 'agenda';
9
+ * import { PostgresBackend } from '@agendajs/postgres-backend';
10
+ *
11
+ * // Create agenda with PostgreSQL backend
12
+ * const agenda = new Agenda({
13
+ * backend: new PostgresBackend({
14
+ * connectionString: 'postgresql://user:pass@localhost:5432/mydb'
15
+ * })
16
+ * });
17
+ *
18
+ * // Define and run jobs
19
+ * agenda.define('myJob', async (job) => {
20
+ * console.log('Running job:', job.attrs.name);
21
+ * });
22
+ *
23
+ * await agenda.start();
24
+ * await agenda.every('5 minutes', 'myJob');
25
+ * ```
26
+ */
27
+ export { PostgresBackend } from './PostgresBackend.js';
28
+ export { PostgresJobRepository } from './PostgresJobRepository.js';
29
+ export { PostgresNotificationChannel } from './PostgresNotificationChannel.js';
30
+ // Re-export schema utilities for advanced use cases
31
+ export { getCreateTableSQL, getCreateIndexesSQL, getDropTableSQL, getUpdateTimestampTriggerSQL } from './schema.js';
32
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACvD,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AACnE,OAAO,EAAE,2BAA2B,EAAE,MAAM,kCAAkC,CAAC;AAK/E,oDAAoD;AACpD,OAAO,EACN,iBAAiB,EACjB,mBAAmB,EACnB,eAAe,EACf,4BAA4B,EAC5B,MAAM,aAAa,CAAC"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * SQL schema for PostgreSQL Agenda jobs table
3
+ */
4
+ export declare function getCreateTableSQL(tableName: string): string;
5
+ export declare function getCreateIndexesSQL(tableName: string): string[];
6
+ export declare function getDropTableSQL(tableName: string): string;
7
+ /**
8
+ * Function and trigger for automatic updated_at timestamp
9
+ */
10
+ export declare function getUpdateTimestampTriggerSQL(tableName: string): string;
package/dist/schema.js ADDED
@@ -0,0 +1,72 @@
1
+ /**
2
+ * SQL schema for PostgreSQL Agenda jobs table
3
+ */
4
+ export function getCreateTableSQL(tableName) {
5
+ return `
6
+ CREATE TABLE IF NOT EXISTS "${tableName}" (
7
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
8
+ name VARCHAR(255) NOT NULL,
9
+ priority INTEGER NOT NULL DEFAULT 0,
10
+ next_run_at TIMESTAMPTZ,
11
+ type VARCHAR(10) NOT NULL DEFAULT 'normal' CHECK (type IN ('normal', 'single')),
12
+ locked_at TIMESTAMPTZ,
13
+ last_finished_at TIMESTAMPTZ,
14
+ failed_at TIMESTAMPTZ,
15
+ fail_count INTEGER DEFAULT NULL,
16
+ fail_reason TEXT,
17
+ repeat_timezone VARCHAR(100),
18
+ last_run_at TIMESTAMPTZ,
19
+ repeat_interval VARCHAR(255),
20
+ data JSONB DEFAULT '{}'::jsonb,
21
+ repeat_at VARCHAR(255),
22
+ disabled BOOLEAN DEFAULT FALSE,
23
+ progress REAL,
24
+ fork BOOLEAN DEFAULT FALSE,
25
+ last_modified_by VARCHAR(255),
26
+ created_at TIMESTAMPTZ DEFAULT NOW(),
27
+ updated_at TIMESTAMPTZ DEFAULT NOW()
28
+ );
29
+ `;
30
+ }
31
+ export function getCreateIndexesSQL(tableName) {
32
+ return [
33
+ // Main index for finding and locking next job
34
+ `CREATE INDEX IF NOT EXISTS "${tableName}_find_and_lock_idx"
35
+ ON "${tableName}" (name, next_run_at, priority DESC, locked_at, disabled)
36
+ WHERE disabled = FALSE`,
37
+ // Index for single jobs (upsert operations)
38
+ `CREATE UNIQUE INDEX IF NOT EXISTS "${tableName}_single_job_idx"
39
+ ON "${tableName}" (name) WHERE type = 'single'`,
40
+ // Index for querying by locked_at (for stale lock detection)
41
+ `CREATE INDEX IF NOT EXISTS "${tableName}_locked_at_idx"
42
+ ON "${tableName}" (locked_at) WHERE locked_at IS NOT NULL`,
43
+ // Index for next_run_at queries (queue size, scheduled jobs)
44
+ `CREATE INDEX IF NOT EXISTS "${tableName}_next_run_at_idx"
45
+ ON "${tableName}" (next_run_at) WHERE next_run_at IS NOT NULL`
46
+ ];
47
+ }
48
+ export function getDropTableSQL(tableName) {
49
+ return `DROP TABLE IF EXISTS "${tableName}" CASCADE;`;
50
+ }
51
+ /**
52
+ * Function and trigger for automatic updated_at timestamp
53
+ */
54
+ export function getUpdateTimestampTriggerSQL(tableName) {
55
+ return `
56
+ CREATE OR REPLACE FUNCTION update_${tableName}_updated_at()
57
+ RETURNS TRIGGER AS $$
58
+ BEGIN
59
+ NEW.updated_at = NOW();
60
+ RETURN NEW;
61
+ END;
62
+ $$ LANGUAGE plpgsql;
63
+
64
+ DROP TRIGGER IF EXISTS "${tableName}_updated_at_trigger" ON "${tableName}";
65
+
66
+ CREATE TRIGGER "${tableName}_updated_at_trigger"
67
+ BEFORE UPDATE ON "${tableName}"
68
+ FOR EACH ROW
69
+ EXECUTE FUNCTION update_${tableName}_updated_at();
70
+ `;
71
+ }
72
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,UAAU,iBAAiB,CAAC,SAAiB;IAClD,OAAO;gCACwB,SAAS;;;;;;;;;;;;;;;;;;;;;;;EAuBvC,CAAC;AACH,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,SAAiB;IACpD,OAAO;QACN,8CAA8C;QAC9C,+BAA+B,SAAS;SACjC,SAAS;0BACQ;QAExB,4CAA4C;QAC5C,sCAAsC,SAAS;SACxC,SAAS,gCAAgC;QAEhD,6DAA6D;QAC7D,+BAA+B,SAAS;SACjC,SAAS,2CAA2C;QAE3D,6DAA6D;QAC7D,+BAA+B,SAAS;SACjC,SAAS,+CAA+C;KAC/D,CAAC;AACH,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,SAAiB;IAChD,OAAO,yBAAyB,SAAS,YAAY,CAAC;AACvD,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,4BAA4B,CAAC,SAAiB;IAC7D,OAAO;sCAC8B,SAAS;;;;;;;;4BAQnB,SAAS,4BAA4B,SAAS;;oBAEtD,SAAS;sBACP,SAAS;;4BAEH,SAAS;EACnC,CAAC;AACH,CAAC"}
@@ -0,0 +1,52 @@
1
+ import type { Pool, PoolConfig } from 'pg';
2
+ import type { SortDirection } from 'agenda';
3
+ /**
4
+ * Configuration options for PostgresBackend
5
+ */
6
+ export interface PostgresBackendConfig {
7
+ /** PostgreSQL connection string (e.g., 'postgresql://user:pass@host:5432/db') */
8
+ connectionString?: string;
9
+ /** PostgreSQL pool configuration (creates a new pool) */
10
+ poolConfig?: PoolConfig;
11
+ /** Existing PostgreSQL pool instance (will not be closed on disconnect) */
12
+ pool?: Pool;
13
+ /** Table name for jobs (default: 'agenda_jobs') */
14
+ tableName?: string;
15
+ /** Channel name for LISTEN/NOTIFY (default: 'agenda_jobs') */
16
+ channelName?: string;
17
+ /** Whether to create the table and indexes on connect (default: true) */
18
+ ensureSchema?: boolean;
19
+ /** Sort order for job queries (default: { nextRunAt: 'asc', priority: 'desc' }) */
20
+ sort?: {
21
+ nextRunAt?: SortDirection;
22
+ priority?: SortDirection;
23
+ };
24
+ /** Disable LISTEN/NOTIFY notification channel (default: false) */
25
+ disableNotifications?: boolean;
26
+ }
27
+ /**
28
+ * Internal row type matching PostgreSQL column names
29
+ */
30
+ export interface PostgresJobRow {
31
+ id: string;
32
+ name: string;
33
+ priority: number;
34
+ next_run_at: Date | null;
35
+ type: 'normal' | 'single';
36
+ locked_at: Date | null;
37
+ last_finished_at: Date | null;
38
+ failed_at: Date | null;
39
+ fail_count: number | null;
40
+ fail_reason: string | null;
41
+ repeat_timezone: string | null;
42
+ last_run_at: Date | null;
43
+ repeat_interval: string | null;
44
+ data: unknown;
45
+ repeat_at: string | null;
46
+ disabled: boolean;
47
+ progress: number | null;
48
+ fork: boolean;
49
+ last_modified_by: string | null;
50
+ created_at: Date;
51
+ updated_at: Date;
52
+ }
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,71 @@
1
+ {
2
+ "name": "@agendajs/postgres-backend",
3
+ "version": "1.0.0-alpha.0",
4
+ "description": "PostgreSQL backend for Agenda job scheduler with LISTEN/NOTIFY support",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "publishConfig": {
15
+ "access": "public"
16
+ },
17
+ "files": [
18
+ "dist"
19
+ ],
20
+ "engines": {
21
+ "node": ">=18.0.0"
22
+ },
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git://github.com/agenda/agenda"
26
+ },
27
+ "keywords": [
28
+ "agenda",
29
+ "job",
30
+ "scheduler",
31
+ "postgresql",
32
+ "postgres",
33
+ "pg",
34
+ "listen",
35
+ "notify"
36
+ ],
37
+ "license": "MIT",
38
+ "bugs": {
39
+ "url": "https://github.com/agenda/agenda/issues"
40
+ },
41
+ "dependencies": {
42
+ "debug": "^4.4.0",
43
+ "pg": "^8.13.0"
44
+ },
45
+ "peerDependencies": {
46
+ "agenda": "6.0.0-alpha.0"
47
+ },
48
+ "devDependencies": {
49
+ "@types/debug": "^4.1.12",
50
+ "@types/node": "^22.0.0",
51
+ "@types/pg": "^8.11.0",
52
+ "@vitest/coverage-v8": "^3.1.0",
53
+ "tsx": "^4.21.0",
54
+ "vitest": "^3.1.0",
55
+ "agenda": "6.0.0-alpha.0"
56
+ },
57
+ "scripts": {
58
+ "build": "tsc -b",
59
+ "typecheck": "tsc -b && tsc -p tsconfig.test.json",
60
+ "test": "vitest run",
61
+ "test:watch": "vitest",
62
+ "test:coverage": "vitest run --coverage",
63
+ "test:debug": "DEBUG=agenda:**,-agenda:internal:** vitest run",
64
+ "test:postgres": "POSTGRES_TEST_URL=postgresql://agenda:agenda@localhost:5432/agenda_test vitest run",
65
+ "test:postgres:watch": "POSTGRES_TEST_URL=postgresql://agenda:agenda@localhost:5432/agenda_test vitest",
66
+ "docker:up": "docker compose up -d --wait",
67
+ "docker:down": "docker compose down",
68
+ "docker:logs": "docker compose logs -f",
69
+ "test:docker": "npm run docker:up && npm run test:postgres; npm run docker:down"
70
+ }
71
+ }