Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): Update defer/stream protocol as per spec #3389

Merged
merged 8 commits into from
Sep 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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];
}
Comment on lines +180 to +184
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initially pending will be present on the ExecutionResult, however when we have nested incremental results then the IncrementalResult will contain new pending information which we have to add to our array here so we can keep building on our result

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