mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2025-12-18 20:30:08 +08:00
Refactor Store to use per-key timers for expiration
Simplifies the Store implementation by removing batch expiration scanning and using per-key setTimeout timers for key expiration. This change improves code clarity and ensures more precise key expiration handling.
This commit is contained in:
parent
06f6a542f5
commit
f4dedf4803
@ -1,190 +1,22 @@
|
||||
export type StoreValueType = string | number | boolean | object | null;
|
||||
|
||||
export type StoreValue<T extends StoreValueType = StoreValueType> = {
|
||||
value: T;
|
||||
expiresAt?: number;
|
||||
};
|
||||
|
||||
class Store {
|
||||
// 使用Map存储键值对
|
||||
private store: Map<string, StoreValue>;
|
||||
// 定时清理器
|
||||
private cleanerTimer: NodeJS.Timeout;
|
||||
// 用于分批次扫描的游标
|
||||
private scanCursor: number = 0;
|
||||
private store = new Map<string, any>();
|
||||
|
||||
/**
|
||||
* Store
|
||||
* @param cleanInterval 清理间隔
|
||||
* @param scanLimit 扫描限制(每次最多检查的键数)
|
||||
*/
|
||||
constructor (
|
||||
cleanInterval: number = 1000, // 默认1秒执行一次
|
||||
private scanLimit: number = 100 // 每次最多检查100个键
|
||||
) {
|
||||
this.store = new Map();
|
||||
this.cleanerTimer = setInterval(() => this.cleanupExpired(), cleanInterval);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置键值对
|
||||
* @param key 键
|
||||
* @param value 值
|
||||
* @param ttl 过期时间
|
||||
* @returns void
|
||||
* @example store.set('key', 'value', 60)
|
||||
*/
|
||||
set<T extends StoreValueType>(key: string, value: T, ttl?: number): void {
|
||||
if (ttl && ttl <= 0) {
|
||||
this.del(key);
|
||||
return;
|
||||
}
|
||||
const expiresAt = ttl ? Date.now() + ttl * 1000 : undefined;
|
||||
this.store.set(key, { value, expiresAt });
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期键
|
||||
*/
|
||||
private cleanupExpired (): void {
|
||||
const now = Date.now();
|
||||
const keys = Array.from(this.store.keys());
|
||||
let scanned = 0;
|
||||
|
||||
// 分批次扫描
|
||||
while (scanned < this.scanLimit && this.scanCursor < keys.length) {
|
||||
const key = keys[this.scanCursor++];
|
||||
const entry = this.store.get(key!)!;
|
||||
|
||||
if (entry.expiresAt && entry.expiresAt < now) {
|
||||
this.store.delete(key!);
|
||||
}
|
||||
|
||||
scanned++;
|
||||
}
|
||||
|
||||
// 重置游标(环形扫描)
|
||||
if (this.scanCursor >= keys.length) {
|
||||
this.scanCursor = 0;
|
||||
set<T> (key: string, value: T, ttl?: number): void {
|
||||
this.store.set(key, value);
|
||||
if (ttl) {
|
||||
setTimeout(() => this.store.delete(key), ttl * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取键值
|
||||
* @param key 键
|
||||
* @returns T | null
|
||||
* @example store.get('key')
|
||||
*/
|
||||
get<T extends StoreValueType>(key: string): T | null {
|
||||
this.checkKeyExpiry(key); // 每次访问都检查
|
||||
const entry = this.store.get(key);
|
||||
return entry ? (entry.value as T) : null;
|
||||
get<T> (key: string): T | null {
|
||||
return this.store.get(key) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查键是否过期
|
||||
* @param key 键
|
||||
*/
|
||||
private checkKeyExpiry (key: string): void {
|
||||
const entry = this.store.get(key);
|
||||
if (entry?.expiresAt && entry.expiresAt < Date.now()) {
|
||||
this.store.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查键是否存在
|
||||
* @param keys 键
|
||||
* @returns number
|
||||
* @example store.exists('key1', 'key2')
|
||||
*/
|
||||
exists (...keys: string[]): number {
|
||||
return keys.filter((key) => {
|
||||
this.checkKeyExpiry(key);
|
||||
return this.store.has(key);
|
||||
}).length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭存储器
|
||||
*/
|
||||
shutdown (): void {
|
||||
clearInterval(this.cleanerTimer);
|
||||
this.store.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除键
|
||||
* @param keys 键
|
||||
* @returns number
|
||||
* @example store.del('key1', 'key2')
|
||||
*/
|
||||
del (...keys: string[]): number {
|
||||
return keys.reduce((count, key) => (this.store.delete(key) ? count + 1 : count), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置键的过期时间
|
||||
* @param key 键
|
||||
* @param seconds 过期时间(秒)
|
||||
* @returns boolean
|
||||
* @example store.expire('key', 60)
|
||||
*/
|
||||
expire (key: string, seconds: number): boolean {
|
||||
const entry = this.store.get(key);
|
||||
if (!entry) return false;
|
||||
|
||||
entry.expiresAt = Date.now() + seconds * 1000;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取键的过期时间
|
||||
* @param key 键
|
||||
* @returns number | null
|
||||
* @example store.ttl('key')
|
||||
*/
|
||||
ttl (key: string): number | null {
|
||||
const entry = this.store.get(key);
|
||||
if (!entry) return null;
|
||||
|
||||
if (!entry.expiresAt) return -1;
|
||||
const remaining = entry.expiresAt - Date.now();
|
||||
return remaining > 0 ? Math.floor(remaining / 1000) : -2;
|
||||
}
|
||||
|
||||
/**
|
||||
* 键值数字递增
|
||||
* @param key 键
|
||||
* @returns number
|
||||
* @example store.incr('key')
|
||||
*/
|
||||
incr (key: string): number {
|
||||
const current = this.get<StoreValueType>(key);
|
||||
|
||||
if (current === null) {
|
||||
this.set(key, 1, 60);
|
||||
return 1;
|
||||
}
|
||||
|
||||
let numericValue: number;
|
||||
if (typeof current === 'number') {
|
||||
numericValue = current;
|
||||
} else if (typeof current === 'string') {
|
||||
if (!/^-?\d+$/.test(current)) {
|
||||
throw new Error('ERR value is not an integer');
|
||||
}
|
||||
numericValue = parseInt(current, 10);
|
||||
} else {
|
||||
throw new Error('ERR value is not an integer');
|
||||
}
|
||||
|
||||
const newValue = numericValue + 1;
|
||||
this.set(key, newValue, 60);
|
||||
return newValue;
|
||||
return keys.filter(key => this.store.has(key)).length;
|
||||
}
|
||||
}
|
||||
|
||||
const store = new Store();
|
||||
|
||||
export default store;
|
||||
export default store;
|
||||
@ -57,7 +57,7 @@ export const WebUiDataRuntime = {
|
||||
return false;
|
||||
}
|
||||
|
||||
store.incr(key);
|
||||
store.set(key, count + 1);
|
||||
return true;
|
||||
},
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user