baja-lite 1.8.13 → 1.8.15
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/boot.js +3 -0
- package/cache.d.ts +24 -1
- package/cache.js +6 -19
- package/event.d.ts +128 -8
- package/event.js +223 -32
- package/package.json +1 -1
package/boot.js
CHANGED
|
@@ -3,6 +3,7 @@ import events from 'events';
|
|
|
3
3
|
import { _Context, _dao, _DataConvert, _defOption, _enum, _EventBus, _fs, _GlobalSqlOption, _LoggerService, _MysqlKeepAliveTime, _path, _primaryDB, _sqlCache, ColumnMode } from './const/index.js';
|
|
4
4
|
import { Mysql, Postgresql, SqlCache, Sqlite, SqliteRemote } from './db/index.js';
|
|
5
5
|
import { PrinterLogger } from './logger.js';
|
|
6
|
+
import { initEventSubscriber } from './event.js';
|
|
6
7
|
export const Boot = async function (options) {
|
|
7
8
|
globalThis[_GlobalSqlOption] = Object.assign({}, _defOption);
|
|
8
9
|
if (options.skipEmptyString !== undefined) {
|
|
@@ -156,6 +157,8 @@ export const Boot = async function (options) {
|
|
|
156
157
|
globalThis[_LoggerService].error('event-bus', error);
|
|
157
158
|
});
|
|
158
159
|
globalThis[_EventBus] = event;
|
|
160
|
+
// 初始化 Redis 事件跨进程桥接(订阅 [event]* 频道)
|
|
161
|
+
await initEventSubscriber();
|
|
159
162
|
}
|
|
160
163
|
if (options.Postgresql) {
|
|
161
164
|
const Pool = await import('pg-pool');
|
package/cache.d.ts
CHANGED
|
@@ -155,13 +155,33 @@ export declare function clearMethodCache(key: string): Promise<void>;
|
|
|
155
155
|
* ```
|
|
156
156
|
*/
|
|
157
157
|
export declare function excuteWithCache<T>(config: {
|
|
158
|
+
/** 返回缓存key,参数=方法的参数[注意:必须和主方法的参数数量、完全一致,同时会追加一个当前用户对象]+当前用户对象,可以用来清空缓存。 */
|
|
158
159
|
key: ((...args: any[]) => string) | string;
|
|
160
|
+
/** 返回缓存清除key,参数=方法的参数[注意:必须和主方法的参数数量、完全一致,同时会追加一个当前用户对象]+当前用户对象,可以用来批量清空缓存 */
|
|
159
161
|
clearKey?: ((...args: any[]) => string[]) | string[];
|
|
162
|
+
/**
|
|
163
|
+
* 自动清空缓存的时间,单位分钟。
|
|
164
|
+
* ⚠️ 不能与 `staleMode: StaleWhileRevalidate` 同用:stale 仅标记过期,autoClearTime 会物理删除 key,语义冲突。
|
|
165
|
+
*/
|
|
160
166
|
autoClearTime?: number;
|
|
167
|
+
/** 是否缓存 null / undefined(负缓存防穿透),默认 false。 */
|
|
161
168
|
cacheNullValue?: boolean;
|
|
169
|
+
/** 负缓存 TTL(分钟),默认沿用 autoClearTime。 */
|
|
162
170
|
nullCacheTime?: number;
|
|
171
|
+
/**
|
|
172
|
+
* 随着当前用户sesion的清空而一起清空,需要业务系统配合,用户退出时通过
|
|
173
|
+
* event 里的 trigger(devid)触发
|
|
174
|
+
*/
|
|
163
175
|
clearWithSession?: boolean;
|
|
176
|
+
/**
|
|
177
|
+
* 缓存被清理时的行为策略。默认 Purge。
|
|
178
|
+
* StaleWhileRevalidate 时设 sa=1 使 clearMethodCache 走"标记 stale + 保留旧值"路径。
|
|
179
|
+
*/
|
|
164
180
|
staleMode?: CacheStaleMode;
|
|
181
|
+
/**
|
|
182
|
+
* 缓存更新回调。异步方法,在 setMethodCache 成功写缓存后调用。
|
|
183
|
+
* 调用时三个参数:this → 执行上下文;args → 方法输入参数;value → 缓存值。
|
|
184
|
+
*/
|
|
165
185
|
onCacheUpdated?: (this: any, value: T, ...args: any[]) => void | Promise<void>;
|
|
166
186
|
ctx?: any;
|
|
167
187
|
args?: any[];
|
|
@@ -247,7 +267,10 @@ export declare function MethodCache<T = any>(config: {
|
|
|
247
267
|
cacheNullValue?: boolean;
|
|
248
268
|
/** 负缓存 TTL(分钟),默认沿用 autoClearTime。 */
|
|
249
269
|
nullCacheTime?: number;
|
|
250
|
-
/**
|
|
270
|
+
/**
|
|
271
|
+
* 随着当前用户sesion的清空而一起清空,需要业务系统配合,用户退出时通过
|
|
272
|
+
* event 里的 trigger(devid)触发
|
|
273
|
+
*/
|
|
251
274
|
clearWithSession?: boolean;
|
|
252
275
|
/**
|
|
253
276
|
* 缓存被清理时的行为策略。默认 Purge。
|
package/cache.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { DBType } from 'baja-lite-field';
|
|
2
2
|
import { sleep } from './fn.js';
|
|
3
3
|
import { Throw } from './error.js';
|
|
4
|
-
import { _dao,
|
|
4
|
+
import { _dao, _LoggerService, _primaryDB, } from './const/symbols.js';
|
|
5
|
+
import { on } from './event.js';
|
|
5
6
|
/** @internal 输出缓存分类日志(仅当 setLogLevels 包含 'cache' 时生效) */
|
|
6
7
|
function cacheLog(message, ...params) {
|
|
7
8
|
const logger = globalThis[_LoggerService];
|
|
@@ -229,27 +230,13 @@ async function setMethodCache(config, devid) {
|
|
|
229
230
|
// staleAllowed 已存储在缓存值内部(CacheValue.sa),无需单独的 meta key
|
|
230
231
|
// 删除旧格式残留(兼容升级)
|
|
231
232
|
// await db.del(`[cache-meta]${config.key}`); // 清理旧格式残留
|
|
232
|
-
// 订阅:清空 clear list —— 同一 key 在缓存生命周期内只注册一次监听器,
|
|
233
|
-
// 否则每次 miss 都会 .on 一次,clearMethodCache 触发时回调会被执行 N 遍,
|
|
234
|
-
// 长期运行造成监听器泄漏。
|
|
235
|
-
if (config.clearKey && config.clearKey.length > 0) {
|
|
236
|
-
const event = CacheKey.value(config.key);
|
|
237
|
-
if (globalThis[_EventBus].listenerCount(event) === 0) {
|
|
238
|
-
globalThis[_EventBus].on(event, async (key) => {
|
|
239
|
-
await clearCacheKey(key);
|
|
240
|
-
cacheLog(`${key} clear by key!`);
|
|
241
|
-
});
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
233
|
if (devid) {
|
|
245
234
|
// 订阅:清空 clear list —— 同样去重,避免每次 miss 都重复注册。
|
|
246
235
|
const event = `user-${devid}`;
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
});
|
|
252
|
-
}
|
|
236
|
+
on(event, async (key) => {
|
|
237
|
+
await clearCacheKey(key);
|
|
238
|
+
cacheLog(`${key} clear by devid!`);
|
|
239
|
+
}, { unique: true });
|
|
253
240
|
}
|
|
254
241
|
}
|
|
255
242
|
/**
|
package/event.d.ts
CHANGED
|
@@ -1,10 +1,130 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
/** on() 的选项 */
|
|
2
|
+
interface OnOptions {
|
|
3
|
+
/**
|
|
4
|
+
* 设为 true 后,若该事件名下已有注册的监听器,则跳过本次注册。
|
|
5
|
+
* 适用于在热点路径中反复调用 on()、不希望重复注册的场景。
|
|
6
|
+
*/
|
|
7
|
+
unique?: boolean;
|
|
8
|
+
/**
|
|
9
|
+
* 设为 true 后,监听器触发一次后自动移除(标准 EventEmitter.once 语义)。
|
|
10
|
+
*/
|
|
11
|
+
once?: boolean;
|
|
9
12
|
}
|
|
13
|
+
/** trigger() 的选项 */
|
|
14
|
+
interface TriggerOptions {
|
|
15
|
+
/**
|
|
16
|
+
* 是否在进程内 emit。默认 true。
|
|
17
|
+
*/
|
|
18
|
+
local?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* 是否通过 Redis 广播到其他进程。默认 true。
|
|
21
|
+
* 无 Redis 连接时静默跳过。
|
|
22
|
+
*/
|
|
23
|
+
remote?: boolean;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* 注册事件监听器(进程级 + 跨进程)。
|
|
27
|
+
*
|
|
28
|
+
* 当前进程内通过 `trigger` 触发,或从同一 Redis 集群的其他进程广播而来,
|
|
29
|
+
* 均会调起此 listener。
|
|
30
|
+
*
|
|
31
|
+
* @param event - 事件名
|
|
32
|
+
* @param listener - 回调函数
|
|
33
|
+
* @param opts.unique - true 时若该事件已有监听器则跳过注册(防止热点路径中重复注册)
|
|
34
|
+
* @param opts.once - true 时触发一次后自动移除(标准 once 语义)
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```ts
|
|
38
|
+
* import { on } from 'baja-lite/event';
|
|
39
|
+
*
|
|
40
|
+
* // 普通注册(每次调用都会新增一个 listener)
|
|
41
|
+
* on('user-login', (uid: string, ts: number) => {
|
|
42
|
+
* console.log(`用户 ${uid} 在 ${ts} 登录`);
|
|
43
|
+
* });
|
|
44
|
+
*
|
|
45
|
+
* // 唯一注册(热点路径中多次调用也只注册一次)
|
|
46
|
+
* on('cache-cleared', handler, { unique: true });
|
|
47
|
+
*
|
|
48
|
+
* // 仅触发一次后自动移除
|
|
49
|
+
* on('ready', () => console.log('只执行一次'), { once: true });
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export declare function on(event: string, listener: (...args: any[]) => void, opts?: OnOptions): void;
|
|
53
|
+
/**
|
|
54
|
+
* 移除事件监听器。
|
|
55
|
+
*
|
|
56
|
+
* - 传入 `listener` → 移除指定监听器(必须与注册时引用一致)
|
|
57
|
+
* - 不传 `listener` → 移除该事件下的全部监听器
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```ts
|
|
61
|
+
* import { on, off } from 'baja-lite/event';
|
|
62
|
+
*
|
|
63
|
+
* const handler = (uid: string) => console.log(uid);
|
|
64
|
+
* on('user-login', handler);
|
|
65
|
+
*
|
|
66
|
+
* // 移除指定监听器
|
|
67
|
+
* off('user-login', handler);
|
|
68
|
+
*
|
|
69
|
+
* // 移除该事件下所有监听器
|
|
70
|
+
* off('user-login');
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
export declare function off(event: string, listener?: (...args: any[]) => void): void;
|
|
74
|
+
/**
|
|
75
|
+
* 触发事件。
|
|
76
|
+
*
|
|
77
|
+
* 通过 `opts` 控制广播范围:
|
|
78
|
+
* - `undefined`(默认)— 进程内 + 跨进程
|
|
79
|
+
* - `{ local: true }` — 仅本进程
|
|
80
|
+
* - `{ remote: true, local: false }` — 仅 Redis 广播到其他进程
|
|
81
|
+
*
|
|
82
|
+
* Redis 发布是 fire-and-forget,返回值仅反映**本进程**是否有监听器。
|
|
83
|
+
* 跨进程广播时参数需可 JSON 序列化(Function / Symbol 会被替换为 null)。
|
|
84
|
+
*
|
|
85
|
+
* @param event - 事件名
|
|
86
|
+
* @param args - 传给监听器的参数数组
|
|
87
|
+
* @param opts - 控制广播范围
|
|
88
|
+
* @returns 进程内是否有 listener
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```ts
|
|
92
|
+
* import { on, trigger } from 'baja-lite/event';
|
|
93
|
+
*
|
|
94
|
+
* // PM2 4 进程全都收到
|
|
95
|
+
* trigger('order-paid', [orderId, amount]);
|
|
96
|
+
*
|
|
97
|
+
* // 仅本进程
|
|
98
|
+
* trigger('timer-tick', [Date.now()], { remote: false });
|
|
99
|
+
*
|
|
100
|
+
* // 仅通知其他进程
|
|
101
|
+
* trigger('cache-invalidated', ['user-list'], { local: false });
|
|
102
|
+
*
|
|
103
|
+
* // 无参数(默认全广播)
|
|
104
|
+
* trigger('reload-config');
|
|
105
|
+
* ```
|
|
106
|
+
*/
|
|
107
|
+
export declare function trigger(event: string, args?: any[], opts?: TriggerOptions): boolean;
|
|
108
|
+
/**
|
|
109
|
+
* 初始化 Redis 事件订阅桥接器。
|
|
110
|
+
*
|
|
111
|
+
* 应在 `boot()` 完成后调用一次(多次调用安全,仅首次生效)。
|
|
112
|
+
* 若未连接 Redis,调用无副作用。
|
|
113
|
+
*
|
|
114
|
+
* **工作原理:**
|
|
115
|
+
* - 通过 `redis.duplicate()` 创建独立订阅连接(不影响主连接的命令操作)
|
|
116
|
+
* - 使用 `psubscribe` 订阅所有 `[event]*` 频道
|
|
117
|
+
* - 收到消息后校验 `__pid`:跳过自身发布的消息(本进程已通过本地 emit 处理)
|
|
118
|
+
* - 其余消息 deserialize 后 emit 到本地 EventBus
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* ```ts
|
|
122
|
+
* import { boot } from 'baja-lite';
|
|
123
|
+
* import { initEventSubscriber } from 'baja-lite/event';
|
|
124
|
+
*
|
|
125
|
+
* await boot({ ... });
|
|
126
|
+
* await initEventSubscriber();
|
|
127
|
+
* ```
|
|
128
|
+
*/
|
|
129
|
+
export declare function initEventSubscriber(): Promise<void>;
|
|
10
130
|
export {};
|
package/event.js
CHANGED
|
@@ -1,38 +1,229 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
import { _dao, _EventBus, _LoggerService, _primaryDB } from './const/symbols.js';
|
|
2
|
+
import { DBType } from 'baja-lite-field';
|
|
3
|
+
// ===== 常量 =====
|
|
4
|
+
/** Redis 事件频道前缀,完整频道名 = `[event]` + 事件名 */
|
|
5
|
+
const CHANNEL_PREFIX = '[event]';
|
|
6
|
+
/** PSubscribe 匹配模式 */
|
|
7
|
+
const SUBSCRIBE_PATTERN = `${CHANNEL_PREFIX}*`;
|
|
8
|
+
/** 当前进程 pid,用于过滤自身发出的 Redis 消息 */
|
|
9
|
+
const __pid = process.pid;
|
|
10
|
+
/** 是否已执行过 initEventSubscriber */
|
|
11
|
+
let _subscriberReady = false;
|
|
12
|
+
// ===== 内部工具 =====
|
|
13
|
+
function getEventBus() {
|
|
14
|
+
const bus = globalThis[_EventBus];
|
|
15
|
+
if (!bus) {
|
|
16
|
+
const logger = globalThis[_LoggerService];
|
|
17
|
+
logger?.warn?.('event-bus not initialized, did you forget to call boot()?');
|
|
18
|
+
throw new Error('EventBus not initialized. Please ensure boot() has been called before using event methods.');
|
|
19
|
+
}
|
|
20
|
+
return bus;
|
|
21
|
+
}
|
|
22
|
+
function getLogger() {
|
|
23
|
+
return globalThis[_LoggerService];
|
|
24
|
+
}
|
|
25
|
+
function getRedis() {
|
|
26
|
+
try {
|
|
27
|
+
return globalThis[_dao]?.[DBType.Redis]?.[_primaryDB];
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** 将参数序列化为 Redis 消息(Function/Symbol 替换为 null,JSON 无法序列化的值跳过) */
|
|
34
|
+
function safeArgs(args) {
|
|
35
|
+
return args.map(v => {
|
|
36
|
+
if (typeof v === 'function' || typeof v === 'symbol')
|
|
37
|
+
return null;
|
|
38
|
+
try {
|
|
39
|
+
JSON.stringify(v);
|
|
40
|
+
return v;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/** 构造 Redis 消息 */
|
|
48
|
+
function packPayload(args) {
|
|
49
|
+
return JSON.stringify({ a: safeArgs(args), p: __pid });
|
|
50
|
+
}
|
|
51
|
+
// ===== 公共 API =====
|
|
52
|
+
/**
|
|
53
|
+
* 注册事件监听器(进程级 + 跨进程)。
|
|
54
|
+
*
|
|
55
|
+
* 当前进程内通过 `trigger` 触发,或从同一 Redis 集群的其他进程广播而来,
|
|
56
|
+
* 均会调起此 listener。
|
|
57
|
+
*
|
|
58
|
+
* @param event - 事件名
|
|
59
|
+
* @param listener - 回调函数
|
|
60
|
+
* @param opts.unique - true 时若该事件已有监听器则跳过注册(防止热点路径中重复注册)
|
|
61
|
+
* @param opts.once - true 时触发一次后自动移除(标准 once 语义)
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```ts
|
|
65
|
+
* import { on } from 'baja-lite/event';
|
|
66
|
+
*
|
|
67
|
+
* // 普通注册(每次调用都会新增一个 listener)
|
|
68
|
+
* on('user-login', (uid: string, ts: number) => {
|
|
69
|
+
* console.log(`用户 ${uid} 在 ${ts} 登录`);
|
|
70
|
+
* });
|
|
71
|
+
*
|
|
72
|
+
* // 唯一注册(热点路径中多次调用也只注册一次)
|
|
73
|
+
* on('cache-cleared', handler, { unique: true });
|
|
74
|
+
*
|
|
75
|
+
* // 仅触发一次后自动移除
|
|
76
|
+
* on('ready', () => console.log('只执行一次'), { once: true });
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
export function on(event, listener, opts) {
|
|
80
|
+
const bus = getEventBus();
|
|
81
|
+
if (opts?.unique && bus.listenerCount(event) > 0)
|
|
82
|
+
return;
|
|
83
|
+
if (opts?.once) {
|
|
13
84
|
const wrapper = (...args) => {
|
|
14
|
-
|
|
15
|
-
|
|
85
|
+
bus.off(event, wrapper);
|
|
86
|
+
listener(...args);
|
|
16
87
|
};
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
88
|
+
bus.on(event, wrapper);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
bus.on(event, listener);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* 移除事件监听器。
|
|
96
|
+
*
|
|
97
|
+
* - 传入 `listener` → 移除指定监听器(必须与注册时引用一致)
|
|
98
|
+
* - 不传 `listener` → 移除该事件下的全部监听器
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* ```ts
|
|
102
|
+
* import { on, off } from 'baja-lite/event';
|
|
103
|
+
*
|
|
104
|
+
* const handler = (uid: string) => console.log(uid);
|
|
105
|
+
* on('user-login', handler);
|
|
106
|
+
*
|
|
107
|
+
* // 移除指定监听器
|
|
108
|
+
* off('user-login', handler);
|
|
109
|
+
*
|
|
110
|
+
* // 移除该事件下所有监听器
|
|
111
|
+
* off('user-login');
|
|
112
|
+
* ```
|
|
113
|
+
*/
|
|
114
|
+
export function off(event, listener) {
|
|
115
|
+
const bus = getEventBus();
|
|
116
|
+
if (listener) {
|
|
117
|
+
bus.off(event, listener);
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
bus.removeAllListeners(event);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* 触发事件。
|
|
125
|
+
*
|
|
126
|
+
* 通过 `opts` 控制广播范围:
|
|
127
|
+
* - `undefined`(默认)— 进程内 + 跨进程
|
|
128
|
+
* - `{ local: true }` — 仅本进程
|
|
129
|
+
* - `{ remote: true, local: false }` — 仅 Redis 广播到其他进程
|
|
130
|
+
*
|
|
131
|
+
* Redis 发布是 fire-and-forget,返回值仅反映**本进程**是否有监听器。
|
|
132
|
+
* 跨进程广播时参数需可 JSON 序列化(Function / Symbol 会被替换为 null)。
|
|
133
|
+
*
|
|
134
|
+
* @param event - 事件名
|
|
135
|
+
* @param args - 传给监听器的参数数组
|
|
136
|
+
* @param opts - 控制广播范围
|
|
137
|
+
* @returns 进程内是否有 listener
|
|
138
|
+
*
|
|
139
|
+
* @example
|
|
140
|
+
* ```ts
|
|
141
|
+
* import { on, trigger } from 'baja-lite/event';
|
|
142
|
+
*
|
|
143
|
+
* // PM2 4 进程全都收到
|
|
144
|
+
* trigger('order-paid', [orderId, amount]);
|
|
145
|
+
*
|
|
146
|
+
* // 仅本进程
|
|
147
|
+
* trigger('timer-tick', [Date.now()], { remote: false });
|
|
148
|
+
*
|
|
149
|
+
* // 仅通知其他进程
|
|
150
|
+
* trigger('cache-invalidated', ['user-list'], { local: false });
|
|
151
|
+
*
|
|
152
|
+
* // 无参数(默认全广播)
|
|
153
|
+
* trigger('reload-config');
|
|
154
|
+
* ```
|
|
155
|
+
*/
|
|
156
|
+
export function trigger(event, args, opts) {
|
|
157
|
+
const a = args ?? [];
|
|
158
|
+
const local = opts?.local ?? true;
|
|
159
|
+
const remote = opts?.remote ?? true;
|
|
160
|
+
if (remote) {
|
|
161
|
+
const redis = getRedis();
|
|
162
|
+
if (redis) {
|
|
163
|
+
redis.publish(`${CHANNEL_PREFIX}${event}`, packPayload(a)).catch((err) => {
|
|
164
|
+
getLogger()?.warn?.(`event publish "${event}" failed: ${err?.message ?? err}`);
|
|
165
|
+
});
|
|
30
166
|
}
|
|
31
|
-
return this;
|
|
32
167
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
168
|
+
if (local) {
|
|
169
|
+
return getEventBus().emit(event, ...a);
|
|
170
|
+
}
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* 初始化 Redis 事件订阅桥接器。
|
|
175
|
+
*
|
|
176
|
+
* 应在 `boot()` 完成后调用一次(多次调用安全,仅首次生效)。
|
|
177
|
+
* 若未连接 Redis,调用无副作用。
|
|
178
|
+
*
|
|
179
|
+
* **工作原理:**
|
|
180
|
+
* - 通过 `redis.duplicate()` 创建独立订阅连接(不影响主连接的命令操作)
|
|
181
|
+
* - 使用 `psubscribe` 订阅所有 `[event]*` 频道
|
|
182
|
+
* - 收到消息后校验 `__pid`:跳过自身发布的消息(本进程已通过本地 emit 处理)
|
|
183
|
+
* - 其余消息 deserialize 后 emit 到本地 EventBus
|
|
184
|
+
*
|
|
185
|
+
* @example
|
|
186
|
+
* ```ts
|
|
187
|
+
* import { boot } from 'baja-lite';
|
|
188
|
+
* import { initEventSubscriber } from 'baja-lite/event';
|
|
189
|
+
*
|
|
190
|
+
* await boot({ ... });
|
|
191
|
+
* await initEventSubscriber();
|
|
192
|
+
* ```
|
|
193
|
+
*/
|
|
194
|
+
export async function initEventSubscriber() {
|
|
195
|
+
if (_subscriberReady)
|
|
196
|
+
return;
|
|
197
|
+
_subscriberReady = true;
|
|
198
|
+
const redis = getRedis();
|
|
199
|
+
if (!redis) {
|
|
200
|
+
getLogger()?.debug?.('event subscriber: Redis not configured, skip');
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
try {
|
|
204
|
+
// duplicate() 基于同一套连接参数创建新实例,用于 subscribe 模式
|
|
205
|
+
const sub = typeof redis.duplicate === 'function'
|
|
206
|
+
? redis.duplicate()
|
|
207
|
+
: redis;
|
|
208
|
+
sub.on('pmessage', (_pattern, channel, message) => {
|
|
209
|
+
try {
|
|
210
|
+
const payload = JSON.parse(message);
|
|
211
|
+
// 跳过自己发出去的消息(本进程已通过本地 emit 处理)
|
|
212
|
+
if (payload.p === __pid)
|
|
213
|
+
return;
|
|
214
|
+
// 从频道名提取原始事件名:`[event]user-login` → `user-login`
|
|
215
|
+
const event = channel.slice(CHANNEL_PREFIX.length);
|
|
216
|
+
getEventBus().emit(event, ...(payload.a ?? []));
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
// 反序列化失败,静默丢弃
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
await sub.psubscribe(SUBSCRIBE_PATTERN);
|
|
223
|
+
getLogger()?.debug?.('event subscriber: Redis bridge ready');
|
|
224
|
+
}
|
|
225
|
+
catch (err) {
|
|
226
|
+
getLogger()?.warn?.(`event subscriber init failed: ${err?.message ?? err}`);
|
|
227
|
+
_subscriberReady = false; // 允许重试
|
|
37
228
|
}
|
|
38
229
|
}
|