baja-lite 1.8.7 → 1.8.9

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 (47) hide show
  1. package/boot-remote.d.ts +1 -1
  2. package/boot-remote.js +6 -1
  3. package/boot.d.ts +1 -1
  4. package/boot.js +6 -1
  5. package/cache.d.ts +91 -0
  6. package/cache.js +515 -0
  7. package/const/index.d.ts +2 -0
  8. package/const/index.js +2 -0
  9. package/const/symbols.d.ts +73 -0
  10. package/const/symbols.js +78 -0
  11. package/const/types.d.ts +504 -0
  12. package/const/types.js +127 -0
  13. package/db/dao/format-dialects.d.ts +6 -0
  14. package/db/dao/format-dialects.js +8 -0
  15. package/db/dao/mysql.d.ts +44 -0
  16. package/db/dao/mysql.js +303 -0
  17. package/db/dao/postgresql.d.ts +44 -0
  18. package/db/dao/postgresql.js +322 -0
  19. package/db/dao/sqlite-remote.d.ts +46 -0
  20. package/db/dao/sqlite-remote.js +226 -0
  21. package/db/dao/sqlite.d.ts +41 -0
  22. package/db/dao/sqlite.js +237 -0
  23. package/db/index.d.ts +8 -0
  24. package/db/index.js +8 -0
  25. package/db/service.d.ts +778 -0
  26. package/db/service.js +1651 -0
  27. package/db/sql-template.d.ts +46 -0
  28. package/db/sql-template.js +660 -0
  29. package/db/stream-query.d.ts +607 -0
  30. package/db/stream-query.js +1626 -0
  31. package/index.d.ts +4 -1
  32. package/index.js +4 -1
  33. package/logger.d.ts +46 -0
  34. package/logger.js +35 -0
  35. package/package.json +3 -6
  36. package/sqlite.d.ts +1 -1
  37. package/sqlite.js +2 -1
  38. package/{test-mysql.js → test/test-mysql.js} +3 -2
  39. package/{test-postgresql.js → test/test-postgresql.js} +3 -2
  40. package/{test-sqlite.js → test/test-sqlite.js} +3 -2
  41. package/sql.d.ts +0 -2207
  42. package/sql.js +0 -5471
  43. /package/{test-mysql.d.ts → test/test-mysql.d.ts} +0 -0
  44. /package/{test-postgresql.d.ts → test/test-postgresql.d.ts} +0 -0
  45. /package/{test-sqlite.d.ts → test/test-sqlite.d.ts} +0 -0
  46. /package/{test-xml.d.ts → test/test-xml.d.ts} +0 -0
  47. /package/{test-xml.js → test/test-xml.js} +0 -0
package/boot-remote.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import { GlobalSqlOptionForWeb } from './sql.js';
1
+ import { GlobalSqlOptionForWeb } from './const/index.js';
2
2
  export declare const BootRomote: (options: GlobalSqlOptionForWeb) => Promise<void>;
package/boot-remote.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import { DBType, _Hump, getEnums } from 'baja-lite-field';
2
- import { ColumnMode, PrinterLogger, SqlCache, SqliteRemote, _Context, _DataConvert, _GlobalSqlOption, _LoggerService, _dao, _defOption, _enum, _primaryDB, _sqlCache } from './sql.js';
2
+ import { SqlCache, SqliteRemote } from './db/index.js';
3
+ import { PrinterLogger } from './logger.js';
4
+ import { ColumnMode, _Context, _DataConvert, _GlobalSqlOption, _LoggerService, _dao, _defOption, _enum, _primaryDB, _sqlCache } from './const/index.js';
3
5
  export const BootRomote = async function (options) {
4
6
  globalThis[_GlobalSqlOption] = Object.assign({}, _defOption);
5
7
  if (options.skipEmptyString !== undefined) {
@@ -11,6 +13,9 @@ export const BootRomote = async function (options) {
11
13
  if (options.maxDeal !== undefined) {
12
14
  globalThis[_GlobalSqlOption].maxDeal = options.maxDeal;
13
15
  }
16
+ if (options.memCacheMaxSize !== undefined) {
17
+ globalThis[_GlobalSqlOption].memCacheMaxSize = options.memCacheMaxSize;
18
+ }
14
19
  if (options.SqliteRemote !== undefined) {
15
20
  globalThis[_GlobalSqlOption].SqliteRemote = options.SqliteRemote;
16
21
  }
package/boot.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import { GlobalSqlOption } from './sql.js';
1
+ import { GlobalSqlOption } from './const/index.js';
2
2
  export declare const Boot: (options: GlobalSqlOption) => Promise<void>;
package/boot.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import { _Hump, DBType, getEnums } from 'baja-lite-field';
2
2
  import events from 'events';
3
- import { _Context, _dao, _DataConvert, _defOption, _enum, _EventBus, _fs, _GlobalSqlOption, _LoggerService, _MysqlKeepAliveTime, _path, _primaryDB, _sqlCache, ColumnMode, Mysql, Postgresql, PrinterLogger, SqlCache, Sqlite, SqliteRemote } from './sql.js';
3
+ import { _Context, _dao, _DataConvert, _defOption, _enum, _EventBus, _fs, _GlobalSqlOption, _LoggerService, _MysqlKeepAliveTime, _path, _primaryDB, _sqlCache, ColumnMode } from './const/index.js';
4
+ import { Mysql, Postgresql, SqlCache, Sqlite, SqliteRemote } from './db/index.js';
5
+ import { PrinterLogger } from './logger.js';
4
6
  export const Boot = async function (options) {
5
7
  globalThis[_GlobalSqlOption] = Object.assign({}, _defOption);
6
8
  if (options.skipEmptyString !== undefined) {
@@ -12,6 +14,9 @@ export const Boot = async function (options) {
12
14
  if (options.maxDeal !== undefined) {
13
15
  globalThis[_GlobalSqlOption].maxDeal = options.maxDeal;
14
16
  }
17
+ if (options.memCacheMaxSize !== undefined) {
18
+ globalThis[_GlobalSqlOption].memCacheMaxSize = options.memCacheMaxSize;
19
+ }
15
20
  if (options.logger) {
16
21
  globalThis[_LoggerService] = options.logger;
17
22
  }
package/cache.d.ts ADDED
@@ -0,0 +1,91 @@
1
+ import { StorageType } from './const/index.js';
2
+ export declare function getRedisDB<T = any>(db?: string): T;
3
+ export declare function GetRedisLock(key: string, lockMaxActive?: number): Promise<boolean>;
4
+ /** 对FN加锁、缓存执行 */
5
+ export declare function excuteWithLock<T>(config: {
6
+ /** 返回缓存key,参数=方法的参数+当前用户对象,可以用来清空缓存。 */
7
+ key: ((...args: any[]) => string) | string;
8
+ /** 被锁定线程是否sleep直到解锁为止? 默认true */
9
+ lockWait?: boolean;
10
+ /** 当设置了lockWait=true时,等待多少【毫秒】进行一次锁查询? 默认:100MS */
11
+ lockRetryInterval?: number;
12
+ /** 当设置了lockWait=true时,等待多少【毫秒】即视为超时,放弃本次访问?默认:永不放弃 */
13
+ lockMaxWaitTime?: number;
14
+ /** 错误信息 */
15
+ errorMessage?: string;
16
+ /** 允许的并发数,默认:1 */
17
+ lockMaxActive?: number;
18
+ /** 单个锁多少【毫秒】后自动释放?默认:60*1000MS */
19
+ lockMaxTime?: number;
20
+ }, fn__: () => Promise<T>): Promise<T>;
21
+ /**
22
+ * 方法加锁装饰器。
23
+ * 与 `MethodCache` 组合时,**不要**手工叠装饰器——`MethodCache` 内部已经实现了
24
+ * "先查缓存 → miss 才加锁 → 锁内二次检查 → 仍 miss 才执行" 的 single-flight 语义,
25
+ * 直接给方法加 `@MethodCache` 即可。`@MethodLock` 单独使用时仅做并发互斥,不感知缓存。
26
+ */
27
+ export declare function MethodLock<T = any>(config: {
28
+ /** 返回缓存key,参数=方法的参数[注意:必须和主方法的参数数量、完全一致,同时会追加一个当前用户对象]+当前用户对象,可以用来清空缓存。 */
29
+ key: ((this: T, ...args: any[]) => string) | string;
30
+ /** 被锁定线程是否sleep直到解锁为止? 默认true */
31
+ lockWait?: boolean;
32
+ /** 当设置了lockWait=true时,等待多少【毫秒】进行一次锁查询? 默认100ms */
33
+ lockRetryInterval?: number;
34
+ /** 当设置了lockWait=true时,等待多少【毫秒】即视为超时,放弃本次访问?默认永不放弃 */
35
+ lockMaxWaitTime?: number;
36
+ /** 错误信息 */
37
+ errorMessage?: string;
38
+ /** 允许的并发数,默认=1 */
39
+ lockMaxActive?: number;
40
+ /** 单个锁多少【毫秒】后自动释放?即时任务没有执行完毕或者没有主动释放锁? */
41
+ lockMaxTime?: number;
42
+ }): (target: T, _propertyKey: string, descriptor: PropertyDescriptor) => void;
43
+ /**
44
+ * 清空方法缓存。同时清 redis 和 memory 两边,业务侧无需关心存储是哪种。
45
+ * - redis 侧只在配置了 redis 时才操作;没配 redis 时安静跳过,不抛错。
46
+ * - 关联清除(clearKey)在两边都生效。
47
+ */
48
+ export declare function clearMethodCache(key: string): Promise<void>;
49
+ /**
50
+ * 执行一个方法fn,
51
+ * 如果有缓存,则返回缓存,否则【加锁、二次检查】后执行方法并缓存,防止缓存击穿。
52
+ * 可选 `cacheNullValue=true` 开启负缓存,防穿透;负缓存默认沿用 autoClearTime,可单独用 nullCacheTime 设短。
53
+ */
54
+ export declare function excuteWithCache<T>(config: {
55
+ /** 返回缓存key,参数=方法的参数+当前用户对象,可以用来清空缓存。 */
56
+ key: ((...args: any[]) => string) | string;
57
+ /** 返回缓存清除key,参数=方法的参数+当前用户对象,可以用来批量清空缓存 */
58
+ clearKey?: string[];
59
+ /** 自动清空缓存的时间,单位分钟 */
60
+ autoClearTime?: number;
61
+ /** 是否缓存 null / undefined(负缓存防穿透),默认 false。 */
62
+ cacheNullValue?: boolean;
63
+ /** 负缓存 TTL(分钟),默认沿用 autoClearTime。 */
64
+ nullCacheTime?: number;
65
+ /** 存储后端:'redis'(默认,跨进程共享)或 'memory'(进程内 LRU,更快但不跨进程)。 */
66
+ storage?: StorageType;
67
+ /** 随着当前用户sesion的清空而一起清空 */
68
+ clearWithSession?: boolean;
69
+ }, fn: () => Promise<T>): Promise<T>;
70
+ /**
71
+ * 缓存注解:先查缓存 → miss 才加锁 → 锁内再查一次 → 仍 miss 才执行原方法。
72
+ * 已内置 single-flight,不需要再叠 `@MethodLock`。
73
+ * 可选 `cacheNullValue=true` 开启负缓存,防穿透。
74
+ * 可选 `storage: 'memory'` 用进程内 LRU 替代 redis(单进程场景,毫秒级延迟)。
75
+ */
76
+ export declare function MethodCache<T = any>(config: {
77
+ /** 返回缓存key,参数=方法的参数[注意:必须和主方法的参数数量、完全一致,同时会追加一个当前用户对象]+当前用户对象,可以用来清空缓存。 */
78
+ key: ((this: T, ...args: any[]) => string) | string;
79
+ /** 返回缓存清除key,参数=方法的参数[注意:必须和主方法的参数数量、完全一致,同时会追加一个当前用户对象]+当前用户对象,可以用来批量清空缓存 */
80
+ clearKey?: ((this: T, ...args: any[]) => string[]) | string[];
81
+ /** 自动清空缓存的时间,单位分钟 */
82
+ autoClearTime?: number;
83
+ /** 是否缓存 null / undefined(负缓存防穿透),默认 false。 */
84
+ cacheNullValue?: boolean;
85
+ /** 负缓存 TTL(分钟),默认沿用 autoClearTime。 */
86
+ nullCacheTime?: number;
87
+ /** 存储后端:'redis'(默认,跨进程共享)或 'memory'(进程内 LRU,更快但不跨进程)。 */
88
+ storage?: StorageType;
89
+ /** 随着当前用户sesion的清空而一起清空 */
90
+ clearWithSession?: boolean;
91
+ }): (_target: T, _propertyKey: string, descriptor: PropertyDescriptor) => void;
package/cache.js ADDED
@@ -0,0 +1,515 @@
1
+ import { DBType } from 'baja-lite-field';
2
+ import { sleep } from './fn.js';
3
+ import { Throw } from './error.js';
4
+ import { _dao, _EventBus, _GlobalSqlOption, _LoggerService, _memCache, _memInflight, _primaryDB, } from './const/symbols.js';
5
+ import { StorageType } from './const/index.js';
6
+ export function getRedisDB(db) {
7
+ const rd = globalThis[_dao][DBType.Redis][db ?? _primaryDB];
8
+ Throw.if(!rd, 'not found redis!');
9
+ return rd;
10
+ }
11
+ /**
12
+ redlock —— 用 redlock 做一把元锁([lockex]key),把"读 count、判断 count、incr"做成原子操作。
13
+ 原实现失败时 `return await GetRedisLock(...)` 递归调用自己,redis 长时间不可用会栈溢出;
14
+ 改成循环 + 指数退避 + 最大重试上限,达到上限后抛出(让上层决定是否走降级路径)。
15
+ */
16
+ const GET_REDIS_LOCK_MAX_RETRIES = 10;
17
+ const GET_REDIS_LOCK_BASE_DELAY = 50; // 毫秒
18
+ export async function GetRedisLock(key, lockMaxActive) {
19
+ const lock = globalThis[_dao][DBType.RedisLock];
20
+ Throw.if(!lock, 'not found lock!');
21
+ const db = getRedisDB();
22
+ let lastError;
23
+ for (let attempt = 0; attempt < GET_REDIS_LOCK_MAX_RETRIES; attempt++) {
24
+ let initLock;
25
+ try {
26
+ initLock = await lock.acquire([`[lockex]${key}`], 5000);
27
+ const count = await db.get(key);
28
+ if (count === null || parseInt(count) < (lockMaxActive ?? 1)) {
29
+ await db.incr(key);
30
+ return true;
31
+ }
32
+ else {
33
+ return false;
34
+ }
35
+ }
36
+ catch (er) {
37
+ lastError = er;
38
+ // 指数退避,避免 redis 抖动时打死服务
39
+ const delay = Math.min(GET_REDIS_LOCK_BASE_DELAY * Math.pow(2, attempt), 1000);
40
+ globalThis[_LoggerService].debug?.(`GetRedisLock ${key} attempt ${attempt + 1} failed: ${er?.message}, retry after ${delay}ms`);
41
+ await sleep(delay);
42
+ }
43
+ finally {
44
+ if (initLock) {
45
+ try {
46
+ await initLock.release();
47
+ // eslint-disable-next-line no-empty
48
+ }
49
+ catch (error) {
50
+ }
51
+ }
52
+ }
53
+ }
54
+ throw new Error(`GetRedisLock ${key} failed after ${GET_REDIS_LOCK_MAX_RETRIES} retries: ${lastError?.message ?? lastError}`);
55
+ }
56
+ ;
57
+ /** 对FN加锁、缓存执行 */
58
+ export async function excuteWithLock(config, fn__) {
59
+ const key = `[lock]${typeof config.key === 'function' ? config.key() : config.key}`;
60
+ const db = getRedisDB();
61
+ let wait_time = 0;
62
+ const fn = async () => {
63
+ const lock = await GetRedisLock(key, config.lockMaxActive);
64
+ if (lock === false) {
65
+ if (config.lockWait !== false && ((config.lockMaxWaitTime ?? 0) === 0 || (wait_time + (config.lockRetryInterval ?? 100)) <= (config.lockMaxWaitTime ?? 0))) {
66
+ globalThis[_LoggerService].debug?.(`get lock ${key} fail, retry after ${config.lockRetryInterval ?? 100}ms...`);
67
+ await sleep(config.lockRetryInterval ?? 100);
68
+ wait_time += (config.lockRetryInterval ?? 100);
69
+ return await fn();
70
+ }
71
+ else {
72
+ globalThis[_LoggerService].debug?.(`get lock ${key} fail`);
73
+ throw new Error(config.errorMessage || `get lock fail: ${key}`);
74
+ }
75
+ }
76
+ else {
77
+ globalThis[_LoggerService].debug?.(`get lock ${key} ok!`);
78
+ await db.pexpire(key, config.lockMaxTime ?? 60000);
79
+ try {
80
+ return await fn__();
81
+ }
82
+ finally {
83
+ globalThis[_LoggerService].debug?.(`unlock ${key} ok!`);
84
+ await db.decr(key);
85
+ }
86
+ }
87
+ };
88
+ return await fn();
89
+ }
90
+ /**
91
+ * 方法加锁装饰器。
92
+ * 与 `MethodCache` 组合时,**不要**手工叠装饰器——`MethodCache` 内部已经实现了
93
+ * "先查缓存 → miss 才加锁 → 锁内二次检查 → 仍 miss 才执行" 的 single-flight 语义,
94
+ * 直接给方法加 `@MethodCache` 即可。`@MethodLock` 单独使用时仅做并发互斥,不感知缓存。
95
+ */
96
+ export function MethodLock(config) {
97
+ return function (target, _propertyKey, descriptor) {
98
+ const fn__ = descriptor.value;
99
+ descriptor.value = async function (...args) {
100
+ // 注意:装饰器闭包里的 `config` 是所有调用共享的同一个对象,
101
+ // 不能把 per-call 的状态写回去——必须每次构造一份新的 config 传给 excuteWithLock。
102
+ const resolvedKey = typeof config.key === 'function' ? config.key.call(this, ...args) : config.key;
103
+ const perCallConfig = { ...config, key: resolvedKey };
104
+ return await excuteWithLock(perCallConfig, async () => await fn__.call(this, ...args));
105
+ };
106
+ };
107
+ }
108
+ /** 全局 LRU 上限。可在 boot 时通过 `globalThis[_GlobalSqlOption].memCacheMaxSize` 覆盖。 */
109
+ const MEMORY_CACHE_DEFAULT_MAX_SIZE = 10000;
110
+ function getMemCache() {
111
+ let m = globalThis[_memCache];
112
+ if (!m) {
113
+ m = new Map();
114
+ globalThis[_memCache] = m;
115
+ }
116
+ return m;
117
+ }
118
+ function getMemInflight() {
119
+ let m = globalThis[_memInflight];
120
+ if (!m) {
121
+ m = new Map();
122
+ globalThis[_memInflight] = m;
123
+ }
124
+ return m;
125
+ }
126
+ function getMemCacheMaxSize() {
127
+ return globalThis[_GlobalSqlOption]?.memCacheMaxSize ?? MEMORY_CACHE_DEFAULT_MAX_SIZE;
128
+ }
129
+ /** 内存版的 parent→children 关联表,对应 redis 的 [cache-parent]* / [cache-child]* set。 */
130
+ const memParentChild = {
131
+ parentToChildren: new Map(),
132
+ childToParents: new Map(),
133
+ link(parents, child) {
134
+ for (const p of parents) {
135
+ let s = this.parentToChildren.get(p);
136
+ if (!s) {
137
+ s = new Set();
138
+ this.parentToChildren.set(p, s);
139
+ }
140
+ s.add(child);
141
+ let r = this.childToParents.get(child);
142
+ if (!r) {
143
+ r = new Set();
144
+ this.childToParents.set(child, r);
145
+ }
146
+ r.add(p);
147
+ }
148
+ },
149
+ unlinkChild(child) {
150
+ const parents = this.childToParents.get(child);
151
+ if (!parents)
152
+ return;
153
+ for (const p of parents) {
154
+ this.parentToChildren.get(p)?.delete(child);
155
+ }
156
+ this.childToParents.delete(child);
157
+ },
158
+ drainParent(parent) {
159
+ const children = this.parentToChildren.get(parent);
160
+ if (!children)
161
+ return [];
162
+ const list = [...children];
163
+ for (const c of list) {
164
+ this.childToParents.get(c)?.delete(parent);
165
+ }
166
+ this.parentToChildren.delete(parent);
167
+ return list;
168
+ }
169
+ };
170
+ /**
171
+ * 写一条内存缓存。
172
+ * - 用 Map 的插入顺序天然实现 LRU:先 delete 再 set,把 key 放到尾部;
173
+ * 超过上限时从头部(最少最近使用)开始淘汰。
174
+ * - `cacheNullValue=true` 时,null/undefined 也会以 null 形式写入(防穿透)。
175
+ */
176
+ function memSet(key, result, config) {
177
+ const isNullish = result === null || result === undefined;
178
+ if (isNullish && !config.cacheNullValue) {
179
+ return;
180
+ }
181
+ const ttlMinutes = isNullish ? (config.nullCacheTime ?? config.autoClearTime) : config.autoClearTime;
182
+ const expiresAt = ttlMinutes ? Date.now() + ttlMinutes * 60000 : 0;
183
+ const value = isNullish ? null : result;
184
+ const cache = getMemCache();
185
+ if (cache.has(key))
186
+ cache.delete(key);
187
+ cache.set(key, { value, expiresAt });
188
+ const max = getMemCacheMaxSize();
189
+ while (cache.size > max) {
190
+ const oldest = cache.keys().next().value;
191
+ if (oldest === undefined)
192
+ break;
193
+ cache.delete(oldest);
194
+ memParentChild.unlinkChild(oldest);
195
+ }
196
+ if (config.clearKey && config.clearKey.length > 0) {
197
+ memParentChild.link(config.clearKey, key);
198
+ }
199
+ globalThis[_LoggerService].debug?.(`memcache ${key} seted${isNullish ? ' (null)' : ''}!`);
200
+ }
201
+ /** 读内存缓存,lazy expire。命中后挪到 LRU 尾部。 */
202
+ function memGet(key) {
203
+ const cache = getMemCache();
204
+ const entry = cache.get(key);
205
+ if (!entry)
206
+ return { hit: false };
207
+ if (entry.expiresAt !== 0 && entry.expiresAt <= Date.now()) {
208
+ cache.delete(key);
209
+ memParentChild.unlinkChild(key);
210
+ return { hit: false };
211
+ }
212
+ cache.delete(key);
213
+ cache.set(key, entry);
214
+ return { hit: true, value: entry.value };
215
+ }
216
+ /** 设置方法缓存 */
217
+ async function setMethodCache(config, devid) {
218
+ const db = getRedisDB();
219
+ const isNullish = config.result === null || config.result === undefined;
220
+ // 旧行为:null / undefined 不进缓存;新增 cacheNullValue=true 时写入 'null'(穿透防御)。
221
+ if (isNullish && !config.cacheNullValue) {
222
+ return;
223
+ }
224
+ // 统一序列化:undefined 不是合法 JSON,归一化成 null。
225
+ const payload = isNullish ? 'null' : JSON.stringify(config.result);
226
+ // 负缓存通常用更短 TTL,避免业务恢复后还长时间返回旧的 null。
227
+ const ttl = isNullish
228
+ ? (config.nullCacheTime ?? config.autoClearTime)
229
+ : config.autoClearTime;
230
+ // 映射关系存放(负缓存也参与关联清除,因为外部清缓存的语义不区分正负)
231
+ if (config.clearKey && config.clearKey.length > 0) {
232
+ for (const clear of config.clearKey) {
233
+ await db.sadd(`[cache-parent]${clear}`, config.key);
234
+ await db.sadd(`[cache-child]${config.key}`, clear);
235
+ }
236
+ }
237
+ if (ttl) { // 自动清空
238
+ await db.set(`[cache]${config.key}`, payload, 'EX', ttl * 60);
239
+ globalThis[_LoggerService].debug?.(`cache ${config.key} seted${isNullish ? ' (null)' : ''}!`);
240
+ // 订阅:清空 clear list —— 同一 key 在缓存生命周期内只注册一次监听器,
241
+ // 否则每次 miss 都会 .on 一次,clearMethodCache 触发时回调会被执行 N 遍,
242
+ // 长期运行造成监听器泄漏。
243
+ if (config.clearKey && config.clearKey.length > 0) {
244
+ const event = `[cache]${config.key}`;
245
+ if (globalThis[_EventBus].listenerCount(event) === 0) {
246
+ globalThis[_EventBus].on(event, async (key) => {
247
+ await clearChild(key, true);
248
+ globalThis[_LoggerService].debug?.(`cache ${key} clear by key!`);
249
+ });
250
+ }
251
+ }
252
+ }
253
+ else {
254
+ await db.set(`[cache]${config.key}`, payload);
255
+ globalThis[_LoggerService].debug?.(`cache ${config.key} seted${isNullish ? ' (null)' : ''}!`);
256
+ }
257
+ if (devid) {
258
+ // 订阅:清空 clear list —— 同样去重,避免每次 miss 都重复注册。
259
+ const event = `user-${devid}`;
260
+ if (globalThis[_EventBus].listenerCount(event) === 0) {
261
+ globalThis[_EventBus].on(event, async function (key) {
262
+ await clearChild(key);
263
+ globalThis[_LoggerService].debug?.(`cache ${key} clear by devid!`);
264
+ });
265
+ }
266
+ }
267
+ }
268
+ /**
269
+ * 清空方法缓存。同时清 redis 和 memory 两边,业务侧无需关心存储是哪种。
270
+ * - redis 侧只在配置了 redis 时才操作;没配 redis 时安静跳过,不抛错。
271
+ * - 关联清除(clearKey)在两边都生效。
272
+ */
273
+ export async function clearMethodCache(key) {
274
+ // 内存侧
275
+ const cache = getMemCache();
276
+ // 1. 如果 key 是 parent,把它关联的所有 children 都清掉
277
+ const childrenFromParent = memParentChild.drainParent(key);
278
+ for (const child of childrenFromParent) {
279
+ cache.delete(child);
280
+ memParentChild.unlinkChild(child);
281
+ }
282
+ // 2. 如果 key 本身是个缓存条目,直接清,并清掉它的反向链接
283
+ if (cache.has(key)) {
284
+ cache.delete(key);
285
+ }
286
+ memParentChild.unlinkChild(key);
287
+ // redis 侧:没配 redis 时跳过
288
+ const redisDao = globalThis[_dao]?.[DBType.Redis]?.[_primaryDB];
289
+ if (!redisDao)
290
+ return;
291
+ const db = getRedisDB();
292
+ let type = await db.type(`[cache-parent]${key}`);
293
+ if (type === 'set') {
294
+ await clearParent(key);
295
+ }
296
+ type = await db.type(`[cache]${key}`);
297
+ if (type !== 'none') {
298
+ await clearChild(key);
299
+ }
300
+ }
301
+ async function clearChild(key, skipDel = false) {
302
+ const db = getRedisDB();
303
+ if (skipDel === false) {
304
+ await db.del(`[cache]${key}`);
305
+ }
306
+ const childtype = await db.type(`[cache-child]${key}`);
307
+ if (childtype === 'set') {
308
+ const parentKeys = await db.smembers(`[cache-child]${key}`);
309
+ for (const clear of parentKeys) {
310
+ const type = await db.type(`[cache-parent]${clear}`);
311
+ if (type === 'set') {
312
+ await db.srem(`[cache-parent]${clear}`, key);
313
+ }
314
+ }
315
+ await db.del(`[cache-child]${key}`);
316
+ }
317
+ }
318
+ async function clearParent(clearKey) {
319
+ const db = getRedisDB();
320
+ const keys = await db.smembers(`[cache-parent]${clearKey}`);
321
+ if (keys) {
322
+ for (const key of keys) {
323
+ globalThis[_LoggerService].debug?.(`cache ${key} cleared!`);
324
+ await clearChild(key);
325
+ }
326
+ }
327
+ }
328
+ /**
329
+ * Redis 缓存执行:先查缓存 → miss 后单飞抢锁执行 → 其它等待者只盯缓存、不抢锁。
330
+ *
331
+ * 关键点(区别于"锁内二次检查"的朴素实现):等待者并不进入 redlock 竞争队列,
332
+ * 只用轻量 SETNX 选出"首飞",其它人在 `wait_for_cache` 循环里轮询缓存。
333
+ * 这样首飞一旦写完缓存,所有等待者下一次 sleep 醒来就能并行命中,
334
+ * 而不是排着队一个个 acquire/release 锁——避免串行化。
335
+ *
336
+ * 命中判断用 `cached !== null`(ioredis 在 key 不存在时返回 `null`),
337
+ * 这样 `cacheNullValue=true` 时存进去的字面量 `'null'` 也会算作命中,达到防穿透效果。
338
+ *
339
+ * redis 不可用或 SETNX 异常时退化为无锁直跑,保留可用性。
340
+ */
341
+ const SINGLEFLIGHT_LOCK_TTL_MS = 30000; // 首飞标记 TTL,兜底首飞挂掉的情况
342
+ const SINGLEFLIGHT_POLL_INTERVAL_MS = 50; // 等待者轮询缓存的间隔
343
+ const SINGLEFLIGHT_MAX_WAIT_MS = 30000; // 等待者最长等多久;超时就自己跑 fn(保底,不应该常发生)
344
+ async function redisCacheCore(opts) {
345
+ const db = getRedisDB();
346
+ const cacheKey = `[cache]${opts.key}`;
347
+ const cached = await db.get(cacheKey);
348
+ if (cached !== null) {
349
+ globalThis[_LoggerService].debug?.(`cache ${opts.key} hit!`);
350
+ return JSON.parse(cached);
351
+ }
352
+ globalThis[_LoggerService].debug?.(`cache ${opts.key} miss!`);
353
+ const setOpts = {
354
+ clearKey: opts.clearKey,
355
+ autoClearTime: opts.autoClearTime,
356
+ cacheNullValue: opts.cacheNullValue,
357
+ nullCacheTime: opts.nullCacheTime
358
+ };
359
+ // 用一个独立的"首飞标记" key 选出谁去执行 fn——SET NX PX 是原子操作,
360
+ // 同一时刻只有一个调用者拿到 true,其它返回 null。这把"标记"不重入也不计数,
361
+ // 只是个 single-flight 指示器,和 redlock / GetRedisLock 的并发计数器不冲突。
362
+ const sfKey = `[sf]${opts.key}`;
363
+ let isLeader = false;
364
+ try {
365
+ const ok = await db.set(sfKey, '1', 'PX', SINGLEFLIGHT_LOCK_TTL_MS, 'NX');
366
+ isLeader = ok === 'OK';
367
+ }
368
+ catch (error) {
369
+ // redis 出错:退化为无锁直跑,保留可用性
370
+ globalThis[_LoggerService].warn?.(`single-flight setnx ${opts.key} failed: ${error?.message}, fallback to no-lock`);
371
+ const result = await opts.fn();
372
+ await setMethodCache({ key: opts.key, ...setOpts, result }, opts.devid);
373
+ return result;
374
+ }
375
+ if (isLeader) {
376
+ // 首飞:跑 fn、写缓存、释放首飞标记
377
+ try {
378
+ const result = await opts.fn();
379
+ await setMethodCache({ key: opts.key, ...setOpts, result }, opts.devid);
380
+ return result;
381
+ }
382
+ finally {
383
+ try {
384
+ await db.del(sfKey);
385
+ }
386
+ catch {
387
+ // 标记 TTL 会兜底,删失败不影响正确性
388
+ }
389
+ }
390
+ }
391
+ // 等待者:循环只看缓存,不抢锁
392
+ const waitStart = Date.now();
393
+ while (true) {
394
+ await sleep(SINGLEFLIGHT_POLL_INTERVAL_MS);
395
+ const recheck = await db.get(cacheKey);
396
+ if (recheck !== null) {
397
+ globalThis[_LoggerService].debug?.(`cache ${opts.key} hit after wait!`);
398
+ return JSON.parse(recheck);
399
+ }
400
+ // 首飞挂了(TTL 到期,sf 标记自动消失)但缓存仍然空——抢做下一任首飞
401
+ const leaderGone = await db.get(sfKey);
402
+ if (leaderGone === null) {
403
+ const taken = await db.set(sfKey, '1', 'PX', SINGLEFLIGHT_LOCK_TTL_MS, 'NX');
404
+ if (taken === 'OK') {
405
+ globalThis[_LoggerService].debug?.(`cache ${opts.key}: previous leader gone, taking over`);
406
+ try {
407
+ const result = await opts.fn();
408
+ await setMethodCache({ key: opts.key, ...setOpts, result }, opts.devid);
409
+ return result;
410
+ }
411
+ finally {
412
+ try {
413
+ await db.del(sfKey);
414
+ }
415
+ catch { /* TTL 兜底 */ }
416
+ }
417
+ }
418
+ // 没抢到说明又有别人接手了,继续等
419
+ }
420
+ // 等待上限保底:极端情况下首飞和接班都不正常时,自己直接跑,宁可重复执行也不挂死
421
+ if (Date.now() - waitStart > SINGLEFLIGHT_MAX_WAIT_MS) {
422
+ globalThis[_LoggerService].warn?.(`cache ${opts.key}: wait timed out, running fn without lock`);
423
+ const result = await opts.fn();
424
+ await setMethodCache({ key: opts.key, ...setOpts, result }, opts.devid);
425
+ return result;
426
+ }
427
+ }
428
+ }
429
+ /**
430
+ * 内存缓存执行:单进程内的 single-flight 不需要任何分布式协调——
431
+ * JS 单线程 + 同一份 in-flight Map 就够了。同一 key 的并发调用全部 await 同一个 Promise,
432
+ * Promise 解析后所有等待者下个事件循环 tick 并发返回,零序列化、零 redis 往返。
433
+ */
434
+ async function memoryCacheCore(opts) {
435
+ const cacheKey = opts.key;
436
+ // 1. 看缓存
437
+ const hit = memGet(cacheKey);
438
+ if (hit.hit) {
439
+ globalThis[_LoggerService].debug?.(`memcache ${opts.key} hit!`);
440
+ return hit.value;
441
+ }
442
+ globalThis[_LoggerService].debug?.(`memcache ${opts.key} miss!`);
443
+ // 2. 已有等待中的 Promise → 直接 await,single-flight 自动达成
444
+ const inflight = getMemInflight();
445
+ const existing = inflight.get(cacheKey);
446
+ if (existing) {
447
+ return await existing;
448
+ }
449
+ // 3. 首飞:把 Promise 提前塞进 inflight,让后到者命中
450
+ const p = (async () => {
451
+ try {
452
+ const result = await opts.fn();
453
+ memSet(cacheKey, result, {
454
+ autoClearTime: opts.autoClearTime,
455
+ cacheNullValue: opts.cacheNullValue,
456
+ nullCacheTime: opts.nullCacheTime,
457
+ clearKey: opts.clearKey
458
+ });
459
+ return result;
460
+ }
461
+ finally {
462
+ inflight.delete(cacheKey);
463
+ }
464
+ })();
465
+ inflight.set(cacheKey, p);
466
+ return await p;
467
+ }
468
+ async function excuteCacheCore(opts) {
469
+ return opts.storage === StorageType.Memory ? await memoryCacheCore(opts) : await redisCacheCore(opts);
470
+ }
471
+ /**
472
+ * 执行一个方法fn,
473
+ * 如果有缓存,则返回缓存,否则【加锁、二次检查】后执行方法并缓存,防止缓存击穿。
474
+ * 可选 `cacheNullValue=true` 开启负缓存,防穿透;负缓存默认沿用 autoClearTime,可单独用 nullCacheTime 设短。
475
+ */
476
+ export async function excuteWithCache(config, fn) {
477
+ const key = typeof config.key === 'function' ? config.key() : config.key;
478
+ return await excuteCacheCore({
479
+ key,
480
+ clearKey: config.clearKey,
481
+ autoClearTime: config.autoClearTime,
482
+ cacheNullValue: config.cacheNullValue,
483
+ nullCacheTime: config.nullCacheTime,
484
+ storage: config.storage,
485
+ fn
486
+ });
487
+ }
488
+ /**
489
+ * 缓存注解:先查缓存 → miss 才加锁 → 锁内再查一次 → 仍 miss 才执行原方法。
490
+ * 已内置 single-flight,不需要再叠 `@MethodLock`。
491
+ * 可选 `cacheNullValue=true` 开启负缓存,防穿透。
492
+ * 可选 `storage: 'memory'` 用进程内 LRU 替代 redis(单进程场景,毫秒级延迟)。
493
+ */
494
+ export function MethodCache(config) {
495
+ return function (_target, _propertyKey, descriptor) {
496
+ const fn = descriptor.value;
497
+ descriptor.value = async function (...args) {
498
+ const key = typeof config.key === 'function' ? config.key.call(this, ...args) : config.key;
499
+ const clearKey = config.clearKey
500
+ ? (typeof config.clearKey === 'function' ? config.clearKey.call(this, ...args) : config.clearKey)
501
+ : undefined;
502
+ const devid = config.clearWithSession && this.ctx && this.ctx.me && this.ctx.me.devid;
503
+ return await excuteCacheCore({
504
+ key,
505
+ clearKey,
506
+ autoClearTime: config.autoClearTime,
507
+ cacheNullValue: config.cacheNullValue,
508
+ nullCacheTime: config.nullCacheTime,
509
+ storage: config.storage,
510
+ devid,
511
+ fn: async () => await fn.call(this, ...args)
512
+ });
513
+ };
514
+ };
515
+ }
@@ -0,0 +1,2 @@
1
+ export * from '../const/symbols.js';
2
+ export * from '../const/types.js';
package/const/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export * from '../const/symbols.js';
2
+ export * from '../const/types.js';