wallet-core/packages/web-util/src/utils/observable.ts

284 lines
8.7 KiB
TypeScript
Raw Normal View History

2023-04-18 15:46:27 +02:00
import { isArrayBufferView } from "util/types";
2023-04-14 18:07:23 +02:00
export type ObservableMap<K, V> = Map<K, V> & {
2023-04-18 15:46:27 +02:00
onAnyUpdate: (callback: () => void) => () => void;
2023-04-14 18:07:23 +02:00
onUpdate: (key: string, callback: () => void) => () => void;
};
//FIXME: allow different type for different properties
2023-04-18 15:46:27 +02:00
export function memoryMap<T>(
backend: Map<string, T> = new Map<string, T>(),
): ObservableMap<string, T> {
2023-04-14 18:07:23 +02:00
const obs = new EventTarget();
const theMemoryMap: ObservableMap<string, T> = {
2023-04-18 15:46:27 +02:00
onAnyUpdate: (handler) => {
obs.addEventListener(`update`, handler);
obs.addEventListener(`clear`, handler);
return () => {
obs.removeEventListener(`update`, handler);
obs.removeEventListener(`clear`, handler);
};
},
2023-04-14 18:07:23 +02:00
onUpdate: (key, handler) => {
obs.addEventListener(`update-${key}`, handler);
obs.addEventListener(`clear`, handler);
return () => {
obs.removeEventListener(`update-${key}`, handler);
obs.removeEventListener(`clear`, handler);
};
},
delete: (key: string) => {
2023-04-18 15:46:27 +02:00
const result = backend.delete(key);
//@ts-ignore
theMemoryMap.size = backend.length;
2023-04-14 18:07:23 +02:00
obs.dispatchEvent(new Event(`update-${key}`));
2023-04-18 15:46:27 +02:00
obs.dispatchEvent(new Event(`update`));
2023-04-14 18:07:23 +02:00
return result;
},
set: (key: string, value: T) => {
2023-04-18 15:46:27 +02:00
backend.set(key, value);
//@ts-ignore
theMemoryMap.size = backend.length;
2023-04-14 18:07:23 +02:00
obs.dispatchEvent(new Event(`update-${key}`));
2023-04-18 15:46:27 +02:00
obs.dispatchEvent(new Event(`update`));
2023-04-14 18:07:23 +02:00
return theMemoryMap;
},
clear: () => {
2023-04-18 15:46:27 +02:00
backend.clear();
2023-04-14 18:07:23 +02:00
obs.dispatchEvent(new Event(`clear`));
},
2023-04-18 15:46:27 +02:00
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],
2023-04-14 18:07:23 +02:00
[Symbol.toStringTag]: "theMemoryMap",
};
return theMemoryMap;
}
2023-04-18 15:46:27 +02:00
//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
2023-04-14 18:07:23 +02:00
export function localStorageMap(): ObservableMap<string, string> {
const obs = new EventTarget();
const theLocalStorageMap: ObservableMap<string, string> = {
2023-04-18 15:46:27 +02:00
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);
};
},
2023-04-14 18:07:23 +02:00
onUpdate: (key, handler) => {
obs.addEventListener(`update-${key}`, handler);
obs.addEventListener(`clear`, handler);
function handleStorageEvent(ev: StorageEvent) {
if (ev.key === null || ev.key === key) {
handler();
}
}
window.addEventListener("storage", handleStorageEvent);
return () => {
window.removeEventListener("storage", handleStorageEvent);
obs.removeEventListener(`update-${key}`, handler);
obs.removeEventListener(`clear`, handler);
};
},
delete: (key: string) => {
const exists = localStorage.getItem(key) !== null;
localStorage.removeItem(key);
2023-04-18 15:46:27 +02:00
//@ts-ignore
theLocalStorageMap.size = localStorage.length;
2023-04-14 18:07:23 +02:00
obs.dispatchEvent(new Event(`update-${key}`));
2023-04-18 15:46:27 +02:00
obs.dispatchEvent(new Event(`update`));
2023-04-14 18:07:23 +02:00
return exists;
},
set: (key: string, v: string) => {
localStorage.setItem(key, v);
2023-04-18 15:46:27 +02:00
//@ts-ignore
theLocalStorageMap.size = localStorage.length;
2023-04-14 18:07:23 +02:00
obs.dispatchEvent(new Event(`update-${key}`));
2023-04-18 15:46:27 +02:00
obs.dispatchEvent(new Event(`update`));
2023-04-14 18:07:23 +02:00
return theLocalStorageMap;
},
clear: () => {
localStorage.clear();
obs.dispatchEvent(new Event(`clear`));
},
entries: (): IterableIterator<[string, string]> => {
let index = 0;
const total = localStorage.length;
return {
next() {
const key = localStorage.key(index);
if (key === null) {
//we are going from 0 until last, this should not happen
throw Error("key cant be null");
}
const item = localStorage.getItem(key);
if (item === null) {
//the key exist, this should not happen
throw Error("value cant be null");
}
if (index == total) return { done: true, value: [key, item] };
index = index + 1;
return { done: false, value: [key, item] };
},
[Symbol.iterator]() {
return this;
},
};
},
forEach: (cb) => {
for (let index = 0; index < localStorage.length; index++) {
const key = localStorage.key(index);
if (key === null) {
//we are going from 0 until last, this should not happen
throw Error("key cant be null");
}
const item = localStorage.getItem(key);
if (item === null) {
//the key exist, this should not happen
throw Error("value cant be null");
}
cb(key, item, theLocalStorageMap);
}
},
get: (key: string) => {
const item = localStorage.getItem(key);
if (item === null) return undefined;
return item;
},
has: (key: string) => {
return localStorage.getItem(key) === null;
},
keys: () => {
let index = 0;
const total = localStorage.length;
return {
next() {
const key = localStorage.key(index);
if (key === null) {
//we are going from 0 until last, this should not happen
throw Error("key cant be null");
}
if (index == total) return { done: true, value: key };
index = index + 1;
return { done: false, value: key };
},
[Symbol.iterator]() {
return this;
},
};
},
size: localStorage.length,
values: () => {
let index = 0;
const total = localStorage.length;
return {
next() {
const key = localStorage.key(index);
if (key === null) {
//we are going from 0 until last, this should not happen
throw Error("key cant be null");
}
const item = localStorage.getItem(key);
if (item === null) {
//the key exist, this should not happen
throw Error("value cant be null");
}
if (index == total) return { done: true, value: item };
index = index + 1;
return { done: false, value: item };
},
[Symbol.iterator]() {
return this;
},
};
},
[Symbol.iterator]: function (): IterableIterator<[string, string]> {
return theLocalStorageMap.entries();
},
[Symbol.toStringTag]: "theLocalStorageMap",
};
return theLocalStorageMap;
}
2023-04-18 15:46:27 +02:00
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;
}