Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

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

Co-authored-by: Phil Pluckthun <phil@kitten.sh>

authored by

Jovi De Croock
Phil Pluckthun
and committed by
GitHub
052d5f49 35a7d540

+172 -5
+5
.changeset/fast-jeans-pay.md
··· 1 + --- 2 + "@urql/core": patch 3 + --- 4 + 5 + Implement new `@defer` / `@stream` transport protocol spec changes.
+7 -1
packages/core/src/internal/fetchSource.ts
··· 175 175 throw new Error(await response.text()); 176 176 } 177 177 178 + let pending: ExecutionResult['pending']; 178 179 for await (const payload of results) { 180 + if (payload.pending && !result) { 181 + pending = payload.pending; 182 + } else if (payload.pending) { 183 + pending = [...pending!, ...payload.pending]; 184 + } 179 185 result = result 180 - ? mergeResultPatch(result, payload, response) 186 + ? mergeResultPatch(result, payload, response, pending) 181 187 : makeResult(operation, payload, response); 182 188 networkMode = false; 183 189 yield result;
+26 -1
packages/core/src/types.ts
··· 114 114 [extension: string]: any; 115 115 } 116 116 117 + type Path = readonly (string | number)[]; 118 + 117 119 /** Incremental Payloads sent as part of "Incremental Delivery" patching prior result data. 118 120 * 119 121 * @remarks ··· 139 141 * entry of the `path` will be an index number at which to start setting the range of 140 142 * items. 141 143 */ 142 - path: readonly (string | number)[]; 144 + path?: Path; 145 + /** An id pointing at an entry in the "pending" set of deferred results 146 + * 147 + * @remarks 148 + * When we resolve this id it will give us the path to the deferred Fragment, this 149 + * can be afterwards combined with the subPath to get the eventual location of the data. 150 + */ 151 + id?: string; 152 + /** A path array from the defer/stream fragment to the location of our data. */ 153 + subPath?: Path; 143 154 /** Data to patch into the result data at the given `path`. 144 155 * 145 156 * @remarks ··· 172 183 extensions?: Extensions; 173 184 } 174 185 186 + type PendingIncrementalResult = { 187 + path: Path; 188 + id: string; 189 + label?: string; 190 + }; 191 + 175 192 export interface ExecutionResult { 193 + /** Payloads we are still waiting for from the server. 194 + * 195 + * @remarks 196 + * This was nely introduced in the defer/stream spec iteration of June 2023 https://github.com/graphql/defer-stream-wg/discussions/69 197 + * Pending can be present on both Incremental as well as normal execution results, the presence of pending on an incremental 198 + * result points at a nested deferred/streamed fragment. 199 + */ 200 + pending?: readonly PendingIncrementalResult[]; 176 201 /** Incremental patches to be applied to a previous result as part of "Incremental Delivery". 177 202 * 178 203 * @remarks
+119 -1
packages/core/src/utils/result.test.ts
··· 38 38 }); 39 39 }); 40 40 41 - describe('mergeResultPatch', () => { 41 + describe('mergeResultPatch (defer/stream latest', () => { 42 + it('should read pending and append the result', () => { 43 + const pending = [{ id: '0', path: [] }]; 44 + const prevResult: OperationResult = { 45 + operation: queryOperation, 46 + stale: false, 47 + hasNext: true, 48 + data: { 49 + f2: { 50 + a: 'a', 51 + b: 'b', 52 + c: { 53 + d: 'd', 54 + e: 'e', 55 + f: { h: 'h', i: 'i' }, 56 + }, 57 + }, 58 + }, 59 + }; 60 + 61 + const merged = mergeResultPatch( 62 + prevResult, 63 + { 64 + incremental: [ 65 + { id: '0', data: { MyFragment: 'Query' } }, 66 + { id: '0', subPath: ['f2', 'c', 'f'], data: { j: 'j' } }, 67 + ], 68 + // TODO: not sure if we need this but it's part of the spec 69 + // completed: [{ id: '0' }], 70 + hasNext: false, 71 + }, 72 + undefined, 73 + pending 74 + ); 75 + 76 + expect(merged.data).toEqual({ 77 + MyFragment: 'Query', 78 + f2: { 79 + a: 'a', 80 + b: 'b', 81 + c: { 82 + d: 'd', 83 + e: 'e', 84 + f: { h: 'h', i: 'i', j: 'j' }, 85 + }, 86 + }, 87 + }); 88 + }); 89 + 90 + it('should read pending and append the result w/ overlapping fields', () => { 91 + const pending = [ 92 + { id: '0', path: [], label: 'D1' }, 93 + { id: '1', path: ['f2', 'c', 'f'], label: 'D2' }, 94 + ]; 95 + const prevResult: OperationResult = { 96 + operation: queryOperation, 97 + stale: false, 98 + hasNext: true, 99 + data: { 100 + f2: { 101 + a: 'A', 102 + b: 'B', 103 + c: { 104 + d: 'D', 105 + e: 'E', 106 + f: { 107 + h: 'H', 108 + i: 'I', 109 + }, 110 + }, 111 + }, 112 + }, 113 + }; 114 + 115 + const merged = mergeResultPatch( 116 + prevResult, 117 + { 118 + incremental: [ 119 + { id: '0', subPath: ['f2', 'c', 'f'], data: { j: 'J', k: 'K' } }, 120 + ], 121 + pending: [{ id: '1', path: ['f2', 'c', 'f'], label: 'D2' }], 122 + hasNext: true, 123 + }, 124 + undefined, 125 + pending 126 + ); 127 + 128 + const merged2 = mergeResultPatch( 129 + merged, 130 + { 131 + incremental: [{ id: '1', data: { l: 'L', m: 'M' } }], 132 + hasNext: false, 133 + }, 134 + undefined, 135 + pending 136 + ); 137 + 138 + expect(merged2.data).toEqual({ 139 + f2: { 140 + a: 'A', 141 + b: 'B', 142 + c: { 143 + d: 'D', 144 + e: 'E', 145 + f: { 146 + h: 'H', 147 + i: 'I', 148 + j: 'J', 149 + k: 'K', 150 + l: 'L', 151 + m: 'M', 152 + }, 153 + }, 154 + }, 155 + }); 156 + }); 157 + }); 158 + 159 + describe('mergeResultPatch (defer/stream pre June-2023)', () => { 42 160 it('should default hasNext to true if the last result was set to true', () => { 43 161 const prevResult: OperationResult = { 44 162 operation: subscriptionOperation,
+15 -2
packages/core/src/utils/result.ts
··· 85 85 export const mergeResultPatch = ( 86 86 prevResult: OperationResult, 87 87 nextResult: ExecutionResult, 88 - response?: any 88 + response?: any, 89 + pending?: ExecutionResult['pending'] 89 90 ): OperationResult => { 90 91 let errors = prevResult.error ? prevResult.error.graphQLErrors : []; 91 92 let hasExtensions = !!prevResult.extensions || !!nextResult.extensions; ··· 112 113 113 114 let prop: string | number = 'data'; 114 115 let part: Record<string, any> | Array<any> = withData; 115 - for (let i = 0, l = patch.path.length; i < l; prop = patch.path[i++]) { 116 + let path: readonly (string | number)[] = []; 117 + if (patch.path) { 118 + path = patch.path; 119 + } else if (pending) { 120 + const res = pending.find(pendingRes => pendingRes.id === patch.id); 121 + if (patch.subPath) { 122 + path = [...res!.path, ...patch.subPath]; 123 + } else { 124 + path = res!.path; 125 + } 126 + } 127 + 128 + for (let i = 0, l = path.length; i < l; prop = path[i++]) { 116 129 part = part[prop] = Array.isArray(part[prop]) 117 130 ? [...part[prop]] 118 131 : { ...part[prop] };