Refactor Store to use per-key timers for expiration
Some checks are pending
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run

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:
手瓜一十雪 2025-11-03 17:06:44 +08:00
parent 06f6a542f5
commit f4dedf4803
2 changed files with 10 additions and 178 deletions

View File

@ -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;

View File

@ -57,7 +57,7 @@ export const WebUiDataRuntime = {
return false;
}
store.incr(key);
store.set(key, count + 1);
return true;
},