build: switch to NodeNext module resolution
[e-mobility-charging-stations-simulator.git] / src / charging-station / IdTagsCache.ts
CommitLineData
d972af76 1import { type FSWatcher, readFileSync } from 'node:fs';
f911a4af 2
a6ef1ece
JB
3import type { ChargingStation } from './ChargingStation.js';
4import { getIdTagsFile } from './Helpers.js';
5import { FileType, IdTagDistribution } from '../types/index.js';
9bf0ef23
JB
6import {
7 handleFileException,
8 isNotEmptyString,
9 logPrefix,
1f8f6332 10 logger,
9bf0ef23 11 secureRandom,
1f8f6332 12 watchJsonFile,
a6ef1ece 13} from '../utils/index.js';
f911a4af 14
e1d9a0f4 15interface IdTagsCacheValueType {
f911a4af 16 idTags: string[];
1f8f6332 17 idTagsFileWatcher: FSWatcher | undefined;
e1d9a0f4 18}
f911a4af
JB
19
20export class IdTagsCache {
21 private static instance: IdTagsCache | null = null;
22 private readonly idTagsCaches: Map<string, IdTagsCacheValueType>;
23 private readonly idTagsCachesAddressableIndexes: Map<string, number>;
24
25 private constructor() {
26 this.idTagsCaches = new Map<string, IdTagsCacheValueType>();
27 this.idTagsCachesAddressableIndexes = new Map<string, number>();
28 }
29
30 public static getInstance(): IdTagsCache {
31 if (IdTagsCache.instance === null) {
32 IdTagsCache.instance = new IdTagsCache();
33 }
34 return IdTagsCache.instance;
35 }
36
7b5dbe91 37 /**
361c98f5 38 * Gets one idtag from the cache given the distribution
7b5dbe91
JB
39 * Must be called after checking the cache is not an empty array
40 *
4c8782ee
JB
41 * @param distribution -
42 * @param chargingStation -
43 * @param connectorId -
7b5dbe91
JB
44 * @returns
45 */
f911a4af
JB
46 public getIdTag(
47 distribution: IdTagDistribution,
48 chargingStation: ChargingStation,
5edd8ba0 49 connectorId: number,
f911a4af
JB
50 ): string {
51 const hashId = chargingStation.stationInfo.hashId;
e1d9a0f4 52 const idTagsFile = getIdTagsFile(chargingStation.stationInfo)!;
f911a4af
JB
53 switch (distribution) {
54 case IdTagDistribution.RANDOM:
55 return this.getRandomIdTag(hashId, idTagsFile);
56 case IdTagDistribution.ROUND_ROBIN:
57 return this.getRoundRobinIdTag(hashId, idTagsFile);
58 case IdTagDistribution.CONNECTOR_AFFINITY:
59 return this.getConnectorAffinityIdTag(chargingStation, connectorId);
60 default:
61 return this.getRoundRobinIdTag(hashId, idTagsFile);
62 }
63 }
64
7b5dbe91 65 /**
361c98f5 66 * Gets all idtags from the cache
7b5dbe91
JB
67 * Must be called after checking the cache is not an empty array
68 *
4c8782ee 69 * @param file -
7b5dbe91
JB
70 * @returns
71 */
f911a4af
JB
72 public getIdTags(file: string): string[] | undefined {
73 if (this.hasIdTagsCache(file) === false) {
6082281f 74 this.setIdTagsCache(file, this.getIdTagsFromFile(file));
f911a4af
JB
75 }
76 return this.getIdTagsCache(file);
77 }
78
79 public deleteIdTags(file: string): boolean {
26a17d93 80 return this.deleteIdTagsCache(file) && this.deleteIdTagsCacheIndexes(file);
f911a4af
JB
81 }
82
83 private getRandomIdTag(hashId: string, file: string): string {
e1d9a0f4 84 const idTags = this.getIdTags(file)!;
32e8c8a5 85 const addressableKey = this.getIdTagsCacheIndexesAddressableKey(file, hashId);
f911a4af
JB
86 this.idTagsCachesAddressableIndexes.set(
87 addressableKey,
5edd8ba0 88 Math.floor(secureRandom() * idTags.length),
f911a4af 89 );
e1d9a0f4 90 return idTags[this.idTagsCachesAddressableIndexes.get(addressableKey)!];
f911a4af
JB
91 }
92
93 private getRoundRobinIdTag(hashId: string, file: string): string {
e1d9a0f4 94 const idTags = this.getIdTags(file)!;
32e8c8a5 95 const addressableKey = this.getIdTagsCacheIndexesAddressableKey(file, hashId);
f911a4af
JB
96 const idTagIndex = this.idTagsCachesAddressableIndexes.get(addressableKey) ?? 0;
97 const idTag = idTags[idTagIndex];
98 this.idTagsCachesAddressableIndexes.set(
99 addressableKey,
5edd8ba0 100 idTagIndex === idTags.length - 1 ? 0 : idTagIndex + 1,
f911a4af
JB
101 );
102 return idTag;
103 }
104
105 private getConnectorAffinityIdTag(chargingStation: ChargingStation, connectorId: number): string {
e1d9a0f4
JB
106 const file = getIdTagsFile(chargingStation.stationInfo)!;
107 const idTags = this.getIdTags(file)!;
728e01f0
JB
108 const addressableKey = this.getIdTagsCacheIndexesAddressableKey(
109 file,
5edd8ba0 110 chargingStation.stationInfo.hashId,
728e01f0 111 );
f911a4af
JB
112 this.idTagsCachesAddressableIndexes.set(
113 addressableKey,
5edd8ba0 114 (chargingStation.index - 1 + (connectorId - 1)) % idTags.length,
f911a4af 115 );
e1d9a0f4 116 return idTags[this.idTagsCachesAddressableIndexes.get(addressableKey)!];
f911a4af
JB
117 }
118
119 private hasIdTagsCache(file: string): boolean {
120 return this.idTagsCaches.has(file);
121 }
122
123 private setIdTagsCache(file: string, idTags: string[]) {
124 return this.idTagsCaches.set(file, {
125 idTags,
1f8f6332
JB
126 idTagsFileWatcher: watchJsonFile(
127 file,
128 FileType.Authorization,
129 this.logPrefix(file),
130 undefined,
131 (event, filename) => {
132 if (isNotEmptyString(filename) && event === 'change') {
133 try {
134 logger.debug(
135 `${this.logPrefix(file)} ${FileType.Authorization} file have changed, reload`,
136 );
137 this.deleteIdTagsCache(file);
138 this.deleteIdTagsCacheIndexes(file);
139 } catch (error) {
140 handleFileException(
141 file,
142 FileType.Authorization,
143 error as NodeJS.ErrnoException,
144 this.logPrefix(file),
145 {
146 throwError: false,
147 },
148 );
149 }
150 }
151 },
152 ),
f911a4af
JB
153 });
154 }
155
156 private getIdTagsCache(file: string): string[] | undefined {
157 return this.idTagsCaches.get(file)?.idTags;
158 }
159
160 private deleteIdTagsCache(file: string): boolean {
161 this.idTagsCaches.get(file)?.idTagsFileWatcher?.close();
162 return this.idTagsCaches.delete(file);
163 }
164
26a17d93 165 private deleteIdTagsCacheIndexes(file: string): boolean {
e1d9a0f4 166 const deleted: boolean[] = [];
f911a4af
JB
167 for (const [key] of this.idTagsCachesAddressableIndexes) {
168 if (key.startsWith(file)) {
26a17d93 169 deleted.push(this.idTagsCachesAddressableIndexes.delete(key));
f911a4af
JB
170 }
171 }
26a17d93 172 return !deleted.some((value) => value === false);
f911a4af
JB
173 }
174
32e8c8a5 175 private getIdTagsCacheIndexesAddressableKey(prefix: string, uid: string): string {
7af183e7
JB
176 return `${prefix}${uid}`;
177 }
178
f911a4af 179 private getIdTagsFromFile(file: string): string[] {
9bf0ef23 180 if (isNotEmptyString(file)) {
f911a4af 181 try {
d972af76 182 return JSON.parse(readFileSync(file, 'utf8')) as string[];
f911a4af 183 } catch (error) {
fa5995d6 184 handleFileException(
f911a4af
JB
185 file,
186 FileType.Authorization,
187 error as NodeJS.ErrnoException,
5edd8ba0 188 this.logPrefix(file),
f911a4af
JB
189 );
190 }
f911a4af 191 }
7b5dbe91 192 return [];
f911a4af
JB
193 }
194
195 private logPrefix = (file: string): string => {
9bf0ef23 196 return logPrefix(` Id tags cache for id tags file '${file}' |`);
f911a4af
JB
197 };
198}