gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-wallet-core] 01/03: observable memory impl


From: gnunet
Subject: [taler-wallet-core] 01/03: observable memory impl
Date: Fri, 14 Apr 2023 19:18:08 +0200

This is an automated email from the git hooks/post-receive script.

sebasjm pushed a commit to branch master
in repository wallet-core.

commit c3e1a0bb519bf5012781891c15c433841203bce2
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Fri Apr 14 13:07:23 2023 -0300

    observable memory impl
---
 packages/web-util/src/context/translation.ts   |   6 +-
 packages/web-util/src/hooks/index.ts           |   2 +-
 packages/web-util/src/hooks/useLang.ts         |   9 +-
 packages/web-util/src/hooks/useLocalStorage.ts | 105 +++++---------
 packages/web-util/src/index.browser.ts         |   1 +
 packages/web-util/src/utils/observable.ts      | 181 +++++++++++++++++++++++++
 6 files changed, 223 insertions(+), 81 deletions(-)

diff --git a/packages/web-util/src/context/translation.ts 
b/packages/web-util/src/context/translation.ts
index 3b79e31d3..53ca87f9d 100644
--- a/packages/web-util/src/context/translation.ts
+++ b/packages/web-util/src/context/translation.ts
@@ -26,7 +26,6 @@ interface Type {
   supportedLang: { [id in keyof typeof supportedLang]: string };
   changeLanguage: (l: string) => void;
   i18n: InternationalizationAPI;
-  isSaved: boolean;
 }
 
 const supportedLang = {
@@ -46,7 +45,6 @@ const initial = {
     // do not change anything
   },
   i18n,
-  isSaved: false,
 };
 const Context = createContext<Type>(initial);
 
@@ -64,7 +62,7 @@ export const TranslationProvider = ({
   forceLang,
   source,
 }: Props): VNode => {
-  const [lang, changeLanguage, isSaved] = useLang(initial);
+  const { value: lang, update: changeLanguage } = useLang(initial);
   useEffect(() => {
     if (forceLang) {
       changeLanguage(forceLang);
@@ -80,7 +78,7 @@ export const TranslationProvider = ({
   }
 
   return h(Context.Provider, {
-    value: { lang, changeLanguage, supportedLang, i18n, isSaved },
+    value: { lang, changeLanguage, supportedLang, i18n },
     children,
   });
 };
diff --git a/packages/web-util/src/hooks/index.ts 
b/packages/web-util/src/hooks/index.ts
index 393a6fcbb..e5cb54e21 100644
--- a/packages/web-util/src/hooks/index.ts
+++ b/packages/web-util/src/hooks/index.ts
@@ -1,5 +1,5 @@
 export { useLang } from "./useLang.js";
-export { useLocalStorage, useNotNullLocalStorage } from "./useLocalStorage.js";
+export { useLocalStorage } from "./useLocalStorage.js";
 export {
   useAsyncAsHook,
   HookError,
diff --git a/packages/web-util/src/hooks/useLang.ts 
b/packages/web-util/src/hooks/useLang.ts
index 5b02c5255..9888cc51a 100644
--- a/packages/web-util/src/hooks/useLang.ts
+++ b/packages/web-util/src/hooks/useLang.ts
@@ -14,17 +14,16 @@
  GNU Anastasis; see the file COPYING.  If not, see 
<http://www.gnu.org/licenses/>
  */
 
-import { useNotNullLocalStorage } from "./useLocalStorage.js";
+import { LocalStorageState, useLocalStorage } from "./useLocalStorage.js";
 
 function getBrowserLang(): string | undefined {
+  if (typeof window === "undefined") return undefined;
   if (window.navigator.languages) return window.navigator.languages[0];
   if (window.navigator.language) return window.navigator.language;
   return undefined;
 }
 
-export function useLang(
-  initial?: string,
-): [string, (s: string) => void, boolean] {
+export function useLang(initial?: string): Required<LocalStorageState> {
   const defaultLang = (getBrowserLang() || initial || "en").substring(0, 2);
-  return useNotNullLocalStorage("lang-preference", defaultLang);
+  return useLocalStorage("lang-preference", defaultLang);
 }
diff --git a/packages/web-util/src/hooks/useLocalStorage.ts 
b/packages/web-util/src/hooks/useLocalStorage.ts
index ab786db13..264919d37 100644
--- a/packages/web-util/src/hooks/useLocalStorage.ts
+++ b/packages/web-util/src/hooks/useLocalStorage.ts
@@ -19,92 +19,55 @@
  * @author Sebastian Javier Marchano (sebasjm)
  */
 
-import { StateUpdater, useEffect, useState } from "preact/hooks";
+import { useEffect, useState } from "preact/hooks";
+import { localStorageMap, memoryMap } from "../utils/observable.js";
 
+export interface LocalStorageState {
+  value?: string;
+  update: (s: string) => void;
+  reset: () => void;
+}
+
+const supportLocalStorage = typeof window !== "undefined";
+
+const storage = supportLocalStorage ? localStorageMap() : memoryMap<string>();
+
+export function useLocalStorage(
+  key: string,
+  initialValue: string,
+): Required<LocalStorageState>;
+export function useLocalStorage(key: string): LocalStorageState;
 export function useLocalStorage(
   key: string,
   initialValue?: string,
-): [string | undefined, StateUpdater<string | undefined>] {
+): LocalStorageState {
   const [storedValue, setStoredValue] = useState<string | undefined>(
     (): string | undefined => {
-      return typeof window !== "undefined"
-        ? window.localStorage.getItem(key) || initialValue
-        : initialValue;
+      return storage.get(key) ?? initialValue;
     },
   );
 
   useEffect(() => {
-    const listener = buildListenerForKey(key, (newValue) => {
+    return storage.onUpdate(key, () => {
+      const newValue = storage.get(key);
+      console.log("new value", key, newValue);
       setStoredValue(newValue ?? initialValue);
     });
-    window.addEventListener("storage", listener);
-    return () => {
-      window.removeEventListener("storage", listener);
-    };
   }, []);
 
-  const setValue = (
-    value?: string | ((val?: string) => string | undefined),
-  ): void => {
-    setStoredValue((p) => {
-      const toStore = value instanceof Function ? value(p) : value;
-      if (typeof window !== "undefined") {
-        if (!toStore) {
-          window.localStorage.removeItem(key);
-        } else {
-          window.localStorage.setItem(key, toStore);
-        }
-      }
-      return toStore;
-    });
-  };
-
-  return [storedValue, setValue];
-}
-
-function buildListenerForKey(
-  key: string,
-  onUpdate: (newValue: string | undefined) => void,
-): () => void {
-  return function listenKeyChange() {
-    const value = window.localStorage.getItem(key);
-    onUpdate(value ?? undefined);
-  };
-}
-
-//TODO: merge with the above function
-export function useNotNullLocalStorage(
-  key: string,
-  initialValue: string,
-): [string, StateUpdater<string>, boolean] {
-  const [storedValue, setStoredValue] = useState<string>((): string => {
-    return typeof window !== "undefined"
-      ? window.localStorage.getItem(key) || initialValue
-      : initialValue;
-  });
-
-  useEffect(() => {
-    const listener = buildListenerForKey(key, (newValue) => {
-      setStoredValue(newValue ?? initialValue);
-    });
-    window.addEventListener("storage", listener);
-    return () => {
-      window.removeEventListener("storage", listener);
-    };
-  });
-
-  const setValue = (value: string | ((val: string) => string)): void => {
-    const valueToStore = value instanceof Function ? value(storedValue) : 
value;
-    setStoredValue(valueToStore);
-    if (typeof window !== "undefined") {
-      if (!valueToStore) {
-        window.localStorage.removeItem(key);
-      } else {
-        window.localStorage.setItem(key, valueToStore);
-      }
+  const setValue = (value?: string): void => {
+    if (!value) {
+      storage.delete(key);
+    } else {
+      storage.set(key, value);
     }
   };
 
-  const isSaved = window.localStorage.getItem(key) !== null;
-  return [storedValue, setValue, isSaved];
+  return {
+    value: storedValue,
+    update: setValue,
+    reset: () => {
+      setValue(initialValue);
+    },
+  };
 }
diff --git a/packages/web-util/src/index.browser.ts 
b/packages/web-util/src/index.browser.ts
index 2ae3f2a0b..b1df2f96e 100644
--- a/packages/web-util/src/index.browser.ts
+++ b/packages/web-util/src/index.browser.ts
@@ -1,5 +1,6 @@
 export * from "./hooks/index.js";
 export * from "./utils/request.js";
+export * from "./utils/observable.js";
 export * from "./context/index.js";
 export * from "./components/index.js";
 export * as tests from "./tests/index.js";
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;
+}

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]