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