Commit | Line | Data |
---|---|---|
66a7748d | 1 | import { type FSWatcher, readFileSync } from 'node:fs' |
f911a4af | 2 | |
66a7748d | 3 | import { FileType, IdTagDistribution } from '../types/index.js' |
9bf0ef23 JB |
4 | import { |
5 | handleFileException, | |
6 | isNotEmptyString, | |
1f8f6332 | 7 | logger, |
4c3f6c20 | 8 | logPrefix, |
9bf0ef23 | 9 | secureRandom, |
66a7748d JB |
10 | watchJsonFile |
11 | } from '../utils/index.js' | |
4c3f6c20 JB |
12 | import type { ChargingStation } from './ChargingStation.js' |
13 | import { getIdTagsFile } from './Helpers.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 | 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 | } |