@taicode/common-server 3.7.4 → 3.7.7
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/LICENSE +21 -0
- package/package.json +9 -10
- package/output/context/index.d.ts +0 -6
- package/output/context/index.d.ts.map +0 -1
- package/output/context/index.js +0 -14
- package/output/context/index.test.d.ts +0 -2
- package/output/context/index.test.d.ts.map +0 -1
- package/output/context/index.test.js +0 -14
- package/output/index.d.ts +0 -5
- package/output/index.d.ts.map +0 -1
- package/output/index.js +0 -5
- package/output/rate-limiter/index.d.ts +0 -4
- package/output/rate-limiter/index.d.ts.map +0 -1
- package/output/rate-limiter/index.js +0 -3
- package/output/rate-limiter/memory-store.d.ts +0 -12
- package/output/rate-limiter/memory-store.d.ts.map +0 -1
- package/output/rate-limiter/memory-store.js +0 -46
- package/output/rate-limiter/rate-limiter.d.ts +0 -17
- package/output/rate-limiter/rate-limiter.d.ts.map +0 -1
- package/output/rate-limiter/rate-limiter.js +0 -64
- package/output/rate-limiter/types.d.ts +0 -27
- package/output/rate-limiter/types.d.ts.map +0 -1
- package/output/rate-limiter/types.js +0 -1
- package/output/redis-queue/index.d.ts +0 -8
- package/output/redis-queue/index.d.ts.map +0 -1
- package/output/redis-queue/index.js +0 -6
- package/output/redis-queue/redis-queue-batch-consumer.d.ts +0 -97
- package/output/redis-queue/redis-queue-batch-consumer.d.ts.map +0 -1
- package/output/redis-queue/redis-queue-batch-consumer.js +0 -341
- package/output/redis-queue/redis-queue-batch-consumer.test.d.ts +0 -26
- package/output/redis-queue/redis-queue-batch-consumer.test.d.ts.map +0 -1
- package/output/redis-queue/redis-queue-batch-consumer.test.js +0 -341
- package/output/redis-queue/redis-queue-common.d.ts +0 -73
- package/output/redis-queue/redis-queue-common.d.ts.map +0 -1
- package/output/redis-queue/redis-queue-common.js +0 -303
- package/output/redis-queue/redis-queue-common.test.d.ts +0 -19
- package/output/redis-queue/redis-queue-common.test.d.ts.map +0 -1
- package/output/redis-queue/redis-queue-common.test.js +0 -585
- package/output/redis-queue/redis-queue-consumer.d.ts +0 -95
- package/output/redis-queue/redis-queue-consumer.d.ts.map +0 -1
- package/output/redis-queue/redis-queue-consumer.js +0 -327
- package/output/redis-queue/redis-queue-consumer.test.d.ts +0 -26
- package/output/redis-queue/redis-queue-consumer.test.d.ts.map +0 -1
- package/output/redis-queue/redis-queue-consumer.test.js +0 -316
- package/output/redis-queue/redis-queue-provider.d.ts +0 -69
- package/output/redis-queue/redis-queue-provider.d.ts.map +0 -1
- package/output/redis-queue/redis-queue-provider.js +0 -205
- package/output/redis-queue/redis-queue-provider.test.d.ts +0 -28
- package/output/redis-queue/redis-queue-provider.test.d.ts.map +0 -1
- package/output/redis-queue/redis-queue-provider.test.js +0 -149
- package/output/redis-queue/test-helpers.d.ts +0 -112
- package/output/redis-queue/test-helpers.d.ts.map +0 -1
- package/output/redis-queue/test-helpers.js +0 -242
- package/output/redis-queue/test-helpers.test.d.ts +0 -28
- package/output/redis-queue/test-helpers.test.d.ts.map +0 -1
- package/output/redis-queue/test-helpers.test.js +0 -572
- package/output/redis-queue/types.d.ts +0 -146
- package/output/redis-queue/types.d.ts.map +0 -1
- package/output/redis-queue/types.js +0 -1
- package/output/schema/index.d.ts +0 -177
- package/output/schema/index.d.ts.map +0 -1
- package/output/schema/index.js +0 -180
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import { RedisQueueCommon } from './redis-queue-common.js';
|
|
2
|
-
import type { RedisQueueRegistry, RedisQueueProviderConfig, Task, QueueStats } from './types.js';
|
|
3
|
-
/**
|
|
4
|
-
* Redis 队列生产者
|
|
5
|
-
*
|
|
6
|
-
* 只负责任务的入队和队列管理,不包含消费逻辑
|
|
7
|
-
*
|
|
8
|
-
* @template R Registry 类型,默认为 RedisQueueRegistry
|
|
9
|
-
* @template K 队列键类型
|
|
10
|
-
*
|
|
11
|
-
* @example
|
|
12
|
-
* ```ts
|
|
13
|
-
* // 方式 1: 使用默认 RedisQueueRegistry (需要提前声明模块扩展)
|
|
14
|
-
* const provider = new RedisQueueProvider('email-queue', {
|
|
15
|
-
* redisUrl: 'redis://localhost:6379'
|
|
16
|
-
* })
|
|
17
|
-
*
|
|
18
|
-
* await provider.enqueue({
|
|
19
|
-
* to: 'user@example.com', // 类型自动推断
|
|
20
|
-
* subject: 'Hello',
|
|
21
|
-
* body: 'World'
|
|
22
|
-
* })
|
|
23
|
-
*
|
|
24
|
-
* // 方式 2: 使用自定义 Registry
|
|
25
|
-
* interface MyRegistry {
|
|
26
|
-
* 'order-queue': { orderId: string; amount: number }
|
|
27
|
-
* }
|
|
28
|
-
*
|
|
29
|
-
* const provider = new RedisQueueProvider<MyRegistry, 'order-queue'>(
|
|
30
|
-
* 'order-queue',
|
|
31
|
-
* { redisUrl: 'redis://localhost:6379' }
|
|
32
|
-
* )
|
|
33
|
-
*
|
|
34
|
-
* await provider.enqueue({ orderId: '123', amount: 100 })
|
|
35
|
-
* ```
|
|
36
|
-
*/
|
|
37
|
-
export declare class RedisQueueProvider<R extends Record<string, any> = RedisQueueRegistry, K extends keyof R & string = keyof R & string> extends RedisQueueCommon {
|
|
38
|
-
private readonly processingDelay;
|
|
39
|
-
constructor(queueKey: K, config?: RedisQueueProviderConfig);
|
|
40
|
-
protected getLogPrefix(): string;
|
|
41
|
-
/**
|
|
42
|
-
* 将任务推入队列(支持单个或批量)
|
|
43
|
-
* @param data 任务数据(单个或数组)。可以在 data 中包含 `id` 字段来实现幂等性
|
|
44
|
-
*
|
|
45
|
-
* @example
|
|
46
|
-
* // 普通任务,自动生成 ID
|
|
47
|
-
* await provider.enqueue({ to: 'user@example.com' })
|
|
48
|
-
*
|
|
49
|
-
* // 幂等任务,手动指定 ID,重复提交会被忽略
|
|
50
|
-
* await provider.enqueue({ id: 'email-123', to: 'user@example.com' })
|
|
51
|
-
* await provider.enqueue({ id: 'email-123', to: 'user@example.com' }) // 会被跳过
|
|
52
|
-
*/
|
|
53
|
-
enqueue(data: R[K][]): Promise<string[]>;
|
|
54
|
-
enqueue(data: R[K]): Promise<string>;
|
|
55
|
-
/**
|
|
56
|
-
* 获取任务详情
|
|
57
|
-
*/
|
|
58
|
-
getTask(taskId: string): Promise<Task<R[K]> | null>;
|
|
59
|
-
/**
|
|
60
|
-
* 获取队列统计信息(O(1) 时间复杂度)
|
|
61
|
-
*/
|
|
62
|
-
statistics(): Promise<QueueStats>;
|
|
63
|
-
/**
|
|
64
|
-
* 清空所有队列和任务数据
|
|
65
|
-
* 使用 Lua 脚本确保原子性
|
|
66
|
-
*/
|
|
67
|
-
clear(): Promise<void>;
|
|
68
|
-
}
|
|
69
|
-
//# sourceMappingURL=redis-queue-provider.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"redis-queue-provider.d.ts","sourceRoot":"","sources":["../../source/redis-queue/redis-queue-provider.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAA;AAC1D,OAAO,KAAK,EAAE,kBAAkB,EAAE,wBAAwB,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAEhG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,qBAAa,kBAAkB,CAC7B,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,kBAAkB,EAClD,CAAC,SAAS,MAAM,CAAC,GAAG,MAAM,GAAG,MAAM,CAAC,GAAG,MAAM,CAC7C,SAAQ,gBAAgB;IACxB,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAQ;gBAE5B,QAAQ,EAAE,CAAC,EAAE,MAAM,GAAE,wBAA6B;IAU9D,SAAS,CAAC,YAAY,IAAI,MAAM;IAMhC;;;;;;;;;;;OAWG;IACG,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IACxC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC;IA0F1C;;OAEG;IACG,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;IAWzD;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,UAAU,CAAC;IAevC;;;OAGG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAmD7B"}
|
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { logger } from '@taicode/common-base';
|
|
3
|
-
import { RedisQueueCommon } from './redis-queue-common.js';
|
|
4
|
-
/**
|
|
5
|
-
* Redis 队列生产者
|
|
6
|
-
*
|
|
7
|
-
* 只负责任务的入队和队列管理,不包含消费逻辑
|
|
8
|
-
*
|
|
9
|
-
* @template R Registry 类型,默认为 RedisQueueRegistry
|
|
10
|
-
* @template K 队列键类型
|
|
11
|
-
*
|
|
12
|
-
* @example
|
|
13
|
-
* ```ts
|
|
14
|
-
* // 方式 1: 使用默认 RedisQueueRegistry (需要提前声明模块扩展)
|
|
15
|
-
* const provider = new RedisQueueProvider('email-queue', {
|
|
16
|
-
* redisUrl: 'redis://localhost:6379'
|
|
17
|
-
* })
|
|
18
|
-
*
|
|
19
|
-
* await provider.enqueue({
|
|
20
|
-
* to: 'user@example.com', // 类型自动推断
|
|
21
|
-
* subject: 'Hello',
|
|
22
|
-
* body: 'World'
|
|
23
|
-
* })
|
|
24
|
-
*
|
|
25
|
-
* // 方式 2: 使用自定义 Registry
|
|
26
|
-
* interface MyRegistry {
|
|
27
|
-
* 'order-queue': { orderId: string; amount: number }
|
|
28
|
-
* }
|
|
29
|
-
*
|
|
30
|
-
* const provider = new RedisQueueProvider<MyRegistry, 'order-queue'>(
|
|
31
|
-
* 'order-queue',
|
|
32
|
-
* { redisUrl: 'redis://localhost:6379' }
|
|
33
|
-
* )
|
|
34
|
-
*
|
|
35
|
-
* await provider.enqueue({ orderId: '123', amount: 100 })
|
|
36
|
-
* ```
|
|
37
|
-
*/
|
|
38
|
-
export class RedisQueueProvider extends RedisQueueCommon {
|
|
39
|
-
processingDelay;
|
|
40
|
-
constructor(queueKey, config = {}) {
|
|
41
|
-
super(queueKey, {
|
|
42
|
-
redisUrl: config.redisUrl,
|
|
43
|
-
redisClient: config.redisClient,
|
|
44
|
-
cleanupDelay: config.cleanupDelay,
|
|
45
|
-
});
|
|
46
|
-
this.processingDelay = config.processingDelay ?? 0; // 默认立即执行
|
|
47
|
-
}
|
|
48
|
-
getLogPrefix() {
|
|
49
|
-
return 'RedisQueueProvider';
|
|
50
|
-
}
|
|
51
|
-
async enqueue(data) {
|
|
52
|
-
if (!this.redis) {
|
|
53
|
-
logger.warn('[RedisQueueProvider] Redis not available, skipping task enqueue');
|
|
54
|
-
return Array.isArray(data) ? [] : '';
|
|
55
|
-
}
|
|
56
|
-
// 统一处理为数组
|
|
57
|
-
const dataList = Array.isArray(data) ? data : [data];
|
|
58
|
-
const taskIds = [];
|
|
59
|
-
const tasks = [];
|
|
60
|
-
// 准备任务数据
|
|
61
|
-
for (const item of dataList) {
|
|
62
|
-
const customId = item.id;
|
|
63
|
-
// 严格检查 id 类型,只接受 string
|
|
64
|
-
if (customId !== undefined && typeof customId !== 'string') {
|
|
65
|
-
throw new TypeError(`[RedisQueueProvider] Task id must be a string, got ${typeof customId}. ` +
|
|
66
|
-
`Please use string type for task id field.`);
|
|
67
|
-
}
|
|
68
|
-
const taskId = customId || randomUUID();
|
|
69
|
-
const task = {
|
|
70
|
-
id: taskId,
|
|
71
|
-
data: item,
|
|
72
|
-
retryCount: 0,
|
|
73
|
-
maxRetries: 3, // 默认值,实际由 Consumer 控制
|
|
74
|
-
createdTime: new Date().toISOString(),
|
|
75
|
-
status: 'pending',
|
|
76
|
-
...(this.processingDelay > 0 && { delayUntil: Date.now() + this.processingDelay }),
|
|
77
|
-
};
|
|
78
|
-
taskIds.push(taskId);
|
|
79
|
-
tasks.push(task);
|
|
80
|
-
}
|
|
81
|
-
// 使用 Lua 脚本批量原子化入队
|
|
82
|
-
const batchEnqueueScript = `
|
|
83
|
-
local pendingQueue = KEYS[1]
|
|
84
|
-
local ttl = tonumber(ARGV[1])
|
|
85
|
-
local queueKeyPrefix = ARGV[2]
|
|
86
|
-
local successCount = 0
|
|
87
|
-
|
|
88
|
-
-- ARGV[3], ARGV[5], ARGV[7]... 是 taskId
|
|
89
|
-
-- ARGV[4], ARGV[6], ARGV[8]... 是 taskData
|
|
90
|
-
for i = 3, #ARGV, 2 do
|
|
91
|
-
local taskId = ARGV[i]
|
|
92
|
-
local taskData = ARGV[i + 1]
|
|
93
|
-
local taskKey = queueKeyPrefix .. ':task:' .. taskId
|
|
94
|
-
|
|
95
|
-
-- 检查任务是否已存在
|
|
96
|
-
local exists = redis.call('EXISTS', taskKey)
|
|
97
|
-
if exists == 0 then
|
|
98
|
-
-- 原子化操作: 创建任务 + 推入队列
|
|
99
|
-
redis.call('SETEX', taskKey, ttl, taskData)
|
|
100
|
-
redis.call('RPUSH', pendingQueue, taskId)
|
|
101
|
-
successCount = successCount + 1
|
|
102
|
-
end
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
return successCount
|
|
106
|
-
`;
|
|
107
|
-
// 构建参数数组 - Redis 要求所有参数都是字符串
|
|
108
|
-
const args = [this.cleanupDelay.toString(), this.queueKey];
|
|
109
|
-
for (let i = 0; i < tasks.length; i++) {
|
|
110
|
-
args.push(taskIds[i]);
|
|
111
|
-
args.push(JSON.stringify(tasks[i]));
|
|
112
|
-
}
|
|
113
|
-
const successCount = await this.redis.eval(batchEnqueueScript, {
|
|
114
|
-
keys: [this.pendingQueue],
|
|
115
|
-
arguments: args,
|
|
116
|
-
});
|
|
117
|
-
if (successCount < tasks.length) {
|
|
118
|
-
logger.info(`[RedisQueueProvider] ${tasks.length - successCount} tasks already exist, skipped`);
|
|
119
|
-
}
|
|
120
|
-
if (successCount > 0) {
|
|
121
|
-
logger.info(`[RedisQueueProvider] Enqueued ${successCount} tasks`);
|
|
122
|
-
}
|
|
123
|
-
// 返回单个 ID 或 ID 数组
|
|
124
|
-
return Array.isArray(data) ? taskIds : taskIds[0];
|
|
125
|
-
}
|
|
126
|
-
/**
|
|
127
|
-
* 获取任务详情
|
|
128
|
-
*/
|
|
129
|
-
async getTask(taskId) {
|
|
130
|
-
if (!this.redis)
|
|
131
|
-
return null;
|
|
132
|
-
const taskKey = `${this.queueKey}:task:${taskId}`;
|
|
133
|
-
const taskData = await this.redis.get(taskKey);
|
|
134
|
-
if (!taskData)
|
|
135
|
-
return null;
|
|
136
|
-
return JSON.parse(taskData);
|
|
137
|
-
}
|
|
138
|
-
/**
|
|
139
|
-
* 获取队列统计信息(O(1) 时间复杂度)
|
|
140
|
-
*/
|
|
141
|
-
async statistics() {
|
|
142
|
-
if (!this.redis) {
|
|
143
|
-
return { pending: 0, processing: 0, completed: 0, failed: 0 };
|
|
144
|
-
}
|
|
145
|
-
const [pending, processing, completed, failed] = await Promise.all([
|
|
146
|
-
this.redis.lLen(this.pendingQueue),
|
|
147
|
-
this.redis.lLen(this.processingQueue),
|
|
148
|
-
this.redis.lLen(this.completedQueue),
|
|
149
|
-
this.redis.lLen(this.failedQueue),
|
|
150
|
-
]);
|
|
151
|
-
return { pending, processing, completed, failed };
|
|
152
|
-
}
|
|
153
|
-
/**
|
|
154
|
-
* 清空所有队列和任务数据
|
|
155
|
-
* 使用 Lua 脚本确保原子性
|
|
156
|
-
*/
|
|
157
|
-
async clear() {
|
|
158
|
-
if (!this.redis)
|
|
159
|
-
return;
|
|
160
|
-
// 使用 Lua 脚本原子化清空队列和任务
|
|
161
|
-
const clearScript = `
|
|
162
|
-
local queueKeyPrefix = ARGV[1]
|
|
163
|
-
local pendingQueue = KEYS[1]
|
|
164
|
-
local processingQueue = KEYS[2]
|
|
165
|
-
local completedQueue = KEYS[3]
|
|
166
|
-
local failedQueue = KEYS[4]
|
|
167
|
-
|
|
168
|
-
-- 获取所有队列中的任务 ID
|
|
169
|
-
local allTaskIds = {}
|
|
170
|
-
local pendingIds = redis.call('LRANGE', pendingQueue, 0, -1)
|
|
171
|
-
local processingIds = redis.call('LRANGE', processingQueue, 0, -1)
|
|
172
|
-
local completedIds = redis.call('LRANGE', completedQueue, 0, -1)
|
|
173
|
-
local failedIds = redis.call('LRANGE', failedQueue, 0, -1)
|
|
174
|
-
|
|
175
|
-
-- 合并所有任务 ID
|
|
176
|
-
for _, id in ipairs(pendingIds) do table.insert(allTaskIds, id) end
|
|
177
|
-
for _, id in ipairs(processingIds) do table.insert(allTaskIds, id) end
|
|
178
|
-
for _, id in ipairs(completedIds) do table.insert(allTaskIds, id) end
|
|
179
|
-
for _, id in ipairs(failedIds) do table.insert(allTaskIds, id) end
|
|
180
|
-
|
|
181
|
-
-- 删除所有任务数据
|
|
182
|
-
for _, taskId in ipairs(allTaskIds) do
|
|
183
|
-
local taskKey = queueKeyPrefix .. ':task:' .. taskId
|
|
184
|
-
redis.call('DEL', taskKey)
|
|
185
|
-
end
|
|
186
|
-
|
|
187
|
-
-- 删除所有队列
|
|
188
|
-
redis.call('DEL', pendingQueue, processingQueue, completedQueue, failedQueue)
|
|
189
|
-
|
|
190
|
-
return #allTaskIds
|
|
191
|
-
`;
|
|
192
|
-
const deletedCount = await this.redis.eval(clearScript, {
|
|
193
|
-
keys: [
|
|
194
|
-
this.pendingQueue,
|
|
195
|
-
this.processingQueue,
|
|
196
|
-
this.completedQueue,
|
|
197
|
-
this.failedQueue,
|
|
198
|
-
],
|
|
199
|
-
arguments: [this.queueKey],
|
|
200
|
-
});
|
|
201
|
-
if (deletedCount > 0) {
|
|
202
|
-
logger.info(`[RedisQueueProvider] Cleared ${deletedCount} tasks`);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
type TestTaskData = {
|
|
2
|
-
id?: string;
|
|
3
|
-
value?: number | string;
|
|
4
|
-
test?: boolean | string;
|
|
5
|
-
index?: number;
|
|
6
|
-
payload?: string;
|
|
7
|
-
order?: number;
|
|
8
|
-
taskId?: number;
|
|
9
|
-
message?: string;
|
|
10
|
-
name?: string;
|
|
11
|
-
shouldFail?: boolean;
|
|
12
|
-
step?: number;
|
|
13
|
-
real?: boolean;
|
|
14
|
-
nested?: {
|
|
15
|
-
value: number;
|
|
16
|
-
};
|
|
17
|
-
example?: string;
|
|
18
|
-
foo?: string;
|
|
19
|
-
number?: number;
|
|
20
|
-
data?: any;
|
|
21
|
-
};
|
|
22
|
-
declare module "./types.js" {
|
|
23
|
-
interface RedisQueueRegistry {
|
|
24
|
-
[key: `test:provider:${string}`]: TestTaskData;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
export {};
|
|
28
|
-
//# sourceMappingURL=redis-queue-provider.test.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"redis-queue-provider.test.d.ts","sourceRoot":"","sources":["../../source/redis-queue/redis-queue-provider.test.ts"],"names":[],"mappings":"AAMA,KAAK,YAAY,GAAG;IAClB,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IACvB,IAAI,CAAC,EAAE,OAAO,GAAG,MAAM,CAAA;IACvB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,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,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,GAAG,CAAA;CACX,CAAA;AAGD,OAAO,QAAQ,YAAY,CAAC;IAC1B,UAAU,kBAAkB;QAC1B,CAAC,GAAG,EAAE,iBAAiB,MAAM,EAAE,GAAG,YAAY,CAAA;KAC/C;CACF"}
|
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, afterEach } from "vitest";
|
|
2
|
-
import { RedisQueueProvider } from "./redis-queue-provider.js";
|
|
3
|
-
import { clearQueue, getQueueTasks } from "./test-helpers.js";
|
|
4
|
-
const REDIS_URL = process.env.REDIS_URL || "redis://localhost:6379";
|
|
5
|
-
describe("RedisQueueProvider", () => {
|
|
6
|
-
const providers = [];
|
|
7
|
-
afterEach(async () => {
|
|
8
|
-
for (const provider of providers) {
|
|
9
|
-
await clearQueue(provider);
|
|
10
|
-
provider.disconnect();
|
|
11
|
-
}
|
|
12
|
-
providers.length = 0;
|
|
13
|
-
});
|
|
14
|
-
const createProvider = (options) => {
|
|
15
|
-
const uniqueKey = `test:provider:${Date.now()}:${Math.random()}`;
|
|
16
|
-
const provider = new RedisQueueProvider(uniqueKey, {
|
|
17
|
-
redisUrl: REDIS_URL,
|
|
18
|
-
...options,
|
|
19
|
-
});
|
|
20
|
-
providers.push(provider);
|
|
21
|
-
return provider;
|
|
22
|
-
};
|
|
23
|
-
describe("连接管理", () => {
|
|
24
|
-
it("应该成功连接到 Redis", async () => {
|
|
25
|
-
const provider = createProvider();
|
|
26
|
-
await provider.connect();
|
|
27
|
-
const health = await provider.health();
|
|
28
|
-
expect(health).toBe(true);
|
|
29
|
-
});
|
|
30
|
-
});
|
|
31
|
-
describe("任务入队", () => {
|
|
32
|
-
it("应该成功将任务推入队列", async () => {
|
|
33
|
-
const provider = createProvider();
|
|
34
|
-
await provider.connect();
|
|
35
|
-
const taskId = await provider.enqueue({ foo: "bar" });
|
|
36
|
-
expect(taskId).toBeTruthy();
|
|
37
|
-
const stats = await provider.statistics();
|
|
38
|
-
expect(stats.pending).toBe(1);
|
|
39
|
-
});
|
|
40
|
-
it("应该能够批量入队任务", async () => {
|
|
41
|
-
const provider = createProvider();
|
|
42
|
-
await provider.connect();
|
|
43
|
-
const taskIds = await provider.enqueue([
|
|
44
|
-
{ value: 1 },
|
|
45
|
-
{ value: 2 },
|
|
46
|
-
{ value: 3 },
|
|
47
|
-
]);
|
|
48
|
-
expect(taskIds).toHaveLength(3);
|
|
49
|
-
const stats = await provider.statistics();
|
|
50
|
-
expect(stats.pending).toBe(3);
|
|
51
|
-
});
|
|
52
|
-
it("入队的任务应该包含正确的元数据", async () => {
|
|
53
|
-
const provider = createProvider();
|
|
54
|
-
await provider.connect();
|
|
55
|
-
const taskId = await provider.enqueue({ test: "data", number: 42 });
|
|
56
|
-
const task = await provider.getTask(taskId);
|
|
57
|
-
expect(task).toBeTruthy();
|
|
58
|
-
expect(task?.id).toBe(taskId);
|
|
59
|
-
expect(task?.data).toEqual({ test: "data", number: 42 });
|
|
60
|
-
expect(task?.status).toBe("pending");
|
|
61
|
-
expect(task?.retryCount).toBe(0);
|
|
62
|
-
});
|
|
63
|
-
});
|
|
64
|
-
describe("队列操作", () => {
|
|
65
|
-
it("应该正确返回队列统计信息", async () => {
|
|
66
|
-
const provider = createProvider();
|
|
67
|
-
await provider.connect();
|
|
68
|
-
await provider.enqueue([{ test: "1" }, { test: "2" }, { test: "3" }]);
|
|
69
|
-
const stats = await provider.statistics();
|
|
70
|
-
expect(stats.pending).toBe(3);
|
|
71
|
-
expect(stats.processing).toBe(0);
|
|
72
|
-
expect(stats.completed).toBe(0);
|
|
73
|
-
expect(stats.failed).toBe(0);
|
|
74
|
-
});
|
|
75
|
-
it("应该能够清空队列", async () => {
|
|
76
|
-
const provider = createProvider();
|
|
77
|
-
await provider.connect();
|
|
78
|
-
await provider.enqueue([{ data: 1 }, { data: 2 }]);
|
|
79
|
-
await provider.clear();
|
|
80
|
-
const stats = await provider.statistics();
|
|
81
|
-
expect(stats.pending).toBe(0);
|
|
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
|
-
});
|
|
100
|
-
});
|
|
101
|
-
describe("幂等性", () => {
|
|
102
|
-
it("应该支持在 data 中指定 id 来实现幂等性", async () => {
|
|
103
|
-
const provider = createProvider();
|
|
104
|
-
await provider.connect();
|
|
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 });
|
|
108
|
-
const stats = await provider.statistics();
|
|
109
|
-
expect(stats.pending).toBe(2); // 只有 unique-1 和 unique-2
|
|
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
|
-
});
|
|
138
|
-
});
|
|
139
|
-
describe("延迟处理", () => {
|
|
140
|
-
it("应该支持延迟入队任务", async () => {
|
|
141
|
-
const provider = createProvider({ processingDelay: 2000 });
|
|
142
|
-
await provider.connect();
|
|
143
|
-
const taskId = (await provider.enqueue({ test: true }));
|
|
144
|
-
const task = await provider.getTask(taskId);
|
|
145
|
-
expect(task?.delayUntil).toBeDefined();
|
|
146
|
-
expect(task?.delayUntil).toBeGreaterThan(Date.now());
|
|
147
|
-
});
|
|
148
|
-
});
|
|
149
|
-
});
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import type { RedisQueueBatchConsumer } from './redis-queue-batch-consumer.js';
|
|
2
|
-
import { RedisQueueConsumer } from './redis-queue-consumer.js';
|
|
3
|
-
import type { RedisQueueProvider } from './redis-queue-provider.js';
|
|
4
|
-
import type { RedisQueueRegistry, Task } from './types.js';
|
|
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<R extends Record<string, any> = RedisQueueRegistry, K extends keyof R & string = keyof R & string>(consumer: RedisQueueConsumer<R, K> | RedisQueueBatchConsumer<R, 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<R extends Record<string, any> = RedisQueueRegistry, K extends keyof R & string = keyof R & string>(instance: RedisQueueProvider<R, K> | RedisQueueConsumer<R, K> | RedisQueueBatchConsumer<R, 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<R extends Record<string, any> = RedisQueueRegistry, K extends keyof R & string = keyof R & string>(instance: RedisQueueProvider<R, K> | RedisQueueConsumer<R, K> | RedisQueueBatchConsumer<R, 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<R extends Record<string, any> = RedisQueueRegistry, K extends keyof R & string = keyof R & string>(provider: RedisQueueProvider<R, K>, status?: 'pending' | 'processing' | 'completed' | 'failed'): Promise<Task<R[K]>[]>;
|
|
112
|
-
//# sourceMappingURL=test-helpers.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
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,iCAAiC,CAAA;AAC9E,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAA;AAC9D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAA;AACnE,OAAO,KAAK,EAAE,kBAAkB,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA;AAE1D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,wBAAsB,iBAAiB,CACrC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,kBAAkB,EAClD,CAAC,SAAS,MAAM,CAAC,GAAG,MAAM,GAAG,MAAM,CAAC,GAAG,MAAM,EAE7C,QAAQ,EAAE,kBAAkB,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,uBAAuB,CAAC,CAAC,EAAE,CAAC,CAAC,EAClE,OAAO,EAAE,MAAM,EAAE,GAChB,OAAO,CAAC,IAAI,CAAC,CA+Ff;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,mBAAmB,CACvC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,kBAAkB,EAClD,CAAC,SAAS,MAAM,CAAC,GAAG,MAAM,GAAG,MAAM,CAAC,GAAG,MAAM,EAE7C,QAAQ,EAAE,kBAAkB,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,kBAAkB,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,uBAAuB,CAAC,CAAC,EAAE,CAAC,CAAC,EAC7F,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,CAC9B,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,kBAAkB,EAClD,CAAC,SAAS,MAAM,CAAC,GAAG,MAAM,GAAG,MAAM,CAAC,GAAG,MAAM,EAE7C,QAAQ,EAAE,kBAAkB,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,kBAAkB,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,uBAAuB,CAAC,CAAC,EAAE,CAAC,CAAC,GAC5F,OAAO,CAAC,IAAI,CAAC,CAcf;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,aAAa,CACjC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,kBAAkB,EAClD,CAAC,SAAS,MAAM,CAAC,GAAG,MAAM,GAAG,MAAM,CAAC,GAAG,MAAM,EAE7C,QAAQ,EAAE,kBAAkB,CAAC,CAAC,EAAE,CAAC,CAAC,EAClC,MAAM,CAAC,EAAE,SAAS,GAAG,YAAY,GAAG,WAAW,GAAG,QAAQ,GACzD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAmCvB"}
|