Skip to content

Commit 92cdcc3

Browse files
committed
Drop lodash & fastpath async APIs for performance
1 parent 73397a2 commit 92cdcc3

File tree

5 files changed

+74
-102
lines changed

5 files changed

+74
-102
lines changed

src/rules/matchers.ts

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -638,23 +638,31 @@ export const MatcherLookup = {
638638
'callback': CallbackMatcher,
639639
};
640640

641-
export async function matchesAll(req: OngoingRequest, matchers: RequestMatcher[]): Promise<boolean> {
641+
export function matchesAll(req: OngoingRequest, matchers: RequestMatcher[]): MaybePromise<boolean> {
642+
// Fast path: if all matchers return synchronously, avoid Promise overhead entirely.
643+
// This is the common case (e.g. WildcardMatcher returns true synchronously).
644+
const asyncResults: Array<Promise<boolean>> = [];
645+
646+
for (const matcher of matchers) {
647+
const result = matcher.matches(req);
648+
if (result === false) return false; // Sync mismatch — bail immediately
649+
if (result !== true) asyncResults.push(result); // Got a Promise
650+
}
651+
652+
if (asyncResults.length === 0) return true; // All sync true
653+
654+
// Async path: at least one matcher returned a Promise
642655
return new Promise<boolean>((resolve, reject) => {
643-
const resultsPromises = matchers.map((matcher) => matcher.matches(req));
644-
645-
resultsPromises.forEach(async (maybePromiseResult) => {
646-
try {
647-
const result = await maybePromiseResult;
648-
if (!result) resolve(false); // Resolve mismatches immediately
649-
} catch (e) {
650-
reject(e); // Resolve matcher failures immediately
651-
}
652-
});
656+
for (const promise of asyncResults) {
657+
promise.then(
658+
(result) => { if (!result) resolve(false); },
659+
(e) => reject(e)
660+
);
661+
}
653662

654-
// Otherwise resolve as normal: all true matches, exceptions reject.
655-
Promise.all(resultsPromises)
656-
.then((result) => resolve(_.every(result)))
657-
.catch((e) => reject(e));
663+
Promise.all(asyncResults)
664+
.then((resolved) => resolve(resolved.every(Boolean)))
665+
.catch(reject);
658666
});
659667
}
660668

src/rules/passthrough-handling.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -525,8 +525,8 @@ function shouldUseStrictHttps(
525525
skipHttpsErrors = true;
526526
} else if (Array.isArray(ignoreHostHttpsErrors) && (
527527
// if the whole hostname or host+port is whitelisted
528-
_.includes(ignoreHostHttpsErrors, hostname) ||
529-
_.includes(ignoreHostHttpsErrors, `${hostname}:${port}`)
528+
ignoreHostHttpsErrors.includes(hostname) ||
529+
ignoreHostHttpsErrors.includes(`${hostname}:${port}`)
530530
)) {
531531
skipHttpsErrors = true;
532532
}

src/server/mockttp-server.ts

Lines changed: 21 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { Buffer } from 'buffer';
2-
import * as stream from 'stream';
32
import * as fs from 'fs';
43
import * as net from "net";
54
import * as tls from "tls";
@@ -18,21 +17,17 @@ import { Mutex } from 'async-mutex';
1817
import { ErrorLike, isErrorLike, UnreachableCheck } from '@httptoolkit/util';
1918

2019
import {
21-
Destination,
2220
InitiatedRequest,
2321
OngoingRequest,
2422
CompletedRequest,
2523
OngoingResponse,
2624
CompletedResponse,
2725
TlsHandshakeFailure,
2826
ClientError,
29-
TimingEvents,
30-
OngoingBody,
3127
WebSocketMessage,
3228
WebSocketClose,
3329
TlsPassthroughEvent,
3430
RuleEvent,
35-
RawTrailers,
3631
RawPassthroughEvent,
3732
RawPassthroughDataEvent,
3833
RawHeaders,
@@ -51,17 +46,7 @@ import { RequestRule, RequestRuleData } from "../rules/requests/request-rule";
5146
import { ServerMockedEndpoint } from "./mocked-endpoint";
5247
import { createComboServer } from "./http-combo-server";
5348
import { filter } from "../util/promise";
54-
import { Mutable } from "../util/type-utils";
55-
import { makePropertyWritable } from "../util/util";
5649

57-
import {
58-
isAbsoluteUrl,
59-
getPathFromAbsoluteUrl,
60-
getHostFromAbsoluteUrl,
61-
getDestination,
62-
normalizeHost,
63-
} from "../util/url";
64-
import { isIP } from "../util/ip-utils";
6550
import {
6651
buildRawSocketEventData,
6752
buildTlsSocketEventData,
@@ -72,15 +57,12 @@ import {
7257
import {
7358
ClientErrorInProgress,
7459
LastHopEncrypted,
75-
LastTunnelAddress,
7660
TlsSetupCompleted,
7761
SocketMetadata,
7862
TlsMetadata,
79-
SocketTimingInfo
8063
} from '../util/socket-extensions';
8164
import { getSocketMetadataTags, getSocketMetadataFromProxyAuth } from '../util/socket-metadata'
8265
import {
83-
parseRequestBody,
8466
waitForCompletedRequest,
8567
trackResponse,
8668
waitForCompletedResponse,
@@ -94,9 +76,7 @@ import {
9476
} from "../util/request-utils";
9577
import { asBuffer } from "../util/buffer-utils";
9678
import {
97-
getHeaderValue,
9879
pairFlatRawHeaders,
99-
rawHeadersToObject
10080
} from "../util/header-utils";
10181
import { AbortError } from "../rules/requests/request-step-impls";
10282
import { WebSocketRuleData, WebSocketRule } from "../rules/websockets/websocket-rule";
@@ -391,13 +371,9 @@ export class MockttpServer extends AbstractMockttp implements Mockttp {
391371

392372
setImmediate(() => {
393373
const initiatedReq = buildInitiatedRequest(request);
394-
emitter.emit('request-initiated', Object.assign(
395-
initiatedReq,
396-
{
397-
timingEvents: _.clone(initiatedReq.timingEvents),
398-
tags: _.clone(initiatedReq.tags)
399-
}
400-
));
374+
initiatedReq.timingEvents = { ...initiatedReq.timingEvents };
375+
initiatedReq.tags = initiatedReq.tags.slice();
376+
emitter.emit('request-initiated', initiatedReq);
401377
});
402378
}
403379

@@ -407,13 +383,9 @@ export class MockttpServer extends AbstractMockttp implements Mockttp {
407383
waitForCompletedRequest(request)
408384
.then((completedReq: CompletedRequest) => {
409385
setImmediate(() => {
410-
emitter.emit('request', Object.assign(
411-
completedReq,
412-
{
413-
timingEvents: _.clone(completedReq.timingEvents),
414-
tags: _.clone(completedReq.tags)
415-
}
416-
));
386+
completedReq.timingEvents = { ...completedReq.timingEvents };
387+
completedReq.tags = completedReq.tags.slice();
388+
emitter.emit('request', completedReq);
417389
});
418390
})
419391
.catch(console.error);
@@ -424,13 +396,9 @@ export class MockttpServer extends AbstractMockttp implements Mockttp {
424396

425397
setImmediate(() => {
426398
const initiatedRes = buildInitiatedResponse(response);
427-
emitter.emit('response-initiated', Object.assign(
428-
initiatedRes,
429-
{
430-
timingEvents: _.clone(initiatedRes.timingEvents),
431-
tags: _.clone(initiatedRes.tags)
432-
}
433-
));
399+
initiatedRes.timingEvents = { ...initiatedRes.timingEvents };
400+
initiatedRes.tags = initiatedRes.tags.slice();
401+
emitter.emit('response-initiated', initiatedRes);
434402
});
435403
}
436404

@@ -440,10 +408,9 @@ export class MockttpServer extends AbstractMockttp implements Mockttp {
440408
waitForCompletedResponse(response)
441409
.then((res: CompletedResponse) => {
442410
setImmediate(() => {
443-
emitter.emit('response', Object.assign(res, {
444-
timingEvents: _.clone(res.timingEvents),
445-
tags: _.clone(res.tags)
446-
}));
411+
res.timingEvents = { ...res.timingEvents };
412+
res.tags = res.tags.slice();
413+
emitter.emit('response', res);
447414
});
448415
})
449416
.catch(console.error);
@@ -455,10 +422,9 @@ export class MockttpServer extends AbstractMockttp implements Mockttp {
455422
waitForCompletedRequest(request)
456423
.then((completedReq: CompletedRequest) => {
457424
setImmediate(() => {
458-
emitter.emit('websocket-request', Object.assign(completedReq, {
459-
timingEvents: _.clone(completedReq.timingEvents),
460-
tags: _.clone(completedReq.tags)
461-
}));
425+
completedReq.timingEvents = { ...completedReq.timingEvents };
426+
completedReq.tags = completedReq.tags.slice();
427+
emitter.emit('websocket-request', completedReq);
462428
});
463429
})
464430
.catch(console.error);
@@ -470,8 +436,8 @@ export class MockttpServer extends AbstractMockttp implements Mockttp {
470436
setImmediate(() => {
471437
emitter.emit('websocket-accepted', {
472438
...response,
473-
timingEvents: _.clone(response.timingEvents),
474-
tags: _.clone(response.tags)
439+
timingEvents: { ...response.timingEvents },
440+
tags: response.tags.slice()
475441
});
476442
});
477443
}
@@ -603,9 +569,9 @@ export class MockttpServer extends AbstractMockttp implements Mockttp {
603569
private async announceAbortAsync(emitter: EventEmitter, request: OngoingRequest, abortError?: ErrorLike) {
604570
setImmediate(() => {
605571
const req = buildInitiatedRequest(request);
572+
req.timingEvents = { ...req.timingEvents };
573+
req.tags = req.tags.slice();
606574
emitter.emit('abort', Object.assign(req, {
607-
timingEvents: _.clone(req.timingEvents),
608-
tags: _.clone(req.tags),
609575
error: abortError ? {
610576
name: abortError.name,
611577
code: abortError.code,
@@ -891,7 +857,8 @@ export class MockttpServer extends AbstractMockttp implements Mockttp {
891857
// There are no incomplete & matching rules! One last option: if the last matching rule is
892858
// maybe-incomplete (i.e. default completion status but has seen >0 requests) then it should
893859
// match anyway. This allows us to add rules and have the last repeat indefinitely.
894-
const lastMatchingRule = _.last(await filter(rulesMatches, m => m.match))?.rule;
860+
const matchingRules = await filter(rulesMatches, m => m.match);
861+
const lastMatchingRule = matchingRules[matchingRules.length - 1]?.rule;
895862
if (!lastMatchingRule || lastMatchingRule.isComplete()) continue; // On to lower priority matches
896863
// Otherwise, must be a rule with isComplete === null, i.e. no specific completion check:
897864
else return lastMatchingRule;

src/util/header-utils.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,12 @@ export function pairFlatRawHeaders(flatRawHeaders: string[]): RawHeaders {
6060
}
6161

6262
export function flattenPairedRawHeaders(rawHeaders: RawHeaders): string[] {
63-
return rawHeaders.flat();
63+
const result = new Array(rawHeaders.length * 2);
64+
for (let i = 0; i < rawHeaders.length; i++) {
65+
result[i * 2] = rawHeaders[i][0];
66+
result[i * 2 + 1] = rawHeaders[i][1];
67+
}
68+
return result;
6469
}
6570

6671
/**

src/util/request-utils.ts

Lines changed: 22 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -488,21 +488,19 @@ export function preprocessRequest(req: ExtendedRawRequest, options: {
488488
*/
489489
export function buildInitiatedRequest(request: OngoingRequest): InitiatedRequest {
490490
return {
491-
..._.pick(request,
492-
'id',
493-
'matchedRuleId',
494-
'protocol',
495-
'httpVersion',
496-
'method',
497-
'url',
498-
'path',
499-
'remoteIpAddress',
500-
'remotePort',
501-
'destination',
502-
'headers',
503-
'rawHeaders',
504-
'tags'
505-
),
491+
id: request.id,
492+
matchedRuleId: request.matchedRuleId,
493+
protocol: request.protocol,
494+
httpVersion: request.httpVersion,
495+
method: request.method,
496+
url: request.url!,
497+
path: request.path!,
498+
remoteIpAddress: request.remoteIpAddress,
499+
remotePort: request.remotePort,
500+
destination: request.destination,
501+
headers: request.headers,
502+
rawHeaders: request.rawHeaders,
503+
tags: request.tags,
506504
timingEvents: request.timingEvents
507505
};
508506
}
@@ -736,23 +734,17 @@ function emitBodyDataEvents(
736734
* that's just started.
737735
*/
738736
export function buildInitiatedResponse(response: OngoingResponse): InitiatedResponse {
739-
const initiatedResponse: InitiatedResponse = _(response).pick([
740-
'id',
741-
'statusCode',
742-
'timingEvents',
743-
'tags'
744-
]).assign({
745-
statusMessage: '',
737+
return {
738+
id: response.id,
739+
statusCode: response.statusCode,
740+
statusMessage: (response instanceof http2.Http2ServerResponse)
741+
? '' // H2 has no status messages, and generates a warning if you look for one
742+
: response.statusMessage,
746743
headers: response.getHeaders(),
747744
rawHeaders: response.getRawHeaders(),
748-
}).valueOf();
749-
750-
if (!(response instanceof http2.Http2ServerResponse)) {
751-
// H2 has no status messages, and generates a warning if you look for one
752-
initiatedResponse.statusMessage = response.statusMessage;
753-
}
754-
755-
return initiatedResponse;
745+
timingEvents: response.timingEvents,
746+
tags: response.tags
747+
};
756748
}
757749

758750
/**

0 commit comments

Comments
 (0)