sync with chrome storage

This commit is contained in:
Sebastian 2023-04-18 10:46:27 -03:00
parent 6833b2bd75
commit b1a0d034fc
No known key found for this signature in database
GPG Key ID: 173909D1A5F66069
3 changed files with 141 additions and 25 deletions

View File

@ -20,7 +20,12 @@
*/ */
import { useEffect, useState } from "preact/hooks"; 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 { export interface LocalStorageState {
value?: string; value?: string;
@ -29,8 +34,18 @@ export interface LocalStorageState {
} }
const supportLocalStorage = typeof window !== "undefined"; 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( export function useLocalStorage(
key: string, key: string,

View File

@ -1,17 +1,25 @@
import { isArrayBufferView } from "util/types";
export type ObservableMap<K, V> = Map<K, V> & { export type ObservableMap<K, V> = Map<K, V> & {
onAnyUpdate: (callback: () => void) => () => void;
onUpdate: (key: string, callback: () => void) => () => void; onUpdate: (key: string, callback: () => void) => () => void;
}; };
const UPDATE_EVENT_NAME = "update";
//FIXME: allow different type for different properties //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 obs = new EventTarget();
const theMap = new Map<string, T>();
const theMemoryMap: ObservableMap<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) => { onUpdate: (key, handler) => {
//@ts-ignore
theMemoryMap.size = theMap.length;
obs.addEventListener(`update-${key}`, handler); obs.addEventListener(`update-${key}`, handler);
obs.addEventListener(`clear`, handler); obs.addEventListener(`clear`, handler);
return () => { return () => {
@ -20,38 +28,56 @@ export function memoryMap<T>(): ObservableMap<string, T> {
}; };
}, },
delete: (key: string) => { 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-${key}`));
obs.dispatchEvent(new Event(`update`));
return result; return result;
}, },
set: (key: string, value: T) => { 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-${key}`));
obs.dispatchEvent(new Event(`update`));
return theMemoryMap; return theMemoryMap;
}, },
clear: () => { clear: () => {
theMap.clear(); backend.clear();
obs.dispatchEvent(new Event(`clear`)); obs.dispatchEvent(new Event(`clear`));
}, },
entries: theMap.entries.bind(theMap), entries: backend.entries.bind(backend),
forEach: theMap.forEach.bind(theMap), forEach: backend.forEach.bind(backend),
get: theMap.get.bind(theMap), get: backend.get.bind(backend),
has: theMap.has.bind(theMap), has: backend.has.bind(backend),
keys: theMap.keys.bind(theMap), keys: backend.keys.bind(backend),
size: theMap.size, size: backend.size,
values: theMap.values.bind(theMap), values: backend.values.bind(backend),
[Symbol.iterator]: theMap[Symbol.iterator], [Symbol.iterator]: backend[Symbol.iterator],
[Symbol.toStringTag]: "theMemoryMap", [Symbol.toStringTag]: "theMemoryMap",
}; };
return 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> { export function localStorageMap(): ObservableMap<string, string> {
const obs = new EventTarget(); const obs = new EventTarget();
const theLocalStorageMap: ObservableMap<string, string> = { 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) => { onUpdate: (key, handler) => {
//@ts-ignore
theLocalStorageMap.size = localStorage.length;
obs.addEventListener(`update-${key}`, handler); obs.addEventListener(`update-${key}`, handler);
obs.addEventListener(`clear`, handler); obs.addEventListener(`clear`, handler);
function handleStorageEvent(ev: StorageEvent) { function handleStorageEvent(ev: StorageEvent) {
@ -69,12 +95,18 @@ export function localStorageMap(): ObservableMap<string, string> {
delete: (key: string) => { delete: (key: string) => {
const exists = localStorage.getItem(key) !== null; const exists = localStorage.getItem(key) !== null;
localStorage.removeItem(key); localStorage.removeItem(key);
//@ts-ignore
theLocalStorageMap.size = localStorage.length;
obs.dispatchEvent(new Event(`update-${key}`)); obs.dispatchEvent(new Event(`update-${key}`));
obs.dispatchEvent(new Event(`update`));
return exists; return exists;
}, },
set: (key: string, v: string) => { set: (key: string, v: string) => {
localStorage.setItem(key, v); localStorage.setItem(key, v);
//@ts-ignore
theLocalStorageMap.size = localStorage.length;
obs.dispatchEvent(new Event(`update-${key}`)); obs.dispatchEvent(new Event(`update-${key}`));
obs.dispatchEvent(new Event(`update`));
return theLocalStorageMap; return theLocalStorageMap;
}, },
clear: () => { clear: () => {
@ -179,3 +211,73 @@ export function localStorageMap(): ObservableMap<string, string> {
}; };
return theLocalStorageMap; 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;
}

View File

@ -560,6 +560,7 @@ importers:
packages/web-util: packages/web-util:
specifiers: specifiers:
'@gnu-taler/taler-util': workspace:* '@gnu-taler/taler-util': workspace:*
'@types/chrome': 0.0.197
'@types/express': ^4.17.14 '@types/express': ^4.17.14
'@types/node': ^18.11.17 '@types/node': ^18.11.17
'@types/web': ^0.0.82 '@types/web': ^0.0.82
@ -576,6 +577,8 @@ importers:
tslib: ^2.4.0 tslib: ^2.4.0
typescript: ^4.9.4 typescript: ^4.9.4
ws: 7.4.5 ws: 7.4.5
dependencies:
'@types/chrome': 0.0.197
devDependencies: devDependencies:
'@gnu-taler/taler-util': link:../taler-util '@gnu-taler/taler-util': link:../taler-util
'@types/express': 4.17.14 '@types/express': 4.17.14
@ -3775,7 +3778,6 @@ packages:
dependencies: dependencies:
'@types/filesystem': 0.0.32 '@types/filesystem': 0.0.32
'@types/har-format': 1.2.9 '@types/har-format': 1.2.9
dev: true
/@types/connect-history-api-fallback/1.3.5: /@types/connect-history-api-fallback/1.3.5:
resolution: {integrity: sha512-h8QJa8xSb1WD4fpKBDcATDNGXghFj6/3GRWG6dhmRcu0RX1Ubasur2Uvx5aeEwlf0MwblEC2bMzzMQntxnw/Cw==} resolution: {integrity: sha512-h8QJa8xSb1WD4fpKBDcATDNGXghFj6/3GRWG6dhmRcu0RX1Ubasur2Uvx5aeEwlf0MwblEC2bMzzMQntxnw/Cw==}
@ -3815,15 +3817,12 @@ packages:
resolution: {integrity: sha512-Yuf4jR5YYMR2DVgwuCiP11s0xuVRyPKmz8vo6HBY3CGdeMj8af93CFZX+T82+VD1+UqHOxTq31lO7MI7lepBtQ==} resolution: {integrity: sha512-Yuf4jR5YYMR2DVgwuCiP11s0xuVRyPKmz8vo6HBY3CGdeMj8af93CFZX+T82+VD1+UqHOxTq31lO7MI7lepBtQ==}
dependencies: dependencies:
'@types/filewriter': 0.0.29 '@types/filewriter': 0.0.29
dev: true
/@types/filewriter/0.0.29: /@types/filewriter/0.0.29:
resolution: {integrity: sha512-BsPXH/irW0ht0Ji6iw/jJaK8Lj3FJemon2gvEqHKpCdDCeemHa+rI3WBGq5z7cDMZgoLjY40oninGxqk+8NzNQ==} resolution: {integrity: sha512-BsPXH/irW0ht0Ji6iw/jJaK8Lj3FJemon2gvEqHKpCdDCeemHa+rI3WBGq5z7cDMZgoLjY40oninGxqk+8NzNQ==}
dev: true
/@types/har-format/1.2.9: /@types/har-format/1.2.9:
resolution: {integrity: sha512-rffW6MhQ9yoa75bdNi+rjZBAvu2HhehWJXlhuWXnWdENeuKe82wUgAwxYOb7KRKKmxYN+D/iRKd2NDQMLqlUmg==} resolution: {integrity: sha512-rffW6MhQ9yoa75bdNi+rjZBAvu2HhehWJXlhuWXnWdENeuKe82wUgAwxYOb7KRKKmxYN+D/iRKd2NDQMLqlUmg==}
dev: true
/@types/history/4.7.11: /@types/history/4.7.11:
resolution: {integrity: sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==} resolution: {integrity: sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==}