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 };
|
||||
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,
|
||||
});
|
||||
};
|
||||
|
@ -1,5 +1,5 @@
|
||||
export { useLang } from "./useLang.js";
|
||||
export { useLocalStorage, useNotNullLocalStorage } from "./useLocalStorage.js";
|
||||
export { useLocalStorage } from "./useLocalStorage.js";
|
||||
export {
|
||||
useAsyncAsHook,
|
||||
HookError,
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -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";
|
||||
|
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