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