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