Files
iistwin/server/utils/cache.ts
Ильяс Султанов 1f5ecb6da4 fix(number-fields): избегаем потери точности длинных чисел
- number-поля теперь рендерятся как text + inputMode=numeric,
  чтобы браузер не округлял значения через input type=number
- пробелы при вставке в number-поля удаляются
- бэкенд нормализует значения number-полей в строку перед сохранением
- добавлен хелпер normalizeFieldValueForStorage

Closes: искажение расчётного счёта и других длинных числовых полей
2026-07-07 21:03:40 +03:00

76 lines
2.0 KiB
TypeScript

interface CacheEntry<T> {
data: T;
expiresAt: number;
}
export class TtlCache {
private store = new Map<string, CacheEntry<unknown>>();
private defaultTtlMs: number;
private maxSize: number;
constructor(ttlSeconds = 60, maxSize = 1000) {
this.defaultTtlMs = ttlSeconds * 1000;
this.maxSize = maxSize;
}
get<T>(key: string): T | undefined {
const entry = this.store.get(key) as CacheEntry<T> | undefined;
if (!entry) return undefined;
if (Date.now() > entry.expiresAt) {
this.store.delete(key);
return undefined;
}
// LRU: move to end on access
this.store.delete(key);
this.store.set(key, entry);
return entry.data;
}
set<T>(key: string, data: T, ttlMs?: number): void {
const effectiveTtl = ttlMs ?? this.defaultTtlMs;
// If key already exists, delete first to refresh insertion order
if (this.store.has(key)) {
this.store.delete(key);
} else if (this.store.size >= this.maxSize) {
// Evict oldest entry (first in Map insertion order)
const oldestKey = this.store.keys().next().value;
if (oldestKey !== undefined) {
this.store.delete(oldestKey);
}
}
this.store.set(key, { data, expiresAt: Date.now() + effectiveTtl });
}
/** Delete all keys that start with the given prefix */
invalidate(prefix: string): void {
for (const key of Array.from(this.store.keys())) {
if (key.startsWith(prefix)) {
this.store.delete(key);
}
}
}
/** Alias for invalidate — delete all keys starting with prefix */
invalidatePrefix(prefix: string): void {
this.invalidate(prefix);
}
/** Remove a single key */
delete(key: string): void {
this.store.delete(key);
}
/** Completely clear the cache */
clear(): void {
this.store.clear();
}
/** Current number of entries (including possibly expired ones) */
get size(): number {
return this.store.size;
}
}
export const formsCache = new TtlCache(60);
export const tasksMinimalCache = new TtlCache(60);