diff options
Diffstat (limited to 'packages/web-util/src/utils')
-rw-r--r-- | packages/web-util/src/utils/observable.ts | 140 |
1 files changed, 121 insertions, 19 deletions
diff --git a/packages/web-util/src/utils/observable.ts b/packages/web-util/src/utils/observable.ts index dfa434635..01e655eaa 100644 --- a/packages/web-util/src/utils/observable.ts +++ b/packages/web-util/src/utils/observable.ts @@ -1,17 +1,25 @@ +import { isArrayBufferView } from "util/types"; + export type ObservableMap<K, V> = Map<K, V> & { + onAnyUpdate: (callback: () => void) => () => void; onUpdate: (key: string, callback: () => void) => () => void; }; -const UPDATE_EVENT_NAME = "update"; - //FIXME: allow different type for different properties -export function memoryMap<T>(): ObservableMap<string, T> { +export function memoryMap<T>( + backend: Map<string, T> = new Map<string, T>(), +): ObservableMap<string, T> { const obs = new EventTarget(); - const theMap = new Map<string, T>(); const theMemoryMap: ObservableMap<string, T> = { + onAnyUpdate: (handler) => { + obs.addEventListener(`update`, handler); + obs.addEventListener(`clear`, handler); + return () => { + obs.removeEventListener(`update`, handler); + obs.removeEventListener(`clear`, handler); + }; + }, onUpdate: (key, handler) => { - //@ts-ignore - theMemoryMap.size = theMap.length; obs.addEventListener(`update-${key}`, handler); obs.addEventListener(`clear`, handler); return () => { @@ -20,38 +28,56 @@ export function memoryMap<T>(): ObservableMap<string, T> { }; }, delete: (key: string) => { - const result = theMap.delete(key); + const result = backend.delete(key); + //@ts-ignore + theMemoryMap.size = backend.length; obs.dispatchEvent(new Event(`update-${key}`)); + obs.dispatchEvent(new Event(`update`)); return result; }, set: (key: string, value: T) => { - theMap.set(key, value); + backend.set(key, value); + //@ts-ignore + theMemoryMap.size = backend.length; obs.dispatchEvent(new Event(`update-${key}`)); + obs.dispatchEvent(new Event(`update`)); return theMemoryMap; }, clear: () => { - theMap.clear(); + backend.clear(); obs.dispatchEvent(new Event(`clear`)); }, - entries: theMap.entries.bind(theMap), - forEach: theMap.forEach.bind(theMap), - get: theMap.get.bind(theMap), - has: theMap.has.bind(theMap), - keys: theMap.keys.bind(theMap), - size: theMap.size, - values: theMap.values.bind(theMap), - [Symbol.iterator]: theMap[Symbol.iterator], + entries: backend.entries.bind(backend), + forEach: backend.forEach.bind(backend), + get: backend.get.bind(backend), + has: backend.has.bind(backend), + keys: backend.keys.bind(backend), + size: backend.size, + values: backend.values.bind(backend), + [Symbol.iterator]: backend[Symbol.iterator], [Symbol.toStringTag]: "theMemoryMap", }; return theMemoryMap; } +//FIXME: change this implementation to match the +// browser storage. instead of creating a sync implementation +// of observable map it should reuse the memoryMap and +// sync the state with local storage export function localStorageMap(): ObservableMap<string, string> { const obs = new EventTarget(); const theLocalStorageMap: ObservableMap<string, string> = { + onAnyUpdate: (handler) => { + obs.addEventListener(`update`, handler); + obs.addEventListener(`clear`, handler); + window.addEventListener("storage", handler); + return () => { + window.removeEventListener("storage", handler); + obs.removeEventListener(`update`, handler); + obs.removeEventListener(`clear`, handler); + }; + }, onUpdate: (key, handler) => { - //@ts-ignore - theLocalStorageMap.size = localStorage.length; obs.addEventListener(`update-${key}`, handler); obs.addEventListener(`clear`, handler); function handleStorageEvent(ev: StorageEvent) { @@ -69,12 +95,18 @@ export function localStorageMap(): ObservableMap<string, string> { delete: (key: string) => { const exists = localStorage.getItem(key) !== null; localStorage.removeItem(key); + //@ts-ignore + theLocalStorageMap.size = localStorage.length; obs.dispatchEvent(new Event(`update-${key}`)); + obs.dispatchEvent(new Event(`update`)); return exists; }, set: (key: string, v: string) => { localStorage.setItem(key, v); + //@ts-ignore + theLocalStorageMap.size = localStorage.length; obs.dispatchEvent(new Event(`update-${key}`)); + obs.dispatchEvent(new Event(`update`)); return theLocalStorageMap; }, clear: () => { @@ -179,3 +211,73 @@ export function localStorageMap(): ObservableMap<string, string> { }; return theLocalStorageMap; } + +const isFirefox = + typeof (window as any) !== "undefined" && + typeof (window as any)["InstallTrigger"] !== "undefined"; + +async function getAllContent() { + //Firefox and Chrome has different storage api + if (isFirefox) { + // @ts-ignore + return browser.storage.local.get(); + } else { + return chrome.storage.local.get(); + } +} + +async function updateContent(obj: Record<string, any>) { + if (isFirefox) { + // @ts-ignore + return browser.storage.local.set(obj); + } else { + return chrome.storage.local.set(obj); + } +} +type Changes = { [key: string]: { oldValue?: any; newValue?: any } }; +function onBrowserStorageUpdate(cb: (changes: Changes) => void): void { + if (isFirefox) { + // @ts-ignore + browser.storage.local.onChanged.addListener(cb); + } else { + chrome.storage.local.onChanged.addListener(cb); + } +} + +export function browserStorageMap( + backend: ObservableMap<string, string>, +): ObservableMap<string, string> { + getAllContent().then((content) => { + Object.entries(content ?? {}).forEach(([k, v]) => { + backend.set(k, v as string); + }); + }); + + backend.onAnyUpdate(async () => { + const result: Record<string, string> = {}; + for (const [key, value] of backend.entries()) { + result[key] = value; + } + await updateContent(result); + }); + + onBrowserStorageUpdate((changes) => { + //another chrome instance made the change + const changedItems = Object.keys(changes); + if (changedItems.length === 0) { + backend.clear(); + } else { + for (const key of changedItems) { + if (!changes[key].newValue) { + backend.delete(key); + } else { + if (changes[key].newValue !== changes[key].oldValue) { + backend.set(key, changes[key].newValue); + } + } + } + } + }); + + return backend; +} |