diff --git a/src/valdi_modules/src/valdi/persistence/web/PersistentStoreNative.ts b/src/valdi_modules/src/valdi/persistence/web/PersistentStoreNative.ts index 15f0258b..5179b9e2 100644 --- a/src/valdi_modules/src/valdi/persistence/web/PersistentStoreNative.ts +++ b/src/valdi_modules/src/valdi/persistence/web/PersistentStoreNative.ts @@ -13,7 +13,7 @@ type Entry = { expiresAt?: number; }; -const storeMap = new Map(); +const memoryStores = new Map>(); let nowOverrideSec: number | undefined; const nowSec = () => (nowOverrideSec ?? Math.floor(Date.now() / 1000)); @@ -21,115 +21,148 @@ const cloneBuf = (b: ArrayBuffer) => b.slice(0); const toBuf = (s: string) => enc.encode(s).buffer; const toStr = (b: ArrayBuffer) => dec.decode(new Uint8Array(b)); +const storageKey = (_storeName: string, key: string) => key; + const isExpired = (e: Entry) => e.expiresAt !== undefined && nowSec() >= e.expiresAt; -const sweepIfExpired = (k: string) => { - const e = storeMap.get(k); - if (e && isExpired(e)) storeMap.delete(k); -}; -/* ---------- implementation ---------- */ +const getMemoryStore = (storeName: string): Map => { + let store = memoryStores.get(storeName); + if (!store) { + store = new Map(); + memoryStores.set(storeName, store); + } + return store; +}; -const impl: PersistentStoreNative = { - store(key, value, ttlSeconds, weight, completion) { +const createStore = (storeName: string): PersistentStoreNative => ({ + store( + key: string, + value: ArrayBuffer | string, + ttlSeconds: number | undefined, + weight: number | undefined, + completion: (error?: string) => void, + ) { microtask(() => { try { const entry: Entry = { - value: typeof value === "string" ? value : cloneBuf(value), + value: typeof value === 'string' ? value : cloneBuf(value), weight, expiresAt: ttlSeconds === undefined ? undefined : ttlSeconds <= 0 ? nowSec() : nowSec() + Math.floor(ttlSeconds), }; - storeMap.set(key, entry); - completion(); // success - } catch (e: any) { - completion(String(e?.message ?? e ?? "store failed")); + getMemoryStore(storeName).set(storageKey(storeName, key), entry); + completion(); + } catch (e: unknown) { + completion(String(e instanceof Error ? e.message : e ?? 'store failed')); } }); }, - fetch(key, completion, asString) { + fetch( + key: string, + completion: (value?: ArrayBuffer | string, error?: string) => void, + asString: boolean, + ) { microtask(() => { - sweepIfExpired(key); - const e = storeMap.get(key); + const fullKey = storageKey(storeName, key); + const store = getMemoryStore(storeName); + sweepIfExpired(store, fullKey); + const e = store.get(fullKey); if (!e) { - completion(undefined, "not found"); + completion(undefined, 'not found'); return; } let v: ArrayBuffer | string = e.value; - - // honor asString flag by converting if needed if (asString && v instanceof ArrayBuffer) { v = toStr(v); - } else if (!asString && typeof v === "string") { + } else if (!asString && typeof v === 'string') { v = toBuf(v); } - completion(v, undefined); }); }, - fetchAll(completion) { + fetchAll(completion: (value: PropertyList) => void) { microtask(() => { - // collect keys first (no for..of, no Array.from) - const keys: string[] = []; - storeMap.forEach((_e, k) => keys.push(k)); - for (let i = 0; i < keys.length; i++) { - sweepIfExpired(keys[i]); - } - const out: PropertyList = {}; - // safe iteration without for..of - storeMap.forEach((e, k) => { - out[k] = e.value; + const store = getMemoryStore(storeName); + store.forEach((e, k) => { + if (!isExpired(e)) { + out[k] = e.value; + } }); completion(out); }); }, - // inside setCurrentTime(...), replace the sweep loop with: - setCurrentTime(timeSeconds) { + setCurrentTime(timeSeconds: number) { nowOverrideSec = Math.floor(timeSeconds); - const keys: string[] = []; - storeMap.forEach((_e, k) => keys.push(k)); - for (let i = 0; i < keys.length; i++) { - sweepIfExpired(keys[i]); - } + memoryStores.forEach(store => { + const keys: string[] = []; + store.forEach((_e, k) => keys.push(k)); + for (let i = 0; i < keys.length; i++) { + sweepIfExpired(store, keys[i]); + } + }); }, - exists(key, completion) { + exists(key: string, completion: (exists: boolean) => void) { microtask(() => { - sweepIfExpired(key); - completion(storeMap.has(key)); + const fullKey = storageKey(storeName, key); + const store = getMemoryStore(storeName); + sweepIfExpired(store, fullKey); + completion(store.has(fullKey)); }); }, - remove(key, completion) { + remove(key: string, completion: (error?: string) => void) { microtask(() => { try { - storeMap.delete(key); + getMemoryStore(storeName).delete(storageKey(storeName, key)); completion(); - } catch (e: any) { - completion(String(e?.message ?? e ?? "remove failed")); + } catch (e: unknown) { + completion(String(e instanceof Error ? e.message : e ?? 'remove failed')); } }); }, - removeAll(completion) { + removeAll(completion: (error?: string) => void) { microtask(() => { try { - storeMap.clear(); + memoryStores.delete(storeName); completion(); - } catch (e: any) { - completion(String(e?.message ?? e ?? "removeAll failed")); + } catch (e: unknown) { + completion(String(e instanceof Error ? e.message : e ?? 'removeAll failed')); } }); }, - -}; +}); + +function sweepIfExpired(store: Map, key: string): void { + const e = store.get(key); + if (e && isExpired(e)) { + store.delete(key); + } +} + +export function newPersistentStore( + name: string, + _disableBatchWrites: boolean, + _userScoped: boolean, + _maxWeight: number, + mockedTime: number | undefined, + _mockedUserId: string | undefined, + _enableEncryption: boolean | undefined, +): PersistentStoreNative { + if (mockedTime !== undefined) { + nowOverrideSec = Math.floor(mockedTime); + } + return createStore(name); +} /** Exported instance + setter so you can swap in a real native binding later. */ -export let persistentStoreNative: PersistentStoreNative = impl; +export let persistentStoreNative: PersistentStoreNative = createStore('default'); export function setPersistentStoreNative(newImpl: PersistentStoreNative): void { persistentStoreNative = newImpl; -} \ No newline at end of file +}