File size: 4,343 Bytes
7a1ad33 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 | /**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
export interface CacheEntry<V> {
value: V;
timestamp: number;
ttl?: number;
}
export interface CacheOptions {
/**
* Default Time To Live in milliseconds.
*/
defaultTtl?: number;
/**
* If true, and V is a Promise, the entry will be removed from the cache
* if the promise rejects.
*/
deleteOnPromiseFailure?: boolean;
/**
* The underlying storage mechanism.
* Use 'weakmap' (default) for object keys to allow garbage collection.
* Use 'map' if you need to use strings as keys or need the clear() method.
*/
storage?: 'map' | 'weakmap';
}
/**
* A generic caching service with TTL support.
*/
export class CacheService<K extends object | string | undefined, V> {
private readonly storage:
| Map<K, CacheEntry<V>>
| WeakMap<WeakKey, CacheEntry<V>>;
private readonly defaultTtl?: number;
private readonly deleteOnPromiseFailure: boolean;
constructor(options: CacheOptions = {}) {
// Default to map for safety unless weakmap is explicitly requested.
this.storage =
options.storage === 'weakmap'
? new WeakMap<WeakKey, CacheEntry<V>>()
: new Map<K, CacheEntry<V>>();
this.defaultTtl = options.defaultTtl;
this.deleteOnPromiseFailure = options.deleteOnPromiseFailure ?? true;
}
/**
* Retrieves a value from the cache. Returns undefined if missing or expired.
*/
get(key: K): V | undefined {
// We have to cast to Map or WeakMap specifically to call get()
// but since they have the same signature for object keys, we can
// safely cast to 'any' internally for the dispatch.
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-type-assertion
const entry = (this.storage as any).get(key) as CacheEntry<V> | undefined;
if (!entry) {
return undefined;
}
const ttl = entry.ttl ?? this.defaultTtl;
if (ttl !== undefined && Date.now() - entry.timestamp > ttl) {
this.delete(key);
return undefined;
}
return entry.value;
}
/**
* Stores a value in the cache.
*/
set(key: K, value: V, ttl?: number): void {
const entry: CacheEntry<V> = {
value,
timestamp: Date.now(),
ttl,
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-type-assertion
(this.storage as any).set(key, entry);
if (this.deleteOnPromiseFailure && value instanceof Promise) {
value.catch(() => {
// Only delete if this exact entry is still in the cache
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-type-assertion
if ((this.storage as any).get(key) === entry) {
this.delete(key);
}
});
}
}
/**
* Helper to retrieve a value or create it if missing/expired.
*/
getOrCreate(key: K, creator: () => V, ttl?: number): V {
let value = this.get(key);
if (value === undefined) {
value = creator();
this.set(key, value, ttl);
}
return value;
}
/**
* Removes an entry from the cache.
*/
delete(key: K): void {
if (this.storage instanceof Map) {
this.storage.delete(key);
} else {
// WeakMap.delete returns a boolean, we can ignore it.
// Cast to any to bypass the WeakKey constraint since we've already
// confirmed the storage type.
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-type-assertion
(this.storage as any).delete(key);
}
}
/**
* Clears all entries. Only supported if using Map storage.
*/
clear(): void {
if (this.storage instanceof Map) {
this.storage.clear();
} else {
throw new Error('clear() is not supported on WeakMap storage');
}
}
}
/**
* Factory function to create a new cache.
*/
export function createCache<K extends string | undefined, V>(
options: CacheOptions & { storage: 'map' },
): CacheService<K, V>;
export function createCache<K extends object, V>(
options?: CacheOptions,
): CacheService<K, V>;
export function createCache<K extends object | string | undefined, V>(
options: CacheOptions = {},
): CacheService<K, V> {
return new CacheService<K, V>(options);
}
|