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