@taicode/common-server 1.0.8 → 1.0.10
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/output/logger/logger.d.ts +33 -0
- package/output/logger/logger.d.ts.map +1 -0
- package/output/logger/logger.js +65 -0
- package/output/logger/logger.test.d.ts +2 -0
- package/output/logger/logger.test.d.ts.map +1 -0
- package/output/logger/logger.test.js +87 -0
- package/output/redis-queue/batch-redis-queue.d.ts +136 -0
- package/output/redis-queue/batch-redis-queue.d.ts.map +1 -0
- package/output/redis-queue/batch-redis-queue.js +580 -0
- package/output/redis-queue/batch-redis-queue.test.d.ts +2 -0
- package/output/redis-queue/batch-redis-queue.test.d.ts.map +1 -0
- package/output/redis-queue/batch-redis-queue.test.js +398 -0
- package/output/redis-queue/index.d.ts +5 -0
- package/output/redis-queue/index.d.ts.map +1 -0
- package/output/redis-queue/index.js +2 -0
- package/output/redis-queue/redis-queue.d.ts +128 -0
- package/output/redis-queue/redis-queue.d.ts.map +1 -0
- package/output/redis-queue/redis-queue.js +550 -0
- package/output/redis-queue/redis-queue.test.d.ts +2 -0
- package/output/redis-queue/redis-queue.test.d.ts.map +1 -0
- package/output/redis-queue/redis-queue.test.js +836 -0
- package/output/redis-queue/types.d.ts +61 -0
- package/output/redis-queue/types.d.ts.map +1 -0
- package/output/redis-queue/types.js +1 -0
- package/output/schema/index.d.ts +1 -1
- package/output/schema/index.d.ts.map +1 -1
- package/output/schema/index.js +1 -1
- package/package.json +9 -4
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { catchIt } from '@taicode/common-base';
|
|
3
|
+
import { BatchRedisQueue } from './batch-redis-queue';
|
|
4
|
+
const REDIS_URL = 'redis://localhost:6379';
|
|
5
|
+
describe('BatchRedisQueue', () => {
|
|
6
|
+
describe('连接管理', () => {
|
|
7
|
+
it('应该能够成功连接和断开 Redis', async () => {
|
|
8
|
+
const queue = new BatchRedisQueue({
|
|
9
|
+
redisUrl: REDIS_URL,
|
|
10
|
+
queueKey: 'test-connection',
|
|
11
|
+
});
|
|
12
|
+
await queue.connect();
|
|
13
|
+
const health = await queue.health();
|
|
14
|
+
expect(health).toBe(true);
|
|
15
|
+
queue.disconnect();
|
|
16
|
+
});
|
|
17
|
+
it('缺少必填参数应该抛出错误', () => {
|
|
18
|
+
expect(() => {
|
|
19
|
+
new BatchRedisQueue({
|
|
20
|
+
redisUrl: '',
|
|
21
|
+
queueKey: 'test',
|
|
22
|
+
});
|
|
23
|
+
}).toThrow('[BatchTaskQueue] redisUrl is required');
|
|
24
|
+
expect(() => {
|
|
25
|
+
new BatchRedisQueue({
|
|
26
|
+
redisUrl: REDIS_URL,
|
|
27
|
+
queueKey: '',
|
|
28
|
+
});
|
|
29
|
+
}).toThrow('[BatchTaskQueue] queueKey is required');
|
|
30
|
+
expect(() => {
|
|
31
|
+
new BatchRedisQueue({
|
|
32
|
+
redisUrl: REDIS_URL,
|
|
33
|
+
queueKey: 'short',
|
|
34
|
+
});
|
|
35
|
+
}).toThrow('[BatchTaskQueue] queueKey must be at least 6 characters long');
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
describe('批量任务处理', () => {
|
|
39
|
+
it('应该批量处理任务', async () => {
|
|
40
|
+
const queue = new BatchRedisQueue({
|
|
41
|
+
redisUrl: REDIS_URL,
|
|
42
|
+
queueKey: `test-batch-process-${Date.now()}`,
|
|
43
|
+
batchSize: 5,
|
|
44
|
+
});
|
|
45
|
+
await queue.connect();
|
|
46
|
+
const processedBatches = [];
|
|
47
|
+
queue.handle('batch-task', async (dataList) => {
|
|
48
|
+
processedBatches.push(dataList.map(d => d.value));
|
|
49
|
+
return catchIt(() => { });
|
|
50
|
+
});
|
|
51
|
+
// 入队 10 个任务
|
|
52
|
+
await queue.enqueue('batch-task', Array.from({ length: 10 }, (_, i) => ({ value: i })));
|
|
53
|
+
// 等待批量处理
|
|
54
|
+
await new Promise(resolve => setTimeout(resolve, 2500));
|
|
55
|
+
// 应该处理了 2 批,每批 5 个
|
|
56
|
+
expect(processedBatches.length).toBe(2);
|
|
57
|
+
expect(processedBatches[0].length).toBe(5);
|
|
58
|
+
expect(processedBatches[1].length).toBe(5);
|
|
59
|
+
queue.disconnect();
|
|
60
|
+
});
|
|
61
|
+
it('应该按类型分组批量处理', async () => {
|
|
62
|
+
const queue = new BatchRedisQueue({
|
|
63
|
+
redisUrl: REDIS_URL,
|
|
64
|
+
queueKey: `test-batch-by-type-${Date.now()}`,
|
|
65
|
+
batchSize: 10,
|
|
66
|
+
});
|
|
67
|
+
await queue.connect();
|
|
68
|
+
const emailBatches = [];
|
|
69
|
+
const smsBatches = [];
|
|
70
|
+
queue.handle('send-email', async (dataList) => {
|
|
71
|
+
emailBatches.push(dataList);
|
|
72
|
+
return catchIt(() => { });
|
|
73
|
+
});
|
|
74
|
+
queue.handle('send-sms', async (dataList) => {
|
|
75
|
+
smsBatches.push(dataList);
|
|
76
|
+
return catchIt(() => { });
|
|
77
|
+
});
|
|
78
|
+
// 混合入队不同类型的任务
|
|
79
|
+
await queue.enqueue('send-email', [
|
|
80
|
+
{ to: 'user1@example.com' },
|
|
81
|
+
{ to: 'user2@example.com' },
|
|
82
|
+
]);
|
|
83
|
+
await queue.enqueue('send-sms', [
|
|
84
|
+
{ phone: '123456' },
|
|
85
|
+
{ phone: '789012' },
|
|
86
|
+
]);
|
|
87
|
+
await queue.enqueue('send-email', { to: 'user3@example.com' });
|
|
88
|
+
// 等待处理
|
|
89
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
90
|
+
// 应该分别处理 email 和 sms
|
|
91
|
+
expect(emailBatches.length).toBe(1);
|
|
92
|
+
expect(emailBatches[0].length).toBe(3);
|
|
93
|
+
expect(smsBatches.length).toBe(1);
|
|
94
|
+
expect(smsBatches[0].length).toBe(2);
|
|
95
|
+
queue.disconnect();
|
|
96
|
+
});
|
|
97
|
+
it('未注册处理器的任务应该标记为失败', async () => {
|
|
98
|
+
const queue = new BatchRedisQueue({
|
|
99
|
+
redisUrl: REDIS_URL,
|
|
100
|
+
queueKey: `test-no-handler-${Date.now()}`,
|
|
101
|
+
batchSize: 5,
|
|
102
|
+
});
|
|
103
|
+
await queue.connect();
|
|
104
|
+
await queue.enqueue('unknown-task', [{ value: 1 }, { value: 2 }]);
|
|
105
|
+
// 等待处理
|
|
106
|
+
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
107
|
+
const stats = await queue.statistics();
|
|
108
|
+
expect(stats.failed).toBe(2);
|
|
109
|
+
queue.disconnect();
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
describe('错误处理和重试', () => {
|
|
113
|
+
it('失败的批次应该自动重试', async () => {
|
|
114
|
+
const queue = new BatchRedisQueue({
|
|
115
|
+
redisUrl: REDIS_URL,
|
|
116
|
+
queueKey: `test-batch-retry-${Date.now()}`,
|
|
117
|
+
batchSize: 3,
|
|
118
|
+
maxRetries: 2,
|
|
119
|
+
});
|
|
120
|
+
await queue.connect();
|
|
121
|
+
let attemptCount = 0;
|
|
122
|
+
queue.handle('retry-batch', async () => {
|
|
123
|
+
attemptCount++;
|
|
124
|
+
if (attemptCount < 2) {
|
|
125
|
+
throw new Error('Simulated batch failure');
|
|
126
|
+
}
|
|
127
|
+
return catchIt(() => { });
|
|
128
|
+
});
|
|
129
|
+
await queue.enqueue('retry-batch', [{ value: 1 }, { value: 2 }]);
|
|
130
|
+
// 等待重试
|
|
131
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
132
|
+
expect(attemptCount).toBe(2);
|
|
133
|
+
queue.disconnect();
|
|
134
|
+
});
|
|
135
|
+
it('超过最大重试次数应该标记为失败', async () => {
|
|
136
|
+
const queue = new BatchRedisQueue({
|
|
137
|
+
redisUrl: REDIS_URL,
|
|
138
|
+
queueKey: `test-max-retries-${Date.now()}`,
|
|
139
|
+
batchSize: 2,
|
|
140
|
+
maxRetries: 1,
|
|
141
|
+
});
|
|
142
|
+
await queue.connect();
|
|
143
|
+
queue.handle('always-fail', async () => {
|
|
144
|
+
throw new Error('Always fails');
|
|
145
|
+
});
|
|
146
|
+
await queue.enqueue('always-fail', [{ value: 1 }, { value: 2 }]);
|
|
147
|
+
// 等待处理和重试
|
|
148
|
+
await new Promise(resolve => setTimeout(resolve, 2500));
|
|
149
|
+
const stats = await queue.statistics();
|
|
150
|
+
expect(stats.failed).toBe(2);
|
|
151
|
+
queue.disconnect();
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
describe('幂等性', () => {
|
|
155
|
+
it('应该支持在 data 中指定 id 来实现幂等性', async () => {
|
|
156
|
+
const queue = new BatchRedisQueue({
|
|
157
|
+
redisUrl: REDIS_URL,
|
|
158
|
+
queueKey: `test-batch-idempotent-${Date.now()}`,
|
|
159
|
+
batchSize: 10,
|
|
160
|
+
});
|
|
161
|
+
await queue.connect();
|
|
162
|
+
let processCount = 0;
|
|
163
|
+
queue.handle('idempotent-batch', async (dataList) => {
|
|
164
|
+
processCount += dataList.length;
|
|
165
|
+
return catchIt(() => { });
|
|
166
|
+
});
|
|
167
|
+
// 使用相同的 id 多次入队
|
|
168
|
+
await queue.enqueue('idempotent-batch', { id: 'unique-1', value: 1 });
|
|
169
|
+
await queue.enqueue('idempotent-batch', { id: 'unique-1', value: 2 });
|
|
170
|
+
await queue.enqueue('idempotent-batch', { id: 'unique-2', value: 3 });
|
|
171
|
+
// 等待处理
|
|
172
|
+
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
173
|
+
// 应该只处理 2 个任务(unique-1 和 unique-2)
|
|
174
|
+
expect(processCount).toBe(2);
|
|
175
|
+
const stats = await queue.statistics();
|
|
176
|
+
expect(stats.completed).toBe(2);
|
|
177
|
+
queue.disconnect();
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
describe('延迟处理', () => {
|
|
181
|
+
it('应该支持延迟批量处理', async () => {
|
|
182
|
+
const queue = new BatchRedisQueue({
|
|
183
|
+
redisUrl: REDIS_URL,
|
|
184
|
+
queueKey: `test-batch-delay-${Date.now()}`,
|
|
185
|
+
batchSize: 3,
|
|
186
|
+
processingDelay: 2000,
|
|
187
|
+
});
|
|
188
|
+
await queue.connect();
|
|
189
|
+
let processCount = 0;
|
|
190
|
+
const startTime = Date.now();
|
|
191
|
+
let processTime = 0;
|
|
192
|
+
queue.handle('delay-batch', async (dataList) => {
|
|
193
|
+
processCount += dataList.length;
|
|
194
|
+
processTime = Date.now() - startTime;
|
|
195
|
+
return catchIt(() => { });
|
|
196
|
+
});
|
|
197
|
+
await queue.enqueue('delay-batch', [{ value: 1 }, { value: 2 }, { value: 3 }]);
|
|
198
|
+
// 等待 1 秒,任务应该还未处理
|
|
199
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
200
|
+
expect(processCount).toBe(0);
|
|
201
|
+
// 再等待 2.5 秒,任务应该已处理
|
|
202
|
+
await new Promise(resolve => setTimeout(resolve, 2500));
|
|
203
|
+
expect(processCount).toBe(3);
|
|
204
|
+
expect(processTime).toBeGreaterThanOrEqual(1900);
|
|
205
|
+
queue.disconnect();
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
describe('队列操作', () => {
|
|
209
|
+
it('应该正确返回队列统计信息', async () => {
|
|
210
|
+
const queue = new BatchRedisQueue({
|
|
211
|
+
redisUrl: REDIS_URL,
|
|
212
|
+
queueKey: `test-batch-stats-${Date.now()}`,
|
|
213
|
+
batchSize: 5,
|
|
214
|
+
});
|
|
215
|
+
await queue.connect();
|
|
216
|
+
queue.handle('stats-batch', async () => {
|
|
217
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
218
|
+
return catchIt(() => { });
|
|
219
|
+
});
|
|
220
|
+
// 入队 10 个任务
|
|
221
|
+
await queue.enqueue('stats-batch', Array.from({ length: 10 }, (_, i) => ({ value: i })));
|
|
222
|
+
// 立即检查统计
|
|
223
|
+
const stats1 = await queue.statistics();
|
|
224
|
+
expect(stats1.pending).toBeGreaterThan(0);
|
|
225
|
+
// 等待处理
|
|
226
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
227
|
+
const stats2 = await queue.statistics();
|
|
228
|
+
expect(stats2.completed).toBe(10);
|
|
229
|
+
queue.disconnect();
|
|
230
|
+
});
|
|
231
|
+
it('应该能够清空队列', async () => {
|
|
232
|
+
const queue = new BatchRedisQueue({
|
|
233
|
+
redisUrl: REDIS_URL,
|
|
234
|
+
queueKey: `test-clear-batch-${Date.now()}`,
|
|
235
|
+
});
|
|
236
|
+
await queue.connect();
|
|
237
|
+
await queue.enqueue('clear-batch', [{ value: 1 }, { value: 2 }]);
|
|
238
|
+
await queue.clear();
|
|
239
|
+
const stats = await queue.statistics();
|
|
240
|
+
expect(stats.pending).toBe(0);
|
|
241
|
+
queue.disconnect();
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
describe('大批量场景', () => {
|
|
245
|
+
it('应该能够处理大量任务', async () => {
|
|
246
|
+
const queue = new BatchRedisQueue({
|
|
247
|
+
redisUrl: REDIS_URL,
|
|
248
|
+
queueKey: `test-large-batch-${Date.now()}`,
|
|
249
|
+
batchSize: 50,
|
|
250
|
+
});
|
|
251
|
+
await queue.connect();
|
|
252
|
+
let processedCount = 0;
|
|
253
|
+
queue.handle('large-batch', async (dataList) => {
|
|
254
|
+
processedCount += dataList.length;
|
|
255
|
+
return catchIt(() => { });
|
|
256
|
+
});
|
|
257
|
+
// 入队 200 个任务
|
|
258
|
+
await queue.enqueue('large-batch', Array.from({ length: 200 }, (_, i) => ({ value: i })));
|
|
259
|
+
// 等待批量处理
|
|
260
|
+
await new Promise(resolve => setTimeout(resolve, 6000));
|
|
261
|
+
expect(processedCount).toBe(200);
|
|
262
|
+
const stats = await queue.statistics();
|
|
263
|
+
expect(stats.completed).toBe(200);
|
|
264
|
+
queue.disconnect();
|
|
265
|
+
}, 10000); // 增加超时到 10 秒
|
|
266
|
+
});
|
|
267
|
+
describe('并发安全性', () => {
|
|
268
|
+
it('多个批量队列实例应该能安全地处理任务', async () => {
|
|
269
|
+
const queueKey = `test-batch-concurrent-${Date.now()}`;
|
|
270
|
+
const queue1 = new BatchRedisQueue({
|
|
271
|
+
redisUrl: REDIS_URL,
|
|
272
|
+
queueKey,
|
|
273
|
+
batchSize: 5,
|
|
274
|
+
});
|
|
275
|
+
const queue2 = new BatchRedisQueue({
|
|
276
|
+
redisUrl: REDIS_URL,
|
|
277
|
+
queueKey,
|
|
278
|
+
batchSize: 5,
|
|
279
|
+
});
|
|
280
|
+
await queue1.connect();
|
|
281
|
+
await queue2.connect();
|
|
282
|
+
let totalProcessed = 0;
|
|
283
|
+
const handler = async (dataList) => {
|
|
284
|
+
totalProcessed += dataList.length;
|
|
285
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
286
|
+
return catchIt(() => { });
|
|
287
|
+
};
|
|
288
|
+
queue1.handle('concurrent-batch', handler);
|
|
289
|
+
queue2.handle('concurrent-batch', handler);
|
|
290
|
+
// 添加 20 个任务
|
|
291
|
+
await queue1.enqueue('concurrent-batch', Array.from({ length: 20 }, (_, i) => ({ value: i })));
|
|
292
|
+
// 等待处理
|
|
293
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
294
|
+
// 所有任务都应该被处理一次
|
|
295
|
+
expect(totalProcessed).toBe(20);
|
|
296
|
+
queue1.disconnect();
|
|
297
|
+
queue2.disconnect();
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
describe('批量数据边界测试', () => {
|
|
301
|
+
it('应该能够处理包含大量数据的批次', async () => {
|
|
302
|
+
const queue = new BatchRedisQueue({
|
|
303
|
+
redisUrl: REDIS_URL,
|
|
304
|
+
queueKey: `test-large-batch-data-${Date.now()}`,
|
|
305
|
+
batchSize: 10,
|
|
306
|
+
});
|
|
307
|
+
await queue.connect();
|
|
308
|
+
let receivedBatch = false;
|
|
309
|
+
queue.handle('large-data-batch', async (dataList) => {
|
|
310
|
+
expect(dataList.length).toBe(10);
|
|
311
|
+
dataList.forEach(data => {
|
|
312
|
+
expect(data.content.length).toBe(1000);
|
|
313
|
+
});
|
|
314
|
+
receivedBatch = true;
|
|
315
|
+
return catchIt(() => { });
|
|
316
|
+
});
|
|
317
|
+
// 每个任务 1KB,10 个任务共 10KB
|
|
318
|
+
await queue.enqueue('large-data-batch', Array.from({ length: 10 }, () => ({
|
|
319
|
+
content: 'x'.repeat(1000)
|
|
320
|
+
})));
|
|
321
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
322
|
+
expect(receivedBatch).toBe(true);
|
|
323
|
+
queue.disconnect();
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
describe('批量错误处理边界测试', () => {
|
|
327
|
+
it('批量处理失败应该重试所有任务', async () => {
|
|
328
|
+
const queue = new BatchRedisQueue({
|
|
329
|
+
redisUrl: REDIS_URL,
|
|
330
|
+
queueKey: `test-batch-retry-${Date.now()}`,
|
|
331
|
+
batchSize: 3,
|
|
332
|
+
maxRetries: 2,
|
|
333
|
+
});
|
|
334
|
+
await queue.connect();
|
|
335
|
+
let attemptCount = 0;
|
|
336
|
+
queue.handle('batch-retry', async (dataList) => {
|
|
337
|
+
attemptCount++;
|
|
338
|
+
if (attemptCount < 2) {
|
|
339
|
+
throw new Error('Batch processing failed');
|
|
340
|
+
}
|
|
341
|
+
return catchIt(() => { });
|
|
342
|
+
});
|
|
343
|
+
await queue.enqueue('batch-retry', [{ value: 1 }, { value: 2 }, { value: 3 }]);
|
|
344
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
345
|
+
// 第一次失败,第二次成功
|
|
346
|
+
expect(attemptCount).toBe(2);
|
|
347
|
+
const stats = await queue.statistics();
|
|
348
|
+
expect(stats.completed).toBe(3);
|
|
349
|
+
expect(stats.failed).toBe(0);
|
|
350
|
+
queue.disconnect();
|
|
351
|
+
});
|
|
352
|
+
it('批量处理超过最大重试次数应该标记为失败', async () => {
|
|
353
|
+
const queue = new BatchRedisQueue({
|
|
354
|
+
redisUrl: REDIS_URL,
|
|
355
|
+
queueKey: `test-batch-max-retry-${Date.now()}`,
|
|
356
|
+
batchSize: 3,
|
|
357
|
+
maxRetries: 1,
|
|
358
|
+
});
|
|
359
|
+
await queue.connect();
|
|
360
|
+
let attemptCount = 0;
|
|
361
|
+
queue.handle('batch-max-retry', async () => {
|
|
362
|
+
attemptCount++;
|
|
363
|
+
throw new Error('Always fails');
|
|
364
|
+
});
|
|
365
|
+
await queue.enqueue('batch-max-retry', [{ value: 1 }, { value: 2 }, { value: 3 }]);
|
|
366
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
367
|
+
// 初始 + 1 次重试 = 2 次
|
|
368
|
+
expect(attemptCount).toBe(2);
|
|
369
|
+
const stats = await queue.statistics();
|
|
370
|
+
expect(stats.failed).toBe(3);
|
|
371
|
+
expect(stats.completed).toBe(0);
|
|
372
|
+
queue.disconnect();
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
describe('批量延迟处理边界测试', () => {
|
|
376
|
+
it('延迟时间为 0 的批量任务应该立即处理', async () => {
|
|
377
|
+
const queue = new BatchRedisQueue({
|
|
378
|
+
redisUrl: REDIS_URL,
|
|
379
|
+
queueKey: `test-batch-zero-delay-${Date.now()}`,
|
|
380
|
+
batchSize: 5,
|
|
381
|
+
processingDelay: 0,
|
|
382
|
+
});
|
|
383
|
+
await queue.connect();
|
|
384
|
+
const startTime = Date.now();
|
|
385
|
+
let processTime = 0;
|
|
386
|
+
queue.handle('zero-delay-batch', async () => {
|
|
387
|
+
processTime = Date.now() - startTime;
|
|
388
|
+
return catchIt(() => { });
|
|
389
|
+
});
|
|
390
|
+
await queue.enqueue('zero-delay-batch', Array.from({ length: 5 }, (_, i) => ({ value: i })));
|
|
391
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
392
|
+
// 应该在 2 秒内处理
|
|
393
|
+
expect(processTime).toBeGreaterThan(0);
|
|
394
|
+
expect(processTime).toBeLessThan(2000);
|
|
395
|
+
queue.disconnect();
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
});
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { RedisQueue } from './redis-queue';
|
|
2
|
+
export { BatchRedisQueue } from './batch-redis-queue';
|
|
3
|
+
export type { TaskQueueConfig, BatchTaskQueueConfig } from './types';
|
|
4
|
+
export type { Status, TaskData, Task, TaskHandler, BatchTaskHandler, QueueStats } from './types';
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../source/redis-queue/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAC1C,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAA;AACrD,YAAY,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAA;AACpE,YAAY,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA"}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type { TaskQueueConfig, TaskData, Task, TaskHandler, QueueStats } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* 通用任务队列类
|
|
4
|
+
*
|
|
5
|
+
* 提供基于 Redis 的任务队列功能,支持:
|
|
6
|
+
* - 任务入队和持久化
|
|
7
|
+
* - 自动重试机制
|
|
8
|
+
* - 任务状态追踪
|
|
9
|
+
* - 分布式消费
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* const queue = new RedisQueue({
|
|
14
|
+
* redisUrl: 'redis://localhost:6379',
|
|
15
|
+
* queueKey: 'my-queue',
|
|
16
|
+
* concurrency: 5, // 并发处理 5 个任务
|
|
17
|
+
* })
|
|
18
|
+
*
|
|
19
|
+
* // 注册任务处理器
|
|
20
|
+
* queue.handle('send-email', async (data) => {
|
|
21
|
+
* await sendEmail(data.to, data.subject)
|
|
22
|
+
* })
|
|
23
|
+
*
|
|
24
|
+
* // 连接 Redis(自动启动消费者)
|
|
25
|
+
* await queue.connect()
|
|
26
|
+
*
|
|
27
|
+
* // 入队任务(自动开始处理)
|
|
28
|
+
* await queue.enqueue('send-email', { to: 'user@example.com', subject: 'Hello' })
|
|
29
|
+
*
|
|
30
|
+
* // 获取队列统计信息(O(1) 时间复杂度)
|
|
31
|
+
* const stats = await queue.statistics()
|
|
32
|
+
* console.log(stats) // { pending: 5, processing: 2, completed: 2, failed: 1 }
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export declare class RedisQueue {
|
|
36
|
+
private consumerRunning;
|
|
37
|
+
private redis;
|
|
38
|
+
private consumerInterval;
|
|
39
|
+
private recoveryInterval;
|
|
40
|
+
private processingTasks;
|
|
41
|
+
private readonly config;
|
|
42
|
+
private readonly handlers;
|
|
43
|
+
private readonly failedQueue;
|
|
44
|
+
private readonly pendingQueue;
|
|
45
|
+
private readonly processingQueue;
|
|
46
|
+
private readonly completedQueue;
|
|
47
|
+
constructor(config: TaskQueueConfig);
|
|
48
|
+
/**
|
|
49
|
+
* 连接 Redis 并自动启动消费者
|
|
50
|
+
*/
|
|
51
|
+
connect(): Promise<void>;
|
|
52
|
+
/**
|
|
53
|
+
* 断开 Redis 连接并停止消费者
|
|
54
|
+
*/
|
|
55
|
+
disconnect(): void;
|
|
56
|
+
/**
|
|
57
|
+
* 注册任务处理器
|
|
58
|
+
*/
|
|
59
|
+
handle<T extends TaskData = TaskData>(type: string, handler: TaskHandler<T>): void;
|
|
60
|
+
/**
|
|
61
|
+
* 将任务推入队列(支持单个或批量)
|
|
62
|
+
* @param type 任务类型
|
|
63
|
+
* @param data 任务数据(单个或数组)。可以在 data 中包含 `id` 字段来实现幂等性
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* // 普通任务,自动生成 ID
|
|
67
|
+
* await queue.enqueue('send-email', { to: 'user@example.com' })
|
|
68
|
+
*
|
|
69
|
+
* // 幂等任务,手动指定 ID,重复提交会被忽略
|
|
70
|
+
* await queue.enqueue('send-email', { id: 'email-123', to: 'user@example.com' })
|
|
71
|
+
* await queue.enqueue('send-email', { id: 'email-123', to: 'user@example.com' }) // 会被跳过
|
|
72
|
+
*/
|
|
73
|
+
enqueue<T extends TaskData = TaskData>(type: string, data: T[]): Promise<string[]>;
|
|
74
|
+
enqueue<T extends TaskData = TaskData>(type: string, data: T): Promise<string>;
|
|
75
|
+
/**
|
|
76
|
+
* 获取任务详情
|
|
77
|
+
*/
|
|
78
|
+
getTask(taskId: string): Promise<Task | null>;
|
|
79
|
+
/**
|
|
80
|
+
* 更新任务状态并移动到对应队列(原子操作)
|
|
81
|
+
*/
|
|
82
|
+
private applyStatus;
|
|
83
|
+
/**
|
|
84
|
+
* 根据状态获取对应的队列键
|
|
85
|
+
*/
|
|
86
|
+
private getQueueByStatus;
|
|
87
|
+
/**
|
|
88
|
+
* 恢复超时的任务
|
|
89
|
+
* 检查 processing 队列中的任务,将超时的任务重试或标记为失败
|
|
90
|
+
*/
|
|
91
|
+
private recoverStalledTasks;
|
|
92
|
+
/**
|
|
93
|
+
* 处理单个任务
|
|
94
|
+
*/
|
|
95
|
+
private processTask;
|
|
96
|
+
/**
|
|
97
|
+
* 启动恢复机制(内部方法,自动调用)
|
|
98
|
+
*/
|
|
99
|
+
private startRecovery;
|
|
100
|
+
/**
|
|
101
|
+
* 停止恢复机制(内部方法,自动调用)
|
|
102
|
+
*/
|
|
103
|
+
private stopRecovery;
|
|
104
|
+
/**
|
|
105
|
+
* 启动消费者(内部方法,自动调用)
|
|
106
|
+
*/
|
|
107
|
+
private startConsumer;
|
|
108
|
+
/**
|
|
109
|
+
* 停止消费者(内部方法,自动调用)
|
|
110
|
+
*/
|
|
111
|
+
private stopConsumer;
|
|
112
|
+
/**
|
|
113
|
+
* 获取队列统计信息
|
|
114
|
+
* 返回队列中各种状态任务的详细数量
|
|
115
|
+
*
|
|
116
|
+
* 使用分离队列设计,O(1) 时间复杂度
|
|
117
|
+
*/
|
|
118
|
+
statistics(): Promise<QueueStats>;
|
|
119
|
+
/**
|
|
120
|
+
* 清空所有队列
|
|
121
|
+
*/
|
|
122
|
+
clear(): Promise<void>;
|
|
123
|
+
/**
|
|
124
|
+
* 健康检查
|
|
125
|
+
*/
|
|
126
|
+
health(): Promise<boolean>;
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=redis-queue.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redis-queue.d.ts","sourceRoot":"","sources":["../../source/redis-queue/redis-queue.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,eAAe,EAAU,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AAE/F;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,eAAe,CAAQ;IAC/B,OAAO,CAAC,KAAK,CAA+B;IAC5C,OAAO,CAAC,gBAAgB,CAA8B;IACtD,OAAO,CAAC,gBAAgB,CAA8B;IACtD,OAAO,CAAC,eAAe,CAAI;IAE3B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA2B;IAClD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAiC;IAG1D,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAQ;IACpC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAQ;IACrC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAQ;IACxC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAQ;gBAE3B,MAAM,EAAE,eAAe;IAqCnC;;OAEG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAY9B;;OAEG;IACH,UAAU,IAAI,IAAI;IAUlB;;OAEG;IACH,MAAM,CAAC,CAAC,SAAS,QAAQ,GAAG,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,CAAC,CAAC,CAAC,GAAG,IAAI;IAIlF;;;;;;;;;;;;OAYG;IACG,OAAO,CAAC,CAAC,SAAS,QAAQ,GAAG,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAClF,OAAO,CAAC,CAAC,SAAS,QAAQ,GAAG,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC;IAmEpF;;OAEG;IACG,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;IAWnD;;OAEG;YACW,WAAW;IA8BzB;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAUxB;;;OAGG;YACW,mBAAmB;IA6EjC;;OAEG;YACW,WAAW;IAmFzB;;OAEG;IACH,OAAO,CAAC,aAAa;IAoBrB;;OAEG;IACH,OAAO,CAAC,YAAY;IAQpB;;OAEG;IACH,OAAO,CAAC,aAAa;IA4ErB;;OAEG;IACH,OAAO,CAAC,YAAY;IASpB;;;;;OAKG;IACG,UAAU,IAAI,OAAO,CAAC,UAAU,CAAC;IA0BvC;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAkB5B;;OAEG;IACG,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC;CAMjC"}
|