| | import { DEFAULT_CACHE_TTL_MS, DEFAULT_CACHE_MAX_ENTRIES } from '$lib/constants/cache';
|
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| |
|
| | export interface TTLCacheOptions {
|
| |
|
| | ttlMs?: number;
|
| |
|
| | maxEntries?: number;
|
| |
|
| | onEvict?: (key: string, value: unknown) => void;
|
| | }
|
| |
|
| | interface CacheEntry<T> {
|
| | value: T;
|
| | expiresAt: number;
|
| | lastAccessed: number;
|
| | }
|
| |
|
| | export class TTLCache<K extends string, V> {
|
| | private cache = new Map<K, CacheEntry<V>>();
|
| | private readonly ttlMs: number;
|
| | private readonly maxEntries: number;
|
| | private readonly onEvict?: (key: string, value: unknown) => void;
|
| |
|
| | constructor(options: TTLCacheOptions = {}) {
|
| | this.ttlMs = options.ttlMs ?? DEFAULT_CACHE_TTL_MS;
|
| | this.maxEntries = options.maxEntries ?? DEFAULT_CACHE_MAX_ENTRIES;
|
| | this.onEvict = options.onEvict;
|
| | }
|
| |
|
| | |
| | |
| |
|
| | get(key: K): V | null {
|
| | const entry = this.cache.get(key);
|
| | if (!entry) return null;
|
| |
|
| | if (Date.now() > entry.expiresAt) {
|
| | this.delete(key);
|
| | return null;
|
| | }
|
| |
|
| |
|
| | entry.lastAccessed = Date.now();
|
| | return entry.value;
|
| | }
|
| |
|
| | |
| | |
| |
|
| | set(key: K, value: V, customTtlMs?: number): void {
|
| |
|
| | if (this.cache.size >= this.maxEntries && !this.cache.has(key)) {
|
| | this.evictOldest();
|
| | }
|
| |
|
| | const ttl = customTtlMs ?? this.ttlMs;
|
| | const now = Date.now();
|
| |
|
| | this.cache.set(key, {
|
| | value,
|
| | expiresAt: now + ttl,
|
| | lastAccessed: now
|
| | });
|
| | }
|
| |
|
| | |
| | |
| |
|
| | has(key: K): boolean {
|
| | const entry = this.cache.get(key);
|
| | if (!entry) return false;
|
| |
|
| | if (Date.now() > entry.expiresAt) {
|
| | this.delete(key);
|
| | return false;
|
| | }
|
| |
|
| | return true;
|
| | }
|
| |
|
| | |
| | |
| |
|
| | delete(key: K): boolean {
|
| | const entry = this.cache.get(key);
|
| | if (entry && this.onEvict) {
|
| | this.onEvict(key, entry.value);
|
| | }
|
| | return this.cache.delete(key);
|
| | }
|
| |
|
| | |
| | |
| |
|
| | clear(): void {
|
| | if (this.onEvict) {
|
| | for (const [key, entry] of this.cache) {
|
| | this.onEvict(key, entry.value);
|
| | }
|
| | }
|
| | this.cache.clear();
|
| | }
|
| |
|
| | |
| | |
| |
|
| | get size(): number {
|
| | return this.cache.size;
|
| | }
|
| |
|
| | |
| | |
| | |
| |
|
| | prune(): number {
|
| | const now = Date.now();
|
| | let pruned = 0;
|
| |
|
| | for (const [key, entry] of this.cache) {
|
| | if (now > entry.expiresAt) {
|
| | this.delete(key);
|
| | pruned++;
|
| | }
|
| | }
|
| |
|
| | return pruned;
|
| | }
|
| |
|
| | |
| | |
| |
|
| | keys(): K[] {
|
| | const now = Date.now();
|
| | const validKeys: K[] = [];
|
| |
|
| | for (const [key, entry] of this.cache) {
|
| | if (now <= entry.expiresAt) {
|
| | validKeys.push(key);
|
| | }
|
| | }
|
| |
|
| | return validKeys;
|
| | }
|
| |
|
| | |
| | |
| |
|
| | private evictOldest(): void {
|
| | let oldestKey: K | null = null;
|
| | let oldestTime = Infinity;
|
| |
|
| | for (const [key, entry] of this.cache) {
|
| | if (entry.lastAccessed < oldestTime) {
|
| | oldestTime = entry.lastAccessed;
|
| | oldestKey = key;
|
| | }
|
| | }
|
| |
|
| | if (oldestKey !== null) {
|
| | this.delete(oldestKey);
|
| | }
|
| | }
|
| |
|
| | |
| | |
| |
|
| | touch(key: K): boolean {
|
| | const entry = this.cache.get(key);
|
| | if (!entry) return false;
|
| |
|
| | const now = Date.now();
|
| | if (now > entry.expiresAt) {
|
| | this.delete(key);
|
| | return false;
|
| | }
|
| |
|
| | entry.expiresAt = now + this.ttlMs;
|
| | entry.lastAccessed = now;
|
| | return true;
|
| | }
|
| | }
|
| |
|
| | |
| | |
| | |
| |
|
| | export class ReactiveTTLMap<K extends string, V> {
|
| | private entries = $state<Map<K, CacheEntry<V>>>(new Map());
|
| | private readonly ttlMs: number;
|
| | private readonly maxEntries: number;
|
| |
|
| | constructor(options: TTLCacheOptions = {}) {
|
| | this.ttlMs = options.ttlMs ?? DEFAULT_CACHE_TTL_MS;
|
| | this.maxEntries = options.maxEntries ?? DEFAULT_CACHE_MAX_ENTRIES;
|
| | }
|
| |
|
| | get(key: K): V | null {
|
| | const entry = this.entries.get(key);
|
| | if (!entry) return null;
|
| |
|
| | if (Date.now() > entry.expiresAt) {
|
| | this.entries.delete(key);
|
| | return null;
|
| | }
|
| |
|
| | entry.lastAccessed = Date.now();
|
| | return entry.value;
|
| | }
|
| |
|
| | set(key: K, value: V, customTtlMs?: number): void {
|
| | if (this.entries.size >= this.maxEntries && !this.entries.has(key)) {
|
| | this.evictOldest();
|
| | }
|
| |
|
| | const ttl = customTtlMs ?? this.ttlMs;
|
| | const now = Date.now();
|
| |
|
| | this.entries.set(key, {
|
| | value,
|
| | expiresAt: now + ttl,
|
| | lastAccessed: now
|
| | });
|
| | }
|
| |
|
| | has(key: K): boolean {
|
| | const entry = this.entries.get(key);
|
| | if (!entry) return false;
|
| |
|
| | if (Date.now() > entry.expiresAt) {
|
| | this.entries.delete(key);
|
| | return false;
|
| | }
|
| |
|
| | return true;
|
| | }
|
| |
|
| | delete(key: K): boolean {
|
| | return this.entries.delete(key);
|
| | }
|
| |
|
| | clear(): void {
|
| | this.entries.clear();
|
| | }
|
| |
|
| | get size(): number {
|
| | return this.entries.size;
|
| | }
|
| |
|
| | prune(): number {
|
| | const now = Date.now();
|
| | let pruned = 0;
|
| |
|
| | for (const [key, entry] of this.entries) {
|
| | if (now > entry.expiresAt) {
|
| | this.entries.delete(key);
|
| | pruned++;
|
| | }
|
| | }
|
| |
|
| | return pruned;
|
| | }
|
| |
|
| | private evictOldest(): void {
|
| | let oldestKey: K | null = null;
|
| | let oldestTime = Infinity;
|
| |
|
| | for (const [key, entry] of this.entries) {
|
| | if (entry.lastAccessed < oldestTime) {
|
| | oldestTime = entry.lastAccessed;
|
| | oldestKey = key;
|
| | }
|
| | }
|
| |
|
| | if (oldestKey !== null) {
|
| | this.entries.delete(oldestKey);
|
| | }
|
| | }
|
| | }
|
| |
|