@pingpolls/redisq 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/.env.example +2 -0
- package/.prototools +1 -0
- package/LICENSE +201 -0
- package/README.md +784 -0
- package/app.test.ts +529 -0
- package/app.ts +774 -0
- package/benchmark/stress-worker.ts +50 -0
- package/benchmark/stress.ts +359 -0
- package/biome.json +80 -0
- package/compose.yml +20 -0
- package/package.json +31 -0
- package/redis.conf +1 -0
- package/tsconfig.json +29 -0
package/app.test.ts
ADDED
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
import { afterAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import RedisQueue from "./app";
|
|
3
|
+
|
|
4
|
+
const redisConfig = {
|
|
5
|
+
host: process.env.REDIS_HOST || "127.0.0.1",
|
|
6
|
+
namespace: "redisq-testing",
|
|
7
|
+
port: process.env.REDIS_PORT || "6379",
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const workerConfig = {
|
|
11
|
+
silent: true,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Helper to wait for a specific duration
|
|
16
|
+
*/
|
|
17
|
+
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
18
|
+
|
|
19
|
+
describe("RedisQueue Tests", () => {
|
|
20
|
+
test("1. Can send and receive single message", async () => {
|
|
21
|
+
const queue = new RedisQueue(redisConfig);
|
|
22
|
+
|
|
23
|
+
await queue.createQueue({ qname: "test-basic" });
|
|
24
|
+
|
|
25
|
+
const message = "Hello, World!";
|
|
26
|
+
let receivedCount = 0;
|
|
27
|
+
|
|
28
|
+
const id = await queue.sendMessage({
|
|
29
|
+
message,
|
|
30
|
+
qname: "test-basic",
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
await new Promise<void>((resolve) => {
|
|
34
|
+
queue.startWorker(
|
|
35
|
+
"test-basic",
|
|
36
|
+
async (received) => {
|
|
37
|
+
expect(received.message).toEqual(message);
|
|
38
|
+
expect(received.attempt).toBe(1);
|
|
39
|
+
expect(received.id).toEqual(id);
|
|
40
|
+
receivedCount++;
|
|
41
|
+
resolve();
|
|
42
|
+
return { success: true };
|
|
43
|
+
},
|
|
44
|
+
workerConfig,
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
expect(receivedCount).toBe(1);
|
|
49
|
+
|
|
50
|
+
await queue.close();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("2. Can send and receive delayed message", async () => {
|
|
54
|
+
const queue = new RedisQueue(redisConfig);
|
|
55
|
+
|
|
56
|
+
await queue.createQueue({ qname: "test-delayed" });
|
|
57
|
+
|
|
58
|
+
const message = "Delayed message";
|
|
59
|
+
const delayMs = 2000;
|
|
60
|
+
let receivedCount = 0;
|
|
61
|
+
|
|
62
|
+
const sentAt = Date.now();
|
|
63
|
+
const id = await queue.sendMessage({
|
|
64
|
+
message,
|
|
65
|
+
qname: "test-delayed",
|
|
66
|
+
delay: delayMs,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
await new Promise<void>((resolve) => {
|
|
70
|
+
queue.startWorker(
|
|
71
|
+
"test-delayed",
|
|
72
|
+
async (received) => {
|
|
73
|
+
const receivedAt = Date.now();
|
|
74
|
+
const actualDelay = receivedAt - sentAt;
|
|
75
|
+
|
|
76
|
+
expect(received.message).toEqual(message);
|
|
77
|
+
expect(received.id).toEqual(id);
|
|
78
|
+
expect(received.attempt).toBe(1);
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Check that message was delayed by at least the specified delay
|
|
82
|
+
* Allow 100ms margin for processing time
|
|
83
|
+
*/
|
|
84
|
+
expect(actualDelay).toBeGreaterThanOrEqual(delayMs - 100);
|
|
85
|
+
|
|
86
|
+
receivedCount++;
|
|
87
|
+
resolve();
|
|
88
|
+
return { success: true };
|
|
89
|
+
},
|
|
90
|
+
workerConfig,
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
expect(receivedCount).toBe(1);
|
|
95
|
+
|
|
96
|
+
await queue.close();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("3. Can retry send and receive single message", async () => {
|
|
100
|
+
const queue = new RedisQueue(redisConfig);
|
|
101
|
+
|
|
102
|
+
await queue.createQueue({
|
|
103
|
+
qname: "test-retry",
|
|
104
|
+
maxRetries: 3,
|
|
105
|
+
maxBackoffSeconds: 1,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const message = "Retry me!";
|
|
109
|
+
let attemptCount = 0;
|
|
110
|
+
|
|
111
|
+
await queue.sendMessage({
|
|
112
|
+
message,
|
|
113
|
+
qname: "test-retry",
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
await new Promise<void>((resolve) => {
|
|
117
|
+
queue.startWorker(
|
|
118
|
+
"test-retry",
|
|
119
|
+
async (received) => {
|
|
120
|
+
attemptCount++;
|
|
121
|
+
expect(received.attempt).toBe(attemptCount);
|
|
122
|
+
expect(received.message).toBe(message);
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Fail first 2 attempts, succeed on 3rd
|
|
126
|
+
*/
|
|
127
|
+
if (received.attempt < 3) {
|
|
128
|
+
return { success: false };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
resolve();
|
|
132
|
+
return { success: true };
|
|
133
|
+
},
|
|
134
|
+
workerConfig,
|
|
135
|
+
);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
expect(attemptCount).toBe(3);
|
|
139
|
+
|
|
140
|
+
await queue.close();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("4. Can retry send and receive delayed message", async () => {
|
|
144
|
+
const queue = new RedisQueue(redisConfig);
|
|
145
|
+
|
|
146
|
+
await queue.createQueue({
|
|
147
|
+
qname: "test-delayed-retry",
|
|
148
|
+
maxRetries: 2,
|
|
149
|
+
maxBackoffSeconds: 1,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const message = "Delayed retry message";
|
|
153
|
+
const delayMs = 1000;
|
|
154
|
+
let attemptCount = 0;
|
|
155
|
+
|
|
156
|
+
const sentAt = Date.now();
|
|
157
|
+
await queue.sendMessage({
|
|
158
|
+
message,
|
|
159
|
+
qname: "test-delayed-retry",
|
|
160
|
+
delay: delayMs,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
await new Promise<void>((resolve) => {
|
|
164
|
+
queue.startWorker(
|
|
165
|
+
"test-delayed-retry",
|
|
166
|
+
async (received) => {
|
|
167
|
+
attemptCount++;
|
|
168
|
+
expect(received.attempt).toBe(attemptCount);
|
|
169
|
+
expect(received.message).toBe(message);
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* First attempt should be delayed by initial delay
|
|
173
|
+
*/
|
|
174
|
+
if (attemptCount === 1) {
|
|
175
|
+
const actualDelay = Date.now() - sentAt;
|
|
176
|
+
expect(actualDelay).toBeGreaterThanOrEqual(
|
|
177
|
+
delayMs - 100,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Fail first attempt, succeed on 2nd
|
|
183
|
+
*/
|
|
184
|
+
if (received.attempt < 2) {
|
|
185
|
+
return { success: false };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
resolve();
|
|
189
|
+
return { success: true };
|
|
190
|
+
},
|
|
191
|
+
workerConfig,
|
|
192
|
+
);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
expect(attemptCount).toBe(2);
|
|
196
|
+
|
|
197
|
+
await queue.close();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("5. Can send and receive batched messages (multiple batches in same period)", async () => {
|
|
201
|
+
const queue = new RedisQueue(redisConfig);
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Create batch queue with 3-second interval
|
|
205
|
+
* This simulates a spreadsheet-queue:batch with shorter period for testing
|
|
206
|
+
*/
|
|
207
|
+
await queue.createQueue({
|
|
208
|
+
qname: "spreadsheet-queue:batch",
|
|
209
|
+
every: 3,
|
|
210
|
+
maxRetries: 0,
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Simulate the scenario:
|
|
215
|
+
* - 10 messages sent at different timestamps
|
|
216
|
+
* - 6 messages with batchId "batch-001"
|
|
217
|
+
* - 4 messages with batchId "batch-002"
|
|
218
|
+
* - All should be processed in the SAME 3-second period
|
|
219
|
+
*/
|
|
220
|
+
const batch001Messages: string[] = [];
|
|
221
|
+
const batch002Messages: string[] = [];
|
|
222
|
+
|
|
223
|
+
const processedBatches = new Set<string>();
|
|
224
|
+
let batch001Processed = false;
|
|
225
|
+
let batch002Processed = false;
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Send messages FIRST, then start worker
|
|
229
|
+
* This allows all messages to be ready before the first cron cycle
|
|
230
|
+
*/
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Send 6 messages to batch-001
|
|
234
|
+
*/
|
|
235
|
+
for (let i = 1; i <= 6; i++) {
|
|
236
|
+
const message = `Spreadsheet row ${i} for batch-001`;
|
|
237
|
+
await queue.sendBatchMessage({
|
|
238
|
+
qname: "spreadsheet-queue:batch",
|
|
239
|
+
batchId: "batch-001",
|
|
240
|
+
message,
|
|
241
|
+
});
|
|
242
|
+
batch001Messages.push(message);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Send 4 messages to batch-002
|
|
247
|
+
*/
|
|
248
|
+
for (let i = 1; i <= 4; i++) {
|
|
249
|
+
const message = `Spreadsheet row ${i} for batch-002`;
|
|
250
|
+
await queue.sendBatchMessage({
|
|
251
|
+
qname: "spreadsheet-queue:batch",
|
|
252
|
+
batchId: "batch-002",
|
|
253
|
+
message,
|
|
254
|
+
});
|
|
255
|
+
batch002Messages.push(message);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Start worker AFTER sending messages
|
|
260
|
+
* This ensures all messages are already pending when the cron starts
|
|
261
|
+
*/
|
|
262
|
+
const workerPromise = new Promise<void>((resolve) => {
|
|
263
|
+
queue.startWorker(
|
|
264
|
+
"spreadsheet-queue:batch",
|
|
265
|
+
async (received) => {
|
|
266
|
+
processedBatches.add(received.batchId);
|
|
267
|
+
|
|
268
|
+
if (received.batchId === "batch-001") {
|
|
269
|
+
expect(received.messages.length).toBe(6);
|
|
270
|
+
expect(received.attempt).toBe(1);
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Verify all messages are present
|
|
274
|
+
*/
|
|
275
|
+
received.messages.forEach((msg, idx) => {
|
|
276
|
+
expect(msg.message).toBe(
|
|
277
|
+
batch001Messages.at(idx) ?? "",
|
|
278
|
+
);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
batch001Processed = true;
|
|
282
|
+
} else if (received.batchId === "batch-002") {
|
|
283
|
+
expect(received.messages.length).toBe(4);
|
|
284
|
+
expect(received.attempt).toBe(1);
|
|
285
|
+
|
|
286
|
+
received.messages.forEach((msg, idx) => {
|
|
287
|
+
expect(msg.message).toBe(
|
|
288
|
+
batch002Messages.at(idx) ?? "",
|
|
289
|
+
);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
batch002Processed = true;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Resolve when both batches are processed
|
|
297
|
+
*/
|
|
298
|
+
if (batch001Processed && batch002Processed) {
|
|
299
|
+
resolve();
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return { success: true };
|
|
303
|
+
},
|
|
304
|
+
workerConfig,
|
|
305
|
+
);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Wait for batches to be processed
|
|
310
|
+
* The cron job will run every 3 seconds and process all pending batches
|
|
311
|
+
* Allow up to 6 seconds for processing (2 cron cycles to be safe)
|
|
312
|
+
*/
|
|
313
|
+
await Promise.race([
|
|
314
|
+
workerPromise,
|
|
315
|
+
sleep(6000).then(() => {
|
|
316
|
+
throw new Error("Test timeout: batches not processed in time");
|
|
317
|
+
}),
|
|
318
|
+
]);
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Assert both batches were processed
|
|
322
|
+
*/
|
|
323
|
+
expect(processedBatches.size).toBe(2);
|
|
324
|
+
expect(batch001Processed).toBe(true);
|
|
325
|
+
expect(batch002Processed).toBe(true);
|
|
326
|
+
|
|
327
|
+
await queue.close();
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
test("6. Can send and receive batched messages with selective retry", async () => {
|
|
331
|
+
const queue = new RedisQueue(redisConfig);
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Create batch queue with 2-second interval and retry enabled
|
|
335
|
+
*/
|
|
336
|
+
await queue.createQueue({
|
|
337
|
+
qname: "retry-test:batch",
|
|
338
|
+
every: 2,
|
|
339
|
+
maxRetries: 3,
|
|
340
|
+
maxBackoffSeconds: 1,
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
const processedBatches: Array<{
|
|
344
|
+
batchId: string;
|
|
345
|
+
attempt: number;
|
|
346
|
+
success: boolean;
|
|
347
|
+
}> = [];
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Create 3 different batches
|
|
351
|
+
*/
|
|
352
|
+
for (let i = 1; i <= 3; i++) {
|
|
353
|
+
await queue.sendBatchMessage({
|
|
354
|
+
qname: "retry-test:batch",
|
|
355
|
+
batchId: `batch-00${i}`,
|
|
356
|
+
message: `Message ${i}`,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Start worker AFTER sending messages
|
|
362
|
+
*/
|
|
363
|
+
const workerPromise = new Promise<void>((resolve) => {
|
|
364
|
+
queue.startWorker(
|
|
365
|
+
"retry-test:batch",
|
|
366
|
+
async (received) => {
|
|
367
|
+
processedBatches.push({
|
|
368
|
+
batchId: received.batchId,
|
|
369
|
+
attempt: received.attempt,
|
|
370
|
+
success: false,
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Simulate batch-001 failing on first 2 attempts
|
|
375
|
+
* batch-002 and batch-003 always succeed
|
|
376
|
+
*/
|
|
377
|
+
if (received.batchId === "batch-001") {
|
|
378
|
+
if (received.attempt < 3) {
|
|
379
|
+
/**
|
|
380
|
+
* Mark as failed, will retry
|
|
381
|
+
*/
|
|
382
|
+
const lastBatch = processedBatches.at(-1);
|
|
383
|
+
if (lastBatch) {
|
|
384
|
+
lastBatch.success = false;
|
|
385
|
+
}
|
|
386
|
+
return { success: false };
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* 3rd attempt succeeds
|
|
390
|
+
*/
|
|
391
|
+
expect(received.attempt).toBe(3);
|
|
392
|
+
const lastBatch = processedBatches.at(-1);
|
|
393
|
+
if (lastBatch) {
|
|
394
|
+
lastBatch.success = true;
|
|
395
|
+
}
|
|
396
|
+
} else {
|
|
397
|
+
/**
|
|
398
|
+
* batch-002 and batch-003 succeed immediately
|
|
399
|
+
*/
|
|
400
|
+
expect(received.attempt).toBe(1);
|
|
401
|
+
const lastBatch = processedBatches.at(-1);
|
|
402
|
+
if (lastBatch) {
|
|
403
|
+
lastBatch.success = true;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Check if all batches are done:
|
|
409
|
+
* - batch-001: should appear 3 times (attempt 1, 2, 3)
|
|
410
|
+
* - batch-002: should appear 1 time (attempt 1)
|
|
411
|
+
* - batch-003: should appear 1 time (attempt 1)
|
|
412
|
+
* Total: 5 invocations
|
|
413
|
+
*/
|
|
414
|
+
const batch001Count = processedBatches.filter(
|
|
415
|
+
(b) => b.batchId === "batch-001",
|
|
416
|
+
).length;
|
|
417
|
+
const batch002Count = processedBatches.filter(
|
|
418
|
+
(b) => b.batchId === "batch-002",
|
|
419
|
+
).length;
|
|
420
|
+
const batch003Count = processedBatches.filter(
|
|
421
|
+
(b) => b.batchId === "batch-003",
|
|
422
|
+
).length;
|
|
423
|
+
|
|
424
|
+
if (
|
|
425
|
+
batch001Count === 3 &&
|
|
426
|
+
batch002Count === 1 &&
|
|
427
|
+
batch003Count === 1
|
|
428
|
+
) {
|
|
429
|
+
resolve();
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return { success: true };
|
|
433
|
+
},
|
|
434
|
+
workerConfig,
|
|
435
|
+
);
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Wait for all batches to be processed (including retries)
|
|
440
|
+
* Allow up to 10 seconds (batch-001 will take multiple cycles)
|
|
441
|
+
*/
|
|
442
|
+
await Promise.race([
|
|
443
|
+
workerPromise,
|
|
444
|
+
sleep(10000).then(() => {
|
|
445
|
+
throw new Error("Test timeout: batches not processed in time");
|
|
446
|
+
}),
|
|
447
|
+
]);
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Verify total handler invocations
|
|
451
|
+
*/
|
|
452
|
+
expect(processedBatches.length).toBe(5);
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Verify batch-001 retry behavior
|
|
456
|
+
*/
|
|
457
|
+
const batch001Attempts = processedBatches.filter(
|
|
458
|
+
(b) => b.batchId === "batch-001",
|
|
459
|
+
);
|
|
460
|
+
expect(batch001Attempts.length).toBe(3);
|
|
461
|
+
|
|
462
|
+
const batch001Attempt1 = batch001Attempts.at(0);
|
|
463
|
+
expect(batch001Attempt1?.attempt).toBe(1);
|
|
464
|
+
expect(batch001Attempt1?.success).toBe(false);
|
|
465
|
+
|
|
466
|
+
const batch001Attempt2 = batch001Attempts.at(1);
|
|
467
|
+
expect(batch001Attempt2?.attempt).toBe(2);
|
|
468
|
+
expect(batch001Attempt2?.success).toBe(false);
|
|
469
|
+
|
|
470
|
+
const batch001Attempt3 = batch001Attempts.at(2);
|
|
471
|
+
expect(batch001Attempt3?.attempt).toBe(3);
|
|
472
|
+
expect(batch001Attempt3?.success).toBe(true);
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Verify batch-002 succeeded on first attempt
|
|
476
|
+
*/
|
|
477
|
+
const batch002Attempts = processedBatches.filter(
|
|
478
|
+
(b) => b.batchId === "batch-002",
|
|
479
|
+
);
|
|
480
|
+
expect(batch002Attempts.length).toBe(1);
|
|
481
|
+
|
|
482
|
+
const batch002Attempt1 = batch002Attempts.at(0);
|
|
483
|
+
expect(batch002Attempt1?.attempt).toBe(1);
|
|
484
|
+
expect(batch002Attempt1?.success).toBe(true);
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Verify batch-003 succeeded on first attempt
|
|
488
|
+
*/
|
|
489
|
+
const batch003Attempts = processedBatches.filter(
|
|
490
|
+
(b) => b.batchId === "batch-003",
|
|
491
|
+
);
|
|
492
|
+
expect(batch003Attempts.length).toBe(1);
|
|
493
|
+
|
|
494
|
+
const batch003Attempt1 = batch003Attempts.at(0);
|
|
495
|
+
expect(batch003Attempt1?.attempt).toBe(1);
|
|
496
|
+
expect(batch003Attempt1?.success).toBe(true);
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Verify independence: batch-002 and batch-003 were not affected by batch-001 failures
|
|
500
|
+
*/
|
|
501
|
+
const successfulBatches = processedBatches.filter(
|
|
502
|
+
(b) => b.success && b.attempt === 1,
|
|
503
|
+
);
|
|
504
|
+
expect(successfulBatches.length).toBe(2); // batch-002 and batch-003
|
|
505
|
+
|
|
506
|
+
await queue.close();
|
|
507
|
+
});
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
afterAll(async () => {
|
|
511
|
+
const redis = new Bun.RedisClient(
|
|
512
|
+
`redis://${redisConfig.host}:${redisConfig.port}`,
|
|
513
|
+
);
|
|
514
|
+
let cursor = "0";
|
|
515
|
+
const pattern = `${redisConfig.namespace}:*`;
|
|
516
|
+
|
|
517
|
+
do {
|
|
518
|
+
const result = await redis.scan(cursor, "MATCH", pattern);
|
|
519
|
+
const [nextCursor, keys] = result as [string, string[]];
|
|
520
|
+
|
|
521
|
+
cursor = nextCursor;
|
|
522
|
+
|
|
523
|
+
if (keys.length > 0) {
|
|
524
|
+
await redis.del(...keys);
|
|
525
|
+
}
|
|
526
|
+
} while (cursor !== "0");
|
|
527
|
+
|
|
528
|
+
redis.close();
|
|
529
|
+
});
|