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