summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/web-util/src/hooks/useLocalStorage.ts19
-rw-r--r--packages/web-util/src/utils/observable.ts140
-rw-r--r--pnpm-lock.yaml7
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<string>();
+const storage: ObservableMap<string, string> = (function buildStorage() {
+ if (supportBrowserStorage) {
+ return browserStorageMap(memoryMap<string>());
+ } else if (supportLocalStorage) {
+ return localStorageMap();
+ } else {
+ return memoryMap<string>();
+ }
+})();
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<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;
+}
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==}