@opentap/runner-client 2.3.2 → 2.3.3-alpha.1.1

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.
@@ -1,5 +1,11 @@
1
1
  import { ComponentSettingsBase, ComponentSettingsIdentifier, ComponentSettingsListItem, DataGridControl, ErrorResponse, FileParameter, FileResponse, ListItemType, ProfileGroup, RepositoryPackageReference, RepositorySettingsPackageDefinition, SettingsTapPackage } from './DTOs';
2
- import { ConnectionOptions, RequestOptions, Subscription, SubscriptionOptions } from 'nats.ws';
2
+ import { ConnectionOptions, Subscription, SubscriptionOptions, PublishOptions } from 'nats.ws';
3
+ interface BaseClientRequestOptions {
4
+ publishOptions?: PublishOptions;
5
+ rawResponse?: boolean;
6
+ fullSubject?: boolean;
7
+ timeout?: number;
8
+ }
3
9
  export declare class BaseClient {
4
10
  private connection;
5
11
  private baseSubject;
@@ -9,7 +15,6 @@ export declare class BaseClient {
9
15
  private _accessToken;
10
16
  private _headers;
11
17
  private _timeout;
12
- private _chunkSize;
13
18
  /** Get request access token */
14
19
  get accessToken(): string;
15
20
  /** Set request access token */
@@ -23,6 +28,7 @@ export declare class BaseClient {
23
28
  /** Set timeout in milliseconds. Default is 40000 milliseconds */
24
29
  set timeout(value: number);
25
30
  constructor(baseSubject: string, options: ConnectionOptions);
31
+ private withTimeout;
26
32
  /**
27
33
  * Send a request to the nats server.
28
34
  * @param subject The subject to request
@@ -30,7 +36,7 @@ export declare class BaseClient {
30
36
  * @param options (optional)
31
37
  * @returns Promise of an object
32
38
  */
33
- protected request<T>(subject: string, payload?: any, options?: RequestOptions, rawResponse?: boolean, fullSubject?: boolean): Promise<T>;
39
+ protected request<T>(subject: string, payload?: any, options?: BaseClientRequestOptions): Promise<T>;
34
40
  /**
35
41
  * Handle the error
36
42
  * @param error
@@ -50,14 +56,6 @@ export declare class BaseClient {
50
56
  * @returns Subscription object
51
57
  */
52
58
  protected subscribe(subject: string, options: SubscriptionOptions): Subscription;
53
- /**
54
- * Receive a chunked file specified by a request response
55
- * @param requestResponse Contains a reply subject and a file descriptor which can be used to download chunks
56
- * @param rawResponse If true, the response should not be decoded
57
- * @param opts Request options
58
- * @returns
59
- */
60
- private downloadChunkedRequest;
61
59
  protected encode(payload: any): Uint8Array;
62
60
  /**
63
61
  * Check if the the response is an error from the server.
@@ -246,3 +244,4 @@ export declare class BaseClient {
246
244
  */
247
245
  downloadSettingsPackage(settingsTapPackage: SettingsTapPackage): Promise<FileResponse>;
248
246
  }
247
+ export {};
package/lib/BaseClient.js CHANGED
@@ -45,17 +45,8 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
45
45
  if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
46
46
  }
47
47
  };
48
- var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
49
- if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
50
- if (ar || !(i in from)) {
51
- if (!ar) ar = Array.prototype.slice.call(from, 0, i);
52
- ar[i] = from[i];
53
- }
54
- }
55
- return to.concat(ar || Array.prototype.slice.call(from));
56
- };
57
48
  import { ComponentSettingsBase, ComponentSettingsIdentifier, ComponentSettingsListItem, DataGridControl, ErrorResponse, FileDescriptor, ListItemType, ProfileGroup, } from './DTOs';
58
- import { Empty, ErrorCode, JSONCodec, NatsError, StringCodec, connect, headers, } from 'nats.ws';
49
+ import { Empty, ErrorCode, JSONCodec, StringCodec, connect, headers, } from 'nats.ws';
59
50
  import { EventEmitter } from 'events';
60
51
  var DEFAULT_TIMEOUT = 40000; // default timeout of 40 seconds
61
52
  var Events;
@@ -66,7 +57,6 @@ var BaseClient = /** @class */ (function () {
66
57
  function BaseClient(baseSubject, options) {
67
58
  this.domainAccess = new Map();
68
59
  this._headers = new Headers();
69
- this._chunkSize = 512000;
70
60
  this.baseSubject = baseSubject;
71
61
  this.connectionOptions = __assign({}, options) || {};
72
62
  this.connectionOptions.timeout = (options === null || options === void 0 ? void 0 : options.timeout) || DEFAULT_TIMEOUT;
@@ -108,6 +98,9 @@ var BaseClient = /** @class */ (function () {
108
98
  enumerable: false,
109
99
  configurable: true
110
100
  });
101
+ BaseClient.prototype.withTimeout = function (promise, timeout) {
102
+ return Promise.race([promise, new Promise(function (_, reject) { return setTimeout(function () { return reject(new Error(ErrorCode.Timeout)); }, timeout); })]);
103
+ };
111
104
  /**
112
105
  * Send a request to the nats server.
113
106
  * @param subject The subject to request
@@ -115,74 +108,113 @@ var BaseClient = /** @class */ (function () {
115
108
  * @param options (optional)
116
109
  * @returns Promise of an object
117
110
  */
118
- BaseClient.prototype.request = function (subject, payload, options, rawResponse, fullSubject) {
111
+ BaseClient.prototype.request = function (subject, payload, options) {
112
+ var _a, _b, _c;
119
113
  return __awaiter(this, void 0, void 0, function () {
120
- var originalSubject, data, headers, timeout, opts, fileDescriptor, getChunk, requestResponse, dataSubject, i, result, error, ex_1;
121
- return __generator(this, function (_a) {
122
- switch (_a.label) {
123
- case 0:
124
- // Prepend the base subject if the given subject does not start with that
125
- if (!fullSubject) {
126
- subject = "".concat(this.baseSubject, ".Request.").concat(subject);
114
+ var data, headers, timeout, replySubject, chunkSize, requestId, opts, fileDescriptor, chunkNumber, getChunk, subscription, responsePromise, chunk, i;
115
+ var _this = this;
116
+ return __generator(this, function (_d) {
117
+ // Prepend the base subject if the given subject does not start with that
118
+ if (!(options === null || options === void 0 ? void 0 : options.fullSubject)) {
119
+ subject = "".concat(this.baseSubject, ".Request.").concat(subject);
120
+ }
121
+ if (!this.connection)
122
+ return [2 /*return*/, Promise.reject("".concat(subject, ": Connection is down! Please try again!"))];
123
+ if (this.connection.isClosed())
124
+ return [2 /*return*/, Promise.reject("".concat(subject, ": Connection has been closed! Please reconnect!"))];
125
+ data = this.encode(payload);
126
+ headers = this.buildHeaders();
127
+ timeout = (options === null || options === void 0 ? void 0 : options.timeout) || this.timeout;
128
+ replySubject = crypto.randomUUID();
129
+ chunkSize = ((_c = (_b = (_a = this.connection) === null || _a === void 0 ? void 0 : _a.info) === null || _b === void 0 ? void 0 : _b.max_payload) !== null && _c !== void 0 ? _c : 512000) - 2000;
130
+ // The Session and the Client need to agree on the chunk size. Put it in a header.
131
+ headers.append('ChunkSize', chunkSize.toString());
132
+ requestId = crypto.randomUUID();
133
+ headers.append('RequestId', requestId);
134
+ opts = __assign(__assign({}, options === null || options === void 0 ? void 0 : options.publishOptions), { headers: headers, reply: replySubject });
135
+ fileDescriptor = new FileDescriptor(data.length, chunkSize);
136
+ chunkNumber = 1;
137
+ headers.set('ChunkNumber', chunkNumber.toString());
138
+ getChunk = function (chunk) {
139
+ var offset = chunk * fileDescriptor.chunkSize;
140
+ return data.slice(offset, offset + fileDescriptor.chunkSize);
141
+ };
142
+ subscription = this.connection.subscribe(replySubject);
143
+ responsePromise = new Promise(function (resolve, reject) {
144
+ var messages = [];
145
+ subscription.callback = function (error, message) {
146
+ var _a;
147
+ if (error) {
148
+ reject(error);
127
149
  }
128
- originalSubject = subject;
129
- if (!this.connection)
130
- return [2 /*return*/, Promise.reject("".concat(subject, ": Connection is down! Please try again!"))];
131
- if (this.connection.isClosed())
132
- return [2 /*return*/, Promise.reject("".concat(subject, ": Connection has been closed! Please reconnect!"))];
133
- data = this.encode(payload);
134
- headers = this.buildHeaders();
135
- headers.append('ChunkSize', this._chunkSize.toString());
136
- timeout = (options === null || options === void 0 ? void 0 : options.timeout) || this.timeout;
137
- opts = __assign(__assign({}, options), { timeout: timeout, headers: headers });
138
- fileDescriptor = new FileDescriptor(data.length, this._chunkSize);
139
- getChunk = function (chunk) {
140
- var offset = chunk * fileDescriptor.chunkSize;
141
- return data.slice(offset, offset + fileDescriptor.chunkSize);
142
- };
143
- _a.label = 1;
144
- case 1:
145
- _a.trys.push([1, 9, , 10]);
146
- requestResponse = void 0;
147
- dataSubject = subject;
148
- i = 0;
149
- _a.label = 2;
150
- case 2:
151
- if (!(i < fileDescriptor.numberOfChunks)) return [3 /*break*/, 5];
152
- return [4 /*yield*/, this.connection.request(dataSubject, getChunk(i), opts)];
153
- case 3:
154
- requestResponse = _a.sent();
155
- dataSubject = requestResponse.reply;
156
- _a.label = 4;
157
- case 4:
158
- i++;
159
- return [3 /*break*/, 2];
160
- case 5:
161
- if (!(fileDescriptor.fileSize % fileDescriptor.chunkSize === 0)) return [3 /*break*/, 7];
162
- return [4 /*yield*/, this.connection.request(dataSubject, Empty, opts)];
163
- case 6:
164
- requestResponse = _a.sent();
165
- _a.label = 7;
166
- case 7: return [4 /*yield*/, this.downloadChunkedRequest(requestResponse, rawResponse, opts)];
167
- case 8:
168
- result = _a.sent();
169
- if (this.isErrorResponse(result)) {
170
- error = ErrorResponse.fromJS(result);
171
- this.eventEmitter.emit(Events.ERROR, error);
172
- return [2 /*return*/, Promise.reject(error)];
150
+ // Put all the response chunks in an array in the order they are received
151
+ messages.push(message);
152
+ // If the chunk has a size equal to the chunkSize, we should expect another message
153
+ if (message.data.length === chunkSize) {
154
+ return;
173
155
  }
174
- return [2 /*return*/, result];
175
- case 9:
176
- ex_1 = _a.sent();
177
- if (ex_1 instanceof NatsError) {
178
- return [2 /*return*/, Promise.reject(this.natsErrorHandler(ex_1, originalSubject))];
156
+ // If the chunk has a length smaller than the chunkSize, the message is complete
157
+ // If the final message was received, we can safely unsubscribe
158
+ subscription.unsubscribe();
159
+ // Check if the number of the final message is equal to the number of
160
+ // messages we received. If this is not the case, we dropped a chunk,
161
+ // likely due to a network error. In this case, the entire message is invalid.
162
+ var finalMessage = message;
163
+ var finalMessageNumber = (_a = finalMessage.headers) === null || _a === void 0 ? void 0 : _a.get('ChunkNumber');
164
+ if (!finalMessageNumber) {
165
+ return reject('Response is not a valid chunk.');
179
166
  }
180
- else {
181
- return [2 /*return*/, Promise.reject(ex_1)];
167
+ if (parseInt(finalMessageNumber) !== messages.length) {
168
+ return reject("Expected {finalMessageNumber} chunks, but received ".concat(messages.length, ". ") +
169
+ "The connection may have been interrupted.");
182
170
  }
183
- return [3 /*break*/, 10];
184
- case 10: return [2 /*return*/];
171
+ // Concatenate the payloads
172
+ // When there are many chunks, doing a single allocation
173
+ // is significantly faster than concatenating arrays in sequence
174
+ var dataArrays = messages.map(function (m) { return m.data; });
175
+ var flattenedSize = dataArrays.reduce(function (sum, array) { return sum + array.length; }, 0);
176
+ var flattenedArray = new Uint8Array(flattenedSize);
177
+ var k = 0;
178
+ dataArrays.map(function (m) {
179
+ for (var i = 0; i < m.length; i++) {
180
+ flattenedArray[k++] = m[i];
181
+ }
182
+ });
183
+ return resolve(flattenedArray);
184
+ };
185
+ })
186
+ .then(function (byteArray) {
187
+ if (byteArray.length === 0) {
188
+ return Promise.resolve(undefined);
189
+ }
190
+ var jsonCodec = JSONCodec();
191
+ // If a raw response is requested, we should avoid decoding the bytes.
192
+ var response = (options === null || options === void 0 ? void 0 : options.rawResponse) ? byteArray : jsonCodec.decode(byteArray);
193
+ return _this.isErrorResponse(response) ? Promise.reject(ErrorResponse.fromJS(response)) : Promise.resolve(response);
194
+ })
195
+ .catch(function (err) {
196
+ return Promise.reject(_this.natsErrorHandler(err, subject));
197
+ });
198
+ chunk = getChunk(0);
199
+ this.connection.publish(subject, chunk, opts);
200
+ chunkNumber += 1;
201
+ for (i = 1; i < fileDescriptor.numberOfChunks; i++) {
202
+ headers.set('ChunkNumber', chunkNumber.toString());
203
+ chunk = getChunk(i);
204
+ this.connection.publish(subject, chunk, opts);
205
+ chunkNumber += 1;
206
+ }
207
+ // In the special case where the last published chunk was full, we need to publish
208
+ // an empty message
209
+ if (data.length > 0 && data.length % fileDescriptor.chunkSize === 0) {
210
+ headers.set('ChunkNumber', chunkNumber.toString());
211
+ this.connection.publish(subject, Empty, opts);
185
212
  }
213
+ // Now that we have sent the terminating chunk, the result should arrive on our promise.
214
+ return [2 /*return*/, this.withTimeout(responsePromise, timeout).catch(function (err) {
215
+ subscription.unsubscribe();
216
+ return Promise.reject(_this.natsErrorHandler(err, subject));
217
+ })];
186
218
  });
187
219
  });
188
220
  };
@@ -234,50 +266,6 @@ var BaseClient = /** @class */ (function () {
234
266
  var natsSubject = "".concat(this.baseSubject, ".").concat(subject);
235
267
  return this.connection.subscribe(natsSubject, options);
236
268
  };
237
- /**
238
- * Receive a chunked file specified by a request response
239
- * @param requestResponse Contains a reply subject and a file descriptor which can be used to download chunks
240
- * @param rawResponse If true, the response should not be decoded
241
- * @param opts Request options
242
- * @returns
243
- */
244
- BaseClient.prototype.downloadChunkedRequest = function (requestResponse, rawResponse, opts) {
245
- return __awaiter(this, void 0, void 0, function () {
246
- var result, resultCodec;
247
- return __generator(this, function (_a) {
248
- switch (_a.label) {
249
- case 0:
250
- if (!this.connection)
251
- return [2 /*return*/, Promise.reject('Chunking: Connection is down! Please try again!')];
252
- if (this.connection.isClosed())
253
- return [2 /*return*/, Promise.reject("Chunking: Connection has been closed! Please reconnect!")];
254
- result = new Uint8Array([]);
255
- _a.label = 1;
256
- case 1:
257
- result = new Uint8Array(__spreadArray(__spreadArray([], Array.from(result), true), Array.from(requestResponse.data), true));
258
- // Acknowledge that the final chunk was received
259
- if (requestResponse.data.length < this._chunkSize) {
260
- this.connection.publish(requestResponse.reply, Empty, opts);
261
- return [3 /*break*/, 4];
262
- }
263
- return [4 /*yield*/, this.connection.request(requestResponse.reply, Empty, opts)];
264
- case 2:
265
- requestResponse = _a.sent();
266
- _a.label = 3;
267
- case 3: return [3 /*break*/, 1];
268
- case 4:
269
- if (result.length !== 0) {
270
- // The runner skips the encoding step if the return type is a byte array, so we must also skip the decoding step in this case.
271
- if (rawResponse)
272
- return [2 /*return*/, Promise.resolve(result)];
273
- resultCodec = JSONCodec();
274
- return [2 /*return*/, resultCodec.decode(result)];
275
- }
276
- return [2 /*return*/];
277
- }
278
- });
279
- });
280
- };
281
269
  BaseClient.prototype.encode = function (payload) {
282
270
  if (!payload) {
283
271
  return Empty;
@@ -51,7 +51,7 @@ var RunnerClient = /** @class */ (function (_super) {
51
51
  */
52
52
  RunnerClient.prototype.resolveImage = function (images, timeout) {
53
53
  return (images === null || images === void 0 ? void 0 : images.length) > 0
54
- ? this.request('ResolveImage', images, { timeout: timeout || this.timeout })
54
+ ? this.request('ResolveImage', images, { timeout: timeout })
55
55
  .then(function (imageJs) { return Image.fromJS(imageJs); })
56
56
  .then(this.success())
57
57
  .catch(this.error())
@@ -218,7 +218,9 @@ var SessionClient = /** @class */ (function (_super) {
218
218
  return Promise.reject('The source of the provided resource is not a nats subject.');
219
219
  }
220
220
  var subject = resource.source.slice(runnerResourcePrefix.length);
221
- return this.request(subject, undefined, undefined, true, true).then(this.success()).catch(this.error());
221
+ return this.request(subject, undefined, { rawResponse: true, fullSubject: true })
222
+ .then(this.success())
223
+ .catch(this.error());
222
224
  };
223
225
  /**
224
226
  * Load test plan using a test plan TapPackage from a repository
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opentap/runner-client",
3
- "version": "2.3.2",
3
+ "version": "2.3.3-alpha.1.1",
4
4
  "description": "This is the web client for the OpenTAP Runner.",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",