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): Force client.reexecuteOperation to dispatch (#3363)

authored by

Phil Pluckthun and committed by
GitHub
64315114 d175d481

+190 -1
+5
.changeset/wicked-seahorses-smash.md
··· 1 + --- 2 + '@urql/core': patch 3 + --- 4 + 5 + Explicitly unblock `client.reexecuteOperation` calls to allow stalled operations from continuing and re-executing. Previously, this could cause `@urql/exchange-graphcache` to stall if an optimistic mutation led to a cache miss.
+139
exchanges/graphcache/src/cacheExchange.test.ts
··· 1257 1257 expect(updates.Mutation.addAuthor).toHaveBeenCalledTimes(2); 1258 1258 expect(response).toHaveBeenCalledTimes(2); 1259 1259 expect(result).toHaveBeenCalledTimes(4); 1260 + expect(reexec).toHaveBeenCalledTimes(2); 1261 + 1262 + next(opOne); 1263 + vi.runAllTimers(); 1264 + expect(result).toHaveBeenCalledTimes(5); 1265 + }); 1266 + 1267 + it('does not block subsequent query operations', () => { 1268 + vi.useFakeTimers(); 1269 + 1270 + const authorsQuery = gql` 1271 + query { 1272 + authors { 1273 + id 1274 + name 1275 + } 1276 + } 1277 + `; 1278 + 1279 + const authorsQueryData = { 1280 + __typename: 'Query', 1281 + authors: [ 1282 + { 1283 + __typename: 'Author', 1284 + id: '123', 1285 + name: 'Author', 1286 + }, 1287 + ], 1288 + }; 1289 + 1290 + const mutation = gql` 1291 + mutation { 1292 + deleteAuthor { 1293 + id 1294 + name 1295 + } 1296 + } 1297 + `; 1298 + 1299 + const optimisticMutationData = { 1300 + __typename: 'Mutation', 1301 + deleteAuthor: { 1302 + __typename: 'Author', 1303 + id: '123', 1304 + name: '[REDACTED OFFLINE]', 1305 + }, 1306 + }; 1307 + 1308 + const client = createClient({ 1309 + url: 'http://0.0.0.0', 1310 + exchanges: [], 1311 + }); 1312 + const { source: ops$, next } = makeSubject<Operation>(); 1313 + 1314 + const reexec = vi 1315 + .spyOn(client, 'reexecuteOperation') 1316 + .mockImplementation(next); 1317 + 1318 + const opOne = client.createRequestOperation('query', { 1319 + key: 1, 1320 + query: authorsQuery, 1321 + variables: undefined, 1322 + }); 1323 + 1324 + const opMutation = client.createRequestOperation('mutation', { 1325 + key: 2, 1326 + query: mutation, 1327 + variables: undefined, 1328 + }); 1329 + 1330 + const response = vi.fn((forwardOp: Operation): OperationResult => { 1331 + if (forwardOp.key === 1) { 1332 + return { ...queryResponse, operation: opOne, data: authorsQueryData }; 1333 + } else if (forwardOp.key === 2) { 1334 + return { 1335 + ...queryResponse, 1336 + operation: opMutation, 1337 + data: { 1338 + __typename: 'Mutation', 1339 + deleteAuthor: optimisticMutationData.deleteAuthor, 1340 + }, 1341 + }; 1342 + } 1343 + 1344 + return undefined as any; 1345 + }); 1346 + 1347 + const result = vi.fn(); 1348 + const forward: ExchangeIO = ops$ => 1349 + pipe(ops$, delay(1), map(response), share); 1350 + 1351 + const optimistic = { 1352 + deleteAuthor: vi.fn(() => optimisticMutationData.deleteAuthor) as any, 1353 + }; 1354 + 1355 + const updates = { 1356 + Mutation: { 1357 + deleteAuthor: vi.fn((_data, _, cache) => { 1358 + cache.invalidate({ 1359 + __typename: 'Author', 1360 + id: optimisticMutationData.deleteAuthor.id, 1361 + }); 1362 + }), 1363 + }, 1364 + }; 1365 + 1366 + pipe( 1367 + cacheExchange({ optimistic, updates })({ 1368 + forward, 1369 + client, 1370 + dispatchDebug, 1371 + })(ops$), 1372 + tap(result), 1373 + publish 1374 + ); 1375 + 1376 + next(opOne); 1377 + vi.runAllTimers(); 1378 + expect(response).toHaveBeenCalledTimes(1); 1379 + expect(result).toHaveBeenCalledTimes(1); 1380 + 1381 + next(opMutation); 1382 + expect(response).toHaveBeenCalledTimes(1); 1383 + expect(optimistic.deleteAuthor).toHaveBeenCalledTimes(1); 1384 + expect(updates.Mutation.deleteAuthor).toHaveBeenCalledTimes(1); 1385 + expect(reexec).toHaveBeenCalledTimes(1); 1386 + expect(result).toHaveBeenCalledTimes(1); 1387 + 1388 + vi.runAllTimers(); 1389 + 1390 + expect(updates.Mutation.deleteAuthor).toHaveBeenCalledTimes(2); 1391 + expect(response).toHaveBeenCalledTimes(2); 1392 + expect(result).toHaveBeenCalledTimes(2); 1393 + expect(reexec).toHaveBeenCalledTimes(2); 1394 + expect(reexec.mock.calls[1][0]).toMatchObject(opOne); 1395 + 1396 + next(opOne); 1397 + vi.runAllTimers(); 1398 + expect(result).toHaveBeenCalledTimes(3); 1260 1399 }); 1261 1400 }); 1262 1401
+2 -1
exchanges/graphcache/src/store/data.test.ts
··· 13 13 it('erases orphaned entities', () => { 14 14 InMemoryData.writeRecord('Todo:1', '__typename', 'Todo'); 15 15 InMemoryData.writeRecord('Todo:1', 'id', '1'); 16 + InMemoryData.writeRecord('Todo:2', '__typename', 'Todo'); 16 17 InMemoryData.writeRecord('Query', '__typename', 'Query'); 17 18 InMemoryData.writeLink('Query', 'todo', 'Todo:1'); 18 19 ··· 27 28 expect(InMemoryData.readRecord('Todo:1', 'id')).toBe(undefined); 28 29 29 30 expect(InMemoryData.getCurrentDependencies()).toEqual( 30 - new Set(['Todo:1', 'Query.todo']) 31 + new Set(['Todo:1', 'Todo:2', 'Query.todo']) 31 32 ); 32 33 }); 33 34
+40
packages/core/src/client.test.ts
··· 700 700 701 701 expect(onOperation).toHaveBeenCalledTimes(2); 702 702 }); 703 + 704 + it('unblocks operations on call to reexecuteOperation', async () => { 705 + const onOperation = vi.fn(); 706 + const onResult = vi.fn(); 707 + 708 + let hasSent = false; 709 + const exchange: Exchange = () => ops$ => 710 + pipe( 711 + ops$, 712 + onPush(onOperation), 713 + map(op => ({ 714 + hasNext: false, 715 + stale: false, 716 + data: 'test', 717 + operation: op, 718 + })), 719 + filter(() => hasSent || !(hasSent = true)) 720 + ); 721 + 722 + const client = createClient({ 723 + url: 'test', 724 + exchanges: [exchange], 725 + }); 726 + 727 + const operation = makeOperation('query', queryOperation, { 728 + ...queryOperation.context, 729 + requestPolicy: 'cache-first', 730 + }); 731 + 732 + pipe(client.executeRequestOperation(operation), subscribe(onResult)); 733 + 734 + expect(onOperation).toHaveBeenCalledTimes(1); 735 + expect(onResult).toHaveBeenCalledTimes(0); 736 + 737 + client.reexecuteOperation(operation); 738 + await Promise.resolve(); 739 + 740 + expect(onOperation).toHaveBeenCalledTimes(2); 741 + expect(onResult).toHaveBeenCalledTimes(1); 742 + }); 703 743 }); 704 744 705 745 describe('shared sources behavior', () => {
+4
packages/core/src/client.ts
··· 729 729 if (operation.kind === 'teardown') { 730 730 dispatchOperation(operation); 731 731 } else if (operation.kind === 'mutation' || active.has(operation.key)) { 732 + let queued = false; 733 + for (let i = 0; i < queue.length; i++) 734 + queued = queued || queue[i].key === operation.key; 735 + if (!queued) dispatched.delete(operation.key); 732 736 queue.push(operation); 733 737 Promise.resolve().then(dispatchOperation); 734 738 }