baja-lite 1.8.8 → 1.8.10
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-remote.d.ts +1 -1
- package/boot-remote.js +6 -1
- package/boot.d.ts +1 -1
- package/boot.js +6 -1
- package/cache.d.ts +202 -0
- package/cache.js +593 -0
- package/const/index.d.ts +2 -0
- package/const/index.js +2 -0
- package/const/symbols.d.ts +71 -0
- package/const/symbols.js +76 -0
- package/const/types.d.ts +520 -0
- package/const/types.js +127 -0
- package/db/dao/format-dialects.d.ts +6 -0
- package/db/dao/format-dialects.js +8 -0
- package/db/dao/mysql.d.ts +44 -0
- package/db/dao/mysql.js +276 -0
- package/db/dao/postgresql.d.ts +44 -0
- package/db/dao/postgresql.js +294 -0
- package/db/dao/sqlite-remote.d.ts +46 -0
- package/db/dao/sqlite-remote.js +211 -0
- package/db/dao/sqlite.d.ts +41 -0
- package/db/dao/sqlite.js +219 -0
- package/db/index.d.ts +8 -0
- package/db/index.js +8 -0
- package/db/service.d.ts +778 -0
- package/db/service.js +1651 -0
- package/db/sql-template.d.ts +46 -0
- package/db/sql-template.js +661 -0
- package/db/stream-query.d.ts +607 -0
- package/db/stream-query.js +1626 -0
- package/index.d.ts +4 -1
- package/index.js +4 -1
- package/logger.d.ts +73 -0
- package/logger.js +116 -0
- package/package.json +1 -1
- package/sqlite.d.ts +1 -1
- package/sqlite.js +8 -7
- package/{test-mysql.js → test/test-mysql.js} +3 -2
- package/{test-postgresql.js → test/test-postgresql.js} +3 -2
- package/{test-sqlite.js → test/test-sqlite.js} +3 -2
- package/sql.d.ts +0 -2222
- package/sql.js +0 -5503
- /package/{test-mysql.d.ts → test/test-mysql.d.ts} +0 -0
- /package/{test-postgresql.d.ts → test/test-postgresql.d.ts} +0 -0
- /package/{test-sqlite.d.ts → test/test-sqlite.d.ts} +0 -0
- /package/{test-xml.d.ts → test/test-xml.d.ts} +0 -0
- /package/{test-xml.js → test/test-xml.js} +0 -0
package/cache.js
ADDED
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
import { DBType } from 'baja-lite-field';
|
|
2
|
+
import { sleep } from './fn.js';
|
|
3
|
+
import { Throw } from './error.js';
|
|
4
|
+
import { _dao, _EventBus, _LoggerService, _primaryDB, } from './const/symbols.js';
|
|
5
|
+
/** @internal 输出缓存分类日志(仅当 setLogLevels 包含 'cache' 时生效) */
|
|
6
|
+
function cacheLog(message, ...params) {
|
|
7
|
+
const logger = globalThis[_LoggerService];
|
|
8
|
+
// pino-pretty 的 messageFormat 会根据 level=24 自动添加 [CACHE] 前缀
|
|
9
|
+
logger.debugCategory?.('cache', message, ...params);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* @internal 全局 stale 刷新 Promise 映射。
|
|
13
|
+
* key = 缓存 key(不含前缀),value = 正在进行的 stale 刷新 Promise。
|
|
14
|
+
* @MethodCache 内部的 staleBackgroundRefresh 通过这个映射注册刷新 Promise,
|
|
15
|
+
* 同一 key 的并发 stale 命中复用同一个 Promise,避免重复执行 fn()。
|
|
16
|
+
* Redis 后端下此映射是每个进程本地的;但因为 [cache-sf]key 在 Redis 里全局可见,
|
|
17
|
+
* 所以跨进程的 single-flight 仍然有效。
|
|
18
|
+
*/
|
|
19
|
+
const globalStaleInflight = new Map();
|
|
20
|
+
/**
|
|
21
|
+
* @internal [cache] 前缀下的子 key 命名全部收拢到这里,避免散落在各处 magic string。
|
|
22
|
+
*
|
|
23
|
+
* 命名规范:
|
|
24
|
+
* - [cache]key 缓存值(JSON: { v, d, s } — v=版本 d=数据 s=是否 stale)
|
|
25
|
+
* - [cache-sf]key single-flight 首飞标记(SETNX 抢锁用)
|
|
26
|
+
* - [cache-meta]key stale-allowed 标记(带 TTL,与缓存同生命周期)
|
|
27
|
+
* - [cache-parent]key / [cache-child]key 关联清除的 parent↔child 双向 set
|
|
28
|
+
*/
|
|
29
|
+
const CacheKey = {
|
|
30
|
+
value: (k) => `[cache]${k}`,
|
|
31
|
+
sf: (k) => `[cache-sf]${k}`,
|
|
32
|
+
meta: (k) => `[cache-meta]${k}`,
|
|
33
|
+
parent: (k) => `[cache-parent]${k}`,
|
|
34
|
+
child: (k) => `[cache-child]${k}`,
|
|
35
|
+
};
|
|
36
|
+
/** @internal 序列化缓存值 */
|
|
37
|
+
function serializeCacheValue(data, timestamp, stale = false, staleAllowed = false) {
|
|
38
|
+
const v = { t: timestamp, d: data };
|
|
39
|
+
if (stale)
|
|
40
|
+
v.s = 1;
|
|
41
|
+
if (staleAllowed)
|
|
42
|
+
v.sa = 1;
|
|
43
|
+
return JSON.stringify(v);
|
|
44
|
+
}
|
|
45
|
+
/** @internal 反序列化缓存值 */
|
|
46
|
+
function deserializeCacheValue(raw) {
|
|
47
|
+
try {
|
|
48
|
+
const v = JSON.parse(raw);
|
|
49
|
+
return { ...v, isNullish: v.d === 'null' || v.d === 'undefined' };
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// 旧格式兼容:纯字符串(无 JSON 包装)
|
|
53
|
+
return { t: 0, d: raw, isNullish: raw === 'null' || raw === 'undefined' };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* @internal 统一的 stale-allowed 注册表 key。
|
|
58
|
+
* 用带 TTL 的 string(而非全局 set),每个 stale-allowed 缓存独立管理自己的标记。
|
|
59
|
+
* key = [cache-meta]realKey,value = '1',TTL = 缓存 TTL。
|
|
60
|
+
* 好处:自动过期,不再需要全局 set,内存占用与缓存数量而非请求数量成正比。
|
|
61
|
+
*/
|
|
62
|
+
/**
|
|
63
|
+
* 缓存被清理时的行为策略。
|
|
64
|
+
* 由 @MethodCache 的 `staleMode` 配置决定,clearMethodCache 依此自动选择清理方式。
|
|
65
|
+
*/
|
|
66
|
+
export var CacheStaleMode;
|
|
67
|
+
(function (CacheStaleMode) {
|
|
68
|
+
/**
|
|
69
|
+
* 默认。清理时直接删除缓存值,下次请求触发 single-flight 重建。
|
|
70
|
+
* 优点:数据强一致,不会读到旧值。
|
|
71
|
+
* 缺点:重建期间所有请求被阻塞,直到 fn() 执行完毕。
|
|
72
|
+
*/
|
|
73
|
+
CacheStaleMode[CacheStaleMode["Purge"] = 0] = "Purge";
|
|
74
|
+
/**
|
|
75
|
+
* 清理时保留旧值并标记为过期(stale),同时设一个 stale 窗口(默认 30s)。
|
|
76
|
+
* 窗口内:请求仍读到旧值(零阻塞),同时后台 single-flight 异步刷新。
|
|
77
|
+
* 窗口过后:stale 标记自动消失,退化回 Purge 行为。
|
|
78
|
+
*
|
|
79
|
+
* 适用场景:读多写少、能容忍秒级旧数据、fn() 执行代价高。
|
|
80
|
+
* 注意:开启后业务层可能收到乱序数据,需自行处理(如按时间戳丢弃过期包)。
|
|
81
|
+
*/
|
|
82
|
+
CacheStaleMode[CacheStaleMode["StaleWhileRevalidate"] = 1] = "StaleWhileRevalidate";
|
|
83
|
+
})(CacheStaleMode || (CacheStaleMode = {}));
|
|
84
|
+
export function getRedisDB(db) {
|
|
85
|
+
const rd = globalThis[_dao][DBType.Redis][db ?? _primaryDB];
|
|
86
|
+
Throw.if(!rd, 'not found redis!');
|
|
87
|
+
return rd;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
redlock —— 用 redlock 做一把元锁([lockex]key),把"读 count、判断 count、incr"做成原子操作。
|
|
91
|
+
原实现失败时 `return await GetRedisLock(...)` 递归调用自己,redis 长时间不可用会栈溢出;
|
|
92
|
+
改成循环 + 指数退避 + 最大重试上限,达到上限后抛出(让上层决定是否走降级路径)。
|
|
93
|
+
*/
|
|
94
|
+
const GET_REDIS_LOCK_MAX_RETRIES = 10;
|
|
95
|
+
const GET_REDIS_LOCK_BASE_DELAY = 50; // 毫秒
|
|
96
|
+
export async function GetRedisLock(key, lockMaxActive) {
|
|
97
|
+
const lock = globalThis[_dao][DBType.RedisLock];
|
|
98
|
+
Throw.if(!lock, 'not found lock!');
|
|
99
|
+
const db = getRedisDB();
|
|
100
|
+
let lastError;
|
|
101
|
+
for (let attempt = 0; attempt < GET_REDIS_LOCK_MAX_RETRIES; attempt++) {
|
|
102
|
+
let initLock;
|
|
103
|
+
try {
|
|
104
|
+
initLock = await lock.acquire([`[lockex]${key}`], 5000);
|
|
105
|
+
const count = await db.get(key);
|
|
106
|
+
if (count === null || parseInt(count) < (lockMaxActive ?? 1)) {
|
|
107
|
+
await db.incr(key);
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
catch (er) {
|
|
115
|
+
lastError = er;
|
|
116
|
+
// 指数退避,避免 redis 抖动时打死服务
|
|
117
|
+
const delay = Math.min(GET_REDIS_LOCK_BASE_DELAY * Math.pow(2, attempt), 1000);
|
|
118
|
+
globalThis[_LoggerService].debugCategory?.('sql', `GetRedisLock ${key} attempt ${attempt + 1} failed: ${er?.message}, retry after ${delay}ms`);
|
|
119
|
+
await sleep(delay);
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
if (initLock) {
|
|
123
|
+
try {
|
|
124
|
+
await initLock.release();
|
|
125
|
+
// eslint-disable-next-line no-empty
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
throw new Error(`GetRedisLock ${key} failed after ${GET_REDIS_LOCK_MAX_RETRIES} retries: ${lastError?.message ?? lastError}`);
|
|
133
|
+
}
|
|
134
|
+
;
|
|
135
|
+
/** 对FN加锁、缓存执行 */
|
|
136
|
+
export async function excuteWithLock(config, fn__) {
|
|
137
|
+
const key = `[lock]${typeof config.key === 'function' ? config.key() : config.key}`;
|
|
138
|
+
const db = getRedisDB();
|
|
139
|
+
let wait_time = 0;
|
|
140
|
+
const fn = async () => {
|
|
141
|
+
const lock = await GetRedisLock(key, config.lockMaxActive);
|
|
142
|
+
if (lock === false) {
|
|
143
|
+
if (config.lockWait !== false && ((config.lockMaxWaitTime ?? 0) === 0 || (wait_time + (config.lockRetryInterval ?? 100)) <= (config.lockMaxWaitTime ?? 0))) {
|
|
144
|
+
globalThis[_LoggerService].debugCategory?.('sql', `get lock ${key} fail, retry after ${config.lockRetryInterval ?? 100}ms...`);
|
|
145
|
+
await sleep(config.lockRetryInterval ?? 100);
|
|
146
|
+
wait_time += (config.lockRetryInterval ?? 100);
|
|
147
|
+
return await fn();
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
globalThis[_LoggerService].debugCategory?.('sql', `get lock ${key} fail`);
|
|
151
|
+
throw new Error(config.errorMessage || `get lock fail: ${key}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
globalThis[_LoggerService].debugCategory?.('sql', `get lock ${key} ok!`);
|
|
156
|
+
await db.pexpire(key, config.lockMaxTime ?? 60000);
|
|
157
|
+
try {
|
|
158
|
+
return await fn__();
|
|
159
|
+
}
|
|
160
|
+
finally {
|
|
161
|
+
globalThis[_LoggerService].debugCategory?.('sql', `unlock ${key} ok!`);
|
|
162
|
+
await db.decr(key);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
return await fn();
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* 方法加锁装饰器。
|
|
170
|
+
* 与 `MethodCache` 组合时,**不要**手工叠装饰器——`MethodCache` 内部已经实现了
|
|
171
|
+
* "先查缓存 → miss 才加锁 → 锁内二次检查 → 仍 miss 才执行" 的 single-flight 语义,
|
|
172
|
+
* 直接给方法加 `@MethodCache` 即可。`@MethodLock` 单独使用时仅做并发互斥,不感知缓存。
|
|
173
|
+
*/
|
|
174
|
+
export function MethodLock(config) {
|
|
175
|
+
return function (target, _propertyKey, descriptor) {
|
|
176
|
+
const fn__ = descriptor.value;
|
|
177
|
+
descriptor.value = async function (...args) {
|
|
178
|
+
// 注意:装饰器闭包里的 `config` 是所有调用共享的同一个对象,
|
|
179
|
+
// 不能把 per-call 的状态写回去——必须每次构造一份新的 config 传给 excuteWithLock。
|
|
180
|
+
const resolvedKey = typeof config.key === 'function' ? config.key.call(this, ...args) : config.key;
|
|
181
|
+
const perCallConfig = { ...config, key: resolvedKey };
|
|
182
|
+
return await excuteWithLock(perCallConfig, async () => await fn__.call(this, ...args));
|
|
183
|
+
};
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* 内存 LRU 条目,存储格式与 Redis 一致(CacheValue)。
|
|
188
|
+
* `expiresAt = 0` 表示永不过期。
|
|
189
|
+
*/
|
|
190
|
+
/** 设置方法缓存 */
|
|
191
|
+
async function setMethodCache(config, devid) {
|
|
192
|
+
const db = getRedisDB();
|
|
193
|
+
const isNullish = config.result === null || config.result === undefined;
|
|
194
|
+
// 旧行为:null / undefined 不进缓存;新增 cacheNullValue=true 时写入 'null'(穿透防御)。
|
|
195
|
+
if (isNullish && !config.cacheNullValue) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
// 统一序列化:undefined 不是合法 JSON,归一化成 null。
|
|
199
|
+
const dataStr = isNullish ? 'null' : JSON.stringify(config.result);
|
|
200
|
+
// 负缓存通常用更短 TTL,避免业务恢复后还长时间返回旧的 null。
|
|
201
|
+
const ttl = isNullish
|
|
202
|
+
? (config.nullCacheTime ?? config.autoClearTime)
|
|
203
|
+
: config.autoClearTime;
|
|
204
|
+
// 映射关系存放(负缓存也参与关联清除,因为外部清缓存的语义不区分正负)
|
|
205
|
+
if (config.clearKey && config.clearKey.length > 0) {
|
|
206
|
+
for (const clear of config.clearKey) {
|
|
207
|
+
await db.sadd(CacheKey.parent(clear), config.key);
|
|
208
|
+
await db.sadd(CacheKey.child(config.key), clear);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
// 写入缓存值(带时间戳 + staleAllowed 标志)。
|
|
212
|
+
// 保留旧 sa:只有显式 StaleWhileRevalidate 才设 1,否则沿用旧值(防止覆盖已有的 sa=1)。
|
|
213
|
+
const timestamp = Date.now();
|
|
214
|
+
const existingSa = await db.get(CacheKey.value(config.key)).then(r => r ? deserializeCacheValue(r).sa : undefined);
|
|
215
|
+
const staleAllowed = config.staleMode === CacheStaleMode.StaleWhileRevalidate ? true : (existingSa ?? false);
|
|
216
|
+
const payload = serializeCacheValue(dataStr, timestamp, false, staleAllowed);
|
|
217
|
+
if (ttl) { // 自动清空
|
|
218
|
+
await db.set(CacheKey.value(config.key), payload, 'EX', ttl * 60);
|
|
219
|
+
cacheLog(`cache ${config.key} seted t${timestamp}${isNullish ? ' (null)' : ''}!`);
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
await db.set(CacheKey.value(config.key), payload);
|
|
223
|
+
cacheLog(`cache ${config.key} seted t${timestamp}${isNullish ? ' (null)' : ''}!`);
|
|
224
|
+
}
|
|
225
|
+
// staleAllowed 已存储在缓存值内部(CacheValue.sa),无需单独的 meta key
|
|
226
|
+
// 删除旧格式残留(兼容升级)
|
|
227
|
+
await db.del(CacheKey.meta(config.key));
|
|
228
|
+
// 订阅:清空 clear list —— 同一 key 在缓存生命周期内只注册一次监听器,
|
|
229
|
+
// 否则每次 miss 都会 .on 一次,clearMethodCache 触发时回调会被执行 N 遍,
|
|
230
|
+
// 长期运行造成监听器泄漏。
|
|
231
|
+
if (config.clearKey && config.clearKey.length > 0) {
|
|
232
|
+
const event = CacheKey.value(config.key);
|
|
233
|
+
if (globalThis[_EventBus].listenerCount(event) === 0) {
|
|
234
|
+
globalThis[_EventBus].on(event, async (key) => {
|
|
235
|
+
await clearCacheKey(key, 30);
|
|
236
|
+
cacheLog(`cache ${key} clear by key!`);
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (devid) {
|
|
241
|
+
// 订阅:清空 clear list —— 同样去重,避免每次 miss 都重复注册。
|
|
242
|
+
const event = `user-${devid}`;
|
|
243
|
+
if (globalThis[_EventBus].listenerCount(event) === 0) {
|
|
244
|
+
globalThis[_EventBus].on(event, async function (key) {
|
|
245
|
+
await clearCacheKey(key, 30);
|
|
246
|
+
cacheLog(`cache ${key} clear by devid!`);
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* 单个缓存 key 的 stale-or-purge 决策与执行。
|
|
253
|
+
* - CacheValue.sa=1 → 标记 s=1(保留缓存值,StaleWhileRevalidate)
|
|
254
|
+
* - 否则 → 直接删缓存值(Purge)
|
|
255
|
+
*
|
|
256
|
+
* 无论哪种路径,parent↔child 关联关系都清理。
|
|
257
|
+
*
|
|
258
|
+
* @returns 受影响的 children 列表(parent 路径会返回所有被级联清理的 child key)
|
|
259
|
+
*/
|
|
260
|
+
async function clearCacheKey(key, staleTimeoutSec) {
|
|
261
|
+
const db = getRedisDB();
|
|
262
|
+
const affectedChildren = [];
|
|
263
|
+
// 1. 先处理 parent 级联:把关联的 children 全部清掉
|
|
264
|
+
const parentType = await db.type(CacheKey.parent(key));
|
|
265
|
+
if (parentType === 'set') {
|
|
266
|
+
const childKeys = await db.smembers(CacheKey.parent(key));
|
|
267
|
+
for (const child of childKeys) {
|
|
268
|
+
cacheLog(`cache ${child} cleared via parent ${key}!`);
|
|
269
|
+
await clearCacheKey(child, staleTimeoutSec);
|
|
270
|
+
affectedChildren.push(child);
|
|
271
|
+
}
|
|
272
|
+
await db.del(CacheKey.parent(key));
|
|
273
|
+
}
|
|
274
|
+
// 2. 判断 stale-allowed:查缓存值内部的 sa 标志
|
|
275
|
+
const valueRaw = await db.get(CacheKey.value(key));
|
|
276
|
+
const v = valueRaw ? deserializeCacheValue(valueRaw) : null;
|
|
277
|
+
if (v?.sa) {
|
|
278
|
+
// stale-allowed:保留缓存值,标记 s=1,同时保留 sa=1(防止下次 clear 变成 purge)
|
|
279
|
+
const newPayload = serializeCacheValue(v.d, v.t, true, true);
|
|
280
|
+
const ttlSec = await db.ttl(CacheKey.value(key));
|
|
281
|
+
if (ttlSec > 0) {
|
|
282
|
+
await db.set(CacheKey.value(key), newPayload, 'EX', ttlSec);
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
await db.set(CacheKey.value(key), newPayload);
|
|
286
|
+
}
|
|
287
|
+
cacheLog(`cache ${key} marked stale (t${v.t})`);
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
// Purge:直接删缓存值
|
|
291
|
+
await db.del(CacheKey.value(key));
|
|
292
|
+
cacheLog(`cache ${key} purged!`);
|
|
293
|
+
}
|
|
294
|
+
// 清理旧格式残留
|
|
295
|
+
await db.del(CacheKey.meta(key));
|
|
296
|
+
// 3. 清理 child→parent 反向关联
|
|
297
|
+
const childType = await db.type(CacheKey.child(key));
|
|
298
|
+
if (childType === 'set') {
|
|
299
|
+
const parentKeys = await db.smembers(CacheKey.child(key));
|
|
300
|
+
for (const pk of parentKeys) {
|
|
301
|
+
const t = await db.type(CacheKey.parent(pk));
|
|
302
|
+
if (t === 'set') {
|
|
303
|
+
await db.srem(CacheKey.parent(pk), key);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
await db.del(CacheKey.child(key));
|
|
307
|
+
}
|
|
308
|
+
return affectedChildren;
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* 清空方法缓存。
|
|
312
|
+
* - redis 侧只在配置了 redis 时才操作;没配 redis 时安静跳过,不抛错。
|
|
313
|
+
* - 关联清除(clearKey)生效。
|
|
314
|
+
* - 被清理的 key 如果启用了 StaleWhileRevalidate(CacheValue.sa=1),会保留旧值并标记 stale。
|
|
315
|
+
*
|
|
316
|
+
* @param key 要清理的缓存 key 或 parent key
|
|
317
|
+
* @param staleTimeoutSec stale 窗口秒数(仅对 StaleWhileRevalidate 的 key 生效)。默认 30。
|
|
318
|
+
*/
|
|
319
|
+
export async function clearMethodCache(key, staleTimeoutSec = 30) {
|
|
320
|
+
const redisDao = globalThis[_dao]?.[DBType.Redis]?.[_primaryDB];
|
|
321
|
+
if (!redisDao)
|
|
322
|
+
return;
|
|
323
|
+
await clearCacheKey(key, staleTimeoutSec);
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Redis 缓存执行:先查缓存 → miss 后单飞抢锁执行 → 其它等待者只盯缓存、不抢锁。
|
|
327
|
+
*
|
|
328
|
+
* 关键点(区别于"锁内二次检查"的朴素实现):等待者并不进入 redlock 竞争队列,
|
|
329
|
+
* 只用轻量 SETNX 选出"首飞",其它人在 `wait_for_cache` 循环里轮询缓存。
|
|
330
|
+
* 这样首飞一旦写完缓存,所有等待者下一次 sleep 醒来就能并行命中,
|
|
331
|
+
* 而不是排着队一个个 acquire/release 锁——避免串行化。
|
|
332
|
+
*
|
|
333
|
+
* 命中判断用 `cached !== null`(ioredis 在 key 不存在时返回 `null`),
|
|
334
|
+
* 这样 `cacheNullValue=true` 时存进去的字面量 `'null'` 也会算作命中,达到防穿透效果。
|
|
335
|
+
*
|
|
336
|
+
* 命中时若发现 stale 标记([cache-stale]key 存在),说明该值是"已被声明过期但保留的旧值",
|
|
337
|
+
* 此时仍返回旧值(零阻塞),同时触发后台 single-flight 异步刷新。
|
|
338
|
+
* 这样 clearMethodCache 后的首次读取不会被阻塞,后续读取将命中刷新后的新值。
|
|
339
|
+
*
|
|
340
|
+
* redis 不可用或 SETNX 异常时退化为无锁直跑,保留可用性。
|
|
341
|
+
*/
|
|
342
|
+
const SINGLEFLIGHT_LOCK_TTL_MS = 30000; // 首飞标记 TTL,兜底首飞挂掉的情况
|
|
343
|
+
const SINGLEFLIGHT_POLL_INTERVAL_MS = 50; // 等待者轮询缓存的间隔
|
|
344
|
+
const SINGLEFLIGHT_MAX_WAIT_MS = 30000; // 等待者最长等多久;超时就自己跑 fn(保底,不应该常发生)
|
|
345
|
+
/**
|
|
346
|
+
* 后台异步刷新:fire-and-forget,不阻塞调用方。
|
|
347
|
+
* 只有抢到首飞标记的实例才会执行 fn() 并写缓存;其它实例直接放弃。
|
|
348
|
+
* 刷新完成后删除 stale 标记,后续读取将命中新值。
|
|
349
|
+
* 如果配置了 onCacheUpdated 回调,调用时传入 ctx(方法的 this 上下文)。
|
|
350
|
+
*/
|
|
351
|
+
async function staleBackgroundRefresh(opts, setOpts) {
|
|
352
|
+
const db = getRedisDB();
|
|
353
|
+
const sfKey = CacheKey.sf(opts.key);
|
|
354
|
+
try {
|
|
355
|
+
const ok = await db.set(sfKey, '1', 'PX', SINGLEFLIGHT_LOCK_TTL_MS, 'NX');
|
|
356
|
+
if (ok !== 'OK')
|
|
357
|
+
return; // 别人已经在刷新了,自己放弃
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
return; // redis 出错,放弃刷新,下次读取再试
|
|
361
|
+
}
|
|
362
|
+
try {
|
|
363
|
+
const result = await opts.fn();
|
|
364
|
+
// 写新值(setMethodCache 内部会清除 stale 标志 s=1)
|
|
365
|
+
await setMethodCache({ key: opts.key, ...setOpts, result }, opts.devid);
|
|
366
|
+
// 调用缓存更新回调(带 ctx)
|
|
367
|
+
void opts.onCacheUpdated?.call(opts.ctx, result, ...(opts.args ?? []));
|
|
368
|
+
cacheLog(`cache ${opts.key} stale background refresh done!`);
|
|
369
|
+
}
|
|
370
|
+
catch (error) {
|
|
371
|
+
globalThis[_LoggerService].warn?.(`cache ${opts.key} stale background refresh failed: ${error?.message}`);
|
|
372
|
+
}
|
|
373
|
+
finally {
|
|
374
|
+
try {
|
|
375
|
+
await db.del(sfKey);
|
|
376
|
+
}
|
|
377
|
+
catch { /* TTL 兜底 */ }
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
async function redisCacheCore(opts) {
|
|
381
|
+
const db = getRedisDB();
|
|
382
|
+
const cacheKey = CacheKey.value(opts.key);
|
|
383
|
+
const raw = await db.get(cacheKey);
|
|
384
|
+
if (raw !== null) {
|
|
385
|
+
// 一次读取拿到全部信息(值 + stale 状态)
|
|
386
|
+
const v = deserializeCacheValue(raw);
|
|
387
|
+
const isStale = v.s === 1;
|
|
388
|
+
if (isStale) {
|
|
389
|
+
// stale 窗口内:返回旧值 + 触发后台刷新(不阻塞)
|
|
390
|
+
cacheLog(`cache ${opts.key} hit (stale t${v.t})!`);
|
|
391
|
+
const setOpts = {
|
|
392
|
+
clearKey: opts.clearKey,
|
|
393
|
+
autoClearTime: opts.autoClearTime,
|
|
394
|
+
cacheNullValue: opts.cacheNullValue,
|
|
395
|
+
nullCacheTime: opts.nullCacheTime,
|
|
396
|
+
staleMode: opts.staleMode
|
|
397
|
+
};
|
|
398
|
+
if (!globalStaleInflight.has(opts.key)) {
|
|
399
|
+
const refreshP = staleBackgroundRefresh(opts, setOpts).finally(() => {
|
|
400
|
+
globalStaleInflight.delete(opts.key);
|
|
401
|
+
});
|
|
402
|
+
globalStaleInflight.set(opts.key, refreshP);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
else {
|
|
406
|
+
cacheLog(`cache ${opts.key} hit (t${v.t})!`);
|
|
407
|
+
}
|
|
408
|
+
return JSON.parse(v.d);
|
|
409
|
+
}
|
|
410
|
+
cacheLog(`cache ${opts.key} miss!`);
|
|
411
|
+
const setOpts = {
|
|
412
|
+
clearKey: opts.clearKey,
|
|
413
|
+
autoClearTime: opts.autoClearTime,
|
|
414
|
+
cacheNullValue: opts.cacheNullValue,
|
|
415
|
+
nullCacheTime: opts.nullCacheTime,
|
|
416
|
+
staleMode: opts.staleMode
|
|
417
|
+
};
|
|
418
|
+
// 用一个独立的"首飞标记" key 选出谁去执行 fn——SET NX PX 是原子操作,
|
|
419
|
+
// 同一时刻只有一个调用者拿到 true,其它返回 null。这把"标记"不重入也不计数,
|
|
420
|
+
// 只是个 single-flight 指示器,和 redlock / GetRedisLock 的并发计数器不冲突。
|
|
421
|
+
const sfKey = CacheKey.sf(opts.key);
|
|
422
|
+
let isLeader = false;
|
|
423
|
+
try {
|
|
424
|
+
const ok = await db.set(sfKey, '1', 'PX', SINGLEFLIGHT_LOCK_TTL_MS, 'NX');
|
|
425
|
+
isLeader = ok === 'OK';
|
|
426
|
+
}
|
|
427
|
+
catch (error) {
|
|
428
|
+
// redis 出错:退化为无锁直跑,保留可用性
|
|
429
|
+
globalThis[_LoggerService].warn?.(`single-flight setnx ${opts.key} failed: ${error?.message}, fallback to no-lock`);
|
|
430
|
+
const result = await opts.fn();
|
|
431
|
+
await setMethodCache({ key: opts.key, ...setOpts, result }, opts.devid);
|
|
432
|
+
void opts.onCacheUpdated?.call(opts.ctx, result, ...(opts.args ?? []));
|
|
433
|
+
return result;
|
|
434
|
+
}
|
|
435
|
+
if (isLeader) {
|
|
436
|
+
// 首飞:跑 fn、写缓存、释放首飞标记
|
|
437
|
+
try {
|
|
438
|
+
const result = await opts.fn();
|
|
439
|
+
await setMethodCache({ key: opts.key, ...setOpts, result }, opts.devid);
|
|
440
|
+
void opts.onCacheUpdated?.call(opts.ctx, result, ...(opts.args ?? []));
|
|
441
|
+
return result;
|
|
442
|
+
}
|
|
443
|
+
finally {
|
|
444
|
+
try {
|
|
445
|
+
await db.del(sfKey);
|
|
446
|
+
}
|
|
447
|
+
catch {
|
|
448
|
+
// 标记 TTL 会兜底,删失败不影响正确性
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
// 等待者:循环只看缓存,不抢锁
|
|
453
|
+
const waitStart = Date.now();
|
|
454
|
+
while (true) {
|
|
455
|
+
await sleep(SINGLEFLIGHT_POLL_INTERVAL_MS);
|
|
456
|
+
const recheck = await db.get(cacheKey);
|
|
457
|
+
if (recheck !== null) {
|
|
458
|
+
cacheLog(`cache ${opts.key} hit after wait!`);
|
|
459
|
+
return JSON.parse(deserializeCacheValue(recheck).d);
|
|
460
|
+
}
|
|
461
|
+
// 首飞挂了(TTL 到期,sf 标记自动消失)但缓存仍然空——抢做下一任首飞
|
|
462
|
+
const leaderGone = await db.get(sfKey);
|
|
463
|
+
if (leaderGone === null) {
|
|
464
|
+
const taken = await db.set(sfKey, '1', 'PX', SINGLEFLIGHT_LOCK_TTL_MS, 'NX');
|
|
465
|
+
if (taken === 'OK') {
|
|
466
|
+
cacheLog(`cache ${opts.key}: previous leader gone, taking over`);
|
|
467
|
+
try {
|
|
468
|
+
const result = await opts.fn();
|
|
469
|
+
await setMethodCache({ key: opts.key, ...setOpts, result }, opts.devid);
|
|
470
|
+
void opts.onCacheUpdated?.call(opts.ctx, result, ...(opts.args ?? []));
|
|
471
|
+
return result;
|
|
472
|
+
}
|
|
473
|
+
finally {
|
|
474
|
+
try {
|
|
475
|
+
await db.del(sfKey);
|
|
476
|
+
}
|
|
477
|
+
catch { /* TTL 兜底 */ }
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
// 没抢到说明又有别人接手了,继续等
|
|
481
|
+
}
|
|
482
|
+
// 等待上限保底:极端情况下首飞和接班都不正常时,自己直接跑,宁可重复执行也不挂死
|
|
483
|
+
if (Date.now() - waitStart > SINGLEFLIGHT_MAX_WAIT_MS) {
|
|
484
|
+
globalThis[_LoggerService].warn?.(`cache ${opts.key}: wait timed out, running fn without lock`);
|
|
485
|
+
const result = await opts.fn();
|
|
486
|
+
await setMethodCache({ key: opts.key, ...setOpts, result }, opts.devid);
|
|
487
|
+
void opts.onCacheUpdated?.call(opts.ctx, result, ...(opts.args ?? []));
|
|
488
|
+
return result;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
/** @internal 唯一的执行入口(Memory 后端已移除) */
|
|
493
|
+
async function excuteCacheCore(opts) {
|
|
494
|
+
return await redisCacheCore(opts);
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* 执行一个方法fn,
|
|
498
|
+
* 如果有缓存,则返回缓存,否则【加锁、二次检查】后执行方法并缓存,防止缓存击穿。
|
|
499
|
+
* 可选 `cacheNullValue=true` 开启负缓存,防穿透;负缓存默认沿用 autoClearTime,可单独用 nullCacheTime 设短。
|
|
500
|
+
* 可选 `staleMode: CacheStaleMode.StaleWhileRevalidate` 开启异步重新验证(清理时保留旧值,零阻塞)。
|
|
501
|
+
*/
|
|
502
|
+
export async function excuteWithCache(config, fn) {
|
|
503
|
+
const key = typeof config.key === 'function' ? config.key() : config.key;
|
|
504
|
+
return await excuteCacheCore({
|
|
505
|
+
key,
|
|
506
|
+
clearKey: config.clearKey,
|
|
507
|
+
autoClearTime: config.autoClearTime,
|
|
508
|
+
cacheNullValue: config.cacheNullValue,
|
|
509
|
+
nullCacheTime: config.nullCacheTime,
|
|
510
|
+
staleMode: config.staleMode,
|
|
511
|
+
onCacheUpdated: config.onCacheUpdated,
|
|
512
|
+
ctx: config.ctx,
|
|
513
|
+
args: config.args ?? [],
|
|
514
|
+
fn
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* 缓存注解:先查缓存 → miss 才加锁 → 锁内再查一次 → 仍 miss 才执行原方法。
|
|
519
|
+
* 已内置 single-flight,不需要再叠 `@MethodLock`。
|
|
520
|
+
* 可选 `cacheNullValue=true` 开启负缓存,防穿透。
|
|
521
|
+
* 可选 `storage: 'memory'` 用进程内 LRU 替代 redis(单进程场景,毫秒级延迟)。
|
|
522
|
+
* 可选 `staleMode: CacheStaleMode.StaleWhileRevalidate` 开启异步重新验证(清理时保留旧值,零阻塞)。
|
|
523
|
+
* 可选 `onCacheUpdated` 设置缓存更新回调(带 this 上下文)。
|
|
524
|
+
*/
|
|
525
|
+
export function MethodCache(config) {
|
|
526
|
+
return function (_target, _propertyKey, descriptor) {
|
|
527
|
+
const fn = descriptor.value;
|
|
528
|
+
descriptor.value = async function (...args) {
|
|
529
|
+
const key = typeof config.key === 'function' ? config.key.call(this, ...args) : config.key;
|
|
530
|
+
const clearKey = config.clearKey
|
|
531
|
+
? (typeof config.clearKey === 'function' ? config.clearKey.call(this, ...args) : config.clearKey)
|
|
532
|
+
: undefined;
|
|
533
|
+
const devid = config.clearWithSession && this.ctx && this.ctx.me && this.ctx.me.devid;
|
|
534
|
+
return await excuteCacheCore({
|
|
535
|
+
key,
|
|
536
|
+
clearKey,
|
|
537
|
+
autoClearTime: config.autoClearTime,
|
|
538
|
+
cacheNullValue: config.cacheNullValue,
|
|
539
|
+
nullCacheTime: config.nullCacheTime,
|
|
540
|
+
staleMode: config.staleMode,
|
|
541
|
+
onCacheUpdated: config.onCacheUpdated,
|
|
542
|
+
ctx: this, // 传递当前 this 上下文
|
|
543
|
+
args, // 传递方法输入参数
|
|
544
|
+
devid,
|
|
545
|
+
fn: async () => await fn.call(this, ...args)
|
|
546
|
+
});
|
|
547
|
+
};
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* 外部直接设置缓存值。用于绕过 @MethodCache 装饰器直接更新缓存的场景
|
|
552
|
+
* (如外部事件驱动的数据变更、手动刷新等)。
|
|
553
|
+
*
|
|
554
|
+
* 内部使用统一格式 { v, d, s } 序列化,确保与 @MethodCache 的读取逻辑兼容。
|
|
555
|
+
*
|
|
556
|
+
* @param cacheKey 缓存 key(不含 [cache] 前缀,与 @MethodCache 的 key 一致)
|
|
557
|
+
* @param value 要缓存的值
|
|
558
|
+
* @param options 选项
|
|
559
|
+
*
|
|
560
|
+
* @example
|
|
561
|
+
* ```ts
|
|
562
|
+
* // 外部事件驱动更新缓存
|
|
563
|
+
* await setCache('match-timer-123', timer, { autoClearTime: 120 });
|
|
564
|
+
*
|
|
565
|
+
* // 同时触发 onCacheUpdated 回调(如果有)
|
|
566
|
+
* await setCache('match-timer-123', timer, {
|
|
567
|
+
* autoClearTime: 120,
|
|
568
|
+
* onCacheUpdated(this: MyService, value) {
|
|
569
|
+
* this.broadcast(value);
|
|
570
|
+
* }
|
|
571
|
+
* });
|
|
572
|
+
* ```
|
|
573
|
+
*/
|
|
574
|
+
export async function setCache(cacheKey, value, options = {}) {
|
|
575
|
+
const { autoClearTime, cacheNullValue = false, onCacheUpdated, ctx, args = [] } = options;
|
|
576
|
+
const isNullish = value === null || value === undefined;
|
|
577
|
+
if (isNullish && !cacheNullValue) {
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
const dataStr = isNullish ? 'null' : JSON.stringify(value);
|
|
581
|
+
const timestamp = Date.now();
|
|
582
|
+
const payload = serializeCacheValue(dataStr, timestamp);
|
|
583
|
+
const db = getRedisDB();
|
|
584
|
+
if (autoClearTime) {
|
|
585
|
+
await db.set(CacheKey.value(cacheKey), payload, 'EX', autoClearTime * 60);
|
|
586
|
+
}
|
|
587
|
+
else {
|
|
588
|
+
await db.set(CacheKey.value(cacheKey), payload);
|
|
589
|
+
}
|
|
590
|
+
// 触发回调
|
|
591
|
+
void onCacheUpdated?.call(ctx, value, ...args);
|
|
592
|
+
cacheLog(`cache ${cacheKey} externally set (t${timestamp})`);
|
|
593
|
+
}
|
package/const/index.d.ts
ADDED
package/const/index.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 全局 Symbol 注册表,跨文件共享。
|
|
3
|
+
*
|
|
4
|
+
* 这些 Symbol 大多用作 `globalThis[symbol]` 的键(保存 dao 实例、配置、缓存等运行时状态),
|
|
5
|
+
* 或者用作类的私有字段键(如 `_inTransaction` / `_daoConnection`)。
|
|
6
|
+
*
|
|
7
|
+
* 标 `@internal` 的项是包内部约定,外部不应当读写——但因为跨文件共享必须 export,
|
|
8
|
+
* 在 TS 里没法严格强制访问范围;保留 `_` 前缀作为风格提示。
|
|
9
|
+
*/
|
|
10
|
+
/** @internal 实体的表名 */
|
|
11
|
+
export declare const _tableName: unique symbol;
|
|
12
|
+
/** @internal 实体绑定的 dbName(多数据源时用) */
|
|
13
|
+
export declare const _daoDBName: unique symbol;
|
|
14
|
+
/** @internal 实体类名(小驼峰) */
|
|
15
|
+
export declare const _className: unique symbol;
|
|
16
|
+
/** @internal 实体类名(大驼峰) */
|
|
17
|
+
export declare const _ClassName: unique symbol;
|
|
18
|
+
/** @internal 实体类名(短横线) */
|
|
19
|
+
export declare const _vueName: unique symbol;
|
|
20
|
+
/** @internal 实体->数据转换器,由 @DB 注入 */
|
|
21
|
+
export declare const _transformer: unique symbol;
|
|
22
|
+
/** @internal 表注释 */
|
|
23
|
+
export declare const _comment: unique symbol;
|
|
24
|
+
/** @internal SqlCache 实例,挂在 globalThis */
|
|
25
|
+
export declare const _sqlCache: unique symbol;
|
|
26
|
+
/** @internal DAO 注册表 {dbType: {dbName: Dao}},挂在 globalThis */
|
|
27
|
+
export declare const _dao: unique symbol;
|
|
28
|
+
/** 主数据源(无名)的占位 key —— 注意:这是字符串,不是 Symbol。 */
|
|
29
|
+
export declare const _primaryDB = "______primaryDB_______";
|
|
30
|
+
/** @internal 实体绑定的 dbType */
|
|
31
|
+
export declare const _dbType: unique symbol;
|
|
32
|
+
/** @internal sqlite 表版本号(用于自动迁移) */
|
|
33
|
+
export declare const _sqlite_version: unique symbol;
|
|
34
|
+
/** @internal Connection 内部持有的真实驱动连接对象 */
|
|
35
|
+
export declare const _daoConnection: unique symbol;
|
|
36
|
+
/** @internal Connection 是否在事务中 */
|
|
37
|
+
export declare const _inTransaction: unique symbol;
|
|
38
|
+
/** @internal Dao 内部持有的真实驱动 pool/db 对象 */
|
|
39
|
+
export declare const _daoDB: unique symbol;
|
|
40
|
+
/** @internal SqliteRemote 的远端 db 名 */
|
|
41
|
+
export declare const _sqliteRemoteName: unique symbol;
|
|
42
|
+
/** @internal @DB 注解写入的 ServiceOption */
|
|
43
|
+
export declare const _SqlOption: unique symbol;
|
|
44
|
+
/** 数据转换函数注册表(boot 时传入),挂在 globalThis */
|
|
45
|
+
export declare const _DataConvert: unique symbol;
|
|
46
|
+
/** 模板渲染上下文(boot 时传入),挂在 globalThis */
|
|
47
|
+
export declare const _Context: unique symbol;
|
|
48
|
+
/** MySQL keepalive 间隔(毫秒),挂在 globalThis */
|
|
49
|
+
export declare const _MysqlKeepAliveTime: unique symbol;
|
|
50
|
+
/** @internal XML resultMap 注册表 */
|
|
51
|
+
export declare const _resultMap: unique symbol;
|
|
52
|
+
/** @internal sqlId -> resultMap 映射 */
|
|
53
|
+
export declare const _resultMap_SQLID: unique symbol;
|
|
54
|
+
/** 枚举映射(boot 时传入),挂在 globalThis */
|
|
55
|
+
export declare const _enum: unique symbol;
|
|
56
|
+
/** 全局配置(boot 时聚合),挂在 globalThis */
|
|
57
|
+
export declare const _GlobalSqlOption: unique symbol;
|
|
58
|
+
/** EventBus,挂在 globalThis */
|
|
59
|
+
export declare const _EventBus: unique symbol;
|
|
60
|
+
/** LoggerService 实例,挂在 globalThis */
|
|
61
|
+
export declare const _LoggerService: unique symbol;
|
|
62
|
+
/** 内存缓存 single-flight 表(key → in-flight Promise),挂在 globalThis */
|
|
63
|
+
export declare const _memInflight: unique symbol;
|
|
64
|
+
/** 延迟加载的 node:path 模块,挂在 globalThis */
|
|
65
|
+
export declare const _path: unique symbol;
|
|
66
|
+
/** 延迟加载的 node:fs 模块,挂在 globalThis */
|
|
67
|
+
export declare const _fs: unique symbol;
|
|
68
|
+
/** MySQL/Postgres keepalive 默认间隔(30 秒) */
|
|
69
|
+
export declare const DEFAULT_KEEPALIVE_INTERVAL = 30000;
|
|
70
|
+
/** 默认批量处理数量 */
|
|
71
|
+
export declare const DEFAULT_MAX_DEAL = 500;
|