uapi-json 1.18.0 → 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/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,6 +1,6 @@
1
1
  {
2
2
  "name": "uapi-json",
3
- "version": "1.18.0",
3
+ "version": "1.19.0",
4
4
  "description": "Travelport Universal API",
5
5
  "main": "src/",
6
6
  "engines": {
@@ -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
 
@@ -209,50 +233,151 @@ module.exports = function (settings) {
209
233
  const getTerminalId = (sessionToken) => getHashSubstr(sessionToken);
210
234
  const isClosed = () => state.terminalState === TERMINAL_STATE_CLOSED;
211
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
+ }
212
261
 
213
- const terminal = {
214
- getToken: async () => {
215
- await getSessionToken();
216
- // Needed here as getSessionToken marks terminal as busy
217
- state.terminalState = TERMINAL_STATE_READY;
218
- return state.sessionToken;
219
- },
220
- executeCommand: async (command, stopMD = defaultStopMD) => {
221
- try {
222
- const sessionToken = await getSessionToken();
223
- const terminalId = getTerminalId(sessionToken);
262
+ const timeToWait = sleepInterval - (Date.now() - state.lastCommandFinishedAt);
224
263
 
225
- if (debug) {
226
- log(`[${terminalId}] Terminal request:\n${command}`);
227
- }
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);
228
272
 
229
- const screen = await executeCommandWithRetry(command);
230
- const response = await processResponse(screen, stopMD);
273
+ await sleepBeforeCommand(sleepInterval);
231
274
 
232
- if (debug) {
233
- log(`[${terminalId}] Terminal response:\n${response}`);
234
- }
275
+ if (debug) {
276
+ log(`[${terminalId}] Terminal request:\n${command}`);
277
+ }
235
278
 
236
- Object.assign(state, {
237
- terminalState: TERMINAL_STATE_READY,
238
- });
279
+ const screen = await executeCommandWithRetry(command);
280
+ const response = await processResponse(screen, stopMD);
239
281
 
240
- if (UNEXPECTED_TERMINAL_ERRORS.some((e) => response.includes(e))) {
241
- const errorObject = isFinancialCommand(command)
242
- ? new TerminalRuntimeError.TerminalUnexpectedFinancialError({ screen: response })
243
- : new TerminalRuntimeError.TerminalUnexpectedError({ screen: response });
282
+ if (debug) {
283
+ log(`[${terminalId}] Terminal response:\n${response}`);
284
+ }
244
285
 
245
- throw errorObject;
246
- }
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
+ }
321
+
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();
247
347
 
248
- return response;
348
+ try {
349
+ await getSessionToken();
350
+ setTerminalReady();
351
+ return state.sessionToken;
249
352
  } catch (err) {
250
- Object.assign(state, {
251
- terminalState: TERMINAL_STATE_ERROR,
252
- });
353
+ setTerminalError(err);
253
354
  throw err;
254
355
  }
255
356
  },
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,
374
+ });
375
+ });
376
+ }
377
+
378
+ setTerminalBusy();
379
+ return executeAndDrainCommands(command, commandOptions);
380
+ },
256
381
  isClosed,
257
382
  isInitialized,
258
383
  closeSessionSafe: async () => {
@@ -267,19 +392,36 @@ module.exports = function (settings) {
267
392
 
268
393
  await terminal.closeSession().catch(console.error);
269
394
  },
270
- closeSession: () => getSessionToken()
271
- .then(
272
- (sessionToken) => service.closeSession({
273
- sessionToken,
274
- })
275
- ).then(
276
- (response) => {
277
- Object.assign(state, {
278
- terminalState: TERMINAL_STATE_CLOSED,
279
- });
280
- return response;
281
- }
282
- ),
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
+ },
283
425
  };
284
426
 
285
427
  if (autoClose) {