uapi-json 1.17.6 → 1.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -89,6 +89,24 @@ It also has several useful helpers to handle errors.
89
89
 
90
90
  `logFunction` - set custom logging function that should match next shape `(...args) => {}`. Will receive all requests and responses from uapi/terminal.
91
91
 
92
+ `httpsAgent` - set custom HTTPS agent for all requests created by the service. If the agent has `options.timeout`, that value is used as the request timeout.
93
+
94
+ ```javascript
95
+ const https = require('https');
96
+
97
+ const TerminalService = uAPI.createTerminalService({
98
+ auth,
99
+ options: {
100
+ httpsAgent: new https.Agent({
101
+ keepAlive: true,
102
+ maxSockets: 1,
103
+ maxFreeSockets: 1,
104
+ timeout: 90000,
105
+ }),
106
+ },
107
+ });
108
+ ```
109
+
92
110
  ### Auth object
93
111
  <a name="auth"></a>
94
112
 
package/docs/Terminal.md CHANGED
@@ -49,11 +49,12 @@ see [close_session](#close_session) method.
49
49
  # API
50
50
 
51
51
  **TerminalService**
52
- * [`.executeCommand(command, stopMD)`](#execute_command)
52
+ * [`.executeCommand(command, options)`](#execute_command)
53
+ * [`.executeStatelessCommandWhenIdle(command, options)`](#execute_stateless_command_when_idle)
53
54
  * [`.closeSession()`](#close_session)
54
55
  * [`.getToken()`](#get_token)
55
56
 
56
- ## .executeCommand(command, stopMD)
57
+ ## .executeCommand(command, options)
57
58
  <a name="execute_command"></a>
58
59
  Executes a command in terminal and returns its terminal response
59
60
 
@@ -62,7 +63,61 @@ Executes a command in terminal and returns its terminal response
62
63
  | Param | Type | Description |
63
64
  | --- | --- | --- |
64
65
  | command | `String` | String representation of the command you want to execute |
66
+ | options | `Object` | Optional command execution options. |
67
+
68
+ **Options**
69
+
70
+ | Param | Type | Description |
71
+ | --- | --- | --- |
65
72
  | stopMD | `(screens) => boolean` | Function which gets all previous screens concatenated and detects if more `MD` command needed. |
73
+ | sleepInterval | `Number` | Minimum delay in milliseconds from the last successful full terminal response to the next command send. |
74
+
75
+ `stopMD` must be passed inside `options`; passing it directly as the second argument
76
+ is not supported.
77
+
78
+ ## .executeStatelessCommandWhenIdle(command, options)
79
+ <a name="execute_stateless_command_when_idle"></a>
80
+ Executes a stateless command when terminal is idle and returns its terminal response.
81
+
82
+ If terminal is busy, command is added to the stateless commands queue and executed
83
+ after the current command and previously queued stateless commands are finished.
84
+ Queued commands are executed one by one in FIFO order.
85
+ If terminal is idle, regular `executeCommand` calls execute before queued stateless
86
+ commands.
87
+
88
+ This method is promise based. Every caller receives the result or error from its own
89
+ command execution. If one queued command fails, later queued commands are still executed.
90
+ If the active command fails before the queue is drained, queued stateless commands are
91
+ skipped and rejected with the same terminal error.
92
+
93
+ Stateless means command is independent of terminal context, such as an open booking,
94
+ fare search, or other GDS working state. This method does not detect or enforce
95
+ statelessness; caller is responsible for using it only for commands safe in any
96
+ terminal context.
97
+
98
+ **Returns**: `Promise` that returns terminal command response in `String` format
99
+
100
+ | Param | Type | Description |
101
+ | --- | --- | --- |
102
+ | command | `String` | String representation of the stateless command you want to execute |
103
+ | options | `Object` | Optional command execution options. |
104
+
105
+ **Options**
106
+
107
+ | Param | Type | Description |
108
+ | --- | --- | --- |
109
+ | stopMD | `(screens) => boolean` | Function which gets all previous screens concatenated and detects if more `MD` command needed. |
110
+ | sleepInterval | `Number` | Minimum delay in milliseconds from the last successful full terminal response to the next command send. |
111
+
112
+ ```javascript
113
+ await TerminalService.executeCommand('TE', {
114
+ stopMD: (screens) => screens.includes('END OF DISPLAY'),
115
+ });
116
+
117
+ await TerminalService.executeStatelessCommandWhenIdle('.CDIEV', {
118
+ sleepInterval: 1000,
119
+ });
120
+ ```
66
121
 
67
122
  ## .closeSession()
68
123
  <a name="close_session"></a>
package/package.json CHANGED
@@ -1,8 +1,11 @@
1
1
  {
2
2
  "name": "uapi-json",
3
- "version": "1.17.6",
3
+ "version": "1.19.0",
4
4
  "description": "Travelport Universal API",
5
5
  "main": "src/",
6
+ "engines": {
7
+ "node": ">=20"
8
+ },
6
9
  "files": [
7
10
  "src/",
8
11
  "README.md",
@@ -27,36 +30,36 @@
27
30
  "node",
28
31
  "travelport"
29
32
  ],
30
- "author": "Mark Orel <mail.ormark@gmail.com>",
31
33
  "contributors": [
32
- "Dmytro Chertousov <d.chertousov@gmail.com>",
33
34
  "Artem Pylypchuk <articicejuice@gmail.com>",
34
- "Yevhenii Huselietov <d46k16@gmail.com>",
35
+ "Dmytro Chertousov <d.chertousov@gmail.com>",
36
+ "Mark Orel <mail.ormark@gmail.com>",
35
37
  "Mark Omarov <dev.mark.omarov@gmail.com>",
36
- "Oleksii Duvanov <colden.aid@gmail.com>"
38
+ "Oleksii Duvanov <colden.aid@gmail.com>",
39
+ "Yevhenii Huselietov <d46k16@gmail.com>"
37
40
  ],
38
41
  "license": "MIT",
39
42
  "dependencies": {
40
- "axios": "^1.12.2",
43
+ "axios": "~1.15.0",
41
44
  "galileo-screen": "1.0.5",
42
- "handlebars": "^4.7.8",
43
- "handlebars-helper-equal": "^1.0.0",
44
- "joi": "^18.0.1",
45
- "moment": "^2.30.1",
46
- "node-errors-helpers": "^1.0.0",
47
- "pretty-data": "^0.40.0",
48
- "promise-retry": "^2.0.1",
49
- "xml2js": "^0.6.2"
45
+ "handlebars": "~4.7.9",
46
+ "handlebars-helper-equal": "~1.0.0",
47
+ "joi": "~18.1.2",
48
+ "moment": "~2.30.1",
49
+ "node-errors-helpers": "~1.0.0",
50
+ "pretty-data": "~0.40.0",
51
+ "promise-retry": "~2.0.1",
52
+ "xml2js": "~0.6.2"
50
53
  },
51
54
  "devDependencies": {
52
55
  "chai": "^4.5.0",
53
56
  "eslint": "^8.39.0",
54
57
  "eslint-config-airbnb-base": "^15.0.0",
55
58
  "eslint-plugin-import": "^2.32.0",
56
- "mocha": "^11.7.4",
59
+ "mocha": "^11.7.5",
57
60
  "nyc": "^17.1.0",
58
- "proxyquire": "^2.1.3",
59
- "readline2": "^1.0.1",
61
+ "proxyquire": "~2.1.3",
62
+ "readline2": "~1.0.1",
60
63
  "sinon": "^21.0.0",
61
64
  "sinon-chai": "^3.7.0"
62
65
  }
@@ -36,8 +36,9 @@ function mergeLeaf(item) {
36
36
  return { ...item, ...leaf };
37
37
  }
38
38
 
39
- function Parser(root, uapiVersion, env, debug, config, provider) {
39
+ function Parser(root, uapiVersion, env, debug, config, provider, log) {
40
40
  this.debug = debug;
41
+ this.log = log || console.info;
41
42
  if (!config) {
42
43
  this.config = defaultConfig(uapiVersion);
43
44
  } else {
@@ -212,7 +213,7 @@ Parser.prototype.parse = function (xml) {
212
213
 
213
214
  const end = new Date() - start;
214
215
  if (this.debug > 1) {
215
- console.info('uAPI_Parse execution time: %dms', end);
216
+ this.log('uAPI_Parse execution time: %dms', end);
216
217
  }
217
218
 
218
219
  return data[self.rootObject];
@@ -1,5 +1,6 @@
1
1
  const handlebars = require('handlebars');
2
2
  const axios = require('axios');
3
+ const https = require('https');
3
4
  const { pd } = require('pretty-data');
4
5
  const {
5
6
  RequestValidationError,
@@ -13,6 +14,14 @@ const configInit = require('../config');
13
14
 
14
15
  handlebars.registerHelper('equal', require('handlebars-helper-equal'));
15
16
 
17
+ const REQUEST_AGENT_OPTIONS = {
18
+ keepAlive: true,
19
+ keepAliveMsecs: 5000,
20
+ maxSockets: 20,
21
+ maxFreeSockets: 20,
22
+ };
23
+ const httpsAgent = new https.Agent(REQUEST_AGENT_OPTIONS);
24
+
16
25
  /**
17
26
  * basic function for requests/responses
18
27
  * @param {string} service service url for current response (gateway)
@@ -43,6 +52,15 @@ module.exports = function uapiRequest(
43
52
 
44
53
  const config = configInit(auth.region);
45
54
  const log = options.logFunction || console.log;
55
+ const customHttpsAgent = options.httpsAgent;
56
+ const requestHttpsAgent = customHttpsAgent || httpsAgent;
57
+ const requestTimeout = (
58
+ customHttpsAgent
59
+ && customHttpsAgent.options
60
+ && typeof customHttpsAgent.options.timeout === 'number'
61
+ )
62
+ ? customHttpsAgent.options.timeout
63
+ : config.timeout || 5000;
46
64
 
47
65
  // Performing checks
48
66
  if (!service || service.length <= 0) {
@@ -64,7 +82,7 @@ module.exports = function uapiRequest(
64
82
  }
65
83
 
66
84
  // create a v52 uAPI parser with default params and request data in env
67
- const uParser = new Parser(rootObject, 'v52_0', params, debugMode, null, auth.provider);
85
+ const uParser = new Parser(rootObject, 'v52_0', params, debugMode, null, auth.provider, log);
68
86
 
69
87
  const validateInput = () => (
70
88
  Promise.resolve(params)
@@ -85,7 +103,8 @@ module.exports = function uapiRequest(
85
103
  const response = await axios.request({
86
104
  url: service,
87
105
  method: 'POST',
88
- timeout: config.timeout || 5000,
106
+ timeout: requestTimeout,
107
+ httpsAgent: requestHttpsAgent,
89
108
  auth: {
90
109
  username: auth.username,
91
110
  password: auth.password,
@@ -156,7 +175,6 @@ module.exports = function uapiRequest(
156
175
  };
157
176
 
158
177
  const validateSOAP = function (parsedXML) {
159
- console.log(parsedXML);
160
178
  if (parsedXML['SOAP:Fault']) {
161
179
  if (debugMode > 2) {
162
180
  log('Parsed error response', pd.json(parsedXML));
@@ -169,7 +187,8 @@ module.exports = function uapiRequest(
169
187
  params,
170
188
  debugMode,
171
189
  errParserConfig,
172
- auth.provider
190
+ auth.provider,
191
+ log
173
192
  );
174
193
  const errData = errParser.mergeLeafRecursive(parsedXML['SOAP:Fault'][0]); // parse error data
175
194
  return errorHandler.call(errParser, errData);
@@ -197,7 +197,7 @@ function formatPassengerCategories(pricingInfo) {
197
197
 
198
198
  [code] = list;
199
199
  if (!list[0] || list.length !== 1) { // TODO throw error
200
- console.log('Warning: different categories '
200
+ console.warn('Warning: different categories '
201
201
  + list.join() + ' in single fare calculation ' + key + ' in fare ' + key);
202
202
  }
203
203
  passengerCounts[code] = count;
@@ -218,7 +218,7 @@ function airPrice(obj) {
218
218
 
219
219
  let pricingSolution = 0;
220
220
  if (priceKeys.length > 1) {
221
- console.log('More than one solution found in booking. Resolving the cheapest one.');
221
+ console.warn('More than one solution found in booking. Resolving the cheapest one.');
222
222
  const solutions = priceKeys.map((key) => pricingSolutions[key]);
223
223
 
224
224
  [pricingSolution] = solutions.sort(
@@ -347,7 +347,7 @@ function airPriceRspPricingSolutionXML(obj) {
347
347
  let pricingSolution = 0;
348
348
  if (pricingSolutions.length > 1) {
349
349
  // TODO: Check result for multiple passenger type results.
350
- console.log('More than one solution found in booking. Resolving the cheapest one.');
350
+ console.warn('More than one solution found in booking. Resolving the cheapest one.');
351
351
  [pricingSolution] = pricingSolutions.sort(
352
352
  (a, b) => parseFloat(a.$.TotalPrice.slice(3)) - parseFloat(b.$.TotalPrice.slice(3))
353
353
  );
@@ -101,6 +101,17 @@ const isErrorRetriable = (e) => {
101
101
  return (errorCode && RETRY_CODES_LIST.includes(errorCode));
102
102
  };
103
103
 
104
+ const defaultStopMD = (screens) => !screenFunctions.hasMore(screens);
105
+
106
+ const getCommandOptions = (options = {}) => ({
107
+ stopMD: options.stopMD || defaultStopMD,
108
+ sleepInterval: options.sleepInterval || 0,
109
+ });
110
+
111
+ const wait = (ms) => new Promise((resolve) => {
112
+ setTimeout(resolve, ms);
113
+ });
114
+
104
115
  module.exports = function (settings) {
105
116
  const {
106
117
  timeout = false, debug = 0, autoClose = true,
@@ -109,25 +120,37 @@ module.exports = function (settings) {
109
120
  } = settings;
110
121
 
111
122
  const service = terminalService(validateServiceSettings(settings));
112
- const defaultStopMD = (screens) => !screenFunctions.hasMore(screens);
113
123
 
114
124
  const state = {
115
125
  terminalState: TERMINAL_STATE_NONE,
116
126
  sessionToken: token,
127
+ terminalError: null,
128
+ lastCommandFinishedAt: null,
129
+ statelessCommandsQueue: [],
117
130
  };
118
131
 
119
132
  // Getting session token
120
- const getSessionToken = async () => {
121
- // Return error if not in correct state
133
+ const throwIfTerminalUnavailable = () => {
122
134
  if (state.terminalState === TERMINAL_STATE_BUSY) {
123
135
  throw new TerminalRuntimeError.TerminalIsBusy();
124
136
  }
125
137
  if (state.terminalState === TERMINAL_STATE_CLOSED) {
126
138
  throw new TerminalRuntimeError.TerminalIsClosed();
127
139
  }
140
+ if (state.terminalState === TERMINAL_STATE_ERROR) {
141
+ throw state.terminalError;
142
+ }
143
+ };
128
144
 
129
- state.terminalState = TERMINAL_STATE_BUSY;
145
+ const setTerminalBusy = () => {
146
+ throwIfTerminalUnavailable();
147
+ Object.assign(state, {
148
+ terminalState: TERMINAL_STATE_BUSY,
149
+ terminalError: null,
150
+ });
151
+ };
130
152
 
153
+ const getSessionToken = async () => {
131
154
  // Return token if already obtained
132
155
  if (state.sessionToken !== null) {
133
156
  return state.sessionToken;
@@ -137,7 +160,6 @@ module.exports = function (settings) {
137
160
  state.sessionToken = sessionToken;
138
161
 
139
162
  if (!emulatePcc) {
140
- state.terminalState = TERMINAL_STATE_READY;
141
163
  return state.sessionToken;
142
164
  }
143
165
 
@@ -158,6 +180,8 @@ module.exports = function (settings) {
158
180
  throw new TerminalRuntimeError.TerminalEmulationFailed(response);
159
181
  }
160
182
 
183
+ state.lastCommandFinishedAt = Date.now();
184
+
161
185
  return sessionToken;
162
186
  };
163
187
 
@@ -207,63 +231,197 @@ module.exports = function (settings) {
207
231
 
208
232
  // Get terminal ID
209
233
  const getTerminalId = (sessionToken) => getHashSubstr(sessionToken);
234
+ const isClosed = () => state.terminalState === TERMINAL_STATE_CLOSED;
235
+ const isInitialized = () => state.terminalState !== TERMINAL_STATE_NONE;
236
+ const hasStatelessCommands = () => (
237
+ state.statelessCommandsQueue.length > 0
238
+ );
239
+ const setTerminalReady = () => {
240
+ Object.assign(state, {
241
+ terminalState: TERMINAL_STATE_READY,
242
+ terminalError: null,
243
+ });
244
+ };
245
+ const setTerminalError = (err) => {
246
+ Object.assign(state, {
247
+ terminalState: TERMINAL_STATE_ERROR,
248
+ terminalError: err,
249
+ });
250
+ };
251
+ const rejectStatelessCommandsQueue = (err) => {
252
+ while (state.statelessCommandsQueue.length > 0) {
253
+ const { reject } = state.statelessCommandsQueue.shift();
254
+ reject(err);
255
+ }
256
+ };
257
+ const sleepBeforeCommand = async (sleepInterval) => {
258
+ if (state.lastCommandFinishedAt === null || sleepInterval <= 0) {
259
+ return;
260
+ }
210
261
 
211
- const terminal = {
212
- getToken: async () => {
213
- await getSessionToken();
214
- // Needed here as getSessionToken marks terminal as busy
215
- state.terminalState = TERMINAL_STATE_READY;
216
- return state.sessionToken;
217
- },
218
- executeCommand: async (command, stopMD = defaultStopMD) => {
219
- try {
220
- const sessionToken = await getSessionToken();
221
- const terminalId = getTerminalId(sessionToken);
262
+ const timeToWait = sleepInterval - (Date.now() - state.lastCommandFinishedAt);
222
263
 
223
- if (debug) {
224
- log(`[${terminalId}] Terminal request:\n${command}`);
225
- }
264
+ if (timeToWait > 0) {
265
+ await wait(timeToWait);
266
+ }
267
+ };
268
+ const executeCommandInternal = async (command, options) => {
269
+ const { stopMD, sleepInterval } = options;
270
+ const sessionToken = await getSessionToken();
271
+ const terminalId = getTerminalId(sessionToken);
226
272
 
227
- const screen = await executeCommandWithRetry(command);
228
- const response = await processResponse(screen, stopMD);
273
+ await sleepBeforeCommand(sleepInterval);
229
274
 
230
- if (debug) {
231
- log(`[${terminalId}] Terminal response:\n${response}`);
232
- }
275
+ if (debug) {
276
+ log(`[${terminalId}] Terminal request:\n${command}`);
277
+ }
233
278
 
234
- Object.assign(state, {
235
- terminalState: TERMINAL_STATE_READY,
236
- });
279
+ const screen = await executeCommandWithRetry(command);
280
+ const response = await processResponse(screen, stopMD);
237
281
 
238
- if (UNEXPECTED_TERMINAL_ERRORS.some((e) => response.includes(e))) {
239
- const errorObject = isFinancialCommand(command)
240
- ? new TerminalRuntimeError.TerminalUnexpectedFinancialError({ screen: response })
241
- : new TerminalRuntimeError.TerminalUnexpectedError({ screen: response });
282
+ if (debug) {
283
+ log(`[${terminalId}] Terminal response:\n${response}`);
284
+ }
242
285
 
243
- throw errorObject;
244
- }
286
+ if (UNEXPECTED_TERMINAL_ERRORS.some((e) => response.includes(e))) {
287
+ const errorObject = isFinancialCommand(command)
288
+ ? new TerminalRuntimeError.TerminalUnexpectedFinancialError({ screen: response })
289
+ : new TerminalRuntimeError.TerminalUnexpectedError({ screen: response });
290
+
291
+ throw errorObject;
292
+ }
293
+
294
+ state.lastCommandFinishedAt = Date.now();
295
+
296
+ return response;
297
+ };
298
+ const drainStatelessCommandsQueue = async () => {
299
+ if (state.statelessCommandsQueue.length === 0) {
300
+ setTerminalReady();
301
+ return;
302
+ }
303
+
304
+ const {
305
+ command, options, resolve, reject,
306
+ } = state.statelessCommandsQueue.shift();
307
+
308
+ try {
309
+ const response = await executeCommandInternal(command, options);
310
+
311
+ if (!hasStatelessCommands()) {
312
+ setTerminalReady();
313
+ }
314
+ resolve(response);
315
+ } catch (err) {
316
+ if (!hasStatelessCommands()) {
317
+ setTerminalReady();
318
+ }
319
+ reject(err);
320
+ }
245
321
 
246
- return response;
322
+ await drainStatelessCommandsQueue();
323
+ };
324
+ const executeAndDrainCommands = async (command, options) => {
325
+ try {
326
+ const response = await executeCommandInternal(command, options);
327
+
328
+ if (hasStatelessCommands()) {
329
+ drainStatelessCommandsQueue()
330
+ .then(setTerminalReady)
331
+ .catch(setTerminalError);
332
+ } else {
333
+ setTerminalReady();
334
+ }
335
+
336
+ return response;
337
+ } catch (err) {
338
+ setTerminalError(err);
339
+ rejectStatelessCommandsQueue(err);
340
+ throw err;
341
+ }
342
+ };
343
+
344
+ const terminal = {
345
+ getToken: async () => {
346
+ setTerminalBusy();
347
+
348
+ try {
349
+ await getSessionToken();
350
+ setTerminalReady();
351
+ return state.sessionToken;
247
352
  } catch (err) {
248
- Object.assign(state, {
249
- terminalState: TERMINAL_STATE_ERROR,
250
- });
353
+ setTerminalError(err);
251
354
  throw err;
252
355
  }
253
356
  },
254
- closeSession: () => getSessionToken()
255
- .then(
256
- (sessionToken) => service.closeSession({
257
- sessionToken,
258
- })
259
- ).then(
260
- (response) => {
261
- Object.assign(state, {
262
- terminalState: TERMINAL_STATE_CLOSED,
357
+ executeCommand: async (command, options) => {
358
+ setTerminalBusy();
359
+ return executeAndDrainCommands(command, getCommandOptions(options));
360
+ },
361
+ executeStatelessCommandWhenIdle: async (command, options) => {
362
+ const commandOptions = getCommandOptions(options);
363
+
364
+ if (state.terminalState === TERMINAL_STATE_CLOSED) {
365
+ throw new TerminalRuntimeError.TerminalIsClosed();
366
+ }
367
+ if (state.terminalState === TERMINAL_STATE_ERROR) {
368
+ throw state.terminalError;
369
+ }
370
+ if (state.terminalState === TERMINAL_STATE_BUSY || hasStatelessCommands()) {
371
+ return new Promise((resolve, reject) => {
372
+ state.statelessCommandsQueue.push({
373
+ command, options: commandOptions, resolve, reject,
263
374
  });
264
- return response;
265
- }
266
- ),
375
+ });
376
+ }
377
+
378
+ setTerminalBusy();
379
+ return executeAndDrainCommands(command, commandOptions);
380
+ },
381
+ isClosed,
382
+ isInitialized,
383
+ closeSessionSafe: async () => {
384
+ if (isClosed()) {
385
+ console.warn('UAPI-JSON WARNING: Terminal session is already closed');
386
+ return;
387
+ }
388
+ if (!isInitialized()) {
389
+ console.warn('UAPI-JSON WARNING: Terminal session is not initialized');
390
+ return;
391
+ }
392
+
393
+ await terminal.closeSession().catch(console.error);
394
+ },
395
+ closeSession: () => {
396
+ if (state.terminalState === TERMINAL_STATE_BUSY) {
397
+ return Promise.reject(new TerminalRuntimeError.TerminalIsBusy());
398
+ }
399
+ if (state.terminalState === TERMINAL_STATE_CLOSED) {
400
+ return Promise.reject(new TerminalRuntimeError.TerminalIsClosed());
401
+ }
402
+ if (state.terminalState === TERMINAL_STATE_ERROR && state.sessionToken === null) {
403
+ return Promise.reject(state.terminalError);
404
+ }
405
+
406
+ Object.assign(state, {
407
+ terminalState: TERMINAL_STATE_BUSY,
408
+ });
409
+
410
+ return getSessionToken()
411
+ .then(
412
+ (sessionToken) => service.closeSession({
413
+ sessionToken,
414
+ })
415
+ ).then(
416
+ (response) => {
417
+ Object.assign(state, {
418
+ terminalState: TERMINAL_STATE_CLOSED,
419
+ terminalError: null,
420
+ });
421
+ return response;
422
+ }
423
+ );
424
+ },
267
425
  };
268
426
 
269
427
  if (autoClose) {