README.md: document added UI protocol features
[e-mobility-charging-stations-simulator.git] / src / charging-station / ui-server / ui-services / AbstractUIService.ts
1 import type { RawData } from 'ws';
2
3 import BaseError from '../../../exception/BaseError';
4 import { Bootstrap } from '../../../internal';
5 import type { JsonType } from '../../../types/JsonType';
6 import {
7 ProcedureName,
8 ProtocolRequest,
9 ProtocolRequestHandler,
10 ProtocolVersion,
11 RequestPayload,
12 ResponsePayload,
13 ResponseStatus,
14 } from '../../../types/UIProtocol';
15 import type {
16 BroadcastChannelProcedureName,
17 BroadcastChannelRequestPayload,
18 } from '../../../types/WorkerBroadcastChannel';
19 import logger from '../../../utils/Logger';
20 import Utils from '../../../utils/Utils';
21 import UIServiceWorkerBroadcastChannel from '../../UIServiceWorkerBroadcastChannel';
22 import type { AbstractUIServer } from '../AbstractUIServer';
23
24 const moduleName = 'AbstractUIService';
25
26 export default abstract class AbstractUIService {
27 protected readonly requestHandlers: Map<ProcedureName, ProtocolRequestHandler>;
28 private readonly version: ProtocolVersion;
29 private readonly uiServer: AbstractUIServer;
30 private readonly uiServiceWorkerBroadcastChannel: UIServiceWorkerBroadcastChannel;
31 private readonly broadcastChannelRequests: Map<string, number>;
32
33 constructor(uiServer: AbstractUIServer, version: ProtocolVersion) {
34 this.version = version;
35 this.uiServer = uiServer;
36 this.requestHandlers = new Map<ProcedureName, ProtocolRequestHandler>([
37 [ProcedureName.LIST_CHARGING_STATIONS, this.handleListChargingStations.bind(this)],
38 [ProcedureName.START_SIMULATOR, this.handleStartSimulator.bind(this)],
39 [ProcedureName.STOP_SIMULATOR, this.handleStopSimulator.bind(this)],
40 ]);
41 this.uiServiceWorkerBroadcastChannel = new UIServiceWorkerBroadcastChannel(this);
42 this.broadcastChannelRequests = new Map<string, number>();
43 }
44
45 public async requestHandler(request: RawData | JsonType): Promise<void> {
46 let messageId: string;
47 let command: ProcedureName;
48 let requestPayload: RequestPayload | undefined;
49 let responsePayload: ResponsePayload;
50 try {
51 [messageId, command, requestPayload] = this.requestValidation(request);
52
53 if (this.requestHandlers.has(command) === false) {
54 throw new BaseError(
55 `${command} is not implemented to handle message payload ${JSON.stringify(
56 requestPayload,
57 null,
58 2
59 )}`
60 );
61 }
62
63 // Call the request handler to build the response payload
64 responsePayload = await this.requestHandlers.get(command)(messageId, requestPayload);
65 } catch (error) {
66 // Log
67 logger.error(`${this.logPrefix(moduleName, 'messageHandler')} Handle request error:`, error);
68 responsePayload = {
69 status: ResponseStatus.FAILURE,
70 command,
71 requestPayload,
72 responsePayload,
73 errorMessage: (error as Error).message,
74 errorStack: (error as Error).stack,
75 };
76 }
77 // Send response for payload not forwarded to broadcast channel
78 if (responsePayload !== undefined) {
79 this.sendResponse(messageId ?? 'error', responsePayload);
80 }
81 }
82
83 public sendRequest(
84 messageId: string,
85 procedureName: ProcedureName,
86 requestPayload: RequestPayload
87 ): void {
88 this.uiServer.sendRequest(
89 this.uiServer.buildProtocolRequest(messageId, procedureName, requestPayload)
90 );
91 }
92
93 public sendResponse(messageId: string, responsePayload: ResponsePayload): void {
94 this.uiServer.sendResponse(this.uiServer.buildProtocolResponse(messageId, responsePayload));
95 }
96
97 public logPrefix(modName: string, methodName: string): string {
98 return this.uiServer.logPrefix(modName, methodName, this.version);
99 }
100
101 public deleteBroadcastChannelRequest(uuid: string): void {
102 this.broadcastChannelRequests.delete(uuid);
103 }
104
105 public getBroadcastChannelExpectedResponses(uuid: string): number {
106 return this.broadcastChannelRequests.get(uuid) ?? 0;
107 }
108
109 protected sendBroadcastChannelRequest(
110 uuid: string,
111 procedureName: BroadcastChannelProcedureName,
112 payload: BroadcastChannelRequestPayload
113 ): void {
114 if (!Utils.isEmptyArray(payload.hashIds)) {
115 payload.hashIds = payload.hashIds
116 .map((hashId) => {
117 if (this.uiServer.chargingStations.has(hashId) === true) {
118 return hashId;
119 }
120 logger.warn(
121 `${this.logPrefix(
122 moduleName,
123 'sendBroadcastChannelRequest'
124 )} Charging station with hashId '${hashId}' not found`
125 );
126 })
127 .filter((hashId) => hashId !== undefined);
128 }
129 const expectedNumberOfResponses = !Utils.isEmptyArray(payload.hashIds)
130 ? payload.hashIds.length
131 : this.uiServer.chargingStations.size;
132 this.uiServiceWorkerBroadcastChannel.sendRequest([uuid, procedureName, payload]);
133 this.broadcastChannelRequests.set(uuid, expectedNumberOfResponses);
134 }
135
136 // Validate the raw data received from the UI server
137 private requestValidation(rawData: RawData | JsonType): ProtocolRequest {
138 // logger.debug(
139 // `${this.logPrefix(
140 // moduleName,
141 // 'requestValidation'
142 // )} Data received in string format: ${rawData.toString()}`
143 // );
144
145 const data = JSON.parse(rawData.toString()) as JsonType[];
146
147 if (Array.isArray(data) === false) {
148 throw new BaseError('UI protocol request is not an array');
149 }
150
151 if (data.length !== 3) {
152 throw new BaseError('UI protocol request is malformed');
153 }
154
155 return data as ProtocolRequest;
156 }
157
158 private handleListChargingStations(): ResponsePayload {
159 // TODO: remove cast to unknown
160 return {
161 status: ResponseStatus.SUCCESS,
162 ...[...this.uiServer.chargingStations.values()],
163 } as unknown as ResponsePayload;
164 }
165
166 private async handleStartSimulator(): Promise<ResponsePayload> {
167 await Bootstrap.getInstance().start();
168 return { status: ResponseStatus.SUCCESS };
169 }
170
171 private async handleStopSimulator(): Promise<ResponsePayload> {
172 await Bootstrap.getInstance().stop();
173 return { status: ResponseStatus.SUCCESS };
174 }
175 }