@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 +25 -0
- package/README.md +186 -0
- package/dist/RedisBackend.d.ts +73 -0
- package/dist/RedisBackend.js +114 -0
- package/dist/RedisBackend.js.map +1 -0
- package/dist/RedisJobRepository.d.ts +75 -0
- package/dist/RedisJobRepository.js +750 -0
- package/dist/RedisJobRepository.js.map +1 -0
- package/dist/RedisNotificationChannel.d.ts +48 -0
- package/dist/RedisNotificationChannel.js +180 -0
- package/dist/RedisNotificationChannel.js.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +49 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +67 -0
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
|
+
}
|