refactor: cleanup eslint configuration
[e-mobility-charging-stations-simulator.git] / src / charging-station / IdTagsCache.ts
CommitLineData
66a7748d 1import { type FSWatcher, readFileSync } from 'node:fs'
f911a4af 2
66a7748d 3import { FileType, IdTagDistribution } from '../types/index.js'
9bf0ef23
JB
4import {
5 handleFileException,
6 isNotEmptyString,
1f8f6332 7 logger,
4c3f6c20 8 logPrefix,
9bf0ef23 9 secureRandom,
66a7748d
JB
10 watchJsonFile
11} from '../utils/index.js'
4c3f6c20
JB
12import type { ChargingStation } from './ChargingStation.js'
13import { getIdTagsFile } from './Helpers.js'
f911a4af 14
e1d9a0f4 15interface IdTagsCacheValueType {
66a7748d
JB
16 idTags: string[]
17 idTagsFileWatcher: FSWatcher | undefined
e1d9a0f4 18}
f911a4af
JB
19
20export class IdTagsCache {
66a7748d
JB
21 private static instance: IdTagsCache | null = null
22 private readonly idTagsCaches: Map<string, IdTagsCacheValueType>
23 private readonly idTagsCachesAddressableIndexes: Map<string, number>
f911a4af 24
66a7748d
JB
25 private constructor () {
26 this.idTagsCaches = new Map<string, IdTagsCacheValueType>()
27 this.idTagsCachesAddressableIndexes = new Map<string, number>()
f911a4af
JB
28 }
29
66a7748d 30 public static getInstance (): IdTagsCache {
f911a4af 31 if (IdTagsCache.instance === null) {
66a7748d 32 IdTagsCache.instance = new IdTagsCache()
f911a4af 33 }
66a7748d 34 return IdTagsCache.instance
f911a4af
JB
35 }
36
7b5dbe91 37 /**
361c98f5 38 * Gets one idtag from the cache given the distribution
7b5dbe91
JB
39 * Must be called after checking the cache is not an empty array
40 *
4c8782ee
JB
41 * @param distribution -
42 * @param chargingStation -
43 * @param connectorId -
7b5dbe91
JB
44 * @returns
45 */
66a7748d 46 public getIdTag (
f911a4af
JB
47 distribution: IdTagDistribution,
48 chargingStation: ChargingStation,
66a7748d 49 connectorId: number
f911a4af 50 ): string {
66a7748d 51 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
5199f9fd
JB
52 const hashId = chargingStation.stationInfo!.hashId
53 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
54 const idTagsFile = getIdTagsFile(chargingStation.stationInfo!)!
f911a4af
JB
55 switch (distribution) {
56 case IdTagDistribution.RANDOM:
66a7748d 57 return this.getRandomIdTag(hashId, idTagsFile)
f911a4af 58 case IdTagDistribution.ROUND_ROBIN:
66a7748d 59 return this.getRoundRobinIdTag(hashId, idTagsFile)
f911a4af 60 case IdTagDistribution.CONNECTOR_AFFINITY:
66a7748d 61 return this.getConnectorAffinityIdTag(chargingStation, connectorId)
f911a4af 62 default:
66a7748d 63 return this.getRoundRobinIdTag(hashId, idTagsFile)
f911a4af
JB
64 }
65 }
66
7b5dbe91 67 /**
361c98f5 68 * Gets all idtags from the cache
7b5dbe91
JB
69 * Must be called after checking the cache is not an empty array
70 *
4c8782ee 71 * @param file -
7b5dbe91
JB
72 * @returns
73 */
66a7748d
JB
74 public getIdTags (file: string): string[] | undefined {
75 if (!this.hasIdTagsCache(file)) {
76 this.setIdTagsCache(file, this.getIdTagsFromFile(file))
f911a4af 77 }
66a7748d 78 return this.getIdTagsCache(file)
f911a4af
JB
79 }
80
66a7748d
JB
81 public deleteIdTags (file: string): boolean {
82 return this.deleteIdTagsCache(file) && this.deleteIdTagsCacheIndexes(file)
f911a4af
JB
83 }
84
66a7748d
JB
85 private getRandomIdTag (hashId: string, file: string): string {
86 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
87 const idTags = this.getIdTags(file)!
88 const addressableKey = this.getIdTagsCacheIndexesAddressableKey(file, hashId)
f911a4af
JB
89 this.idTagsCachesAddressableIndexes.set(
90 addressableKey,
66a7748d
JB
91 Math.floor(secureRandom() * idTags.length)
92 )
93 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
94 return idTags[this.idTagsCachesAddressableIndexes.get(addressableKey)!]
f911a4af
JB
95 }
96
66a7748d
JB
97 private getRoundRobinIdTag (hashId: string, file: string): string {
98 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
99 const idTags = this.getIdTags(file)!
100 const addressableKey = this.getIdTagsCacheIndexesAddressableKey(file, hashId)
101 const idTagIndex = this.idTagsCachesAddressableIndexes.get(addressableKey) ?? 0
102 const idTag = idTags[idTagIndex]
f911a4af
JB
103 this.idTagsCachesAddressableIndexes.set(
104 addressableKey,
66a7748d
JB
105 idTagIndex === idTags.length - 1 ? 0 : idTagIndex + 1
106 )
107 return idTag
f911a4af
JB
108 }
109
66a7748d
JB
110 private getConnectorAffinityIdTag (chargingStation: ChargingStation, connectorId: number): string {
111 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
5199f9fd 112 const file = getIdTagsFile(chargingStation.stationInfo!)!
66a7748d
JB
113 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
114 const idTags = this.getIdTags(file)!
728e01f0
JB
115 const addressableKey = this.getIdTagsCacheIndexesAddressableKey(
116 file,
5199f9fd
JB
117 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
118 chargingStation.stationInfo!.hashId
66a7748d 119 )
f911a4af
JB
120 this.idTagsCachesAddressableIndexes.set(
121 addressableKey,
66a7748d
JB
122 (chargingStation.index - 1 + (connectorId - 1)) % idTags.length
123 )
124 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
125 return idTags[this.idTagsCachesAddressableIndexes.get(addressableKey)!]
f911a4af
JB
126 }
127
66a7748d
JB
128 private hasIdTagsCache (file: string): boolean {
129 return this.idTagsCaches.has(file)
f911a4af
JB
130 }
131
66a7748d 132 private setIdTagsCache (file: string, idTags: string[]): Map<string, IdTagsCacheValueType> {
f911a4af
JB
133 return this.idTagsCaches.set(file, {
134 idTags,
1f8f6332
JB
135 idTagsFileWatcher: watchJsonFile(
136 file,
137 FileType.Authorization,
138 this.logPrefix(file),
139 undefined,
140 (event, filename) => {
141 if (isNotEmptyString(filename) && event === 'change') {
142 try {
143 logger.debug(
66a7748d
JB
144 `${this.logPrefix(file)} ${FileType.Authorization} file have changed, reload`
145 )
36b73d95 146 this.deleteIdTags(file)
1f8f6332
JB
147 } catch (error) {
148 handleFileException(
149 file,
150 FileType.Authorization,
151 error as NodeJS.ErrnoException,
152 this.logPrefix(file),
153 {
66a7748d
JB
154 throwError: false
155 }
156 )
1f8f6332
JB
157 }
158 }
66a7748d
JB
159 }
160 )
161 })
f911a4af
JB
162 }
163
66a7748d
JB
164 private getIdTagsCache (file: string): string[] | undefined {
165 return this.idTagsCaches.get(file)?.idTags
f911a4af
JB
166 }
167
66a7748d
JB
168 private deleteIdTagsCache (file: string): boolean {
169 this.idTagsCaches.get(file)?.idTagsFileWatcher?.close()
170 return this.idTagsCaches.delete(file)
f911a4af
JB
171 }
172
66a7748d
JB
173 private deleteIdTagsCacheIndexes (file: string): boolean {
174 const deleted: boolean[] = []
f911a4af
JB
175 for (const [key] of this.idTagsCachesAddressableIndexes) {
176 if (key.startsWith(file)) {
66a7748d 177 deleted.push(this.idTagsCachesAddressableIndexes.delete(key))
f911a4af
JB
178 }
179 }
a974c8e4 180 return !deleted.some(value => !value)
f911a4af
JB
181 }
182
66a7748d
JB
183 private getIdTagsCacheIndexesAddressableKey (prefix: string, uid: string): string {
184 return `${prefix}${uid}`
7af183e7
JB
185 }
186
66a7748d 187 private getIdTagsFromFile (file: string): string[] {
9bf0ef23 188 if (isNotEmptyString(file)) {
f911a4af 189 try {
66a7748d 190 return JSON.parse(readFileSync(file, 'utf8')) as string[]
f911a4af 191 } catch (error) {
fa5995d6 192 handleFileException(
f911a4af
JB
193 file,
194 FileType.Authorization,
195 error as NodeJS.ErrnoException,
66a7748d
JB
196 this.logPrefix(file)
197 )
f911a4af 198 }
f911a4af 199 }
66a7748d 200 return []
f911a4af
JB
201 }
202
66a7748d
JB
203 private readonly logPrefix = (file: string): string => {
204 return logPrefix(` Id tags cache for id tags file '${file}' |`)
205 }
f911a4af 206}