From 9a39041209d2a8eb16062377b49111e45dc2efae Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Loi=CC=88c=20Correnson?= <loic.correnson@cea.fr>
Date: Mon, 5 Dec 2022 15:09:15 +0100
Subject: [PATCH] [states] refactor states with global states

---
 ivette/src/dome/renderer/data/states.ts |   4 +-
 ivette/src/frama-c/states.ts            | 164 ++++++++++--------------
 2 files changed, 69 insertions(+), 99 deletions(-)

diff --git a/ivette/src/dome/renderer/data/states.ts b/ivette/src/dome/renderer/data/states.ts
index cb2aba5085d..a30de7bf0b8 100644
--- a/ivette/src/dome/renderer/data/states.ts
+++ b/ivette/src/dome/renderer/data/states.ts
@@ -100,6 +100,7 @@ export class GlobalState<A> {
   constructor(initValue: A) {
     this.value = initValue;
     this.emitter = new Emitter();
+    this.emitter.setMaxListeners(200);
     this.getValue = this.getValue.bind(this);
     this.setValue = this.setValue.bind(this);
   }
@@ -108,7 +109,8 @@ export class GlobalState<A> {
   getValue(): A { return this.value; }
 
   /** Notify callbacks on change. By default, changed are detected
-      by using _deep_ structural comparison, using `react-fast-compare` comparison.
+      by using _deep_ structural comparison, using `react-fast-compare`
+      comparison.
       @param value the new value of the state
       @param forced when set to `true`, notify callbacks without comparison.
   */
diff --git a/ivette/src/frama-c/states.ts b/ivette/src/frama-c/states.ts
index b882635b268..9497b6a18c5 100644
--- a/ivette/src/frama-c/states.ts
+++ b/ivette/src/frama-c/states.ts
@@ -32,20 +32,15 @@
 
 import React from 'react';
 import * as Dome from 'dome';
-import * as Json from 'dome/data/json';
 import { Order } from 'dome/data/compare';
 import { GlobalState, useGlobalState } from 'dome/data/states';
 import { Client, useModel } from 'dome/table/models';
 import { CompactModel } from 'dome/table/arrays';
+import { getCurrent, setCurrent } from 'frama-c/kernel/api/project';
 import * as Ast from 'frama-c/kernel/api/ast';
 import * as Server from './server';
 
-const PROJECT = new Dome.Event('frama-c.project');
-class STATE extends Dome.Event {
-  constructor(id: string) {
-    super(`frama-c.state.${id}`);
-  }
-}
+const CurrentProject = new GlobalState<string>('default');
 
 // --------------------------------------------------------------------------
 // --- Pretty Printing (Browser Console)
@@ -57,28 +52,18 @@ const D = new Dome.Debug('States');
 // --- Synchronized Current Project
 // --------------------------------------------------------------------------
 
-let currentProject: string | undefined;
-
 Server.onReady(async () => {
   try {
-    const sr: Server.GetRequest<null, { id?: string }> = {
-      kind: Server.RqKind.GET,
-      name: 'kernel.project.getCurrent',
-      input: Json.jNull,
-      output: Json.jObject({ id: Json.jString }),
-      signals: [],
-    };
-    const current: { id?: string } = await Server.send(sr, null);
-    currentProject = current.id;
-    PROJECT.emit();
+    CurrentProject.setValue('default');
+    const { id } = await Server.send(getCurrent, null);
+    CurrentProject.setValue(id);
   } catch (error) {
     D.error(`Fail to retrieve the current project. ${error}`);
   }
 });
 
 Server.onShutdown(() => {
-  currentProject = '';
-  PROJECT.emit();
+  CurrentProject.setValue('default');
 });
 
 // --------------------------------------------------------------------------
@@ -89,9 +74,9 @@ Server.onShutdown(() => {
  * Current Project (Custom React Hook).
  * @return The current project.
  */
-export function useProject(): string | undefined {
-  Dome.useUpdate(PROJECT);
-  return currentProject;
+export function useProject(): string {
+  const [s] = useGlobalState(CurrentProject);
+  return s;
 }
 
 /**
@@ -105,16 +90,9 @@ export function useProject(): string | undefined {
 export async function setProject(project: string): Promise<void> {
   if (Server.isRunning()) {
     try {
-      const sr: Server.SetRequest<string, null> = {
-        kind: Server.RqKind.SET,
-        name: 'kernel.project.setCurrent',
-        input: Json.jString,
-        output: Json.jNull,
-        signals: [],
-      };
-      await Server.send(sr, project);
-      currentProject = project;
-      PROJECT.emit();
+      await Server.send(setCurrent, project);
+      const { id } = await Server.send(getCurrent, null);
+      CurrentProject.setValue(id);
     } catch (error) {
       D.error(`Fail to set the current project. ${error}`);
     }
@@ -222,7 +200,7 @@ export function useTags(rq: GetTags): Map<string, Tag> {
 }
 
 // --------------------------------------------------------------------------
-// --- Synchronized States
+// --- Synchronized States from API
 // --------------------------------------------------------------------------
 
 export interface Value<A> {
@@ -255,7 +233,7 @@ export interface Array<K, A> {
 }
 
 // --------------------------------------------------------------------------
-// --- Handler for Synchronized St byates
+// --- Handler for Synchronized States
 // --------------------------------------------------------------------------
 
 interface Handler<A> {
@@ -265,68 +243,50 @@ interface Handler<A> {
   setter?: Server.SetRequest<A, null>;
 }
 
-// shared for all projects
-class SyncState<A> {
-  UPDATE: Dome.Event;
+enum SyncStatus { OffLine, Loading, Loaded }
+
+class SyncState<A> extends GlobalState<A | undefined> {
   handler: Handler<A>;
-  upToDate: boolean;
-  value?: A;
+  status = SyncStatus.OffLine;
 
   constructor(h: Handler<A>) {
+    super(undefined);
     this.handler = h;
-    this.UPDATE = new STATE(h.name);
-    this.upToDate = false;
-    this.value = undefined;
-    this.update = this.update.bind(this);
-    this.getValue = this.getValue.bind(this);
-    this.setValue = this.setValue.bind(this);
-    PROJECT.on(this.update);
+    this.load = this.load.bind(this);
+    this.fetch = this.fetch.bind(this);
+    this.offline = this.offline.bind(this);
+    Server.onReady(this.load);
+    Server.onShutdown(this.offline);
   }
 
-  getValue(): A | undefined {
-    const running = Server.isRunning();
-    if (!this.upToDate && running) {
-      this.update();
-    }
-    return running ? this.value : undefined;
+  signal(): Server.Signal { return this.handler.signal; }
+
+  load(): void {
+    if (this.status === SyncStatus.OffLine) this.fetch();
   }
 
-  async setValue(v: A): Promise<void> {
-    try {
-      this.upToDate = true;
-      this.value = v;
-      const setter = this.handler.getter;
-      if (setter) await Server.send(setter, v);
-      this.UPDATE.emit();
-    } catch (error) {
-      D.error(
-        `Fail to set value of SyncState '${this.handler.name}'.`,
-        `${error}`,
-      );
-      this.UPDATE.emit();
-    }
+  offline(): void {
+    this.status = SyncStatus.OffLine;
+    this.setValue(undefined);
   }
 
-  async update(): Promise<void> {
+  async fetch(): Promise<void> {
     try {
-      this.upToDate = true;
       if (Server.isRunning()) {
+        this.status = SyncStatus.Loading;
         const v = await Server.send(this.handler.getter, null);
-        this.value = v;
-        this.UPDATE.emit();
-      } else if (this.value !== undefined) {
-        this.value = undefined;
-        this.UPDATE.emit();
+        this.status = SyncStatus.Loaded;
+        this.setValue(v);
       }
     } catch (error) {
       D.error(
         `Fail to update SyncState '${this.handler.name}'.`,
         `${error}`,
       );
-      this.value = undefined;
-      this.UPDATE.emit();
+      this.setValue(undefined);
     }
   }
+
 }
 
 // --------------------------------------------------------------------------
@@ -337,7 +297,10 @@ const syncStates = new Map<string, SyncState<unknown>>();
 
 // Remark: use current project state
 
-function currentSyncState<A>(h: Handler<A>): SyncState<A> {
+function lookupSyncState<A>(
+  currentProject: string,
+  h: Handler<A>
+): SyncState<A> {
   const id = `${currentProject}@${h.name}`;
   let s = syncStates.get(id) as SyncState<A> | undefined;
   if (!s) {
@@ -355,20 +318,25 @@ Server.onShutdown(() => syncStates.clear());
 
 /** Synchronization with a (projectified) server state. */
 export function useSyncState<A>(
-  st: State<A>,
+  state: State<A>,
 ): [A | undefined, (value: A) => void] {
-  const s = currentSyncState(st);
-  Dome.useUpdate(PROJECT, s.UPDATE);
-  Server.useSignal(s.handler.signal, s.update);
-  return [s.getValue(), s.setValue];
+  Server.useStatus();
+  const pr = useProject();
+  const st = lookupSyncState(pr, state);
+  Server.useSignal(st.signal(), st.fetch);
+  st.load();
+  return useGlobalState(st);
 }
 
 /** Synchronization with a (projectified) server value. */
-export function useSyncValue<A>(va: Value<A>): A | undefined {
-  const s = currentSyncState(va);
-  Dome.useUpdate(PROJECT, s.UPDATE);
-  Server.useSignal(s.handler.signal, s.update);
-  return s.getValue();
+export function useSyncValue<A>(value: Value<A>): A | undefined {
+  Server.useStatus();
+  const pr = useProject();
+  const st = lookupSyncState(pr, value);
+  Server.useSignal(st.signal(), st.fetch);
+  st.load();
+  const [v] = useGlobalState(st);
+  return v;
 }
 
 // --------------------------------------------------------------------------
@@ -395,7 +363,6 @@ class SyncArray<K, A> {
   update(): void {
     if (
       !this.upToDate &&
-      currentProject !== undefined &&
       Server.isRunning()
     ) this.fetch();
   }
@@ -403,7 +370,6 @@ class SyncArray<K, A> {
   async fetch(): Promise<void> {
     if (
       this.fetching ||
-      currentProject === undefined ||
       !Server.isRunning()
     ) return;
     try {
@@ -460,6 +426,7 @@ const syncArrays = new Map<string, SyncArray<unknown, unknown>>();
 // Remark: lookup for current project
 
 function currentSyncArray<K, A>(
+  currentProject: string,
   array: Array<K, A>,
 ): SyncArray<K, A> {
   const id = `${currentProject}@${array.name}`;
@@ -479,7 +446,7 @@ Server.onShutdown(() => syncArrays.clear());
 
 /** Force a Synchronized Array to reload. */
 export function reloadArray<K, A>(arr: Array<K, A>): void {
-  currentSyncArray(arr).reload();
+  currentSyncArray(CurrentProject.getValue(), arr).reload();
 }
 
 /**
@@ -495,8 +462,9 @@ export function useSyncArray<K, A>(
   arr: Array<K, A>,
   sync = true,
 ): CompactModel<K, A> {
-  Dome.useUpdate(PROJECT);
-  const st = currentSyncArray(arr);
+  Server.useStatus();
+  const pr = useProject();
+  const st = currentSyncArray(pr, arr);
   Server.useSignal(arr.signal, st.fetch);
   st.update();
   useModel(st.model, sync);
@@ -509,7 +477,8 @@ export function useSyncArray<K, A>(
 export function getSyncArray<K, A>(
   arr: Array<K, A>,
 ): CompactModel<K, A> {
-  const st = currentSyncArray(arr);
+  const pr = CurrentProject.getValue();
+  const st = currentSyncArray(pr, arr);
   return st.model;
 }
 
@@ -523,7 +492,8 @@ export function onSyncArray<K, A>(
   onReload?: () => void,
   onUpdate?: () => void,
 ): Client {
-  const st = currentSyncArray(arr);
+  const pr = CurrentProject.getValue();
+  const st = currentSyncArray(pr, arr);
   return st.model.link(onReload, onUpdate);
 }
 
@@ -822,9 +792,7 @@ export async function resetSelection(): Promise<void> {
   }
 }
 
-/* Select the main function when the current project changes and the selection
-   is still empty (which happens at the start of the GUI). */
-PROJECT.on(async () => {
+Server.onReady(() => {
   if (GlobalSelection.getValue() === emptySelection)
     resetSelection();
 });
-- 
GitLab