import { createCache } from 'async-cache-dedupe';
const DB_NAME = 'nucleus-cache';
const STORE_NAME = 'keyvalue';
const DB_VERSION = 1;
type WriteOp =
| {
type: 'put';
key: string;
value: { value: unknown; expires: number };
resolve: () => void;
reject: (err: unknown) => void;
}
| { type: 'delete'; key: string; resolve: () => void; reject: (err: unknown) => void };
type ReadOp = {
key: string;
resolve: (val: unknown) => void;
reject: (err: unknown) => void;
};
class IDBStorage {
private dbPromise: Promise<IDBDatabase> | null = null;
private getBatch: ReadOp[] = [];
private writeBatch: WriteOp[] = [];
private getFlushScheduled = false;
private writeFlushScheduled = false;
constructor() {
if (typeof indexedDB === 'undefined') return;
this.dbPromise = new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, DB_VERSION);
request.onerror = () => {
console.error('IDB open error:', request.error);
reject(request.error);
};
request.onsuccess = () => resolve(request.result);
request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result;
if (!db.objectStoreNames.contains(STORE_NAME)) db.createObjectStore(STORE_NAME);
};
});
}
async get(key: string): Promise<unknown> {
// checking in-flight writes
for (let i = this.writeBatch.length - 1; i >= 0; i--) {
const op = this.writeBatch[i];
if (op.key === key) {
if (op.type === 'delete') return undefined;
if (op.type === 'put') {
// if expired we dont want it
if (op.value.expires < Date.now()) return undefined;
return op.value.value;
}
}
}
if (!this.dbPromise) return undefined;
return new Promise((resolve, reject) => {
this.getBatch.push({ key, resolve, reject });
this.scheduleGetFlush();
});
}
private scheduleGetFlush() {
if (this.getFlushScheduled) return;
this.getFlushScheduled = true;
queueMicrotask(() => this.flushGetBatch());
}
private async flushGetBatch() {
this.getFlushScheduled = false;
const batch = this.getBatch;
this.getBatch = [];
if (batch.length === 0) return;
try {
const db = await this.dbPromise;
if (!db) throw new Error('DB not available');
const transaction = db.transaction(STORE_NAME, 'readonly');
const store = transaction.objectStore(STORE_NAME);
batch.forEach(({ key, resolve }) => {
try {
const request = store.get(key);
request.onsuccess = () => {
const result = request.result;
if (!result) {
resolve(undefined);
return;
}
if (result.expires < Date.now()) {
// Fire-and-forget removal for expired items
this.remove(key).catch(() => {});
resolve(undefined);
return;
}
resolve(result.value);
};
request.onerror = () => resolve(undefined);
} catch {
resolve(undefined);
}
});
} catch (error) {
batch.forEach(({ reject }) => reject(error));
}
}
async set(key: string, value: unknown, ttl: number): Promise<void> {
if (!this.dbPromise) return;
const expires = Date.now() + ttl * 1000;
const storageValue = { value, expires };
return new Promise((resolve, reject) => {
this.writeBatch.push({ type: 'put', key, value: storageValue, resolve, reject });
this.scheduleWriteFlush();
});
}
async remove(key: string): Promise<void> {
if (!this.dbPromise) return;
return new Promise((resolve, reject) => {
this.writeBatch.push({ type: 'delete', key, resolve, reject });
this.scheduleWriteFlush();
});
}
private scheduleWriteFlush() {
if (this.writeFlushScheduled) return;
this.writeFlushScheduled = true;
queueMicrotask(() => this.flushWriteBatch());
}
private async flushWriteBatch() {
this.writeFlushScheduled = false;
const batch = this.writeBatch;
this.writeBatch = [];
if (batch.length === 0) return;
try {
const db = await this.dbPromise;
if (!db) throw new Error('DB not available');
const transaction = db.transaction(STORE_NAME, 'readwrite');
const store = transaction.objectStore(STORE_NAME);
batch.forEach((op) => {
try {
let request: IDBRequest;
if (op.type === 'put') request = store.put(op.value, op.key);
else request = store.delete(op.key);
request.onsuccess = () => op.resolve();
request.onerror = () => op.reject(request.error);
} catch (err) {
op.reject(err);
}
});
} catch (error) {
batch.forEach(({ reject }) => reject(error));
}
}
async clear(): Promise<void> {
if (!this.dbPromise) return;
try {
const db = await this.dbPromise;
return new Promise<void>((resolve, reject) => {
const transaction = db.transaction(STORE_NAME, 'readwrite');
const store = transaction.objectStore(STORE_NAME);
const request = store.clear();
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve();
});
} catch (e) {
console.error('IDB clear error', e);
}
}
async exists(key: string): Promise<boolean> {
return (await this.get(key)) !== undefined;
}
async invalidate(key: string): Promise<void> {
return this.remove(key);
}
// noops
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async getTTL(key: string): Promise<void> {
return;
}
async refresh(): Promise<void> {
return;
}
}
export const ttl = 60 * 60 * 3; // 3 hours
export const cache = createCache({
storage: {
type: 'custom',
options: {
storage: new IDBStorage()
}
},
ttl,
onError: (err) => console.error(err)
});