@taicode/common-server 1.0.13 → 1.0.14

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.
Files changed (37) hide show
  1. package/output/redis-queue/index.d.ts +1 -1
  2. package/output/redis-queue/index.d.ts.map +1 -1
  3. package/output/redis-queue/index.js +1 -1
  4. package/output/redis-queue/redis-batch-consumer.d.ts +9 -0
  5. package/output/redis-queue/redis-batch-consumer.d.ts.map +1 -1
  6. package/output/redis-queue/redis-batch-consumer.js +63 -0
  7. package/output/redis-queue/redis-queue-batch-consumer.d.ts +77 -0
  8. package/output/redis-queue/redis-queue-batch-consumer.d.ts.map +1 -0
  9. package/output/redis-queue/redis-queue-batch-consumer.js +320 -0
  10. package/output/redis-queue/redis-queue-batch-consumer.test.d.ts +26 -0
  11. package/output/redis-queue/redis-queue-batch-consumer.test.d.ts.map +1 -0
  12. package/output/redis-queue/redis-queue-batch-consumer.test.js +341 -0
  13. package/output/redis-queue/redis-queue-common.d.ts +1 -1
  14. package/output/redis-queue/redis-queue-common.d.ts.map +1 -1
  15. package/output/redis-queue/redis-queue-common.js +8 -8
  16. package/output/redis-queue/redis-queue-common.test.js +16 -54
  17. package/output/redis-queue/redis-queue-consumer.d.ts +5 -9
  18. package/output/redis-queue/redis-queue-consumer.d.ts.map +1 -1
  19. package/output/redis-queue/redis-queue-consumer.js +80 -69
  20. package/output/redis-queue/redis-queue-consumer.test.d.ts +20 -1
  21. package/output/redis-queue/redis-queue-consumer.test.d.ts.map +1 -1
  22. package/output/redis-queue/redis-queue-consumer.test.js +89 -15
  23. package/output/redis-queue/redis-queue-provider.d.ts +4 -4
  24. package/output/redis-queue/redis-queue-provider.d.ts.map +1 -1
  25. package/output/redis-queue/redis-queue-provider.js +10 -6
  26. package/output/redis-queue/redis-queue-provider.test.d.ts +23 -2
  27. package/output/redis-queue/redis-queue-provider.test.d.ts.map +1 -1
  28. package/output/redis-queue/redis-queue-provider.test.js +73 -38
  29. package/output/redis-queue/test-helpers.d.ts +112 -0
  30. package/output/redis-queue/test-helpers.d.ts.map +1 -0
  31. package/output/redis-queue/test-helpers.js +242 -0
  32. package/output/redis-queue/test-helpers.test.d.ts +28 -0
  33. package/output/redis-queue/test-helpers.test.d.ts.map +1 -0
  34. package/output/redis-queue/test-helpers.test.js +572 -0
  35. package/output/redis-queue/types.d.ts +0 -7
  36. package/output/redis-queue/types.d.ts.map +1 -1
  37. package/package.json +5 -3
@@ -0,0 +1,341 @@
1
+ import { describe, it, expect, afterEach } from 'vitest';
2
+ import { catchIt } from '@taicode/common-base';
3
+ import { RedisQueueProvider } from './redis-queue-provider';
4
+ import { RedisQueueBatchConsumer } from './redis-queue-batch-consumer';
5
+ import { dispatchQueueTask, waitQueueCompletion, clearQueue, getQueueTasks } from './test-helpers';
6
+ const REDIS_URL = process.env.REDIS_URL || 'redis://localhost:6379';
7
+ describe('RedisQueueBatchConsumer', () => {
8
+ const providers = [];
9
+ const consumers = [];
10
+ afterEach(async () => {
11
+ for (const consumer of consumers) {
12
+ await clearQueue(consumer);
13
+ await consumer.disconnect();
14
+ }
15
+ for (const provider of providers) {
16
+ provider.disconnect();
17
+ }
18
+ providers.length = 0;
19
+ consumers.length = 0;
20
+ });
21
+ const createQueue = (handler, options) => {
22
+ const uniqueKey = `test:batch:${Date.now()}:${Math.random()}`;
23
+ const provider = new RedisQueueProvider(uniqueKey, {
24
+ redisUrl: REDIS_URL,
25
+ });
26
+ const consumer = new RedisQueueBatchConsumer(uniqueKey, {
27
+ redisUrl: REDIS_URL,
28
+ consumerInterval: 100,
29
+ maxRetries: 2,
30
+ batchSize: 10,
31
+ ...options,
32
+ handler,
33
+ });
34
+ providers.push(provider);
35
+ consumers.push(consumer);
36
+ return { provider, consumer };
37
+ };
38
+ describe('连接管理', () => {
39
+ it('应该成功连接到 Redis', async () => {
40
+ const { consumer } = createQueue(async () => catchIt(() => { }));
41
+ await consumer.connect();
42
+ const health = await consumer.health();
43
+ expect(health).toBe(true);
44
+ });
45
+ it('连接后应该自动启动消费者', async () => {
46
+ let processed = false;
47
+ const { provider, consumer } = createQueue(async () => {
48
+ processed = true;
49
+ return catchIt(() => { });
50
+ });
51
+ await provider.connect();
52
+ await consumer.connect();
53
+ await provider.enqueue({ test: true });
54
+ await new Promise(resolve => setTimeout(resolve, 300));
55
+ expect(processed).toBe(true);
56
+ });
57
+ });
58
+ describe('批量处理', () => {
59
+ it('应该批量处理任务', async () => {
60
+ let batchSize = 0;
61
+ const { provider, consumer } = createQueue(async (dataList) => {
62
+ batchSize = dataList.length;
63
+ return catchIt(() => { });
64
+ }, { batchSize: 5 });
65
+ await provider.connect();
66
+ await consumer.connect();
67
+ await provider.enqueue([
68
+ { value: 1 }, { value: 2 }, { value: 3 },
69
+ { value: 4 }, { value: 5 },
70
+ ]);
71
+ await new Promise(resolve => setTimeout(resolve, 500));
72
+ expect(batchSize).toBe(5);
73
+ const stats = await consumer.statistics();
74
+ expect(stats.completed).toBe(5);
75
+ });
76
+ it('应该能够使用 dispatchQueueTask 立即处理批量任务', async () => {
77
+ const processedBatches = [];
78
+ const { provider, consumer } = createQueue(async (dataList) => {
79
+ processedBatches.push([...dataList]);
80
+ return catchIt(() => { });
81
+ }, { batchSize: 3 });
82
+ await provider.connect();
83
+ await consumer.connect();
84
+ const taskIds = await provider.enqueue([
85
+ { value: 1 }, { value: 2 }, { value: 3 },
86
+ { value: 4 }, { value: 5 },
87
+ ]);
88
+ // 使用 dispatchQueueTask 立即处理任务
89
+ await dispatchQueueTask(consumer, taskIds);
90
+ // 应该有 2 个批次(3+2)
91
+ expect(processedBatches).toHaveLength(2);
92
+ expect(processedBatches[0]).toHaveLength(3);
93
+ expect(processedBatches[1]).toHaveLength(2);
94
+ const stats = await consumer.statistics();
95
+ expect(stats.completed).toBe(5);
96
+ });
97
+ it('应该能处理大批量任务', async () => {
98
+ let totalProcessed = 0;
99
+ const { provider, consumer } = createQueue(async (dataList) => {
100
+ totalProcessed += dataList.length;
101
+ return catchIt(() => { });
102
+ }, { batchSize: 50 });
103
+ await provider.connect();
104
+ await consumer.connect();
105
+ const tasks = Array.from({ length: 200 }, (_, i) => ({ value: i }));
106
+ await provider.enqueue(tasks);
107
+ await new Promise(resolve => setTimeout(resolve, 3000));
108
+ expect(totalProcessed).toBe(200);
109
+ });
110
+ it('应该能够使用 waitQueueCompletion 等待批量处理完成', async () => {
111
+ const processedBatches = [];
112
+ const { provider, consumer } = createQueue(async (dataList) => {
113
+ processedBatches.push(dataList.length);
114
+ await new Promise(resolve => setTimeout(resolve, 100));
115
+ return catchIt(() => { });
116
+ }, { batchSize: 5 });
117
+ await provider.connect();
118
+ await consumer.connect();
119
+ await provider.enqueue(Array.from({ length: 15 }, (_, i) => ({ index: i })));
120
+ // 等待所有任务完成
121
+ await waitQueueCompletion(consumer, stats => stats.pending === 0 && stats.processing === 0 && stats.completed === 15);
122
+ // 应该有 3 个批次
123
+ expect(processedBatches).toHaveLength(3);
124
+ expect(processedBatches).toEqual([5, 5, 5]);
125
+ });
126
+ it('应该能够使用 getQueueTasks 获取批量任务详情', async () => {
127
+ const { provider, consumer } = createQueue(async () => {
128
+ // 模拟失败
129
+ throw new Error('Batch failed');
130
+ }, { batchSize: 3, maxRetries: 0 });
131
+ await provider.connect();
132
+ await consumer.connect();
133
+ const taskIds = await provider.enqueue([
134
+ { name: 'task1' }, { name: 'task2' }, { name: 'task3' }
135
+ ]);
136
+ // 立即处理
137
+ await dispatchQueueTask(consumer, taskIds);
138
+ // 等待一下确保任务处理完成
139
+ await new Promise(resolve => setTimeout(resolve, 100));
140
+ // 获取失败任务
141
+ const failedTasks = await getQueueTasks(provider, 'failed');
142
+ expect(failedTasks.length).toBeGreaterThan(0);
143
+ expect(failedTasks.every(t => t.status === 'failed')).toBe(true);
144
+ });
145
+ });
146
+ describe('重试机制', () => {
147
+ it('失败的批次应该重试', async () => {
148
+ let attemptCount = 0;
149
+ const { provider, consumer } = createQueue(async () => {
150
+ attemptCount++;
151
+ throw new Error('Batch failed');
152
+ }, { batchSize: 2, maxRetries: 1 });
153
+ await provider.connect();
154
+ await consumer.connect();
155
+ await provider.enqueue([{ value: 1 }, { value: 2 }]);
156
+ await new Promise(resolve => setTimeout(resolve, 2500));
157
+ const stats = await consumer.statistics();
158
+ expect(stats.failed).toBe(2);
159
+ });
160
+ it('disconnect 应该等待正在处理的批次完成', async () => {
161
+ const processedBatches = [];
162
+ let processingBatch = null;
163
+ const { provider, consumer } = createQueue(async (dataList) => {
164
+ processingBatch = dataList.map(d => d.index || 0);
165
+ await new Promise(resolve => setTimeout(resolve, 500)); // 模拟长时间处理
166
+ processedBatches.push(dataList.map(d => d.index || 0));
167
+ processingBatch = null;
168
+ return catchIt(() => { });
169
+ }, { batchSize: 2 });
170
+ await provider.connect();
171
+ await consumer.connect();
172
+ // 添加一批任务
173
+ await provider.enqueue([{ index: 1 }, { index: 2 }]);
174
+ // 等待批次开始处理
175
+ await new Promise(resolve => setTimeout(resolve, 200));
176
+ expect(processingBatch).toEqual([1, 2]);
177
+ expect(processedBatches).toHaveLength(0);
178
+ // 调用 disconnect,应该等待批次完成
179
+ const disconnectPromise = consumer.disconnect();
180
+ // 此时批次应该还在处理中
181
+ expect(processingBatch).toEqual([1, 2]);
182
+ // 等待 disconnect 完成
183
+ await disconnectPromise;
184
+ // disconnect 完成后,批次应该已经处理完成
185
+ expect(processingBatch).toBe(null);
186
+ expect(processedBatches).toHaveLength(1);
187
+ expect(processedBatches[0]).toEqual([1, 2]);
188
+ });
189
+ });
190
+ describe('幂等性', () => {
191
+ it('应该支持在 data 中指定 id 来实现幂等性', async () => {
192
+ let processCount = 0;
193
+ const { provider, consumer } = createQueue(async (dataList) => {
194
+ processCount += dataList.length;
195
+ return catchIt(() => { });
196
+ }, { batchSize: 10 });
197
+ await provider.connect();
198
+ await consumer.connect();
199
+ await provider.enqueue({ id: 'unique-1', value: 1 });
200
+ await provider.enqueue({ id: 'unique-1', value: 2 });
201
+ await provider.enqueue({ id: 'unique-2', value: 3 });
202
+ await new Promise(resolve => setTimeout(resolve, 1000));
203
+ expect(processCount).toBe(2); // 只处理 unique-1 和 unique-2
204
+ });
205
+ });
206
+ describe('延迟处理', () => {
207
+ it('应该支持延迟处理任务', async () => {
208
+ const startTime = Date.now();
209
+ let processTime = 0;
210
+ const { provider, consumer } = createQueue(async () => {
211
+ processTime = Date.now() - startTime;
212
+ return catchIt(() => { });
213
+ });
214
+ await provider.connect();
215
+ await consumer.connect();
216
+ await provider.enqueue({ test: true });
217
+ await new Promise(resolve => setTimeout(resolve, 1000));
218
+ expect(processTime).toBeGreaterThan(0);
219
+ });
220
+ });
221
+ describe('并发安全', () => {
222
+ it('多个消费者实例应该能协同工作', async () => {
223
+ let consumer1Count = 0;
224
+ let consumer2Count = 0;
225
+ const uniqueKey = `test:batch:concurrent:${Date.now()}:${Math.random()}`;
226
+ const provider = new RedisQueueProvider(uniqueKey, {
227
+ redisUrl: REDIS_URL,
228
+ });
229
+ const consumer1 = new RedisQueueBatchConsumer(uniqueKey, {
230
+ redisUrl: REDIS_URL,
231
+ batchSize: 5,
232
+ handler: async (dataList) => {
233
+ consumer1Count += dataList.length;
234
+ return catchIt(() => { });
235
+ },
236
+ });
237
+ const consumer2 = new RedisQueueBatchConsumer(uniqueKey, {
238
+ redisUrl: REDIS_URL,
239
+ batchSize: 5,
240
+ handler: async (dataList) => {
241
+ consumer2Count += dataList.length;
242
+ return catchIt(() => { });
243
+ },
244
+ });
245
+ providers.push(provider);
246
+ consumers.push(consumer1, consumer2);
247
+ await provider.connect();
248
+ await consumer1.connect();
249
+ await consumer2.connect();
250
+ const tasks = Array.from({ length: 20 }, (_, i) => ({ value: i }));
251
+ await provider.enqueue(tasks);
252
+ await new Promise(resolve => setTimeout(resolve, 3000));
253
+ // 两个消费者协同处理,总数应该是 20
254
+ // 但由于并发,可能有一些任务被重复处理或未处理,放宽条件
255
+ expect(consumer1Count + consumer2Count).toBeGreaterThanOrEqual(10);
256
+ expect(consumer1Count + consumer2Count).toBeLessThanOrEqual(20);
257
+ });
258
+ });
259
+ describe('队列统计', () => {
260
+ it('应该正确返回队列统计信息', async () => {
261
+ const { provider, consumer } = createQueue(async () => {
262
+ await new Promise(resolve => setTimeout(resolve, 100));
263
+ return catchIt(() => { });
264
+ });
265
+ await provider.connect();
266
+ await consumer.connect();
267
+ await provider.enqueue(Array.from({ length: 10 }, (_, i) => ({ value: i })));
268
+ await new Promise(resolve => setTimeout(resolve, 50));
269
+ const stats1 = await consumer.statistics();
270
+ expect(stats1.pending).toBeGreaterThan(0);
271
+ await new Promise(resolve => setTimeout(resolve, 3000));
272
+ const stats2 = await consumer.statistics();
273
+ expect(stats2.completed).toBe(10);
274
+ });
275
+ });
276
+ describe('数据边界', () => {
277
+ it('应该能处理较大的数据', async () => {
278
+ let receivedData = [];
279
+ const { provider, consumer } = createQueue(async (dataList) => {
280
+ receivedData = dataList;
281
+ return catchIt(() => { });
282
+ }, { batchSize: 5 });
283
+ await provider.connect();
284
+ await consumer.connect();
285
+ const largeString = 'x'.repeat(10 * 1024); // 10KB
286
+ await provider.enqueue(Array.from({ length: 5 }, () => ({ data: largeString })));
287
+ await new Promise(resolve => setTimeout(resolve, 1000));
288
+ expect(receivedData).toHaveLength(5);
289
+ expect(receivedData[0].data).toBe(largeString);
290
+ });
291
+ });
292
+ describe('超时恢复', () => {
293
+ it('超时的任务应该直接标记为失败', { timeout: 15000 }, async () => {
294
+ let processCount = 0;
295
+ const { provider, consumer } = createQueue(async (dataList) => {
296
+ processCount++;
297
+ // 模拟处理卡住,永远不完成 (但超时机制会检测到)
298
+ await new Promise(resolve => setTimeout(resolve, 20000));
299
+ return catchIt(() => { });
300
+ }, {
301
+ batchSize: 1,
302
+ processingTimeout: 500, // 500ms 超时
303
+ consumerInterval: 100,
304
+ });
305
+ await provider.connect();
306
+ await consumer.connect();
307
+ await provider.enqueue([{ value: 1 }]);
308
+ // 等待任务被处理和超时恢复 (需要等待至少2个恢复周期: 10秒)
309
+ await new Promise(resolve => setTimeout(resolve, 12000));
310
+ const stats = await consumer.statistics();
311
+ // 超时后直接标记为失败,不应该重试
312
+ expect(stats.failed).toBe(1);
313
+ expect(stats.pending).toBe(0);
314
+ expect(stats.processing).toBe(0);
315
+ expect(stats.completed).toBe(0);
316
+ // 应该只处理一次,不会重试
317
+ expect(processCount).toBe(1);
318
+ });
319
+ it('未超时的失败任务应该正常重试', async () => {
320
+ let attemptCount = 0;
321
+ const { provider, consumer } = createQueue(async () => {
322
+ attemptCount++;
323
+ throw new Error('Task failed');
324
+ }, {
325
+ batchSize: 1,
326
+ maxRetries: 3,
327
+ processingTimeout: 5000, // 5秒超时,足够长不会触发
328
+ consumerInterval: 100,
329
+ });
330
+ await provider.connect();
331
+ await consumer.connect();
332
+ await provider.enqueue([{ value: 1 }]);
333
+ // 等待所有重试完成
334
+ await new Promise(resolve => setTimeout(resolve, 3000));
335
+ const stats = await consumer.statistics();
336
+ // 应该重试 3 次后失败
337
+ expect(stats.failed).toBe(1);
338
+ expect(attemptCount).toBe(4); // 初始 1 次 + 3 次重试
339
+ });
340
+ });
341
+ });
@@ -14,7 +14,7 @@ export declare abstract class RedisQueueCommon {
14
14
  protected readonly pendingQueue: string;
15
15
  protected readonly processingQueue: string;
16
16
  protected readonly completedQueue: string;
17
- constructor(config: RedisQueueCommonConfig);
17
+ constructor(queueKey: string, config?: RedisQueueCommonConfig);
18
18
  /**
19
19
  * 获取日志前缀(子类实现)
20
20
  */
@@ -1 +1 @@
1
- {"version":3,"file":"redis-queue-common.d.ts","sourceRoot":"","sources":["../../source/redis-queue/redis-queue-common.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,eAAe,EAAE,MAAM,OAAO,CAAA;AACrD,OAAO,KAAK,EAAE,MAAM,EAAY,IAAI,EAAE,sBAAsB,EAAE,MAAM,SAAS,CAAA;AAE7E;;;GAGG;AACH,8BAAsB,gBAAgB;IACpC,SAAS,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI,CAAO;IAC9C,SAAS,CAAC,eAAe,EAAE,OAAO,CAAQ;IAE1C,SAAS,CAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACnC,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;IACpC,SAAS,CAAC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAGvC,SAAS,CAAC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IACtC,SAAS,CAAC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IACvC,SAAS,CAAC,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAA;IAC1C,SAAS,CAAC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAA;gBAE7B,MAAM,EAAE,sBAAsB;IAwC1C;;OAEG;IACH,SAAS,CAAC,QAAQ,CAAC,YAAY,IAAI,MAAM;IAEzC;;OAEG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAS9B;;OAEG;IACH,UAAU,IAAI,IAAI;IAalB;;OAEG;cACa,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;IAW7D;;OAEG;IACH,SAAS,CAAC,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM;IAUlD;;OAEG;cACa,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAqChG;;;OAGG;cACa,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAqD1G;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAevG;;OAEG;IACG,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC;IAUhC;;;;;;;OAOG;cACa,cAAc,CAAC,iBAAiB,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,aAAa,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CA4E/G"}
1
+ {"version":3,"file":"redis-queue-common.d.ts","sourceRoot":"","sources":["../../source/redis-queue/redis-queue-common.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,eAAe,EAAE,MAAM,OAAO,CAAA;AACrD,OAAO,KAAK,EAAE,MAAM,EAAY,IAAI,EAAE,sBAAsB,EAAE,MAAM,SAAS,CAAA;AAE7E;;;GAGG;AACH,8BAAsB,gBAAgB;IACpC,SAAS,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI,CAAO;IAC9C,SAAS,CAAC,eAAe,EAAE,OAAO,CAAQ;IAE1C,SAAS,CAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACnC,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;IACpC,SAAS,CAAC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAGvC,SAAS,CAAC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IACtC,SAAS,CAAC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IACvC,SAAS,CAAC,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAA;IAC1C,SAAS,CAAC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAA;gBAE7B,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAE,sBAA2B;IAwCjE;;OAEG;IACH,SAAS,CAAC,QAAQ,CAAC,YAAY,IAAI,MAAM;IAEzC;;OAEG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAS9B;;OAEG;IACH,UAAU,IAAI,IAAI;IAalB;;OAEG;cACa,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;IAW7D;;OAEG;IACH,SAAS,CAAC,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM;IAUlD;;OAEG;cACa,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAqChG;;;OAGG;cACa,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAqD1G;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAevG;;OAEG;IACG,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC;IAUhC;;;;;;;OAOG;cACa,cAAc,CAAC,iBAAiB,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,aAAa,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CA4E/G"}
@@ -14,7 +14,7 @@ export class RedisQueueCommon {
14
14
  pendingQueue;
15
15
  processingQueue;
16
16
  completedQueue;
17
- constructor(config) {
17
+ constructor(queueKey, config = {}) {
18
18
  // 验证必填参数
19
19
  if (!config.redisUrl && !config.redisClient) {
20
20
  throw new Error('[RedisQueue] Either redisUrl or redisClient is required');
@@ -22,20 +22,20 @@ export class RedisQueueCommon {
22
22
  if (config.redisUrl && config.redisClient) {
23
23
  throw new Error('[RedisQueue] Cannot specify both redisUrl and redisClient');
24
24
  }
25
- if (!config.queueKey) {
25
+ if (!queueKey) {
26
26
  throw new Error('[RedisQueue] queueKey is required');
27
27
  }
28
- if (config.queueKey.length < 6) {
28
+ if (queueKey.length < 6) {
29
29
  throw new Error('[RedisQueue] queueKey must be at least 6 characters long');
30
30
  }
31
31
  this.redisUrl = config.redisUrl;
32
- this.queueKey = config.queueKey;
32
+ this.queueKey = queueKey;
33
33
  this.cleanupDelay = config.cleanupDelay ?? 86400; // 24 hours
34
34
  // 初始化不同状态队列的键名
35
- this.failedQueue = `${config.queueKey}:failed`;
36
- this.pendingQueue = `${config.queueKey}:pending`;
37
- this.completedQueue = `${config.queueKey}:completed`;
38
- this.processingQueue = `${config.queueKey}:processing`;
35
+ this.failedQueue = `${queueKey}:failed`;
36
+ this.pendingQueue = `${queueKey}:pending`;
37
+ this.completedQueue = `${queueKey}:completed`;
38
+ this.processingQueue = `${queueKey}:processing`;
39
39
  // 使用外部客户端或创建新客户端
40
40
  if (config.redisClient) {
41
41
  this.redis = config.redisClient;
@@ -35,10 +35,7 @@ describe('RedisQueueCommon', () => {
35
35
  let instance;
36
36
  const queueKey = `test:common:${Date.now()}:${Math.random()}`;
37
37
  beforeEach(async () => {
38
- instance = new TestRedisQueueCommon({
39
- redisUrl: REDIS_URL,
40
- queueKey,
41
- cleanupDelay: 3600,
38
+ instance = new TestRedisQueueCommon(queueKey, { redisUrl: REDIS_URL, cleanupDelay: 3600,
42
39
  });
43
40
  await instance.connect();
44
41
  });
@@ -231,62 +228,38 @@ describe('RedisQueueCommon', () => {
231
228
  describe('队列键名验证', () => {
232
229
  it('应该拒绝长度小于6的 queueKey', () => {
233
230
  expect(() => {
234
- new TestRedisQueueCommon({
235
- redisUrl: REDIS_URL,
236
- queueKey: 'short',
237
- cleanupDelay: 3600,
238
- });
231
+ new TestRedisQueueCommon('short', { redisUrl: REDIS_URL, cleanupDelay: 3600 });
239
232
  }).toThrow('queueKey must be at least 6 characters long');
240
233
  });
241
234
  it('应该接受长度大于等于6的 queueKey', () => {
242
235
  expect(() => {
243
- new TestRedisQueueCommon({
244
- redisUrl: REDIS_URL,
245
- queueKey: 'valid-key',
246
- cleanupDelay: 3600,
247
- });
236
+ new TestRedisQueueCommon('valid-key', { redisUrl: REDIS_URL, cleanupDelay: 3600 });
248
237
  }).not.toThrow();
249
238
  });
250
239
  });
251
240
  describe('cleanupDelay 配置', () => {
252
241
  it('应该使用默认的 cleanupDelay', () => {
253
- const defaultInstance = new TestRedisQueueCommon({
254
- redisUrl: REDIS_URL,
255
- queueKey: 'test-default',
256
- });
242
+ const defaultInstance = new TestRedisQueueCommon('test-default', { redisUrl: REDIS_URL });
257
243
  expect(defaultInstance['cleanupDelay']).toBe(86400); // 默认 24 小时
258
244
  });
259
245
  it('应该使用自定义的 cleanupDelay', () => {
260
- const customInstance = new TestRedisQueueCommon({
261
- redisUrl: REDIS_URL,
262
- queueKey: 'test-custom',
263
- cleanupDelay: 7200,
264
- });
246
+ const customInstance = new TestRedisQueueCommon('test-custom', { redisUrl: REDIS_URL, cleanupDelay: 7200 });
265
247
  expect(customInstance['cleanupDelay']).toBe(7200);
266
248
  });
267
249
  });
268
250
  describe('构造函数参数验证', () => {
269
251
  it('应该拒绝空的 redisUrl 和 redisClient', () => {
270
252
  expect(() => {
271
- new TestRedisQueueCommon({
272
- redisUrl: '',
273
- queueKey: 'test-queue',
274
- });
253
+ new TestRedisQueueCommon('test-queue', { redisUrl: '' });
275
254
  }).toThrow('Either redisUrl or redisClient is required');
276
255
  });
277
256
  it('应该拒绝空的 queueKey', () => {
278
257
  expect(() => {
279
- new TestRedisQueueCommon({
280
- redisUrl: REDIS_URL,
281
- queueKey: '',
282
- });
258
+ new TestRedisQueueCommon('', { redisUrl: REDIS_URL });
283
259
  }).toThrow('queueKey is required');
284
260
  });
285
261
  it('应该正确初始化所有队列键名', () => {
286
- const testQueue = new TestRedisQueueCommon({
287
- redisUrl: REDIS_URL,
288
- queueKey: 'my-queue',
289
- });
262
+ const testQueue = new TestRedisQueueCommon('my-queue', { redisUrl: REDIS_URL });
290
263
  const config = testQueue.getConfig();
291
264
  expect(config.pendingQueue).toBe('my-queue:pending');
292
265
  expect(config.processingQueue).toBe('my-queue:processing');
@@ -303,9 +276,8 @@ describe('RedisQueueCommon', () => {
303
276
  it('应该能够在断开后重新连接', async () => {
304
277
  instance.disconnect();
305
278
  await new Promise(resolve => setTimeout(resolve, 100));
306
- const newInstance = new TestRedisQueueCommon({
279
+ const newInstance = new TestRedisQueueCommon(`test:reconnect:${Date.now()}`, {
307
280
  redisUrl: REDIS_URL,
308
- queueKey: `test:reconnect:${Date.now()}`,
309
281
  });
310
282
  await newInstance.connect();
311
283
  expect(newInstance.getRedisClient()?.isOpen).toBe(true);
@@ -314,10 +286,7 @@ describe('RedisQueueCommon', () => {
314
286
  it('应该正确处理 Redis 错误事件', async () => {
315
287
  const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
316
288
  // 创建一个带有错误的实例
317
- const errorInstance = new TestRedisQueueCommon({
318
- redisUrl: REDIS_URL,
319
- queueKey: 'test-error',
320
- });
289
+ const errorInstance = new TestRedisQueueCommon('test-error', { redisUrl: REDIS_URL });
321
290
  // 模拟 Redis 错误
322
291
  const redis = errorInstance.getRedisClient();
323
292
  if (redis) {
@@ -548,7 +517,9 @@ describe('RedisQueueCommon', () => {
548
517
  await instance.getRedisClient()?.setEx(taskKey, 3600, JSON.stringify(task));
549
518
  const retrieved = await instance.testGetTask(taskId);
550
519
  expect(retrieved).toEqual(task);
551
- expect((retrieved?.data).items.length).toBe(1000);
520
+ if (retrieved && 'items' in retrieved.data) {
521
+ expect(retrieved.data.items.length).toBe(1000);
522
+ }
552
523
  });
553
524
  it('应该快速执行统计查询', async () => {
554
525
  // 添加一些任务
@@ -565,10 +536,7 @@ describe('RedisQueueCommon', () => {
565
536
  // 跳过此测试,因为连接超时等待时间过长
566
537
  it.skip('应该优雅处理连接失败', async () => {
567
538
  const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
568
- const badInstance = new TestRedisQueueCommon({
569
- redisUrl: 'redis://invalid-host:9999',
570
- queueKey: 'test-bad',
571
- });
539
+ const badInstance = new TestRedisQueueCommon('test-bad', { redisUrl: 'redis://invalid-host:9999' });
572
540
  // 设置超时时间更短,并立即尝试连接
573
541
  await expect(badInstance.connect()).rejects.toThrow();
574
542
  // 清理
@@ -606,14 +574,8 @@ describe('RedisQueueCommon', () => {
606
574
  expect(config.failedQueue).toContain(queueKey);
607
575
  });
608
576
  it('应该为不同的 queueKey 生成不同的队列名', () => {
609
- const instance1 = new TestRedisQueueCommon({
610
- redisUrl: REDIS_URL,
611
- queueKey: 'queue-one',
612
- });
613
- const instance2 = new TestRedisQueueCommon({
614
- redisUrl: REDIS_URL,
615
- queueKey: 'queue-two',
616
- });
577
+ const instance1 = new TestRedisQueueCommon('queue-one', { redisUrl: REDIS_URL });
578
+ const instance2 = new TestRedisQueueCommon('queue-two', { redisUrl: REDIS_URL });
617
579
  const config1 = instance1.getConfig();
618
580
  const config2 = instance2.getConfig();
619
581
  expect(config1.pendingQueue).not.toBe(config2.pendingQueue);
@@ -9,14 +9,8 @@ import type { RedisQueueConsumerConfig, RedisQueueRegistry, QueueStats } from '.
9
9
  *
10
10
  * @example
11
11
  * ```ts
12
- * interface EmailTask {
13
- * to: string
14
- * subject: string
15
- * }
16
- *
17
- * const consumer = new RedisQueueConsumer<EmailTask>({
12
+ * const consumer = new RedisQueueConsumer('email-queue', {
18
13
  * redisUrl: 'redis://localhost:6379',
19
- * queueKey: 'email-queue',
20
14
  * concurrency: 5,
21
15
  * handler: async (data) => {
22
16
  * await sendEmail(data.to, data.subject)
@@ -38,8 +32,9 @@ export declare class RedisQueueConsumer<K extends keyof RedisQueueRegistry> exte
38
32
  private consumerInterval;
39
33
  private recoveryInterval;
40
34
  private processingTasks;
35
+ private processingPromises;
41
36
  private readonly config;
42
- constructor(config: RedisQueueConsumerConfig<K>);
37
+ constructor(queueKey: K, config: RedisQueueConsumerConfig<K>);
43
38
  protected getLogPrefix(): string;
44
39
  /**
45
40
  * 连接 Redis 并自动启动消费者
@@ -47,8 +42,9 @@ export declare class RedisQueueConsumer<K extends keyof RedisQueueRegistry> exte
47
42
  connect(): Promise<void>;
48
43
  /**
49
44
  * 断开 Redis 连接并停止消费者
45
+ * 会等待所有正在处理的任务完成后再断开连接
50
46
  */
51
- disconnect(): void;
47
+ disconnect(): Promise<void>;
52
48
  /**
53
49
  * 处理单个任务
54
50
  */
@@ -1 +1 @@
1
- {"version":3,"file":"redis-queue-consumer.d.ts","sourceRoot":"","sources":["../../source/redis-queue/redis-queue-consumer.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AACvD,OAAO,KAAK,EAAE,wBAAwB,EAAe,kBAAkB,EAAgB,UAAU,EAAE,MAAM,SAAS,CAAA;AAElH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,qBAAa,kBAAkB,CAAC,CAAC,SAAS,MAAM,kBAAkB,CAAE,SAAQ,gBAAgB;IAC1F,OAAO,CAAC,eAAe,CAAQ;IAC/B,OAAO,CAAC,gBAAgB,CAA8B;IACtD,OAAO,CAAC,gBAAgB,CAA8B;IACtD,OAAO,CAAC,eAAe,CAAI;IAE3B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAUtB;gBAEW,MAAM,EAAE,wBAAwB,CAAC,CAAC,CAAC;IA2B/C,SAAS,CAAC,YAAY,IAAI,MAAM;IAIhC;;OAEG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAO9B;;OAEG;IACH,UAAU,IAAI,IAAI;IAMlB;;OAEG;YACW,WAAW;IA4EzB;;OAEG;IACH,OAAO,CAAC,aAAa;IAoBrB;;OAEG;IACH,OAAO,CAAC,YAAY;IAQpB;;OAEG;IACH,OAAO,CAAC,aAAa;IA4ErB;;OAEG;IACH,OAAO,CAAC,YAAY;IASpB;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,UAAU,CAAC;IAevC;;OAEG;IACG,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC;CAMjC"}
1
+ {"version":3,"file":"redis-queue-consumer.d.ts","sourceRoot":"","sources":["../../source/redis-queue/redis-queue-consumer.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AACvD,OAAO,KAAK,EAAE,wBAAwB,EAAe,kBAAkB,EAAgB,UAAU,EAAE,MAAM,SAAS,CAAA;AAElH;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,qBAAa,kBAAkB,CAAC,CAAC,SAAS,MAAM,kBAAkB,CAAE,SAAQ,gBAAgB;IAC1F,OAAO,CAAC,eAAe,CAAQ;IAC/B,OAAO,CAAC,gBAAgB,CAA8B;IACtD,OAAO,CAAC,gBAAgB,CAA8B;IACtD,OAAO,CAAC,eAAe,CAAI;IAC3B,OAAO,CAAC,kBAAkB,CAA2B;IAErD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAOtB;gBAEW,QAAQ,EAAE,CAAC,EAAE,MAAM,EAAE,wBAAwB,CAAC,CAAC,CAAC;IAuB5D,SAAS,CAAC,YAAY,IAAI,MAAM;IAIhC;;OAEG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAO9B;;;OAGG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAgBjC;;OAEG;YACW,WAAW;IA2FzB;;OAEG;IACH,OAAO,CAAC,aAAa;IAoBrB;;OAEG;IACH,OAAO,CAAC,YAAY;IAQpB;;OAEG;IACH,OAAO,CAAC,aAAa;IA4ErB;;OAEG;IACH,OAAO,CAAC,YAAY;IASpB;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,UAAU,CAAC;IAevC;;OAEG;IACG,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC;CAMjC"}