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.

fix(core): Fix immediate cancellation of fetch requests (#3076)

authored by

Phil Pluckthun and committed by
GitHub
8b1d1641 59f43ca6

+46 -17
+20 -8
packages/core/src/exchanges/fetch.test.ts
··· 147 147 expect(true).toEqual(false); 148 148 }; 149 149 150 - it('does not start the outgoing request on immediate teardowns', () => { 151 - fetch.mockResolvedValue(new Response('text', { status: 200 })); 150 + it('does not start the outgoing request on immediate teardowns', async () => { 151 + fetch.mockImplementation(async () => { 152 + await new Promise(() => { 153 + /*noop*/ 154 + }); 155 + }); 152 156 153 157 const { unsubscribe } = pipe( 154 158 fromValue(queryOperation), ··· 157 161 ); 158 162 159 163 unsubscribe(); 164 + 165 + // NOTE: We can only observe the async iterator's final run after a macro tick 166 + await new Promise(resolve => setTimeout(resolve)); 160 167 expect(fetch).toHaveBeenCalledTimes(0); 161 - expect(abort).toHaveBeenCalledTimes(0); 168 + expect(abort).toHaveBeenCalledTimes(1); 162 169 }); 163 170 164 171 it('aborts the outgoing request', async () => { 165 - fetch.mockResolvedValue(new Response('text', { status: 200 })); 172 + fetch.mockResolvedValue({ 173 + status: 200, 174 + headers: new Map([['Content-Type', 'application/json']]), 175 + text: vi.fn().mockResolvedValue('{ "data": null }'), 176 + }); 166 177 167 178 const { unsubscribe } = pipe( 168 179 fromValue(queryOperation), 169 180 fetchExchange(exchangeArgs), 170 - subscribe(fail) 181 + subscribe(() => { 182 + /*noop*/ 183 + }) 171 184 ); 172 185 173 - await Promise.resolve(); 174 - 186 + await new Promise(resolve => setTimeout(resolve)); 175 187 unsubscribe(); 176 188 177 189 // NOTE: We can only observe the async iterator's final run after a macro tick 178 190 await new Promise(resolve => setTimeout(resolve)); 179 191 expect(fetch).toHaveBeenCalledTimes(1); 180 - expect(abort).toHaveBeenCalled(); 192 + expect(abort).toHaveBeenCalledTimes(1); 181 193 }); 182 194 183 195 it('does not call the query', () => {
+20 -6
packages/core/src/internal/fetchSource.test.ts
··· 164 164 expect(true).toEqual(false); 165 165 }; 166 166 167 - it('does not start the outgoing request on immediate teardowns', () => { 168 - fetch.mockResolvedValue(new Response('text', { status: 200 })); 167 + it('does not start the outgoing request on immediate teardowns', async () => { 168 + fetch.mockImplementation(async () => { 169 + await new Promise(() => { 170 + /*noop*/ 171 + }); 172 + }); 169 173 170 174 const { unsubscribe } = pipe( 171 175 makeFetchSource(queryOperation, 'https://test.com/graphql', {}), ··· 173 177 ); 174 178 175 179 unsubscribe(); 180 + 181 + // NOTE: We can only observe the async iterator's final run after a macro tick 182 + 183 + await new Promise(resolve => setTimeout(resolve)); 176 184 expect(fetch).toHaveBeenCalledTimes(0); 177 - expect(abort).toHaveBeenCalledTimes(0); 185 + expect(abort).toHaveBeenCalledTimes(1); 178 186 }); 179 187 180 188 it('aborts the outgoing request', async () => { 181 - fetch.mockResolvedValue(new Response('text', { status: 200 })); 189 + fetch.mockResolvedValue({ 190 + status: 200, 191 + headers: new Map([['Content-Type', 'application/json']]), 192 + text: vi.fn().mockResolvedValue('{ "data": null }'), 193 + }); 182 194 183 195 const { unsubscribe } = pipe( 184 196 makeFetchSource(queryOperation, 'https://test.com/graphql', {}), 185 - subscribe(fail) 197 + subscribe(() => { 198 + /*noop*/ 199 + }) 186 200 ); 187 201 188 - await Promise.resolve(); 202 + await new Promise(resolve => setTimeout(resolve)); 189 203 unsubscribe(); 190 204 191 205 // NOTE: We can only observe the async iterator's final run after a macro tick
+6 -3
packages/core/src/internal/fetchSource.ts
··· 1 - import { Source, fromAsyncIterable } from 'wonka'; 1 + import { Source, fromAsyncIterable, filter, pipe } from 'wonka'; 2 2 import { Operation, OperationResult, ExecutionResult } from '../types'; 3 3 import { makeResult, makeErrorResult, mergeResultPatch } from '../utils'; 4 4 ··· 116 116 117 117 // Delay for a tick to give the Client a chance to cancel the request 118 118 // if a teardown comes in immediately 119 - await Promise.resolve(); 119 + yield await Promise.resolve(); 120 120 121 121 response = await (operation.context.fetch || fetch)(url, fetchOptions); 122 122 const contentType = response.headers.get('Content-Type') || ''; ··· 194 194 url: string, 195 195 fetchOptions: RequestInit 196 196 ): Source<OperationResult> { 197 - return fromAsyncIterable(fetchOperation(operation, url, fetchOptions)); 197 + return pipe( 198 + fromAsyncIterable(fetchOperation(operation, url, fetchOptions)), 199 + filter((result): result is OperationResult => !!result) 200 + ); 198 201 }