observable memory impl
This commit is contained in:
parent
665adb69f0
commit
c3e1a0bb51
@ -26,7 +26,6 @@ interface Type {
|
|||||||
supportedLang: { [id in keyof typeof supportedLang]: string };
|
supportedLang: { [id in keyof typeof supportedLang]: string };
|
||||||
changeLanguage: (l: string) => void;
|
changeLanguage: (l: string) => void;
|
||||||
i18n: InternationalizationAPI;
|
i18n: InternationalizationAPI;
|
||||||
isSaved: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const supportedLang = {
|
const supportedLang = {
|
||||||
@ -46,7 +45,6 @@ const initial = {
|
|||||||
// do not change anything
|
// do not change anything
|
||||||
},
|
},
|
||||||
i18n,
|
i18n,
|
||||||
isSaved: false,
|
|
||||||
};
|
};
|
||||||
const Context = createContext<Type>(initial);
|
const Context = createContext<Type>(initial);
|
||||||
|
|
||||||
@ -64,7 +62,7 @@ export const TranslationProvider = ({
|
|||||||
forceLang,
|
forceLang,
|
||||||
source,
|
source,
|
||||||
}: Props): VNode => {
|
}: Props): VNode => {
|
||||||
const [lang, changeLanguage, isSaved] = useLang(initial);
|
const { value: lang, update: changeLanguage } = useLang(initial);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (forceLang) {
|
if (forceLang) {
|
||||||
changeLanguage(forceLang);
|
changeLanguage(forceLang);
|
||||||
@ -80,7 +78,7 @@ export const TranslationProvider = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return h(Context.Provider, {
|
return h(Context.Provider, {
|
||||||
value: { lang, changeLanguage, supportedLang, i18n, isSaved },
|
value: { lang, changeLanguage, supportedLang, i18n },
|
||||||
children,
|
children,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
export { useLang } from "./useLang.js";
|
export { useLang } from "./useLang.js";
|
||||||
export { useLocalStorage, useNotNullLocalStorage } from "./useLocalStorage.js";
|
export { useLocalStorage } from "./useLocalStorage.js";
|
||||||
export {
|
export {
|
||||||
useAsyncAsHook,
|
useAsyncAsHook,
|
||||||
HookError,
|
HookError,
|
||||||
|
@ -14,17 +14,16 @@
|
|||||||
GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
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 {
|
function getBrowserLang(): string | undefined {
|
||||||
|
if (typeof window === "undefined") return undefined;
|
||||||
if (window.navigator.languages) return window.navigator.languages[0];
|
if (window.navigator.languages) return window.navigator.languages[0];
|
||||||
if (window.navigator.language) return window.navigator.language;
|
if (window.navigator.language) return window.navigator.language;
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useLang(
|
export function useLang(initial?: string): Required<LocalStorageState> {
|
||||||
initial?: string,
|
|
||||||
): [string, (s: string) => void, boolean] {
|
|
||||||
const defaultLang = (getBrowserLang() || initial || "en").substring(0, 2);
|
const defaultLang = (getBrowserLang() || initial || "en").substring(0, 2);
|
||||||
return useNotNullLocalStorage("lang-preference", defaultLang);
|
return useLocalStorage("lang-preference", defaultLang);
|
||||||
}
|
}
|
||||||
|
@ -19,92 +19,55 @@
|
|||||||
* @author Sebastian Javier Marchano (sebasjm)
|
* @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(
|
export function useLocalStorage(
|
||||||
key: string,
|
key: string,
|
||||||
initialValue?: string,
|
initialValue?: string,
|
||||||
): [string | undefined, StateUpdater<string | undefined>] {
|
): LocalStorageState {
|
||||||
const [storedValue, setStoredValue] = useState<string | undefined>(
|
const [storedValue, setStoredValue] = useState<string | undefined>(
|
||||||
(): string | undefined => {
|
(): string | undefined => {
|
||||||
return typeof window !== "undefined"
|
return storage.get(key) ?? initialValue;
|
||||||
? window.localStorage.getItem(key) || initialValue
|
|
||||||
: initialValue;
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const listener = buildListenerForKey(key, (newValue) => {
|
return storage.onUpdate(key, () => {
|
||||||
|
const newValue = storage.get(key);
|
||||||
|
console.log("new value", key, newValue);
|
||||||
setStoredValue(newValue ?? initialValue);
|
setStoredValue(newValue ?? initialValue);
|
||||||
});
|
});
|
||||||
window.addEventListener("storage", listener);
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener("storage", listener);
|
|
||||||
};
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const setValue = (
|
const setValue = (value?: string): void => {
|
||||||
value?: string | ((val?: string) => string | undefined),
|
if (!value) {
|
||||||
): void => {
|
storage.delete(key);
|
||||||
setStoredValue((p) => {
|
|
||||||
const toStore = value instanceof Function ? value(p) : value;
|
|
||||||
if (typeof window !== "undefined") {
|
|
||||||
if (!toStore) {
|
|
||||||
window.localStorage.removeItem(key);
|
|
||||||
} else {
|
} else {
|
||||||
window.localStorage.setItem(key, toStore);
|
storage.set(key, value);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return toStore;
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return [storedValue, setValue];
|
return {
|
||||||
}
|
value: storedValue,
|
||||||
|
update: setValue,
|
||||||
function buildListenerForKey(
|
reset: () => {
|
||||||
key: string,
|
setValue(initialValue);
|
||||||
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 isSaved = window.localStorage.getItem(key) !== null;
|
|
||||||
return [storedValue, setValue, isSaved];
|
|
||||||
}
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
export * from "./hooks/index.js";
|
export * from "./hooks/index.js";
|
||||||
export * from "./utils/request.js";
|
export * from "./utils/request.js";
|
||||||
|
export * from "./utils/observable.js";
|
||||||
export * from "./context/index.js";
|
export * from "./context/index.js";
|
||||||
export * from "./components/index.js";
|
export * from "./components/index.js";
|
||||||
export * as tests from "./tests/index.js";
|
export * as tests from "./tests/index.js";
|
||||||
|
181
packages/web-util/src/utils/observable.ts
Normal file
181
packages/web-util/src/utils/observable.ts
Normal 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;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user