From c5d1bf865e54db15dff77d1fb2dd0d8793d152a6 Mon Sep 17 00:00:00 2001
From: Leonardo Christino <leomilho@gmail.com>
Date: Sat, 2 Mar 2024 20:36:05 +0100
Subject: [PATCH] feat: update deps, add sync ws api and fixes

---
 apps/web/package.json                         |   1 +
 apps/web/src/app/app.tsx                      |  50 +++---
 .../dbConnectionSelector.tsx                  |   5 +-
 .../DatabaseManagement/forms/settings.tsx     |  89 ++++------
 apps/web/tsconfig.json                        |   3 +-
 libs/shared/lib/data-access/api/eventBus.tsx  | 146 +++++++++-------
 libs/shared/lib/data-access/broker/broker.tsx |  61 ++++---
 libs/shared/lib/data-access/broker/index.ts   |   1 +
 libs/shared/lib/data-access/broker/types.ts   |   2 +
 libs/shared/lib/data-access/broker/wsQuery.ts |  18 ++
 .../shared/lib/data-access/broker/wsSchema.ts |   8 +
 .../shared/lib/data-access/broker/wsState.tsx | 157 ++++++++++++------
 .../lib/data-access/store/sessionSlice.ts     |   6 +
 libs/shared/package.json                      |   1 +
 libs/shared/tsconfig.json                     |   3 +-
 pnpm-lock.yaml                                |   7 +-
 16 files changed, 346 insertions(+), 212 deletions(-)

diff --git a/apps/web/package.json b/apps/web/package.json
index 10a840e83..739dd1408 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -50,6 +50,7 @@
     "react-is": "^18.2.0",
     "redux": "^5.0.1",
     "redux-thunk": "^3.1.0",
+    "reselect": "^5.1.0",
     "tailwindcss": "^3.4.1",
     "typescript": "^5.3.3",
     "vite": "^5.1.4",
diff --git a/apps/web/src/app/app.tsx b/apps/web/src/app/app.tsx
index 8998b609c..11413ebde 100644
--- a/apps/web/src/app/app.tsx
+++ b/apps/web/src/app/app.tsx
@@ -52,32 +52,36 @@ export function App(props: App) {
           setAuthCheck(true);
         }}
       />
-      <Onboarding />
-      <DashboardAlerts />
-      <div className={'h-screen w-screen ' + (!auth.authorized ? 'blur-sm pointer-events-none ' : '')}>
-        <div className="flex flex-col h-screen max-h-screen relative">
-          <aside className="h-auto w-auto">
-            <Navbar />
-          </aside>
-          <main className="flex w-screen h-[calc(100%-4.2rem)]">
-            <Resizable divisorSize={3} horizontal={true} defaultProportion={0.33}>
-              <div className="h-full w-full panel">
-                <Schema auth={authCheck} />
-              </div>
-              <div className="h-full w-full">
-                <Resizable divisorSize={3} horizontal={false}>
-                  <div className="w-full h-full panel">
-                    <VisualizationPanel />
+      {authCheck && (
+        <>
+          <Onboarding />
+          <DashboardAlerts />
+          <div className={'h-screen w-screen ' + (!auth.authorized ? 'blur-sm pointer-events-none ' : '')}>
+            <div className="flex flex-col h-screen max-h-screen relative">
+              <aside className="h-auto w-auto">
+                <Navbar />
+              </aside>
+              <main className="flex w-screen h-[calc(100%-4.2rem)]">
+                <Resizable divisorSize={3} horizontal={true} defaultProportion={0.33}>
+                  <div className="h-full w-full panel">
+                    <Schema auth={authCheck} />
                   </div>
-                  <div className="w-full h-full panel">
-                    <QueryBuilder onRunQuery={runQuery} />
+                  <div className="h-full w-full">
+                    <Resizable divisorSize={3} horizontal={false}>
+                      <div className="w-full h-full panel">
+                        <VisualizationPanel />
+                      </div>
+                      <div className="w-full h-full panel">
+                        <QueryBuilder onRunQuery={runQuery} />
+                      </div>
+                    </Resizable>
                   </div>
                 </Resizable>
-              </div>
-            </Resizable>
-          </main>
-        </div>
-      </div>
+              </main>
+            </div>
+          </div>
+        </>
+      )}
     </div>
   );
 }
diff --git a/apps/web/src/components/navbar/DatabaseManagement/dbConnectionSelector.tsx b/apps/web/src/components/navbar/DatabaseManagement/dbConnectionSelector.tsx
index 6ca365292..e15109c95 100644
--- a/apps/web/src/components/navbar/DatabaseManagement/dbConnectionSelector.tsx
+++ b/apps/web/src/components/navbar/DatabaseManagement/dbConnectionSelector.tsx
@@ -25,7 +25,10 @@ export default function DatabaseSelector({}) {
 
   useEffect(() => {
     if (
-      (session.saveStates && Object.keys(session.saveStates).length === 0 && settingsMenuOpen === undefined) ||
+      (!session.fetchingSaveStates &&
+        session.saveStates &&
+        Object.keys(session.saveStates).length === 0 &&
+        settingsMenuOpen === undefined) ||
       session.currentSaveState === nilUUID
     ) {
       setSettingsMenuOpen('create');
diff --git a/apps/web/src/components/navbar/DatabaseManagement/forms/settings.tsx b/apps/web/src/components/navbar/DatabaseManagement/forms/settings.tsx
index c9f35f7b4..fe477e31a 100644
--- a/apps/web/src/components/navbar/DatabaseManagement/forms/settings.tsx
+++ b/apps/web/src/components/navbar/DatabaseManagement/forms/settings.tsx
@@ -7,7 +7,6 @@ import {
   wsUpdateState,
   DatabaseStatus,
   wsTestDatabaseConnection,
-  TestDatabaseConnectionResponse,
   wsCreateState,
   nilUUID,
   DatabaseType,
@@ -47,19 +46,32 @@ export const SettingsForm = (props: {
     status: null,
     verified: null,
   });
-  const formDataRef = useRef<SaveStateI | null>(null);
   const formTitle = props.open === 'create' ? 'Create' : 'Update';
 
-  const refImperativeHandles = useRef<any>(null);
-  useImperativeHandle(refImperativeHandles, () => ({
-    processDbTested: (data: TestDatabaseConnectionResponse) => {
-      if (!formDataRef.current) {
-        console.error('formDataRef.current is null');
+  useEffect(() => {
+    if (props.saveState && props.open === 'update') {
+      setFormData(props.saveState);
+      setSampleDataPanel(null);
+    } else {
+      setSampleDataPanel(false);
+    }
+  }, [props.saveState]);
+
+  async function handleSubmit() {
+    setConnection(() => ({
+      updating: true,
+      status: formTitle.slice(0, -1) + 'ing database credentials',
+      verified: null,
+    }));
+
+    wsTestDatabaseConnection(formData.db, (data) => {
+      if (!formData) {
+        console.error('formData is null');
         return;
       }
-      if (formDataRef.current.user_id !== auth.userID && auth.userID) {
-        console.error('formDataRef.current.user_id is not equal to auth.userID');
-        formDataRef.current.user_id = auth.userID;
+      if (formData.user_id !== auth.userID && auth.userID) {
+        console.error('formData.user_id is not equal to auth.userID');
+        formData.user_id = auth.userID;
       }
       if (data && data.status === 'success') {
         setConnection((prevState) => ({
@@ -68,9 +80,17 @@ export const SettingsForm = (props: {
           verified: true,
         }));
         if (props.open === 'create') {
-          wsCreateState(formDataRef.current);
+          wsCreateState(formData, (_data) => {
+            dispatch(addSaveState(_data));
+            dispatch(testedSaveState(_data.id));
+            closeDialog();
+          });
         } else {
-          wsUpdateState(formDataRef.current);
+          dispatch(testedSaveState(data.saveStateID));
+          wsUpdateState(formData, (_data) => {
+            dispatch(addSaveState(_data));
+            closeDialog();
+          });
         }
       } else {
         setConnection((prevState) => ({
@@ -79,50 +99,7 @@ export const SettingsForm = (props: {
           verified: false,
         }));
       }
-    },
-    processStateUpdated: (data: SaveStateI) => {
-      let _data = JSON.parse(JSON.stringify(data));
-      _data.db.status = DatabaseStatus.tested;
-      dispatch(addSaveState(_data));
-      dispatch(testedSaveState(_data.id));
-      formDataRef.current = null;
-      closeDialog();
-    },
-  }));
-
-  useEffect(() => {
-    Broker.instance().subscribe(refImperativeHandles.current.processDbTested, 'tested_connection');
-    Broker.instance().subscribe(refImperativeHandles.current.processStateUpdated, 'updated_save_state');
-    Broker.instance().subscribe(refImperativeHandles.current.processStateUpdated, 'save_state');
-
-    return () => {
-      Broker.instance().unSubscribeAll('tested_connection');
-      Broker.instance().unSubscribeAll('updated_save_state');
-      Broker.instance().unSubscribeAll('save_state');
-    };
-  }, []);
-
-  useEffect(() => {
-    if (props.saveState && props.open === 'update') {
-      setFormData(props.saveState);
-      setSampleDataPanel(null);
-    } else {
-      setSampleDataPanel(false);
-    }
-  }, [props.saveState]);
-
-  useEffect(() => {
-    formDataRef.current = formData;
-  }, [formData]);
-
-  async function handleSubmit() {
-    setConnection(() => ({
-      updating: true,
-      status: formTitle.slice(0, -1) + 'ing database credentials',
-      verified: null,
-    }));
-
-    wsTestDatabaseConnection(formData.db);
+    });
   }
 
   function handlePortChanged(port: string): void {
diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json
index 1fac97092..b3699e023 100644
--- a/apps/web/tsconfig.json
+++ b/apps/web/tsconfig.json
@@ -27,7 +27,8 @@
       "@graphpolaris/config/*": ["../../libs/config/src/*"],
       "redux": ["./node_modules/redux"],
       "@storybook/types": ["./node_modules/@storybook/types"],
-      "redux-thunk": ["./node_modules/redux-thunk"]
+      "redux-thunk": ["./node_modules/redux-thunk"],
+      "reselect": ["./node_modules/reselect"]
     }
   },
   "exclude": ["node_modules", "public", "dist", "build"],
diff --git a/libs/shared/lib/data-access/api/eventBus.tsx b/libs/shared/lib/data-access/api/eventBus.tsx
index b94a648b2..0c7038aa2 100644
--- a/libs/shared/lib/data-access/api/eventBus.tsx
+++ b/libs/shared/lib/data-access/api/eventBus.tsx
@@ -13,8 +13,10 @@ import {
   assignNewGraphQueryResult,
   useQuerybuilder,
   useVisualizationState,
+  wsSchemaRequest,
+  wsSchemaSubscription,
 } from '@graphpolaris/shared/lib/data-access';
-import { Broker } from '@graphpolaris/shared/lib/data-access/broker';
+import { Broker, wsQuerySubscription, wsQueryTranslationSubscription } from '@graphpolaris/shared/lib/data-access/broker';
 import { addInfo } from '@graphpolaris/shared/lib/data-access/store/configSlice';
 import { GraphQueryResultFromBackendPayload, queryingBackend } from '@graphpolaris/shared/lib/data-access/store/graphQueryResultSlice';
 import { allMLTypes, LinkPredictionInstance, setMLResult } from '@graphpolaris/shared/lib/data-access/store/mlSlice';
@@ -23,15 +25,25 @@ import { SchemaFromBackend } from '@graphpolaris/shared/lib/schema';
 import { useEffect } from 'react';
 import {
   SaveStateI,
-  TestDatabaseConnectionResponse,
   wsGetState,
   wsGetStates,
   wsUpdateState,
   wsSelectState,
   nilUUID,
+  wsGetStatesSubscription,
+  wsDeleteStateSubscription,
+  wsGetStateSubscription,
+  wsSelectStateSubscription,
+  wsTestSaveStateConnectionSubscription,
 } from '../broker/wsState';
-import { wsSchemaRequest } from '../broker/wsSchema';
-import { addSaveState, testedSaveState, selectSaveState, updateSaveStateList, updateSelectedSaveState } from '../store/sessionSlice';
+import {
+  addSaveState,
+  testedSaveState,
+  selectSaveState,
+  updateSaveStateList,
+  updateSelectedSaveState,
+  setFetchingSaveStates,
+} from '../store/sessionSlice';
 import { URLParams, getParam, deleteParam } from './url';
 import { setVisualizationState } from '../store/visualizationSlice';
 import { isEqual } from 'lodash-es';
@@ -49,7 +61,7 @@ export const EventBus = (props: { onRunQuery: Function; onAuthorized: Function }
 
   function loadSaveState(saveStateID: string | undefined, saveStates: Record<string, SaveStateI>) {
     if (saveStateID && saveStates && saveStateID in saveStates) {
-      console.debug('Setting state from database', saveStateID, saveStates);
+      console.debug('Setting state fetched from database', saveStateID, saveStates);
       const state = saveStates[saveStateID];
       if (state) {
         dispatch(setQuerybuilderNodes(state.queryBuilder));
@@ -59,73 +71,79 @@ export const EventBus = (props: { onRunQuery: Function; onAuthorized: Function }
   }
 
   useEffect(() => {
-    Broker.instance().subscribe((data: SchemaFromBackend) => {
-      dispatch(readInSchemaFromBackend(data));
-      dispatch(addInfo('Schema graph updated'));
-    }, 'schema_result');
-
-    Broker.instance().subscribe((data: GraphQueryResultFromBackendPayload) => {
-      dispatch(assignNewGraphQueryResult(data));
-      dispatch(addInfo('Query Executed!'));
-    }, 'query_result');
+    const unsubs: Function[] = [];
+
+    unsubs.push(
+      wsSchemaSubscription((data) => {
+        dispatch(readInSchemaFromBackend(data));
+        dispatch(addInfo('Schema graph updated'));
+      }),
+    );
+
+    unsubs.push(
+      wsQuerySubscription((data) => {
+        dispatch(assignNewGraphQueryResult(data));
+        dispatch(addInfo('Query Executed!'));
+      }),
+    );
+
+    unsubs.push(
+      wsTestSaveStateConnectionSubscription((data) => {
+        if (data.status === 'success' && data.saveStateID) dispatch(testedSaveState(data.saveStateID));
+      }),
+    );
 
     // Broker.instance().subscribe((data: QueryBuilderState) => dispatch(setQuerybuilderNodes(data)), 'query_builder_state');
 
     allMLTypes.forEach((mlType) => {
-      Broker.instance().subscribe((data: LinkPredictionInstance[]) => dispatch(setMLResult({ type: mlType, result: data })), mlType);
+      const id = Broker.instance().subscribe(
+        (data: LinkPredictionInstance[]) => dispatch(setMLResult({ type: mlType, result: data })),
+        mlType,
+      );
+      unsubs.push(() => Broker.instance().unSubscribe(mlType, id));
     });
 
-    Broker.instance().subscribe((data: SaveStateI[]) => {
-      console.debug('Save States updated', data);
-      dispatch(updateSaveStateList(data));
-      const d = Object.fromEntries(data.map((x) => [x.id, x]));
-      loadSaveState(session.currentSaveState, d);
-      // useEffect(() => {
-
-      // }, [session.currentSaveState, session.saveStates]);
-    }, 'save_states');
-
-    Broker.instance().subscribe((data: any) => {}, 'save_state_status');
-
-    Broker.instance().subscribe((data: SaveStateI) => {
-      if (data.id !== nilUUID) {
-        dispatch(addSaveState(data));
-        dispatch(selectSaveState(data.id));
-        loadSaveState(data.id, session.saveStates);
-      }
-    }, 'save_state');
-
-    Broker.instance().subscribe((data: SaveStateI) => {}, 'delete_save_state');
-
-    Broker.instance().subscribe((data: { saveStateID: string; success: boolean }) => {}, 'save_state_selected');
-
-    Broker.instance().subscribe((response: TestDatabaseConnectionResponse) => {
-      if (response && response.status === 'success') dispatch(testedSaveState(response.saveStateID));
-    }, 'tested_connection');
-
-    Broker.instance().subscribe((response: QueryBuilderText) => {
-      if (response && response.result) {
-        dispatch(setQueryText(response));
-      }
-    }, 'query_translation_result');
+    unsubs.push(
+      wsGetStatesSubscription((data) => {
+        console.debug('Save States updated', data);
+        dispatch(updateSaveStateList(data));
+        const d = Object.fromEntries(data.map((x) => [x.id, x]));
+        loadSaveState(session.currentSaveState, d);
+      }),
+    );
+
+    unsubs.push(
+      wsGetStateSubscription((data) => {
+        if (data.id !== nilUUID) {
+          dispatch(addSaveState(data));
+          dispatch(selectSaveState(data.id));
+          loadSaveState(data.id, session.saveStates);
+        }
+      }),
+    );
+
+    unsubs.push(wsDeleteStateSubscription((data) => {}));
+    unsubs.push(wsSelectStateSubscription((data) => {}));
+
+    // Broker.instance().subscribe((response: TestDatabaseConnectionResponse) => {
+    //   if (response && response.status === 'success') dispatch(testedSaveState(response.saveStateID));
+    // }, 'tested_connection');
+
+    unsubs.push(
+      wsQueryTranslationSubscription((response) => {
+        if (response && response.result) {
+          dispatch(setQueryText(response));
+        }
+      }),
+    );
 
     login();
 
     // Setup cleanup
     return () => {
-      Broker.instance().unSubscribeAll('schema_result');
-      Broker.instance().unSubscribeAll('query_result');
-
-      Broker.instance().unSubscribeAll('save_states');
-      Broker.instance().unSubscribeAll('save_state');
-      Broker.instance().unSubscribeAll('save_state_status');
-      Broker.instance().unSubscribeAll('delete_save_state');
-      Broker.instance().unSubscribeAll('tested_connection');
-      Broker.instance().unSubscribeAll('save_state_selected');
-      Broker.instance().unSubscribeAll('query_translation_result');
-      // Broker.instance().unSubscribeAll('query_builder_state');
-      allMLTypes.forEach((mlType) => {
-        Broker.instance().unSubscribeAll(mlType);
+      // clear callback subscriptions
+      unsubs.forEach((unsub) => {
+        unsub();
       });
     };
   }, []);
@@ -179,7 +197,11 @@ export const EventBus = (props: { onRunQuery: Function; onAuthorized: Function }
             wsGetState(paramSaveState);
           }
 
-          wsGetStates();
+          dispatch(setFetchingSaveStates(true));
+          wsGetStates((data) => {
+            dispatch(setFetchingSaveStates(false));
+            return true;
+          });
 
           // Broker.instance().sendMessage({ //TODO!!
           //   sessionID: auth?.sessionID || '',
diff --git a/libs/shared/lib/data-access/broker/broker.tsx b/libs/shared/lib/data-access/broker/broker.tsx
index 5b1031cfc..9be76a06b 100644
--- a/libs/shared/lib/data-access/broker/broker.tsx
+++ b/libs/shared/lib/data-access/broker/broker.tsx
@@ -25,6 +25,7 @@ export class Broker {
 
   private listeners: Record<string, Record<string, Function>> = {};
   private catchAllListener: ((data: Record<string, any>, routingKey: string) => void) | undefined;
+  private callbackListeners: Record<string, Function> = {};
 
   private webSocket: WebSocket | undefined;
   private url: string;
@@ -138,26 +139,11 @@ export class Broker {
       this.connected = true;
       onOpen();
     };
-    this.webSocket.onmessage = this.onWebSocketMessage;
+    this.webSocket.onmessage = this.receiveMessage;
     this.webSocket.onerror = this.onError;
     this.webSocket.onclose = this.onClose;
   }
 
-  public sendMessage(message: SendMessageI): void {
-    console.debug('%cSending WS message: ', 'background: #222; color: #bada55', message);
-    let fullMessage = message as SendMessageWithSessionI;
-    fullMessage.sessionID = this.authHeader?.sessionID ?? '';
-    if (message.body && typeof message.body !== 'string') {
-      fullMessage.body = JSON.stringify(message.body);
-    }
-
-    if (this.webSocket && this.connected && this.webSocket.readyState === 1) this.webSocket.send(JSON.stringify(fullMessage));
-    else
-      this.connect(() => {
-        if (this.webSocket && this.connected && this.webSocket.readyState === 1) this.webSocket.send(JSON.stringify(fullMessage));
-      });
-  }
-
   /** Closes the current websocket connection. */
   public close = (): void => {
     if (this.webSocket) this.webSocket.close();
@@ -194,18 +180,55 @@ export class Broker {
     setTimeout(() => Broker.instance().attemptReconnect(), 5000);
   }
 
+  public sendMessage(message: SendMessageI, callback?: Function): void {
+    console.debug('%cSending WS message: ', 'background: #222; color: #bada55', message);
+    let fullMessage = message as SendMessageWithSessionI;
+
+    const uuid = (Date.now() + Math.floor(Math.random() * 100)).toString();
+    fullMessage.callID = uuid;
+
+    if (callback) {
+      this.callbackListeners[uuid] = callback;
+    }
+
+    fullMessage.sessionID = this.authHeader?.sessionID ?? '';
+    if (message.body && typeof message.body !== 'string') {
+      fullMessage.body = JSON.stringify(message.body);
+    }
+
+    if (this.webSocket && this.connected && this.webSocket.readyState === 1) this.webSocket.send(JSON.stringify(fullMessage));
+    else
+      this.connect(() => {
+        if (this.webSocket && this.connected && this.webSocket.readyState === 1) this.webSocket.send(JSON.stringify(fullMessage));
+      });
+  }
+
   /**
    * Websocket connection message event handler. Called if a new message is received through the socket.
    * @param {any} event Contains the event data.
    */
-  public onWebSocketMessage = (event: MessageEvent<any>) => {
+  public receiveMessage = (event: MessageEvent<any>) => {
     let jsonObject: ReceiveMessageI = JSON.parse(event.data);
     const routingKey = jsonObject.type;
     const data = jsonObject.value;
+    const uuid = jsonObject.callID;
 
+    let stop = false; // check in case there is a specific callback listener and, if its response is true, also call the subscriptions
     this.mostRecentMessages[routingKey] = data;
+    if (uuid in this.callbackListeners) {
+      stop = this.callbackListeners[uuid](data) === true;
+      console.debug(
+        '%c' + routingKey + ` WS response WITH CALLBACK`,
+        'background: #222; color: #DBAB2F',
+        data,
+        this.callbackListeners,
+        'stop=',
+        stop,
+      );
+      delete this.callbackListeners[uuid];
+    }
 
-    if (this.listeners[routingKey] && Object.keys(this.listeners[routingKey]).length != 0) {
+    if (!stop && this.listeners[routingKey] && Object.keys(this.listeners[routingKey]).length != 0) {
       if (this.catchAllListener) {
         this.catchAllListener(data, routingKey);
       }
@@ -213,7 +236,7 @@ export class Broker {
       console.debug('%c' + routingKey + ` WS response`, 'background: #222; color: #DBAB2F', data);
     }
     // If there are no listeners, log the message
-    else {
+    else if (!stop) {
       if (this.catchAllListener) {
         this.catchAllListener(data, routingKey);
         console.debug(routingKey, `catch all used for message with routing key`, data);
diff --git a/libs/shared/lib/data-access/broker/index.ts b/libs/shared/lib/data-access/broker/index.ts
index 1ca07b467..e29a86394 100644
--- a/libs/shared/lib/data-access/broker/index.ts
+++ b/libs/shared/lib/data-access/broker/index.ts
@@ -2,3 +2,4 @@ export * from './broker';
 export * from './wsState';
 export * from './wsQuery';
 export * from './wsSchema';
+export * from './types';
diff --git a/libs/shared/lib/data-access/broker/types.ts b/libs/shared/lib/data-access/broker/types.ts
index ce9f967eb..804715a37 100644
--- a/libs/shared/lib/data-access/broker/types.ts
+++ b/libs/shared/lib/data-access/broker/types.ts
@@ -1,4 +1,5 @@
 export type ReceiveMessageI = {
+  callID: string;
   type: string;
   status: string;
   value: Record<string, any>;
@@ -45,6 +46,7 @@ export type SendMessageI = {
 };
 
 export type SendMessageWithSessionI = SendMessageI & {
+  callID: string;
   sessionID: string;
   body?: string;
 };
diff --git a/libs/shared/lib/data-access/broker/wsQuery.ts b/libs/shared/lib/data-access/broker/wsQuery.ts
index 9a2117171..77025d916 100644
--- a/libs/shared/lib/data-access/broker/wsQuery.ts
+++ b/libs/shared/lib/data-access/broker/wsQuery.ts
@@ -5,6 +5,8 @@ import { useAuth } from '../authorization';
 import { useSessionCache } from '../store';
 import { BackendQueryFormat } from '../../querybuilder';
 import { Broker } from './broker';
+import { QueryBuilderText } from '../store/querybuilderSlice';
+import { GraphQueryResultFromBackendPayload } from '../store/graphQueryResultSlice';
 
 export function wsQueryRequest(query: BackendQueryFormat) {
   if (query.cached === undefined) query.cached = false;
@@ -14,3 +16,19 @@ export function wsQueryRequest(query: BackendQueryFormat) {
     body: query,
   });
 }
+
+type QueryTranslationResponse = (data: QueryBuilderText) => void;
+export function wsQueryTranslationSubscription(callback: QueryTranslationResponse) {
+  const id = Broker.instance().subscribe(callback, 'query_translation_result');
+  return () => {
+    Broker.instance().unSubscribe('query_translation_result', id);
+  };
+}
+
+type QueryResultResponse = (data: GraphQueryResultFromBackendPayload) => void;
+export function wsQuerySubscription(callback: QueryResultResponse) {
+  const id = Broker.instance().subscribe(callback, 'query_result');
+  return () => {
+    Broker.instance().unSubscribe('query_result', id);
+  };
+}
diff --git a/libs/shared/lib/data-access/broker/wsSchema.ts b/libs/shared/lib/data-access/broker/wsSchema.ts
index 9a6eebc0b..81944b7f8 100644
--- a/libs/shared/lib/data-access/broker/wsSchema.ts
+++ b/libs/shared/lib/data-access/broker/wsSchema.ts
@@ -1,5 +1,6 @@
 // All database related API calls
 
+import { SchemaFromBackend } from '../../schema';
 import { Broker } from './broker';
 
 export function wsSchemaRequest(saveStateID: string) {
@@ -12,3 +13,10 @@ export function wsSchemaRequest(saveStateID: string) {
     },
   });
 }
+type SchemaResponse = (data: SchemaFromBackend) => void;
+export function wsSchemaSubscription(callback: SchemaResponse) {
+  const id = Broker.instance().subscribe(callback, 'schema_result');
+  return () => {
+    Broker.instance().unSubscribe('schema_result', id);
+  };
+}
diff --git a/libs/shared/lib/data-access/broker/wsState.tsx b/libs/shared/lib/data-access/broker/wsState.tsx
index 160706213..4a375ef39 100644
--- a/libs/shared/lib/data-access/broker/wsState.tsx
+++ b/libs/shared/lib/data-access/broker/wsState.tsx
@@ -2,6 +2,7 @@ import { QueryBuilderState } from '../store/querybuilderSlice';
 import { URLParams, setParam } from '../api/url';
 import { Broker } from './broker';
 import { VisState } from '../store/visualizationSlice';
+import { DateStringStatement } from '../../querybuilder/model/logic/general';
 
 // export function wsGetState() {
 //   Broker.instance().subscribe((data) => dispatch(readInSchemaFromBackend(data)), 'schema_result');
@@ -45,70 +46,130 @@ export type SaveStateI = {
   share_state: any;
 };
 
-export function wsGetState(saveStateId: string) {
-  Broker.instance().sendMessage({
-    key: 'state',
-    subKey: 'get',
-    body: { saveStateId: saveStateId }, //messageTypeGetSaveState
-  });
+type GetStateResponse = (data: SaveStateI) => void;
+export function wsGetState(saveStateId: string, callback?: GetStateResponse) {
+  Broker.instance().sendMessage(
+    {
+      key: 'state',
+      subKey: 'get',
+      body: { saveStateId: saveStateId }, //messageTypeGetSaveState
+    },
+    callback,
+  );
+}
+export function wsGetStateSubscription(callback: GetStateResponse) {
+  const id = Broker.instance().subscribe(callback, 'save_state');
+  return () => {
+    Broker.instance().unSubscribe('save_state', id);
+  };
 }
 
-export function wsGetStates() {
-  Broker.instance().sendMessage({
-    key: 'state',
-    subKey: 'getAll',
-  });
+type GetStatesResponse = (data: SaveStateI[]) => void | boolean;
+export function wsGetStates(callback?: GetStatesResponse) {
+  Broker.instance().sendMessage(
+    {
+      key: 'state',
+      subKey: 'getAll',
+    },
+    callback,
+  );
+}
+export function wsGetStatesSubscription(callback: GetStatesResponse) {
+  const id = Broker.instance().subscribe(callback, 'save_states');
+  return () => {
+    Broker.instance().unSubscribe('save_states', id);
+  };
 }
 
-export function wsSelectState(saveStateId: string | undefined) {
+type SelectStateResponse = (data: { saveStateID: string; success: boolean }) => void;
+export function wsSelectState(saveStateId: string | undefined, callback?: SelectStateResponse) {
   if (saveStateId === undefined) saveStateId = '';
-  Broker.instance().sendMessage({
-    key: 'state',
-    subKey: 'select',
-    body: { saveStateId: saveStateId }, //messageTypeGetSaveState
-  });
+  Broker.instance().sendMessage(
+    {
+      key: 'state',
+      subKey: 'select',
+      body: { saveStateId: saveStateId }, //messageTypeGetSaveState
+    },
+    callback,
+  );
 
   Broker.instance().useSaveStateID(saveStateId);
   setParam(URLParams.saveState, saveStateId);
 }
+export function wsSelectStateSubscription(callback: SelectStateResponse) {
+  const id = Broker.instance().subscribe(callback, 'save_state_selected');
+  return () => {
+    Broker.instance().unSubscribe('save_state_selected', id);
+  };
+}
 
-export function wsCreateState(request: SaveStateI) {
-  Broker.instance().sendMessage({
-    key: 'state',
-    subKey: 'create',
-    body: request, //SaveStateModel
-  });
+export function wsCreateState(request: SaveStateI, callback?: GetStateResponse) {
+  Broker.instance().sendMessage(
+    {
+      key: 'state',
+      subKey: 'create',
+      body: request, //SaveStateModel
+    },
+    callback,
+  );
+  // Also returns save_state
 }
 
-export function wsDeleteState(id: string) {
-  Broker.instance().sendMessage({
-    key: 'state',
-    subKey: 'delete',
-    body: { saveStateId: id }, //messageTypeGetSaveState
-  });
+type DeleteStateResponse = (status: 'deleted' | DateStringStatement) => void;
+export function wsDeleteState(id: string, callback?: Function) {
+  Broker.instance().sendMessage(
+    {
+      key: 'state',
+      subKey: 'delete',
+      body: { saveStateId: id }, //messageTypeGetSaveState
+    },
+    callback,
+  );
+}
+export function wsDeleteStateSubscription(callback: DeleteStateResponse) {
+  const id = Broker.instance().subscribe(callback, 'delete_save_state');
+  return () => {
+    Broker.instance().unSubscribe('delete_save_state', id);
+  };
 }
 
-export function wsTestSaveStateConnection(id: string) {
-  Broker.instance().sendMessage({
-    key: 'state',
-    subKey: 'testConnection',
-    body: { saveStateId: id }, //messageTypeGetSaveState
-  });
+type TestSaveStateConnectionResponse = (data: { status: 'success' | 'fail'; saveStateID: string }) => void;
+export function wsTestSaveStateConnection(id: string, callback?: Function) {
+  Broker.instance().sendMessage(
+    {
+      key: 'state',
+      subKey: 'testConnection',
+      body: { saveStateId: id }, //messageTypeGetSaveState
+    },
+    callback,
+  );
+}
+export function wsTestSaveStateConnectionSubscription(callback: TestSaveStateConnectionResponse) {
+  const id = Broker.instance().subscribe(callback, 'tested_connection');
+  return () => {
+    Broker.instance().unSubscribe('tested_connection', id);
+  };
 }
 
-export function wsUpdateState(request: SaveStateI) {
-  Broker.instance().sendMessage({
-    key: 'state',
-    subKey: 'update',
-    body: request, //SaveStateModel
-  });
+export function wsUpdateState(request: SaveStateI, callback?: GetStateResponse) {
+  Broker.instance().sendMessage(
+    {
+      key: 'state',
+      subKey: 'update',
+      body: request, //SaveStateModel
+    },
+    callback,
+  );
+  // Also returns save_state
 }
 
-export type TestDatabaseConnectionResponse = { status: 'success' | 'fail'; saveStateID: string };
-export function wsTestDatabaseConnection(dbConnection: DatabaseInfo) {
-  Broker.instance().sendMessage({
-    key: 'dbConnection',
-    subKey: 'testConnection',
-    body: dbConnection, //DBConnectionModel
-  });
+export function wsTestDatabaseConnection(dbConnection: DatabaseInfo, callback?: TestSaveStateConnectionResponse) {
+  Broker.instance().sendMessage(
+    {
+      key: 'dbConnection',
+      subKey: 'testConnection',
+      body: dbConnection, //DBConnectionModel
+    },
+    callback,
+  );
 }
diff --git a/libs/shared/lib/data-access/store/sessionSlice.ts b/libs/shared/lib/data-access/store/sessionSlice.ts
index 045558b8b..a161f0d19 100644
--- a/libs/shared/lib/data-access/store/sessionSlice.ts
+++ b/libs/shared/lib/data-access/store/sessionSlice.ts
@@ -13,6 +13,7 @@ export type ErrorMessage = {
 export type SessionCacheI = {
   currentSaveState?: string; // id of the current save state
   saveStates: Record<string, SaveStateI>;
+  fetchingSaveStates: boolean;
   testedSaveState: Record<string, DatabaseStatus>;
 };
 
@@ -20,6 +21,7 @@ export type SessionCacheI = {
 export const initialState: SessionCacheI = {
   currentSaveState: undefined,
   saveStates: {},
+  fetchingSaveStates: true, // default to true to prevent flashing of the UI
   testedSaveState: {},
 };
 
@@ -28,6 +30,9 @@ export const sessionSlice = createSlice({
   name: 'session',
   initialState: initialState,
   reducers: {
+    setFetchingSaveStates: (state: SessionCacheI, action: PayloadAction<boolean>) => {
+      state.fetchingSaveStates = action.payload;
+    },
     selectSaveState: (state: SessionCacheI, action: PayloadAction<string | undefined>) => {
       if (action.payload && action.payload in state.saveStates) {
         state.currentSaveState = action.payload;
@@ -95,6 +100,7 @@ export const {
   setSaveStateList,
   addSaveState,
   testedSaveState,
+  setFetchingSaveStates,
   updateSelectedSaveState,
 } = sessionSlice.actions;
 
diff --git a/libs/shared/package.json b/libs/shared/package.json
index f40bd4dfc..c59615c5a 100644
--- a/libs/shared/package.json
+++ b/libs/shared/package.json
@@ -135,6 +135,7 @@
     "redux": "^5.0.1",
     "redux-thunk": "^3.1.0",
     "require-from-string": "^2.0.2",
+    "reselect": "^5.1.0",
     "tailwindcss": "^3.4.1",
     "ts-node": "10.9.2",
     "typescript": "^5.3.3",
diff --git a/libs/shared/tsconfig.json b/libs/shared/tsconfig.json
index fb21f80da..42051d225 100644
--- a/libs/shared/tsconfig.json
+++ b/libs/shared/tsconfig.json
@@ -28,7 +28,8 @@
       "@graphpolaris/config/*": ["../../libs/config/src/*"],
       "redux": ["./node_modules/redux"],
       "@storybook/types": ["./node_modules/@storybook/types"],
-      "redux-thunk": ["./node_modules/redux-thunk"]
+      "redux-thunk": ["./node_modules/redux-thunk"],
+      "reselect": ["./node_modules/reselect"]
     },
     "types": ["node", "vite/client"]
   },
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index a4e1616f8..a8312fdf6 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -178,6 +178,9 @@ importers:
       redux-thunk:
         specifier: ^3.1.0
         version: 3.1.0(redux@5.0.1)
+      reselect:
+        specifier: ^5.1.0
+        version: 5.1.0
       tailwindcss:
         specifier: ^3.4.1
         version: 3.4.1(ts-node@10.9.2)
@@ -548,6 +551,9 @@ importers:
       require-from-string:
         specifier: ^2.0.2
         version: 2.0.2
+      reselect:
+        specifier: ^5.1.0
+        version: 5.1.0
       tailwindcss:
         specifier: ^3.4.1
         version: 3.4.1(ts-node@10.9.2)
@@ -18488,7 +18494,6 @@ packages:
 
   /reselect@5.1.0:
     resolution: {integrity: sha512-aw7jcGLDpSgNDyWBQLv2cedml85qd95/iszJjN988zX1t7AVRJi19d9kto5+W7oCfQ94gyo40dVbT6g2k4/kXg==}
-    dev: false
 
   /resize-observer-polyfill@1.5.1:
     resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==}
-- 
GitLab