From 126514e242aa76551659d6daf9f47504eb7483ed Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Loi=CC=88c=20Correnson?= <loic.correnson@cea.fr>
Date: Mon, 8 Jun 2020 23:01:36 +0200
Subject: [PATCH] [dome] typed states

---
 ivette/src/dome/src/renderer/data/json.ts   |  13 ++-
 ivette/src/dome/src/renderer/data/states.ts | 104 ++++++++++++++++++++
 2 files changed, 113 insertions(+), 4 deletions(-)
 create mode 100644 ivette/src/dome/src/renderer/data/states.ts

diff --git a/ivette/src/dome/src/renderer/data/json.ts b/ivette/src/dome/src/renderer/data/json.ts
index e4d36b7cde7..e4b5e385b49 100644
--- a/ivette/src/dome/src/renderer/data/json.ts
+++ b/ivette/src/dome/src/renderer/data/json.ts
@@ -50,13 +50,13 @@ export function pretty(js: any) {
 // --------------------------------------------------------------------------
 
 /** Decoder for values of type `D`. */
-export type Safe<D> = (js: json) => D;
+export type Safe<D> = (js?: json) => D;
 
 /**
    Decode for values of type `D`, if any.
    Same as `Safe<D | undefined>`.
 */
-export type Loose<D> = (js: json) => D | undefined;
+export type Loose<D> = (js?: json) => D | undefined;
 
 // --------------------------------------------------------------------------
 // --- Primitives
@@ -64,12 +64,17 @@ export type Loose<D> = (js: json) => D | undefined;
 
 /** Primitive JSON number or `undefined`. */
 export const jNumber: Loose<number> = (js: json) => (
-  typeof js === 'number' ? js : undefined
+  typeof js === 'number' && !Number.isNaN(js) ? js : undefined
+);
+
+/** Primitive JSON number, rounded to integer, or `undefined`. */
+export const jInt: Loose<number> = (js: json) => (
+  typeof js === 'number' && !Number.isNaN(js) ? Math.round(js) : undefined
 );
 
 /** Primitive JSON number or `0`. */
 export const jZero: Safe<number> = (js: json) => (
-  typeof js === 'number' ? js : 0
+  typeof js === 'number' && !Number.isNaN(js) ? js : 0
 );
 
 /** Primitive JSON boolean or `undefined`. */
diff --git a/ivette/src/dome/src/renderer/data/states.ts b/ivette/src/dome/src/renderer/data/states.ts
new file mode 100644
index 00000000000..93b200ecc12
--- /dev/null
+++ b/ivette/src/dome/src/renderer/data/states.ts
@@ -0,0 +1,104 @@
+// --------------------------------------------------------------------------
+// --- States
+// --------------------------------------------------------------------------
+
+/**
+   Typed States & Settings
+   @package dome/data/states
+*/
+
+import React from 'react';
+import isEqual from 'react-fast-compare';
+import * as Dome from 'dome';
+
+export type NonFunction =
+  undefined | null | boolean | number | string | object | any[] | bigint | symbol;
+
+/** State updater. `undefined` is no-op, `null` is reset, new value,
+    or updating function applied to the current, lastly updated value. */
+export type updateAction<A extends NonFunction> =
+  undefined | null | A | ((current: A) => A);
+
+/** The type of updater callbacks. Typically used for `[A,setState<A>]` hooks. */
+export type setState<A extends NonFunction> = (action: updateAction<A>) => void;
+
+/** Base state interface. */
+export interface State<A extends NonFunction> {
+  readonly get: () => A;
+  readonly set: (value: A) => void;
+  readonly update: setState<A>;
+  on(callback: (value: A) => void): void;
+  off(callback: (value: A) => void): void;
+}
+
+/** React Hook, similar to `React.useState()`. */
+export function useState<A extends NonFunction>(s: State<A>): [A, setState<A>] {
+  const [current, setCurrent] = React.useState<A>(s.get);
+  React.useEffect(() => {
+    s.on(setCurrent);
+    return () => s.off(setCurrent);
+  });
+  return [current, s.update];
+};
+
+/**
+   State with initial default value.
+ */
+export class StateDef<A extends NonFunction> implements State<A> {
+  protected value: A;
+  protected defaultValue: A;
+  protected event: symbol;
+
+  constructor(defaultValue: A) {
+    this.value = this.defaultValue = defaultValue;
+    this.event = Symbol('dome.state');
+    this.get = this.get.bind(this);
+    this.set = this.get.bind(this);
+    this.reset = this.reset.bind(this);
+    this.update = this.update.bind(this);
+  }
+
+  get(): A { return this.value; }
+
+  /** Notify callbacks on change, using _deep_ structural comparison. */
+  set(value: A) {
+    if (!isEqual(value, this.value)) {
+      this.value = value;
+      Dome.emit(this.event, value);
+    }
+  }
+
+  /** State updater. */
+  update(upd: updateAction<A>) {
+    if (upd === undefined) return;
+    if (upd === null) { this.reset(); return; }
+    if (typeof upd === 'function') this.set(upd(this.value));
+  }
+
+  /** Restore default value. */
+  reset() {
+    this.set(this.defaultValue);
+  }
+
+  /** Callback Emitter. */
+  on(callback: (value: A) => void) {
+    Dome.emitter.on(this.event, callback);
+  }
+
+  /** Callback Emitter. */
+  off(callback: (value: A) => void) {
+    Dome.emitter.off(this.event, callback);
+  }
+
+}
+
+/**
+   State with possibly undefined initial value.
+ */
+export class StateOpt<A extends NonFunction> extends StateDef<undefined | A> {
+  constructor(defaultValue?: A) {
+    super(defaultValue);
+  }
+}
+
+// --------------------------------------------------------------------------
-- 
GitLab