Skip to content

Commit

Permalink
feat(core): Update defer/stream protocol as per spec (#3389)
Browse files Browse the repository at this point in the history
Co-authored-by: Phil Pluckthun <phil@kitten.sh>
  • Loading branch information
JoviDeCroock and kitten authored Sep 26, 2023
1 parent 35a7d54 commit 052d5f4
Show file tree
Hide file tree
Showing 5 changed files with 172 additions and 5 deletions.
5 changes: 5 additions & 0 deletions .changeset/fast-jeans-pay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@urql/core": patch
---

Implement new `@defer` / `@stream` transport protocol spec changes.
8 changes: 7 additions & 1 deletion packages/core/src/internal/fetchSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,9 +175,15 @@ async function* fetchOperation(
throw new Error(await response.text());
}

let pending: ExecutionResult['pending'];
for await (const payload of results) {
if (payload.pending && !result) {
pending = payload.pending;
} else if (payload.pending) {
pending = [...pending!, ...payload.pending];
}
result = result
? mergeResultPatch(result, payload, response)
? mergeResultPatch(result, payload, response, pending)
: makeResult(operation, payload, response);
networkMode = false;
yield result;
Expand Down
27 changes: 26 additions & 1 deletion packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ export interface RequestExtensions {
[extension: string]: any;
}

type Path = readonly (string | number)[];

/** Incremental Payloads sent as part of "Incremental Delivery" patching prior result data.
*
* @remarks
Expand All @@ -139,7 +141,16 @@ export interface IncrementalPayload {
* entry of the `path` will be an index number at which to start setting the range of
* items.
*/
path: readonly (string | number)[];
path?: Path;
/** An id pointing at an entry in the "pending" set of deferred results
*
* @remarks
* When we resolve this id it will give us the path to the deferred Fragment, this
* can be afterwards combined with the subPath to get the eventual location of the data.
*/
id?: string;
/** A path array from the defer/stream fragment to the location of our data. */
subPath?: Path;
/** Data to patch into the result data at the given `path`.
*
* @remarks
Expand Down Expand Up @@ -172,7 +183,21 @@ export interface IncrementalPayload {
extensions?: Extensions;
}

type PendingIncrementalResult = {
path: Path;
id: string;
label?: string;
};

export interface ExecutionResult {
/** Payloads we are still waiting for from the server.
*
* @remarks
* This was nely introduced in the defer/stream spec iteration of June 2023 https://github.com/graphql/defer-stream-wg/discussions/69
* Pending can be present on both Incremental as well as normal execution results, the presence of pending on an incremental
* result points at a nested deferred/streamed fragment.
*/
pending?: readonly PendingIncrementalResult[];
/** Incremental patches to be applied to a previous result as part of "Incremental Delivery".
*
* @remarks
Expand Down
120 changes: 119 additions & 1 deletion packages/core/src/utils/result.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,125 @@ describe('makeResult', () => {
});
});

describe('mergeResultPatch', () => {
describe('mergeResultPatch (defer/stream latest', () => {
it('should read pending and append the result', () => {
const pending = [{ id: '0', path: [] }];
const prevResult: OperationResult = {
operation: queryOperation,
stale: false,
hasNext: true,
data: {
f2: {
a: 'a',
b: 'b',
c: {
d: 'd',
e: 'e',
f: { h: 'h', i: 'i' },
},
},
},
};

const merged = mergeResultPatch(
prevResult,
{
incremental: [
{ id: '0', data: { MyFragment: 'Query' } },
{ id: '0', subPath: ['f2', 'c', 'f'], data: { j: 'j' } },
],
// TODO: not sure if we need this but it's part of the spec
// completed: [{ id: '0' }],
hasNext: false,
},
undefined,
pending
);

expect(merged.data).toEqual({
MyFragment: 'Query',
f2: {
a: 'a',
b: 'b',
c: {
d: 'd',
e: 'e',
f: { h: 'h', i: 'i', j: 'j' },
},
},
});
});

it('should read pending and append the result w/ overlapping fields', () => {
const pending = [
{ id: '0', path: [], label: 'D1' },
{ id: '1', path: ['f2', 'c', 'f'], label: 'D2' },
];
const prevResult: OperationResult = {
operation: queryOperation,
stale: false,
hasNext: true,
data: {
f2: {
a: 'A',
b: 'B',
c: {
d: 'D',
e: 'E',
f: {
h: 'H',
i: 'I',
},
},
},
},
};

const merged = mergeResultPatch(
prevResult,
{
incremental: [
{ id: '0', subPath: ['f2', 'c', 'f'], data: { j: 'J', k: 'K' } },
],
pending: [{ id: '1', path: ['f2', 'c', 'f'], label: 'D2' }],
hasNext: true,
},
undefined,
pending
);

const merged2 = mergeResultPatch(
merged,
{
incremental: [{ id: '1', data: { l: 'L', m: 'M' } }],
hasNext: false,
},
undefined,
pending
);

expect(merged2.data).toEqual({
f2: {
a: 'A',
b: 'B',
c: {
d: 'D',
e: 'E',
f: {
h: 'H',
i: 'I',
j: 'J',
k: 'K',
l: 'L',
m: 'M',
},
},
},
});
});
});

describe('mergeResultPatch (defer/stream pre June-2023)', () => {
it('should default hasNext to true if the last result was set to true', () => {
const prevResult: OperationResult = {
operation: subscriptionOperation,
Expand Down
17 changes: 15 additions & 2 deletions packages/core/src/utils/result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,8 @@ const deepMerge = (target: any, source: any) => {
export const mergeResultPatch = (
prevResult: OperationResult,
nextResult: ExecutionResult,
response?: any
response?: any,
pending?: ExecutionResult['pending']
): OperationResult => {
let errors = prevResult.error ? prevResult.error.graphQLErrors : [];
let hasExtensions = !!prevResult.extensions || !!nextResult.extensions;
Expand All @@ -112,7 +113,19 @@ export const mergeResultPatch = (

let prop: string | number = 'data';
let part: Record<string, any> | Array<any> = withData;
for (let i = 0, l = patch.path.length; i < l; prop = patch.path[i++]) {
let path: readonly (string | number)[] = [];
if (patch.path) {
path = patch.path;
} else if (pending) {
const res = pending.find(pendingRes => pendingRes.id === patch.id);
if (patch.subPath) {
path = [...res!.path, ...patch.subPath];
} else {
path = res!.path;
}
}

for (let i = 0, l = path.length; i < l; prop = path[i++]) {
part = part[prop] = Array.isArray(part[prop])
? [...part[prop]]
: { ...part[prop] };
Expand Down

0 comments on commit 052d5f4

Please sign in to comment.