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