@loadmill/executer 0.1.35 → 0.1.39
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/dist/errors.d.ts +5 -0
- package/dist/errors.js +38 -0
- package/dist/errors.js.map +1 -0
- package/dist/extraction-combiner.d.ts +4 -2
- package/dist/extraction-combiner.js +96 -46
- package/dist/extraction-combiner.js.map +1 -1
- package/dist/mill-version.js +1 -1
- package/dist/post-script/ast-walker/index.js.map +1 -1
- package/dist/sampler.js +4 -3
- package/dist/sampler.js.map +1 -1
- package/dist/sequence.js +254 -114
- package/dist/sequence.js.map +1 -1
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +29 -0
- package/dist/utils.js.map +1 -0
- package/dist/ws.d.ts +69 -0
- package/dist/ws.js +462 -0
- package/dist/ws.js.map +1 -0
- package/package.json +5 -5
- package/src/errors.ts +10 -0
- package/src/extraction-combiner.ts +15 -4
- package/src/mill-version.ts +1 -1
- package/src/post-script/ast-walker/index.ts +1 -1
- package/src/sampler.ts +4 -3
- package/src/sequence.ts +109 -37
- package/src/utils.ts +8 -0
- package/src/ws.ts +268 -0
- package/test/post-script-executor.spec.ts +24 -24
- package/yarn-error.log +0 -18827
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,15 @@ 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
|
|
|
77
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';
|
|
78
80
|
|
|
79
81
|
export const reqIdParamName = 'loadmill-request-id';
|
|
80
82
|
|
|
@@ -127,6 +129,7 @@ class SequenceExecutor {
|
|
|
127
129
|
resolvedRequests: ResolvedRequest[] = [];
|
|
128
130
|
keepaliveHTTPSAgent;
|
|
129
131
|
keepaliveHTTPAgent;
|
|
132
|
+
wsHandler: WSSequenceHandler;
|
|
130
133
|
|
|
131
134
|
constructor(
|
|
132
135
|
private httpAgent,
|
|
@@ -143,6 +146,7 @@ class SequenceExecutor {
|
|
|
143
146
|
freeSocketTimeout: SOCKET_TIMEOUT / 2
|
|
144
147
|
});
|
|
145
148
|
this.postScriptRunner = new PostScriptRunner();
|
|
149
|
+
this.wsHandler = new WSSequenceHandler();
|
|
146
150
|
}
|
|
147
151
|
|
|
148
152
|
startAndPass(requests: LoadmillRequest[]) {
|
|
@@ -213,9 +217,11 @@ class SequenceExecutor {
|
|
|
213
217
|
log.info('Unexpected error info:', unexpectedError);
|
|
214
218
|
}
|
|
215
219
|
|
|
220
|
+
this.wsHandler.closeAllConnections();
|
|
216
221
|
throw error;
|
|
217
222
|
}
|
|
218
223
|
}
|
|
224
|
+
this.wsHandler.closeAllConnections();
|
|
219
225
|
}
|
|
220
226
|
|
|
221
227
|
shouldStop(request: LoadmillRequest) {
|
|
@@ -249,7 +255,11 @@ class SequenceExecutor {
|
|
|
249
255
|
reqIndex: number,
|
|
250
256
|
requests: LoadmillRequest[]
|
|
251
257
|
) {
|
|
252
|
-
const { skipBefore } = request;
|
|
258
|
+
const { skipBefore, disabled } = request;
|
|
259
|
+
|
|
260
|
+
if (disabled) {
|
|
261
|
+
return reqIndex + 1;
|
|
262
|
+
}
|
|
253
263
|
|
|
254
264
|
if (skipBefore) {
|
|
255
265
|
const { goTo, negate, condition } = skipBefore;
|
|
@@ -305,12 +315,14 @@ class SequenceExecutor {
|
|
|
305
315
|
let failedAssertionsHistogram, resTime;
|
|
306
316
|
let loopIteration = 0;
|
|
307
317
|
const maxIterations = getLoopIterations(requestConf.loop);
|
|
318
|
+
this.wsHandler.clearMessages();
|
|
308
319
|
|
|
309
320
|
while (loopIteration < maxIterations) {
|
|
310
321
|
loopIteration++;
|
|
311
322
|
const request = this.prepareRequest(requestConf, index);
|
|
312
323
|
|
|
313
324
|
const res = await this.sendRequest(request, index);
|
|
325
|
+
|
|
314
326
|
// Setting now to avoid possible user overwrite:
|
|
315
327
|
resTime = Number(this.parameters.__responseTime);
|
|
316
328
|
|
|
@@ -321,7 +333,9 @@ class SequenceExecutor {
|
|
|
321
333
|
);
|
|
322
334
|
}
|
|
323
335
|
|
|
324
|
-
|
|
336
|
+
res.wsExtractionData = { messages: this.wsHandler.messages, timeLimit: timeout - resTime } as WSExtractionData;
|
|
337
|
+
|
|
338
|
+
failedAssertionsHistogram = await this.processSuccessfulResponse(
|
|
325
339
|
index,
|
|
326
340
|
requestConf,
|
|
327
341
|
res
|
|
@@ -497,8 +511,18 @@ class SequenceExecutor {
|
|
|
497
511
|
}
|
|
498
512
|
|
|
499
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) {
|
|
500
524
|
const urlObj = new URI(
|
|
501
|
-
resolveUrl(requestConf.url, this.parameters, (err) =>
|
|
525
|
+
resolveUrl(requestConf.url, this.parameters, (err: Error) =>
|
|
502
526
|
setParameterErrorHistogram(err, 'Failed to compute URL - ')
|
|
503
527
|
)
|
|
504
528
|
);
|
|
@@ -516,7 +540,7 @@ class SequenceExecutor {
|
|
|
516
540
|
|
|
517
541
|
const url = urlObj.toString();
|
|
518
542
|
this.resolvedRequests[reqIndex].url = url;
|
|
519
|
-
|
|
543
|
+
|
|
520
544
|
this.validateDomain(uriUtils.getDomain(url));
|
|
521
545
|
|
|
522
546
|
const method = requestConf.method.toLowerCase();
|
|
@@ -633,7 +657,7 @@ class SequenceExecutor {
|
|
|
633
657
|
// This makes superagent populate res.text regardless of the response's content type:
|
|
634
658
|
request.buffer();
|
|
635
659
|
|
|
636
|
-
// Otherwise we expose superagent and its version -
|
|
660
|
+
// Otherwise we expose superagent and its version -
|
|
637
661
|
// better have something more general (in case the user didnt set one):
|
|
638
662
|
const uaHeader = request.get('User-Agent');
|
|
639
663
|
if (!uaHeader || uaHeader.startsWith('node-superagent')) {
|
|
@@ -650,6 +674,66 @@ class SequenceExecutor {
|
|
|
650
674
|
return request;
|
|
651
675
|
}
|
|
652
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
|
+
|
|
653
737
|
checkProgressEvent = (
|
|
654
738
|
requestIndex: number,
|
|
655
739
|
request,
|
|
@@ -664,7 +748,7 @@ class SequenceExecutor {
|
|
|
664
748
|
};
|
|
665
749
|
|
|
666
750
|
private validateDomain(domain: string) {
|
|
667
|
-
if (isEmpty(domain)){
|
|
751
|
+
if (isEmpty(domain)) {
|
|
668
752
|
const message = 'HTTP request domain name is empty';
|
|
669
753
|
throw new RequestFailuresError(message, { [message]: 1 });
|
|
670
754
|
}
|
|
@@ -711,9 +795,9 @@ class SequenceExecutor {
|
|
|
711
795
|
)
|
|
712
796
|
);
|
|
713
797
|
|
|
714
|
-
processSuccessfulResponse(reqIndex, requestConf: LoadmillRequest, res) {
|
|
798
|
+
async processSuccessfulResponse(reqIndex, requestConf: LoadmillRequest, res) {
|
|
715
799
|
// modifies parameters:
|
|
716
|
-
this.handleExtractions(requestConf, res);
|
|
800
|
+
await this.handleExtractions(requestConf, res);
|
|
717
801
|
|
|
718
802
|
if (!envUtils.isBrowser()) {
|
|
719
803
|
this.handlePostScript(requestConf, res);
|
|
@@ -741,33 +825,33 @@ class SequenceExecutor {
|
|
|
741
825
|
);
|
|
742
826
|
}
|
|
743
827
|
|
|
744
|
-
handleExtractions(requestConf: LoadmillRequest, res) {
|
|
745
|
-
requestConf.extract
|
|
746
|
-
this.extractInScope(res, extractions)
|
|
747
|
-
|
|
828
|
+
async handleExtractions(requestConf: LoadmillRequest, res) {
|
|
829
|
+
for (const extractions of (requestConf.extract || [])) {
|
|
830
|
+
await this.extractInScope(res, extractions);
|
|
831
|
+
}
|
|
748
832
|
}
|
|
749
833
|
|
|
750
|
-
extractInScope(res, extractions: Extractions) {
|
|
834
|
+
async extractInScope(res, extractions: Extractions) {
|
|
751
835
|
const contextParameters = Object.assign({}, this.parameters);
|
|
752
|
-
const extractionCombiner = new ExtractionCombiner(contextParameters, res);
|
|
836
|
+
const extractionCombiner = new ExtractionCombiner(contextParameters, res, res.wsExtractionData);
|
|
753
837
|
|
|
754
|
-
|
|
755
|
-
this.extract(name, extraction, extractionCombiner)
|
|
756
|
-
|
|
838
|
+
for (const [name, extraction] of Object.entries(extractions)) {
|
|
839
|
+
await this.extract(name, extraction, extractionCombiner);
|
|
840
|
+
}
|
|
757
841
|
}
|
|
758
842
|
|
|
759
|
-
extract(
|
|
843
|
+
async extract(
|
|
760
844
|
parameterName: string,
|
|
761
845
|
extraction: Extraction,
|
|
762
846
|
extractionCombiner: ExtractionCombiner
|
|
763
847
|
) {
|
|
764
848
|
log.trace('Parameter extraction start: ', { parameterName, extraction });
|
|
765
849
|
|
|
766
|
-
const combinedExtractor = extractionCombiner.combine(extraction);
|
|
850
|
+
const combinedExtractor = await extractionCombiner.combine(extraction);
|
|
767
851
|
let result;
|
|
768
852
|
|
|
769
853
|
try {
|
|
770
|
-
result = combinedExtractor();
|
|
854
|
+
result = await combinedExtractor();
|
|
771
855
|
} catch (error) {
|
|
772
856
|
const genericMessage = `Failed to extract value for parameter "${parameterName}"`;
|
|
773
857
|
log.debug(genericMessage, error);
|
|
@@ -889,9 +973,6 @@ function isSimpleRequest(headers) {
|
|
|
889
973
|
);
|
|
890
974
|
}
|
|
891
975
|
|
|
892
|
-
const getMaxRequestBodySize = () =>
|
|
893
|
-
envUtils.isBrowser() ? MAX_LOAD_REQUEST_BODY_LENGTH : MAX_API_REQUEST_BODY_LENGTH;
|
|
894
|
-
|
|
895
976
|
const getLoopIterations = (LoopConf?: LoopConf) => {
|
|
896
977
|
const declared = (LoopConf && LoopConf.iterations) || 1;
|
|
897
978
|
return Math.min(MAX_REQUEST_LOOPS_ITERATIONS, declared);
|
|
@@ -909,7 +990,7 @@ const extendResponseHeaders = (headers, redirectHeaders) => {
|
|
|
909
990
|
|
|
910
991
|
const isExpectedStatus = ({ expectedStatus, url }, status: number) => {
|
|
911
992
|
if (expectedStatus === ALLOWED_RESPONSE_STATUSES.SUCCESS) {
|
|
912
|
-
return 200 <= status && status < 400;
|
|
993
|
+
return (200 <= status && status < 400) || status === 101;
|
|
913
994
|
}
|
|
914
995
|
else if (expectedStatus === ALLOWED_RESPONSE_STATUSES.ERROR) {
|
|
915
996
|
log.debug('user asked to fail this request', url);
|
|
@@ -945,12 +1026,3 @@ const setTCPReuse = (request, agent, sslAgent) => {
|
|
|
945
1026
|
};
|
|
946
1027
|
|
|
947
1028
|
};
|
|
948
|
-
|
|
949
|
-
class RequestFailuresError extends Error {
|
|
950
|
-
constructor(message: string, public histogram: Histogram = { [message]: 1 }) {
|
|
951
|
-
super(message);
|
|
952
|
-
|
|
953
|
-
// 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
|
|
954
|
-
Object.setPrototypeOf(this, RequestFailuresError.prototype);
|
|
955
|
-
}
|
|
956
|
-
}
|
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,268 @@
|
|
|
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;
|
|
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 = {
|
|
51
|
+
status: 101,
|
|
52
|
+
res: {
|
|
53
|
+
statusMessage: SWITCHING_PROTOCOLS,
|
|
54
|
+
},
|
|
55
|
+
header: {},
|
|
56
|
+
req: {},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const existingWS = this.wsHandler.getConnection(this.wsRequestArgs.url);
|
|
60
|
+
|
|
61
|
+
log.debug(`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
|
+
const statusCode = ws._req.socket.parser.incoming.statusCode;
|
|
123
|
+
const statusMessage = ws._req.socket.parser.incoming.statusMessage;
|
|
124
|
+
const headers = ws._req.socket.parser.incoming.headers;
|
|
125
|
+
log.debug('addEventListener error statusCode', statusCode);
|
|
126
|
+
log.debug('addEventListener error statusMessage', statusMessage);
|
|
127
|
+
log.debug('addEventListener error headers', headers);
|
|
128
|
+
response.status = statusCode;
|
|
129
|
+
response.res.statusMessage = statusMessage;
|
|
130
|
+
response.header = headers;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private addOnUpgradeListener(response: WSResponse) {
|
|
134
|
+
this.ws.on('upgrade', (event) => {
|
|
135
|
+
try {
|
|
136
|
+
const { statusCode, statusMessage, headers } = event;
|
|
137
|
+
log.debug('inside upgrade event', { statusCode, statusMessage, headers });
|
|
138
|
+
response.status = statusCode || 101;
|
|
139
|
+
response.res.statusMessage = statusMessage || SWITCHING_PROTOCOLS;
|
|
140
|
+
response.header = headers as LoadmillHeaders;
|
|
141
|
+
} catch (e) {
|
|
142
|
+
log.debug('upgrade event err', e);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private addOnOpenListener() {
|
|
148
|
+
this.ws.on('open', () => {
|
|
149
|
+
log.debug('inside on open, currently doing nothing here.');
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private addOnMessageListener() {
|
|
154
|
+
this.ws.on('message', (message: string) => {
|
|
155
|
+
try {
|
|
156
|
+
log.debug('got incoming message', message);
|
|
157
|
+
this.wsHandler.addMessage(message);
|
|
158
|
+
} catch (e) {
|
|
159
|
+
log.debug('error getting message', e);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async verifyOpen() {
|
|
165
|
+
const readyState = await this.waitForConnectedState(this.wsRequestArgs.timeout);
|
|
166
|
+
log.debug(`Connection state ${WSState[readyState]}`);
|
|
167
|
+
if (readyState !== WebSocket.OPEN && !this.hasErrorOccured) {
|
|
168
|
+
log.error('Could not connect to WebSocket address', { connectionState: WSState[readyState], url: this.ws.url });
|
|
169
|
+
throw new RequestFailuresError('Could not connect to WebSocket address');
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Waiting for WS connection to go out of CONNECTING state
|
|
175
|
+
* @param socket The ws connection object
|
|
176
|
+
* @param timeout The timeout in milliseconds for the waiting procedure
|
|
177
|
+
* @returns WSState
|
|
178
|
+
*/
|
|
179
|
+
waitForConnectedState = async (timeout: number = WS_CONNECTION_TIMEOUT_MS): Promise<WSState> => {
|
|
180
|
+
const WS_CONNECTION_INTERVAL_MS = 100;
|
|
181
|
+
if (this.ws.readyState !== WebSocket.CONNECTING) {
|
|
182
|
+
return this.ws.readyState;
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
const maxIterations = timeout / WS_CONNECTION_INTERVAL_MS;
|
|
186
|
+
let i = 0;
|
|
187
|
+
while (this.ws.readyState === WebSocket.CONNECTING && i < maxIterations) {
|
|
188
|
+
await delay(WS_CONNECTION_INTERVAL_MS);
|
|
189
|
+
i++;
|
|
190
|
+
}
|
|
191
|
+
return this.ws.readyState;
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
private sendMessage() {
|
|
196
|
+
log.debug('about to send message', { readyState: WSState[this.ws.readyState] } );
|
|
197
|
+
if (this.ws.readyState === WSState.OPEN && this.wsRequestArgs.message) {
|
|
198
|
+
log.debug('sending message', this.wsRequestArgs.message);
|
|
199
|
+
this.ws.send(this.wsRequestArgs.message);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// need this because the SequenceExecutor expectes a request obj with on func prop
|
|
204
|
+
on(_eventName: string, _cb: Function) {
|
|
205
|
+
//do nothing
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export class WSSequenceHandler {
|
|
210
|
+
private _connections: { [url: string]: WebSocket };
|
|
211
|
+
messages: string[];
|
|
212
|
+
constructor() {
|
|
213
|
+
this._connections = {};
|
|
214
|
+
this.clearMessages();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
getConnection(url: string): WebSocket | undefined {
|
|
218
|
+
return this._connections[url];
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
storeConnection(url: string, wsConnection: WebSocket) {
|
|
222
|
+
this._connections[url] = wsConnection;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async closeAllConnections() {
|
|
226
|
+
try {
|
|
227
|
+
log.debug('Closing all ws connections');
|
|
228
|
+
for (const connection of Object.values(this._connections)) {
|
|
229
|
+
connection.close();
|
|
230
|
+
}
|
|
231
|
+
} catch (e) {
|
|
232
|
+
log.warn('Failed to close all connections', e, { connections: this._connections });
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
getMessages() {
|
|
237
|
+
return this.messages;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
addMessage(message: string) {
|
|
241
|
+
this.messages.push(message);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
clearMessages() {
|
|
245
|
+
this.messages = [];
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function getConnectionState(existingWS?: WebSocket) {
|
|
250
|
+
return existingWS ? WSState[existingWS.readyState] : null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export type WSRequestArguments = {
|
|
254
|
+
expectedStatus: HttpResponseStatus;
|
|
255
|
+
headers: LoadmillHeaders
|
|
256
|
+
message: string;
|
|
257
|
+
timeout: number;
|
|
258
|
+
url: string;
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
export type WSResponse = {
|
|
262
|
+
status?: number;
|
|
263
|
+
res: {
|
|
264
|
+
statusMessage?: string;
|
|
265
|
+
};
|
|
266
|
+
header: LoadmillHeaders | IncomingHttpHeaders;
|
|
267
|
+
req: {};
|
|
268
|
+
}
|