observable memory impl

This commit is contained in:
Sebastian 2023-04-14 13:07:23 -03:00
parent 665adb69f0
commit c3e1a0bb51
No known key found for this signature in database
GPG Key ID: 173909D1A5F66069
6 changed files with 223 additions and 81 deletions

View File

@ -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,
});
};

View File

@ -1,5 +1,5 @@
export { useLang } from "./useLang.js";
export { useLocalStorage, useNotNullLocalStorage } from "./useLocalStorage.js";
export { useLocalStorage } from "./useLocalStorage.js";
export {
useAsyncAsHook,
HookError,

View File

@ -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);
}

View File

@ -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);
},
};
}

View File

@ -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";

View File

@ -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;
}