chore: switch coding style to JS standard
[e-mobility-charging-stations-simulator.git] / src / utils / Configuration.ts
1 import { type FSWatcher, readFileSync, watch } from 'node:fs'
2 import { dirname, join } from 'node:path'
3 import { env } from 'node:process'
4 import { fileURLToPath } from 'node:url'
5
6 import chalk from 'chalk'
7 import merge from 'just-merge'
8
9 import {
10 buildPerformanceUriFilePath,
11 checkWorkerElementsPerWorker,
12 checkWorkerProcessType,
13 getDefaultPerformanceStorageUri,
14 handleFileException,
15 logPrefix
16 } from './ConfigurationUtils.js'
17 import { Constants } from './Constants.js'
18 import { hasOwnProp, isCFEnvironment, isUndefined, once } from './Utils.js'
19 import {
20 ApplicationProtocol,
21 type ConfigurationData,
22 ConfigurationSection,
23 FileType,
24 type LogConfiguration,
25 type StationTemplateUrl,
26 type StorageConfiguration,
27 StorageType,
28 SupervisionUrlDistribution,
29 type UIServerConfiguration,
30 type WorkerConfiguration
31 } from '../types/index.js'
32 import {
33 DEFAULT_ELEMENT_START_DELAY,
34 DEFAULT_POOL_MAX_SIZE,
35 DEFAULT_POOL_MIN_SIZE,
36 DEFAULT_WORKER_START_DELAY,
37 WorkerProcessType
38 } from '../worker/index.js'
39
40 type ConfigurationSectionType =
41 | LogConfiguration
42 | StorageConfiguration
43 | WorkerConfiguration
44 | UIServerConfiguration
45
46 // eslint-disable-next-line @typescript-eslint/no-extraneous-class
47 export class Configuration {
48 public static configurationChangeCallback: () => Promise<void>
49
50 private static readonly configurationFile = join(
51 dirname(fileURLToPath(import.meta.url)),
52 'assets',
53 'config.json'
54 )
55
56 private static configurationFileReloading = false
57 private static configurationData?: ConfigurationData
58 private static configurationFileWatcher?: FSWatcher
59 private static readonly configurationSectionCache = new Map<
60 ConfigurationSection,
61 ConfigurationSectionType
62 >([
63 [ConfigurationSection.log, Configuration.buildLogSection()],
64 [ConfigurationSection.performanceStorage, Configuration.buildPerformanceStorageSection()],
65 [ConfigurationSection.worker, Configuration.buildWorkerSection()],
66 [ConfigurationSection.uiServer, Configuration.buildUIServerSection()]
67 ])
68
69 private constructor () {
70 // This is intentional
71 }
72
73 public static getConfigurationSection<T extends ConfigurationSectionType>(
74 sectionName: ConfigurationSection
75 ): T {
76 if (!Configuration.isConfigurationSectionCached(sectionName)) {
77 Configuration.cacheConfigurationSection(sectionName)
78 }
79 return Configuration.configurationSectionCache.get(sectionName) as T
80 }
81
82 public static getStationTemplateUrls (): StationTemplateUrl[] | undefined {
83 const checkDeprecatedConfigurationKeysOnce = once(
84 Configuration.checkDeprecatedConfigurationKeys.bind(Configuration),
85 Configuration
86 )
87 checkDeprecatedConfigurationKeysOnce()
88 return Configuration.getConfigurationData()?.stationTemplateUrls
89 }
90
91 public static getSupervisionUrls (): string | string[] | undefined {
92 if (
93 !isUndefined(
94 Configuration.getConfigurationData()?.['supervisionURLs' as keyof ConfigurationData]
95 )
96 ) {
97 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
98 Configuration.getConfigurationData()!.supervisionUrls = Configuration.getConfigurationData()![
99 'supervisionURLs' as keyof ConfigurationData
100 ] as string | string[]
101 }
102 return Configuration.getConfigurationData()?.supervisionUrls
103 }
104
105 public static getSupervisionUrlDistribution (): SupervisionUrlDistribution | undefined {
106 return hasOwnProp(Configuration.getConfigurationData(), 'supervisionUrlDistribution')
107 ? Configuration.getConfigurationData()?.supervisionUrlDistribution
108 : SupervisionUrlDistribution.ROUND_ROBIN
109 }
110
111 public static workerPoolInUse (): boolean {
112 return [WorkerProcessType.dynamicPool, WorkerProcessType.fixedPool].includes(
113 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
114 Configuration.getConfigurationSection<WorkerConfiguration>(ConfigurationSection.worker)
115 .processType!
116 )
117 }
118
119 public static workerDynamicPoolInUse (): boolean {
120 return (
121 Configuration.getConfigurationSection<WorkerConfiguration>(ConfigurationSection.worker)
122 .processType === WorkerProcessType.dynamicPool
123 )
124 }
125
126 private static isConfigurationSectionCached (sectionName: ConfigurationSection): boolean {
127 return Configuration.configurationSectionCache.has(sectionName)
128 }
129
130 private static cacheConfigurationSection (sectionName: ConfigurationSection): void {
131 switch (sectionName) {
132 case ConfigurationSection.log:
133 Configuration.configurationSectionCache.set(sectionName, Configuration.buildLogSection())
134 break
135 case ConfigurationSection.performanceStorage:
136 Configuration.configurationSectionCache.set(
137 sectionName,
138 Configuration.buildPerformanceStorageSection()
139 )
140 break
141 case ConfigurationSection.worker:
142 Configuration.configurationSectionCache.set(sectionName, Configuration.buildWorkerSection())
143 break
144 case ConfigurationSection.uiServer:
145 Configuration.configurationSectionCache.set(
146 sectionName,
147 Configuration.buildUIServerSection()
148 )
149 break
150 default:
151 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
152 throw new Error(`Unknown configuration section '${sectionName}'`)
153 }
154 }
155
156 private static buildUIServerSection (): UIServerConfiguration {
157 let uiServerConfiguration: UIServerConfiguration = {
158 enabled: false,
159 type: ApplicationProtocol.WS,
160 options: {
161 host: Constants.DEFAULT_UI_SERVER_HOST,
162 port: Constants.DEFAULT_UI_SERVER_PORT
163 }
164 }
165 if (hasOwnProp(Configuration.getConfigurationData(), ConfigurationSection.uiServer)) {
166 uiServerConfiguration = merge<UIServerConfiguration>(
167 uiServerConfiguration,
168 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
169 Configuration.getConfigurationData()!.uiServer!
170 )
171 }
172 if (isCFEnvironment()) {
173 delete uiServerConfiguration.options?.host
174 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
175 uiServerConfiguration.options!.port = parseInt(env.PORT!)
176 }
177 return uiServerConfiguration
178 }
179
180 private static buildPerformanceStorageSection (): StorageConfiguration {
181 let storageConfiguration: StorageConfiguration = {
182 enabled: false,
183 type: StorageType.JSON_FILE,
184 uri: getDefaultPerformanceStorageUri(StorageType.JSON_FILE)
185 }
186 if (hasOwnProp(Configuration.getConfigurationData(), ConfigurationSection.performanceStorage)) {
187 storageConfiguration = {
188 ...storageConfiguration,
189 ...Configuration.getConfigurationData()?.performanceStorage,
190 ...(Configuration.getConfigurationData()?.performanceStorage?.type ===
191 StorageType.JSON_FILE &&
192 Configuration.getConfigurationData()?.performanceStorage?.uri != null && {
193 uri: buildPerformanceUriFilePath(
194 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
195 new URL(Configuration.getConfigurationData()!.performanceStorage!.uri!).pathname
196 )
197 })
198 }
199 }
200 return storageConfiguration
201 }
202
203 private static buildLogSection (): LogConfiguration {
204 const defaultLogConfiguration: LogConfiguration = {
205 enabled: true,
206 file: 'logs/combined.log',
207 errorFile: 'logs/error.log',
208 statisticsInterval: Constants.DEFAULT_LOG_STATISTICS_INTERVAL,
209 level: 'info',
210 format: 'simple',
211 rotate: true
212 }
213 const deprecatedLogConfiguration: LogConfiguration = {
214 ...(hasOwnProp(Configuration.getConfigurationData(), 'logEnabled') && {
215 enabled: Configuration.getConfigurationData()?.logEnabled
216 }),
217 ...(hasOwnProp(Configuration.getConfigurationData(), 'logFile') && {
218 file: Configuration.getConfigurationData()?.logFile
219 }),
220 ...(hasOwnProp(Configuration.getConfigurationData(), 'logErrorFile') && {
221 errorFile: Configuration.getConfigurationData()?.logErrorFile
222 }),
223 ...(hasOwnProp(Configuration.getConfigurationData(), 'logStatisticsInterval') && {
224 statisticsInterval: Configuration.getConfigurationData()?.logStatisticsInterval
225 }),
226 ...(hasOwnProp(Configuration.getConfigurationData(), 'logLevel') && {
227 level: Configuration.getConfigurationData()?.logLevel
228 }),
229 ...(hasOwnProp(Configuration.getConfigurationData(), 'logConsole') && {
230 console: Configuration.getConfigurationData()?.logConsole
231 }),
232 ...(hasOwnProp(Configuration.getConfigurationData(), 'logFormat') && {
233 format: Configuration.getConfigurationData()?.logFormat
234 }),
235 ...(hasOwnProp(Configuration.getConfigurationData(), 'logRotate') && {
236 rotate: Configuration.getConfigurationData()?.logRotate
237 }),
238 ...(hasOwnProp(Configuration.getConfigurationData(), 'logMaxFiles') && {
239 maxFiles: Configuration.getConfigurationData()?.logMaxFiles
240 }),
241 ...(hasOwnProp(Configuration.getConfigurationData(), 'logMaxSize') && {
242 maxSize: Configuration.getConfigurationData()?.logMaxSize
243 })
244 }
245 const logConfiguration: LogConfiguration = {
246 ...defaultLogConfiguration,
247 ...deprecatedLogConfiguration,
248 ...(hasOwnProp(Configuration.getConfigurationData(), ConfigurationSection.log) &&
249 Configuration.getConfigurationData()?.log)
250 }
251 return logConfiguration
252 }
253
254 private static buildWorkerSection (): WorkerConfiguration {
255 const defaultWorkerConfiguration: WorkerConfiguration = {
256 processType: WorkerProcessType.workerSet,
257 startDelay: DEFAULT_WORKER_START_DELAY,
258 elementsPerWorker: 'auto',
259 elementStartDelay: DEFAULT_ELEMENT_START_DELAY,
260 poolMinSize: DEFAULT_POOL_MIN_SIZE,
261 poolMaxSize: DEFAULT_POOL_MAX_SIZE
262 }
263 const deprecatedWorkerConfiguration: WorkerConfiguration = {
264 ...(hasOwnProp(Configuration.getConfigurationData(), 'workerProcess') && {
265 processType: Configuration.getConfigurationData()?.workerProcess
266 }),
267 ...(hasOwnProp(Configuration.getConfigurationData(), 'workerStartDelay') && {
268 startDelay: Configuration.getConfigurationData()?.workerStartDelay
269 }),
270 ...(hasOwnProp(Configuration.getConfigurationData(), 'chargingStationsPerWorker') && {
271 elementsPerWorker: Configuration.getConfigurationData()?.chargingStationsPerWorker
272 }),
273 ...(hasOwnProp(Configuration.getConfigurationData(), 'elementStartDelay') && {
274 elementStartDelay: Configuration.getConfigurationData()?.elementStartDelay
275 }),
276 ...(hasOwnProp(Configuration.getConfigurationData(), 'workerPoolMinSize') && {
277 poolMinSize: Configuration.getConfigurationData()?.workerPoolMinSize
278 }),
279 ...(hasOwnProp(Configuration.getConfigurationData(), 'workerPoolMaxSize') && {
280 poolMaxSize: Configuration.getConfigurationData()?.workerPoolMaxSize
281 })
282 }
283 hasOwnProp(Configuration.getConfigurationData(), 'workerPoolStrategy') &&
284 delete Configuration.getConfigurationData()?.workerPoolStrategy
285 const workerConfiguration: WorkerConfiguration = {
286 ...defaultWorkerConfiguration,
287 ...deprecatedWorkerConfiguration,
288 ...(hasOwnProp(Configuration.getConfigurationData(), ConfigurationSection.worker) &&
289 Configuration.getConfigurationData()?.worker)
290 }
291 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
292 checkWorkerProcessType(workerConfiguration.processType!)
293 checkWorkerElementsPerWorker(workerConfiguration.elementsPerWorker)
294 return workerConfiguration
295 }
296
297 private static checkDeprecatedConfigurationKeys (): void {
298 // connection timeout
299 Configuration.warnDeprecatedConfigurationKey(
300 'autoReconnectTimeout',
301 undefined,
302 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead"
303 )
304 Configuration.warnDeprecatedConfigurationKey(
305 'connectionTimeout',
306 undefined,
307 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead"
308 )
309 // connection retries
310 Configuration.warnDeprecatedConfigurationKey(
311 'autoReconnectMaxRetries',
312 undefined,
313 'Use it in charging station template instead'
314 )
315 // station template url(s)
316 Configuration.warnDeprecatedConfigurationKey(
317 'stationTemplateURLs',
318 undefined,
319 "Use 'stationTemplateUrls' instead"
320 )
321 !isUndefined(
322 Configuration.getConfigurationData()?.['stationTemplateURLs' as keyof ConfigurationData]
323 ) &&
324 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
325 (Configuration.getConfigurationData()!.stationTemplateUrls =
326 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
327 Configuration.getConfigurationData()![
328 'stationTemplateURLs' as keyof ConfigurationData
329 ] as StationTemplateUrl[])
330 Configuration.getConfigurationData()?.stationTemplateUrls.forEach(
331 (stationTemplateUrl: StationTemplateUrl) => {
332 if (!isUndefined(stationTemplateUrl?.['numberOfStation' as keyof StationTemplateUrl])) {
333 console.error(
334 `${chalk.green(logPrefix())} ${chalk.red(
335 `Deprecated configuration key 'numberOfStation' usage for template file '${stationTemplateUrl.file}' in 'stationTemplateUrls'. Use 'numberOfStations' instead`
336 )}`
337 )
338 }
339 }
340 )
341 // supervision url(s)
342 Configuration.warnDeprecatedConfigurationKey(
343 'supervisionURLs',
344 undefined,
345 "Use 'supervisionUrls' instead"
346 )
347 // supervision urls distribution
348 Configuration.warnDeprecatedConfigurationKey(
349 'distributeStationToTenantEqually',
350 undefined,
351 "Use 'supervisionUrlDistribution' instead"
352 )
353 Configuration.warnDeprecatedConfigurationKey(
354 'distributeStationsToTenantsEqually',
355 undefined,
356 "Use 'supervisionUrlDistribution' instead"
357 )
358 // worker section
359 Configuration.warnDeprecatedConfigurationKey(
360 'useWorkerPool',
361 undefined,
362 `Use '${ConfigurationSection.worker}' section to define the type of worker process model instead`
363 )
364 Configuration.warnDeprecatedConfigurationKey(
365 'workerProcess',
366 undefined,
367 `Use '${ConfigurationSection.worker}' section to define the type of worker process model instead`
368 )
369 Configuration.warnDeprecatedConfigurationKey(
370 'workerStartDelay',
371 undefined,
372 `Use '${ConfigurationSection.worker}' section to define the worker start delay instead`
373 )
374 Configuration.warnDeprecatedConfigurationKey(
375 'chargingStationsPerWorker',
376 undefined,
377 `Use '${ConfigurationSection.worker}' section to define the number of element(s) per worker instead`
378 )
379 Configuration.warnDeprecatedConfigurationKey(
380 'elementStartDelay',
381 undefined,
382 `Use '${ConfigurationSection.worker}' section to define the worker's element start delay instead`
383 )
384 Configuration.warnDeprecatedConfigurationKey(
385 'workerPoolMinSize',
386 undefined,
387 `Use '${ConfigurationSection.worker}' section to define the worker pool minimum size instead`
388 )
389 Configuration.warnDeprecatedConfigurationKey(
390 'workerPoolSize',
391 undefined,
392 `Use '${ConfigurationSection.worker}' section to define the worker pool maximum size instead`
393 )
394 Configuration.warnDeprecatedConfigurationKey(
395 'workerPoolMaxSize',
396 undefined,
397 `Use '${ConfigurationSection.worker}' section to define the worker pool maximum size instead`
398 )
399 Configuration.warnDeprecatedConfigurationKey(
400 'workerPoolStrategy',
401 undefined,
402 `Use '${ConfigurationSection.worker}' section to define the worker pool strategy instead`
403 )
404 Configuration.warnDeprecatedConfigurationKey(
405 'poolStrategy',
406 ConfigurationSection.worker,
407 'Not publicly exposed to end users'
408 )
409 if (
410 Configuration.getConfigurationData()?.worker?.processType ===
411 ('staticPool' as WorkerProcessType)
412 ) {
413 console.error(
414 `${chalk.green(logPrefix())} ${chalk.red(
415 `Deprecated configuration 'staticPool' value usage in worker section 'processType' field. Use '${WorkerProcessType.fixedPool}' value instead`
416 )}`
417 )
418 }
419 // log section
420 Configuration.warnDeprecatedConfigurationKey(
421 'logEnabled',
422 undefined,
423 `Use '${ConfigurationSection.log}' section to define the logging enablement instead`
424 )
425 Configuration.warnDeprecatedConfigurationKey(
426 'logFile',
427 undefined,
428 `Use '${ConfigurationSection.log}' section to define the log file instead`
429 )
430 Configuration.warnDeprecatedConfigurationKey(
431 'logErrorFile',
432 undefined,
433 `Use '${ConfigurationSection.log}' section to define the log error file instead`
434 )
435 Configuration.warnDeprecatedConfigurationKey(
436 'logConsole',
437 undefined,
438 `Use '${ConfigurationSection.log}' section to define the console logging enablement instead`
439 )
440 Configuration.warnDeprecatedConfigurationKey(
441 'logStatisticsInterval',
442 undefined,
443 `Use '${ConfigurationSection.log}' section to define the log statistics interval instead`
444 )
445 Configuration.warnDeprecatedConfigurationKey(
446 'logLevel',
447 undefined,
448 `Use '${ConfigurationSection.log}' section to define the log level instead`
449 )
450 Configuration.warnDeprecatedConfigurationKey(
451 'logFormat',
452 undefined,
453 `Use '${ConfigurationSection.log}' section to define the log format instead`
454 )
455 Configuration.warnDeprecatedConfigurationKey(
456 'logRotate',
457 undefined,
458 `Use '${ConfigurationSection.log}' section to define the log rotation enablement instead`
459 )
460 Configuration.warnDeprecatedConfigurationKey(
461 'logMaxFiles',
462 undefined,
463 `Use '${ConfigurationSection.log}' section to define the log maximum files instead`
464 )
465 Configuration.warnDeprecatedConfigurationKey(
466 'logMaxSize',
467 undefined,
468 `Use '${ConfigurationSection.log}' section to define the log maximum size instead`
469 )
470 // performanceStorage section
471 Configuration.warnDeprecatedConfigurationKey(
472 'URI',
473 ConfigurationSection.performanceStorage,
474 "Use 'uri' instead"
475 )
476 // uiServer section
477 if (hasOwnProp(Configuration.getConfigurationData(), 'uiWebSocketServer')) {
478 console.error(
479 `${chalk.green(logPrefix())} ${chalk.red(
480 `Deprecated configuration section 'uiWebSocketServer' usage. Use '${ConfigurationSection.uiServer}' instead`
481 )}`
482 )
483 }
484 }
485
486 private static warnDeprecatedConfigurationKey (
487 key: string,
488 sectionName?: string,
489 logMsgToAppend = ''
490 ): void {
491 if (
492 sectionName != null &&
493 !isUndefined(
494 Configuration.getConfigurationData()?.[sectionName as keyof ConfigurationData]
495 ) &&
496 !isUndefined(
497 (
498 Configuration.getConfigurationData()?.[sectionName as keyof ConfigurationData] as Record<
499 string,
500 unknown
501 >
502 )?.[key]
503 )
504 ) {
505 console.error(
506 `${chalk.green(logPrefix())} ${chalk.red(
507 `Deprecated configuration key '${key}' usage in section '${sectionName}'${
508 logMsgToAppend.trim().length > 0 ? `. ${logMsgToAppend}` : ''
509 }`
510 )}`
511 )
512 } else if (
513 !isUndefined(Configuration.getConfigurationData()?.[key as keyof ConfigurationData])
514 ) {
515 console.error(
516 `${chalk.green(logPrefix())} ${chalk.red(
517 `Deprecated configuration key '${key}' usage${
518 logMsgToAppend.trim().length > 0 ? `. ${logMsgToAppend}` : ''
519 }`
520 )}`
521 )
522 }
523 }
524
525 private static getConfigurationData (): ConfigurationData | undefined {
526 if (Configuration.configurationData == null) {
527 try {
528 Configuration.configurationData = JSON.parse(
529 readFileSync(Configuration.configurationFile, 'utf8')
530 ) as ConfigurationData
531 if (Configuration.configurationFileWatcher == null) {
532 Configuration.configurationFileWatcher = Configuration.getConfigurationFileWatcher()
533 }
534 } catch (error) {
535 handleFileException(
536 Configuration.configurationFile,
537 FileType.Configuration,
538 error as NodeJS.ErrnoException,
539 logPrefix()
540 )
541 }
542 }
543 return Configuration.configurationData
544 }
545
546 private static getConfigurationFileWatcher (): FSWatcher | undefined {
547 try {
548 return watch(Configuration.configurationFile, (event, filename): void => {
549 if (
550 !Configuration.configurationFileReloading &&
551 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
552 filename!.trim()!.length > 0 &&
553 event === 'change'
554 ) {
555 Configuration.configurationFileReloading = true
556 const consoleWarnOnce = once(console.warn, this)
557 consoleWarnOnce(
558 `${chalk.green(logPrefix())} ${chalk.yellow(
559 `${FileType.Configuration} ${this.configurationFile} file have changed, reload`
560 )}`
561 )
562 delete Configuration.configurationData
563 Configuration.configurationSectionCache.clear()
564 if (!isUndefined(Configuration.configurationChangeCallback)) {
565 Configuration.configurationChangeCallback()
566 .catch((error) => {
567 throw typeof error === 'string' ? new Error(error) : error
568 })
569 .finally(() => {
570 Configuration.configurationFileReloading = false
571 })
572 } else {
573 Configuration.configurationFileReloading = false
574 }
575 }
576 })
577 } catch (error) {
578 handleFileException(
579 Configuration.configurationFile,
580 FileType.Configuration,
581 error as NodeJS.ErrnoException,
582 logPrefix()
583 )
584 }
585 }
586 }