Skip to content

Commit

Permalink
Remove support for applyToPrevious (#7)
Browse files Browse the repository at this point in the history
* Remove support for `applyToPrevious`

All patches will be returned as provided by the server.

* return array of parts to onNext

* add prettier

* update tests and remove snapshots
  • Loading branch information
robrichard authored Mar 10, 2020
1 parent 2f7be4a commit 2860aea
Show file tree
Hide file tree
Showing 8 changed files with 119 additions and 750 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ function fetchQuery(operation, variables) {
variables,
}),
credentials: 'same-origin',
onNext: json => sink.next(json),
onNext: parts => sink.next(parts),
onError: err => sink.error(err),
onComplete: () => sink.complete(),
});
Expand Down
10 changes: 10 additions & 0 deletions prettier.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module.exports = {
bracketSpacing: true,
jsxBracketSameLine: false,
printWidth: 100,
requirePragma: false,
singleQuote: true,
tabWidth: 4,
trailingComma: 'es5',
useTabs: false,
};
63 changes: 2 additions & 61 deletions src/PatchResolver.js
Original file line number Diff line number Diff line change
@@ -1,45 +1,7 @@
import { parseMultipartHttp } from './parseMultipartHttp';

function insertPatch(obj, path, data) {
if (Array.isArray(obj) && typeof path === 'number') {
return [].concat(obj.slice(0, path), [data], obj.slice(path + 1));
} else {
return {
...obj,
[path]: data,
};
}
}

// recursive function to apply the patch to the previous response
function applyPatch(previousResponse, patchPath, patchData) {
const [nextPath, ...rest] = patchPath;
if (rest.length === 0) {
return insertPatch(previousResponse, nextPath, patchData);
}
return insertPatch(
previousResponse,
nextPath,
applyPatch(previousResponse[nextPath], rest, patchData)
);
}

function mergeErrors(previousErrors, patchErrors) {
if (previousErrors && patchErrors) {
return [].concat(previousErrors, patchErrors);
} else if (previousErrors) {
return previousErrors;
} else if (patchErrors) {
return patchErrors;
}
return undefined;
}

export function PatchResolver({ onResponse, applyToPrevious, mergeExtensions = () => {} }) {
this.applyToPrevious = typeof applyToPrevious === 'boolean' ? applyToPrevious : true;
export function PatchResolver({ onResponse }) {
this.onResponse = onResponse;
this.mergeExtensions = mergeExtensions;
this.previousResponse = null;
this.processedChunks = 0;
this.chunkBuffer = '';
}
Expand All @@ -49,27 +11,6 @@ PatchResolver.prototype.handleChunk = function(data) {
const { newBuffer, parts } = parseMultipartHttp(this.chunkBuffer);
this.chunkBuffer = newBuffer;
if (parts.length) {
if (this.applyToPrevious) {
parts.forEach(part => {
if (this.processedChunks === 0) {
this.previousResponse = part;
} else {
if (!(Array.isArray(part.path) && typeof part.data !== 'undefined')) {
throw new Error('invalid patch format ' + JSON.stringify(part, null, 2));
}
this.previousResponse = {
...this.previousResponse,
data: applyPatch(this.previousResponse.data, part.path, part.data),
errors: mergeErrors(this.previousResponse.errors, part.errors),
extensions: this.mergeExtensions(this.previousResponse.extensions, part.extensions),
};
}
this.processedChunks += 1;
});
// don't need to re-trigger every intermediate state
this.onResponse(this.previousResponse);
} else {
parts.forEach(part => this.onResponse(part));
}
this.onResponse(parts);
}
};
155 changes: 55 additions & 100 deletions src/__test__/PatchResolver.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,61 +4,49 @@ import { TextEncoder, TextDecoder } from 'util';
global.TextEncoder = TextEncoder;
global.TextDecoder = TextDecoder;

const chunk1 = [
'',
'---',
'Content-Type: application/json',
'Content-Length: 142',
'',
'{"data":{"viewer":{"currencies":null,"user":{"profile":null,"items":{"edges":[{"node":{"isFavorite":null}},{"node":{"isFavorite":null}}]}}}}}\n',
].join('\r\n');

const chunk1error = [
'',
'---',
'Content-Type: application/json',
'Content-Length: 104',
'',
'{"data":{"viewer":{"currencies":null,"user":{"profile":null}}},"errors":[{"message":"Very Bad Error"}]}\n',
].join('\r\n');

const chunk2 = [
'',
'---',
'Content-Type: application/json',
'Content-Length: 85',
'',
'{"path":["viewer","currencies"],"data":["USD","GBP","EUR","CAD","AUD","CHF","😂"]}\n', // test unicode
].join('\r\n');

const chunk2error = [
'',
'---',
'Content-Type: application/json',
'Content-Length: 127',
'',
'{"path":["viewer","currencies"],"data":["USD","GBP","EUR","CAD","AUD","CHF","😂"],"errors":[{"message":"Not So Bad Error"}]}\n',
].join('\r\n');

const chunk3 = [
'',
'---',
'Content-Type: application/json',
'Content-Length: 76',
'',
'{"path":["viewer","user","profile"],"data":{"displayName":"Steven Seagal"}}\n',
].join('\r\n');

const chunk4 = [
'',
'---',
'Content-Type: application/json',
'Content-Length: 78',
'',
'{"data":false,"path":["viewer","user","items","edges",1,"node","isFavorite"]}\n',
'',
'-----\r\n',
].join('\r\n');
function getMultiPartResponse(data) {
const json = JSON.stringify(data);
const chunk = Buffer.from(json, 'utf8');

return [
'',
'---',
'Content-Type: application/json',
`Content-Length: ${String(chunk.length)}`,
'',
json,
'',
].join('\r\n');
}

const chunk1Data = {
data: {
viewer: {
currencies: null,
user: {
profile: null,
items: { edges: [{ node: { isFavorite: null } }, { node: { isFavorite: null } }] },
},
},
},
};
const chunk1 = getMultiPartResponse(chunk1Data);

const chunk2Data = {
path: ['viewer', 'currencies'],
data: ['USD', 'GBP', 'EUR', 'CAD', 'AUD', 'CHF', '😂'], // test unicode
errors: [{ message: 'Not So Bad Error' }],
};
const chunk2 = getMultiPartResponse(chunk2Data);

const chunk3Data = { path: ['viewer', 'user', 'profile'], data: { displayName: 'Steven Seagal' } };
const chunk3 = getMultiPartResponse(chunk3Data);

const chunk4Data = {
data: false,
path: ['viewer', 'user', 'items', 'edges', 1, 'node', 'isFavorite'],
};
const chunk4 = getMultiPartResponse(chunk4Data);

describe('PathResolver', function() {
it('should work on each chunk', function() {
Expand All @@ -68,19 +56,19 @@ describe('PathResolver', function() {
});

resolver.handleChunk(chunk1);
expect(onResponse.mock.calls[0][0]).toMatchSnapshot();
expect(onResponse.mock.calls[0][0]).toEqual([chunk1Data]);

onResponse.mockClear();
resolver.handleChunk(chunk2);
expect(onResponse.mock.calls[0][0]).toMatchSnapshot();
expect(onResponse.mock.calls[0][0]).toEqual([chunk2Data]);

onResponse.mockClear();
resolver.handleChunk(chunk3);
expect(onResponse.mock.calls[0][0]).toMatchSnapshot();
expect(onResponse.mock.calls[0][0]).toEqual([chunk3Data]);

onResponse.mockClear();
resolver.handleChunk(chunk4);
expect(onResponse.mock.calls[0][0]).toMatchSnapshot();
expect(onResponse.mock.calls[0][0]).toEqual([chunk4Data]);
});

it('should work when chunks are split', function() {
Expand All @@ -98,7 +86,7 @@ describe('PathResolver', function() {
resolver.handleChunk(chunk1b);
expect(onResponse).not.toHaveBeenCalled();
resolver.handleChunk(chunk1c);
expect(onResponse.mock.calls[0][0]).toMatchSnapshot();
expect(onResponse.mock.calls[0][0]).toEqual([chunk1Data]);
onResponse.mockClear();

const chunk2a = chunk2.substr(0, 35);
Expand All @@ -107,7 +95,7 @@ describe('PathResolver', function() {
resolver.handleChunk(chunk2a);
expect(onResponse).not.toHaveBeenCalled();
resolver.handleChunk(chunk2b);
expect(onResponse.mock.calls[0][0]).toMatchSnapshot();
expect(onResponse.mock.calls[0][0]).toEqual([chunk2Data]);
onResponse.mockClear();

const chunk3a = chunk3.substr(0, 10);
Expand All @@ -119,7 +107,7 @@ describe('PathResolver', function() {
resolver.handleChunk(chunk3b);
expect(onResponse).not.toHaveBeenCalled();
resolver.handleChunk(chunk3c);
expect(onResponse.mock.calls[0][0]).toMatchSnapshot();
expect(onResponse.mock.calls[0][0]).toEqual([chunk3Data]);
});

it('should work when chunks are combined', function() {
Expand All @@ -129,7 +117,7 @@ describe('PathResolver', function() {
});

resolver.handleChunk(chunk1 + chunk2);
expect(onResponse.mock.calls[0][0]).toMatchSnapshot();
expect(onResponse.mock.calls[0][0]).toEqual([chunk1Data, chunk2Data]);
});

it('should work when chunks are combined and split', function() {
Expand All @@ -143,13 +131,13 @@ describe('PathResolver', function() {
const chunk3c = chunk3.substr(11 + 20);

resolver.handleChunk(chunk1 + chunk2 + chunk3a);
expect(onResponse.mock.calls[0][0]).toMatchSnapshot();
expect(onResponse.mock.calls[0][0]).toEqual([chunk1Data, chunk2Data]);
onResponse.mockClear();

resolver.handleChunk(chunk3b);
expect(onResponse).not.toHaveBeenCalled();
resolver.handleChunk(chunk3c);
expect(onResponse.mock.calls[0][0]).toMatchSnapshot();
expect(onResponse.mock.calls[0][0]).toEqual([chunk3Data]);
});

it('should work when chunks are combined across boundaries', function() {
Expand All @@ -162,42 +150,9 @@ describe('PathResolver', function() {
const chunk2b = chunk2.substring(35);

resolver.handleChunk(chunk1 + chunk2a);
expect(onResponse.mock.calls[0][0]).toMatchSnapshot();
expect(onResponse.mock.calls[0][0]).toEqual([chunk1Data]);
onResponse.mockClear();
resolver.handleChunk(chunk2b);
expect(onResponse.mock.calls[0][0]).toMatchSnapshot();
});

it('should merge errors', function() {
const onResponse = jest.fn();
const resolver = new PatchResolver({
onResponse,
});

resolver.handleChunk(chunk1error);
expect(onResponse.mock.calls[0][0]).toMatchSnapshot();
onResponse.mockClear();
resolver.handleChunk(chunk2error);
expect(onResponse.mock.calls[0][0]).toMatchSnapshot();
onResponse.mockClear();
resolver.handleChunk(chunk3);
expect(onResponse.mock.calls[0][0]).toMatchSnapshot();
});

it('should work when not applying to previous', function() {
const onResponse = jest.fn();
const resolver = new PatchResolver({
onResponse,
applyToPrevious: false,
});

const chunk2a = chunk2.substring(0, 35);
const chunk2b = chunk2.substring(35);

resolver.handleChunk(chunk1 + chunk2a);
expect(onResponse.mock.calls[0][0]).toMatchSnapshot();
onResponse.mockClear();
resolver.handleChunk(chunk2b);
expect(onResponse.mock.calls[0][0]).toMatchSnapshot();
expect(onResponse.mock.calls[0][0]).toEqual([chunk2Data]);
});
});
Loading

0 comments on commit 2860aea

Please sign in to comment.