README.md: document added UI protocol features
[e-mobility-charging-stations-simulator.git] / src / charging-station / ui-server / ui-services / AbstractUIService.ts
CommitLineData
6c1761d4 1import type { RawData } from 'ws';
8114d10e
JB
2
3import BaseError from '../../../exception/BaseError';
5a010bf0 4import { Bootstrap } from '../../../internal';
6c1761d4 5import type { JsonType } from '../../../types/JsonType';
675fa8e3 6import {
32de5a57 7 ProcedureName,
675fa8e3
JB
8 ProtocolRequest,
9 ProtocolRequestHandler,
33cea517 10 ProtocolVersion,
89b7a234 11 RequestPayload,
32de5a57
LM
12 ResponsePayload,
13 ResponseStatus,
675fa8e3 14} from '../../../types/UIProtocol';
0d2cec76
JB
15import type {
16 BroadcastChannelProcedureName,
17 BroadcastChannelRequestPayload,
18} from '../../../types/WorkerBroadcastChannel';
675fa8e3 19import logger from '../../../utils/Logger';
0d2cec76 20import Utils from '../../../utils/Utils';
6c8f5d90 21import UIServiceWorkerBroadcastChannel from '../../UIServiceWorkerBroadcastChannel';
db2336d9 22import type { AbstractUIServer } from '../AbstractUIServer';
4198ad5c 23
32de5a57
LM
24const moduleName = 'AbstractUIService';
25
4198ad5c 26export default abstract class AbstractUIService {
02a6943a 27 protected readonly requestHandlers: Map<ProcedureName, ProtocolRequestHandler>;
53870cff
JB
28 private readonly version: ProtocolVersion;
29 private readonly uiServer: AbstractUIServer;
30 private readonly uiServiceWorkerBroadcastChannel: UIServiceWorkerBroadcastChannel;
0d2cec76 31 private readonly broadcastChannelRequests: Map<string, number>;
4198ad5c 32
33cea517
JB
33 constructor(uiServer: AbstractUIServer, version: ProtocolVersion) {
34 this.version = version;
675fa8e3 35 this.uiServer = uiServer;
02a6943a 36 this.requestHandlers = new Map<ProcedureName, ProtocolRequestHandler>([
32de5a57
LM
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)],
4198ad5c 40 ]);
6812b4e1 41 this.uiServiceWorkerBroadcastChannel = new UIServiceWorkerBroadcastChannel(this);
0d2cec76 42 this.broadcastChannelRequests = new Map<string, number>();
4198ad5c
JB
43 }
44
1f7fa4de 45 public async requestHandler(request: RawData | JsonType): Promise<void> {
98a5256a 46 let messageId: string;
32de5a57 47 let command: ProcedureName;
6c8f5d90 48 let requestPayload: RequestPayload | undefined;
32de5a57
LM
49 let responsePayload: ResponsePayload;
50 try {
97f0a1a5 51 [messageId, command, requestPayload] = this.requestValidation(request);
32de5a57 52
02a6943a 53 if (this.requestHandlers.has(command) === false) {
32de5a57
LM
54 throw new BaseError(
55 `${command} is not implemented to handle message payload ${JSON.stringify(
56 requestPayload,
57 null,
58 2
59 )}`
60 );
4198ad5c 61 }
89b7a234 62
6c8f5d90 63 // Call the request handler to build the response payload
02a6943a 64 responsePayload = await this.requestHandlers.get(command)(messageId, requestPayload);
32de5a57
LM
65 } catch (error) {
66 // Log
1f7fa4de 67 logger.error(`${this.logPrefix(moduleName, 'messageHandler')} Handle request error:`, error);
6c8f5d90
JB
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 }
0d2cec76 77 // Send response for payload not forwarded to broadcast channel
6c8f5d90 78 if (responsePayload !== undefined) {
1f7fa4de 79 this.sendResponse(messageId ?? 'error', responsePayload);
4198ad5c 80 }
6c8f5d90
JB
81 }
82
83 public sendRequest(
84 messageId: string,
85 procedureName: ProcedureName,
86 requestPayload: RequestPayload
87 ): void {
852a4c5f
JB
88 this.uiServer.sendRequest(
89 this.uiServer.buildProtocolRequest(messageId, procedureName, requestPayload)
90 );
6c8f5d90 91 }
32de5a57 92
6c8f5d90 93 public sendResponse(messageId: string, responsePayload: ResponsePayload): void {
852a4c5f 94 this.uiServer.sendResponse(this.uiServer.buildProtocolResponse(messageId, responsePayload));
4198ad5c
JB
95 }
96
6c8f5d90 97 public logPrefix(modName: string, methodName: string): string {
0d2cec76
JB
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;
0d2cec76 132 this.uiServiceWorkerBroadcastChannel.sendRequest([uuid, procedureName, payload]);
853ed24a 133 this.broadcastChannelRequests.set(uuid, expectedNumberOfResponses);
6c8f5d90
JB
134 }
135
49bc4b80 136 // Validate the raw data received from the UI server
1f7fa4de 137 private requestValidation(rawData: RawData | JsonType): ProtocolRequest {
4e3ff94d 138 // logger.debug(
1f7fa4de 139 // `${this.logPrefix(
4e3ff94d 140 // moduleName,
1f7fa4de
JB
141 // 'requestValidation'
142 // )} Data received in string format: ${rawData.toString()}`
4e3ff94d
JB
143 // );
144
32de5a57
LM
145 const data = JSON.parse(rawData.toString()) as JsonType[];
146
53e5fd67
JB
147 if (Array.isArray(data) === false) {
148 throw new BaseError('UI protocol request is not an array');
32de5a57
LM
149 }
150
151 if (data.length !== 3) {
152 throw new BaseError('UI protocol request is malformed');
153 }
154
155 return data as ProtocolRequest;
4198ad5c
JB
156 }
157
89b7a234
JB
158 private handleListChargingStations(): ResponsePayload {
159 // TODO: remove cast to unknown
32de5a57
LM
160 return {
161 status: ResponseStatus.SUCCESS,
bbc6c092 162 ...[...this.uiServer.chargingStations.values()],
89b7a234 163 } as unknown as ResponsePayload;
32de5a57
LM
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 };
4198ad5c
JB
174 }
175}