import { watch, type Ref, type WatchHandle } from "vue"; import { debounce } from "./debounce"; export interface MultitabValue { value: T; expires?: string; updated: string; } const listening: Record void> = {}; const lastUpdate: Record = {}; const unwatchList: Record = {}; let started = false; function startStorageListening() { window.addEventListener("storage", function (e) { for (const key in listening) { if (e.key && e.key === key) { listening[key](); } } }); } export function makeMultitab( refs: { [key: string]: Ref }, opts: { prefix?: string; expireAfter?: number; defaultValue?: T; callback?: (key: string) => void; } = {}, ) { if (!started) { started = true; startStorageListening(); } const { prefix = "multitab:", expireAfter = 1000 * 60 * 60, defaultValue, } = opts; Object.keys(refs).forEach((key) => { const raw = key; if (prefix) { key = prefix + key; } if (listening[key]) { throw new Error(`Multitab key is already in use: ${key}`); } // check if the value can be restored from storage const existing = loadMultitab(key); if (existing) { refs[raw].value = existing; } if (!existing && defaultValue) { refs[raw].value = defaultValue; saveMultitab(key, defaultValue, expireAfter); } listening[key] = async () => { if (await debounce(`multitab:update:${key}`)) { refs[raw].value = loadMultitab(key) ?? defaultValue; } }; unwatchList[key] = watch( refs[raw], async () => { if ( willChangeKey(key, refs[raw].value) && (await debounce(`multitab:save:${key}`)) ) { saveMultitab(key, refs[raw].value, expireAfter); } }, { deep: true }, ); }); } export function unMultitab( refs: { [key: string]: Ref }, opts: { prefix: string } = { prefix: "multitab:" }, ) { for (const rawkey in refs) { const key = opts.prefix ? opts.prefix + rawkey : rawkey; localStorage.removeItem(key); delete listening[key]; delete lastUpdate[key]; if (unwatchList[key]) { unwatchList[key](); delete unwatchList[key]; } } } function parseKey(key: string): MultitabValue { let raw = localStorage.getItem(key); let value = undefined; if (raw) { try { value = JSON.parse(raw) as MultitabValue; } catch (ex) {} } return value || { value: undefined, updated: new Date().toISOString() }; } export function loadMultitab(key: string) { const value: MultitabValue = parseKey(key); if (value.expires && new Date(value.expires) < new Date()) { value.value = undefined; } if (!lastUpdate[key] || new Date(lastUpdate[key]) < new Date(value.updated)) { return value.value; } return undefined; } function willChangeKey(key: string, value: any) { return JSON.stringify(value) !== JSON.stringify(parseKey(key).value); } export function saveMultitab( key: string, value: unknown, expireAfter?: number, ) { const packet: MultitabValue = { value: value, updated: new Date().toISOString(), }; if (expireAfter) { packet.expires = new Date(Date.now() + expireAfter).toISOString(); } if (!lastUpdate[key] || packet.updated > lastUpdate[key]) { lastUpdate[key] = packet.updated; localStorage.setItem(key, JSON.stringify(packet)); } } export interface PromiseResolve { promise?: Promise; resolve?: (r: boolean) => void; } const deferred: PromiseResolve[] = []; export function lockPromise(ref: Ref): PromiseResolve { const mine: PromiseResolve = {}; resetPromiseLock(mine, ref.value); deferred.push(mine); watch(ref, () => { if (ref.value) { if (mine.resolve) { mine.resolve(true); } } else { resetPromiseLock(mine, ref.value); } }); return mine; } function resetPromiseLock(lock: PromiseResolve, current: boolean) { if (current) { lock.promise = Promise.resolve(true); } else { lock.promise = new Promise((resolve) => { lock.resolve = resolve; }); } }