From b1a0d034fc1be0824e1eac46661604558264beee Mon Sep 17 00:00:00 2001 From: Sebastian Date: Tue, 18 Apr 2023 10:46:27 -0300 Subject: [PATCH] sync with chrome storage --- .../web-util/src/hooks/useLocalStorage.ts | 19 ++- packages/web-util/src/utils/observable.ts | 140 +++++++++++++++--- pnpm-lock.yaml | 7 +- 3 files changed, 141 insertions(+), 25 deletions(-) diff --git a/packages/web-util/src/hooks/useLocalStorage.ts b/packages/web-util/src/hooks/useLocalStorage.ts index dd6c5def8..495c9b0f8 100644 --- a/packages/web-util/src/hooks/useLocalStorage.ts +++ b/packages/web-util/src/hooks/useLocalStorage.ts @@ -20,7 +20,12 @@ */ import { useEffect, useState } from "preact/hooks"; -import { localStorageMap, memoryMap } from "../utils/observable.js"; +import { + ObservableMap, + browserStorageMap, + localStorageMap, + memoryMap, +} from "../utils/observable.js"; export interface LocalStorageState { value?: string; @@ -29,8 +34,18 @@ export interface LocalStorageState { } const supportLocalStorage = typeof window !== "undefined"; +const supportBrowserStorage = + typeof chrome !== "undefined" && typeof chrome.storage !== "undefined"; -const storage = supportLocalStorage ? localStorageMap() : memoryMap(); +const storage: ObservableMap = (function buildStorage() { + if (supportBrowserStorage) { + return browserStorageMap(memoryMap()); + } else if (supportLocalStorage) { + return localStorageMap(); + } else { + return memoryMap(); + } +})(); export function useLocalStorage( key: string, 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 = Map & { + 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(): ObservableMap { +export function memoryMap( + backend: Map = new Map(), +): ObservableMap { const obs = new EventTarget(); - const theMap = new Map(); const theMemoryMap: ObservableMap = { + 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(): ObservableMap { }; }, 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 { const obs = new EventTarget(); const theLocalStorageMap: ObservableMap = { + 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 { 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 { }; 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) { + 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, +): ObservableMap { + getAllContent().then((content) => { + Object.entries(content ?? {}).forEach(([k, v]) => { + backend.set(k, v as string); + }); + }); + + backend.onAnyUpdate(async () => { + const result: Record = {}; + 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; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 56f678e46..c7b2df4bf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -560,6 +560,7 @@ importers: packages/web-util: specifiers: '@gnu-taler/taler-util': workspace:* + '@types/chrome': 0.0.197 '@types/express': ^4.17.14 '@types/node': ^18.11.17 '@types/web': ^0.0.82 @@ -576,6 +577,8 @@ importers: tslib: ^2.4.0 typescript: ^4.9.4 ws: 7.4.5 + dependencies: + '@types/chrome': 0.0.197 devDependencies: '@gnu-taler/taler-util': link:../taler-util '@types/express': 4.17.14 @@ -3775,7 +3778,6 @@ packages: dependencies: '@types/filesystem': 0.0.32 '@types/har-format': 1.2.9 - dev: true /@types/connect-history-api-fallback/1.3.5: resolution: {integrity: sha512-h8QJa8xSb1WD4fpKBDcATDNGXghFj6/3GRWG6dhmRcu0RX1Ubasur2Uvx5aeEwlf0MwblEC2bMzzMQntxnw/Cw==} @@ -3815,15 +3817,12 @@ packages: resolution: {integrity: sha512-Yuf4jR5YYMR2DVgwuCiP11s0xuVRyPKmz8vo6HBY3CGdeMj8af93CFZX+T82+VD1+UqHOxTq31lO7MI7lepBtQ==} dependencies: '@types/filewriter': 0.0.29 - dev: true /@types/filewriter/0.0.29: resolution: {integrity: sha512-BsPXH/irW0ht0Ji6iw/jJaK8Lj3FJemon2gvEqHKpCdDCeemHa+rI3WBGq5z7cDMZgoLjY40oninGxqk+8NzNQ==} - dev: true /@types/har-format/1.2.9: resolution: {integrity: sha512-rffW6MhQ9yoa75bdNi+rjZBAvu2HhehWJXlhuWXnWdENeuKe82wUgAwxYOb7KRKKmxYN+D/iRKd2NDQMLqlUmg==} - dev: true /@types/history/4.7.11: resolution: {integrity: sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==}