mm_redis 2.1.7 → 2.3.0

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.
@@ -0,0 +1,16 @@
1
+ 'use strict';
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ /** 模块能力层:懒加载工具实现,避免模块入口被 require 时加载全部实现 */
5
+ module.exports = new Proxy({}, {
6
+ get(_t, key) {
7
+ if (typeof key !== 'string') return undefined;
8
+ if (!fs.existsSync(path.join(__dirname, key, 'index.js'))) return undefined;
9
+ return require('./' + key);
10
+ },
11
+ has(_t, key) {
12
+ return typeof key === 'string' && fs.existsSync(path.join(__dirname, key, 'index.js'));
13
+ },
14
+ ownKeys() { return fs.readdirSync(__dirname, { withFileTypes: true }).filter(e => e.isDirectory()).map(e => e.name); },
15
+ getOwnPropertyDescriptor(_t, key) { return this.has(_t, key) ? { enumerable: true, configurable: true } : undefined; }
16
+ });
@@ -0,0 +1,115 @@
1
+ /**
2
+ * 队列发信器
3
+ * 发送消息到消息队列(redis v4 官方包,与 mm_redis 共用底层库)
4
+ */
5
+
6
+ /**
7
+ * 获取 Redis 客户端(复用 mm_redis 连接池,不可用时自建独立连接)
8
+ * @param {object} opts 配置选项
9
+ * @returns {object} { client, release } 或 null
10
+ */
11
+ async function _getClient(opts) {
12
+ var opts2 = { ...opts };
13
+ // 统一 db/database 字段,mm_redis 读取的是 database 字段
14
+ if (opts2.db !== undefined && opts2.database === undefined) {
15
+ opts2.database = opts2.db;
16
+ }
17
+
18
+ var redis_admin = null;
19
+ try {
20
+ var { redisAdmin } = require('mm_redis');
21
+ if (redisAdmin) {
22
+ // 与 queue 管理器共用 queue 作用域,保证发布/消费配置一致
23
+ redis_admin = redisAdmin('queue', opts2);
24
+ }
25
+ } catch (e) {
26
+ // mm_redis 不可用时使用传入的 opts
27
+ }
28
+
29
+ // 优先复用 mm_redis 连接池,保证配置一致且不新建独立连接
30
+ if (redis_admin && typeof redis_admin._getConnection === 'function') {
31
+ var conn = await redis_admin._getConnection();
32
+ return {
33
+ client: conn,
34
+ release: function () {
35
+ try {
36
+ redis_admin._releaseConnection(conn);
37
+ } catch (e) {
38
+ void e;
39
+ }
40
+ }
41
+ };
42
+ }
43
+
44
+ // 回退: mm_redis 不可用,自建独立连接
45
+ var redis_mod = require('redis');
46
+ if (!redis_mod || !redis_mod.createClient) {
47
+ return null;
48
+ }
49
+ var createClient = redis_mod.createClient;
50
+ var client = createClient({
51
+ socket: {
52
+ host: opts2.host || 'localhost',
53
+ port: opts2.port || 6379,
54
+ connectTimeout: opts2.connect_timeout || 5000,
55
+ reconnectStrategy: false
56
+ },
57
+ password: opts2.password || '',
58
+ database: opts2.database || 0
59
+ });
60
+ await client.connect();
61
+ return {
62
+ client: client,
63
+ release: function () {
64
+ try {
65
+ client.quit();
66
+ } catch (e) {
67
+ void e;
68
+ }
69
+ }
70
+ };
71
+ }
72
+
73
+ /**
74
+ * 主方法
75
+ * @param {object} ctx 上下文对象
76
+ * @param {object} params 步骤参数
77
+ * @returns {object} 发送结果
78
+ */
79
+ async function main(params, ctx, options) {
80
+ var opts = (this && typeof this.getOptions === 'function') ? this.getOptions() : {};
81
+ var driver = opts.driver || 'redis';
82
+
83
+ if (driver !== 'redis') {
84
+ throw new Error('不支持的队列驱动: ' + driver);
85
+ }
86
+
87
+ var queue_name = params.queue_name || '';
88
+ var data = params.data;
89
+
90
+ if (!queue_name) {
91
+ ctx.output = { error: { code: 'QUEUE_NO_NAME', message: '队列名称不能为空' } };
92
+ return ctx.output;
93
+ }
94
+
95
+ var holder = await _getClient(opts);
96
+ if (!holder || !holder.client) {
97
+ throw new Error('队列发信需要 redis 库,请手动执行: npm install redis');
98
+ }
99
+ var client = holder.client;
100
+ try {
101
+ await client.lPush(queue_name, JSON.stringify(data));
102
+ } finally {
103
+ await holder.release();
104
+ }
105
+
106
+ ctx.output = {
107
+ sent: true,
108
+ tip: '消息已入队',
109
+ queue_name: queue_name,
110
+ message: data
111
+ };
112
+ return ctx.output;
113
+ }
114
+
115
+ module.exports = { main };
@@ -0,0 +1,395 @@
1
+ /**
2
+ * 消息队列管理器
3
+ * 管理消息队列的通用操作,支持发布、消费、确认、重试、死信、延迟、批量消费
4
+ * 驱动: Redis (redis v4 官方包,与 mm_redis 共用底层库)
5
+ */
6
+
7
+ /**
8
+ * 带超时的 Promise 竞争,超时后抛出错误
9
+ * @param {Promise} promise 原始 Promise
10
+ * @param {number} timeout_ms 超时毫秒数
11
+ * @returns {Promise} 竞争结果
12
+ * @throws {Error} 超时时抛出
13
+ */
14
+ function _withTimeout(promise, timeout_ms) {
15
+ var timer = null;
16
+ var timeout_promise = new Promise(function (resolve, reject) {
17
+ timer = setTimeout(function () {
18
+ reject(new Error('Redis 连接超时'));
19
+ }, timeout_ms);
20
+ });
21
+ return Promise.race([promise, timeout_promise]).finally(function () {
22
+ clearTimeout(timer);
23
+ });
24
+ }
25
+
26
+ /**
27
+ * 获取 Redis 客户端(通过 mm_redis 的 redisAdmin 获取配置,创建独立连接)
28
+ * @param {object} opts 配置选项
29
+ * @returns {object} Redis 客户端
30
+ * @throws {Error} 连接失败或超时时抛出
31
+ */
32
+ async function _getClient(opts) {
33
+ var opts2 = { ...opts };
34
+ // 统一 db/database 字段,mm_redis 读取的是 database 字段
35
+ if (opts2.db !== undefined && opts2.database === undefined) {
36
+ opts2.database = opts2.db;
37
+ }
38
+
39
+ var redis_admin = null;
40
+ try {
41
+ var { redisAdmin } = require('mm_redis');
42
+ if (redisAdmin) {
43
+ // 使用独立作用域,避免与 redis 管理器共用 sys 池导致本模块配置被覆盖
44
+ redis_admin = redisAdmin('queue', opts2);
45
+ }
46
+ } catch (e) {
47
+ // mm_redis 不可用时使用传入的 opts
48
+ }
49
+
50
+ // 优先复用 mm_redis 连接池,保证配置一致且不新建独立连接
51
+ // 使用超时限制,避免 Redis 不可用时连接无限挂起
52
+ if (redis_admin && typeof redis_admin._getConnection === 'function') {
53
+ var conn_promise = redis_admin._getConnection();
54
+ var conn;
55
+ try {
56
+ conn = await _withTimeout(conn_promise, opts2.connect_timeout || 3000);
57
+ } catch (e) {
58
+ // 超时后底层连接可能仍在创建,创建完成后强制回收,避免连接泄漏
59
+ conn_promise.then(function (c) {
60
+ try {
61
+ redis_admin._releaseConnection(c);
62
+ } catch (e2) {
63
+ void e2;
64
+ }
65
+ }).catch(function (e2) {
66
+ void e2;
67
+ });
68
+ throw e;
69
+ }
70
+ return {
71
+ client: conn,
72
+ release: function () {
73
+ try {
74
+ redis_admin._releaseConnection(conn);
75
+ } catch (e2) {
76
+ void e2;
77
+ }
78
+ }
79
+ };
80
+ }
81
+
82
+ // 回退: mm_redis 不可用,自建独立连接
83
+ // 禁用自动重连,连接失败时立即抛出,避免无限重连挂起
84
+ var redis_mod = require('redis');
85
+ if (!redis_mod || !redis_mod.createClient) {
86
+ return null;
87
+ }
88
+ var createClient = redis_mod.createClient;
89
+ var client = createClient({
90
+ socket: {
91
+ host: opts2.host || 'localhost',
92
+ port: opts2.port || 6379,
93
+ connectTimeout: opts2.connect_timeout || 5000,
94
+ reconnectStrategy: false
95
+ },
96
+ password: opts2.password || '',
97
+ database: opts2.database || 0
98
+ });
99
+ try {
100
+ await client.connect();
101
+ } catch (e) {
102
+ try {
103
+ await client.quit();
104
+ } catch (e2) {
105
+ void e2;
106
+ }
107
+ throw new Error('Redis 连接失败: ' + (e && e.message ? e.message : String(e)));
108
+ }
109
+ return {
110
+ client: client,
111
+ release: function () {
112
+ try {
113
+ client.quit();
114
+ } catch (e2) {
115
+ void e2;
116
+ }
117
+ }
118
+ };
119
+ }
120
+
121
+ /**
122
+ * 发布消息到队列
123
+ * @param {object} client Redis 客户端
124
+ * @param {string} queue_name 队列名称
125
+ * @param {*} message 消息内容
126
+ * @returns {object} 发布结果
127
+ */
128
+ async function _publish(client, queue_name, message) {
129
+ // lPush 返回的是列表长度而非消息ID,这里生成唯一消息ID
130
+ var message_id = Date.now().toString(36) + '-' + Math.random().toString(36).substr(2, 9);
131
+ await client.lPush(queue_name, JSON.stringify(message));
132
+ return { msg: '消息发布成功', message_id: message_id, queue: queue_name };
133
+ }
134
+
135
+ /**
136
+ * 消费队列消息
137
+ * @param {object} client Redis 客户端
138
+ * @param {string} queue_name 队列名称
139
+ * @returns {object} 消费结果
140
+ */
141
+ async function _consume(client, queue_name) {
142
+ var raw = await client.rPopLPush(queue_name, queue_name + ':processing');
143
+ if (!raw) {
144
+ return { obj: null, queue: queue_name };
145
+ }
146
+ var parsed = JSON.parse(raw);
147
+ return { obj: parsed, queue: queue_name, _raw: raw };
148
+ }
149
+
150
+ /**
151
+ * 批量消费队列消息
152
+ * @param {object} client Redis 客户端
153
+ * @param {string} queue_name 队列名称
154
+ * @param {number} count 消费数量
155
+ * @returns {object} 批量消费结果
156
+ */
157
+ async function _batchConsume(client, queue_name, count) {
158
+ var list = [];
159
+ for (var i = 0; i < count; i++) {
160
+ var raw = await client.rPopLPush(queue_name, queue_name + ':processing');
161
+ if (!raw) break;
162
+ list.push(JSON.parse(raw));
163
+ }
164
+ return { list: list, count: list.length, queue: queue_name };
165
+ }
166
+
167
+ /**
168
+ * 确认消费(删除处理中消息)
169
+ * @param {object} client Redis 客户端
170
+ * @param {string} queue_name 队列名称
171
+ * @param {string} message_raw 原始消息字符串
172
+ * @returns {object} 确认结果
173
+ */
174
+ async function _ack(client, queue_name, message_raw) {
175
+ if (!message_raw) {
176
+ return { error: { code: 'QUEUE_ACK_NO_MSG', message: '确认消费需要传入 _raw 消息原文' } };
177
+ }
178
+ var removed = await client.lRem(queue_name + ':processing', 1, message_raw);
179
+ return { msg: removed > 0 ? '消息已确认' : '消息未找到', queue: queue_name };
180
+ }
181
+
182
+ /**
183
+ * 消费失败重试(移回主队列)
184
+ * @param {object} client Redis 客户端
185
+ * @param {string} queue_name 队列名称
186
+ * @param {string} message_raw 原始消息字符串
187
+ * @param {number} max_retries 最大重试次数
188
+ * @returns {object} 重试结果
189
+ */
190
+ async function _retry(client, queue_name, message_raw, max_retries) {
191
+ if (!message_raw) {
192
+ return { error: { code: 'QUEUE_RETRY_NO_MSG', message: '重试需要传入 _raw 消息原文' } };
193
+ }
194
+ var max_retry = max_retries || 3;
195
+ var parsed = JSON.parse(message_raw);
196
+ var retry_count = (parsed._retry_count || 0) + 1;
197
+ if (retry_count > max_retry) {
198
+ await client.lRem(queue_name + ':processing', 1, message_raw);
199
+ parsed._retry_count = retry_count;
200
+ parsed._dlq_at = new Date().toISOString();
201
+ await client.lPush(queue_name + ':dlq', JSON.stringify(parsed));
202
+ return { msg: '已达最大重试次数,移入死信队列', retry_count: retry_count, queue: queue_name };
203
+ }
204
+ parsed._retry_count = retry_count;
205
+ await client.lRem(queue_name + ':processing', 1, message_raw);
206
+ await client.lPush(queue_name, JSON.stringify(parsed));
207
+ return { msg: '消息已重新入队', retry_count: retry_count, queue: queue_name };
208
+ }
209
+
210
+ /**
211
+ * 死信队列操作
212
+ * @param {object} client Redis 客户端
213
+ * @param {string} queue_name 队列名称
214
+ * @param {string} dlq_action 死信操作: view/replay/clear
215
+ * @returns {object} 死信队列结果
216
+ */
217
+ async function _dlq(client, queue_name, dlq_action) {
218
+ var dlq_key = queue_name + ':dlq';
219
+ switch (dlq_action) {
220
+ case 'view': {
221
+ var all = await client.lRange(dlq_key, 0, -1);
222
+ var list = (all || []).map(function (r) { return JSON.parse(r); });
223
+ return { list: list, count: list.length, queue: queue_name };
224
+ }
225
+ case 'replay': {
226
+ var moved = 0;
227
+ var msg;
228
+ while ((msg = await client.rPopLPush(dlq_key, queue_name)) !== null) {
229
+ moved++;
230
+ }
231
+ return { msg: '已重放 ' + moved + ' 条死信消息到主队列', count: moved, queue: queue_name };
232
+ }
233
+ case 'clear': {
234
+ var deleted = await client.del(dlq_key);
235
+ return { msg: '已清空死信队列', queue: queue_name };
236
+ }
237
+ default:
238
+ return { error: { code: 'QUEUE_DLQ_UNKNOWN', message: '不支持的死信操作: ' + dlq_action } };
239
+ }
240
+ }
241
+
242
+ /**
243
+ * 延迟消息
244
+ * @param {object} client Redis 客户端
245
+ * @param {string} queue_name 队列名称
246
+ * @param {*} message 消息内容
247
+ * @param {number} delay_seconds 延迟秒数
248
+ * @returns {object} 延迟消息结果
249
+ */
250
+ async function _delay(client, queue_name, message, delay_seconds) {
251
+ var delay_sec = delay_seconds || 0;
252
+ if (delay_sec <= 0) {
253
+ return { error: { code: 'QUEUE_DELAY_INVALID', message: '延迟时间必须大于0秒' } };
254
+ }
255
+ var exec_at = Date.now() + delay_sec * 1000;
256
+ await client.zAdd(queue_name + ':delay', { score: exec_at, value: JSON.stringify(message) });
257
+ return { msg: '延迟消息已添加', exec_at: new Date(exec_at).toISOString(), delay_seconds: delay_sec, queue: queue_name };
258
+ }
259
+
260
+ /**
261
+ * 将到期延迟消息移入主队列
262
+ * @param {object} client Redis 客户端
263
+ * @param {string} queue_name 队列名称
264
+ * @returns {object} 迁移结果
265
+ */
266
+ async function _delayPoll(client, queue_name) {
267
+ var now = Date.now();
268
+ var due_msgs = await client.zRangeByScore(queue_name + ':delay', 0, now);
269
+ if (!due_msgs || due_msgs.length === 0) {
270
+ return { msg: '无到期延迟消息', count: 0, queue: queue_name };
271
+ }
272
+ for (var i = 0; i < due_msgs.length; i++) {
273
+ await client.lPush(queue_name, due_msgs[i]);
274
+ }
275
+ await client.zRemRangeByScore(queue_name + ':delay', 0, now);
276
+ return { msg: '已迁移到期消息', count: due_msgs.length, queue: queue_name };
277
+ }
278
+
279
+ /**
280
+ * 查看队列状态
281
+ * @param {object} client Redis 客户端
282
+ * @param {string} queue_name 队列名称
283
+ * @returns {object} 队列状态
284
+ */
285
+ async function _status(client, queue_name) {
286
+ var main_len = await client.lLen(queue_name);
287
+ var proc_len = await client.lLen(queue_name + ':processing');
288
+ var dlq_len = await client.lLen(queue_name + ':dlq');
289
+ var delay_len = await client.zCard(queue_name + ':delay');
290
+ return {
291
+ obj: {
292
+ queue: queue_name,
293
+ pending: main_len,
294
+ processing: proc_len,
295
+ dead_letter: dlq_len,
296
+ delayed: delay_len
297
+ }
298
+ };
299
+ }
300
+
301
+ /**
302
+ * 主函数
303
+ * @param {object} ctx 上下文
304
+ * @param {object} params 参数
305
+ * @returns {object} 执行结果
306
+ */
307
+ async function main(params, ctx, options) {
308
+ var opts = (this && typeof this.getOptions === 'function') ? this.getOptions() : {};
309
+ var driver = opts.driver || 'redis';
310
+
311
+ if (driver !== 'redis') {
312
+ ctx.output = {
313
+ error: {
314
+ code: 'MGR_UNSUPPORTED_DRIVER',
315
+ message: '不支持的队列驱动: ' + driver + ',当前支持: redis'
316
+ }
317
+ };
318
+ return ctx.output;
319
+ }
320
+
321
+ var holder;
322
+ try {
323
+ holder = await _getClient(opts);
324
+ } catch (e) {
325
+ ctx.output = {
326
+ error: {
327
+ code: 'MGR_CONNECT_FAIL',
328
+ message: 'Redis 连接失败: ' + e.message
329
+ }
330
+ };
331
+ return ctx.output;
332
+ }
333
+
334
+ if (!holder || !holder.client) {
335
+ ctx.output = {
336
+ error: {
337
+ code: 'MGR_MISSING_DEP',
338
+ message: '消息队列需要 redis 库,请手动执行: npm install redis'
339
+ }
340
+ };
341
+ return ctx.output;
342
+ }
343
+ var client = holder.client;
344
+
345
+ var action = params.action || '';
346
+ var queue_name = params.queue_name || '';
347
+
348
+ if (!queue_name) {
349
+ ctx.output = { error: { code: 'QUEUE_NO_NAME', message: '队列名称不能为空' } };
350
+ await holder.release();
351
+ return ctx.output;
352
+ }
353
+
354
+ try {
355
+ switch (action) {
356
+ case 'publish':
357
+ ctx.output = await _publish(client, queue_name, params.message);
358
+ break;
359
+ case 'consume':
360
+ ctx.output = await _consume(client, queue_name);
361
+ break;
362
+ case 'batch_consume':
363
+ ctx.output = await _batchConsume(client, queue_name, params.count || 10);
364
+ break;
365
+ case 'ack':
366
+ ctx.output = await _ack(client, queue_name, params._raw);
367
+ break;
368
+ case 'retry':
369
+ ctx.output = await _retry(client, queue_name, params._raw, params.max_retries);
370
+ break;
371
+ case 'dlq':
372
+ ctx.output = await _dlq(client, queue_name, params.dlq_action);
373
+ break;
374
+ case 'delay':
375
+ ctx.output = await _delay(client, queue_name, params.message, params.delay_seconds);
376
+ break;
377
+ case 'delay_poll':
378
+ ctx.output = await _delayPoll(client, queue_name);
379
+ break;
380
+ case 'status':
381
+ ctx.output = await _status(client, queue_name);
382
+ break;
383
+ default:
384
+ ctx.output = {
385
+ error: { code: 'MGR_UNKNOWN_ACTION', message: '不支持的操作: ' + action }
386
+ };
387
+ }
388
+ } finally {
389
+ await holder.release();
390
+ }
391
+
392
+ return ctx.output;
393
+ }
394
+
395
+ module.exports = { main };
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Redis管理器
3
+ * 管理 Redis 连接和缓存操作(基于 mm_redis)
4
+ */
5
+ async function main(params, ctx, options) {
6
+ const { redisAdmin } = require('mm_redis');
7
+ var action = params.action || '';
8
+ var key = params.key || '';
9
+ var value = params.value;
10
+ var ttl = params.ttl || 0;
11
+ var field = params.field;
12
+ var body = params.body || {};
13
+
14
+ var opts = this.getOptions();
15
+
16
+ let config = { ...opts };
17
+
18
+ var redis = redisAdmin('os', config);
19
+ await redis.open();
20
+
21
+ const SIMPLE = [
22
+ 'get', 'set', 'del', 'has', 'exists', 'add', 'getset',
23
+ 'expire', 'pexpire', 'persist', 'ttl',
24
+ 'incr', 'decr', 'incrby', 'decrby', 'incrbyfloat',
25
+ 'keys', 'clear', 'clearExpire', 'dbsize', 'flushdb', 'isConnected',
26
+ 'mget', 'mset',
27
+ 'strlen', 'append', 'setrange',
28
+ 'hgetall', 'hkeys', 'hvals', 'hlen', 'hdel', 'hmset', 'hmget',
29
+ 'setForList', 'addForList', 'getForList', 'clearForList', 'hasForList',
30
+ 'sadd', 'srem', 'sismember', 'smembers', 'scard', 'sinter', 'sunion', 'sdiff',
31
+ 'zcard', 'zrem', 'zcount', 'zrevrange',
32
+ 'publish', 'unsubscribe'
33
+ ];
34
+
35
+ const FIELD_METHODS = [
36
+ 'hget', 'hset', 'hexists',
37
+ 'zadd', 'zscore', 'zrank', 'zrevrank', 'zincrby'
38
+ ];
39
+
40
+ if (SIMPLE.indexOf(action) !== -1) {
41
+ ctx.output = await redis[action](key, value, ttl);
42
+ } else if (FIELD_METHODS.indexOf(action) !== -1) {
43
+ ctx.output = await redis[action](key, field, value, ttl);
44
+ } else if (action === 'hsetObj') {
45
+ ctx.output = await redis.hmset(key, body);
46
+ } else if (action === 'msetObj') {
47
+ ctx.output = await redis.mset(body, ttl);
48
+ } else {
49
+ ctx.output = {
50
+ error: { code: 'MGR_UNKNOWN_ACTION', message: '不支持的操作: ' + action }
51
+ };
52
+ }
53
+
54
+ return ctx.output;
55
+ }
56
+
57
+ module.exports = { main };