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