@mayurbhusare/resilient-queue 1.0.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 +109 -0
- package/package.json +39 -0
- package/src/core/ResilientQueue.js +157 -0
- package/src/errors/Errors.js +26 -0
- package/src/index.js +8 -0
- package/src/managers/DLQManager.js +30 -0
- package/src/managers/IdempotencyManager.js +47 -0
- package/src/managers/RetryManager.js +68 -0
- package/src/redis/RedisClient.js +67 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mayur Bhusare
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# @mayurbhusare/resilient-queue
|
|
2
|
+
|
|
3
|
+
Minimal resilient Redis-backed job queue for Node.js.
|
|
4
|
+
|
|
5
|
+
Provides exponential retry, dead-letter queue (DLQ) handling, idempotency guarantees, and graceful shutdown — without heavy frameworks.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## ✨ Features
|
|
10
|
+
|
|
11
|
+
- Redis-backed job queue
|
|
12
|
+
- Exponential backoff retry
|
|
13
|
+
- Dead Letter Queue (DLQ) support
|
|
14
|
+
- Idempotency protection
|
|
15
|
+
- Graceful shutdown support
|
|
16
|
+
- Lightweight and minimal design
|
|
17
|
+
- Fully tested with Jest
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 📦 Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install @mayurbhusare/resilient-queue
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## 🚀 Quick Example
|
|
28
|
+
```bash
|
|
29
|
+
|
|
30
|
+
const {
|
|
31
|
+
ResilientQueue,
|
|
32
|
+
RetryableError,
|
|
33
|
+
FatalError
|
|
34
|
+
} = require("@mayurbhusare/resilient-queue");
|
|
35
|
+
|
|
36
|
+
const queue = new ResilientQueue({
|
|
37
|
+
redisUrl: "redis://127.0.0.1:6379",
|
|
38
|
+
maxRetries: 2,
|
|
39
|
+
baseDelay: 500
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
queue.process(async (job) => {
|
|
43
|
+
console.log("Processing:", job.data);
|
|
44
|
+
|
|
45
|
+
if (job.data.retry) {
|
|
46
|
+
throw new RetryableError("Temporary failure");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (job.data.fatal) {
|
|
50
|
+
throw new FatalError("Permanent failure");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
console.log("Completed");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
queue.enqueue({ message: "Hello World" });
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
## 🧠 How It Works
|
|
60
|
+
Main Queue
|
|
61
|
+
|
|
62
|
+
Jobs are pushed into:
|
|
63
|
+
```bash
|
|
64
|
+
rq:main
|
|
65
|
+
```
|
|
66
|
+
Workers consume jobs using blocking Redis BLPOP.
|
|
67
|
+
|
|
68
|
+
## Retry Strategy
|
|
69
|
+
|
|
70
|
+
Retryable errors trigger exponential backoff:
|
|
71
|
+
```bash
|
|
72
|
+
delay = baseDelay * 2^attempt
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
After exceeding maxRetries, the job is moved to the DLQ.
|
|
76
|
+
|
|
77
|
+
## Dead Letter Queue (DLQ)
|
|
78
|
+
|
|
79
|
+
Failed jobs are stored in:
|
|
80
|
+
```bash
|
|
81
|
+
rq:dlq
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### DLQ entries contain:
|
|
85
|
+
|
|
86
|
+
- failure reason
|
|
87
|
+
- attempt count
|
|
88
|
+
- timestamps
|
|
89
|
+
|
|
90
|
+
## Idempotency
|
|
91
|
+
|
|
92
|
+
If an `idempotencyKey` is provided:
|
|
93
|
+
|
|
94
|
+
- First execution succeeds
|
|
95
|
+
- Duplicate submissions are ignored
|
|
96
|
+
- Retries are not blocked
|
|
97
|
+
|
|
98
|
+
## Graceful Shutdown
|
|
99
|
+
```bash
|
|
100
|
+
await queue.close();
|
|
101
|
+
```
|
|
102
|
+
Safely stops worker and closes Redis connections.
|
|
103
|
+
|
|
104
|
+
## 🎯 Use Cases
|
|
105
|
+
- Email processing
|
|
106
|
+
- Webhook consumers
|
|
107
|
+
- Payment confirmation retries
|
|
108
|
+
- Background jobs
|
|
109
|
+
- Distributed microservices tasks
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mayurbhusare/resilient-queue",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A lightweight Redis-backed job queue with exponential retry, dead-letter queue support, and idempotency guarantees.",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"type": "commonjs",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"redis",
|
|
10
|
+
"queue",
|
|
11
|
+
"job-queue",
|
|
12
|
+
"retry",
|
|
13
|
+
"dead-letter-queue",
|
|
14
|
+
"dlq",
|
|
15
|
+
"idempotency",
|
|
16
|
+
"distributed-systems",
|
|
17
|
+
"nodejs",
|
|
18
|
+
"backend"
|
|
19
|
+
],
|
|
20
|
+
"author": "Mayur Bhusare",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/mayurbhusare/resilient-queue.git"
|
|
24
|
+
},
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/mayurbhusare/resilient-queue/issues"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://github.com/mayurbhusare/resilient-queue#readme",
|
|
29
|
+
"scripts": {
|
|
30
|
+
"test": "jest --runInBand"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"ioredis": "^5.9.3",
|
|
34
|
+
"uuid": "^8.3.2"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"jest": "^30.2.0"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
const { v4: uuidv4 } = require("uuid");
|
|
2
|
+
|
|
3
|
+
const RedisClient = require("../redis/RedisClient");
|
|
4
|
+
const IdempotencyManager = require("../managers/IdempotencyManager");
|
|
5
|
+
const DLQManager = require("../managers/DLQManager");
|
|
6
|
+
const RetryManager = require("../managers/RetryManager");
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Main queue class.
|
|
10
|
+
*/
|
|
11
|
+
class ResilientQueue {
|
|
12
|
+
constructor(options = {}) {
|
|
13
|
+
const {
|
|
14
|
+
redisUrl,
|
|
15
|
+
maxRetries = 3,
|
|
16
|
+
baseDelay = 500,
|
|
17
|
+
keyPrefix = "rq"
|
|
18
|
+
} = options;
|
|
19
|
+
|
|
20
|
+
if (!redisUrl) {
|
|
21
|
+
throw new Error("redisUrl is required to initialize ResilientQueue");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
this.keyPrefix = keyPrefix;
|
|
25
|
+
this.mainQueueKey = `${keyPrefix}:main`;
|
|
26
|
+
this.isRunning = false;
|
|
27
|
+
|
|
28
|
+
const redisClientWrapper = new RedisClient(redisUrl);
|
|
29
|
+
|
|
30
|
+
this.producer = redisClientWrapper.getProducer();
|
|
31
|
+
this.consumer = redisClientWrapper.getConsumer();
|
|
32
|
+
|
|
33
|
+
this.idempotencyManager = new IdempotencyManager(
|
|
34
|
+
this.producer,
|
|
35
|
+
keyPrefix
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
this.dlqManager = new DLQManager(
|
|
39
|
+
this.producer,
|
|
40
|
+
keyPrefix
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
this.retryManager = new RetryManager(
|
|
44
|
+
this.producer,
|
|
45
|
+
this.dlqManager,
|
|
46
|
+
{ maxRetries, baseDelay, keyPrefix }
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Enqueue a new job.
|
|
52
|
+
*/
|
|
53
|
+
async enqueue(data, options = {}) {
|
|
54
|
+
const { idempotencyKey, ttlSeconds } = options;
|
|
55
|
+
|
|
56
|
+
const job = {
|
|
57
|
+
id: uuidv4(),
|
|
58
|
+
data,
|
|
59
|
+
attempt: 0,
|
|
60
|
+
idempotencyKey: idempotencyKey || null,
|
|
61
|
+
ttlSeconds: ttlSeconds || null,
|
|
62
|
+
createdAt: Date.now()
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
await this.producer.rpush(
|
|
66
|
+
this.mainQueueKey,
|
|
67
|
+
JSON.stringify(job)
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
return job.id;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Start processing jobs.
|
|
75
|
+
*/
|
|
76
|
+
async process(handler) {
|
|
77
|
+
if (typeof handler !== "function") {
|
|
78
|
+
throw new Error("A handler function must be provided to process()");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
this.isRunning = true;
|
|
82
|
+
|
|
83
|
+
while (this.isRunning) {
|
|
84
|
+
try {
|
|
85
|
+
// IMPORTANT: timeout = 1 (not 0)
|
|
86
|
+
const result = await this.consumer.blpop(
|
|
87
|
+
this.mainQueueKey,
|
|
88
|
+
1
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
if (!result || result.length < 2) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const rawJob = result[1];
|
|
96
|
+
let job;
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
job = JSON.parse(rawJob);
|
|
100
|
+
} catch {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let shouldProceed = true;
|
|
105
|
+
|
|
106
|
+
// Apply idempotency only on first attempt
|
|
107
|
+
if (job.attempt === 0) {
|
|
108
|
+
shouldProceed =
|
|
109
|
+
await this.idempotencyManager.shouldProcess(
|
|
110
|
+
job.idempotencyKey,
|
|
111
|
+
job.ttlSeconds
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!shouldProceed) {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
await handler(job);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
// Prevent retry during shutdown
|
|
123
|
+
if (this.isRunning) {
|
|
124
|
+
await this.retryManager.handleFailure(job, error);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
} catch {
|
|
129
|
+
// Silent safety net
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Stop the worker loop.
|
|
136
|
+
*/
|
|
137
|
+
async stop() {
|
|
138
|
+
this.isRunning = false;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Stop worker and close Redis connections safely.
|
|
143
|
+
*/
|
|
144
|
+
async close() {
|
|
145
|
+
this.isRunning = false;
|
|
146
|
+
|
|
147
|
+
// Wait for BLPOP (timeout 1s) to exit naturally
|
|
148
|
+
await new Promise(resolve => setTimeout(resolve, 1100));
|
|
149
|
+
|
|
150
|
+
await Promise.all([
|
|
151
|
+
this.producer.quit(),
|
|
152
|
+
this.consumer.quit()
|
|
153
|
+
]);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
module.exports = ResilientQueue;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error used to indicate a job can be retried.
|
|
3
|
+
*/
|
|
4
|
+
class RetryableError extends Error {
|
|
5
|
+
constructor(message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "RetryableError";
|
|
8
|
+
Error.captureStackTrace(this, this.constructor);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Error used to indicate a job should not be retried.
|
|
14
|
+
*/
|
|
15
|
+
class FatalError extends Error {
|
|
16
|
+
constructor(message) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = "FatalError";
|
|
19
|
+
Error.captureStackTrace(this, this.constructor);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = {
|
|
24
|
+
RetryableError,
|
|
25
|
+
FatalError
|
|
26
|
+
};
|
package/src/index.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handles Dead Letter Queue (DLQ) operations.
|
|
3
|
+
*/
|
|
4
|
+
class DLQManager {
|
|
5
|
+
constructor(redisClient, keyPrefix = "rq") {
|
|
6
|
+
if (!redisClient) {
|
|
7
|
+
throw new Error("Redis client is required for DLQManager");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
this.redis = redisClient;
|
|
11
|
+
this.keyPrefix = keyPrefix;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Moves a failed job to DLQ.
|
|
16
|
+
*/
|
|
17
|
+
async moveToDLQ(job, reason) {
|
|
18
|
+
const dlqKey = `${this.keyPrefix}:dlq`;
|
|
19
|
+
|
|
20
|
+
const failedJob = {
|
|
21
|
+
...job,
|
|
22
|
+
failedAt: Date.now(),
|
|
23
|
+
failureReason: reason?.message || "Unknown error"
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
await this.redis.rpush(dlqKey, JSON.stringify(failedJob));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = DLQManager;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handles idempotency checks using Redis.
|
|
3
|
+
*/
|
|
4
|
+
class IdempotencyManager {
|
|
5
|
+
constructor(redisClient, keyPrefix = "rq") {
|
|
6
|
+
if (!redisClient) {
|
|
7
|
+
throw new Error("Redis client is required for IdempotencyManager");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
this.redis = redisClient;
|
|
11
|
+
this.keyPrefix = keyPrefix;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Returns true if job should proceed.
|
|
16
|
+
* Returns false if duplicate.
|
|
17
|
+
*/
|
|
18
|
+
async shouldProcess(idempotencyKey, ttlSeconds = null) {
|
|
19
|
+
if (!idempotencyKey) {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const redisKey = `${this.keyPrefix}:idempotency:${idempotencyKey}`;
|
|
24
|
+
|
|
25
|
+
let result;
|
|
26
|
+
|
|
27
|
+
if (ttlSeconds) {
|
|
28
|
+
result = await this.redis.set(
|
|
29
|
+
redisKey,
|
|
30
|
+
"1",
|
|
31
|
+
"EX",
|
|
32
|
+
ttlSeconds,
|
|
33
|
+
"NX"
|
|
34
|
+
);
|
|
35
|
+
} else {
|
|
36
|
+
result = await this.redis.set(
|
|
37
|
+
redisKey,
|
|
38
|
+
"1",
|
|
39
|
+
"NX"
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return result === "OK";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
module.exports = IdempotencyManager;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
const { RetryableError } = require("../errors/Errors");
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Handles retry logic with exponential backoff.
|
|
5
|
+
*/
|
|
6
|
+
class RetryManager {
|
|
7
|
+
constructor(redisClient, dlqManager, options = {}) {
|
|
8
|
+
if (!redisClient) {
|
|
9
|
+
throw new Error("Redis client is required for RetryManager");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (!dlqManager) {
|
|
13
|
+
throw new Error("DLQManager is required for RetryManager");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
this.redis = redisClient;
|
|
17
|
+
this.dlqManager = dlqManager;
|
|
18
|
+
|
|
19
|
+
this.maxRetries = options.maxRetries ?? 3;
|
|
20
|
+
this.baseDelay = options.baseDelay ?? 500;
|
|
21
|
+
this.keyPrefix = options.keyPrefix ?? "rq";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Handle job failure and decide whether to retry or send to DLQ.
|
|
26
|
+
*/
|
|
27
|
+
async handleFailure(job, error) {
|
|
28
|
+
const mainQueueKey = `${this.keyPrefix}:main`;
|
|
29
|
+
|
|
30
|
+
// If error is not retryable → send to DLQ immediately
|
|
31
|
+
if (!(error instanceof RetryableError)) {
|
|
32
|
+
await this.dlqManager.moveToDLQ(job, error);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const nextAttempt = (job.attempt || 0) + 1;
|
|
37
|
+
|
|
38
|
+
// If max retries exceeded → send to DLQ
|
|
39
|
+
if (nextAttempt > this.maxRetries) {
|
|
40
|
+
await this.dlqManager.moveToDLQ(
|
|
41
|
+
{ ...job, attempt: nextAttempt },
|
|
42
|
+
error
|
|
43
|
+
);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const delay = this.baseDelay * Math.pow(2, nextAttempt);
|
|
48
|
+
|
|
49
|
+
const retryJob = {
|
|
50
|
+
...job,
|
|
51
|
+
attempt: nextAttempt,
|
|
52
|
+
lastError: error.message,
|
|
53
|
+
retriedAt: Date.now()
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// Requeue job after exponential delay
|
|
57
|
+
setTimeout(() => {
|
|
58
|
+
this.redis
|
|
59
|
+
.rpush(mainQueueKey, JSON.stringify(retryJob))
|
|
60
|
+
.catch(err => {
|
|
61
|
+
// If requeue fails, move job to DLQ
|
|
62
|
+
this.dlqManager.moveToDLQ(retryJob, err);
|
|
63
|
+
});
|
|
64
|
+
}, delay);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
module.exports = RetryManager;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
const Redis = require("ioredis");
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* RedisClient
|
|
5
|
+
*
|
|
6
|
+
* Creates separate Redis connections for:
|
|
7
|
+
* - Producer (writes: RPUSH, SET, etc.)
|
|
8
|
+
* - Consumer (blocking reads: BLPOP)
|
|
9
|
+
*
|
|
10
|
+
* This separation is critical because a connection
|
|
11
|
+
* blocked by BLPOP cannot execute other commands.
|
|
12
|
+
*/
|
|
13
|
+
class RedisClient {
|
|
14
|
+
constructor(redisUrl) {
|
|
15
|
+
if (!redisUrl) {
|
|
16
|
+
throw new Error("redisUrl is required to initialize RedisClient");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Producer connection (writes)
|
|
20
|
+
this.producer = new Redis(redisUrl, {
|
|
21
|
+
maxRetriesPerRequest: null
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Consumer connection (blocking reads)
|
|
25
|
+
this.consumer = new Redis(redisUrl, {
|
|
26
|
+
maxRetriesPerRequest: null
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
this._attachEventHandlers();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Attach safe error handlers.
|
|
34
|
+
* Library should not log by default.
|
|
35
|
+
* Host application can manage Redis errors.
|
|
36
|
+
*/
|
|
37
|
+
_attachEventHandlers() {
|
|
38
|
+
this.producer.on("error", () => {});
|
|
39
|
+
this.consumer.on("error", () => {});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Returns producer connection.
|
|
44
|
+
*/
|
|
45
|
+
getProducer() {
|
|
46
|
+
return this.producer;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Returns consumer connection.
|
|
51
|
+
*/
|
|
52
|
+
getConsumer() {
|
|
53
|
+
return this.consumer;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Gracefully close connections.
|
|
58
|
+
*/
|
|
59
|
+
async disconnect() {
|
|
60
|
+
await Promise.all([
|
|
61
|
+
this.producer.quit(),
|
|
62
|
+
this.consumer.quit()
|
|
63
|
+
]);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = RedisClient;
|