feat!: handle Set at JSON serialization to string
authorJérôme Benoit <jerome.benoit@sap.com>
Sun, 10 Mar 2024 16:05:34 +0000 (17:05 +0100)
committerJérôme Benoit <jerome.benoit@sap.com>
Sun, 10 Mar 2024 16:05:34 +0000 (17:05 +0100)
Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
15 files changed:
src/charging-station/Bootstrap.ts
src/charging-station/ui-server/UIHttpServer.ts
src/charging-station/ui-server/UIWebSocketServer.ts
src/performance/PerformanceStatistics.ts
src/performance/storage/JsonFileStorage.ts
src/types/MapStringifyFormat.ts [new file with mode: 0644]
src/types/SimulatorState.ts [new file with mode: 0644]
src/types/Statistics.ts
src/types/UIProtocol.ts
src/types/index.ts
src/utils/MessageChannelUtils.ts
src/utils/Utils.ts
src/utils/index.ts
ui/web/src/composables/UIClient.ts
ui/web/start.js

index d31b1e5dc657cf7bf49a84e7f1cbdaa1d17b0884..ef64c8fcdd123d9005dacadf6e5ee12f8b035a6c 100644 (file)
@@ -22,16 +22,15 @@ import {
   type ChargingStationWorkerMessageData,
   ChargingStationWorkerMessageEvents,
   ConfigurationSection,
-  type InternalTemplateStatistics,
   ProcedureName,
   type SimulatorState,
   type Statistics,
   type StorageConfiguration,
+  type TemplateStatistics,
   type UIServerConfiguration,
   type WorkerConfiguration
 } from '../types/index.js'
 import {
-  buildTemplateStatisticsPayload,
   Configuration,
   Constants,
   formatDurationMilliSeconds,
@@ -63,7 +62,7 @@ export class Bootstrap extends EventEmitter {
   private workerImplementation?: WorkerAbstract<ChargingStationWorkerData>
   private readonly uiServer: AbstractUIServer
   private storage?: Storage
-  private readonly templateStatistics: Map<string, InternalTemplateStatistics>
+  private readonly templateStatistics: Map<string, TemplateStatistics>
   private readonly version: string = version
   private initializedCounters: boolean
   private started: boolean
@@ -86,7 +85,7 @@ export class Bootstrap extends EventEmitter {
     this.uiServer = UIServerFactory.getUIServerImplementation(
       Configuration.getConfigurationSection<UIServerConfiguration>(ConfigurationSection.uiServer)
     )
-    this.templateStatistics = new Map<string, InternalTemplateStatistics>()
+    this.templateStatistics = new Map<string, TemplateStatistics>()
     this.initializedCounters = false
     this.initializeCounters()
     Configuration.configurationChangeCallback = async () => {
@@ -118,7 +117,7 @@ export class Bootstrap extends EventEmitter {
     return {
       version: this.version,
       started: this.started,
-      templateStatistics: buildTemplateStatisticsPayload(this.templateStatistics)
+      templateStatistics: this.templateStatistics
     }
   }
 
index b5c30b31375c825c318a65350a3c204164a8613d..5bcae7beff6ed84fc6d0e202437361bc7f1c3725 100644 (file)
@@ -5,6 +5,7 @@ import { StatusCodes } from 'http-status-codes'
 import { BaseError } from '../../exception/index.js'
 import {
   ApplicationProtocolVersion,
+  MapStringifyFormat,
   type ProcedureName,
   type Protocol,
   type ProtocolRequest,
@@ -18,7 +19,7 @@ import {
   Constants,
   generateUUID,
   isNotEmptyString,
-  JSONStringifyWithMapSupport,
+  JSONStringify,
   logger,
   logPrefix
 } from '../../utils/index.js'
@@ -61,7 +62,7 @@ export class UIHttpServer extends AbstractUIServer {
           .writeHead(this.responseStatusToStatusCode(payload.status), {
             'Content-Type': 'application/json'
           })
-          .end(JSONStringifyWithMapSupport(payload))
+          .end(JSONStringify(payload, undefined, MapStringifyFormat.object))
       } else {
         logger.error(
           `${this.logPrefix(moduleName, 'sendResponse')} Response for unknown request id: ${uuid}`
index 29a6567549105a7cc8e202b9249c99c8fa6b4200..bf0a6c444749201fe06d2cdcc6f24b10fa09582a 100644 (file)
@@ -5,6 +5,7 @@ import { StatusCodes } from 'http-status-codes'
 import { type RawData, WebSocket, WebSocketServer } from 'ws'
 
 import {
+  MapStringifyFormat,
   type ProtocolRequest,
   type ProtocolResponse,
   type UIServerConfiguration,
@@ -14,7 +15,7 @@ import {
   Constants,
   getWebSocketCloseEventStatusString,
   isNotEmptyString,
-  JSONStringifyWithMapSupport,
+  JSONStringify,
   logger,
   logPrefix,
   validateUUID
@@ -136,7 +137,7 @@ export class UIWebSocketServer extends AbstractUIServer {
       if (this.hasResponseHandler(responseId)) {
         const ws = this.responseHandlers.get(responseId) as WebSocket
         if (ws.readyState === WebSocket.OPEN) {
-          ws.send(JSONStringifyWithMapSupport(response))
+          ws.send(JSONStringify(response, undefined, MapStringifyFormat.object))
         } else {
           logger.error(
             `${this.logPrefix(
index bf333dfd4c9c1449334bd979933b7d868d0e0c02..59d697d1ec76559d36c175f68256da4ebe5d9d18 100644 (file)
@@ -11,6 +11,7 @@ import {
   ConfigurationSection,
   type IncomingRequestCommand,
   type LogConfiguration,
+  MapStringifyFormat,
   MessageType,
   type RequestCommand,
   type Statistics,
@@ -27,7 +28,7 @@ import {
   extractTimeSeriesValues,
   formatDurationSeconds,
   generateUUID,
-  JSONStringifyWithMapSupport,
+  JSONStringify,
   logger,
   logPrefix,
   max,
@@ -214,7 +215,7 @@ export class PerformanceStatistics {
     logger.info(this.logPrefix(), {
       ...this.statistics,
       statisticsData: JSON.parse(
-        JSONStringifyWithMapSupport(this.statistics.statisticsData)
+        JSONStringify(this.statistics.statisticsData, undefined, MapStringifyFormat.object)
       ) as Map<string | RequestCommand | IncomingRequestCommand, StatisticsData>
     })
   }
index 648b9c62f292474b0b6dcd3d871af4c0849fa36d..592504a77c73ae104035da36f6415f4bf2986ffb 100644 (file)
@@ -4,13 +4,8 @@ import { closeSync, existsSync, mkdirSync, openSync, writeSync } from 'node:fs'
 import { dirname } from 'node:path'
 
 import { BaseError } from '../../exception/index.js'
-import { FileType, type Statistics } from '../../types/index.js'
-import {
-  AsyncLock,
-  AsyncLockType,
-  handleFileException,
-  JSONStringifyWithMapSupport
-} from '../../utils/index.js'
+import { FileType, MapStringifyFormat, type Statistics } from '../../types/index.js'
+import { AsyncLock, AsyncLockType, handleFileException, JSONStringify } from '../../utils/index.js'
 import { Storage } from './Storage.js'
 
 export class JsonFileStorage extends Storage {
@@ -28,7 +23,7 @@ export class JsonFileStorage extends Storage {
       writeSync(
         // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
         this.fd!,
-        JSONStringifyWithMapSupport([...this.getPerformanceStatistics()], 2),
+        JSONStringify([...this.getPerformanceStatistics()], 2, MapStringifyFormat.object),
         0,
         'utf8'
       )
diff --git a/src/types/MapStringifyFormat.ts b/src/types/MapStringifyFormat.ts
new file mode 100644 (file)
index 0000000..8543ef2
--- /dev/null
@@ -0,0 +1,4 @@
+export enum MapStringifyFormat {
+  array = 'array',
+  object = 'object'
+}
diff --git a/src/types/SimulatorState.ts b/src/types/SimulatorState.ts
new file mode 100644 (file)
index 0000000..d3816ed
--- /dev/null
@@ -0,0 +1,7 @@
+import type { TemplateStatistics } from './Statistics.js'
+
+export interface SimulatorState {
+  version: string
+  started: boolean
+  templateStatistics: Map<string, TemplateStatistics>
+}
index 9abd0a89982d7be726eb23d13825d2262b42696d..81ba1d564e11c5018caabda84e16284968b51f12 100644 (file)
@@ -32,7 +32,7 @@ export interface Statistics extends WorkerData {
   statisticsData: Map<string | RequestCommand | IncomingRequestCommand, StatisticsData>
 }
 
-export interface InternalTemplateStatistics {
+export interface TemplateStatistics {
   configured: number
   added: number
   started: number
index f53696dae914b02ed0c3a24a66d89939393707e4..00d939317c5e93fbacb20c01487b116da27bb9e6 100644 (file)
@@ -86,9 +86,3 @@ export interface TemplateStatistics extends JsonObject {
   started: number
   indexes: number[]
 }
-
-export interface SimulatorState extends JsonObject {
-  version: string
-  started: boolean
-  templateStatistics: Record<string, TemplateStatistics>
-}
index 023107fd3c0f9a7f309a26c6dbb4d43429aaa438..e24d7f5437cfbff9afe4e204bb185d2749100af1 100644 (file)
@@ -51,6 +51,7 @@ export type { HandleErrorParams } from './Error.js'
 export type { EvseStatus, EvseTemplate } from './Evse.js'
 export { FileType } from './FileType.js'
 export type { JsonObject, JsonType } from './JsonType.js'
+export { MapStringifyFormat } from './MapStringifyFormat.js'
 export type {
   MeasurandPerPhaseSampledValueTemplates,
   SampledValueTemplate
@@ -247,10 +248,11 @@ export {
   type StopTransactionResponse
 } from './ocpp/Transaction.js'
 export { PerformanceRecord } from './orm/entities/PerformanceRecord.js'
+export type { SimulatorState } from './SimulatorState.js'
 export type {
-  InternalTemplateStatistics,
   Statistics,
   StatisticsData,
+  TemplateStatistics,
   TimestampedData
 } from './Statistics.js'
 export { DBName, StorageType } from './Storage.js'
@@ -265,9 +267,7 @@ export {
   ProtocolVersion,
   type RequestPayload,
   type ResponsePayload,
-  ResponseStatus,
-  type SimulatorState,
-  type TemplateStatistics
+  ResponseStatus
 } from './UIProtocol.js'
 export {
   WebSocketCloseEventStatusCode,
index 0306d60edd743e037f02221e9587065074541265..1af746daafe67840e536c95a8ef719171b37de4a 100644 (file)
@@ -3,9 +3,7 @@ import {
   type ChargingStationData,
   type ChargingStationWorkerMessage,
   ChargingStationWorkerMessageEvents,
-  type InternalTemplateStatistics,
-  type Statistics,
-  type TemplateStatistics
+  type Statistics
 } from '../types/index.js'
 import {
   buildChargingStationAutomaticTransactionGeneratorConfiguration,
@@ -13,7 +11,6 @@ import {
   buildEvsesStatus,
   OutputFormat
 } from './ChargingStationConfigurationUtils.js'
-import { clone } from './Utils.js'
 
 export const buildAddedMessage = (
   chargingStation: ChargingStation
@@ -89,13 +86,3 @@ export const buildChargingStationDataPayload = (
     })
   }
 }
-
-export const buildTemplateStatisticsPayload = (
-  map: Map<string, InternalTemplateStatistics>
-): Record<string, TemplateStatistics> => {
-  map = clone(map)
-  for (const value of map.values()) {
-    (value as unknown as TemplateStatistics).indexes = [...value.indexes]
-  }
-  return Object.fromEntries(map.entries() as unknown as Array<[string, TemplateStatistics]>)
-}
index 69f0cd4280f5e82df830c83f885efc2f77c5feaf..fd3f4e2b27a744033097f73b294c0d0351534e01 100644 (file)
@@ -15,7 +15,8 @@ import {
 
 import {
   type EmptyObject,
-  type ProtocolResponse,
+  type JsonType,
+  MapStringifyFormat,
   type TimestampedData,
   WebSocketCloseEventStatusString
 } from '../types/index.js'
@@ -298,22 +299,26 @@ export const secureRandom = (): number => {
   return getRandomValues(new Uint32Array(1))[0] / 0x100000000
 }
 
-export const JSONStringifyWithMapSupport = (
-  object:
-  | Record<string, unknown>
-  | Array<Record<string, unknown>>
-  | Map<unknown, unknown>
-  | ProtocolResponse,
-  space?: string | number
-): string => {
+export const JSONStringify = <
+  T extends JsonType | Array<Record<string, unknown>> | Map<string, Record<string, unknown>>
+>(
+    object: T,
+    space?: string | number,
+    mapFormat?: MapStringifyFormat
+  ): string => {
   return JSON.stringify(
     object,
     (_, value: Record<string, unknown>) => {
       if (value instanceof Map) {
-        return {
-          dataType: 'Map',
-          value: [...value]
+        switch (mapFormat) {
+          case MapStringifyFormat.object:
+            return { ...Object.fromEntries<Map<string, T>>(value.entries()) }
+          case MapStringifyFormat.array:
+          default:
+            return [...value]
         }
+      } else if (value instanceof Set) {
+        return [...value] as unknown[]
       }
       return value
     },
index 81dc04d2b3b94979b3a64d36b549e3abcc54039a..daa67094d76ed4126fc9a57df1dd8e51dd137053 100644 (file)
@@ -25,7 +25,6 @@ export {
   buildPerformanceStatisticsMessage,
   buildStartedMessage,
   buildStoppedMessage,
-  buildTemplateStatisticsPayload,
   buildUpdatedMessage
 } from './MessageChannelUtils.js'
 export { average, median, nthPercentile, stdDeviation } from './StatisticUtils.js'
@@ -52,7 +51,7 @@ export {
   isNotEmptyArray,
   isNotEmptyString,
   isValidDate,
-  JSONStringifyWithMapSupport,
+  JSONStringify,
   logPrefix,
   max,
   min,
index 781854dbb759fdfc542aa535573fa7f61eb1e606..fcbf191f3c9d2dabfc6fa6354359d703eb67e28b 100644 (file)
@@ -246,13 +246,15 @@ export class UIClient {
     }
 
     if (!Array.isArray(response)) {
-      useToast().error(`Response not an array`)
-      console.error(`Response not an array:`, response)
+      useToast().error('Response not an array')
+      console.error('Response not an array:', response)
       return
     }
 
     const [uuid, responsePayload] = response
 
+    console.log('responsePayload', responsePayload.state?.templateStatistics)
+
     if (this.responseHandlers.has(uuid)) {
       const { procedureName, resolve, reject } = this.responseHandlers.get(uuid)!
       switch (responsePayload.status) {
index 9a325e569a1b4a2724adceeb52bd315d8f43f053..46a8bb75884f67da0c4b2ecfc9138bff44fb7ab2 100644 (file)
@@ -12,8 +12,6 @@ const uiPath = join(dirname(fileURLToPath(import.meta.url)), './dist')
 
 const serve = serveStatic(uiPath)
 
-const server = createServer(function onRequest(req, res) {
-  serve(req, res, finalhandler(req, res))
-})
+const server = createServer((req, res) => serve(req, res, finalhandler(req, res)))
 
 server.listen(PORT, () => console.info(`Web UI running at: http://localhost:${PORT}`))