5c0a454a05d6615389de331d19b72832bd4724a4
[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 './Helpers';
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 interface 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 // FIXME: Disabled until the spurious configuration file change detection is identified
127 // idTagsFileWatcher: watchJsonFile(
128 // file,
129 // FileType.Authorization,
130 // this.logPrefix(file),
131 // undefined,
132 // (event, filename) => {
133 // if (isNotEmptyString(filename) && event === 'change') {
134 // try {
135 // logger.debug(
136 // `${this.logPrefix(file)} ${FileType.Authorization} file have changed, reload`,
137 // );
138 // this.deleteIdTagsCache(file);
139 // this.deleteIdTagsCacheIndexes(file);
140 // } catch (error) {
141 // handleFileException(
142 // file,
143 // FileType.Authorization,
144 // error as NodeJS.ErrnoException,
145 // this.logPrefix(file),
146 // {
147 // throwError: false,
148 // },
149 // );
150 // }
151 // }
152 // },
153 // ),
154 });
155 }
156
157 private getIdTagsCache(file: string): string[] | undefined {
158 return this.idTagsCaches.get(file)?.idTags;
159 }
160
161 private deleteIdTagsCache(file: string): boolean {
162 this.idTagsCaches.get(file)?.idTagsFileWatcher?.close();
163 return this.idTagsCaches.delete(file);
164 }
165
166 private deleteIdTagsCacheIndexes(file: string): boolean {
167 const deleted: boolean[] = [];
168 for (const [key] of this.idTagsCachesAddressableIndexes) {
169 if (key.startsWith(file)) {
170 deleted.push(this.idTagsCachesAddressableIndexes.delete(key));
171 }
172 }
173 return !deleted.some((value) => value === false);
174 }
175
176 private getIdTagsCacheIndexesAddressableKey(prefix: string, uid: string): string {
177 return `${prefix}${uid}`;
178 }
179
180 private getIdTagsFromFile(file: string): string[] {
181 if (isNotEmptyString(file)) {
182 try {
183 return JSON.parse(readFileSync(file, 'utf8')) as string[];
184 } catch (error) {
185 handleFileException(
186 file,
187 FileType.Authorization,
188 error as NodeJS.ErrnoException,
189 this.logPrefix(file),
190 );
191 }
192 }
193 return [];
194 }
195
196 private logPrefix = (file: string): string => {
197 return logPrefix(` Id tags cache for id tags file '${file}' |`);
198 };
199 }