@agendajs/redis-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.
package/LICENSE.md ADDED
@@ -0,0 +1,25 @@
1
+ ## License
2
+ (The MIT License)
3
+
4
+ Copyright (c) 2013 Ryan Schmukler <ryan@slingingcode.com>
5
+
6
+ Copyright (c) 2022-2026 Simon Tretter <s.tretter@gmail.com>
7
+
8
+ Contributors: Mikael Korpela, Alexis Tyler, Vasyl Boroviak, Haris Sulaiman, Loris Guignard
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
11
+ this software and associated documentation files (the 'Software'), to deal in
12
+ the Software without restriction, including without limitation the rights to
13
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
14
+ the Software, and to permit persons to whom the Software is furnished to do so,
15
+ subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
22
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
23
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
24
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
25
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,186 @@
1
+ # @agendajs/redis-backend
2
+
3
+ Redis backend for [Agenda](https://github.com/agenda/agenda) job scheduler with Pub/Sub support for real-time job processing.
4
+
5
+ ## Features
6
+
7
+ - Full Redis storage backend for Agenda jobs
8
+ - Real-time job notifications using Redis Pub/Sub
9
+ - Efficient job indexing with sorted sets
10
+ - Atomic job locking with WATCH/MULTI/EXEC
11
+ - Connection pooling via `ioredis` library
12
+ - TypeScript support with full type definitions
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install @agendajs/redis-backend
18
+ # or
19
+ pnpm add @agendajs/redis-backend
20
+ # or
21
+ yarn add @agendajs/redis-backend
22
+ ```
23
+
24
+ ## Usage
25
+
26
+ ### Basic Usage
27
+
28
+ ```typescript
29
+ import { Agenda } from 'agenda';
30
+ import { RedisBackend } from '@agendajs/redis-backend';
31
+
32
+ // Create agenda with Redis backend
33
+ const agenda = new Agenda({
34
+ backend: new RedisBackend({
35
+ connectionString: 'redis://localhost:6379'
36
+ })
37
+ });
38
+
39
+ // Define jobs
40
+ agenda.define('send-email', async (job) => {
41
+ const { to, subject, body } = job.attrs.data;
42
+ await sendEmail(to, subject, body);
43
+ });
44
+
45
+ // Start processing
46
+ await agenda.start();
47
+
48
+ // Schedule jobs
49
+ await agenda.every('5 minutes', 'send-email', { to: 'user@example.com', subject: 'Hello', body: 'World' });
50
+ await agenda.schedule('in 10 minutes', 'send-email', { to: 'other@example.com', subject: 'Hi', body: 'There' });
51
+ ```
52
+
53
+ ### Configuration Options
54
+
55
+ ```typescript
56
+ import { RedisBackend } from '@agendajs/redis-backend';
57
+
58
+ const backend = new RedisBackend({
59
+ // Redis connection string (required unless redis/redisOptions is provided)
60
+ connectionString: 'redis://localhost:6379',
61
+
62
+ // Or use Redis client options (creates a new client)
63
+ redisOptions: {
64
+ host: 'localhost',
65
+ port: 6379,
66
+ password: 'secret',
67
+ db: 0
68
+ },
69
+
70
+ // Key prefix for all Redis keys (default: 'agenda:')
71
+ keyPrefix: 'agenda:',
72
+
73
+ // Channel name for Pub/Sub notifications (default: 'agenda:notifications')
74
+ channelName: 'agenda:notifications',
75
+
76
+ // Name to identify this Agenda instance (stored as lastModifiedBy)
77
+ name: 'worker-1',
78
+
79
+ // Sort order for job queries
80
+ sort: {
81
+ nextRunAt: 1, // 1 for ASC, -1 for DESC
82
+ priority: -1
83
+ }
84
+ });
85
+ ```
86
+
87
+ ### Using an Existing Redis Client
88
+
89
+ If your application already has a Redis client, you can pass it directly. The client will **not** be closed when Agenda disconnects:
90
+
91
+ ```typescript
92
+ import { Redis } from 'ioredis';
93
+ import { RedisBackend } from '@agendajs/redis-backend';
94
+
95
+ // Your app's existing Redis client
96
+ const redis = new Redis('redis://localhost:6379');
97
+
98
+ const backend = new RedisBackend({ redis });
99
+
100
+ // After agenda.stop(), your Redis client is still usable
101
+ ```
102
+
103
+ ## How It Works
104
+
105
+ ### Storage
106
+
107
+ The backend stores jobs using Redis data structures:
108
+
109
+ - **Hashes** (`{prefix}job:{id}`) - Store job data as key-value pairs
110
+ - **Sets** (`{prefix}jobs:all`) - Track all job IDs
111
+ - **Sets** (`{prefix}jobs:by_name:{name}`) - Index jobs by name for fast lookup
112
+ - **Sorted Sets** (`{prefix}jobs:by_next_run_at`) - Index jobs by nextRunAt for efficient scheduling
113
+ - **Strings** (`{prefix}jobs:single:{name}`) - Track single-type jobs for upsert operations
114
+
115
+ ### Real-Time Notifications
116
+
117
+ Redis has built-in pub/sub capabilities. When a job is saved, a notification is published to all subscribing Agenda instances, triggering immediate job processing without waiting for the next poll interval.
118
+
119
+ This means:
120
+ - Lower latency for job execution
121
+ - Reduced polling overhead
122
+ - Efficient cross-process coordination
123
+
124
+ ### Distributed Locking
125
+
126
+ The backend uses Redis WATCH/MULTI/EXEC transactions to atomically lock jobs for processing, preventing duplicate execution across multiple Agenda instances.
127
+
128
+ ## Persistence
129
+
130
+ By default, Redis keeps data in memory. For production use with Agenda, configure Redis persistence:
131
+
132
+ ```bash
133
+ # In redis.conf
134
+ appendonly yes # Enable AOF persistence
135
+ appendfsync everysec # Sync to disk every second
136
+ ```
137
+
138
+ Or use RDB snapshots:
139
+
140
+ ```bash
141
+ save 900 1 # Save after 900 seconds if at least 1 key changed
142
+ save 300 10 # Save after 300 seconds if at least 10 keys changed
143
+ save 60 10000 # Save after 60 seconds if at least 10000 keys changed
144
+ ```
145
+
146
+ For maximum durability, enable both AOF and RDB.
147
+
148
+ ## Testing
149
+
150
+ Tests require a Redis server. The easiest way is to use Docker:
151
+
152
+ ```bash
153
+ # Start Redis container and run tests
154
+ pnpm test:docker
155
+
156
+ # Or manually:
157
+ pnpm docker:up # Start Redis container
158
+ pnpm test # Run tests
159
+ pnpm docker:down # Stop container
160
+ ```
161
+
162
+ You can also use an existing Redis server:
163
+
164
+ ```bash
165
+ REDIS_TEST_URL=redis://localhost:6379 pnpm test
166
+ ```
167
+
168
+ ### Available Scripts
169
+
170
+ | Script | Description |
171
+ |--------|-------------|
172
+ | `pnpm test` | Run tests (requires Redis or Docker) |
173
+ | `pnpm test:docker` | Start container, run tests, stop container |
174
+ | `pnpm docker:up` | Start Redis container |
175
+ | `pnpm docker:down` | Stop Redis container |
176
+ | `pnpm docker:logs` | View container logs |
177
+
178
+ ## Requirements
179
+
180
+ - Node.js >= 18.0.0
181
+ - Redis >= 6.0
182
+ - Agenda >= 6.0.0
183
+
184
+ ## License
185
+
186
+ MIT
@@ -0,0 +1,73 @@
1
+ import type { AgendaBackend, JobRepository, NotificationChannel } from 'agenda';
2
+ import type { RedisBackendConfig } from './types.js';
3
+ /**
4
+ * Redis backend for Agenda
5
+ *
6
+ * Provides both storage (via RedisJobRepository) and real-time notifications
7
+ * (via RedisNotificationChannel using Pub/Sub).
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * import { Agenda } from 'agenda';
12
+ * import { RedisBackend } from '@agendajs/redis-backend';
13
+ *
14
+ * // Using connection string
15
+ * const agenda = new Agenda({
16
+ * backend: new RedisBackend({
17
+ * connectionString: 'redis://localhost:6379'
18
+ * })
19
+ * });
20
+ *
21
+ * // Or using an existing Redis client (e.g., shared with your app)
22
+ * // The client will NOT be closed when Agenda disconnects
23
+ * import Redis from 'ioredis';
24
+ * const redis = new Redis();
25
+ *
26
+ * const agenda = new Agenda({
27
+ * backend: new RedisBackend({ redis })
28
+ * });
29
+ *
30
+ * // Your app can continue using the client after agenda.stop()
31
+ *
32
+ * agenda.define('myJob', async (job) => {
33
+ * console.log('Running job:', job.attrs.name);
34
+ * });
35
+ *
36
+ * await agenda.start();
37
+ * await agenda.every('5 minutes', 'myJob');
38
+ * ```
39
+ */
40
+ export declare class RedisBackend implements AgendaBackend {
41
+ private _repository;
42
+ private _notificationChannel;
43
+ private config;
44
+ private _ownsConnection;
45
+ constructor(config: RedisBackendConfig);
46
+ /**
47
+ * The job repository for storage operations
48
+ */
49
+ get repository(): JobRepository;
50
+ /**
51
+ * The notification channel for real-time notifications via Pub/Sub
52
+ */
53
+ get notificationChannel(): NotificationChannel;
54
+ /**
55
+ * Whether this backend owns its Redis connection.
56
+ * True if created from connectionString/redisOptions, false if redis client was passed in.
57
+ */
58
+ get ownsConnection(): boolean;
59
+ /**
60
+ * Connect to Redis
61
+ *
62
+ * - Establishes Redis connection
63
+ * - Sets up Pub/Sub for real-time notifications
64
+ */
65
+ connect(): Promise<void>;
66
+ /**
67
+ * Disconnect from Redis
68
+ *
69
+ * - Unsubscribes from Pub/Sub channel
70
+ * - Closes Redis connection
71
+ */
72
+ disconnect(): Promise<void>;
73
+ }
@@ -0,0 +1,114 @@
1
+ import debug from 'debug';
2
+ import { RedisJobRepository } from './RedisJobRepository.js';
3
+ import { RedisNotificationChannel } from './RedisNotificationChannel.js';
4
+ const log = debug('agenda:redis:backend');
5
+ /**
6
+ * Redis backend for Agenda
7
+ *
8
+ * Provides both storage (via RedisJobRepository) and real-time notifications
9
+ * (via RedisNotificationChannel using Pub/Sub).
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * import { Agenda } from 'agenda';
14
+ * import { RedisBackend } from '@agendajs/redis-backend';
15
+ *
16
+ * // Using connection string
17
+ * const agenda = new Agenda({
18
+ * backend: new RedisBackend({
19
+ * connectionString: 'redis://localhost:6379'
20
+ * })
21
+ * });
22
+ *
23
+ * // Or using an existing Redis client (e.g., shared with your app)
24
+ * // The client will NOT be closed when Agenda disconnects
25
+ * import Redis from 'ioredis';
26
+ * const redis = new Redis();
27
+ *
28
+ * const agenda = new Agenda({
29
+ * backend: new RedisBackend({ redis })
30
+ * });
31
+ *
32
+ * // Your app can continue using the client after agenda.stop()
33
+ *
34
+ * agenda.define('myJob', async (job) => {
35
+ * console.log('Running job:', job.attrs.name);
36
+ * });
37
+ *
38
+ * await agenda.start();
39
+ * await agenda.every('5 minutes', 'myJob');
40
+ * ```
41
+ */
42
+ export class RedisBackend {
43
+ _repository;
44
+ _notificationChannel;
45
+ config;
46
+ _ownsConnection;
47
+ constructor(config) {
48
+ this.config = config;
49
+ // Determine if we own the connection (not passed in by user)
50
+ this._ownsConnection = !config.redis;
51
+ // Create repository
52
+ this._repository = new RedisJobRepository(config);
53
+ // Create notification channel
54
+ this._notificationChannel = new RedisNotificationChannel({
55
+ channelName: config.channelName,
56
+ reconnect: {
57
+ enabled: true
58
+ }
59
+ });
60
+ log('RedisBackend created with config: %O', {
61
+ keyPrefix: config.keyPrefix || 'agenda:',
62
+ channelName: config.channelName || 'agenda:jobs'
63
+ });
64
+ }
65
+ /**
66
+ * The job repository for storage operations
67
+ */
68
+ get repository() {
69
+ return this._repository;
70
+ }
71
+ /**
72
+ * The notification channel for real-time notifications via Pub/Sub
73
+ */
74
+ get notificationChannel() {
75
+ return this._notificationChannel;
76
+ }
77
+ /**
78
+ * Whether this backend owns its Redis connection.
79
+ * True if created from connectionString/redisOptions, false if redis client was passed in.
80
+ */
81
+ get ownsConnection() {
82
+ return this._ownsConnection;
83
+ }
84
+ /**
85
+ * Connect to Redis
86
+ *
87
+ * - Establishes Redis connection
88
+ * - Sets up Pub/Sub for real-time notifications
89
+ */
90
+ async connect() {
91
+ log('connecting RedisBackend');
92
+ // Connect repository first
93
+ await this._repository.connect();
94
+ // Share the Redis client with notification channel
95
+ this._notificationChannel.setRedis(this._repository.getRedis());
96
+ // Note: notification channel is connected by Agenda.start()
97
+ // when it's needed, so we don't connect it here
98
+ log('RedisBackend connected');
99
+ }
100
+ /**
101
+ * Disconnect from Redis
102
+ *
103
+ * - Unsubscribes from Pub/Sub channel
104
+ * - Closes Redis connection
105
+ */
106
+ async disconnect() {
107
+ log('disconnecting RedisBackend');
108
+ // Notification channel is disconnected by Agenda.stop()
109
+ // Repository disconnect will close the shared client
110
+ await this._repository.disconnect();
111
+ log('RedisBackend disconnected');
112
+ }
113
+ }
114
+ //# sourceMappingURL=RedisBackend.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RedisBackend.js","sourceRoot":"","sources":["../src/RedisBackend.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAC7D,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AAGzE,MAAM,GAAG,GAAG,KAAK,CAAC,sBAAsB,CAAC,CAAC;AAE1C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,MAAM,OAAO,YAAY;IAChB,WAAW,CAAqB;IAChC,oBAAoB,CAA2B;IAC/C,MAAM,CAAqB;IAC3B,eAAe,CAAU;IAEjC,YAAY,MAA0B;QACrC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QAErB,6DAA6D;QAC7D,IAAI,CAAC,eAAe,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC;QAErC,oBAAoB;QACpB,IAAI,CAAC,WAAW,GAAG,IAAI,kBAAkB,CAAC,MAAM,CAAC,CAAC;QAElD,8BAA8B;QAC9B,IAAI,CAAC,oBAAoB,GAAG,IAAI,wBAAwB,CAAC;YACxD,WAAW,EAAE,MAAM,CAAC,WAAW;YAC/B,SAAS,EAAE;gBACV,OAAO,EAAE,IAAI;aACb;SACD,CAAC,CAAC;QAEH,GAAG,CAAC,sCAAsC,EAAE;YAC3C,SAAS,EAAE,MAAM,CAAC,SAAS,IAAI,SAAS;YACxC,WAAW,EAAE,MAAM,CAAC,WAAW,IAAI,aAAa;SAChD,CAAC,CAAC;IACJ,CAAC;IAED;;OAEG;IACH,IAAI,UAAU;QACb,OAAO,IAAI,CAAC,WAAW,CAAC;IACzB,CAAC;IAED;;OAEG;IACH,IAAI,mBAAmB;QACtB,OAAO,IAAI,CAAC,oBAAoB,CAAC;IAClC,CAAC;IAED;;;OAGG;IACH,IAAI,cAAc;QACjB,OAAO,IAAI,CAAC,eAAe,CAAC;IAC7B,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,OAAO;QACZ,GAAG,CAAC,yBAAyB,CAAC,CAAC;QAE/B,2BAA2B;QAC3B,MAAM,IAAI,CAAC,WAAW,CAAC,OAAO,EAAE,CAAC;QAEjC,mDAAmD;QACnD,IAAI,CAAC,oBAAoB,CAAC,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,QAAQ,EAAE,CAAC,CAAC;QAEhE,4DAA4D;QAC5D,gDAAgD;QAEhD,GAAG,CAAC,wBAAwB,CAAC,CAAC;IAC/B,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,UAAU;QACf,GAAG,CAAC,4BAA4B,CAAC,CAAC;QAElC,wDAAwD;QACxD,qDAAqD;QACrD,MAAM,IAAI,CAAC,WAAW,CAAC,UAAU,EAAE,CAAC;QAEpC,GAAG,CAAC,2BAA2B,CAAC,CAAC;IAClC,CAAC;CACD"}
@@ -0,0 +1,75 @@
1
+ import { Redis } from 'ioredis';
2
+ import type { JobRepository, JobRepositoryOptions, RemoveJobsOptions, JobParameters, JobId, JobsQueryOptions, JobsResult, JobsOverview } from 'agenda';
3
+ import type { RedisBackendConfig } from './types.js';
4
+ /**
5
+ * Redis implementation of JobRepository
6
+ *
7
+ * Data structure:
8
+ * - `{prefix}job:{id}` - Hash containing job data
9
+ * - `{prefix}jobs:all` - Set of all job IDs
10
+ * - `{prefix}jobs:by_name:{name}` - Set of job IDs by name
11
+ * - `{prefix}jobs:by_next_run_at` - Sorted set of job IDs by nextRunAt timestamp
12
+ * - `{prefix}jobs:single:{name}` - String storing ID of single-type job for a name
13
+ */
14
+ export declare class RedisJobRepository implements JobRepository {
15
+ private config;
16
+ private redis;
17
+ private ownClient;
18
+ private keyPrefix;
19
+ private sort;
20
+ constructor(config: RedisBackendConfig);
21
+ /**
22
+ * Get the underlying Redis client (for use by notification channel)
23
+ */
24
+ getRedis(): Redis;
25
+ /**
26
+ * Generate Redis key with prefix
27
+ */
28
+ private key;
29
+ connect(): Promise<void>;
30
+ disconnect(): Promise<void>;
31
+ /**
32
+ * Convert Redis hash data to JobParameters
33
+ */
34
+ private hashToJob;
35
+ /**
36
+ * Convert JobParameters to Redis hash data
37
+ */
38
+ private jobToHash;
39
+ /**
40
+ * Get score for sorted set based on nextRunAt and priority
41
+ * Score format: timestamp.priority (e.g., 1234567890.005 for priority 5)
42
+ */
43
+ private getJobScore;
44
+ getJobById(id: string): Promise<JobParameters | null>;
45
+ queryJobs(options?: JobsQueryOptions): Promise<JobsResult>;
46
+ private sortJobs;
47
+ getJobsOverview(): Promise<JobsOverview[]>;
48
+ getDistinctJobNames(): Promise<string[]>;
49
+ getQueueSize(): Promise<number>;
50
+ removeJobs(options: RemoveJobsOptions): Promise<number>;
51
+ private deleteJob;
52
+ unlockJob(job: JobParameters): Promise<void>;
53
+ unlockJobs(jobIds: (JobId | string)[]): Promise<void>;
54
+ lockJob(job: JobParameters, options: JobRepositoryOptions | undefined): Promise<JobParameters | undefined>;
55
+ /**
56
+ * Lua script for atomic find-and-lock operation.
57
+ * This ensures that only one worker can lock a job, even under concurrent access.
58
+ *
59
+ * Arguments:
60
+ * KEYS[1] = jobs:by_name:{jobName} set
61
+ * KEYS[2] = jobs:by_next_run_at sorted set
62
+ * KEYS[3] = job:{id} hash prefix (without the id)
63
+ * ARGV[1] = nextScanAt timestamp (ms)
64
+ * ARGV[2] = lockDeadline timestamp (ms)
65
+ * ARGV[3] = lockTime ISO string
66
+ * ARGV[4] = lastModifiedBy
67
+ * ARGV[5] = sort direction ('asc' or 'desc')
68
+ *
69
+ * Returns: job ID if locked, nil if no job available
70
+ */
71
+ private static readonly FIND_AND_LOCK_SCRIPT;
72
+ getNextJobToRun(jobName: string, nextScanAt: Date, lockDeadline: Date, now: Date | undefined, options: JobRepositoryOptions | undefined): Promise<JobParameters | undefined>;
73
+ saveJobState(job: JobParameters, options: JobRepositoryOptions | undefined): Promise<void>;
74
+ saveJob<DATA = unknown>(job: JobParameters<DATA>, options: JobRepositoryOptions | undefined): Promise<JobParameters<DATA>>;
75
+ }