Commit | Line | Data |
---|---|---|
66a7748d | 1 | import { type FSWatcher, readFileSync } from 'node:fs' |
f911a4af | 2 | |
66a7748d JB |
3 | import type { ChargingStation } from './ChargingStation.js' |
4 | import { getIdTagsFile } from './Helpers.js' | |
5 | import { FileType, IdTagDistribution } from '../types/index.js' | |
9bf0ef23 JB |
6 | import { |
7 | handleFileException, | |
8 | isNotEmptyString, | |
9 | logPrefix, | |
1f8f6332 | 10 | logger, |
9bf0ef23 | 11 | secureRandom, |
66a7748d JB |
12 | watchJsonFile |
13 | } from '../utils/index.js' | |
f911a4af | 14 | |
e1d9a0f4 | 15 | interface IdTagsCacheValueType { |
66a7748d JB |
16 | idTags: string[] |
17 | idTagsFileWatcher: FSWatcher | undefined | |
e1d9a0f4 | 18 | } |
f911a4af JB |
19 | |
20 | export 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 JB |
51 | const hashId = chargingStation.stationInfo.hashId |
52 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion | |
53 | const idTagsFile = getIdTagsFile(chargingStation.stationInfo)! | |
f911a4af JB |
54 | switch (distribution) { |
55 | case IdTagDistribution.RANDOM: | |
66a7748d | 56 | return this.getRandomIdTag(hashId, idTagsFile) |
f911a4af | 57 | case IdTagDistribution.ROUND_ROBIN: |
66a7748d | 58 | return this.getRoundRobinIdTag(hashId, idTagsFile) |
f911a4af | 59 | case IdTagDistribution.CONNECTOR_AFFINITY: |
66a7748d | 60 | return this.getConnectorAffinityIdTag(chargingStation, connectorId) |
f911a4af | 61 | default: |
66a7748d | 62 | return this.getRoundRobinIdTag(hashId, idTagsFile) |
f911a4af JB |
63 | } |
64 | } | |
65 | ||
7b5dbe91 | 66 | /** |
361c98f5 | 67 | * Gets all idtags from the cache |
7b5dbe91 JB |
68 | * Must be called after checking the cache is not an empty array |
69 | * | |
4c8782ee | 70 | * @param file - |
7b5dbe91 JB |
71 | * @returns |
72 | */ | |
66a7748d JB |
73 | public getIdTags (file: string): string[] | undefined { |
74 | if (!this.hasIdTagsCache(file)) { | |
75 | this.setIdTagsCache(file, this.getIdTagsFromFile(file)) | |
f911a4af | 76 | } |
66a7748d | 77 | return this.getIdTagsCache(file) |
f911a4af JB |
78 | } |
79 | ||
66a7748d JB |
80 | public deleteIdTags (file: string): boolean { |
81 | return this.deleteIdTagsCache(file) && this.deleteIdTagsCacheIndexes(file) | |
f911a4af JB |
82 | } |
83 | ||
66a7748d JB |
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) | |
f911a4af JB |
88 | this.idTagsCachesAddressableIndexes.set( |
89 | addressableKey, | |
66a7748d JB |
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)!] | |
f911a4af JB |
94 | } |
95 | ||
66a7748d JB |
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] | |
f911a4af JB |
102 | this.idTagsCachesAddressableIndexes.set( |
103 | addressableKey, | |
66a7748d JB |
104 | idTagIndex === idTags.length - 1 ? 0 : idTagIndex + 1 |
105 | ) | |
106 | return idTag | |
f911a4af JB |
107 | } |
108 | ||
66a7748d JB |
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)! | |
728e01f0 JB |
114 | const addressableKey = this.getIdTagsCacheIndexesAddressableKey( |
115 | file, | |
66a7748d JB |
116 | chargingStation.stationInfo.hashId |
117 | ) | |
f911a4af JB |
118 | this.idTagsCachesAddressableIndexes.set( |
119 | addressableKey, | |
66a7748d JB |
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)!] | |
f911a4af JB |
124 | } |
125 | ||
66a7748d JB |
126 | private hasIdTagsCache (file: string): boolean { |
127 | return this.idTagsCaches.has(file) | |
f911a4af JB |
128 | } |
129 | ||
66a7748d | 130 | private setIdTagsCache (file: string, idTags: string[]): Map<string, IdTagsCacheValueType> { |
f911a4af JB |
131 | return this.idTagsCaches.set(file, { |
132 | idTags, | |
1f8f6332 JB |
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( | |
66a7748d JB |
142 | `${this.logPrefix(file)} ${FileType.Authorization} file have changed, reload` |
143 | ) | |
144 | this.deleteIdTagsCache(file) | |
145 | this.deleteIdTagsCacheIndexes(file) | |
1f8f6332 JB |
146 | } catch (error) { |
147 | handleFileException( | |
148 | file, | |
149 | FileType.Authorization, | |
150 | error as NodeJS.ErrnoException, | |
151 | this.logPrefix(file), | |
152 | { | |
66a7748d JB |
153 | throwError: false |
154 | } | |
155 | ) | |
1f8f6332 JB |
156 | } |
157 | } | |
66a7748d JB |
158 | } |
159 | ) | |
160 | }) | |
f911a4af JB |
161 | } |
162 | ||
66a7748d JB |
163 | private getIdTagsCache (file: string): string[] | undefined { |
164 | return this.idTagsCaches.get(file)?.idTags | |
f911a4af JB |
165 | } |
166 | ||
66a7748d JB |
167 | private deleteIdTagsCache (file: string): boolean { |
168 | this.idTagsCaches.get(file)?.idTagsFileWatcher?.close() | |
169 | return this.idTagsCaches.delete(file) | |
f911a4af JB |
170 | } |
171 | ||
66a7748d JB |
172 | private deleteIdTagsCacheIndexes (file: string): boolean { |
173 | const deleted: boolean[] = [] | |
f911a4af JB |
174 | for (const [key] of this.idTagsCachesAddressableIndexes) { |
175 | if (key.startsWith(file)) { | |
66a7748d | 176 | deleted.push(this.idTagsCachesAddressableIndexes.delete(key)) |
f911a4af JB |
177 | } |
178 | } | |
66a7748d | 179 | return !deleted.some((value) => !value) |
f911a4af JB |
180 | } |
181 | ||
66a7748d JB |
182 | private getIdTagsCacheIndexesAddressableKey (prefix: string, uid: string): string { |
183 | return `${prefix}${uid}` | |
7af183e7 JB |
184 | } |
185 | ||
66a7748d | 186 | private getIdTagsFromFile (file: string): string[] { |
9bf0ef23 | 187 | if (isNotEmptyString(file)) { |
f911a4af | 188 | try { |
66a7748d | 189 | return JSON.parse(readFileSync(file, 'utf8')) as string[] |
f911a4af | 190 | } catch (error) { |
fa5995d6 | 191 | handleFileException( |
f911a4af JB |
192 | file, |
193 | FileType.Authorization, | |
194 | error as NodeJS.ErrnoException, | |
66a7748d JB |
195 | this.logPrefix(file) |
196 | ) | |
f911a4af | 197 | } |
f911a4af | 198 | } |
66a7748d | 199 | return [] |
f911a4af JB |
200 | } |
201 | ||
66a7748d JB |
202 | private readonly logPrefix = (file: string): string => { |
203 | return logPrefix(` Id tags cache for id tags file '${file}' |`) | |
204 | } | |
f911a4af | 205 | } |