From 6f30869b2896d6a98b7063d3b20d1c4c22200fca Mon Sep 17 00:00:00 2001 From: Jesse Watts-Russell Date: Fri, 5 May 2023 17:33:12 -0700 Subject: [PATCH] Updating connection handler to be able to deal with streamed edges that are already in memory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: Fix fix fix tl;dr: *Remembering the page_info of previously streamed pages causes ConnectionHandler to prematurely update the end cursor and not accept more than the initial count of items for the connection.* Original Post: https://fb.workplace.com/groups/relay.support/permalink/24536709345951016/ # Debugging: On reload of pages/connections that have been in memory before it seems to recall/remember the pageInfo of those. https://www.internalfb.com/code/fbsource/[8aede072216b66fd056bd12e1de7a463250acb3c]/xplat/js/RKJSModules/Libraries/Relay/oss/relay-runtime/handlers/connection/ConnectionHandler.js?lines=67 Usually, with stream_connection it doesn’t know the page_info till the end of the stream (its null until it comes back deferred). But on second reload of the exact same page, it looks up the same key, and finds the page info from last time causing it to set the end cursor prematurely. https://www.internalfb.com/code/fbsource/[8aede072216b66fd056bd12e1de7a463250acb3c][history][blame]/xplat/js/RKJSModules/Libraries/Relay/oss/relay-runtime/handlers/connection/ConnectionHandler.js?lines=221 This causes all subsequently streamed edges to fail due to this call: https://www.internalfb.com/code/fbsource/[8aede072216b66fd056bd12e1de7a463250acb3c][history][blame]/xplat/js/RKJSModules/Libraries/Relay/oss/relay-runtime/handlers/connection/ConnectionHandler.js?lines=150 Remembering the page_info of previously streamed pages causes ConnectionHandler to prematurely update the end cursor and not accept more than the initial count of items for subsequent pages in the connection. This modifies the logic to be able to accept pages that either agree on after/end cursor or matching end cursors. Reviewed By: keoskate, fred2028 Differential Revision: D45586049 fbshipit-source-id: 34ac5e342ca0975f2ad841a382ffde4ff5fc6bd3 --- .../handlers/connection/ConnectionHandler.js | 20 +- .../__tests__/ConnectionHandler-test.js | 184 ++++++++++++++++++ 2 files changed, 200 insertions(+), 4 deletions(-) diff --git a/packages/relay-runtime/handlers/connection/ConnectionHandler.js b/packages/relay-runtime/handlers/connection/ConnectionHandler.js index 64ad418f80e1d..e47d6ac9b0d0e 100644 --- a/packages/relay-runtime/handlers/connection/ConnectionHandler.js +++ b/packages/relay-runtime/handlers/connection/ConnectionHandler.js @@ -144,11 +144,23 @@ function update(store: RecordSourceProxy, payload: HandleFieldPayload): void { const args = payload.args; if (prevEdges && serverEdges) { if (args.after != null) { + const clientEndCursor = clientPageInfo?.getValue(END_CURSOR); + const serverEndCursor = serverPageInfo?.getValue(END_CURSOR); + + const isAddingEdgesAfterCurrentPage = + clientPageInfo && args.after === clientEndCursor; + const isFillingOutCurrentPage = + clientPageInfo && clientEndCursor === serverEndCursor; + // Forward pagination from the end of the connection: append edges - if ( - clientPageInfo && - args.after === clientPageInfo.getValue(END_CURSOR) - ) { + // Case 1: We're fetching edges for the first time and pageInfo for + // the upcoming page is missing, but our after cursor matches + // the last ending cursor. (adding after current page) + // Case 2: We've fetched these edges before and we know the end cursor + // from the first edge updating the END_CURSOR field. If the + // end cursor from the server matches the end cursor from the + // client then we're just filling out the rest of this page. + if (isAddingEdgesAfterCurrentPage || isFillingOutCurrentPage) { const nodeIDs = new Set(); mergeEdges(prevEdges, nextEdges, nodeIDs); mergeEdges(serverEdges, nextEdges, nodeIDs); diff --git a/packages/relay-runtime/handlers/connection/__tests__/ConnectionHandler-test.js b/packages/relay-runtime/handlers/connection/__tests__/ConnectionHandler-test.js index 084094e6f4ca5..2ea05b1aeb2db 100644 --- a/packages/relay-runtime/handlers/connection/__tests__/ConnectionHandler-test.js +++ b/packages/relay-runtime/handlers/connection/__tests__/ConnectionHandler-test.js @@ -848,6 +848,190 @@ describe('ConnectionHandler', () => { }); }); + it('appends two streamed edges, which have been streamed before and know their end cursors', () => { + // First edge + normalize( + { + node: { + id: '4', + __typename: 'User', + friends: { + edges: [ + { + cursor: 'cursor:2', + node: { + id: '2', + }, + }, + ], + [PAGE_INFO]: { + // EACH EDGE ALREADY WILL KNOW ITS END CURSOR FOR THAT PAGE + [END_CURSOR]: 'cursor:3', + [HAS_NEXT_PAGE]: false, + [HAS_PREV_PAGE]: false, + [START_CURSOR]: 'cursor:2', + }, + }, + }, + }, + { + after: 'cursor:1', + before: null, + count: 10, + orderby: ['first name'], + id: '4', + }, + ); + const args = {after: 'cursor:1', first: 10, orderby: ['first name']}; + const handleKey = + getRelayHandleKey( + 'connection', + 'ConnectionQuery_friends', + 'friends', + ) + '(orderby:["first name"])'; + const payload = { + args, + dataID: '4', + fieldKey: getStableStorageKey('friends', args), + handleKey, + }; + ConnectionHandler.update(proxy, payload); + expect(sinkSource.toJSON()).toEqual({ + 'client:4:__ConnectionQuery_friends_connection(orderby:["first name"])': + { + [ID_KEY]: + 'client:4:__ConnectionQuery_friends_connection(orderby:["first name"])', + [TYPENAME_KEY]: 'FriendsConnection', + edges: { + [REFS_KEY]: [ + 'client:4:__ConnectionQuery_friends_connection(orderby:["first name"]):edges:0', + 'client:4:__ConnectionQuery_friends_connection(orderby:["first name"]):edges:1', + ], + }, + pageInfo: { + [REF_KEY]: + 'client:4:__ConnectionQuery_friends_connection(orderby:["first name"]):pageInfo', + }, + __connection_next_edge_index: 2, + }, + 'client:4:__ConnectionQuery_friends_connection(orderby:["first name"]):edges:1': + { + [ID_KEY]: + 'client:4:__ConnectionQuery_friends_connection(orderby:["first name"]):edges:1', + [TYPENAME_KEY]: 'FriendsEdge', + cursor: 'cursor:2', + node: {[REF_KEY]: '2'}, + }, + 'client:4:__ConnectionQuery_friends_connection(orderby:["first name"]):pageInfo': + { + [ID_KEY]: + 'client:4:__ConnectionQuery_friends_connection(orderby:["first name"]):pageInfo', + [TYPENAME_KEY]: 'PageInfo', + [END_CURSOR]: 'cursor:3', + [HAS_NEXT_PAGE]: false, + }, + }); + + // Second Edge + normalize( + { + node: { + id: '4', + __typename: 'User', + friends: { + edges: [ + { + cursor: 'cursor:3', + node: { + id: '3', + }, + }, + ], + [PAGE_INFO]: { + // EACH EDGE ALREADY WILL KNOW ITS END CURSOR FOR THAT PAGE + // (THIS IS FINAL EDGE, BUT STILL...) + [END_CURSOR]: 'cursor:3', + [HAS_NEXT_PAGE]: false, + [HAS_PREV_PAGE]: false, + [START_CURSOR]: 'cursor:2', + }, + }, + }, + }, + { + after: 'cursor:1', + before: null, + count: 10, + orderby: ['first name'], + id: '4', + }, + ); + const secondArgs = { + after: 'cursor:1', + first: 10, + orderby: ['first name'], + }; + const secondHandleKey = + getRelayHandleKey( + 'connection', + 'ConnectionQuery_friends', + 'friends', + ) + '(orderby:["first name"])'; + const secondPayload = { + args, + dataID: '4', + fieldKey: getStableStorageKey('friends', secondArgs), + handleKey: secondHandleKey, + }; + ConnectionHandler.update(proxy, secondPayload); + + const result = { + 'client:4:__ConnectionQuery_friends_connection(orderby:["first name"])': + { + [ID_KEY]: + 'client:4:__ConnectionQuery_friends_connection(orderby:["first name"])', + [TYPENAME_KEY]: 'FriendsConnection', + edges: { + [REFS_KEY]: [ + 'client:4:__ConnectionQuery_friends_connection(orderby:["first name"]):edges:0', + 'client:4:__ConnectionQuery_friends_connection(orderby:["first name"]):edges:1', + 'client:4:__ConnectionQuery_friends_connection(orderby:["first name"]):edges:2', + ], + }, + pageInfo: { + [REF_KEY]: + 'client:4:__ConnectionQuery_friends_connection(orderby:["first name"]):pageInfo', + }, + __connection_next_edge_index: 3, + }, + 'client:4:__ConnectionQuery_friends_connection(orderby:["first name"]):edges:1': + { + [ID_KEY]: + 'client:4:__ConnectionQuery_friends_connection(orderby:["first name"]):edges:1', + [TYPENAME_KEY]: 'FriendsEdge', + cursor: 'cursor:2', + node: {[REF_KEY]: '2'}, + }, + 'client:4:__ConnectionQuery_friends_connection(orderby:["first name"]):edges:2': + { + [ID_KEY]: + 'client:4:__ConnectionQuery_friends_connection(orderby:["first name"]):edges:2', + [TYPENAME_KEY]: 'FriendsEdge', + cursor: 'cursor:3', + node: {[REF_KEY]: '3'}, + }, + 'client:4:__ConnectionQuery_friends_connection(orderby:["first name"]):pageInfo': + { + [ID_KEY]: + 'client:4:__ConnectionQuery_friends_connection(orderby:["first name"]):pageInfo', + [TYPENAME_KEY]: 'PageInfo', + [END_CURSOR]: 'cursor:3', + [HAS_NEXT_PAGE]: false, + }, + }; + expect(sinkSource.toJSON()).toEqual(result); + }); + it('prepends new edges', () => { normalize( {