bf0838e91890614f9e0fdbf48af0b541aba8dcdd
[e-mobility-charging-stations-simulator.git] / src / charging-station / IdTagsCache.ts
1 import { type FSWatcher, readFileSync } from 'node:fs';
2
3 import type { ChargingStation } from './ChargingStation';
4 import { getIdTagsFile } from './ChargingStationUtils';
5 import { FileType, IdTagDistribution } from '../types';
6 import {
7 handleFileException,
8 isNotEmptyString,
9 logPrefix,
10 logger,
11 secureRandom,
12 watchJsonFile,
13 } from '../utils';
14
15 type IdTagsCacheValueType = {
16 idTags: string[];
17 idTagsFileWatcher: FSWatcher | undefined;
18 };
19
20 export 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
37 /**
38 * Gets one idtag from the cache given the distribution
39 * Must be called after checking the cache is not an empty array
40 *
41 * @param distribution -
42 * @param chargingStation -
43 * @param connectorId -
44 * @returns
45 */
46 public getIdTag(
47 distribution: IdTagDistribution,
48 chargingStation: ChargingStation,
49 connectorId: number
50 ): string {
51 const hashId = chargingStation.stationInfo.hashId;
52 const idTagsFile = getIdTagsFile(chargingStation.stationInfo);
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
65 /**
66 * Gets all idtags from the cache
67 * Must be called after checking the cache is not an empty array
68 *
69 * @param file -
70 * @returns
71 */
72 public getIdTags(file: string): string[] | undefined {
73 if (this.hasIdTagsCache(file) === false) {
74 this.setIdTagsCache(file, this.getIdTagsFromFile(file));
75 }
76 return this.getIdTagsCache(file);
77 }
78
79 public deleteIdTags(file: string): boolean {
80 return this.deleteIdTagsCache(file) && this.deleteIdTagsCacheIndexes(file);
81 }
82
83 private getRandomIdTag(hashId: string, file: string): string {
84 const idTags = this.getIdTags(file);
85 const addressableKey = this.getIdTagsCacheIndexesAddressableKey(file, hashId);
86 this.idTagsCachesAddressableIndexes.set(
87 addressableKey,
88 Math.floor(secureRandom() * idTags.length)
89 );
90 return idTags[this.idTagsCachesAddressableIndexes.get(addressableKey)];
91 }
92
93 private getRoundRobinIdTag(hashId: string, file: string): string {
94 const idTags = this.getIdTags(file);
95 const addressableKey = this.getIdTagsCacheIndexesAddressableKey(file, hashId);
96 const idTagIndex = this.idTagsCachesAddressableIndexes.get(addressableKey) ?? 0;
97 const idTag = idTags[idTagIndex];
98 this.idTagsCachesAddressableIndexes.set(
99 addressableKey,
100 idTagIndex === idTags.length - 1 ? 0 : idTagIndex + 1
101 );
102 return idTag;
103 }
104
105 private getConnectorAffinityIdTag(chargingStation: ChargingStation, connectorId: number): string {
106 const file = getIdTagsFile(chargingStation.stationInfo);
107 const idTags = this.getIdTags(file);
108 const addressableKey = this.getIdTagsCacheIndexesAddressableKey(
109 file,
110 chargingStation.stationInfo.hashId
111 );
112 this.idTagsCachesAddressableIndexes.set(
113 addressableKey,
114 (chargingStation.index - 1 + (connectorId - 1)) % idTags.length
115 );
116 return idTags[this.idTagsCachesAddressableIndexes.get(addressableKey)];
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,
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 ),
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
165 private deleteIdTagsCacheIndexes(file: string): boolean {
166 let deleted: boolean[];
167 for (const [key] of this.idTagsCachesAddressableIndexes) {
168 if (key.startsWith(file)) {
169 deleted.push(this.idTagsCachesAddressableIndexes.delete(key));
170 }
171 }
172 return !deleted.some((value) => value === false);
173 }
174
175 private getIdTagsCacheIndexesAddressableKey(prefix: string, uid: string): string {
176 return `${prefix}${uid}`;
177 }
178
179 private getIdTagsFromFile(file: string): string[] {
180 if (isNotEmptyString(file)) {
181 try {
182 return JSON.parse(readFileSync(file, 'utf8')) as string[];
183 } catch (error) {
184 handleFileException(
185 file,
186 FileType.Authorization,
187 error as NodeJS.ErrnoException,
188 this.logPrefix(file)
189 );
190 }
191 }
192 return [];
193 }
194
195 private logPrefix = (file: string): string => {
196 return logPrefix(` Id tags cache for id tags file '${file}' |`);
197 };
198 }