README.md: refine UI protocol documentation
[e-mobility-charging-stations-simulator.git] / src / charging-station / ui-server / ui-services / AbstractUIService.ts
index d4ad33988e2062f7c99b779026f07cf5323c1fe0..db9a036d183054b7fd77cb98dda7ac0e3eb7626f 100644 (file)
+import { RawData } from 'ws';
+
+import BaseError from '../../../exception/BaseError';
+import { JsonType } from '../../../types/JsonType';
 import {
-  ProtocolCommand,
+  ProcedureName,
   ProtocolRequest,
   ProtocolRequestHandler,
+  ProtocolResponse,
+  ProtocolVersion,
+  RequestPayload,
+  ResponsePayload,
+  ResponseStatus,
 } from '../../../types/UIProtocol';
-
-import BaseError from '../../../exception/BaseError';
-import { JsonType } from '../../../types/JsonType';
-import { RawData } from 'ws';
-import UIWebSocketServer from '../UIWebSocketServer';
-import Utils from '../../../utils/Utils';
 import logger from '../../../utils/Logger';
+import Utils from '../../../utils/Utils';
+import Bootstrap from '../../Bootstrap';
+import UIServiceWorkerBroadcastChannel from '../../UIServiceWorkerBroadcastChannel';
+import { AbstractUIServer } from '../AbstractUIServer';
+
+const moduleName = 'AbstractUIService';
 
 export default abstract class AbstractUIService {
-  protected readonly uiServer: UIWebSocketServer;
-  protected readonly messageHandlers: Map<ProtocolCommand, ProtocolRequestHandler>;
+  protected readonly version: ProtocolVersion;
+  protected readonly uiServer: AbstractUIServer;
+  protected readonly requestHandlers: Map<ProcedureName, ProtocolRequestHandler>;
+  protected uiServiceWorkerBroadcastChannel: UIServiceWorkerBroadcastChannel;
 
-  constructor(uiServer: UIWebSocketServer) {
+  constructor(uiServer: AbstractUIServer, version: ProtocolVersion) {
+    this.version = version;
     this.uiServer = uiServer;
-    this.messageHandlers = new Map<ProtocolCommand, ProtocolRequestHandler>([
-      [ProtocolCommand.LIST_CHARGING_STATIONS, this.handleListChargingStations.bind(this)],
+    this.requestHandlers = new Map<ProcedureName, ProtocolRequestHandler>([
+      [ProcedureName.LIST_CHARGING_STATIONS, this.handleListChargingStations.bind(this)],
+      [ProcedureName.START_SIMULATOR, this.handleStartSimulator.bind(this)],
+      [ProcedureName.STOP_SIMULATOR, this.handleStopSimulator.bind(this)],
     ]);
+    this.uiServiceWorkerBroadcastChannel = new UIServiceWorkerBroadcastChannel(this);
   }
 
-  public async messageHandler(request: RawData): Promise<void> {
-    let command: ProtocolCommand;
-    let payload: JsonType;
-    const protocolRequest = JSON.parse(request.toString()) as ProtocolRequest;
-    if (Utils.isIterable(protocolRequest)) {
-      [command, payload] = protocolRequest;
-    } else {
-      throw new BaseError('UI protocol request is not iterable');
-    }
-    let messageResponse: JsonType;
-    if (this.messageHandlers.has(command)) {
-      try {
-        // Call the message handler to build the message response
-        messageResponse = (await this.messageHandlers.get(command)(payload)) as JsonType;
-      } catch (error) {
-        // Log
-        logger.error(this.uiServer.logPrefix() + ' Handle message error: %j', error);
-        throw error;
+  public async requestHandler(request: RawData): Promise<void> {
+    let messageId: string;
+    let command: ProcedureName;
+    let requestPayload: RequestPayload | undefined;
+    let responsePayload: ResponsePayload;
+    try {
+      [messageId, command, requestPayload] = this.requestValidation(request);
+
+      if (this.requestHandlers.has(command) === false) {
+        throw new BaseError(
+          `${command} is not implemented to handle message payload ${JSON.stringify(
+            requestPayload,
+            null,
+            2
+          )}`
+        );
       }
-    } else {
-      // Throw exception
-      throw new BaseError(
-        `${command} is not implemented to handle message payload ${JSON.stringify(
-          payload,
-          null,
-          2
-        )}`
+
+      // Call the request handler to build the response payload
+      responsePayload = await this.requestHandlers.get(command)(messageId, requestPayload);
+    } catch (error) {
+      // Log
+      logger.error(
+        `${this.uiServer.logPrefix(moduleName, 'messageHandler')} Handle request error:`,
+        error
       );
+      responsePayload = {
+        status: ResponseStatus.FAILURE,
+        command,
+        requestPayload,
+        responsePayload,
+        errorMessage: (error as Error).message,
+        errorStack: (error as Error).stack,
+      };
+    }
+
+    if (responsePayload !== undefined) {
+      // Send the response
+      this.uiServer.sendResponse(this.buildProtocolResponse(messageId ?? 'error', responsePayload));
+    }
+  }
+
+  public sendRequest(
+    messageId: string,
+    procedureName: ProcedureName,
+    requestPayload: RequestPayload
+  ): void {
+    this.uiServer.sendRequest(this.buildProtocolRequest(messageId, procedureName, requestPayload));
+  }
+
+  public sendResponse(messageId: string, responsePayload: ResponsePayload): void {
+    this.uiServer.sendResponse(this.buildProtocolResponse(messageId, responsePayload));
+  }
+
+  public logPrefix(modName: string, methodName: string): string {
+    return this.uiServer.logPrefix(modName, methodName);
+  }
+
+  private buildProtocolRequest(
+    messageId: string,
+    procedureName: ProcedureName,
+    requestPayload: RequestPayload
+  ): string {
+    return JSON.stringify([messageId, procedureName, requestPayload] as ProtocolRequest);
+  }
+
+  private buildProtocolResponse(messageId: string, responsePayload: ResponsePayload): string {
+    return JSON.stringify([messageId, responsePayload] as ProtocolResponse);
+  }
+
+  // Validate the raw data received from the WebSocket
+  // TODO: should probably be moved to the ws verify clients callback
+  private requestValidation(rawData: RawData): ProtocolRequest {
+    // logger.debug(
+    //   `${this.uiServer.logPrefix(
+    //     moduleName,
+    //     'dataValidation'
+    //   )} Raw data received: ${rawData.toString()}`
+    // );
+
+    const data = JSON.parse(rawData.toString()) as JsonType[];
+
+    if (Utils.isIterable(data) === false) {
+      throw new BaseError('UI protocol request is not iterable');
     }
-    // Send the message response
-    this.uiServer.sendResponse(this.buildProtocolMessage(command, messageResponse));
+
+    if (data.length !== 3) {
+      throw new BaseError('UI protocol request is malformed');
+    }
+
+    return data as ProtocolRequest;
+  }
+
+  private handleListChargingStations(): ResponsePayload {
+    // TODO: remove cast to unknown
+    return {
+      status: ResponseStatus.SUCCESS,
+      ...Array.from(this.uiServer.chargingStations.values()),
+    } as unknown as ResponsePayload;
   }
 
-  protected buildProtocolMessage(command: ProtocolCommand, payload: JsonType): string {
-    return JSON.stringify([command, payload]);
+  private async handleStartSimulator(): Promise<ResponsePayload> {
+    await Bootstrap.getInstance().start();
+    return { status: ResponseStatus.SUCCESS };
   }
 
-  private handleListChargingStations(): JsonType {
-    return Array.from(this.uiServer.chargingStations);
+  private async handleStopSimulator(): Promise<ResponsePayload> {
+    await Bootstrap.getInstance().stop();
+    return { status: ResponseStatus.SUCCESS };
   }
 }