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/>
*/
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 {
Codec,
buildCodecForObject,
codecForBoolean,
} from "@gnu-taler/taler-util";
function parse_json_or_undefined<T>(str: string | undefined): T | 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(): [
Readonly<Settings>,
<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]) {
const newValue = { ...parsed, [k]: v };
const json = JSON.stringify(newValue);
update(json);
update({ ...parsed, [k]: v });
}
return [parsed, updateField];
}

View File

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

View File

@ -14,7 +14,11 @@
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 {
if (typeof window === "undefined") return undefined;
@ -23,7 +27,9 @@ function getBrowserLang(): string | 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);
return useLocalStorage("lang-preference", { defaultValue: defaultValue });
return useLocalStorage(langPreferenceKey, defaultValue);
}

View File

@ -19,7 +19,7 @@
* @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 {
ObservableMap,
@ -28,7 +28,26 @@ import {
memoryMap,
} 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;
update: (s: Type) => void;
reset: () => void;
@ -50,59 +69,48 @@ const storage: ObservableMap<string, string> = (function buildStorage() {
//with initial value
export function useLocalStorage<Type = string>(
key: string,
options?: {
defaultValue: Type;
codec?: Codec<Type>;
},
): Required<LocalStorageState<Type>>;
key: StorageKey<Type>,
defaultValue: Type,
): Required<StorageState<Type>>;
//without initial value
export function useLocalStorage<Type = string>(
key: string,
options?: {
codec?: Codec<Type>;
},
): LocalStorageState<Type>;
key: StorageKey<Type>,
): StorageState<Type>;
// impl
export function useLocalStorage<Type = string>(
key: string,
options?: {
defaultValue?: Type;
codec?: Codec<Type>;
},
): LocalStorageState<Type> {
key: StorageKey<Type>,
defaultValue?: Type,
): StorageState<Type> {
function convert(updated: string | undefined): Type | undefined {
if (updated === undefined) return options?.defaultValue; //optional
if (updated === undefined) return defaultValue; //optional
try {
return !options?.codec
? (updated as Type)
: options.codec.decode(JSON.parse(updated));
return key.codec.decode(JSON.parse(updated));
} catch (e) {
//decode error
return options?.defaultValue;
return defaultValue;
}
}
const [storedValue, setStoredValue] = useState<Type | undefined>(
(): Type | undefined => {
const prev = storage.get(key);
const prev = storage.get(key.id);
return convert(prev);
},
);
useEffect(() => {
return storage.onUpdate(key, () => {
const newValue = storage.get(key);
return storage.onUpdate(key.id, () => {
const newValue = storage.get(key.id);
setStoredValue(convert(newValue));
});
}, []);
const setValue = (value?: Type): void => {
if (value === undefined) {
storage.delete(key);
storage.delete(key.id);
} else {
storage.set(
key,
options?.codec ? JSON.stringify(value) : (value as string),
key.id,
key.codec ? JSON.stringify(value) : (value as string),
);
}
};
@ -111,7 +119,7 @@ export function useLocalStorage<Type = string>(
value: storedValue,
update: setValue,
reset: () => {
setValue(options?.defaultValue);
setValue(defaultValue);
},
};
}

View File

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