@loadmill/executer 0.1.36 → 0.1.40

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/src/sequence.ts CHANGED
@@ -10,7 +10,6 @@ import clamp from 'lodash/clamp';
10
10
  import find from 'lodash/find';
11
11
  import map from 'lodash/map';
12
12
  import filter from 'lodash/filter';
13
- import forEach from 'lodash/forEach';
14
13
 
15
14
  import { ResolvedRequest } from './request-sequence-result';
16
15
  import { Asserter } from './asserter';
@@ -52,8 +51,6 @@ const {
52
51
  DEFAULT_REQUEST_DELAY,
53
52
  } = confDefaults;
54
53
  const {
55
- MAX_API_REQUEST_BODY_LENGTH,
56
- MAX_LOAD_REQUEST_BODY_LENGTH,
57
54
  MAX_RESPONSE_BYTES,
58
55
  MAX_RESPONSE_COLLECT,
59
56
  MIN_REQUEST_DELAY,
@@ -71,10 +68,16 @@ import {
71
68
  ALLOWED_RESPONSE_STATUSES,
72
69
  LoopConf,
73
70
  DEFAULT_REQUEST_TIMEOUT,
74
- CachePenetrationModes
71
+ CachePenetrationModes,
72
+ RequestPostData,
73
+ PostFormData
75
74
  } from '@loadmill/core/dist/request';
76
75
 
77
76
  import { testRunEventsEmitter } from './test-run-event-emitter';
77
+ import { WSRequest, WSRequestArguments, WSSequenceHandler } from './ws';
78
+ import { RequestFailuresError } from './errors';
79
+ import { getMaxRequestBodySize } from './utils';
80
+ import { WSExtractionData } from '@loadmill/core/dist/parameters/extractors/ws-extractor';
78
81
 
79
82
  export const reqIdParamName = 'loadmill-request-id';
80
83
 
@@ -127,6 +130,7 @@ class SequenceExecutor {
127
130
  resolvedRequests: ResolvedRequest[] = [];
128
131
  keepaliveHTTPSAgent;
129
132
  keepaliveHTTPAgent;
133
+ wsHandler: WSSequenceHandler;
130
134
 
131
135
  constructor(
132
136
  private httpAgent,
@@ -143,6 +147,7 @@ class SequenceExecutor {
143
147
  freeSocketTimeout: SOCKET_TIMEOUT / 2
144
148
  });
145
149
  this.postScriptRunner = new PostScriptRunner();
150
+ this.wsHandler = new WSSequenceHandler();
146
151
  }
147
152
 
148
153
  startAndPass(requests: LoadmillRequest[]) {
@@ -213,9 +218,11 @@ class SequenceExecutor {
213
218
  log.info('Unexpected error info:', unexpectedError);
214
219
  }
215
220
 
221
+ this.wsHandler.closeAllConnections();
216
222
  throw error;
217
223
  }
218
224
  }
225
+ this.wsHandler.closeAllConnections();
219
226
  }
220
227
 
221
228
  shouldStop(request: LoadmillRequest) {
@@ -309,12 +316,14 @@ class SequenceExecutor {
309
316
  let failedAssertionsHistogram, resTime;
310
317
  let loopIteration = 0;
311
318
  const maxIterations = getLoopIterations(requestConf.loop);
319
+ this.wsHandler.clearMessages();
312
320
 
313
321
  while (loopIteration < maxIterations) {
314
322
  loopIteration++;
315
323
  const request = this.prepareRequest(requestConf, index);
316
324
 
317
325
  const res = await this.sendRequest(request, index);
326
+
318
327
  // Setting now to avoid possible user overwrite:
319
328
  resTime = Number(this.parameters.__responseTime);
320
329
 
@@ -325,7 +334,9 @@ class SequenceExecutor {
325
334
  );
326
335
  }
327
336
 
328
- failedAssertionsHistogram = this.processSuccessfulResponse(
337
+ res.wsExtractionData = { messages: this.wsHandler.messages, timeLimit: timeout - resTime } as WSExtractionData;
338
+
339
+ failedAssertionsHistogram = await this.processSuccessfulResponse(
329
340
  index,
330
341
  requestConf,
331
342
  res
@@ -501,8 +512,18 @@ class SequenceExecutor {
501
512
  }
502
513
 
503
514
  prepareRequest(requestConf: LoadmillRequest, reqIndex: number) {
515
+ if (this.isWSRequest(requestConf) && !envUtils.isBrowser()) {
516
+ return this.prepareWSRequest(requestConf, reqIndex);
517
+ }
518
+ return this.prepareHttpRequest(requestConf, reqIndex);
519
+ }
520
+ private isWSRequest({ url }: LoadmillRequest) {
521
+ const resolvedUrl = this.resolve(url, (e) => setParameterErrorHistogram(e, 'Failed to compute URL - '));
522
+ return resolvedUrl.startsWith('ws://') || resolvedUrl.startsWith('wss://');
523
+ }
524
+ private prepareHttpRequest(requestConf: LoadmillRequest, reqIndex: number) {
504
525
  const urlObj = new URI(
505
- resolveUrl(requestConf.url, this.parameters, (err) =>
526
+ resolveUrl(requestConf.url, this.parameters, (err: Error) =>
506
527
  setParameterErrorHistogram(err, 'Failed to compute URL - ')
507
528
  )
508
529
  );
@@ -520,7 +541,7 @@ class SequenceExecutor {
520
541
 
521
542
  const url = urlObj.toString();
522
543
  this.resolvedRequests[reqIndex].url = url;
523
-
544
+
524
545
  this.validateDomain(uriUtils.getDomain(url));
525
546
 
526
547
  const method = requestConf.method.toLowerCase();
@@ -546,8 +567,11 @@ class SequenceExecutor {
546
567
  request.auth(auth.user || '', auth.password || '');
547
568
  }
548
569
 
549
- const postData = requestConf.postData;
550
- if (postData) {
570
+ const { postData, postFormData } = requestConf;
571
+ if (postFormData) {
572
+ this.preparePostFormData(postFormData, request, reqIndex);
573
+ }
574
+ else if (postData) {
551
575
  // It is CRUCIAL that the type is set before the body:
552
576
  const mimeType = postData.mimeType;
553
577
  if (mimeType && request._header['content-type'] == null) {
@@ -637,7 +661,7 @@ class SequenceExecutor {
637
661
  // This makes superagent populate res.text regardless of the response's content type:
638
662
  request.buffer();
639
663
 
640
- // Otherwise we expose superagent and its version -
664
+ // Otherwise we expose superagent and its version -
641
665
  // better have something more general (in case the user didnt set one):
642
666
  const uaHeader = request.get('User-Agent');
643
667
  if (!uaHeader || uaHeader.startsWith('node-superagent')) {
@@ -654,6 +678,94 @@ class SequenceExecutor {
654
678
  return request;
655
679
  }
656
680
 
681
+ private preparePostFormData(postFormData: PostFormData, request: any, reqIndex: number) {
682
+ const resolvedPostFormData: PostFormData = [];
683
+ let size = 0;
684
+ const MAX_BODY_SIZE = getMaxRequestBodySize();
685
+ for (const entry of postFormData) {
686
+ const { name, value, fileName } = entry;
687
+ const resolvedName = this.resolve(name, (err) => setParameterErrorHistogram(err, 'Failed to resolve form-data key - '));
688
+ size += resolvedName.length;
689
+ if (fileName) {
690
+ const resolvedFileName = this.resolve(fileName, (err) => setParameterErrorHistogram(err, 'Failed to resolve form-data fileName - '));
691
+ size += (resolvedFileName.length + value.length);
692
+ const buffer = Buffer.from(value, 'binary');
693
+ request.attach(resolvedName, buffer, resolvedFileName);
694
+ resolvedPostFormData.push({ name: resolvedName, value, fileName: resolvedFileName });
695
+ } else {
696
+ const resolvedValue = this.resolve(value, (err) => setParameterErrorHistogram(err, 'Failed to resolve form-data value - '));
697
+ size += resolvedValue.length;
698
+ request.field(resolvedName, resolvedValue);
699
+ resolvedPostFormData.push({ name: resolvedName, value: resolvedValue });
700
+ }
701
+ if (size > MAX_BODY_SIZE) {
702
+ throw new RequestFailuresError('Form Data size is too large');
703
+ }
704
+ }
705
+
706
+ this.resolvedRequests[reqIndex].postFormData = resolvedPostFormData;
707
+ }
708
+
709
+ prepareWSRequest(requestConf: LoadmillRequest, reqIndex: number) {
710
+ const wsRequestArgs = this.resolveAndSetWSReqData(requestConf, reqIndex);
711
+
712
+ return new WSRequest(
713
+ wsRequestArgs,
714
+ this.wsHandler,
715
+ (e: Error) => this.setSingleFailure(reqIndex, 'Websocket error: ' + e.message),
716
+ );
717
+ }
718
+
719
+ resolveAndSetWSReqData = (
720
+ { url, headers, postData, timeout = DEFAULT_REQUEST_TIMEOUT, expectedStatus = 'SUCCESS' }: LoadmillRequest,
721
+ reqIndex: number
722
+ ): WSRequestArguments => {
723
+ const preparedUrl = this.prepareWsUrl(url, reqIndex);
724
+ const preparedHeaders = this.prepareWsHeaders(headers, reqIndex);
725
+ const preparedMessage = this.prepareWsMessage(postData, reqIndex);
726
+ return {
727
+ expectedStatus,
728
+ headers: preparedHeaders,
729
+ message: preparedMessage,
730
+ timeout,
731
+ url: preparedUrl,
732
+ };
733
+ };
734
+
735
+ prepareWsUrl = (url: string, reqIndex: number) => {
736
+ const resolvedUrl = this.resolve(url, (e: Error) => setParameterErrorHistogram(e, 'Failed to compute URL - '));
737
+ this.resolvedRequests[reqIndex].url = resolvedUrl;
738
+ return resolvedUrl;
739
+ };
740
+
741
+ prepareWsHeaders = (headers: LoadmillHeaders[] | undefined, reqIndex: number) => {
742
+ const resolvedHeadersObj: LoadmillHeaders = {};
743
+ const resolvedHeaders: LoadmillHeaders[] = [];
744
+ if (headers && !isEmpty(headers)) {
745
+ resolvedHeaders.push(...this.resolveHeaders(headers));
746
+ resolvedHeaders.forEach(({ name, value }) => resolvedHeadersObj[name] = value);
747
+ }
748
+ this.resolvedRequests[reqIndex].headers = resolvedHeaders;
749
+ return resolvedHeadersObj;
750
+ };
751
+
752
+ prepareWsMessage = (postData: RequestPostData | undefined, reqIndex: number) => {
753
+ let resolvedMessage: string = '';
754
+ if (postData) {
755
+ resolvedMessage = this.resolve(postData.text, (err: Error) =>
756
+ setParameterErrorHistogram(err, 'Failed to compute Websocket message - ')
757
+ );
758
+ if (resolvedMessage && resolvedMessage.length > getMaxRequestBodySize()) {
759
+ throw new RequestFailuresError('Websocket message size is too large');
760
+ }
761
+ this.resolvedRequests[reqIndex].postData = {
762
+ mimeType: postData.mimeType,
763
+ text: resolvedMessage,
764
+ };
765
+ }
766
+ return resolvedMessage;
767
+ };
768
+
657
769
  checkProgressEvent = (
658
770
  requestIndex: number,
659
771
  request,
@@ -668,7 +780,7 @@ class SequenceExecutor {
668
780
  };
669
781
 
670
782
  private validateDomain(domain: string) {
671
- if (isEmpty(domain)){
783
+ if (isEmpty(domain)) {
672
784
  const message = 'HTTP request domain name is empty';
673
785
  throw new RequestFailuresError(message, { [message]: 1 });
674
786
  }
@@ -715,9 +827,9 @@ class SequenceExecutor {
715
827
  )
716
828
  );
717
829
 
718
- processSuccessfulResponse(reqIndex, requestConf: LoadmillRequest, res) {
830
+ async processSuccessfulResponse(reqIndex, requestConf: LoadmillRequest, res) {
719
831
  // modifies parameters:
720
- this.handleExtractions(requestConf, res);
832
+ await this.handleExtractions(requestConf, res);
721
833
 
722
834
  if (!envUtils.isBrowser()) {
723
835
  this.handlePostScript(requestConf, res);
@@ -745,33 +857,33 @@ class SequenceExecutor {
745
857
  );
746
858
  }
747
859
 
748
- handleExtractions(requestConf: LoadmillRequest, res) {
749
- requestConf.extract?.forEach((extractions: Extractions) =>
750
- this.extractInScope(res, extractions)
751
- );
860
+ async handleExtractions(requestConf: LoadmillRequest, res) {
861
+ for (const extractions of (requestConf.extract || [])) {
862
+ await this.extractInScope(res, extractions);
863
+ }
752
864
  }
753
865
 
754
- extractInScope(res, extractions: Extractions) {
866
+ async extractInScope(res, extractions: Extractions) {
755
867
  const contextParameters = Object.assign({}, this.parameters);
756
- const extractionCombiner = new ExtractionCombiner(contextParameters, res);
868
+ const extractionCombiner = new ExtractionCombiner(contextParameters, res, res.wsExtractionData);
757
869
 
758
- forEach(extractions, (extraction: Extraction, name: string) =>
759
- this.extract(name, extraction, extractionCombiner)
760
- );
870
+ for (const [name, extraction] of Object.entries(extractions)) {
871
+ await this.extract(name, extraction, extractionCombiner);
872
+ }
761
873
  }
762
874
 
763
- extract(
875
+ async extract(
764
876
  parameterName: string,
765
877
  extraction: Extraction,
766
878
  extractionCombiner: ExtractionCombiner
767
879
  ) {
768
880
  log.trace('Parameter extraction start: ', { parameterName, extraction });
769
881
 
770
- const combinedExtractor = extractionCombiner.combine(extraction);
882
+ const combinedExtractor = await extractionCombiner.combine(extraction);
771
883
  let result;
772
884
 
773
885
  try {
774
- result = combinedExtractor();
886
+ result = await combinedExtractor();
775
887
  } catch (error) {
776
888
  const genericMessage = `Failed to extract value for parameter "${parameterName}"`;
777
889
  log.debug(genericMessage, error);
@@ -893,9 +1005,6 @@ function isSimpleRequest(headers) {
893
1005
  );
894
1006
  }
895
1007
 
896
- const getMaxRequestBodySize = () =>
897
- envUtils.isBrowser() ? MAX_LOAD_REQUEST_BODY_LENGTH : MAX_API_REQUEST_BODY_LENGTH;
898
-
899
1008
  const getLoopIterations = (LoopConf?: LoopConf) => {
900
1009
  const declared = (LoopConf && LoopConf.iterations) || 1;
901
1010
  return Math.min(MAX_REQUEST_LOOPS_ITERATIONS, declared);
@@ -913,7 +1022,7 @@ const extendResponseHeaders = (headers, redirectHeaders) => {
913
1022
 
914
1023
  const isExpectedStatus = ({ expectedStatus, url }, status: number) => {
915
1024
  if (expectedStatus === ALLOWED_RESPONSE_STATUSES.SUCCESS) {
916
- return 200 <= status && status < 400;
1025
+ return (200 <= status && status < 400) || status === 101;
917
1026
  }
918
1027
  else if (expectedStatus === ALLOWED_RESPONSE_STATUSES.ERROR) {
919
1028
  log.debug('user asked to fail this request', url);
@@ -949,12 +1058,3 @@ const setTCPReuse = (request, agent, sslAgent) => {
949
1058
  };
950
1059
 
951
1060
  };
952
-
953
- class RequestFailuresError extends Error {
954
- constructor(message: string, public histogram: Histogram = { [message]: 1 }) {
955
- super(message);
956
-
957
- // Workaround suggested in: https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
958
- Object.setPrototypeOf(this, RequestFailuresError.prototype);
959
- }
960
- }
package/src/utils.ts ADDED
@@ -0,0 +1,8 @@
1
+ import {
2
+ MAX_API_REQUEST_BODY_LENGTH,
3
+ MAX_LOAD_REQUEST_BODY_LENGTH
4
+ } from '@loadmill/core/dist/conf/extrema';
5
+ import * as envUtils from '@loadmill/universal/dist/env-utils';
6
+
7
+ export const getMaxRequestBodySize = () =>
8
+ envUtils.isBrowser() ? MAX_LOAD_REQUEST_BODY_LENGTH : MAX_API_REQUEST_BODY_LENGTH;
package/src/ws.ts ADDED
@@ -0,0 +1,273 @@
1
+ import WebSocket from 'ws'; // todo should be picked with other option (socketio or non in browser)
2
+ import { delay } from '@loadmill/universal/dist/promise-utils';
3
+ import log from '@loadmill/universal/dist/log';
4
+ import { HttpResponseStatus, LoadmillHeaders } from '@loadmill/core/dist/request';
5
+ import { RequestFailuresError } from './errors';
6
+ import { ClientRequest, IncomingMessage, IncomingHttpHeaders } from 'http';
7
+ import { Socket } from 'net';
8
+
9
+ declare class WS extends WebSocket {
10
+ _req: WSClientRequest | null;
11
+ }
12
+ declare class WSClientRequest extends ClientRequest {
13
+ socket: Socket & { parser: ParserIncomingMessage };
14
+ }
15
+ declare class ParserIncomingMessage {
16
+ incoming: IncomingMessage;
17
+ }
18
+
19
+
20
+ enum WSState {
21
+ CONNECTING,
22
+ OPEN,
23
+ CLOSING,
24
+ CLOSED,
25
+ }
26
+
27
+ const SWITCHING_PROTOCOLS = 'Switching Protocols';
28
+ const WS_CONNECTION_TIMEOUT_MS = 10000;
29
+
30
+ export class WSRequest {
31
+ private hasErrorOccured?: boolean; // need this otherwise we have race condition between verifyConnectedAndOpen and onError
32
+ private ws: WebSocket;
33
+ public url: string; // need this for isExpectedStatus function
34
+ public expectedStatus: HttpResponseStatus; // need this for isExpectedStatus function
35
+
36
+ constructor(
37
+ private readonly wsRequestArgs: WSRequestArguments,
38
+ private readonly wsHandler: WSSequenceHandler,
39
+ private readonly onError: (e: Error) => void,
40
+ ) {
41
+ this.url = wsRequestArgs.url;
42
+ this.expectedStatus = wsRequestArgs.expectedStatus;
43
+ }
44
+
45
+ /**
46
+ * This function is executed when we are ready to send the ws request
47
+ * @param cb This callback is being executed after we successfully connected to ws and sent a ws message if there was any
48
+ */
49
+ async ok(cb: (response: WSResponse) => boolean) {
50
+ const response: WSResponse = {
51
+ status: undefined,
52
+ res: {
53
+ statusMessage: undefined,
54
+ },
55
+ header: {},
56
+ req: {},
57
+ };
58
+
59
+ const existingWS = this.wsHandler.getConnection(this.wsRequestArgs.url);
60
+
61
+ existingWS && log.debug(`Existing Connection state ${getConnectionState(existingWS)}`);
62
+ if (!existingWS || (existingWS && existingWS.readyState !== WSState.OPEN)) {
63
+ await this.addConnection(response);
64
+ } else {
65
+ this.ws = existingWS;
66
+ log.debug('Reusing existing ws connection', this.ws.url);
67
+ }
68
+
69
+ try {
70
+ this.sendMessage();
71
+ } catch (e) {
72
+ log.error('Failed to send a ws message', e);
73
+ }
74
+ cb(response);
75
+ return { };
76
+ }
77
+
78
+ private async addConnection(response: WSResponse) {
79
+ this.ws = new WebSocket(this.wsRequestArgs.url, undefined, { headers: this.wsRequestArgs.headers });
80
+ this.addEventListeners(response);
81
+ !this.hasErrorOccured && await this.verifyOpen();
82
+ this.wsHandler.storeConnection(this.wsRequestArgs.url, this.ws);
83
+ // todo add some debug details for the user to know we created a new connection in this request
84
+ }
85
+
86
+ private async addEventListeners(response: WSResponse) {
87
+ this.addOnErrorListener(response);
88
+ this.addOnUpgradeListener(response);
89
+ this.addOnOpenListener();
90
+ this.addOnMessageListener();
91
+ }
92
+
93
+ private addOnErrorListener(response: WSResponse) {
94
+ this.ws.addEventListener('error', event => {
95
+ log.debug('inside addEventListener error');
96
+ try {
97
+ const target = event.target as WS;
98
+ this.updateResponseOnError(target, response);
99
+ } catch (e) {
100
+ log.error('Got an error inside addEventListener error', e);
101
+ }
102
+ });
103
+
104
+ this.ws.on('error', (e) => {
105
+ try {
106
+ log.debug('inside on error', e);
107
+ this.onError(e);
108
+ this.hasErrorOccured = true;
109
+ } catch (e) {
110
+ log.warn('Failed to handle a ws error', e);
111
+ }
112
+ try {
113
+ log.debug('closing ws');
114
+ this.ws.close();
115
+ } catch (e) {
116
+ log.warn('Failed to close a ws connection', e);
117
+ }
118
+ });
119
+ }
120
+
121
+ private updateResponseOnError(ws: WS, response: WSResponse) {
122
+ if (ws._req) {
123
+ const { statusCode, statusMessage, headers } = ws._req.socket.parser.incoming;
124
+ log.debug('Got incoming incoming request', { statusCode, statusMessage, headers });
125
+ response.status = statusCode;
126
+ response.res.statusMessage = statusMessage;
127
+ response.header = headers;
128
+ }
129
+ }
130
+
131
+ private addOnUpgradeListener(response: WSResponse) {
132
+ this.ws.on('upgrade', (event) => {
133
+ try {
134
+ const { statusCode, statusMessage, headers } = event;
135
+ log.debug('inside upgrade event', { statusCode, statusMessage, headers });
136
+ response.status = statusCode || 101;
137
+ response.res.statusMessage = statusMessage || SWITCHING_PROTOCOLS;
138
+ response.header = headers as LoadmillHeaders;
139
+ } catch (e) {
140
+ log.debug('upgrade event err', e);
141
+ }
142
+ });
143
+ }
144
+
145
+ private addOnOpenListener() {
146
+ this.ws.on('open', () => {
147
+ log.debug('inside on open, currently doing nothing here.');
148
+ });
149
+ }
150
+
151
+ private addOnMessageListener() {
152
+ this.ws.on('message', (message: string) => {
153
+ try {
154
+ log.debug('got incoming message', message);
155
+ this.wsHandler.addMessage(message);
156
+ } catch (e) {
157
+ log.debug('error getting message', e);
158
+ }
159
+ });
160
+ }
161
+
162
+ async verifyOpen() {
163
+ const readyState = await this.waitForConnectedState(this.wsRequestArgs.timeout);
164
+ const connectionDebugMsg = `Connection ${WSState[readyState]}`;
165
+ log.debug(connectionDebugMsg);
166
+ if (readyState !== WebSocket.OPEN && !this.hasErrorOccured) {
167
+ let msg = 'Could not connect to WebSocket address: ';
168
+ log.debug(msg, { connectionState: WSState[readyState], url: this.ws.url });
169
+ if (readyState === WSState.CONNECTING) {
170
+ msg += 'request timeout';
171
+ } else { // CLOSING or CLOSED state
172
+ msg += connectionDebugMsg;
173
+ }
174
+ throw new RequestFailuresError(msg);
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Waiting for WS connection to go out of CONNECTING state
180
+ * @param socket The ws connection object
181
+ * @param timeout The timeout in milliseconds for the waiting procedure
182
+ * @returns WSState
183
+ */
184
+ waitForConnectedState = async (timeout: number = WS_CONNECTION_TIMEOUT_MS): Promise<WSState> => {
185
+ const WS_CONNECTION_INTERVAL_MS = 100;
186
+ if (this.ws.readyState != null && this.ws.readyState !== WebSocket.CONNECTING) {
187
+ return this.ws.readyState;
188
+ }
189
+ else {
190
+ const maxIterations = timeout / WS_CONNECTION_INTERVAL_MS;
191
+ let i = 0;
192
+ while (this.ws.readyState === WebSocket.CONNECTING && i < maxIterations) {
193
+ await delay(WS_CONNECTION_INTERVAL_MS);
194
+ i++;
195
+ }
196
+ return this.ws.readyState;
197
+ }
198
+ };
199
+
200
+ private sendMessage() {
201
+ log.debug('about to send message', { readyState: WSState[this.ws.readyState] } );
202
+ if (this.ws.readyState === WSState.OPEN && this.wsRequestArgs.message) {
203
+ log.debug('sending message', this.wsRequestArgs.message);
204
+ this.ws.send(this.wsRequestArgs.message);
205
+ }
206
+ }
207
+
208
+ // need this because the SequenceExecutor expectes a request obj with on func prop
209
+ on(_eventName: string, _cb: Function) {
210
+ //do nothing
211
+ }
212
+ }
213
+
214
+ export class WSSequenceHandler {
215
+ private _connections: { [url: string]: WebSocket };
216
+ messages: string[];
217
+ constructor() {
218
+ this._connections = {};
219
+ this.clearMessages();
220
+ }
221
+
222
+ getConnection(url: string): WebSocket | undefined {
223
+ return this._connections[url];
224
+ }
225
+
226
+ storeConnection(url: string, wsConnection: WebSocket) {
227
+ this._connections[url] = wsConnection;
228
+ }
229
+
230
+ async closeAllConnections() {
231
+ try {
232
+ log.debug('Closing all ws connections');
233
+ for (const connection of Object.values(this._connections)) {
234
+ connection.close();
235
+ }
236
+ } catch (e) {
237
+ log.warn('Failed to close all connections', e, { connections: this._connections });
238
+ }
239
+ }
240
+
241
+ getMessages() {
242
+ return this.messages;
243
+ }
244
+
245
+ addMessage(message: string) {
246
+ this.messages.push(message);
247
+ }
248
+
249
+ clearMessages() {
250
+ this.messages = [];
251
+ }
252
+ }
253
+
254
+ function getConnectionState(existingWS?: WebSocket) {
255
+ return existingWS ? WSState[existingWS.readyState] : null;
256
+ }
257
+
258
+ export type WSRequestArguments = {
259
+ expectedStatus: HttpResponseStatus;
260
+ headers: LoadmillHeaders
261
+ message: string;
262
+ timeout: number;
263
+ url: string;
264
+ };
265
+
266
+ export type WSResponse = {
267
+ status?: number;
268
+ res: {
269
+ statusMessage?: string;
270
+ };
271
+ header: LoadmillHeaders | IncomingHttpHeaders;
272
+ req: {};
273
+ }