ceea0e5bd084c3cada92d63c4c88d7905517d63e
[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 { 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 public getIdTag(
31 distribution: IdTagDistribution,
32 chargingStation: ChargingStation,
33 connectorId: number
34 ): string {
35 const hashId = chargingStation.stationInfo.hashId;
36 const idTagsFile = ChargingStationUtils.getIdTagsFile(chargingStation.stationInfo);
37 switch (distribution) {
38 case IdTagDistribution.RANDOM:
39 return this.getRandomIdTag(hashId, idTagsFile);
40 case IdTagDistribution.ROUND_ROBIN:
41 return this.getRoundRobinIdTag(hashId, idTagsFile);
42 case IdTagDistribution.CONNECTOR_AFFINITY:
43 return this.getConnectorAffinityIdTag(chargingStation, connectorId);
44 default:
45 return this.getRoundRobinIdTag(hashId, idTagsFile);
46 }
47 }
48
49 public getIdTags(file: string): string[] | undefined {
50 if (this.hasIdTagsCache(file) === false) {
51 this.setIdTagsCache(file, this.getIdTagsFromFile(file));
52 }
53 return this.getIdTagsCache(file);
54 }
55
56 public deleteIdTags(file: string): boolean {
57 return this.deleteIdTagsCache(file);
58 }
59
60 private getRandomIdTag(hashId: string, file: string): string {
61 const idTags = this.getIdTags(file);
62 const addressableKey = this.getIdTagsCacheIndexesAddressableKey(file, hashId);
63 this.idTagsCachesAddressableIndexes.set(
64 addressableKey,
65 Math.floor(Utils.secureRandom() * idTags.length)
66 );
67 return idTags[this.idTagsCachesAddressableIndexes.get(addressableKey)];
68 }
69
70 private getRoundRobinIdTag(hashId: string, file: string): string {
71 const idTags = this.getIdTags(file);
72 const addressableKey = this.getIdTagsCacheIndexesAddressableKey(file, hashId);
73 const idTagIndex = this.idTagsCachesAddressableIndexes.get(addressableKey) ?? 0;
74 const idTag = idTags[idTagIndex];
75 this.idTagsCachesAddressableIndexes.set(
76 addressableKey,
77 idTagIndex === idTags.length - 1 ? 0 : idTagIndex + 1
78 );
79 return idTag;
80 }
81
82 private getConnectorAffinityIdTag(chargingStation: ChargingStation, connectorId: number): string {
83 const file = ChargingStationUtils.getIdTagsFile(chargingStation.stationInfo);
84 const idTags = this.getIdTags(file);
85 const addressableKey = this.getIdTagsCacheIndexesAddressableKey(
86 file,
87 chargingStation.stationInfo.hashId
88 );
89 this.idTagsCachesAddressableIndexes.set(
90 addressableKey,
91 (chargingStation.index - 1 + (connectorId - 1)) % idTags.length
92 );
93 return idTags[this.idTagsCachesAddressableIndexes.get(addressableKey)];
94 }
95
96 private hasIdTagsCache(file: string): boolean {
97 return this.idTagsCaches.has(file);
98 }
99
100 private setIdTagsCache(file: string, idTags: string[]) {
101 return this.idTagsCaches.set(file, {
102 idTags,
103 idTagsFileWatcher: FileUtils.watchJsonFile(
104 file,
105 FileType.Authorization,
106 this.logPrefix(file),
107 undefined,
108 (event, filename) => {
109 if (Utils.isNotEmptyString(filename) && event === 'change') {
110 try {
111 logger.debug(
112 `${this.logPrefix(file)} ${FileType.Authorization} file have changed, reload`
113 );
114 this.deleteIdTagsCache(file);
115 this.deleteIdTagsCacheIndexes(file);
116 } catch (error) {
117 FileUtils.handleFileException(
118 file,
119 FileType.Authorization,
120 error as NodeJS.ErrnoException,
121 this.logPrefix(file),
122 {
123 throwError: false,
124 }
125 );
126 }
127 }
128 }
129 ),
130 });
131 }
132
133 private getIdTagsCache(file: string): string[] | undefined {
134 return this.idTagsCaches.get(file)?.idTags;
135 }
136
137 private deleteIdTagsCache(file: string): boolean {
138 this.idTagsCaches.get(file)?.idTagsFileWatcher?.close();
139 return this.idTagsCaches.delete(file);
140 }
141
142 private deleteIdTagsCacheIndexes(file: string): void {
143 for (const [key] of this.idTagsCachesAddressableIndexes) {
144 if (key.startsWith(file)) {
145 this.idTagsCachesAddressableIndexes.delete(key);
146 }
147 }
148 }
149
150 private getIdTagsCacheIndexesAddressableKey(prefix: string, uid: string): string {
151 return `${prefix}${uid}`;
152 }
153
154 private getIdTagsFromFile(file: string): string[] {
155 let idTags: string[] = [];
156 if (file) {
157 try {
158 // Load id tags file
159 idTags = JSON.parse(fs.readFileSync(file, 'utf8')) as string[];
160 } catch (error) {
161 FileUtils.handleFileException(
162 file,
163 FileType.Authorization,
164 error as NodeJS.ErrnoException,
165 this.logPrefix(file)
166 );
167 }
168 } else {
169 logger.info(`${this.logPrefix(file)} No id tags file given`);
170 }
171 return idTags;
172 }
173
174 private logPrefix = (file: string): string => {
175 return Utils.logPrefix(` Id tags cache for id tags file '${file}' |`);
176 };
177 }