b50c062008b9b1388d9ea21cfeb21ac3ba0018ce
[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.js'
4 import { getIdTagsFile } from './Helpers.js'
5 import { FileType, IdTagDistribution } from '../types/index.js'
6 import {
7 handleFileException,
8 isNotEmptyString,
9 logPrefix,
10 logger,
11 secureRandom,
12 watchJsonFile
13 } from '../utils/index.js'
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 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
53 const idTagsFile = getIdTagsFile(chargingStation.stationInfo)!
54 switch (distribution) {
55 case IdTagDistribution.RANDOM:
56 return this.getRandomIdTag(hashId, idTagsFile)
57 case IdTagDistribution.ROUND_ROBIN:
58 return this.getRoundRobinIdTag(hashId, idTagsFile)
59 case IdTagDistribution.CONNECTOR_AFFINITY:
60 return this.getConnectorAffinityIdTag(chargingStation, connectorId)
61 default:
62 return this.getRoundRobinIdTag(hashId, idTagsFile)
63 }
64 }
65
66 /**
67 * Gets all idtags from the cache
68 * Must be called after checking the cache is not an empty array
69 *
70 * @param file -
71 * @returns
72 */
73 public getIdTags (file: string): string[] | undefined {
74 if (!this.hasIdTagsCache(file)) {
75 this.setIdTagsCache(file, this.getIdTagsFromFile(file))
76 }
77 return this.getIdTagsCache(file)
78 }
79
80 public deleteIdTags (file: string): boolean {
81 return this.deleteIdTagsCache(file) && this.deleteIdTagsCacheIndexes(file)
82 }
83
84 private getRandomIdTag (hashId: string, file: string): string {
85 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
86 const idTags = this.getIdTags(file)!
87 const addressableKey = this.getIdTagsCacheIndexesAddressableKey(file, hashId)
88 this.idTagsCachesAddressableIndexes.set(
89 addressableKey,
90 Math.floor(secureRandom() * idTags.length)
91 )
92 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
93 return idTags[this.idTagsCachesAddressableIndexes.get(addressableKey)!]
94 }
95
96 private getRoundRobinIdTag (hashId: string, file: string): string {
97 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
98 const idTags = this.getIdTags(file)!
99 const addressableKey = this.getIdTagsCacheIndexesAddressableKey(file, hashId)
100 const idTagIndex = this.idTagsCachesAddressableIndexes.get(addressableKey) ?? 0
101 const idTag = idTags[idTagIndex]
102 this.idTagsCachesAddressableIndexes.set(
103 addressableKey,
104 idTagIndex === idTags.length - 1 ? 0 : idTagIndex + 1
105 )
106 return idTag
107 }
108
109 private getConnectorAffinityIdTag (chargingStation: ChargingStation, connectorId: number): string {
110 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
111 const file = getIdTagsFile(chargingStation.stationInfo)!
112 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
113 const idTags = this.getIdTags(file)!
114 const addressableKey = this.getIdTagsCacheIndexesAddressableKey(
115 file,
116 chargingStation.stationInfo.hashId
117 )
118 this.idTagsCachesAddressableIndexes.set(
119 addressableKey,
120 (chargingStation.index - 1 + (connectorId - 1)) % idTags.length
121 )
122 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
123 return idTags[this.idTagsCachesAddressableIndexes.get(addressableKey)!]
124 }
125
126 private hasIdTagsCache (file: string): boolean {
127 return this.idTagsCaches.has(file)
128 }
129
130 private setIdTagsCache (file: string, idTags: string[]): Map<string, IdTagsCacheValueType> {
131 return this.idTagsCaches.set(file, {
132 idTags,
133 idTagsFileWatcher: watchJsonFile(
134 file,
135 FileType.Authorization,
136 this.logPrefix(file),
137 undefined,
138 (event, filename) => {
139 if (isNotEmptyString(filename) && event === 'change') {
140 try {
141 logger.debug(
142 `${this.logPrefix(file)} ${FileType.Authorization} file have changed, reload`
143 )
144 this.deleteIdTagsCache(file)
145 this.deleteIdTagsCacheIndexes(file)
146 } catch (error) {
147 handleFileException(
148 file,
149 FileType.Authorization,
150 error as NodeJS.ErrnoException,
151 this.logPrefix(file),
152 {
153 throwError: false
154 }
155 )
156 }
157 }
158 }
159 )
160 })
161 }
162
163 private getIdTagsCache (file: string): string[] | undefined {
164 return this.idTagsCaches.get(file)?.idTags
165 }
166
167 private deleteIdTagsCache (file: string): boolean {
168 this.idTagsCaches.get(file)?.idTagsFileWatcher?.close()
169 return this.idTagsCaches.delete(file)
170 }
171
172 private deleteIdTagsCacheIndexes (file: string): boolean {
173 const deleted: boolean[] = []
174 for (const [key] of this.idTagsCachesAddressableIndexes) {
175 if (key.startsWith(file)) {
176 deleted.push(this.idTagsCachesAddressableIndexes.delete(key))
177 }
178 }
179 return !deleted.some((value) => !value)
180 }
181
182 private getIdTagsCacheIndexesAddressableKey (prefix: string, uid: string): string {
183 return `${prefix}${uid}`
184 }
185
186 private getIdTagsFromFile (file: string): string[] {
187 if (isNotEmptyString(file)) {
188 try {
189 return JSON.parse(readFileSync(file, 'utf8')) as string[]
190 } catch (error) {
191 handleFileException(
192 file,
193 FileType.Authorization,
194 error as NodeJS.ErrnoException,
195 this.logPrefix(file)
196 )
197 }
198 }
199 return []
200 }
201
202 private readonly logPrefix = (file: string): string => {
203 return logPrefix(` Id tags cache for id tags file '${file}' |`)
204 }
205 }