Skip to content

Commit 42fb23e

Browse files
committed
Add timestamps & explicit start/end events to client API
1 parent 9a002eb commit 42fb23e

File tree

4 files changed

+63
-25
lines changed

4 files changed

+63
-25
lines changed

src/api/rest-api.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -119,16 +119,12 @@ export function exposeRestAPI(
119119
res.write('\n');
120120
});
121121

122-
resultStream.on('end', () => {
123-
res.write(JSON.stringify({
124-
'type': 'response-end'
125-
}) + '\n');
126-
res.end();
127-
});
122+
resultStream.on('end', () => res.end());
128123

129124
resultStream.on('error', (error: ErrorLike) => {
130125
res.write(JSON.stringify({
131126
type: 'error',
127+
timestamp: performance.now(),
132128
error: {
133129
code: error.code,
134130
message: error.message,

src/client/client.ts

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,20 +30,35 @@ export interface RequestOptions {
3030
}
3131

3232
export type ResponseStreamEvents =
33+
| RequestStart
3334
| ResponseHead
34-
| ResponseBodyPart;
35+
| ResponseBodyPart
36+
| ResponseEnd;
3537
// Other notable events: errors (via 'error' event) and clean closure (via 'end').
3638

39+
export interface RequestStart {
40+
type: 'request-start';
41+
startTime: number; // Unix timestamp
42+
timestamp: number; // High precision timer (for relative calculations on later events)
43+
}
44+
3745
export interface ResponseHead {
3846
type: 'response-head';
3947
statusCode: number;
4048
statusMessage?: string;
4149
headers: RawHeaders;
50+
timestamp: number;
4251
}
4352

4453
export interface ResponseBodyPart {
4554
type: 'response-body-part';
4655
rawBody: Buffer;
56+
timestamp: number;
57+
}
58+
59+
export interface ResponseEnd {
60+
type: 'response-end';
61+
timestamp: number;
4762
}
4863

4964
export function sendRequest(
@@ -86,6 +101,12 @@ export function sendRequest(
86101
read() {} // Can't pull data - we manually fill this with .push() instead.
87102
});
88103

104+
resultsStream.push({
105+
type: 'request-start',
106+
startTime: Date.now(),
107+
timestamp: performance.now()
108+
});
109+
89110
new Promise<http.IncomingMessage>((resolve, reject) => {
90111
request.on('error', reject);
91112
request.on('response', resolve);
@@ -94,15 +115,20 @@ export function sendRequest(
94115
type: 'response-head',
95116
statusCode: response.statusCode!,
96117
statusMessage: response.statusMessage,
97-
headers: pairFlatRawHeaders(response.rawHeaders)
118+
headers: pairFlatRawHeaders(response.rawHeaders),
119+
timestamp: performance.now()
98120
});
99121

100122
response.on('data', (data) => resultsStream.push({
101123
type: 'response-body-part',
102-
rawBody: data
124+
rawBody: data,
125+
timestamp: performance.now()
103126
}));
104127

105-
response.on('end', () => resultsStream.push(null));
128+
response.on('end', () => {
129+
resultsStream.push({ type: 'response-end', timestamp: performance.now() });
130+
resultsStream.push(null);
131+
});
106132
response.on('error', (error) => resultsStream.destroy(error));
107133
}).catch((error) => {
108134
resultsStream.destroy(error);

test/client/send-request.spec.ts

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
import * as _ from 'lodash';
12
import { expect } from 'chai';
23
import * as mockttp from 'mockttp';
34

4-
import { ResponseStreamEvents, sendRequest } from '../../src/client/client';
5+
import { sendRequest } from '../../src/client/client';
56
import { streamToArray } from '../../src/util/stream';
67
import { delay } from '../../src/util/promise';
78

@@ -44,8 +45,11 @@ describe("The HTTP client API", () => {
4445

4546
const responseEvents = await streamToArray<any>(responseStream);
4647

47-
expect(responseEvents.length).to.equal(2);
48-
expect(responseEvents[0]).to.deep.equal({
48+
expect(responseEvents.length).to.equal(4);
49+
expect(_.omit(responseEvents[0], 'timestamp', 'startTime')).to.deep.equal({
50+
type: 'request-start'
51+
});
52+
expect(_.omit(responseEvents[1], 'timestamp')).to.deep.equal({
4953
type: 'response-head',
5054
statusCode: 200,
5155
statusMessage: 'Custom status message',
@@ -54,8 +58,11 @@ describe("The HTTP client API", () => {
5458
]
5559
});
5660

57-
expect(responseEvents[1].type).equal('response-body-part');
58-
expect(responseEvents[1].rawBody.toString()).to.equal('Mock response body');
61+
expect(responseEvents[2].type).equal('response-body-part');
62+
expect(responseEvents[2].rawBody.toString()).to.equal('Mock response body');
63+
expect(_.omit(responseEvents[3], 'timestamp')).to.deep.equal({
64+
type: 'response-end'
65+
});
5966
});
6067

6168
it("should stop requests if cancelled", async () => {
@@ -83,18 +90,23 @@ describe("The HTTP client API", () => {
8390

8491
expect(requests.length).to.equal(1);
8592
expect(aborts.length).to.equal(0);
86-
expect(responseEvents.length).to.equal(0);
93+
94+
// Start is emitted immediately:
95+
expect(responseEvents.length).to.equal(1);
96+
expect(_.omit(responseEvents[0], 'timestamp', 'startTime')).to.deep.equal({
97+
type: 'request-start'
98+
});
8799

88100
abortController.abort();
89101
await delay(10);
90102

91103
expect(requests.length).to.equal(1);
92104
expect(aborts.length).to.equal(1); // <-- Server sees the request cancelled
93105

94-
// Only emitted event is a thrown error:
95-
expect(responseEvents.length).to.equal(1);
96-
expect(responseEvents[0]).to.be.instanceOf(Error);
97-
expect(responseEvents[0].code).to.be.oneOf([
106+
// Only other events is a thrown error:
107+
expect(responseEvents.length).to.equal(2);
108+
expect(responseEvents[1]).to.be.instanceOf(Error);
109+
expect(responseEvents[1].code).to.be.oneOf([
98110
// Depends on the Node version you're testing with:
99111
'ECONNRESET',
100112
'ABORT_ERR'

test/integration-test.spec.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import * as _ from 'lodash';
12
import { spawn, ChildProcess } from 'child_process';
23
import * as path from 'path';
34

@@ -358,8 +359,11 @@ describe('Integration test', function () {
358359
.filter(l => l.trim().length)
359360
.map(l => JSON.parse(l));
360361

361-
expect(responseEvents.length).to.equal(3);
362-
expect(responseEvents[0]).to.deep.equal({
362+
expect(responseEvents.length).to.equal(4);
363+
expect(_.omit(responseEvents[0], 'timestamp', 'startTime')).to.deep.equal({
364+
type: 'request-start'
365+
});
366+
expect(_.omit(responseEvents[1], 'timestamp')).to.deep.equal({
363367
type: 'response-head',
364368
statusCode: 200,
365369
statusMessage: 'Custom status message',
@@ -368,13 +372,13 @@ describe('Integration test', function () {
368372
]
369373
});
370374

371-
expect(responseEvents[1].type).equal('response-body-part');
375+
expect(responseEvents[2].type).equal('response-body-part');
372376
expect(
373-
Buffer.from(responseEvents[1].rawBody, 'base64').toString('utf8')
377+
Buffer.from(responseEvents[2].rawBody, 'base64').toString('utf8')
374378
).to.equal('Mock response body');
375379

376380

377-
expect(responseEvents[2]).to.deep.equal({ type: 'response-end' });
381+
expect(_.omit(responseEvents[3], 'timestamp')).to.deep.equal({ type: 'response-end' });
378382
});
379383
})
380384
});

0 commit comments

Comments
 (0)