@loadmill/executer 0.1.34 → 0.1.38

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,9 +68,16 @@ import {
71
68
  ALLOWED_RESPONSE_STATUSES,
72
69
  LoopConf,
73
70
  DEFAULT_REQUEST_TIMEOUT,
74
- CachePenetrationModes
71
+ CachePenetrationModes,
72
+ RequestPostData
75
73
  } from '@loadmill/core/dist/request';
76
74
 
75
+ import { testRunEventsEmitter } from './test-run-event-emitter';
76
+ import { WSRequest, WSRequestArguments, WSSequenceHandler } from './ws';
77
+ import { RequestFailuresError } from './errors';
78
+ import { getMaxRequestBodySize } from './utils';
79
+ import { WSExtractionData } from '@loadmill/core/dist/parameters/extractors/ws-extractor';
80
+
77
81
  export const reqIdParamName = 'loadmill-request-id';
78
82
 
79
83
  declare const Promise: typeof BluebirdPromise; // This is a hack for the loadmill-runner. I hate TypeScript.
@@ -102,12 +106,12 @@ export const sequence = {
102
106
  httpAgent,
103
107
  requests: LoadmillRequest[],
104
108
  parameters: Parameters[],
105
- domainsWhiteList: string[]
109
+ domainsWhiteList: string[],
106
110
  ) {
107
111
  const sequenceExecutor = new SequenceExecutor(
108
112
  httpAgent,
109
113
  parameters,
110
- domainsWhiteList
114
+ domainsWhiteList,
111
115
  );
112
116
  return sequenceExecutor.startAndPass(requests);
113
117
  },
@@ -115,6 +119,7 @@ export const sequence = {
115
119
 
116
120
  class SequenceExecutor {
117
121
  parameters: SequenceExecutorParameters;
122
+ postScriptRunner: PostScriptRunner;
118
123
 
119
124
  avgResTime = 0;
120
125
  successfulHits = 0;
@@ -124,6 +129,7 @@ class SequenceExecutor {
124
129
  resolvedRequests: ResolvedRequest[] = [];
125
130
  keepaliveHTTPSAgent;
126
131
  keepaliveHTTPAgent;
132
+ wsHandler: WSSequenceHandler;
127
133
 
128
134
  constructor(
129
135
  private httpAgent,
@@ -139,6 +145,8 @@ class SequenceExecutor {
139
145
  timeout: SOCKET_TIMEOUT,
140
146
  freeSocketTimeout: SOCKET_TIMEOUT / 2
141
147
  });
148
+ this.postScriptRunner = new PostScriptRunner();
149
+ this.wsHandler = new WSSequenceHandler();
142
150
  }
143
151
 
144
152
  startAndPass(requests: LoadmillRequest[]) {
@@ -209,9 +217,11 @@ class SequenceExecutor {
209
217
  log.info('Unexpected error info:', unexpectedError);
210
218
  }
211
219
 
220
+ this.wsHandler.closeAllConnections();
212
221
  throw error;
213
222
  }
214
223
  }
224
+ this.wsHandler.closeAllConnections();
215
225
  }
216
226
 
217
227
  shouldStop(request: LoadmillRequest) {
@@ -245,7 +255,11 @@ class SequenceExecutor {
245
255
  reqIndex: number,
246
256
  requests: LoadmillRequest[]
247
257
  ) {
248
- const { skipBefore } = request;
258
+ const { skipBefore, disabled } = request;
259
+
260
+ if (disabled) {
261
+ return reqIndex + 1;
262
+ }
249
263
 
250
264
  if (skipBefore) {
251
265
  const { goTo, negate, condition } = skipBefore;
@@ -301,23 +315,27 @@ class SequenceExecutor {
301
315
  let failedAssertionsHistogram, resTime;
302
316
  let loopIteration = 0;
303
317
  const maxIterations = getLoopIterations(requestConf.loop);
318
+ this.wsHandler.clearMessages();
304
319
 
305
320
  while (loopIteration < maxIterations) {
306
321
  loopIteration++;
307
322
  const request = this.prepareRequest(requestConf, index);
308
323
 
309
324
  const res = await this.sendRequest(request, index);
325
+
310
326
  // Setting now to avoid possible user overwrite:
311
327
  resTime = Number(this.parameters.__responseTime);
312
328
 
313
- const { timeout } = requestConf;
329
+ const { timeout = DEFAULT_REQUEST_TIMEOUT } = requestConf;
314
330
  if (resTime > timeout) {
315
331
  throw new RequestFailuresError(
316
332
  `Response received after timeout of ${timeout}ms exceeded`
317
333
  );
318
334
  }
319
335
 
320
- failedAssertionsHistogram = this.processSuccessfulResponse(
336
+ res.wsExtractionData = { messages: this.wsHandler.messages, timeLimit: timeout - resTime } as WSExtractionData;
337
+
338
+ failedAssertionsHistogram = await this.processSuccessfulResponse(
321
339
  index,
322
340
  requestConf,
323
341
  res
@@ -493,8 +511,18 @@ class SequenceExecutor {
493
511
  }
494
512
 
495
513
  prepareRequest(requestConf: LoadmillRequest, reqIndex: number) {
514
+ if (this.isWSRequest(requestConf) && !envUtils.isBrowser()) {
515
+ return this.prepareWSRequest(requestConf, reqIndex);
516
+ }
517
+ return this.prepareHttpRequest(requestConf, reqIndex);
518
+ }
519
+ private isWSRequest({ url }: LoadmillRequest) {
520
+ const resolvedUrl = this.resolve(url, (e) => setParameterErrorHistogram(e, 'Failed to compute URL - '));
521
+ return resolvedUrl.startsWith('ws://') || resolvedUrl.startsWith('wss://');
522
+ }
523
+ private prepareHttpRequest(requestConf: LoadmillRequest, reqIndex: number) {
496
524
  const urlObj = new URI(
497
- resolveUrl(requestConf.url, this.parameters, (err) =>
525
+ resolveUrl(requestConf.url, this.parameters, (err: Error) =>
498
526
  setParameterErrorHistogram(err, 'Failed to compute URL - ')
499
527
  )
500
528
  );
@@ -512,7 +540,7 @@ class SequenceExecutor {
512
540
 
513
541
  const url = urlObj.toString();
514
542
  this.resolvedRequests[reqIndex].url = url;
515
-
543
+
516
544
  this.validateDomain(uriUtils.getDomain(url));
517
545
 
518
546
  const method = requestConf.method.toLowerCase();
@@ -629,7 +657,7 @@ class SequenceExecutor {
629
657
  // This makes superagent populate res.text regardless of the response's content type:
630
658
  request.buffer();
631
659
 
632
- // Otherwise we expose superagent and its version -
660
+ // Otherwise we expose superagent and its version -
633
661
  // better have something more general (in case the user didnt set one):
634
662
  const uaHeader = request.get('User-Agent');
635
663
  if (!uaHeader || uaHeader.startsWith('node-superagent')) {
@@ -641,11 +669,71 @@ class SequenceExecutor {
641
669
  request.header
642
670
  );
643
671
 
644
- request.expectedStatus = requestConf.expectedStatus;
672
+ request.expectedStatus = requestConf.expectedStatus || ALLOWED_RESPONSE_STATUSES.SUCCESS;
645
673
 
646
674
  return request;
647
675
  }
648
676
 
677
+ prepareWSRequest(requestConf: LoadmillRequest, reqIndex: number) {
678
+ const wsRequestArgs = this.resolveAndSetWSReqData(requestConf, reqIndex);
679
+
680
+ return new WSRequest(
681
+ wsRequestArgs,
682
+ this.wsHandler,
683
+ (e: Error) => this.setSingleFailure(reqIndex, 'Websocket error ' + e.message),
684
+ );
685
+ }
686
+
687
+ resolveAndSetWSReqData = (
688
+ { url, headers, postData, timeout = DEFAULT_REQUEST_TIMEOUT, expectedStatus = 'SUCCESS' }: LoadmillRequest,
689
+ reqIndex: number
690
+ ): WSRequestArguments => {
691
+ const preparedUrl = this.prepareWsUrl(url, reqIndex);
692
+ const preparedHeaders = this.prepareWsHeaders(headers, reqIndex);
693
+ const preparedMessage = this.prepareWsMessage(postData, reqIndex);
694
+ return {
695
+ expectedStatus,
696
+ headers: preparedHeaders,
697
+ message: preparedMessage,
698
+ timeout,
699
+ url: preparedUrl,
700
+ };
701
+ };
702
+
703
+ prepareWsUrl = (url: string, reqIndex: number) => {
704
+ const resolvedUrl = this.resolve(url, (e: Error) => setParameterErrorHistogram(e, 'Failed to compute URL - '));
705
+ this.resolvedRequests[reqIndex].url = resolvedUrl;
706
+ return resolvedUrl;
707
+ };
708
+
709
+ prepareWsHeaders = (headers: LoadmillHeaders[] | undefined, reqIndex: number) => {
710
+ const resolvedHeadersObj: LoadmillHeaders = {};
711
+ const resolvedHeaders: LoadmillHeaders[] = [];
712
+ if (headers && !isEmpty(headers)) {
713
+ resolvedHeaders.push(...this.resolveHeaders(headers));
714
+ resolvedHeaders.forEach(({ name, value }) => resolvedHeadersObj[name] = value);
715
+ }
716
+ this.resolvedRequests[reqIndex].headers = resolvedHeaders;
717
+ return resolvedHeadersObj;
718
+ };
719
+
720
+ prepareWsMessage = (postData: RequestPostData | undefined, reqIndex: number) => {
721
+ let resolvedMessage: string = '';
722
+ if (postData) {
723
+ resolvedMessage = this.resolve(postData.text, (err: Error) =>
724
+ setParameterErrorHistogram(err, 'Failed to compute Websocket message - ')
725
+ );
726
+ if (resolvedMessage && resolvedMessage.length > getMaxRequestBodySize()) {
727
+ throw new RequestFailuresError('Websocket message size is too large');
728
+ }
729
+ this.resolvedRequests[reqIndex].postData = {
730
+ mimeType: postData.mimeType,
731
+ text: resolvedMessage,
732
+ };
733
+ }
734
+ return resolvedMessage;
735
+ };
736
+
649
737
  checkProgressEvent = (
650
738
  requestIndex: number,
651
739
  request,
@@ -660,7 +748,7 @@ class SequenceExecutor {
660
748
  };
661
749
 
662
750
  private validateDomain(domain: string) {
663
- if (isEmpty(domain)){
751
+ if (isEmpty(domain)) {
664
752
  const message = 'HTTP request domain name is empty';
665
753
  throw new RequestFailuresError(message, { [message]: 1 });
666
754
  }
@@ -707,9 +795,9 @@ class SequenceExecutor {
707
795
  )
708
796
  );
709
797
 
710
- processSuccessfulResponse(reqIndex, requestConf: LoadmillRequest, res) {
798
+ async processSuccessfulResponse(reqIndex, requestConf: LoadmillRequest, res) {
711
799
  // modifies parameters:
712
- this.handleExtractions(requestConf, res);
800
+ await this.handleExtractions(requestConf, res);
713
801
 
714
802
  if (!envUtils.isBrowser()) {
715
803
  this.handlePostScript(requestConf, res);
@@ -737,33 +825,33 @@ class SequenceExecutor {
737
825
  );
738
826
  }
739
827
 
740
- handleExtractions(requestConf: LoadmillRequest, res) {
741
- requestConf.extract?.forEach((extractions: Extractions) =>
742
- this.extractInScope(res, extractions)
743
- );
828
+ async handleExtractions(requestConf: LoadmillRequest, res) {
829
+ for (const extractions of (requestConf.extract || [])) {
830
+ await this.extractInScope(res, extractions);
831
+ }
744
832
  }
745
833
 
746
- extractInScope(res, extractions: Extractions) {
834
+ async extractInScope(res, extractions: Extractions) {
747
835
  const contextParameters = Object.assign({}, this.parameters);
748
- const extractionCombiner = new ExtractionCombiner(contextParameters, res);
836
+ const extractionCombiner = new ExtractionCombiner(contextParameters, res, res.wsExtractionData);
749
837
 
750
- forEach(extractions, (extraction: Extraction, name: string) =>
751
- this.extract(name, extraction, extractionCombiner)
752
- );
838
+ for (const [name, extraction] of Object.entries(extractions)) {
839
+ await this.extract(name, extraction, extractionCombiner);
840
+ }
753
841
  }
754
842
 
755
- extract(
843
+ async extract(
756
844
  parameterName: string,
757
845
  extraction: Extraction,
758
846
  extractionCombiner: ExtractionCombiner
759
847
  ) {
760
848
  log.trace('Parameter extraction start: ', { parameterName, extraction });
761
849
 
762
- const combinedExtractor = extractionCombiner.combine(extraction);
850
+ const combinedExtractor = await extractionCombiner.combine(extraction);
763
851
  let result;
764
852
 
765
853
  try {
766
- result = combinedExtractor();
854
+ result = await combinedExtractor();
767
855
  } catch (error) {
768
856
  const genericMessage = `Failed to extract value for parameter "${parameterName}"`;
769
857
  log.debug(genericMessage, error);
@@ -797,27 +885,34 @@ class SequenceExecutor {
797
885
  }
798
886
 
799
887
  handlePostScript({ postScript }: LoadmillRequest, { text = '' }) {
800
- try {
801
- if (postScript) {
802
- let $ = {};
803
- try {
804
- $ = JSON.parse(text);
805
- } catch (e) {
806
- log.debug('res text (body) cannot be JSON parsed', e.name, e.message);
807
- }
888
+ if (postScript) {
889
+ testRunEventsEmitter.postScript.started();
808
890
 
809
- const staticContext = { $, __: parameterFunctionOperations };
810
-
811
- Object.assign(this.parameters, new PostScriptRunner().run(
812
- { staticContext },
813
- this.parameters,
814
- postScript
815
- ));
891
+ try {
892
+ const result = this.runPostScript(postScript, text);
893
+ Object.assign(this.parameters, result);
894
+ } catch (e) {
895
+ const message = ['Post Script', e.name + ':', e.message].join(' ');
896
+ throw new RequestFailuresError(message);
816
897
  }
898
+
899
+ testRunEventsEmitter.postScript.finished();
900
+ }
901
+ }
902
+
903
+ private runPostScript(postScript: string, text: string) {
904
+ const staticContext = this.getStaticContext(text);
905
+ return this.postScriptRunner.run({ staticContext }, this.parameters, postScript);
906
+ }
907
+
908
+ private getStaticContext(text: string) {
909
+ let $ = {};
910
+ try {
911
+ $ = JSON.parse(text);
817
912
  } catch (e) {
818
- const message = ['Post Script', e.name + ':', e.message].join(' ');
819
- throw new RequestFailuresError(message);
913
+ log.debug('res text (body) cannot be JSON parsed', e.name, e.message);
820
914
  }
915
+ return { $, __: parameterFunctionOperations };
821
916
  }
822
917
 
823
918
  handleAssertions(requestConf: LoadmillRequest) {
@@ -878,9 +973,6 @@ function isSimpleRequest(headers) {
878
973
  );
879
974
  }
880
975
 
881
- const getMaxRequestBodySize = () =>
882
- envUtils.isBrowser() ? MAX_LOAD_REQUEST_BODY_LENGTH : MAX_API_REQUEST_BODY_LENGTH;
883
-
884
976
  const getLoopIterations = (LoopConf?: LoopConf) => {
885
977
  const declared = (LoopConf && LoopConf.iterations) || 1;
886
978
  return Math.min(MAX_REQUEST_LOOPS_ITERATIONS, declared);
@@ -898,7 +990,7 @@ const extendResponseHeaders = (headers, redirectHeaders) => {
898
990
 
899
991
  const isExpectedStatus = ({ expectedStatus, url }, status: number) => {
900
992
  if (expectedStatus === ALLOWED_RESPONSE_STATUSES.SUCCESS) {
901
- return 200 <= status && status < 400;
993
+ return (200 <= status && status < 400) || status === 101;
902
994
  }
903
995
  else if (expectedStatus === ALLOWED_RESPONSE_STATUSES.ERROR) {
904
996
  log.debug('user asked to fail this request', url);
@@ -934,12 +1026,3 @@ const setTCPReuse = (request, agent, sslAgent) => {
934
1026
  };
935
1027
 
936
1028
  };
937
-
938
- class RequestFailuresError extends Error {
939
- constructor(message: string, public histogram: Histogram = { [message]: 1 }) {
940
- super(message);
941
-
942
- // 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
943
- Object.setPrototypeOf(this, RequestFailuresError.prototype);
944
- }
945
- }
@@ -11,14 +11,14 @@ import {
11
11
  } from './request-sequence-result';
12
12
 
13
13
  export async function runSingleIterationAndMergeResolved(
14
- conf: ExecutableConf
14
+ conf: ExecutableConf,
15
15
  ): Promise<ExtendedSequenceResult> {
16
16
  const result = await runSingleIteration(conf);
17
17
  return extendSequenceResult(result, conf.requests);
18
18
  }
19
19
 
20
20
  export async function runSingleIteration(
21
- conf: ExecutableConf
21
+ conf: ExecutableConf,
22
22
  ): Promise<RequestSequenceResult> {
23
23
  let httpAgent;
24
24
  if (conf.useCookies && !envUtils.isBrowser()) {
@@ -35,7 +35,7 @@ export async function runSingleIteration(
35
35
  httpAgent,
36
36
  extendRequests(conf),
37
37
  conf.parameters,
38
- conf.domainsWhiteList
38
+ conf.domainsWhiteList,
39
39
  );
40
40
  }
41
41
 
@@ -0,0 +1,24 @@
1
+ import log from '@loadmill/universal/dist/log';
2
+
3
+ class TestRunEventsEmitter {
4
+ postScript: EventsEmitter;
5
+ constructor() {
6
+ const warningMsg = ' event emitter callback not assigned';
7
+ this.postScript = {
8
+ started: () => log.warn('postScript' + warningMsg),
9
+ finished: () => log.warn('postScript' + warningMsg),
10
+ };
11
+ }
12
+
13
+ setPostScriptHandlers = (socket: SocketIOClient.Socket, token: string) => {
14
+ this.postScript.started = () => socket.emit(`postscript-started:${token}`);
15
+ this.postScript.finished = () => socket.emit(`postscript-finished:${token}`);
16
+ }
17
+ }
18
+
19
+ const testRunEventsEmitter = new TestRunEventsEmitter();
20
+ export { testRunEventsEmitter };
21
+
22
+ type EventsEmitter = {
23
+ [funcName: string]: () => void | SocketIOClient.Socket;
24
+ }
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,233 @@
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
+
7
+ enum WSState {
8
+ CONNECTING,
9
+ OPEN,
10
+ CLOSING,
11
+ CLOSED,
12
+ }
13
+
14
+ const SWITCHING_PROTOCOLS = 'Switching Protocols';
15
+ const WS_CONNECTION_TIMEOUT_MS = 10000;
16
+
17
+ export class WSRequest {
18
+ private hasErrorOccured?: boolean; // need this otherwise we have race condition between verifyConnectedAndOpen and onError
19
+ private ws: WebSocket;
20
+ public url: string; // need this for isExpectedStatus function
21
+ public expectedStatus: HttpResponseStatus; // need this for isExpectedStatus function
22
+
23
+ constructor(
24
+ private readonly wsRequestArgs: WSRequestArguments,
25
+ private readonly wsHandler: WSSequenceHandler,
26
+ private readonly onError: (e: Error) => void,
27
+ ) {
28
+ this.url = wsRequestArgs.url;
29
+ this.expectedStatus = wsRequestArgs.expectedStatus;
30
+ }
31
+
32
+ /**
33
+ * This function is executed when we are ready to send the ws request
34
+ * @param cb This callback is being executed after we successfully connected to ws and sent a ws message if there was any
35
+ */
36
+ async ok(cb: (response: WSResponse) => boolean) {
37
+ const response = {
38
+ status: 101,
39
+ res: {
40
+ statusMessage: SWITCHING_PROTOCOLS,
41
+ },
42
+ header: {},
43
+ req: {},
44
+ };
45
+
46
+ const existingWS = this.wsHandler.getConnection(this.wsRequestArgs.url);
47
+
48
+ log.debug(`Connection state ${getConnectionState(existingWS)}`);
49
+ if (!existingWS || (existingWS && existingWS.readyState !== WSState.OPEN)) {
50
+ await this.addConnection(response);
51
+ } else {
52
+ this.ws = existingWS;
53
+ log.debug('Reusing existing ws connection', this.ws.url);
54
+ }
55
+
56
+ try {
57
+ this.sendMessage();
58
+ } catch (e) {
59
+ log.error('Failed to send a ws message', e);
60
+ }
61
+ cb(response);
62
+ return { };
63
+ }
64
+
65
+ private async addConnection(response: WSResponse) {
66
+ this.ws = new WebSocket(this.wsRequestArgs.url, undefined, { headers: this.wsRequestArgs.headers });
67
+ this.addEventListeners(response);
68
+ !this.hasErrorOccured && await this.verifyOpen();
69
+ this.wsHandler.storeConnection(this.wsRequestArgs.url, this.ws);
70
+ // todo add some debug details for the user to know we created a new connection in this request
71
+ }
72
+
73
+ private async addEventListeners(response: WSResponse) {
74
+ this.addOnErrorListener();
75
+ this.addOnUpgradeListener(response);
76
+ this.addOnOpenListener();
77
+ this.addOnMessageListener();
78
+ }
79
+
80
+ private addOnErrorListener() {
81
+ this.ws.on('error', (e) => {
82
+ try {
83
+ log.debug('inside on error', e);
84
+ this.onError(e);
85
+ this.hasErrorOccured = true;
86
+ } catch (e) {
87
+ log.warn('Failed to handle a ws error', e);
88
+ }
89
+ try {
90
+ log.debug('closing ws');
91
+ this.ws.close();
92
+ } catch (e) {
93
+ log.warn('Failed to close a ws connection', e);
94
+ }
95
+ });
96
+ }
97
+
98
+ private addOnUpgradeListener(response: WSResponse) {
99
+ this.ws.on('upgrade', (event) => {
100
+ try {
101
+ const { statusCode, statusMessage, headers } = event;
102
+ log.debug('inside upgrade event', { statusCode, statusMessage, headers });
103
+ response.status = statusCode || 101;
104
+ response.res.statusMessage = statusMessage || SWITCHING_PROTOCOLS;
105
+ response.header = headers as LoadmillHeaders;
106
+ } catch (e) {
107
+ log.debug('upgrade event err', e);
108
+ }
109
+ });
110
+ }
111
+
112
+ private addOnOpenListener() {
113
+ this.ws.on('open', () => {
114
+ log.debug('inside on open, currently doing nothing here.');
115
+ });
116
+ }
117
+
118
+ private addOnMessageListener() {
119
+ this.ws.on('message', (message: string) => {
120
+ try {
121
+ log.debug('got incoming message', message);
122
+ this.wsHandler.addMessage(message);
123
+ } catch (e) {
124
+ log.debug('error getting message', e);
125
+ }
126
+ });
127
+ }
128
+
129
+ async verifyOpen() {
130
+ const readyState = await this.waitForConnectedState(this.wsRequestArgs.timeout);
131
+ log.debug(`Connection state ${WSState[readyState]}`);
132
+ if (readyState !== WebSocket.OPEN && !this.hasErrorOccured) {
133
+ log.error('Could not connect to WebSocket address', { connectionState: WSState[readyState], url: this.ws.url });
134
+ throw new RequestFailuresError('Could not connect to WebSocket address');
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Waiting for WS connection to go out of CONNECTING state
140
+ * @param socket The ws connection object
141
+ * @param timeout The timeout in milliseconds for the waiting procedure
142
+ * @returns WSState
143
+ */
144
+ waitForConnectedState = async (timeout: number = WS_CONNECTION_TIMEOUT_MS): Promise<WSState> => {
145
+ const WS_CONNECTION_INTERVAL_MS = 100;
146
+ if (this.ws.readyState !== WebSocket.CONNECTING) {
147
+ return this.ws.readyState;
148
+ }
149
+ else {
150
+ const maxIterations = timeout / WS_CONNECTION_INTERVAL_MS;
151
+ let i = 0;
152
+ while (this.ws.readyState === WebSocket.CONNECTING && i < maxIterations) {
153
+ await delay(WS_CONNECTION_INTERVAL_MS);
154
+ i++;
155
+ }
156
+ return this.ws.readyState;
157
+ }
158
+ };
159
+
160
+ private sendMessage() {
161
+ log.debug('about to send message', { readyState: WSState[this.ws.readyState] } );
162
+ if (this.ws.readyState === WSState.OPEN && this.wsRequestArgs.message) {
163
+ log.debug('sending message', this.wsRequestArgs.message);
164
+ this.ws.send(this.wsRequestArgs.message);
165
+ }
166
+ }
167
+
168
+ // need this because the SequenceExecutor expectes a request obj with on func prop
169
+ on(_eventName: string, _cb: Function) {
170
+ //do nothing
171
+ }
172
+ }
173
+
174
+ export class WSSequenceHandler {
175
+ private _connections: { [url: string]: WebSocket };
176
+ messages: string[];
177
+ constructor() {
178
+ this._connections = {};
179
+ this.clearMessages();
180
+ }
181
+
182
+ getConnection(url: string): WebSocket | undefined {
183
+ return this._connections[url];
184
+ }
185
+
186
+ storeConnection(url: string, wsConnection: WebSocket) {
187
+ this._connections[url] = wsConnection;
188
+ }
189
+
190
+ async closeAllConnections() {
191
+ try {
192
+ log.debug('Closing all ws connections');
193
+ for (const connection of Object.values(this._connections)) {
194
+ connection.close();
195
+ }
196
+ } catch (e) {
197
+ log.warn('Failed to close all connections', e, { connections: this._connections });
198
+ }
199
+ }
200
+
201
+ getMessages() {
202
+ return this.messages;
203
+ }
204
+
205
+ addMessage(message: string) {
206
+ this.messages.push(message);
207
+ }
208
+
209
+ clearMessages() {
210
+ this.messages = [];
211
+ }
212
+ }
213
+
214
+ function getConnectionState(existingWS?: WebSocket) {
215
+ return existingWS ? WSState[existingWS.readyState] : null;
216
+ }
217
+
218
+ export type WSRequestArguments = {
219
+ expectedStatus: HttpResponseStatus;
220
+ headers: LoadmillHeaders
221
+ message: string;
222
+ timeout: number;
223
+ url: string;
224
+ };
225
+
226
+ export type WSResponse = {
227
+ status: number;
228
+ res: {
229
+ statusMessage: string;
230
+ };
231
+ header: LoadmillHeaders;
232
+ req: {};
233
+ }