]>
Commit | Line | Data |
---|---|---|
24d15716 | 1 | import _Ajv, { type ValidateFunction } from 'ajv' |
66a7748d | 2 | import _ajvFormats from 'ajv-formats' |
b52c969d | 3 | |
66a7748d | 4 | import type { ChargingStation } from '../../charging-station/index.js' |
0749233f JB |
5 | import type { OCPPResponseService } from './OCPPResponseService.js' |
6 | ||
66a7748d JB |
7 | import { OCPPError } from '../../exception/index.js' |
8 | import { PerformanceStatistics } from '../../performance/index.js' | |
e7aeea18 | 9 | import { |
8baae8ee | 10 | ChargingStationEvents, |
27782dbc | 11 | type ErrorCallback, |
268a74bb JB |
12 | type ErrorResponse, |
13 | ErrorType, | |
27782dbc | 14 | type IncomingRequestCommand, |
268a74bb JB |
15 | type JsonType, |
16 | MessageType, | |
17 | type OCPPVersion, | |
27782dbc | 18 | type OutgoingRequest, |
e7aeea18 | 19 | RequestCommand, |
27782dbc | 20 | type RequestParams, |
268a74bb | 21 | type Response, |
27782dbc | 22 | type ResponseCallback, |
d1f5bfd8 | 23 | type ResponseType, |
66a7748d | 24 | } from '../../types/index.js' |
2b94ad12 | 25 | import { |
40615072 | 26 | clone, |
2b94ad12 JB |
27 | formatDurationMilliSeconds, |
28 | handleSendMessageError, | |
d1f5bfd8 | 29 | logger, |
66a7748d | 30 | } from '../../utils/index.js' |
4c3f6c20 | 31 | import { OCPPConstants } from './OCPPConstants.js' |
01b82de5 JB |
32 | import { |
33 | ajvErrorsToErrorType, | |
34 | convertDateToISOString, | |
d1f5bfd8 | 35 | getMessageTypeString, |
01b82de5 | 36 | } from './OCPPServiceUtils.js' |
66a7748d JB |
37 | type Ajv = _Ajv.default |
38 | // eslint-disable-next-line @typescript-eslint/no-redeclare | |
39 | const Ajv = _Ajv.default | |
40 | const ajvFormats = _ajvFormats.default | |
c0560973 | 41 | |
66a7748d | 42 | const moduleName = 'OCPPRequestService' |
e3018bc4 | 43 | |
b9da1bc2 JB |
44 | const defaultRequestParams: RequestParams = { |
45 | skipBufferingOnError: false, | |
d1f5bfd8 | 46 | throwError: false, |
0749233f | 47 | triggerMessage: false, |
66a7748d | 48 | } |
b9da1bc2 | 49 | |
268a74bb | 50 | export abstract class OCPPRequestService { |
0749233f | 51 | private static instance: null | OCPPRequestService = null |
24d15716 | 52 | protected readonly ajv: Ajv |
d5490a13 | 53 | protected abstract payloadValidateFunctions: Map<RequestCommand, ValidateFunction<JsonType>> |
0749233f JB |
54 | private readonly ocppResponseService: OCPPResponseService |
55 | private readonly version: OCPPVersion | |
c0560973 | 56 | |
66a7748d JB |
57 | protected constructor (version: OCPPVersion, ocppResponseService: OCPPResponseService) { |
58 | this.version = version | |
45988780 | 59 | this.ajv = new Ajv({ |
98fc1389 | 60 | keywords: ['javaType'], |
d1f5bfd8 | 61 | multipleOfPrecision: 2, |
66a7748d JB |
62 | }) |
63 | ajvFormats(this.ajv) | |
66a7748d | 64 | this.ocppResponseService = ocppResponseService |
ba9a56a6 JB |
65 | this.requestHandler = this.requestHandler.bind(this) |
66 | this.sendMessage = this.sendMessage.bind(this) | |
67 | this.sendResponse = this.sendResponse.bind(this) | |
68 | this.sendError = this.sendError.bind(this) | |
69 | this.internalSendMessage = this.internalSendMessage.bind(this) | |
70 | this.buildMessageToSend = this.buildMessageToSend.bind(this) | |
71 | this.validateRequestPayload = this.validateRequestPayload.bind(this) | |
72 | this.validateIncomingRequestResponsePayload = | |
73 | this.validateIncomingRequestResponsePayload.bind(this) | |
c0560973 JB |
74 | } |
75 | ||
e7aeea18 | 76 | public static getInstance<T extends OCPPRequestService>( |
08f130a0 | 77 | this: new (ocppResponseService: OCPPResponseService) => T, |
66a7748d | 78 | ocppResponseService: OCPPResponseService |
e7aeea18 | 79 | ): T { |
678ffc93 | 80 | OCPPRequestService.instance ??= new this(ocppResponseService) |
66a7748d | 81 | return OCPPRequestService.instance as T |
9f2e3130 JB |
82 | } |
83 | ||
c4a89082 JB |
84 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters |
85 | public abstract requestHandler<ReqType extends JsonType, ResType extends JsonType>( | |
86 | chargingStation: ChargingStation, | |
87 | commandName: RequestCommand, | |
88 | commandParams?: ReqType, | |
89 | params?: RequestParams | |
90 | ): Promise<ResType> | |
91 | ||
92 | public async sendError ( | |
93 | chargingStation: ChargingStation, | |
94 | messageId: string, | |
95 | ocppError: OCPPError, | |
96 | commandName: IncomingRequestCommand | RequestCommand | |
97 | ): Promise<ResponseType> { | |
98 | try { | |
99 | // Send error message | |
100 | return await this.internalSendMessage( | |
101 | chargingStation, | |
102 | messageId, | |
103 | ocppError, | |
104 | MessageType.CALL_ERROR_MESSAGE, | |
105 | commandName | |
106 | ) | |
107 | } catch (error) { | |
108 | handleSendMessageError( | |
109 | chargingStation, | |
110 | commandName, | |
111 | MessageType.CALL_ERROR_MESSAGE, | |
112 | error as Error | |
113 | ) | |
114 | return null | |
115 | } | |
116 | } | |
117 | ||
118 | public async sendResponse ( | |
119 | chargingStation: ChargingStation, | |
120 | messageId: string, | |
121 | messagePayload: JsonType, | |
122 | commandName: IncomingRequestCommand | |
123 | ): Promise<ResponseType> { | |
124 | try { | |
125 | // Send response message | |
126 | return await this.internalSendMessage( | |
127 | chargingStation, | |
128 | messageId, | |
129 | messagePayload, | |
130 | MessageType.CALL_RESULT_MESSAGE, | |
131 | commandName | |
132 | ) | |
133 | } catch (error) { | |
134 | handleSendMessageError( | |
135 | chargingStation, | |
136 | commandName, | |
137 | MessageType.CALL_RESULT_MESSAGE, | |
138 | error as Error, | |
139 | { | |
140 | throwError: true, | |
141 | } | |
142 | ) | |
143 | return null | |
144 | } | |
145 | } | |
146 | ||
66a7748d | 147 | protected async sendMessage ( |
08f130a0 | 148 | chargingStation: ChargingStation, |
e7aeea18 | 149 | messageId: string, |
5cc4b63b | 150 | messagePayload: JsonType, |
e7aeea18 | 151 | commandName: RequestCommand, |
66a7748d | 152 | params?: RequestParams |
e7aeea18 | 153 | ): Promise<ResponseType> { |
7b5dbe91 | 154 | params = { |
b9da1bc2 | 155 | ...defaultRequestParams, |
d1f5bfd8 | 156 | ...params, |
66a7748d | 157 | } |
5e0c67e8 | 158 | try { |
e7aeea18 | 159 | return await this.internalSendMessage( |
08f130a0 | 160 | chargingStation, |
e7aeea18 JB |
161 | messageId, |
162 | messagePayload, | |
163 | MessageType.CALL_MESSAGE, | |
164 | commandName, | |
66a7748d JB |
165 | params |
166 | ) | |
5e0c67e8 | 167 | } catch (error) { |
436569b1 JB |
168 | handleSendMessageError( |
169 | chargingStation, | |
170 | commandName, | |
171 | MessageType.CALL_MESSAGE, | |
172 | error as Error, | |
173 | { | |
d1f5bfd8 | 174 | throwError: params.throwError, |
436569b1 JB |
175 | } |
176 | ) | |
66a7748d | 177 | return null |
5e0c67e8 JB |
178 | } |
179 | } | |
180 | ||
0749233f | 181 | private buildMessageToSend ( |
b3fc3ff5 | 182 | chargingStation: ChargingStation, |
0749233f JB |
183 | messageId: string, |
184 | messagePayload: JsonType | OCPPError, | |
185 | messageType: MessageType, | |
186 | commandName: IncomingRequestCommand | RequestCommand | |
187 | ): string { | |
188 | let messageToSend: string | |
189 | // Type of message | |
190 | switch (messageType) { | |
191 | // Error Message | |
192 | case MessageType.CALL_ERROR_MESSAGE: | |
193 | // Build Error Message | |
194 | messageToSend = JSON.stringify([ | |
195 | messageType, | |
196 | messageId, | |
197 | (messagePayload as OCPPError).code, | |
198 | (messagePayload as OCPPError).message, | |
199 | (messagePayload as OCPPError).details ?? { | |
200 | command: (messagePayload as OCPPError).command, | |
201 | }, | |
202 | ] satisfies ErrorResponse) | |
203 | break | |
204 | // Request | |
205 | case MessageType.CALL_MESSAGE: | |
206 | // Build request | |
207 | this.validateRequestPayload(chargingStation, commandName, messagePayload as JsonType) | |
208 | messageToSend = JSON.stringify([ | |
209 | messageType, | |
210 | messageId, | |
211 | commandName as RequestCommand, | |
212 | messagePayload as JsonType, | |
213 | ] satisfies OutgoingRequest) | |
214 | break | |
215 | // Response | |
216 | case MessageType.CALL_RESULT_MESSAGE: | |
217 | // Build response | |
218 | this.validateIncomingRequestResponsePayload( | |
219 | chargingStation, | |
220 | commandName, | |
221 | messagePayload as JsonType | |
222 | ) | |
223 | messageToSend = JSON.stringify([ | |
224 | messageType, | |
225 | messageId, | |
226 | messagePayload as JsonType, | |
227 | ] satisfies Response) | |
228 | break | |
b3fc3ff5 | 229 | } |
0749233f | 230 | return messageToSend |
b3fc3ff5 JB |
231 | } |
232 | ||
66a7748d | 233 | private async internalSendMessage ( |
08f130a0 | 234 | chargingStation: ChargingStation, |
e7aeea18 | 235 | messageId: string, |
5cc4b63b | 236 | messagePayload: JsonType | OCPPError, |
e7aeea18 | 237 | messageType: MessageType, |
0749233f | 238 | commandName: IncomingRequestCommand | RequestCommand, |
66a7748d | 239 | params?: RequestParams |
e7aeea18 | 240 | ): Promise<ResponseType> { |
7b5dbe91 | 241 | params = { |
b9da1bc2 | 242 | ...defaultRequestParams, |
d1f5bfd8 | 243 | ...params, |
66a7748d | 244 | } |
e7aeea18 | 245 | if ( |
b72d1749 JB |
246 | ((chargingStation.inUnknownState() || chargingStation.inPendingState()) && |
247 | commandName === RequestCommand.BOOT_NOTIFICATION) || | |
5398cecf | 248 | (chargingStation.stationInfo?.ocppStrictCompliance === false && |
b72d1749 | 249 | (chargingStation.inUnknownState() || chargingStation.inPendingState())) || |
66a7748d JB |
250 | chargingStation.inAcceptedState() || |
251 | (chargingStation.inPendingState() && | |
3a13fc92 | 252 | (params.triggerMessage === true || messageType === MessageType.CALL_RESULT_MESSAGE)) |
e7aeea18 | 253 | ) { |
caad9d6b | 254 | // eslint-disable-next-line @typescript-eslint/no-this-alias |
66a7748d | 255 | const self = this |
caad9d6b | 256 | // Send a message through wsConnection |
ea32ea05 | 257 | return await new Promise<ResponseType>((resolve, reject: (reason?: unknown) => void) => { |
1b2acf4e JB |
258 | /** |
259 | * Function that will receive the request's response | |
1b2acf4e JB |
260 | * @param payload - |
261 | * @param requestPayload - | |
262 | */ | |
263 | const responseCallback = (payload: JsonType, requestPayload: JsonType): void => { | |
264 | if (chargingStation.stationInfo?.enableStatistics === true) { | |
265 | chargingStation.performanceStatistics?.addRequestStatistic( | |
266 | commandName, | |
66a7748d JB |
267 | MessageType.CALL_RESULT_MESSAGE |
268 | ) | |
1b2acf4e JB |
269 | } |
270 | // Handle the request's response | |
271 | self.ocppResponseService | |
272 | .responseHandler( | |
273 | chargingStation, | |
274 | commandName as RequestCommand, | |
275 | payload, | |
66a7748d | 276 | requestPayload |
1b2acf4e JB |
277 | ) |
278 | .then(() => { | |
66a7748d | 279 | resolve(payload) |
d1f5bfd8 | 280 | return undefined |
1b2acf4e | 281 | }) |
1b2acf4e | 282 | .finally(() => { |
66a7748d JB |
283 | chargingStation.requests.delete(messageId) |
284 | chargingStation.emit(ChargingStationEvents.updated) | |
285 | }) | |
d1f5bfd8 | 286 | .catch(reject) |
66a7748d | 287 | } |
e8a92d57 | 288 | |
1b2acf4e JB |
289 | /** |
290 | * Function that will receive the request's error response | |
9d7b5fa3 | 291 | * @param ocppError - |
1b2acf4e JB |
292 | * @param requestStatistic - |
293 | */ | |
9d7b5fa3 | 294 | const errorCallback = (ocppError: OCPPError, requestStatistic = true): void => { |
66a7748d | 295 | if (requestStatistic && chargingStation.stationInfo?.enableStatistics === true) { |
1b2acf4e JB |
296 | chargingStation.performanceStatistics?.addRequestStatistic( |
297 | commandName, | |
66a7748d JB |
298 | MessageType.CALL_ERROR_MESSAGE |
299 | ) | |
764d2c91 | 300 | } |
1b2acf4e | 301 | logger.error( |
436569b1 | 302 | `${chargingStation.logPrefix()} Error occurred at ${getMessageTypeString( |
66a7748d | 303 | messageType |
1b2acf4e | 304 | )} command ${commandName} with PDU %j:`, |
e7aeea18 | 305 | messagePayload, |
66a7748d JB |
306 | ocppError |
307 | ) | |
308 | chargingStation.requests.delete(messageId) | |
309 | chargingStation.emit(ChargingStationEvents.updated) | |
310 | reject(ocppError) | |
311 | } | |
9d7b5fa3 | 312 | |
3c80de96 | 313 | const handleSendError = (ocppError: OCPPError): void => { |
e2baeffc | 314 | if (params.skipBufferingOnError === false) { |
3c80de96 | 315 | // Buffer |
66a7748d | 316 | chargingStation.bufferMessage(messageToSend) |
3c80de96 | 317 | if (messageType === MessageType.CALL_MESSAGE) { |
f10f20c9 | 318 | this.setCachedRequest( |
3c80de96 JB |
319 | chargingStation, |
320 | messageId, | |
321 | messagePayload as JsonType, | |
322 | commandName, | |
323 | responseCallback, | |
66a7748d JB |
324 | errorCallback |
325 | ) | |
3c80de96 | 326 | } |
9aa1a33f | 327 | } else if ( |
e2baeffc | 328 | params.skipBufferingOnError === true && |
9aa1a33f JB |
329 | messageType === MessageType.CALL_MESSAGE |
330 | ) { | |
2b94ad12 | 331 | // Remove request from the cache |
66a7748d | 332 | chargingStation.requests.delete(messageId) |
d42379d8 | 333 | } |
66a7748d JB |
334 | reject(ocppError) |
335 | } | |
d42379d8 | 336 | |
1b2acf4e | 337 | if (chargingStation.stationInfo?.enableStatistics === true) { |
66a7748d | 338 | chargingStation.performanceStatistics?.addRequestStatistic(commandName, messageType) |
1b2acf4e JB |
339 | } |
340 | const messageToSend = this.buildMessageToSend( | |
341 | chargingStation, | |
342 | messageId, | |
343 | messagePayload, | |
344 | messageType, | |
66a7748d JB |
345 | commandName |
346 | ) | |
1b2acf4e | 347 | // Check if wsConnection opened |
66a7748d JB |
348 | if (chargingStation.isWebSocketConnectionOpened()) { |
349 | const beginId = PerformanceStatistics.beginMeasure(commandName) | |
1a32c36b | 350 | const sendTimeout = setTimeout(() => { |
66a7748d | 351 | handleSendError( |
1a32c36b JB |
352 | new OCPPError( |
353 | ErrorType.GENERIC_ERROR, | |
2b94ad12 | 354 | `Timeout ${formatDurationMilliSeconds( |
66a7748d | 355 | OCPPConstants.OCPP_WEBSOCKET_TIMEOUT |
2b94ad12 | 356 | )} reached for ${ |
e2baeffc | 357 | params.skipBufferingOnError === false ? '' : 'non ' |
42b8cf5c | 358 | }buffered message id '${messageId}' with content '${messageToSend}'`, |
1a32c36b | 359 | commandName, |
66a7748d JB |
360 | (messagePayload as OCPPError).details |
361 | ) | |
362 | ) | |
363 | }, OCPPConstants.OCPP_WEBSOCKET_TIMEOUT) | |
1a32c36b | 364 | chargingStation.wsConnection?.send(messageToSend, (error?: Error) => { |
66a7748d JB |
365 | PerformanceStatistics.endMeasure(commandName, beginId) |
366 | clearTimeout(sendTimeout) | |
aa63c9b7 | 367 | if (error == null) { |
d42379d8 | 368 | logger.debug( |
436569b1 | 369 | `${chargingStation.logPrefix()} >> Command '${commandName}' sent ${getMessageTypeString( |
66a7748d JB |
370 | messageType |
371 | )} payload: ${messageToSend}` | |
372 | ) | |
d42379d8 | 373 | if (messageType === MessageType.CALL_MESSAGE) { |
f10f20c9 | 374 | this.setCachedRequest( |
d42379d8 JB |
375 | chargingStation, |
376 | messageId, | |
377 | messagePayload as JsonType, | |
378 | commandName, | |
379 | responseCallback, | |
66a7748d JB |
380 | errorCallback |
381 | ) | |
69dae411 JB |
382 | } else { |
383 | // Resolve response | |
66a7748d | 384 | resolve(messagePayload) |
d42379d8 | 385 | } |
5199f9fd | 386 | } else { |
66a7748d | 387 | handleSendError( |
3c80de96 JB |
388 | new OCPPError( |
389 | ErrorType.GENERIC_ERROR, | |
390 | `WebSocket errored for ${ | |
e2baeffc | 391 | params.skipBufferingOnError === false ? '' : 'non ' |
3c80de96 JB |
392 | }buffered message id '${messageId}' with content '${messageToSend}'`, |
393 | commandName, | |
48847bc0 | 394 | { |
48847bc0 | 395 | message: error.message, |
0749233f | 396 | name: error.name, |
d1f5bfd8 | 397 | stack: error.stack, |
48847bc0 | 398 | } |
66a7748d JB |
399 | ) |
400 | ) | |
1a32c36b | 401 | } |
66a7748d | 402 | }) |
82fa1110 | 403 | } else { |
66a7748d | 404 | handleSendError( |
3c80de96 JB |
405 | new OCPPError( |
406 | ErrorType.GENERIC_ERROR, | |
407 | `WebSocket closed for ${ | |
e2baeffc | 408 | params.skipBufferingOnError === false ? '' : 'non ' |
3c80de96 JB |
409 | }buffered message id '${messageId}' with content '${messageToSend}'`, |
410 | commandName, | |
66a7748d JB |
411 | (messagePayload as OCPPError).details |
412 | ) | |
413 | ) | |
1b2acf4e | 414 | } |
66a7748d | 415 | }) |
caad9d6b | 416 | } |
e7aeea18 JB |
417 | throw new OCPPError( |
418 | ErrorType.SECURITY_ERROR, | |
d1f5bfd8 | 419 | // eslint-disable-next-line @typescript-eslint/restrict-template-expressions |
5199f9fd | 420 | `Cannot send command ${commandName} PDU when the charging station is in ${chargingStation.bootNotificationResponse?.status} state on the central server`, |
66a7748d JB |
421 | commandName |
422 | ) | |
c0560973 JB |
423 | } |
424 | ||
f10f20c9 | 425 | private setCachedRequest ( |
82fa1110 | 426 | chargingStation: ChargingStation, |
82fa1110 JB |
427 | messageId: string, |
428 | messagePayload: JsonType, | |
0749233f | 429 | commandName: IncomingRequestCommand | RequestCommand, |
54a8fbc7 | 430 | responseCallback: ResponseCallback, |
66a7748d | 431 | errorCallback: ErrorCallback |
82fa1110 JB |
432 | ): void { |
433 | chargingStation.requests.set(messageId, [ | |
434 | responseCallback, | |
435 | errorCallback, | |
436 | commandName, | |
d1f5bfd8 | 437 | messagePayload, |
66a7748d | 438 | ]) |
82fa1110 JB |
439 | } |
440 | ||
0749233f JB |
441 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters |
442 | private validateIncomingRequestResponsePayload<T extends JsonType>( | |
443 | chargingStation: ChargingStation, | |
444 | commandName: IncomingRequestCommand | RequestCommand, | |
445 | payload: T | |
446 | ): boolean { | |
447 | if (chargingStation.stationInfo?.ocppStrictCompliance === false) { | |
448 | return true | |
449 | } | |
450 | if ( | |
451 | !this.ocppResponseService.incomingRequestResponsePayloadValidateFunctions.has( | |
452 | commandName as IncomingRequestCommand | |
453 | ) | |
454 | ) { | |
455 | logger.warn( | |
456 | `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: No JSON schema validation function found for command '${commandName}' PDU validation` | |
457 | ) | |
458 | return true | |
459 | } | |
460 | const validate = this.ocppResponseService.incomingRequestResponsePayloadValidateFunctions.get( | |
461 | commandName as IncomingRequestCommand | |
462 | ) | |
463 | payload = clone<T>(payload) | |
464 | convertDateToISOString<T>(payload) | |
465 | if (validate?.(payload) === true) { | |
466 | return true | |
467 | } | |
468 | logger.error( | |
469 | `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: Command '${commandName}' incoming request response PDU is invalid: %j`, | |
470 | validate?.errors | |
471 | ) | |
472 | // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError(). | |
473 | throw new OCPPError( | |
474 | ajvErrorsToErrorType(validate?.errors), | |
475 | 'Incoming request response PDU is invalid', | |
476 | commandName, | |
477 | JSON.stringify(validate?.errors, undefined, 2) | |
478 | ) | |
479 | } | |
480 | ||
481 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters | |
482 | private validateRequestPayload<T extends JsonType>( | |
483 | chargingStation: ChargingStation, | |
484 | commandName: IncomingRequestCommand | RequestCommand, | |
485 | payload: T | |
486 | ): boolean { | |
487 | if (chargingStation.stationInfo?.ocppStrictCompliance === false) { | |
488 | return true | |
489 | } | |
490 | if (!this.payloadValidateFunctions.has(commandName as RequestCommand)) { | |
491 | logger.warn( | |
492 | `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: No JSON schema found for command '${commandName}' PDU validation` | |
493 | ) | |
494 | return true | |
495 | } | |
496 | const validate = this.payloadValidateFunctions.get(commandName as RequestCommand) | |
497 | payload = clone<T>(payload) | |
498 | convertDateToISOString<T>(payload) | |
499 | if (validate?.(payload) === true) { | |
500 | return true | |
501 | } | |
502 | logger.error( | |
503 | `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: Command '${commandName}' request PDU is invalid: %j`, | |
504 | validate?.errors | |
505 | ) | |
506 | // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError(). | |
507 | throw new OCPPError( | |
508 | ajvErrorsToErrorType(validate?.errors), | |
509 | 'Request PDU is invalid', | |
510 | commandName, | |
511 | JSON.stringify(validate?.errors, undefined, 2) | |
512 | ) | |
513 | } | |
c0560973 | 514 | } |