char-fork/llama.cpp

git clone https://git.t4t.associates/char-fork/llama.cpp

Aleksander Grygierui: Stores split refactor (#27240)521a64cd0

char/aliceai-t5-moe
5.9 KiB311 linesraw
1import { CACHE } from '$lib/constants';
2
3/**
4 * TTL Cache - Time-To-Live cache implementation for memory optimization
5 *
6 * Provides automatic expiration of cached entries to prevent memory bloat
7 * in long-running sessions.
8 *
9 * @example
10 * ```ts
11 * const cache = new TTLCache<string, ApiData>({ ttlMs: 5 * 60 * 1000 }); // 5 minutes
12 * cache.set('key', data);
13 * const value = cache.get('key'); // null if expired
14 * ```
15 */
16
17export interface TTLCacheOptions {
18	/** Time-to-live in milliseconds. Default: 5 minutes */
19	ttlMs?: number;
20	/** Maximum number of entries. Oldest entries are evicted when exceeded. Default: 100 */
21	maxEntries?: number;
22	/** Callback when an entry expires or is evicted */
23	onEvict?: (key: string, value: unknown) => void;
24}
25
26interface CacheEntry<T> {
27	value: T;
28	expiresAt: number;
29	lastAccessed: number;
30}
31
32export class TTLCache<K extends string, V> {
33	private cache = new Map<K, CacheEntry<V>>();
34	private readonly maxEntries: number;
35	private readonly onEvict?: (key: string, value: unknown) => void;
36	private readonly ttlMs: number;
37
38	/**
39	 * Get the number of entries (including potentially expired ones).
40	 */
41	get size(): number {
42		return this.cache.size;
43	}
44
45	/**
46	 * Clear all entries from cache.
47	 */
48	clear(): void {
49		if (this.onEvict) {
50			for (const [key, entry] of this.cache) {
51				this.onEvict(key, entry.value);
52			}
53		}
54
55		this.cache.clear();
56	}
57
58	constructor(options: TTLCacheOptions = {}) {
59		this.ttlMs = options.ttlMs ?? CACHE.DEFAULT_TTL_MS;
60		this.maxEntries = options.maxEntries ?? CACHE.DEFAULT_MAX_ENTRIES;
61		this.onEvict = options.onEvict;
62	}
63
64	/**
65	 * Delete a specific key from cache.
66	 */
67	delete(key: K): boolean {
68		const entry = this.cache.get(key);
69
70		if (entry && this.onEvict) {
71			this.onEvict(key, entry.value);
72		}
73
74		return this.cache.delete(key);
75	}
76
77	/**
78	 * Get a value from cache. Returns null if expired or not found.
79	 */
80	get(key: K): V | null {
81		const entry = this.cache.get(key);
82
83		if (!entry) return null;
84
85		if (Date.now() > entry.expiresAt) {
86			this.delete(key);
87
88			return null;
89		}
90
91		// Update last accessed time for LRU-like behavior
92		entry.lastAccessed = Date.now();
93
94		return entry.value;
95	}
96
97	/**
98	 * Check if key exists and is not expired.
99	 */
100	has(key: K): boolean {
101		const entry = this.cache.get(key);
102
103		if (!entry) return false;
104
105		if (Date.now() > entry.expiresAt) {
106			this.delete(key);
107
108			return false;
109		}
110
111		return true;
112	}
113
114	/**
115	 * Get all valid (non-expired) keys.
116	 */
117	keys(): K[] {
118		const now = Date.now();
119		const validKeys: K[] = [];
120
121		for (const [key, entry] of this.cache) {
122			if (now <= entry.expiresAt) {
123				validKeys.push(key);
124			}
125		}
126
127		return validKeys;
128	}
129
130	/**
131	 * Remove all expired entries from cache.
132	 * Call periodically for proactive cleanup.
133	 */
134	prune(): number {
135		const now = Date.now();
136
137		let pruned = 0;
138
139		for (const [key, entry] of this.cache) {
140			if (now > entry.expiresAt) {
141				this.delete(key);
142				pruned++;
143			}
144		}
145
146		return pruned;
147	}
148
149	/**
150	 * Set a value in cache with TTL.
151	 */
152	set(key: K, value: V, customTtlMs?: number): void {
153		// Evict oldest entries if at capacity
154		if (this.cache.size >= this.maxEntries && !this.cache.has(key)) {
155			this.evictOldest();
156		}
157
158		const ttl = customTtlMs ?? this.ttlMs;
159		const now = Date.now();
160
161		this.cache.set(key, {
162			expiresAt: now + ttl,
163			lastAccessed: now,
164			value
165		});
166	}
167
168	/**
169	 * Refresh TTL for an existing entry without changing the value.
170	 */
171	touch(key: K): boolean {
172		const entry = this.cache.get(key);
173
174		if (!entry) return false;
175
176		const now = Date.now();
177
178		if (now > entry.expiresAt) {
179			this.delete(key);
180
181			return false;
182		}
183
184		entry.expiresAt = now + this.ttlMs;
185		entry.lastAccessed = now;
186
187		return true;
188	}
189
190	/**
191	 * Evict the oldest (least recently accessed) entry.
192	 */
193	private evictOldest(): void {
194		let oldestKey: K | null = null;
195		let oldestTime = Infinity;
196
197		for (const [key, entry] of this.cache) {
198			if (entry.lastAccessed < oldestTime) {
199				oldestTime = entry.lastAccessed;
200				oldestKey = key;
201			}
202		}
203
204		if (oldestKey !== null) {
205			this.delete(oldestKey);
206		}
207	}
208}
209
210/**
211 * Reactive TTL Map for Svelte stores
212 * Wraps SvelteMap with TTL functionality
213 */
214export class ReactiveTTLMap<K extends string, V> {
215	private entries = $state<Map<K, CacheEntry<V>>>(new Map());
216	private readonly maxEntries: number;
217	private readonly ttlMs: number;
218
219	get size(): number {
220		return this.entries.size;
221	}
222
223	clear(): void {
224		this.entries.clear();
225	}
226
227	constructor(options: TTLCacheOptions = {}) {
228		this.ttlMs = options.ttlMs ?? CACHE.DEFAULT_TTL_MS;
229		this.maxEntries = options.maxEntries ?? CACHE.DEFAULT_MAX_ENTRIES;
230	}
231
232	delete(key: K): boolean {
233		return this.entries.delete(key);
234	}
235
236	get(key: K): V | null {
237		const entry = this.entries.get(key);
238
239		if (!entry) return null;
240
241		if (Date.now() > entry.expiresAt) {
242			this.entries.delete(key);
243
244			return null;
245		}
246
247		entry.lastAccessed = Date.now();
248
249		return entry.value;
250	}
251
252	has(key: K): boolean {
253		const entry = this.entries.get(key);
254
255		if (!entry) return false;
256
257		if (Date.now() > entry.expiresAt) {
258			this.entries.delete(key);
259
260			return false;
261		}
262
263		return true;
264	}
265
266	prune(): number {
267		const now = Date.now();
268
269		let pruned = 0;
270
271		for (const [key, entry] of this.entries) {
272			if (now > entry.expiresAt) {
273				this.entries.delete(key);
274				pruned++;
275			}
276		}
277
278		return pruned;
279	}
280
281	set(key: K, value: V, customTtlMs?: number): void {
282		if (this.entries.size >= this.maxEntries && !this.entries.has(key)) {
283			this.evictOldest();
284		}
285
286		const ttl = customTtlMs ?? this.ttlMs;
287		const now = Date.now();
288
289		this.entries.set(key, {
290			expiresAt: now + ttl,
291			lastAccessed: now,
292			value
293		});
294	}
295
296	private evictOldest(): void {
297		let oldestKey: K | null = null;
298		let oldestTime = Infinity;
299
300		for (const [key, entry] of this.entries) {
301			if (entry.lastAccessed < oldestTime) {
302				oldestTime = entry.lastAccessed;
303				oldestKey = key;
304			}
305		}
306
307		if (oldestKey !== null) {
308			this.entries.delete(oldestKey);
309		}
310	}
311}