@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.
- package/output/redis-queue/index.d.ts +1 -1
- package/output/redis-queue/index.d.ts.map +1 -1
- package/output/redis-queue/index.js +1 -1
- package/output/redis-queue/redis-batch-consumer.d.ts +9 -0
- package/output/redis-queue/redis-batch-consumer.d.ts.map +1 -1
- package/output/redis-queue/redis-batch-consumer.js +63 -0
- package/output/redis-queue/redis-queue-batch-consumer.d.ts +77 -0
- package/output/redis-queue/redis-queue-batch-consumer.d.ts.map +1 -0
- package/output/redis-queue/redis-queue-batch-consumer.js +320 -0
- package/output/redis-queue/redis-queue-batch-consumer.test.d.ts +26 -0
- package/output/redis-queue/redis-queue-batch-consumer.test.d.ts.map +1 -0
- package/output/redis-queue/redis-queue-batch-consumer.test.js +341 -0
- package/output/redis-queue/redis-queue-common.d.ts +1 -1
- package/output/redis-queue/redis-queue-common.d.ts.map +1 -1
- package/output/redis-queue/redis-queue-common.js +8 -8
- package/output/redis-queue/redis-queue-common.test.js +16 -54
- package/output/redis-queue/redis-queue-consumer.d.ts +5 -9
- package/output/redis-queue/redis-queue-consumer.d.ts.map +1 -1
- package/output/redis-queue/redis-queue-consumer.js +80 -69
- package/output/redis-queue/redis-queue-consumer.test.d.ts +20 -1
- package/output/redis-queue/redis-queue-consumer.test.d.ts.map +1 -1
- package/output/redis-queue/redis-queue-consumer.test.js +89 -15
- package/output/redis-queue/redis-queue-provider.d.ts +4 -4
- package/output/redis-queue/redis-queue-provider.d.ts.map +1 -1
- package/output/redis-queue/redis-queue-provider.js +10 -6
- package/output/redis-queue/redis-queue-provider.test.d.ts +23 -2
- package/output/redis-queue/redis-queue-provider.test.d.ts.map +1 -1
- package/output/redis-queue/redis-queue-provider.test.js +73 -38
- package/output/redis-queue/test-helpers.d.ts +112 -0
- package/output/redis-queue/test-helpers.d.ts.map +1 -0
- package/output/redis-queue/test-helpers.js +242 -0
- package/output/redis-queue/test-helpers.test.d.ts +28 -0
- package/output/redis-queue/test-helpers.test.d.ts.map +1 -0
- package/output/redis-queue/test-helpers.test.js +572 -0
- package/output/redis-queue/types.d.ts +0 -7
- package/output/redis-queue/types.d.ts.map +1 -1
- package/package.json +5 -3
|
@@ -1,48 +1,43 @@
|
|
|
1
|
-
import { describe, it, expect, afterEach } from
|
|
2
|
-
import { RedisQueueProvider } from
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
31
|
+
describe("任务入队", () => {
|
|
32
|
+
it("应该成功将任务推入队列", async () => {
|
|
38
33
|
const provider = createProvider();
|
|
39
34
|
await provider.connect();
|
|
40
|
-
const taskId = await provider.enqueue({ foo:
|
|
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(
|
|
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(
|
|
52
|
+
it("入队的任务应该包含正确的元数据", async () => {
|
|
58
53
|
const provider = createProvider();
|
|
59
54
|
await provider.connect();
|
|
60
|
-
const taskId = await provider.enqueue({ test:
|
|
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:
|
|
65
|
-
expect(task?.status).toBe(
|
|
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(
|
|
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(
|
|
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(
|
|
101
|
+
describe("幂等性", () => {
|
|
102
|
+
it("应该支持在 data 中指定 id 来实现幂等性", async () => {
|
|
95
103
|
const provider = createProvider();
|
|
96
104
|
await provider.connect();
|
|
97
|
-
await provider.enqueue({ id:
|
|
98
|
-
await provider.enqueue({ id:
|
|
99
|
-
await provider.enqueue({ id:
|
|
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(
|
|
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"}
|