From b2a54d2f578c7fdcaf5504292aa38b3447ca2495 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Loi=CC=88c=20Correnson?= <loic.correnson@cea.fr> Date: Wed, 10 Jun 2020 08:52:03 +0200 Subject: [PATCH] [dome] full featured settings API --- ivette/src/dome/src/renderer/data/json.ts | 14 ++-- ivette/src/dome/src/renderer/data/states.ts | 88 ++++++++++++++++++--- 2 files changed, 84 insertions(+), 18 deletions(-) diff --git a/ivette/src/dome/src/renderer/data/json.ts b/ivette/src/dome/src/renderer/data/json.ts index 666a1ba172c..2e28501d599 100644 --- a/ivette/src/dome/src/renderer/data/json.ts +++ b/ivette/src/dome/src/renderer/data/json.ts @@ -50,23 +50,21 @@ export function pretty(js: any) { // --- SAFE Decoder // -------------------------------------------------------------------------- -/** Decoder for values of type `D`. */ +/** Decoder for values of type `D`. + You can abbreviate `Safe<D | undefined>` with `Loose<D>`. */ export type Safe<D> = (js?: json) => D; -/** - Decode for values of type `D`, if any. - Same as `Safe<D | undefined>`. -*/ +/** Decoder for values of type `D`, if any. + Same as `Safe<D | undefined>`. */ export type Loose<D> = (js?: json) => D | undefined; /** Encoder for value of type `D`. + In most cases, you only need [[identity]]. */ export type Encoder<D> = (v: D) => json; -/** - Can be used for any encoder / decoder function. - */ +/** Can be used for most encoders. */ export function identity<A>(v: A): A { return v; }; // -------------------------------------------------------------------------- diff --git a/ivette/src/dome/src/renderer/data/states.ts b/ivette/src/dome/src/renderer/data/states.ts index b530d1ce269..0fa2e25f110 100644 --- a/ivette/src/dome/src/renderer/data/states.ts +++ b/ivette/src/dome/src/renderer/data/states.ts @@ -113,6 +113,34 @@ export class StateOpt<A extends NonFunction> extends StateDef<undefined | A> { // --- Settings // -------------------------------------------------------------------------- +/** + Generic interface to Window and Global Settings. + To be used with [[useSettings]] with instances of its derived classes, + typically [[WindowSettings]] and [[GlobalSettings]]. You should never have + to implement a Settings class on your own. + + All setting values are identified with + an untyped `dataKey: string`, that can be dynamically modified + for each component. Hence, two components might share both datakeys + and settings. + + When several components share the same setting `dataKey` the behavior will be + different depending on the situation: + - for Window Settings, each component in each window retains its own + setting value, although the last modified value from _any_ of them will be saved + and used for any further initial value; + - for Global Settings, all components synchronize to the last modified value + from any component of any window. + + Type safety is ensured by safe JSON encoders and decoders, however, they + might fail at runtime, causing settings value to be initialized to their + fallback and not to be saved or synchronized. This is not harmfull but annoying. + + To mitigate this effect, each instance of a Settings class has its + own, private, unique symbol that we call its « role ». A given `dataKey` + shall always be used with the same « role » otherwized it is discarded, + and an error message is logged when in DEVEL mode. + */ abstract class Settings<A> { private static keyRoles = new Map<string, symbol>(); @@ -121,43 +149,71 @@ abstract class Settings<A> { protected readonly decoder: JSON.Safe<A>; protected readonly encoder: JSON.Encoder<A>; + /** + @param role - Debugging name of instance roles (each instance has its unique role, though) + @param decoder - JSON decoder for the setting values + @param encoder - JSON encoder for the setting values + */ constructor(role: string, decoder: JSON.Safe<A>, encoder: JSON.Encoder<A>) { this.role = Symbol(role); this.decoder = decoder; this.encoder = encoder; } - validateKey(k?: string): string | undefined { - if (k === undefined) return undefined; + /** + Returns identity if the data key is only + used with the same setting instance. + Otherwise, returns `undefined`. + */ + validateKey(dataKey?: string): string | undefined { + if (dataKey === undefined) return undefined; const rq = this.role; - const rk = Settings.keyRoles.get(k); + const rk = Settings.keyRoles.get(dataKey); if (rk === undefined) { - Settings.keyRoles.set(k, rq); + Settings.keyRoles.set(dataKey, rq); } else { if (rk !== rq) { if (DEVEL) console.error( - `[Dome.settings] key ${k} used with incompatible roles`, rk, rq, + `[Dome.settings] key ${dataKey} used with incompatible roles`, rk, rq, ); return undefined; } } - return k; + return dataKey; } + /** @internal */ abstract loadData(key: string): JSON.json; + + /** @internal */ abstract saveData(key: string, data: JSON.json): void; + + /** @internal */ abstract event: symbol; - loadValue(key?: string) { - return this.decoder(key ? this.loadData(key) : undefined) + /** Returns the current setting value for the provided data key. You shall + only use validated keys otherwise you might fallback to default values. */ + loadValue(dataKey?: string) { + return this.decoder(dataKey ? this.loadData(dataKey) : undefined) } - saveValue(key: string, value: A) { - this.saveData(key, this.encoder(value)); + /** Push the new setting value for the provided data key. + You only use validated keys otherwise further loads + might fail and fallback to defaults. */ + saveValue(dataKey: string, value: A) { + this.saveData(dataKey, this.encoder(value)); } } +/** + Generic React Hook to be used with any kind of [[Settings]]. + You may share `dataKey` between components, or change it dynamically. + However, a given data key shall always be used for the same Setting instance. + See [[Settings]] documentation for details. + @param S - the instance settings to be used + @param dataKey - identifies which value in the settings to be used + */ export function useSettings<A>( S: Settings<A>, dataKey?: string, @@ -186,6 +242,9 @@ export function useSettings<A>( } +/** Window Settings for non-JSON data. + In most situations, you can use [[WindowSettings]] instead. + You can use a [[JSON.Loose]] decoder for optional values. */ export class WindowSettingsData<A> extends Settings<A> { constructor(role: string, decoder: JSON.Safe<A>, encoder: JSON.Encoder<A>) { @@ -198,6 +257,9 @@ export class WindowSettingsData<A> extends Settings<A> { } +/** Global Settings for non-JSON data. + In most situations, you can use [[WindowSettings]] instead. + You can use a [[JSON.Loose]] decoder for optional values. */ export class GlobalSettingsData<A> extends Settings<A> { constructor(role: string, decoder: JSON.Safe<A>, encoder: JSON.Encoder<A>) { @@ -210,6 +272,9 @@ export class GlobalSettingsData<A> extends Settings<A> { } +/** Window Settings. + For non-JSON data, use [[WindowSettingsdata]] instead. + You can use a [[JSON.Loose]] decoder for optional values. */ export class WindowSettings<A extends JSON.json> extends WindowSettingsData<A> { constructor(role: string, decoder: JSON.Safe<A>) { @@ -218,6 +283,9 @@ export class WindowSettings<A extends JSON.json> extends WindowSettingsData<A> { } +/** Global Settings. + For non-JSON data, use [[WindowSettingsdata]] instead. + You can use a [[JSON.Loose]] decoder for optional values. */ export class GlobalSettings<A extends JSON.json> extends GlobalSettingsData<A> { constructor(role: string, decoder: JSON.Safe<A>) { -- GitLab