1import { Cache, type CacheOptions } from '@wora/cache-persist';
2import { LRUCache } from 'lru-cache';
3
4export interface PersistedLRUOptions {
5 prefix?: string;
6 max: number;
7 ttl?: number;
8 persistOptions?: CacheOptions;
9}
10
11// eslint-disable-next-line @typescript-eslint/no-empty-object-type
12export class PersistedLRU<K extends string, V extends {}> {
13 private memory: LRUCache<K, V>;
14 private storage: Cache;
15 private signals: Map<K, ((data: V) => void)[]>;
16
17 private prefix = '';
18
19 constructor(opts: PersistedLRUOptions) {
20 this.memory = new LRUCache<K, V>({
21 max: opts.max,
22 ttl: opts.ttl
23 });
24 this.storage = new Cache(opts.persistOptions);
25 this.prefix = opts.prefix ? `${opts.prefix}%` : '';
26 this.signals = new Map();
27
28 this.init();
29 }
30
31 async init(): Promise<void> {
32 await this.storage.restore();
33
34 const state = this.storage.getState();
35 for (const [key, val] of Object.entries(state)) {
36 try {
37 // console.log('restoring', key);
38 const k = this.unprefix(key) as unknown as K;
39 const v = val as V;
40 this.memory.set(k, v);
41 } catch (err) {
42 console.warn('skipping invalid persisted entry', key, err);
43 }
44 }
45 }
46
47 get(key: K): V | undefined {
48 return this.memory.get(key);
49 }
50 getSignal(key: K): Promise<V> {
51 return new Promise<V>((resolve) => {
52 if (!this.signals.has(key)) {
53 this.signals.set(key, [resolve]);
54 return;
55 }
56 const signals = this.signals.get(key)!;
57 signals.push(resolve);
58 this.signals.set(key, signals);
59 });
60 }
61 set(key: K, value: V): void {
62 this.memory.set(key, value);
63 this.storage.set(this.prefixed(key), value);
64 const signals = this.signals.get(key);
65 let signal = signals?.pop();
66 while (signal) {
67 signal(value);
68 signal = signals?.pop();
69 }
70 this.storage.flush(); // TODO: uh evil and fucked up (this whole file is evil honestly)
71 }
72 has(key: K): boolean {
73 return this.memory.has(key);
74 }
75 delete(key: K): void {
76 this.memory.delete(key);
77 this.storage.delete(this.prefixed(key));
78 this.storage.flush();
79 }
80 clear(): void {
81 this.memory.clear();
82 this.storage.purge();
83 this.storage.flush();
84 }
85
86 private prefixed(key: K): string {
87 return this.prefix + key;
88 }
89 private unprefix(prefixed: string): string {
90 return prefixed.slice(this.prefix.length);
91 }
92}