perf(simulator): build once address for tags caches addressable indexes
[e-mobility-charging-stations-simulator.git] / src / charging-station / AuthorizedTagsCache.ts
1 import fs from 'node:fs';
2
3 import { type ChargingStation, ChargingStationUtils } from './internal';
4 import { FileType, IdTagDistribution } from '../types';
5 import { FileUtils, Utils, logger } from '../utils';
6
7 type TagsCacheValueType = {
8 tags: string[];
9 tagsFileWatcher: fs.FSWatcher | undefined;
10 };
11
12 export class AuthorizedTagsCache {
13 private static instance: AuthorizedTagsCache | null = null;
14 private readonly tagsCaches: Map<string, TagsCacheValueType>;
15 private readonly tagsCachesAddressableIndexes: Map<string, number>;
16
17 private constructor() {
18 this.tagsCaches = new Map<string, TagsCacheValueType>();
19 this.tagsCachesAddressableIndexes = new Map<string, number>();
20 }
21
22 public static getInstance(): AuthorizedTagsCache {
23 if (AuthorizedTagsCache.instance === null) {
24 AuthorizedTagsCache.instance = new AuthorizedTagsCache();
25 }
26 return AuthorizedTagsCache.instance;
27 }
28
29 public getIdTag(
30 distribution: IdTagDistribution,
31 chargingStation: ChargingStation,
32 connectorId: number
33 ): string {
34 const hashId = chargingStation.stationInfo.hashId;
35 const authorizationFile = ChargingStationUtils.getAuthorizationFile(
36 chargingStation.stationInfo
37 );
38 switch (distribution) {
39 case IdTagDistribution.RANDOM:
40 return this.getRandomIdTag(hashId, authorizationFile);
41 case IdTagDistribution.ROUND_ROBIN:
42 return this.getRoundRobinIdTag(hashId, authorizationFile);
43 case IdTagDistribution.CONNECTOR_AFFINITY:
44 return this.getConnectorAffinityIdTag(chargingStation, connectorId);
45 default:
46 return this.getRoundRobinIdTag(hashId, authorizationFile);
47 }
48 }
49
50 public getAuthorizedTags(file: string): string[] | undefined {
51 if (this.hasTags(file) === false) {
52 this.setTags(file, this.getAuthorizedTagsFromFile(file));
53 }
54 return this.getTags(file);
55 }
56
57 public deleteAuthorizedTags(file: string): boolean {
58 return this.deleteTags(file);
59 }
60
61 private getRandomIdTag(hashId: string, file: string): string {
62 const tags = this.getAuthorizedTags(file);
63 const addressableKey = file + hashId;
64 this.tagsCachesAddressableIndexes.set(
65 addressableKey,
66 Math.floor(Utils.secureRandom() * tags.length)
67 );
68 return tags[this.tagsCachesAddressableIndexes.get(addressableKey)];
69 }
70
71 private getRoundRobinIdTag(hashId: string, file: string): string {
72 const tags = this.getAuthorizedTags(file);
73 const addressableKey = file + hashId;
74 const idTagIndex = this.tagsCachesAddressableIndexes.get(addressableKey) ?? 0;
75 const idTag = tags[idTagIndex];
76 this.tagsCachesAddressableIndexes.set(
77 addressableKey,
78 idTagIndex === tags.length - 1 ? 0 : idTagIndex + 1
79 );
80 return idTag;
81 }
82
83 private getConnectorAffinityIdTag(chargingStation: ChargingStation, connectorId: number): string {
84 const file = ChargingStationUtils.getAuthorizationFile(chargingStation.stationInfo);
85 const tags = this.getAuthorizedTags(file);
86 const hashId = chargingStation.stationInfo.hashId;
87 const addressableKey = file + hashId;
88 this.tagsCachesAddressableIndexes.set(
89 addressableKey,
90 (chargingStation.index - 1 + (connectorId - 1)) % tags.length
91 );
92 return tags[this.tagsCachesAddressableIndexes.get(addressableKey)];
93 }
94
95 private hasTags(file: string): boolean {
96 return this.tagsCaches.has(file);
97 }
98
99 private setTags(file: string, tags: string[]) {
100 return this.tagsCaches.set(file, {
101 tags,
102 tagsFileWatcher: FileUtils.watchJsonFile(
103 file,
104 FileType.Authorization,
105 this.logPrefix(file),
106 undefined,
107 (event, filename) => {
108 if (Utils.isNotEmptyString(filename) && event === 'change') {
109 try {
110 logger.debug(
111 `${this.logPrefix(file)} ${FileType.Authorization} file have changed, reload`
112 );
113 this.deleteTags(file);
114 this.deleteTagsIndexes(file);
115 } catch (error) {
116 FileUtils.handleFileException(
117 file,
118 FileType.Authorization,
119 error as NodeJS.ErrnoException,
120 this.logPrefix(file),
121 {
122 throwError: false,
123 }
124 );
125 }
126 }
127 }
128 ),
129 });
130 }
131
132 private getTags(file: string): string[] | undefined {
133 return this.tagsCaches.get(file)?.tags;
134 }
135
136 private deleteTags(file: string): boolean {
137 this.tagsCaches.get(file)?.tagsFileWatcher?.close();
138 return this.tagsCaches.delete(file);
139 }
140
141 private deleteTagsIndexes(file: string): void {
142 for (const [key] of this.tagsCachesAddressableIndexes) {
143 if (key.startsWith(file)) {
144 this.tagsCachesAddressableIndexes.delete(key);
145 }
146 }
147 }
148
149 private getAuthorizedTagsFromFile(file: string): string[] {
150 let authorizedTags: string[] = [];
151 if (file) {
152 try {
153 // Load authorization file
154 authorizedTags = JSON.parse(fs.readFileSync(file, 'utf8')) as string[];
155 } catch (error) {
156 FileUtils.handleFileException(
157 file,
158 FileType.Authorization,
159 error as NodeJS.ErrnoException,
160 this.logPrefix(file)
161 );
162 }
163 } else {
164 logger.info(`${this.logPrefix(file)} No authorization file given`);
165 }
166 return authorizedTags;
167 }
168
169 private logPrefix = (file: string): string => {
170 return Utils.logPrefix(` Authorized tags cache for authorization file '${file}' |`);
171 };
172 }