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