@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/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/request-sequence-result.d.ts +2 -1
- package/dist/request-sequence-result.js.map +1 -1
- package/dist/sequence.js +288 -115
- 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 +468 -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/request-sequence-result.ts +2 -0
- package/src/sequence.ts +138 -38
- package/src/utils.ts +8 -0
- package/src/ws.ts +273 -0
- package/test/post-script-executor.spec.ts +24 -24
- package/yarn-error.log +18845 -0
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
|
-
|
|
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
|
|
550
|
-
if (
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|