sync with chrome storage
This commit is contained in:
parent
6833b2bd75
commit
b1a0d034fc
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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==}
|
||||
|
Loading…
Reference in New Issue
Block a user