Skip to content

Commit 705db5c

Browse files
committed
feat: add support for incremental multipart payloads
1 parent 9aaa958 commit 705db5c

File tree

2 files changed

+74
-37
lines changed

2 files changed

+74
-37
lines changed

src/__test__/PatchResolver.spec.js

Lines changed: 71 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@ function getMultiPartResponse(data, boundary) {
1010
return ['Content-Type: application/json', '', json, `--${boundary}\r\n`].join('\r\n');
1111
}
1212

13+
function assertChunksRecieved(mockCall, chunks) {
14+
const nonIncrementalChunks = chunks.flatMap((chunk) =>
15+
chunk.incremental ? chunk.incremental : chunk
16+
);
17+
expect(mockCall).toEqual(nonIncrementalChunks);
18+
}
19+
1320
describe('PathResolver', function () {
1421
for (const boundary of ['-', 'gc0p4Jq0M2Yt08jU534c0p']) {
1522
describe(`boundary ${boundary}`, () => {
@@ -32,23 +39,48 @@ describe('PathResolver', function () {
3239
const chunk1 = getMultiPartResponse(chunk1Data, boundary);
3340

3441
const chunk2Data = {
35-
path: ['viewer', 'currencies'],
36-
data: ['USD', 'GBP', 'EUR', 'CAD', 'AUD', 'CHF', '😂'], // test unicode
37-
errors: [{ message: 'Not So Bad Error' }],
42+
incremental: [
43+
{
44+
path: ['viewer', 'currencies'],
45+
data: ['USD', 'GBP', 'EUR', 'CAD', 'AUD', 'CHF', '😂'], // test unicode
46+
errors: [{ message: 'Not So Bad Error' }],
47+
},
48+
],
3849
};
3950
const chunk2 = getMultiPartResponse(chunk2Data, boundary);
4051

4152
const chunk3Data = {
42-
path: ['viewer', 'user', 'profile'],
43-
data: { displayName: 'Steven Seagal' },
53+
incremental: [
54+
{
55+
path: ['viewer', 'user', 'profile'],
56+
data: { displayName: 'Steven Seagal' },
57+
},
58+
],
4459
};
4560
const chunk3 = getMultiPartResponse(chunk3Data, boundary);
4661

4762
const chunk4Data = {
48-
data: false,
49-
path: ['viewer', 'user', 'items', 'edges', 1, 'node', 'isFavorite'],
63+
incremental: [
64+
{
65+
data: false,
66+
path: ['viewer', 'user', 'items', 'edges', 1, 'node', 'isFavorite'],
67+
},
68+
],
5069
};
5170
const chunk4 = getMultiPartResponse(chunk4Data, boundary);
71+
const chunk5Data = {
72+
incremental: [
73+
{
74+
data: true,
75+
path: ['viewer', 'user', 'items', 'edges', 2, 'node', 'isFavorite'],
76+
},
77+
{
78+
data: false,
79+
path: ['viewer', 'user', 'items', 'edges', 3, 'node', 'isFavorite'],
80+
},
81+
],
82+
};
83+
const chunk5 = getMultiPartResponse(chunk5Data, boundary);
5284
it('should work on each chunk', function () {
5385
const onResponse = jest.fn();
5486
const resolver = new PatchResolver({
@@ -61,19 +93,23 @@ describe('PathResolver', function () {
6193
expect(onResponse).not.toHaveBeenCalled();
6294

6395
resolver.handleChunk(chunk1);
64-
expect(onResponse.mock.calls[0][0]).toEqual([chunk1Data]);
96+
assertChunksRecieved(onResponse.mock.calls[0][0], [chunk1Data]);
6597

6698
onResponse.mockClear();
6799
resolver.handleChunk(chunk2);
68-
expect(onResponse.mock.calls[0][0]).toEqual([chunk2Data]);
100+
assertChunksRecieved(onResponse.mock.calls[0][0], [chunk2Data]);
69101

70102
onResponse.mockClear();
71103
resolver.handleChunk(chunk3);
72-
expect(onResponse.mock.calls[0][0]).toEqual([chunk3Data]);
104+
assertChunksRecieved(onResponse.mock.calls[0][0], [chunk3Data]);
73105

74106
onResponse.mockClear();
75107
resolver.handleChunk(chunk4);
76-
expect(onResponse.mock.calls[0][0]).toEqual([chunk4Data]);
108+
assertChunksRecieved(onResponse.mock.calls[0][0], [chunk4Data]);
109+
110+
onResponse.mockClear();
111+
resolver.handleChunk(chunk5);
112+
assertChunksRecieved(onResponse.mock.calls[0][0], [chunk5Data]);
77113
});
78114

79115
it('should work when chunks are split', function () {
@@ -96,7 +132,7 @@ describe('PathResolver', function () {
96132
resolver.handleChunk(chunk1b);
97133
expect(onResponse).not.toHaveBeenCalled();
98134
resolver.handleChunk(chunk1c);
99-
expect(onResponse.mock.calls[0][0]).toEqual([chunk1Data]);
135+
assertChunksRecieved(onResponse.mock.calls[0][0], [chunk1Data]);
100136
onResponse.mockClear();
101137

102138
const chunk2a = chunk2.substr(0, 35);
@@ -105,7 +141,7 @@ describe('PathResolver', function () {
105141
resolver.handleChunk(chunk2a);
106142
expect(onResponse).not.toHaveBeenCalled();
107143
resolver.handleChunk(chunk2b);
108-
expect(onResponse.mock.calls[0][0]).toEqual([chunk2Data]);
144+
assertChunksRecieved(onResponse.mock.calls[0][0], [chunk2Data]);
109145
onResponse.mockClear();
110146

111147
const chunk3a = chunk3.substr(0, 10);
@@ -117,7 +153,7 @@ describe('PathResolver', function () {
117153
resolver.handleChunk(chunk3b);
118154
expect(onResponse).not.toHaveBeenCalled();
119155
resolver.handleChunk(chunk3c);
120-
expect(onResponse.mock.calls[0][0]).toEqual([chunk3Data]);
156+
assertChunksRecieved(onResponse.mock.calls[0][0], [chunk3Data]);
121157
});
122158

123159
it('should work when chunks are combined', function () {
@@ -132,7 +168,7 @@ describe('PathResolver', function () {
132168
expect(onResponse).not.toHaveBeenCalled();
133169

134170
resolver.handleChunk(chunk1 + chunk2);
135-
expect(onResponse.mock.calls[0][0]).toEqual([chunk1Data, chunk2Data]);
171+
assertChunksRecieved(onResponse.mock.calls[0][0], [chunk1Data, chunk2Data]);
136172
});
137173

138174
it('should work when chunks are combined and split', function () {
@@ -159,13 +195,13 @@ describe('PathResolver', function () {
159195
expect(onResponse).not.toHaveBeenCalled();
160196

161197
resolver.handleChunk(chunk1 + chunk2 + chunk3a);
162-
expect(onResponse.mock.calls[0][0]).toEqual([chunk1Data, chunk2Data]);
198+
assertChunksRecieved(onResponse.mock.calls[0][0], [chunk1Data, chunk2Data]);
163199
onResponse.mockClear();
164200

165201
resolver.handleChunk(chunk3b);
166202
expect(onResponse).not.toHaveBeenCalled();
167203
resolver.handleChunk(chunk3c);
168-
expect(onResponse.mock.calls[0][0]).toEqual([chunk3Data]);
204+
assertChunksRecieved(onResponse.mock.calls[0][0], [chunk3Data]);
169205
});
170206

171207
it('should work when chunks are combined across boundaries', function () {
@@ -183,10 +219,10 @@ describe('PathResolver', function () {
183219
const chunk2b = chunk2.substring(35);
184220

185221
resolver.handleChunk(chunk1 + chunk2a);
186-
expect(onResponse.mock.calls[0][0]).toEqual([chunk1Data]);
222+
assertChunksRecieved(onResponse.mock.calls[0][0], [chunk1Data]);
187223
onResponse.mockClear();
188224
resolver.handleChunk(chunk2b);
189-
expect(onResponse.mock.calls[0][0]).toEqual([chunk2Data]);
225+
assertChunksRecieved(onResponse.mock.calls[0][0], [chunk2Data]);
190226
});
191227
it('should work when final chunk ends with terminating boundary', function () {
192228
const onResponse = jest.fn();
@@ -200,20 +236,20 @@ describe('PathResolver', function () {
200236
expect(onResponse).not.toHaveBeenCalled();
201237

202238
resolver.handleChunk(chunk1);
203-
expect(onResponse.mock.calls[0][0]).toEqual([chunk1Data]);
239+
assertChunksRecieved(onResponse.mock.calls[0][0], [chunk1Data]);
204240

205241
onResponse.mockClear();
206242
resolver.handleChunk(chunk2);
207-
expect(onResponse.mock.calls[0][0]).toEqual([chunk2Data]);
243+
assertChunksRecieved(onResponse.mock.calls[0][0], [chunk2Data]);
208244

209245
onResponse.mockClear();
210246
resolver.handleChunk(chunk3);
211-
expect(onResponse.mock.calls[0][0]).toEqual([chunk3Data]);
247+
assertChunksRecieved(onResponse.mock.calls[0][0], [chunk3Data]);
212248

213249
onResponse.mockClear();
214250
const chunk4FinalBoundary = getMultiPartResponse(chunk4Data, `${boundary}--`);
215251
resolver.handleChunk(chunk4FinalBoundary);
216-
expect(onResponse.mock.calls[0][0]).toEqual([chunk4Data]);
252+
assertChunksRecieved(onResponse.mock.calls[0][0], [chunk4Data]);
217253
});
218254

219255
it('should work with preamble', function () {
@@ -229,20 +265,20 @@ describe('PathResolver', function () {
229265
expect(onResponse).not.toHaveBeenCalled();
230266

231267
resolver.handleChunk(chunk1);
232-
expect(onResponse.mock.calls[0][0]).toEqual([chunk1Data]);
268+
assertChunksRecieved(onResponse.mock.calls[0][0], [chunk1Data]);
233269

234270
onResponse.mockClear();
235271
resolver.handleChunk(chunk2);
236-
expect(onResponse.mock.calls[0][0]).toEqual([chunk2Data]);
272+
assertChunksRecieved(onResponse.mock.calls[0][0], [chunk2Data]);
237273

238274
onResponse.mockClear();
239275
resolver.handleChunk(chunk3);
240-
expect(onResponse.mock.calls[0][0]).toEqual([chunk3Data]);
276+
assertChunksRecieved(onResponse.mock.calls[0][0], [chunk3Data]);
241277

242278
onResponse.mockClear();
243279
const chunk4FinalBoundary = getMultiPartResponse(chunk4Data, `${boundary}--`);
244280
resolver.handleChunk(chunk4FinalBoundary);
245-
expect(onResponse.mock.calls[0][0]).toEqual([chunk4Data]);
281+
assertChunksRecieved(onResponse.mock.calls[0][0], [chunk4Data]);
246282
});
247283
it('should work with epilogue', function () {
248284
const onResponse = jest.fn();
@@ -256,20 +292,20 @@ describe('PathResolver', function () {
256292
expect(onResponse).not.toHaveBeenCalled();
257293

258294
resolver.handleChunk(chunk1);
259-
expect(onResponse.mock.calls[0][0]).toEqual([chunk1Data]);
295+
assertChunksRecieved(onResponse.mock.calls[0][0], [chunk1Data]);
260296

261297
onResponse.mockClear();
262298
resolver.handleChunk(chunk2);
263-
expect(onResponse.mock.calls[0][0]).toEqual([chunk2Data]);
299+
assertChunksRecieved(onResponse.mock.calls[0][0], [chunk2Data]);
264300

265301
onResponse.mockClear();
266302
resolver.handleChunk(chunk3);
267-
expect(onResponse.mock.calls[0][0]).toEqual([chunk3Data]);
303+
assertChunksRecieved(onResponse.mock.calls[0][0], [chunk3Data]);
268304

269305
onResponse.mockClear();
270306
resolver.handleChunk(chunk4);
271307
resolver.handleChunk(`This is some epilogue data that should be ignored\r\n`);
272-
expect(onResponse.mock.calls[0][0]).toEqual([chunk4Data]);
308+
assertChunksRecieved(onResponse.mock.calls[0][0], [chunk4Data]);
273309
});
274310
it('should work with epilogue after chunk with terminating boundary', function () {
275311
const onResponse = jest.fn();
@@ -283,21 +319,21 @@ describe('PathResolver', function () {
283319
expect(onResponse).not.toHaveBeenCalled();
284320

285321
resolver.handleChunk(chunk1);
286-
expect(onResponse.mock.calls[0][0]).toEqual([chunk1Data]);
322+
assertChunksRecieved(onResponse.mock.calls[0][0], [chunk1Data]);
287323

288324
onResponse.mockClear();
289325
resolver.handleChunk(chunk2);
290-
expect(onResponse.mock.calls[0][0]).toEqual([chunk2Data]);
326+
assertChunksRecieved(onResponse.mock.calls[0][0], [chunk2Data]);
291327

292328
onResponse.mockClear();
293329
resolver.handleChunk(chunk3);
294-
expect(onResponse.mock.calls[0][0]).toEqual([chunk3Data]);
330+
assertChunksRecieved(onResponse.mock.calls[0][0], [chunk3Data]);
295331

296332
onResponse.mockClear();
297333
const chunk4FinalBoundary = getMultiPartResponse(chunk4Data, `${boundary}--`);
298334
resolver.handleChunk(chunk4FinalBoundary);
299335
resolver.handleChunk(`This is some epilogue data that should be ignored\r\n`);
300-
expect(onResponse.mock.calls[0][0]).toEqual([chunk4Data]);
336+
assertChunksRecieved(onResponse.mock.calls[0][0], [chunk4Data]);
301337
});
302338
});
303339
}

src/parseMultipartHttp.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,9 @@ export function parseMultipartHttp(buffer, boundary, previousParts = [], isPream
4747
// remove trailing boundary things
4848
body = body.replace(delimiter + '\r\n', '').replace(delimiter + '--\r\n', '');
4949

50-
const payload = JSON.parse(body);
51-
const parts = [...previousParts, payload];
50+
let payload = JSON.parse(body);
51+
payload = payload.incremental ? payload.incremental : [payload];
52+
const parts = [...previousParts, ...payload];
5253

5354
if (next && next.length) {
5455
// we have more parts

0 commit comments

Comments
 (0)