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