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