284 lines
8.7 KiB
TypeScript
284 lines
8.7 KiB
TypeScript
import { isArrayBufferView } from "util/types";
|
|
|
|
export type ObservableMap<K, V> = Map<K, V> & {
|
|
onAnyUpdate: (callback: () => void) => () => void;
|
|
onUpdate: (key: string, callback: () => void) => () => void;
|
|
};
|
|
|
|
//FIXME: allow different type for different properties
|
|
export function memoryMap<T>(
|
|
backend: Map<string, T> = new Map<string, T>(),
|
|
): ObservableMap<string, T> {
|
|
const obs = new EventTarget();
|
|
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) => {
|
|
obs.addEventListener(`update-${key}`, handler);
|
|
obs.addEventListener(`clear`, handler);
|
|
return () => {
|
|
obs.removeEventListener(`update-${key}`, handler);
|
|
obs.removeEventListener(`clear`, handler);
|
|
};
|
|
},
|
|
delete: (key: string) => {
|
|
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) => {
|
|
backend.set(key, value);
|
|
//@ts-ignore
|
|
theMemoryMap.size = backend.length;
|
|
obs.dispatchEvent(new Event(`update-${key}`));
|
|
obs.dispatchEvent(new Event(`update`));
|
|
return theMemoryMap;
|
|
},
|
|
clear: () => {
|
|
backend.clear();
|
|
obs.dispatchEvent(new Event(`clear`));
|
|
},
|
|
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) => {
|
|
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);
|
|
//@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: () => {
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|