use buildKeyStorage to prevent different type for same key

This commit is contained in:
Sebastian 2023-05-26 16:51:47 -03:00
parent 62cec67983
commit be27647ff7
No known key found for this signature in database
GPG Key ID: 173909D1A5F66069
5 changed files with 101 additions and 73 deletions

View File

@ -14,8 +14,13 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/ */
import { useLocalStorage } from "@gnu-taler/web-util/browser"; import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser";
import { Settings, defaultSettings } from "../platform/api.js"; import { Settings, defaultSettings } from "../platform/api.js";
import {
Codec,
buildCodecForObject,
codecForBoolean,
} from "@gnu-taler/taler-util";
function parse_json_or_undefined<T>(str: string | undefined): T | undefined { function parse_json_or_undefined<T>(str: string | undefined): T | undefined {
if (str === undefined) return undefined; if (str === undefined) return undefined;
@ -26,17 +31,30 @@ function parse_json_or_undefined<T>(str: string | undefined): T | undefined {
} }
} }
export const codecForSettings = (): Codec<Settings> =>
buildCodecForObject<Settings>()
.property("walletAllowHttp", codecForBoolean())
.property("walletBatchWithdrawal", codecForBoolean())
.property("injectTalerSupport", codecForBoolean())
.property("advanceMode", codecForBoolean())
.property("backup", codecForBoolean())
.property("langSelector", codecForBoolean())
.property("showJsonOnError", codecForBoolean())
.property("extendedAccountTypes", codecForBoolean())
.property("deleteActiveTransactions", codecForBoolean())
.build("Settings");
const SETTINGS_KEY = buildStorageKey("wallet-settings", codecForSettings());
export function useSettings(): [ export function useSettings(): [
Readonly<Settings>, Readonly<Settings>,
<T extends keyof Settings>(key: T, value: Settings[T]) => void, <T extends keyof Settings>(key: T, value: Settings[T]) => void,
] { ] {
const { value, update } = useLocalStorage("wallet-settings"); const { value, update } = useLocalStorage(SETTINGS_KEY);
const parsed: Settings = parse_json_or_undefined(value) ?? defaultSettings; const parsed: Settings = value ?? defaultSettings;
function updateField<T extends keyof Settings>(k: T, v: Settings[T]) { function updateField<T extends keyof Settings>(k: T, v: Settings[T]) {
const newValue = { ...parsed, [k]: v }; update({ ...parsed, [k]: v });
const json = JSON.stringify(newValue);
update(json);
} }
return [parsed, updateField]; return [parsed, updateField];
} }

View File

@ -1,5 +1,5 @@
export { useLang } from "./useLang.js"; export { useLang } from "./useLang.js";
export { useLocalStorage } from "./useLocalStorage.js"; export { useLocalStorage, buildStorageKey } from "./useLocalStorage.js";
export { useMemoryStorage } from "./useMemoryStorage.js"; export { useMemoryStorage } from "./useMemoryStorage.js";
export { export {
useNotifications, useNotifications,

View File

@ -14,7 +14,11 @@
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 { LocalStorageState, useLocalStorage } from "./useLocalStorage.js"; import {
StorageState,
buildStorageKey,
useLocalStorage,
} from "./useLocalStorage.js";
function getBrowserLang(): string | undefined { function getBrowserLang(): string | undefined {
if (typeof window === "undefined") return undefined; if (typeof window === "undefined") return undefined;
@ -23,7 +27,9 @@ function getBrowserLang(): string | undefined {
return undefined; return undefined;
} }
export function useLang(initial?: string): Required<LocalStorageState> { const langPreferenceKey = buildStorageKey("lang-preference");
export function useLang(initial?: string): Required<StorageState> {
const defaultValue = (getBrowserLang() || initial || "en").substring(0, 2); const defaultValue = (getBrowserLang() || initial || "en").substring(0, 2);
return useLocalStorage("lang-preference", { defaultValue: defaultValue }); return useLocalStorage(langPreferenceKey, defaultValue);
} }

View File

@ -19,7 +19,7 @@
* @author Sebastian Javier Marchano (sebasjm) * @author Sebastian Javier Marchano (sebasjm)
*/ */
import { Codec } from "@gnu-taler/taler-util"; import { Codec, codecForString } from "@gnu-taler/taler-util";
import { useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
import { import {
ObservableMap, ObservableMap,
@ -28,7 +28,26 @@ import {
memoryMap, memoryMap,
} from "../utils/observable.js"; } from "../utils/observable.js";
export interface LocalStorageState<Type = string> { declare const opaque_StorageKey: unique symbol;
export type StorageKey<Key> = {
id: string;
[opaque_StorageKey]: true;
codec: Codec<Key>;
};
export function buildStorageKey<Key = string>(
name: string,
codec?: Codec<Key>,
): StorageKey<Key> {
return {
id: name,
codec: codec ?? (codecForString() as Codec<Key>),
[opaque_StorageKey]: true,
};
}
export interface StorageState<Type = string> {
value?: Type; value?: Type;
update: (s: Type) => void; update: (s: Type) => void;
reset: () => void; reset: () => void;
@ -50,59 +69,48 @@ const storage: ObservableMap<string, string> = (function buildStorage() {
//with initial value //with initial value
export function useLocalStorage<Type = string>( export function useLocalStorage<Type = string>(
key: string, key: StorageKey<Type>,
options?: { defaultValue: Type,
defaultValue: Type; ): Required<StorageState<Type>>;
codec?: Codec<Type>;
},
): Required<LocalStorageState<Type>>;
//without initial value //without initial value
export function useLocalStorage<Type = string>( export function useLocalStorage<Type = string>(
key: string, key: StorageKey<Type>,
options?: { ): StorageState<Type>;
codec?: Codec<Type>;
},
): LocalStorageState<Type>;
// impl // impl
export function useLocalStorage<Type = string>( export function useLocalStorage<Type = string>(
key: string, key: StorageKey<Type>,
options?: { defaultValue?: Type,
defaultValue?: Type; ): StorageState<Type> {
codec?: Codec<Type>;
},
): LocalStorageState<Type> {
function convert(updated: string | undefined): Type | undefined { function convert(updated: string | undefined): Type | undefined {
if (updated === undefined) return options?.defaultValue; //optional if (updated === undefined) return defaultValue; //optional
try { try {
return !options?.codec return key.codec.decode(JSON.parse(updated));
? (updated as Type)
: options.codec.decode(JSON.parse(updated));
} catch (e) { } catch (e) {
//decode error //decode error
return options?.defaultValue; return defaultValue;
} }
} }
const [storedValue, setStoredValue] = useState<Type | undefined>( const [storedValue, setStoredValue] = useState<Type | undefined>(
(): Type | undefined => { (): Type | undefined => {
const prev = storage.get(key); const prev = storage.get(key.id);
return convert(prev); return convert(prev);
}, },
); );
useEffect(() => { useEffect(() => {
return storage.onUpdate(key, () => { return storage.onUpdate(key.id, () => {
const newValue = storage.get(key); const newValue = storage.get(key.id);
setStoredValue(convert(newValue)); setStoredValue(convert(newValue));
}); });
}, []); }, []);
const setValue = (value?: Type): void => { const setValue = (value?: Type): void => {
if (value === undefined) { if (value === undefined) {
storage.delete(key); storage.delete(key.id);
} else { } else {
storage.set( storage.set(
key, key.id,
options?.codec ? JSON.stringify(value) : (value as string), key.codec ? JSON.stringify(value) : (value as string),
); );
} }
}; };
@ -111,7 +119,7 @@ export function useLocalStorage<Type = string>(
value: storedValue, value: storedValue,
update: setValue, update: setValue,
reset: () => { reset: () => {
setValue(options?.defaultValue); setValue(defaultValue);
}, },
}; };
} }

View File

@ -20,48 +20,44 @@
*/ */
import { useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
import { import { ObservableMap, memoryMap } from "../utils/observable.js";
ObservableMap, import { StorageKey, StorageState } from "./useLocalStorage.js";
browserStorageMap,
localStorageMap,
memoryMap,
} from "../utils/observable.js";
export interface LocalStorageState { const storage: ObservableMap<string, any> = memoryMap<any>();
value?: string;
update: (s: string) => void;
reset: () => void;
}
const storage: ObservableMap<string, string> = memoryMap<string>(); //with initial value
export function useMemoryStorage<Type = string>(
export function useMemoryStorage( key: StorageKey<Type>,
key: string, defaultValue: Type,
initialValue: string, ): Required<StorageState<Type>>;
): Required<LocalStorageState>; //with initial value
export function useMemoryStorage(key: string): LocalStorageState; export function useMemoryStorage<Type = string>(
export function useMemoryStorage( key: StorageKey<Type>,
key: string, ): StorageState<Type>;
initialValue?: string, // impl
): LocalStorageState { export function useMemoryStorage<Type = string>(
const [storedValue, setStoredValue] = useState<string | undefined>( key: StorageKey<Type>,
(): string | undefined => { defaultValue?: Type,
return storage.get(key) ?? initialValue; ): StorageState<Type> {
const [storedValue, setStoredValue] = useState<Type | undefined>(
(): Type | undefined => {
const prev = storage.get(key.id);
return prev;
}, },
); );
useEffect(() => { useEffect(() => {
return storage.onUpdate(key, () => { return storage.onUpdate(key.id, () => {
const newValue = storage.get(key); const newValue = storage.get(key.id);
setStoredValue(newValue ?? initialValue); setStoredValue(newValue);
}); });
}, []); }, []);
const setValue = (value?: string): void => { const setValue = (value?: Type): void => {
if (value === undefined) { if (value === undefined) {
storage.delete(key); storage.delete(key.id);
} else { } else {
storage.set(key, value); storage.set(key.id, value);
} }
}; };
@ -69,7 +65,7 @@ export function useMemoryStorage(
value: storedValue, value: storedValue,
update: setValue, update: setValue,
reset: () => { reset: () => {
setValue(initialValue); setValue(defaultValue);
}, },
}; };
} }