Move the UI WS protocol version handling is protocol header
authorJérôme Benoit <jerome.benoit@sap.com>
Tue, 30 Nov 2021 20:50:41 +0000 (21:50 +0100)
committerJérôme Benoit <jerome.benoit@sap.com>
Tue, 30 Nov 2021 20:50:41 +0000 (21:50 +0100)
Signed-off-by: Jérôme Benoit <jerome.benoit@piment-noir.org>
src/charging-station/Bootstrap.ts
src/charging-station/UIWebSocketServer.ts [new file with mode: 0644]
src/charging-station/UIWebSocketServices/AbstractUIService.ts [new file with mode: 0644]
src/charging-station/UIWebSocketServices/UIServiceFactory.ts [new file with mode: 0644]
src/charging-station/UIWebSocketServices/UIServiceUtils.ts [new file with mode: 0644]
src/charging-station/UIWebSocketServices/UIService_0_0_1.ts [new file with mode: 0644]
src/charging-station/WebSocketServer.ts [deleted file]
src/charging-station/WebSocketServices/ui/0.0.1/UIService.ts [deleted file]
src/charging-station/WebSocketServices/ui/AbstractUIService.ts [deleted file]
src/types/UIProtocol.ts

index fe344407dc03400970fc89fb911f45011ce422b5..74fb74b7cb6e195524f0440b3eb831e85f25804d 100644 (file)
@@ -5,8 +5,9 @@ import { ChargingStationWorkerData, ChargingStationWorkerMessage, ChargingStatio
 import Configuration from '../utils/Configuration';
 import { Storage } from '../performance/storage/Storage';
 import { StorageFactory } from '../performance/storage/StorageFactory';
+import { UIServiceUtils } from './UIWebSocketServices/UIServiceUtils';
+import UIWebSocketServer from './UIWebSocketServer';
 import Utils from '../utils/Utils';
-import WebSocketServer from './WebSocketServer';
 import WorkerAbstract from '../worker/WorkerAbstract';
 import WorkerFactory from '../worker/WorkerFactory';
 import chalk from 'chalk';
@@ -17,7 +18,7 @@ import { version } from '../../package.json';
 export default class Bootstrap {
   private static instance: Bootstrap | null = null;
   private workerImplementation: WorkerAbstract | null = null;
-  private readonly webSocketServer: WebSocketServer;
+  private readonly uiWebSocketServer: UIWebSocketServer;
   private readonly storage: Storage;
   private numberOfChargingStations: number;
   private readonly version: string = version;
@@ -28,7 +29,7 @@ export default class Bootstrap {
     this.started = false;
     this.workerScript = path.join(path.resolve(__dirname, '../'), 'charging-station', 'ChargingStationWorker.js');
     this.initWorkerImplementation();
-    this.webSocketServer = new WebSocketServer();
+    this.uiWebSocketServer = new UIWebSocketServer({ port: 80, handleProtocols: UIServiceUtils.handleProtocols });
     this.storage = StorageFactory.getStorage(Configuration.getPerformanceStorage().type, Configuration.getPerformanceStorage().URI, this.logPrefix());
     Configuration.setConfigurationChangeCallback(async () => Bootstrap.getInstance().restart());
   }
@@ -46,7 +47,7 @@ export default class Bootstrap {
         this.numberOfChargingStations = 0;
         await this.storage.open();
         await this.workerImplementation.start();
-        this.webSocketServer.start();
+        this.uiWebSocketServer.start();
         // Start ChargingStation object in worker thread
         if (Configuration.getStationTemplateURLs()) {
           for (const stationURL of Configuration.getStationTemplateURLs()) {
@@ -84,7 +85,7 @@ export default class Bootstrap {
   public async stop(): Promise<void> {
     if (isMainThread && this.started) {
       await this.workerImplementation.stop();
-      this.webSocketServer.stop();
+      this.uiWebSocketServer.stop();
       await this.storage.close();
     } else {
       console.error(chalk.red('Trying to stop the charging stations simulator while not started'));
@@ -110,9 +111,9 @@ export default class Bootstrap {
         },
         messageHandler: async (msg: ChargingStationWorkerMessage) => {
           if (msg.id === ChargingStationWorkerMessageEvents.STARTED) {
-            this.webSocketServer.webSocketServerService.chargingStations.add(msg.data.id);
+            this.uiWebSocketServer.uiService.chargingStations.add(msg.data.id);
           } else if (msg.id === ChargingStationWorkerMessageEvents.STOPPED) {
-            this.webSocketServer.webSocketServerService.chargingStations.delete(msg.data.id);
+            this.uiWebSocketServer.uiService.chargingStations.delete(msg.data.id);
           } else if (msg.id === ChargingStationWorkerMessageEvents.PERFORMANCE_STATISTICS) {
             await this.storage.storePerformanceStatistics(msg.data);
           }
diff --git a/src/charging-station/UIWebSocketServer.ts b/src/charging-station/UIWebSocketServer.ts
new file mode 100644 (file)
index 0000000..78ca751
--- /dev/null
@@ -0,0 +1,61 @@
+import { Protocol, ProtocolCommand, ProtocolRequest, ProtocolVersion } from '../types/UIProtocol';
+
+import AbstractUIService from './UIWebSocketServices/AbstractUIService';
+import BaseError from '../exception/BaseError';
+import { IncomingMessage } from 'http';
+import UIServiceFactory from './UIWebSocketServices/UIServiceFactory';
+import Utils from '../utils/Utils';
+import WebSocket from 'ws';
+import logger from '../utils/Logger';
+
+export default class UIWebSocketServer extends WebSocket.Server {
+  public uiService: AbstractUIService;
+
+  public constructor(options?: WebSocket.ServerOptions, callback?: () => void) {
+    // Create the WebSocket Server
+    super(options ?? { port: 80 }, callback);
+  }
+
+  public broadcastToClients(message: string | Record<string, unknown>): void {
+    for (const client of this.clients) {
+      if (client?.readyState === WebSocket.OPEN) {
+        client.send(message);
+      }
+    }
+  }
+
+  public start(): void {
+    this.on('connection', (socket: WebSocket, request: IncomingMessage): void => {
+      const protocolIndex = socket.protocol.indexOf(Protocol.UI);
+      const version = socket.protocol.substring(protocolIndex + Protocol.UI.length) as ProtocolVersion;
+      this.uiService = UIServiceFactory.getUIServiceImplementation(version, this);
+      if (!this.uiService) {
+        throw new BaseError(`Could not find a UI service implementation for protocol version ${version}`);
+      }
+      // FIXME: check connection validity
+      socket.on('message', (messageData) => {
+        let [command, payload]: ProtocolRequest = [ProtocolCommand.UNKNOWN, {}];
+        const protocolRequest = JSON.parse(messageData.toString()) as ProtocolRequest;
+        if (Utils.isIterable(protocolRequest)) {
+          [command, payload] = protocolRequest;
+        } else {
+          throw new BaseError('Protocol request is not iterable');
+        }
+        this.uiService.handleMessage(command, payload).catch(() => {
+          logger.error(`${this.logPrefix()} Error while handling command %s message: %j`, command, payload);
+        });
+      });
+      socket.on('error', (error) => {
+        logger.error(`${this.logPrefix()} Error on WebSocket: %j`, error);
+      });
+    });
+  }
+
+  public stop(): void {
+    this.close();
+  }
+
+  public logPrefix(): string {
+    return Utils.logPrefix('WebSocket Server:');
+  }
+}
diff --git a/src/charging-station/UIWebSocketServices/AbstractUIService.ts b/src/charging-station/UIWebSocketServices/AbstractUIService.ts
new file mode 100644 (file)
index 0000000..fafcdb6
--- /dev/null
@@ -0,0 +1,50 @@
+import { ProtocolCommand, ProtocolRequestHandler } from '../../types/UIProtocol';
+
+import BaseError from '../../exception/BaseError';
+import UIWebSocketServer from '../UIWebSocketServer';
+import logger from '../../utils/Logger';
+
+export default abstract class AbstractUIService {
+  public readonly chargingStations: Set<string>;
+  protected readonly uiWebSocketServer: UIWebSocketServer;
+  protected readonly messageHandlers: Map<ProtocolCommand, ProtocolRequestHandler>;
+
+  constructor(uiWebSocketServer: UIWebSocketServer) {
+    this.chargingStations = new Set<string>();
+    this.uiWebSocketServer = uiWebSocketServer;
+    // TODO: Move the shared service code to AbstractUIService
+    this.messageHandlers = new Map<ProtocolCommand, ProtocolRequestHandler>([
+      [ProtocolCommand.LIST_CHARGING_STATIONS, this.handleListChargingStations.bind(this)],
+    ]);
+  }
+
+  public async handleMessage(command: ProtocolCommand, payload: Record<string, unknown>): Promise<void> {
+    let messageResponse: Record<string, unknown>;
+    if (this.messageHandlers.has(command)) {
+      try {
+        // Call the method to build the message response
+        messageResponse = await this.messageHandlers.get(command)(payload) as Record<string, unknown>;
+      } catch (error) {
+        // Log
+        logger.error(this.uiWebSocketServer.logPrefix() + ' Handle message error: %j', error);
+        throw error;
+      }
+    } else {
+      // Throw exception
+      throw new BaseError(`${command} is not implemented to handle message payload ${JSON.stringify(payload, null, 2)}`);
+    }
+    // Send the built message response
+    this.uiWebSocketServer.broadcastToClients(this.buildProtocolMessage(command, messageResponse));
+  }
+
+  protected buildProtocolMessage(
+      command: ProtocolCommand,
+      payload: Record<string, unknown>,
+  ): string {
+    return JSON.stringify([command, payload]);
+  }
+
+  protected handleListChargingStations(): Set<string> {
+    return this.chargingStations;
+  }
+}
diff --git a/src/charging-station/UIWebSocketServices/UIServiceFactory.ts b/src/charging-station/UIWebSocketServices/UIServiceFactory.ts
new file mode 100644 (file)
index 0000000..a4d8d6e
--- /dev/null
@@ -0,0 +1,19 @@
+import AbstractUIService from './AbstractUIService';
+import { ProtocolVersion } from '../../types/UIProtocol';
+import UIService_0_0_1 from './UIService_0_0_1';
+import UIWebSocketServer from '../UIWebSocketServer';
+
+export default class UIServiceFactory {
+  private constructor() {
+    // This is intentional
+  }
+
+  public static getUIServiceImplementation(version: ProtocolVersion, uiWebSocketServer: UIWebSocketServer): AbstractUIService | null {
+    switch (version) {
+      case ProtocolVersion['0.0.1']:
+        return new UIService_0_0_1(uiWebSocketServer);
+      default:
+        return null;
+    }
+  }
+}
diff --git a/src/charging-station/UIWebSocketServices/UIServiceUtils.ts b/src/charging-station/UIWebSocketServices/UIServiceUtils.ts
new file mode 100644 (file)
index 0000000..f95a748
--- /dev/null
@@ -0,0 +1,23 @@
+import { Protocol, ProtocolVersion } from '../../types/UIProtocol';
+
+import { IncomingMessage } from 'http';
+import Utils from '../../utils/Utils';
+import logger from '../../utils/Logger';
+
+export class UIServiceUtils {
+  public static handleProtocols = (protocols: Set<string>, request: IncomingMessage): string | false => {
+    let protocolIndex: number;
+    let protocol: Protocol;
+    let version: ProtocolVersion;
+    for (const fullProtocol of protocols) {
+      protocolIndex = fullProtocol.indexOf(Protocol.UI);
+      protocol = fullProtocol.substring(protocolIndex, protocolIndex + Protocol.UI.length) as Protocol;
+      version = fullProtocol.substring(protocolIndex + Protocol.UI.length) as ProtocolVersion;
+      if (Object.values(Protocol).includes(protocol) && Object.values(ProtocolVersion).includes(version)) {
+        return fullProtocol;
+      }
+    }
+    logger.error(`${Utils.logPrefix('WebSocket Server:')} Unsupported protocol: ${protocol} or protocol version: ${version}`);
+    return false;
+  };
+}
diff --git a/src/charging-station/UIWebSocketServices/UIService_0_0_1.ts b/src/charging-station/UIWebSocketServices/UIService_0_0_1.ts
new file mode 100644 (file)
index 0000000..eebaf01
--- /dev/null
@@ -0,0 +1,14 @@
+import AbstractUIService from './AbstractUIService';
+import { ProtocolCommand } from '../../types/UIProtocol';
+import UIWebSocketServer from '../UIWebSocketServer';
+
+export default class UIService_0_0_1 extends AbstractUIService {
+  constructor(uiWebSocketServer: UIWebSocketServer) {
+    super(uiWebSocketServer);
+    this.messageHandlers.set(ProtocolCommand.START_TRANSACTION, this.handleStartTransaction.bind(this));
+    this.messageHandlers.set(ProtocolCommand.STOP_TRANSACTION, this.handleStopTransaction.bind(this));
+  }
+
+  private handleStartTransaction(payload: Record<string, unknown>): void { }
+  private handleStopTransaction(payload: Record<string, unknown>): void { }
+}
diff --git a/src/charging-station/WebSocketServer.ts b/src/charging-station/WebSocketServer.ts
deleted file mode 100644 (file)
index e36725c..0000000
+++ /dev/null
@@ -1,58 +0,0 @@
-import { ProtocolCommand, ProtocolRequest, ProtocolVersion } from '../types/UIProtocol';
-
-import AbstractUIService from './WebSocketServices/ui/AbstractUIService';
-import { IncomingMessage } from 'http';
-import UIService from './WebSocketServices/ui/0.0.1/UIService';
-import Utils from '../utils/Utils';
-import WebSocket from 'ws';
-import logger from '../utils/Logger';
-
-export default class WebSocketServer extends WebSocket.Server {
-  public webSocketServerService: AbstractUIService;
-
-  public constructor(options?: WebSocket.ServerOptions, callback?: () => void) {
-    // Create the WebSocket Server
-    super(options ?? { port: 80 }, callback);
-    // FIXME: version the instantiation
-    this.webSocketServerService = new UIService(this);
-  }
-
-  public broadcastToClients(message: string | Record<string, unknown>): void {
-    for (const client of this.clients) {
-      if (client?.readyState === WebSocket.OPEN) {
-        client.send(message);
-      }
-    }
-  }
-
-  public start(): void {
-    this.on('connection', (socket: WebSocket, request: IncomingMessage): void => {
-      // FIXME: check connection validity
-      socket.on('message', (messageData) => {
-        let [version, command, payload]: ProtocolRequest = [ProtocolVersion['0.0.1'], ProtocolCommand.UNKNOWN, {}];
-        // FIXME: check for iterable object
-        [version, command, payload] = JSON.parse(messageData.toString()) as ProtocolRequest;
-        switch (version) {
-          case ProtocolVersion['0.0.1']:
-            this.webSocketServerService.handleMessage(version, command, payload).catch(() => {
-              logger.error(`${this.logPrefix()} Error while handling command %s message: %j`, command, payload);
-            });
-            break;
-          default:
-            logger.error(`${this.logPrefix()} Unknown protocol version: ${version}`);
-        }
-      });
-      socket.on('error', (error) => {
-        logger.error(`${this.logPrefix()} Error on WebSocket: %j`, error);
-      });
-    });
-  }
-
-  public stop(): void {
-    this.close();
-  }
-
-  public logPrefix(): string {
-    return Utils.logPrefix('WebSocket Server:');
-  }
-}
diff --git a/src/charging-station/WebSocketServices/ui/0.0.1/UIService.ts b/src/charging-station/WebSocketServices/ui/0.0.1/UIService.ts
deleted file mode 100644 (file)
index 6c507fd..0000000
+++ /dev/null
@@ -1,45 +0,0 @@
-import { ProtocolCommand, ProtocolRequestHandler, ProtocolVersion } from '../../../../types/UIProtocol';
-
-import AbstractUIService from '../AbstractUIService';
-import BaseError from '../../../../exception/BaseError';
-import WebSocketServer from '../../../WebSocketServer';
-import logger from '../../../../utils/Logger';
-
-export default class UIService extends AbstractUIService {
-  private readonly messageHandlers: Map<ProtocolCommand, ProtocolRequestHandler>;
-
-  constructor(webSocketServer: WebSocketServer) {
-    super(webSocketServer);
-    this.messageHandlers = new Map<ProtocolCommand, ProtocolRequestHandler>([
-      [ProtocolCommand.LIST_CHARGING_STATIONS, this.handleListChargingStations.bind(this)],
-      [ProtocolCommand.START_TRANSACTION, this.handleStartTransaction.bind(this)],
-      [ProtocolCommand.STOP_TRANSACTION, this.handleStopTransaction.bind(this)],
-    ]);
-  }
-
-  async handleMessage(version: ProtocolVersion, command: ProtocolCommand, payload: Record<string, unknown>): Promise<void> {
-    let messageResponse: Record<string, unknown>;
-    if (this.messageHandlers.has(command)) {
-      try {
-        // Call the method to build the response
-        messageResponse = await this.messageHandlers.get(command)(payload);
-      } catch (error) {
-        // Log
-        logger.error(this.webSocketServer.logPrefix() + ' Handle message error: %j', error);
-        throw error;
-      }
-    } else {
-      // Throw exception
-      throw new BaseError(`${command} is not implemented to handle message payload ${JSON.stringify(payload, null, 2)}`);
-    }
-    // Send the built response
-    this.webSocketServer.broadcastToClients(this.buildProtocolMessage(version, command, messageResponse));
-  }
-
-  private handleListChargingStations(payload: Record<string, unknown>) {
-    return this.chargingStations;
-  }
-
-  private handleStartTransaction(payload: Record<string, unknown>) { }
-  private handleStopTransaction(payload: Record<string, unknown>) { }
-}
diff --git a/src/charging-station/WebSocketServices/ui/AbstractUIService.ts b/src/charging-station/WebSocketServices/ui/AbstractUIService.ts
deleted file mode 100644 (file)
index d8c6186..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-import { ProtocolCommand, ProtocolVersion } from '../../../types/UIProtocol';
-
-import WebSocketServer from '../../WebSocketServer';
-
-export default abstract class AbstractUIService {
-  public readonly chargingStations: Set<string>;
-  protected readonly webSocketServer: WebSocketServer;
-
-  constructor(webSocketServer: WebSocketServer) {
-    this.chargingStations = new Set<string>();
-    this.webSocketServer = webSocketServer;
-  }
-
-  protected buildProtocolMessage(
-      version: ProtocolVersion,
-      command: ProtocolCommand,
-      payload: Record<string, unknown>,
-  ): string {
-    return JSON.stringify([version, command, payload]);
-  }
-
-  abstract handleMessage(version: ProtocolVersion, command: ProtocolCommand, payload: Record<string, unknown>): Promise<void>;
-}
index 3e4d6232ba6202ca395fa57791fe8886b0f332e5..2abefe258dc991de01a9723d3d5fb563de181149 100644 (file)
@@ -1,3 +1,6 @@
+export enum Protocol {
+  UI = 'ui',
+}
 
 export enum ProtocolVersion {
   '0.0.1' = '0.0.1',
@@ -11,6 +14,6 @@ export enum ProtocolCommand {
   UNKNOWN = 'unknown',
 }
 
-export type ProtocolRequest = [ProtocolVersion, ProtocolCommand, Record<string, unknown>];
+export type ProtocolRequest = [ProtocolCommand, Record<string, unknown>];
 
-export type ProtocolRequestHandler = (payload: Record<string, unknown>) => Record<string, unknown> | Promise<Record<string, unknown>>;
+export type ProtocolRequestHandler = (payload: Record<string, unknown>) => void | Promise<void> | Record<string, unknown> | Promise<Record<string, unknown>>;