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