@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
@@ -1,48 +1,43 @@
1
- import { describe, it, expect, afterEach } from 'vitest';
2
- import { RedisQueueProvider } from './redis-queue-provider';
3
- const REDIS_URL = process.env.REDIS_URL || 'redis://localhost:6379';
4
- describe('RedisQueueProvider', () => {
1
+ import { describe, it, expect, afterEach } from "vitest";
2
+ import { RedisQueueProvider } from "./redis-queue-provider";
3
+ import { clearQueue, getQueueTasks } from "./test-helpers";
4
+ const REDIS_URL = process.env.REDIS_URL || "redis://localhost:6379";
5
+ describe("RedisQueueProvider", () => {
5
6
  const providers = [];
6
7
  afterEach(async () => {
7
8
  for (const provider of providers) {
8
- try {
9
- await provider.clear();
10
- }
11
- catch (error) {
12
- // 忽略清理错误
13
- }
9
+ await clearQueue(provider);
14
10
  provider.disconnect();
15
11
  }
16
12
  providers.length = 0;
17
13
  });
18
14
  const createProvider = (options) => {
19
15
  const uniqueKey = `test:provider:${Date.now()}:${Math.random()}`;
20
- const provider = new RedisQueueProvider({
16
+ const provider = new RedisQueueProvider(uniqueKey, {
21
17
  redisUrl: REDIS_URL,
22
- queueKey: uniqueKey,
23
18
  ...options,
24
19
  });
25
20
  providers.push(provider);
26
21
  return provider;
27
22
  };
28
- describe('连接管理', () => {
29
- it('应该成功连接到 Redis', async () => {
23
+ describe("连接管理", () => {
24
+ it("应该成功连接到 Redis", async () => {
30
25
  const provider = createProvider();
31
26
  await provider.connect();
32
27
  const health = await provider.health();
33
28
  expect(health).toBe(true);
34
29
  });
35
30
  });
36
- describe('任务入队', () => {
37
- it('应该成功将任务推入队列', async () => {
31
+ describe("任务入队", () => {
32
+ it("应该成功将任务推入队列", async () => {
38
33
  const provider = createProvider();
39
34
  await provider.connect();
40
- const taskId = await provider.enqueue({ foo: 'bar' });
35
+ const taskId = await provider.enqueue({ foo: "bar" });
41
36
  expect(taskId).toBeTruthy();
42
37
  const stats = await provider.statistics();
43
38
  expect(stats.pending).toBe(1);
44
39
  });
45
- it('应该能够批量入队任务', async () => {
40
+ it("应该能够批量入队任务", async () => {
46
41
  const provider = createProvider();
47
42
  await provider.connect();
48
43
  const taskIds = await provider.enqueue([
@@ -54,34 +49,30 @@ describe('RedisQueueProvider', () => {
54
49
  const stats = await provider.statistics();
55
50
  expect(stats.pending).toBe(3);
56
51
  });
57
- it('入队的任务应该包含正确的元数据', async () => {
52
+ it("入队的任务应该包含正确的元数据", async () => {
58
53
  const provider = createProvider();
59
54
  await provider.connect();
60
- const taskId = await provider.enqueue({ test: 'data', number: 42 });
55
+ const taskId = await provider.enqueue({ test: "data", number: 42 });
61
56
  const task = await provider.getTask(taskId);
62
57
  expect(task).toBeTruthy();
63
58
  expect(task?.id).toBe(taskId);
64
- expect(task?.data).toEqual({ test: 'data', number: 42 });
65
- expect(task?.status).toBe('pending');
59
+ expect(task?.data).toEqual({ test: "data", number: 42 });
60
+ expect(task?.status).toBe("pending");
66
61
  expect(task?.retryCount).toBe(0);
67
62
  });
68
63
  });
69
- describe('队列操作', () => {
70
- it('应该正确返回队列统计信息', async () => {
64
+ describe("队列操作", () => {
65
+ it("应该正确返回队列统计信息", async () => {
71
66
  const provider = createProvider();
72
67
  await provider.connect();
73
- await provider.enqueue([
74
- { test: '1' },
75
- { test: '2' },
76
- { test: '3' },
77
- ]);
68
+ await provider.enqueue([{ test: "1" }, { test: "2" }, { test: "3" }]);
78
69
  const stats = await provider.statistics();
79
70
  expect(stats.pending).toBe(3);
80
71
  expect(stats.processing).toBe(0);
81
72
  expect(stats.completed).toBe(0);
82
73
  expect(stats.failed).toBe(0);
83
74
  });
84
- it('应该能够清空队列', async () => {
75
+ it("应该能够清空队列", async () => {
85
76
  const provider = createProvider();
86
77
  await provider.connect();
87
78
  await provider.enqueue([{ data: 1 }, { data: 2 }]);
@@ -89,23 +80,67 @@ describe('RedisQueueProvider', () => {
89
80
  const stats = await provider.statistics();
90
81
  expect(stats.pending).toBe(0);
91
82
  });
83
+ it("应该能够使用 getQueueTasks 获取特定状态的任务", async () => {
84
+ const provider = createProvider();
85
+ await provider.connect();
86
+ await provider.enqueue([
87
+ { name: "task1" },
88
+ { name: "task2" },
89
+ { name: "task3" },
90
+ ]);
91
+ const pendingTasks = await getQueueTasks(provider, "pending");
92
+ expect(pendingTasks).toHaveLength(3);
93
+ expect(pendingTasks.every(t => t.status === "pending")).toBe(true);
94
+ // 验证任务数据
95
+ const taskNames = pendingTasks.map(t => t.data.name);
96
+ expect(taskNames).toContain("task1");
97
+ expect(taskNames).toContain("task2");
98
+ expect(taskNames).toContain("task3");
99
+ });
92
100
  });
93
- describe('幂等性', () => {
94
- it('应该支持在 data 中指定 id 来实现幂等性', async () => {
101
+ describe("幂等性", () => {
102
+ it("应该支持在 data 中指定 id 来实现幂等性", async () => {
95
103
  const provider = createProvider();
96
104
  await provider.connect();
97
- await provider.enqueue({ id: 'unique-1', value: 1 });
98
- await provider.enqueue({ id: 'unique-1', value: 2 });
99
- await provider.enqueue({ id: 'unique-2', value: 3 });
105
+ await provider.enqueue({ id: "unique-1", value: 1 });
106
+ await provider.enqueue({ id: "unique-1", value: 2 });
107
+ await provider.enqueue({ id: "unique-2", value: 3 });
100
108
  const stats = await provider.statistics();
101
109
  expect(stats.pending).toBe(2); // 只有 unique-1 和 unique-2
102
110
  });
111
+ it("应该拒绝非 string 类型的 id 并抛出 TypeError", async () => {
112
+ const provider = createProvider();
113
+ await provider.connect();
114
+ // 尝试传入数字类型的 id,应该抛出 TypeError
115
+ await expect(async () => {
116
+ await provider.enqueue({ id: 123, value: "test" });
117
+ }).rejects.toThrow(TypeError);
118
+ await expect(async () => {
119
+ await provider.enqueue({ id: 123, value: "test" });
120
+ }).rejects.toThrow(/must be a string/);
121
+ // 尝试传入布尔类型的 id,应该抛出 TypeError
122
+ await expect(async () => {
123
+ await provider.enqueue({ id: true, value: "test" });
124
+ }).rejects.toThrow(TypeError);
125
+ // 尝试传入对象类型的 id,应该抛出 TypeError
126
+ await expect(async () => {
127
+ await provider.enqueue({
128
+ id: { nested: "value" },
129
+ value: "test",
130
+ });
131
+ }).rejects.toThrow(TypeError);
132
+ // 字符串类型的 id 应该正常工作
133
+ await expect(provider.enqueue({ id: "123", value: "test" })).resolves.not.toThrow();
134
+ // 验证只有字符串 ID 的任务被入队
135
+ const stats = await provider.statistics();
136
+ expect(stats.pending).toBe(1);
137
+ });
103
138
  });
104
- describe('延迟处理', () => {
105
- it('应该支持延迟入队任务', async () => {
139
+ describe("延迟处理", () => {
140
+ it("应该支持延迟入队任务", async () => {
106
141
  const provider = createProvider({ processingDelay: 2000 });
107
142
  await provider.connect();
108
- const taskId = await provider.enqueue({ test: true });
143
+ const taskId = (await provider.enqueue({ test: true }));
109
144
  const task = await provider.getTask(taskId);
110
145
  expect(task?.delayUntil).toBeDefined();
111
146
  expect(task?.delayUntil).toBeGreaterThan(Date.now());
@@ -0,0 +1,112 @@
1
+ import type { RedisQueueBatchConsumer } from './redis-queue-batch-consumer';
2
+ import { RedisQueueConsumer } from './redis-queue-consumer';
3
+ import type { RedisQueueProvider } from './redis-queue-provider';
4
+ import type { RedisQueueRegistry, Task } from './types';
5
+ /**
6
+ * 测试辅助函数:立即派发任务到消费者
7
+ *
8
+ * 这个辅助函数提供了一个简洁的测试接口,让你能够立即派发任务到消费者,
9
+ * 而不需要等待定时轮询机制。这使得测试更加快速和可控。
10
+ *
11
+ * @param consumer RedisQueueConsumer 或 RedisQueueBatchConsumer 实例
12
+ * @param taskIds 要派发的任务 ID 列表
13
+ * @returns Promise,等待所有任务处理完成
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * import { dispatchQueueTask } from '@taicode/common-server/redis-queue/test-helpers'
18
+ * import { RedisQueueProvider, RedisQueueConsumer } from '@taicode/common-server/redis-queue'
19
+ *
20
+ * describe('My Queue Tests', () => {
21
+ * it('should process tasks', async () => {
22
+ * const provider = new RedisQueueProvider({ ... })
23
+ * const consumer = new RedisQueueConsumer({ ... })
24
+ *
25
+ * await provider.connect()
26
+ * await consumer.connect()
27
+ *
28
+ * // 添加任务
29
+ * const taskId = await provider.enqueue({ data: 'test' })
30
+ *
31
+ * // 立即派发任务进行测试
32
+ * await dispatchQueueTask(consumer, [taskId as string])
33
+ *
34
+ * // 验证结果
35
+ * const stats = await consumer.statistics()
36
+ * expect(stats.completed).toBe(1)
37
+ * })
38
+ * })
39
+ * ```
40
+ */
41
+ export declare function dispatchQueueTask<K extends keyof RedisQueueRegistry>(consumer: RedisQueueConsumer<K> | RedisQueueBatchConsumer<K>, taskIds: string[]): Promise<void>;
42
+ /**
43
+ * 测试辅助函数:等待队列达到指定状态
44
+ *
45
+ * @param instance Provider 或 Consumer 实例
46
+ * @param predicate 状态断言函数
47
+ * @param options 配置选项
48
+ * @returns Promise,在条件满足时 resolve
49
+ *
50
+ * @example
51
+ * ```ts
52
+ * // 使用 consumer 等待所有任务完成
53
+ * await waitQueueCompletion(consumer, stats => stats.pending === 0 && stats.processing === 0)
54
+ *
55
+ * // 使用 provider 等待至少完成 10 个任务
56
+ * await waitQueueCompletion(provider, stats => stats.completed >= 10, { timeout: 5000 })
57
+ * ```
58
+ */
59
+ export declare function waitQueueCompletion<K extends keyof RedisQueueRegistry>(instance: RedisQueueProvider<K> | RedisQueueConsumer<K> | RedisQueueBatchConsumer<K>, predicate: (stats: {
60
+ pending: number;
61
+ processing: number;
62
+ completed: number;
63
+ failed: number;
64
+ }) => boolean, options?: {
65
+ timeout?: number;
66
+ interval?: number;
67
+ }): Promise<void>;
68
+ /**
69
+ * 测试辅助函数:清空队列所有状态
70
+ *
71
+ * 用于测试前后的清理工作,支持 Provider 或 Consumer 实例
72
+ *
73
+ * @param instance Provider 或 Consumer 实例
74
+ * @returns Promise
75
+ *
76
+ * @example
77
+ * ```ts
78
+ * // 使用 consumer 清理
79
+ * afterEach(async () => {
80
+ * await clearQueue(consumer)
81
+ * consumer.disconnect()
82
+ * })
83
+ *
84
+ * // 使用 provider 清理
85
+ * afterEach(async () => {
86
+ * await clearQueue(provider)
87
+ * provider.disconnect()
88
+ * })
89
+ * ```
90
+ */
91
+ export declare function clearQueue<K extends keyof RedisQueueRegistry>(instance: RedisQueueProvider<K> | RedisQueueConsumer<K> | RedisQueueBatchConsumer<K>): Promise<void>;
92
+ /**
93
+ * 测试辅助函数:获取队列中的所有任务详情
94
+ *
95
+ * 用于测试验证,可以检查任务的完整状态
96
+ *
97
+ * @param provider Provider 实例
98
+ * @param status 可选,筛选特定状态的任务
99
+ * @returns 任务列表
100
+ *
101
+ * @example
102
+ * ```typescript
103
+ * // 获取所有失败的任务
104
+ * const failedTasks = await getQueueTasks(provider, 'failed')
105
+ * expect(failedTasks).toHaveLength(3)
106
+ *
107
+ * // 验证任务数据
108
+ * expect(failedTasks[0].data).toMatchObject({ id: 123 })
109
+ * ```
110
+ */
111
+ export declare function getQueueTasks<K extends keyof RedisQueueRegistry>(provider: RedisQueueProvider<K>, status?: 'pending' | 'processing' | 'completed' | 'failed'): Promise<Task<RedisQueueRegistry[K]>[]>;
112
+ //# sourceMappingURL=test-helpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-helpers.d.ts","sourceRoot":"","sources":["../../source/redis-queue/test-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAA;AAC3E,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAA;AAC3D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAA;AAChE,OAAO,KAAK,EAAE,kBAAkB,EAAE,IAAI,EAAE,MAAM,SAAS,CAAA;AAEvD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,wBAAsB,iBAAiB,CAAC,CAAC,SAAS,MAAM,kBAAkB,EACxE,QAAQ,EAAE,kBAAkB,CAAC,CAAC,CAAC,GAAG,uBAAuB,CAAC,CAAC,CAAC,EAC5D,OAAO,EAAE,MAAM,EAAE,GAChB,OAAO,CAAC,IAAI,CAAC,CA+Ff;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,mBAAmB,CAAC,CAAC,SAAS,MAAM,kBAAkB,EAC1E,QAAQ,EAAE,kBAAkB,CAAC,CAAC,CAAC,GAAG,kBAAkB,CAAC,CAAC,CAAC,GAAG,uBAAuB,CAAC,CAAC,CAAC,EACpF,SAAS,EAAE,CAAC,KAAK,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,KAAK,OAAO,EACzG,OAAO,GAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAO,GACpD,OAAO,CAAC,IAAI,CAAC,CAiBf;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAsB,UAAU,CAAC,CAAC,SAAS,MAAM,kBAAkB,EACjE,QAAQ,EAAE,kBAAkB,CAAC,CAAC,CAAC,GAAG,kBAAkB,CAAC,CAAC,CAAC,GAAG,uBAAuB,CAAC,CAAC,CAAC,GACnF,OAAO,CAAC,IAAI,CAAC,CAcf;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,aAAa,CAAC,CAAC,SAAS,MAAM,kBAAkB,EACpE,QAAQ,EAAE,kBAAkB,CAAC,CAAC,CAAC,EAC/B,MAAM,CAAC,EAAE,SAAS,GAAG,YAAY,GAAG,WAAW,GAAG,QAAQ,GACzD,OAAO,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAmCxC"}
@@ -0,0 +1,242 @@
1
+ import { RedisQueueConsumer } from './redis-queue-consumer';
2
+ /**
3
+ * 测试辅助函数:立即派发任务到消费者
4
+ *
5
+ * 这个辅助函数提供了一个简洁的测试接口,让你能够立即派发任务到消费者,
6
+ * 而不需要等待定时轮询机制。这使得测试更加快速和可控。
7
+ *
8
+ * @param consumer RedisQueueConsumer 或 RedisQueueBatchConsumer 实例
9
+ * @param taskIds 要派发的任务 ID 列表
10
+ * @returns Promise,等待所有任务处理完成
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * import { dispatchQueueTask } from '@taicode/common-server/redis-queue/test-helpers'
15
+ * import { RedisQueueProvider, RedisQueueConsumer } from '@taicode/common-server/redis-queue'
16
+ *
17
+ * describe('My Queue Tests', () => {
18
+ * it('should process tasks', async () => {
19
+ * const provider = new RedisQueueProvider({ ... })
20
+ * const consumer = new RedisQueueConsumer({ ... })
21
+ *
22
+ * await provider.connect()
23
+ * await consumer.connect()
24
+ *
25
+ * // 添加任务
26
+ * const taskId = await provider.enqueue({ data: 'test' })
27
+ *
28
+ * // 立即派发任务进行测试
29
+ * await dispatchQueueTask(consumer, [taskId as string])
30
+ *
31
+ * // 验证结果
32
+ * const stats = await consumer.statistics()
33
+ * expect(stats.completed).toBe(1)
34
+ * })
35
+ * })
36
+ * ```
37
+ */
38
+ export async function dispatchQueueTask(consumer, taskIds) {
39
+ if (taskIds.length === 0) {
40
+ return;
41
+ }
42
+ // @ts-ignore - 访问 protected 属性用于测试
43
+ const redis = consumer.redis;
44
+ // @ts-ignore
45
+ const queueKey = consumer.queueKey;
46
+ // @ts-ignore
47
+ const pendingQueue = consumer.pendingQueue;
48
+ // @ts-ignore
49
+ const processingQueue = consumer.processingQueue;
50
+ if (!redis || !redis.isOpen) {
51
+ throw new Error('Redis client is not connected');
52
+ }
53
+ // 使用 Lua 脚本将指定的任务从 pending 移到 processing 队列
54
+ const moveTasksScript = `
55
+ local pendingQueue = KEYS[1]
56
+ local processingQueue = KEYS[2]
57
+ local moved = 0
58
+
59
+ for i = 3, #KEYS do
60
+ local taskId = KEYS[i]
61
+ -- 从 pending 队列移除
62
+ local removed = redis.call('LREM', pendingQueue, 1, taskId)
63
+ if (removed > 0) then
64
+ -- 添加到 processing 队列
65
+ redis.call('RPUSH', processingQueue, taskId)
66
+ moved = moved + 1
67
+ end
68
+ end
69
+
70
+ return moved
71
+ `;
72
+ // 持续处理直到所有任务完成或失败
73
+ let remainingTasks = [...taskIds];
74
+ let maxAttempts = 100; // 防止无限循环
75
+ let attempts = 0;
76
+ while (remainingTasks.length > 0 && attempts < maxAttempts) {
77
+ attempts++;
78
+ // 移动任务到 processing 队列
79
+ await redis.eval(moveTasksScript, {
80
+ keys: [pendingQueue, processingQueue, ...remainingTasks],
81
+ arguments: [],
82
+ });
83
+ // 处理任务
84
+ if (consumer instanceof RedisQueueConsumer) {
85
+ // RedisQueueConsumer: 逐个处理任务
86
+ const promises = remainingTasks.map(taskId =>
87
+ // @ts-ignore
88
+ consumer.processTask(taskId).catch(() => {
89
+ // 忽略错误,让 consumer 内部处理
90
+ }));
91
+ await Promise.all(promises);
92
+ }
93
+ else {
94
+ // RedisQueueBatchConsumer: 按照 consumer 的 batchSize 分批处理任务
95
+ // @ts-ignore
96
+ const batchConsumer = consumer;
97
+ const batchSize = batchConsumer.config?.batchSize || 10;
98
+ // 分批处理任务
99
+ for (let i = 0; i < remainingTasks.length; i += batchSize) {
100
+ const batch = remainingTasks.slice(i, i + batchSize);
101
+ try {
102
+ // @ts-ignore
103
+ await batchConsumer.processBatch(batch);
104
+ }
105
+ catch (error) {
106
+ // 批次处理失败是正常的,consumer 会处理重试逻辑
107
+ }
108
+ }
109
+ }
110
+ // 等待一小段时间让重试任务被放回 pending 队列
111
+ await new Promise(resolve => setTimeout(resolve, 10));
112
+ // 检查还有哪些任务在 pending 队列中(需要重试)
113
+ const stillPending = [];
114
+ for (const taskId of remainingTasks) {
115
+ // @ts-ignore
116
+ const task = await consumer.getTask(taskId);
117
+ if (task && task.status === 'pending') {
118
+ stillPending.push(taskId);
119
+ }
120
+ }
121
+ remainingTasks = stillPending;
122
+ }
123
+ }
124
+ /**
125
+ * 测试辅助函数:等待队列达到指定状态
126
+ *
127
+ * @param instance Provider 或 Consumer 实例
128
+ * @param predicate 状态断言函数
129
+ * @param options 配置选项
130
+ * @returns Promise,在条件满足时 resolve
131
+ *
132
+ * @example
133
+ * ```ts
134
+ * // 使用 consumer 等待所有任务完成
135
+ * await waitQueueCompletion(consumer, stats => stats.pending === 0 && stats.processing === 0)
136
+ *
137
+ * // 使用 provider 等待至少完成 10 个任务
138
+ * await waitQueueCompletion(provider, stats => stats.completed >= 10, { timeout: 5000 })
139
+ * ```
140
+ */
141
+ export async function waitQueueCompletion(instance, predicate, options = {}) {
142
+ const timeout = options.timeout ?? 10000; // 默认 10 秒超时
143
+ const interval = options.interval ?? 100; // 默认 100ms 检查间隔
144
+ const startTime = Date.now();
145
+ while (Date.now() - startTime < timeout) {
146
+ const stats = await instance.statistics();
147
+ if (predicate(stats)) {
148
+ return;
149
+ }
150
+ await new Promise(resolve => setTimeout(resolve, interval));
151
+ }
152
+ const finalStats = await instance.statistics();
153
+ throw new Error(`Queue state timeout after ${timeout}ms. Final state: ${JSON.stringify(finalStats)}`);
154
+ }
155
+ /**
156
+ * 测试辅助函数:清空队列所有状态
157
+ *
158
+ * 用于测试前后的清理工作,支持 Provider 或 Consumer 实例
159
+ *
160
+ * @param instance Provider 或 Consumer 实例
161
+ * @returns Promise
162
+ *
163
+ * @example
164
+ * ```ts
165
+ * // 使用 consumer 清理
166
+ * afterEach(async () => {
167
+ * await clearQueue(consumer)
168
+ * consumer.disconnect()
169
+ * })
170
+ *
171
+ * // 使用 provider 清理
172
+ * afterEach(async () => {
173
+ * await clearQueue(provider)
174
+ * provider.disconnect()
175
+ * })
176
+ * ```
177
+ */
178
+ export async function clearQueue(instance) {
179
+ // 访问 protected 属性需要类型断言
180
+ const redis = instance.redis;
181
+ const queueKey = instance.queueKey;
182
+ if (!redis || !redis.isOpen) {
183
+ return;
184
+ }
185
+ // 删除所有队列相关的键
186
+ const keys = await redis.keys(`${queueKey}:*`);
187
+ if (keys.length > 0) {
188
+ await redis.del(keys);
189
+ }
190
+ }
191
+ /**
192
+ * 测试辅助函数:获取队列中的所有任务详情
193
+ *
194
+ * 用于测试验证,可以检查任务的完整状态
195
+ *
196
+ * @param provider Provider 实例
197
+ * @param status 可选,筛选特定状态的任务
198
+ * @returns 任务列表
199
+ *
200
+ * @example
201
+ * ```typescript
202
+ * // 获取所有失败的任务
203
+ * const failedTasks = await getQueueTasks(provider, 'failed')
204
+ * expect(failedTasks).toHaveLength(3)
205
+ *
206
+ * // 验证任务数据
207
+ * expect(failedTasks[0].data).toMatchObject({ id: 123 })
208
+ * ```
209
+ */
210
+ export async function getQueueTasks(provider, status) {
211
+ // @ts-ignore
212
+ const redis = provider.redis;
213
+ // @ts-ignore
214
+ const queueKey = provider.queueKey;
215
+ if (!redis || !redis.isOpen) {
216
+ return [];
217
+ }
218
+ const queueName = status ? `${queueKey}:${status}` : null;
219
+ const tasks = [];
220
+ if (queueName) {
221
+ // 获取特定状态队列中的所有任务 ID
222
+ const taskIds = await redis.lRange(queueName, 0, -1);
223
+ for (const taskId of taskIds) {
224
+ const taskKey = `${queueKey}:task:${taskId}`;
225
+ const taskData = await redis.get(taskKey);
226
+ if (taskData) {
227
+ tasks.push(JSON.parse(taskData));
228
+ }
229
+ }
230
+ }
231
+ else {
232
+ // 获取所有任务
233
+ const taskKeys = await redis.keys(`${queueKey}:task:*`);
234
+ for (const taskKey of taskKeys) {
235
+ const taskData = await redis.get(taskKey);
236
+ if (taskData) {
237
+ tasks.push(JSON.parse(taskData));
238
+ }
239
+ }
240
+ }
241
+ return tasks;
242
+ }
@@ -0,0 +1,28 @@
1
+ type TestTaskData = {
2
+ id?: string;
3
+ message?: string;
4
+ value?: string | number;
5
+ name?: string;
6
+ test?: string | boolean;
7
+ shouldFail?: boolean;
8
+ order?: number;
9
+ taskId?: number;
10
+ index?: number;
11
+ step?: number;
12
+ real?: boolean;
13
+ nested?: {
14
+ value: number;
15
+ };
16
+ example?: string;
17
+ payload?: string;
18
+ data?: any;
19
+ foo?: string;
20
+ number?: number;
21
+ };
22
+ declare module './types' {
23
+ interface RedisQueueRegistry {
24
+ [key: `test:${string}`]: TestTaskData;
25
+ }
26
+ }
27
+ export {};
28
+ //# sourceMappingURL=test-helpers.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-helpers.test.d.ts","sourceRoot":"","sources":["../../source/redis-queue/test-helpers.test.ts"],"names":[],"mappings":"AAcA,KAAK,YAAY,GAAG;IAClB,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IACvB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;IACvB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,MAAM,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,CAAA;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,IAAI,CAAC,EAAE,GAAG,CAAA;IACV,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB,CAAA;AAGD,OAAO,QAAQ,SAAS,CAAC;IACvB,UAAU,kBAAkB;QAC1B,CAAC,GAAG,EAAE,QAAQ,MAAM,EAAE,GAAG,YAAY,CAAA;KACtC;CACF"}