aboutsummaryrefslogtreecommitdiff
path: root/packages/web-util/src/utils/observable.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/web-util/src/utils/observable.ts')
-rw-r--r--packages/web-util/src/utils/observable.ts181
1 files changed, 181 insertions, 0 deletions
diff --git a/packages/web-util/src/utils/observable.ts b/packages/web-util/src/utils/observable.ts
new file mode 100644
index 000000000..dfa434635
--- /dev/null
+++ b/packages/web-util/src/utils/observable.ts
@@ -0,0 +1,181 @@
+export type ObservableMap<K, V> = Map<K, V> & {
+ 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> {
+ const obs = new EventTarget();
+ const theMap = new Map<string, T>();
+ const theMemoryMap: ObservableMap<string, T> = {
+ onUpdate: (key, handler) => {
+ //@ts-ignore
+ theMemoryMap.size = theMap.length;
+ obs.addEventListener(`update-${key}`, handler);
+ obs.addEventListener(`clear`, handler);
+ return () => {
+ obs.removeEventListener(`update-${key}`, handler);
+ obs.removeEventListener(`clear`, handler);
+ };
+ },
+ delete: (key: string) => {
+ const result = theMap.delete(key);
+ obs.dispatchEvent(new Event(`update-${key}`));
+ return result;
+ },
+ set: (key: string, value: T) => {
+ theMap.set(key, value);
+ obs.dispatchEvent(new Event(`update-${key}`));
+ return theMemoryMap;
+ },
+ clear: () => {
+ theMap.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],
+ [Symbol.toStringTag]: "theMemoryMap",
+ };
+ return theMemoryMap;
+}
+
+export function localStorageMap(): ObservableMap<string, string> {
+ const obs = new EventTarget();
+ const theLocalStorageMap: ObservableMap<string, string> = {
+ onUpdate: (key, handler) => {
+ //@ts-ignore
+ theLocalStorageMap.size = localStorage.length;
+ 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);
+ obs.dispatchEvent(new Event(`update-${key}`));
+ return exists;
+ },
+ set: (key: string, v: string) => {
+ localStorage.setItem(key, v);
+ obs.dispatchEvent(new Event(`update-${key}`));
+ 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;
+}