test: add ErrorUtils test
[e-mobility-charging-stations-simulator.git] / src / utils / Configuration.ts
index 8b0b1c8fa532c3afdd879dd6f6418576e5531139..118ea64011aca7d5a9f435c922d2b01c8f9d12de 100644 (file)
@@ -1,23 +1,14 @@
-import { type FSWatcher, readFileSync, watch } from 'node:fs'
+import { existsSync, type FSWatcher, readFileSync, watch } from 'node:fs'
 import { dirname, join } from 'node:path'
 import { env } from 'node:process'
 import { fileURLToPath } from 'node:url'
 
 import chalk from 'chalk'
-import merge from 'just-merge'
+import { mergeDeepRight, once } from 'rambda'
 
-import {
-  buildPerformanceUriFilePath,
-  checkWorkerElementsPerWorker,
-  checkWorkerProcessType,
-  getDefaultPerformanceStorageUri,
-  handleFileException,
-  logPrefix
-} from './ConfigurationUtils.js'
-import { Constants } from './Constants.js'
-import { hasOwnProp, isCFEnvironment, once } from './Utils.js'
 import {
   ApplicationProtocol,
+  ApplicationProtocolVersion,
   type ConfigurationData,
   ConfigurationSection,
   FileType,
@@ -30,12 +21,22 @@ import {
   type WorkerConfiguration
 } from '../types/index.js'
 import {
-  DEFAULT_ELEMENT_START_DELAY,
+  DEFAULT_ELEMENT_ADD_DELAY,
   DEFAULT_POOL_MAX_SIZE,
   DEFAULT_POOL_MIN_SIZE,
   DEFAULT_WORKER_START_DELAY,
   WorkerProcessType
 } from '../worker/index.js'
+import {
+  buildPerformanceUriFilePath,
+  checkWorkerElementsPerWorker,
+  checkWorkerProcessType,
+  getDefaultPerformanceStorageUri,
+  handleFileException,
+  logPrefix
+} from './ConfigurationUtils.js'
+import { Constants } from './Constants.js'
+import { hasOwnProp, isCFEnvironment } from './Utils.js'
 
 type ConfigurationSectionType =
   | LogConfiguration
@@ -43,28 +44,80 @@ type ConfigurationSectionType =
   | WorkerConfiguration
   | UIServerConfiguration
 
+const defaultUIServerConfiguration: UIServerConfiguration = {
+  enabled: false,
+  type: ApplicationProtocol.WS,
+  version: ApplicationProtocolVersion.VERSION_11,
+  options: {
+    host: Constants.DEFAULT_UI_SERVER_HOST,
+    port: Constants.DEFAULT_UI_SERVER_PORT
+  }
+}
+
+const defaultStorageConfiguration: StorageConfiguration = {
+  enabled: true,
+  type: StorageType.NONE
+}
+
+const defaultLogConfiguration: LogConfiguration = {
+  enabled: true,
+  file: 'logs/combined.log',
+  errorFile: 'logs/error.log',
+  statisticsInterval: Constants.DEFAULT_LOG_STATISTICS_INTERVAL,
+  level: 'info',
+  format: 'simple',
+  rotate: true
+}
+
+const defaultWorkerConfiguration: WorkerConfiguration = {
+  processType: WorkerProcessType.workerSet,
+  startDelay: DEFAULT_WORKER_START_DELAY,
+  elementsPerWorker: 'auto',
+  elementAddDelay: DEFAULT_ELEMENT_ADD_DELAY,
+  poolMinSize: DEFAULT_POOL_MIN_SIZE,
+  poolMaxSize: DEFAULT_POOL_MAX_SIZE
+}
+
 // eslint-disable-next-line @typescript-eslint/no-extraneous-class
 export class Configuration {
-  public static configurationChangeCallback: () => Promise<void>
-
-  private static readonly configurationFile = join(
-    dirname(fileURLToPath(import.meta.url)),
-    'assets',
-    'config.json'
-  )
+  public static configurationChangeCallback?: () => Promise<void>
 
+  private static configurationFile: string | undefined
   private static configurationFileReloading = false
   private static configurationData?: ConfigurationData
   private static configurationFileWatcher?: FSWatcher
-  private static readonly configurationSectionCache = new Map<
-  ConfigurationSection,
-  ConfigurationSectionType
-  >([
-    [ConfigurationSection.log, Configuration.buildLogSection()],
-    [ConfigurationSection.performanceStorage, Configuration.buildPerformanceStorageSection()],
-    [ConfigurationSection.worker, Configuration.buildWorkerSection()],
-    [ConfigurationSection.uiServer, Configuration.buildUIServerSection()]
-  ])
+  private static configurationSectionCache: Map<ConfigurationSection, ConfigurationSectionType>
+
+  static {
+    const configurationFile = join(dirname(fileURLToPath(import.meta.url)), 'assets', 'config.json')
+    if (existsSync(configurationFile)) {
+      Configuration.configurationFile = configurationFile
+    } else {
+      console.error(
+        `${chalk.green(logPrefix())} ${chalk.red(
+          `Configuration file '${configurationFile}' not found, using default configuration`
+        )}`
+      )
+      Configuration.configurationData = {
+        stationTemplateUrls: [],
+        supervisionUrls: 'ws://localhost:8180/steve/websocket/CentralSystemService',
+        supervisionUrlDistribution: SupervisionUrlDistribution.ROUND_ROBIN,
+        uiServer: defaultUIServerConfiguration,
+        performanceStorage: defaultStorageConfiguration,
+        log: defaultLogConfiguration,
+        worker: defaultWorkerConfiguration
+      }
+    }
+    Configuration.configurationSectionCache = new Map<
+    ConfigurationSection,
+    ConfigurationSectionType
+    >([
+      [ConfigurationSection.log, Configuration.buildLogSection()],
+      [ConfigurationSection.performanceStorage, Configuration.buildPerformanceStorageSection()],
+      [ConfigurationSection.worker, Configuration.buildWorkerSection()],
+      [ConfigurationSection.uiServer, Configuration.buildUIServerSection()]
+    ])
+  }
 
   private constructor () {
     // This is intentional
@@ -81,8 +134,7 @@ export class Configuration {
 
   public static getStationTemplateUrls (): StationTemplateUrl[] | undefined {
     const checkDeprecatedConfigurationKeysOnce = once(
-      Configuration.checkDeprecatedConfigurationKeys.bind(Configuration),
-      Configuration
+      Configuration.checkDeprecatedConfigurationKeys.bind(Configuration)
     )
     checkDeprecatedConfigurationKeysOnce()
     return Configuration.getConfigurationData()?.stationTemplateUrls
@@ -90,8 +142,7 @@ export class Configuration {
 
   public static getSupervisionUrls (): string | string[] | undefined {
     if (
-      Configuration.getConfigurationData()?.['supervisionURLs' as keyof ConfigurationData] !==
-      undefined
+      Configuration.getConfigurationData()?.['supervisionURLs' as keyof ConfigurationData] != null
     ) {
       // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
       Configuration.getConfigurationData()!.supervisionUrls = Configuration.getConfigurationData()![
@@ -153,16 +204,9 @@ export class Configuration {
   }
 
   private static buildUIServerSection (): UIServerConfiguration {
-    let uiServerConfiguration: UIServerConfiguration = {
-      enabled: false,
-      type: ApplicationProtocol.WS,
-      options: {
-        host: Constants.DEFAULT_UI_SERVER_HOST,
-        port: Constants.DEFAULT_UI_SERVER_PORT
-      }
-    }
+    let uiServerConfiguration: UIServerConfiguration = defaultUIServerConfiguration
     if (hasOwnProp(Configuration.getConfigurationData(), ConfigurationSection.uiServer)) {
-      uiServerConfiguration = merge<UIServerConfiguration>(
+      uiServerConfiguration = mergeDeepRight(
         uiServerConfiguration,
         // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
         Configuration.getConfigurationData()!.uiServer!
@@ -171,23 +215,40 @@ export class Configuration {
     if (isCFEnvironment()) {
       delete uiServerConfiguration.options?.host
       // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-      uiServerConfiguration.options!.port = parseInt(env.PORT!)
+      uiServerConfiguration.options!.port = Number.parseInt(env.PORT!)
     }
     return uiServerConfiguration
   }
 
   private static buildPerformanceStorageSection (): StorageConfiguration {
-    let storageConfiguration: StorageConfiguration = {
-      enabled: false,
-      type: StorageType.JSON_FILE,
-      uri: getDefaultPerformanceStorageUri(StorageType.JSON_FILE)
+    let storageConfiguration: StorageConfiguration
+    switch (Configuration.getConfigurationData()?.performanceStorage?.type) {
+      case StorageType.SQLITE:
+        storageConfiguration = {
+          enabled: false,
+          type: StorageType.SQLITE,
+          uri: getDefaultPerformanceStorageUri(StorageType.SQLITE)
+        }
+        break
+      case StorageType.JSON_FILE:
+        storageConfiguration = {
+          enabled: false,
+          type: StorageType.JSON_FILE,
+          uri: getDefaultPerformanceStorageUri(StorageType.JSON_FILE)
+        }
+        break
+      case StorageType.NONE:
+      default:
+        storageConfiguration = defaultStorageConfiguration
+        break
     }
     if (hasOwnProp(Configuration.getConfigurationData(), ConfigurationSection.performanceStorage)) {
       storageConfiguration = {
         ...storageConfiguration,
         ...Configuration.getConfigurationData()?.performanceStorage,
-        ...(Configuration.getConfigurationData()?.performanceStorage?.type ===
-          StorageType.JSON_FILE &&
+        ...((Configuration.getConfigurationData()?.performanceStorage?.type ===
+          StorageType.JSON_FILE ||
+          Configuration.getConfigurationData()?.performanceStorage?.type === StorageType.SQLITE) &&
           Configuration.getConfigurationData()?.performanceStorage?.uri != null && {
           uri: buildPerformanceUriFilePath(
             // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@@ -200,15 +261,6 @@ export class Configuration {
   }
 
   private static buildLogSection (): LogConfiguration {
-    const defaultLogConfiguration: LogConfiguration = {
-      enabled: true,
-      file: 'logs/combined.log',
-      errorFile: 'logs/error.log',
-      statisticsInterval: Constants.DEFAULT_LOG_STATISTICS_INTERVAL,
-      level: 'info',
-      format: 'simple',
-      rotate: true
-    }
     const deprecatedLogConfiguration: LogConfiguration = {
       ...(hasOwnProp(Configuration.getConfigurationData(), 'logEnabled') && {
         enabled: Configuration.getConfigurationData()?.logEnabled
@@ -251,14 +303,6 @@ export class Configuration {
   }
 
   private static buildWorkerSection (): WorkerConfiguration {
-    const defaultWorkerConfiguration: WorkerConfiguration = {
-      processType: WorkerProcessType.workerSet,
-      startDelay: DEFAULT_WORKER_START_DELAY,
-      elementsPerWorker: 'auto',
-      elementStartDelay: DEFAULT_ELEMENT_START_DELAY,
-      poolMinSize: DEFAULT_POOL_MIN_SIZE,
-      poolMaxSize: DEFAULT_POOL_MAX_SIZE
-    }
     const deprecatedWorkerConfiguration: WorkerConfiguration = {
       ...(hasOwnProp(Configuration.getConfigurationData(), 'workerProcess') && {
         processType: Configuration.getConfigurationData()?.workerProcess
@@ -269,8 +313,11 @@ export class Configuration {
       ...(hasOwnProp(Configuration.getConfigurationData(), 'chargingStationsPerWorker') && {
         elementsPerWorker: Configuration.getConfigurationData()?.chargingStationsPerWorker
       }),
-      ...(hasOwnProp(Configuration.getConfigurationData(), 'elementStartDelay') && {
-        elementStartDelay: Configuration.getConfigurationData()?.elementStartDelay
+      ...(hasOwnProp(Configuration.getConfigurationData(), 'elementAddDelay') && {
+        elementAddDelay: Configuration.getConfigurationData()?.elementAddDelay
+      }),
+      ...(hasOwnProp(Configuration.getConfigurationData()?.worker, 'elementStartDelay') && {
+        elementAddDelay: Configuration.getConfigurationData()?.worker?.elementStartDelay
       }),
       ...(hasOwnProp(Configuration.getConfigurationData(), 'workerPoolMinSize') && {
         poolMinSize: Configuration.getConfigurationData()?.workerPoolMinSize
@@ -317,8 +364,8 @@ export class Configuration {
       undefined,
       "Use 'stationTemplateUrls' instead"
     )
-    Configuration.getConfigurationData()?.['stationTemplateURLs' as keyof ConfigurationData] !==
-      undefined &&
+    Configuration.getConfigurationData()?.['stationTemplateURLs' as keyof ConfigurationData] !=
+      null &&
       // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
       (Configuration.getConfigurationData()!.stationTemplateUrls =
         // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@@ -327,7 +374,8 @@ export class Configuration {
         ] as StationTemplateUrl[])
     Configuration.getConfigurationData()?.stationTemplateUrls.forEach(
       (stationTemplateUrl: StationTemplateUrl) => {
-        if (stationTemplateUrl?.['numberOfStation' as keyof StationTemplateUrl] !== undefined) {
+        // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+        if (stationTemplateUrl['numberOfStation' as keyof StationTemplateUrl] != null) {
           console.error(
             `${chalk.green(logPrefix())} ${chalk.red(
               `Deprecated configuration key 'numberOfStation' usage for template file '${stationTemplateUrl.file}' in 'stationTemplateUrls'. Use 'numberOfStations' instead`
@@ -375,9 +423,9 @@ export class Configuration {
       `Use '${ConfigurationSection.worker}' section to define the number of element(s) per worker instead`
     )
     Configuration.warnDeprecatedConfigurationKey(
-      'elementStartDelay',
+      'elementAddDelay',
       undefined,
-      `Use '${ConfigurationSection.worker}' section to define the worker's element start delay instead`
+      `Use '${ConfigurationSection.worker}' section to define the worker's element add delay instead`
     )
     Configuration.warnDeprecatedConfigurationKey(
       'workerPoolMinSize',
@@ -404,6 +452,11 @@ export class Configuration {
       ConfigurationSection.worker,
       'Not publicly exposed to end users'
     )
+    Configuration.warnDeprecatedConfigurationKey(
+      'elementStartDelay',
+      ConfigurationSection.worker,
+      "Use 'elementAddDelay' instead"
+    )
     if (
       Configuration.getConfigurationData()?.worker?.processType ===
       ('staticPool' as WorkerProcessType)
@@ -483,30 +536,27 @@ export class Configuration {
 
   private static warnDeprecatedConfigurationKey (
     key: string,
-    sectionName?: string,
+    configurationSection?: ConfigurationSection,
     logMsgToAppend = ''
   ): void {
     if (
-      sectionName != null &&
-      Configuration.getConfigurationData()?.[sectionName as keyof ConfigurationData] !==
-        undefined &&
+      configurationSection != null &&
+      Configuration.getConfigurationData()?.[configurationSection as keyof ConfigurationData] !=
+        null &&
       (
-        Configuration.getConfigurationData()?.[sectionName as keyof ConfigurationData] as Record<
-        string,
-        unknown
-        >
-      )?.[key] !== undefined
+        Configuration.getConfigurationData()?.[
+          configurationSection as keyof ConfigurationData
+        ] as Record<string, unknown>
+      )[key] != null
     ) {
       console.error(
         `${chalk.green(logPrefix())} ${chalk.red(
-          `Deprecated configuration key '${key}' usage in section '${sectionName}'${
+          `Deprecated configuration key '${key}' usage in section '${configurationSection}'${
             logMsgToAppend.trim().length > 0 ? `. ${logMsgToAppend}` : ''
           }`
         )}`
       )
-    } else if (
-      Configuration.getConfigurationData()?.[key as keyof ConfigurationData] !== undefined
-    ) {
+    } else if (Configuration.getConfigurationData()?.[key as keyof ConfigurationData] != null) {
       console.error(
         `${chalk.green(logPrefix())} ${chalk.red(
           `Deprecated configuration key '${key}' usage${
@@ -517,8 +567,12 @@ export class Configuration {
     }
   }
 
-  private static getConfigurationData (): ConfigurationData | undefined {
-    if (Configuration.configurationData == null) {
+  public static getConfigurationData (): ConfigurationData | undefined {
+    if (
+      Configuration.configurationData == null &&
+      Configuration.configurationFile != null &&
+      Configuration.configurationFile.length > 0
+    ) {
       try {
         Configuration.configurationData = JSON.parse(
           readFileSync(Configuration.configurationFile, 'utf8')
@@ -539,16 +593,19 @@ export class Configuration {
   }
 
   private static getConfigurationFileWatcher (): FSWatcher | undefined {
+    if (Configuration.configurationFile == null || Configuration.configurationFile.length === 0) {
+      return
+    }
     try {
       return watch(Configuration.configurationFile, (event, filename): void => {
         if (
           !Configuration.configurationFileReloading &&
           // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-          filename!.trim()!.length > 0 &&
+          filename!.trim().length > 0 &&
           event === 'change'
         ) {
           Configuration.configurationFileReloading = true
-          const consoleWarnOnce = once(console.warn, this)
+          const consoleWarnOnce = once(console.warn)
           consoleWarnOnce(
             `${chalk.green(logPrefix())} ${chalk.yellow(
               `${FileType.Configuration} ${this.configurationFile} file have changed, reload`
@@ -556,9 +613,9 @@ export class Configuration {
           )
           delete Configuration.configurationData
           Configuration.configurationSectionCache.clear()
-          if (Configuration.configurationChangeCallback !== undefined) {
+          if (Configuration.configurationChangeCallback != null) {
             Configuration.configurationChangeCallback()
-              .catch((error) => {
+              .catch((error: unknown) => {
                 throw typeof error === 'string' ? new Error(error) : error
               })
               .finally(() => {