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.

(core) - Fix ssrExchange.restoreData adding invalidated results (#1776)

* (core) - Fix ssrExchange.restoreData adding invalidated results

* Update tests with new case

* Fix type issues due to exposed SSR type

authored by

Phil Pluckthun and committed by
GitHub
35474918 a0fcea4b

+57 -7
+5
.changeset/cuddly-readers-walk.md
··· 1 + --- 2 + '@urql/core': patch 3 + --- 4 + 5 + Prevent `ssrExchange().restoreData()` from adding results to the exchange that have already been invalidated. This may happen when `restoreData()` is called repeatedly, e.g. per page. When a prior run has already invalidated an SSR result then the result is 'migrated' to the user's `cacheExchange`, which means that `restoreData()` should never attempt to re-add it again.
+32 -1
packages/core/src/exchanges/ssr.test.ts
··· 134 134 publish(exchange); 135 135 next(queryOperation); 136 136 137 - const error = ssr.extractData()[queryOperation.key].error; 137 + const error = ssr.extractData()[queryOperation.key]!.error; 138 138 139 139 expect(error).toHaveProperty('graphQLErrors.0.message', 'Oh no!'); 140 140 expect(error).toHaveProperty('graphQLErrors.0.path', ['Query']); ··· 182 182 // NOTE: The operation should not be duplicated 183 183 expect(output).not.toHaveBeenCalled(); 184 184 }); 185 + 186 + it('never allows restoration of invalidated results', async () => { 187 + client.suspense = false; 188 + 189 + const onPush = jest.fn(); 190 + const initialState = { [queryOperation.key]: serializedQueryResponse as any }; 191 + 192 + const ssr = ssrExchange({ 193 + isClient: true, 194 + initialState: { ...initialState }, 195 + }); 196 + 197 + const { source: ops$, next } = input; 198 + const exchange = ssr(exchangeInput)(ops$); 199 + 200 + pipe(exchange, forEach(onPush)); 201 + next(queryOperation); 202 + 203 + await Promise.resolve(); 204 + 205 + expect(Object.keys(ssr.extractData()).length).toBe(0); 206 + expect(onPush).toHaveBeenCalledTimes(1); 207 + expect(output).not.toHaveBeenCalled(); 208 + 209 + ssr.restoreData(initialState); 210 + expect(Object.keys(ssr.extractData()).length).toBe(0); 211 + 212 + next(queryOperation); 213 + expect(onPush).toHaveBeenCalledTimes(2); 214 + expect(output).toHaveBeenCalledTimes(1); 215 + });
+20 -6
packages/core/src/exchanges/ssr.ts
··· 91 91 92 92 /** The ssrExchange can be created to capture data during SSR and also to rehydrate it on the client */ 93 93 export const ssrExchange = (params?: SSRExchangeParams): SSRExchange => { 94 - const data: SSRData = {}; 94 + const data: Record<string, SerializedResult | null> = {}; 95 95 96 96 // On the client-side, we delete results from the cache as they're resolved 97 97 // this is delayed so that concurrent queries don't delete each other's data ··· 101 101 if (invalidateQueue.length === 1) { 102 102 Promise.resolve().then(() => { 103 103 let key: number | void; 104 - while ((key = invalidateQueue.shift())) delete data[key]; 104 + while ((key = invalidateQueue.shift())) { 105 + data[key] = null; 106 + } 105 107 }); 106 108 } 107 109 }; 108 110 109 111 const isCached = (operation: Operation) => { 110 - return !shouldSkip(operation) && data[operation.key] !== undefined; 112 + return !shouldSkip(operation) && data[operation.key] != null; 111 113 }; 112 114 113 115 // The SSR Exchange is a temporary cache that can populate results into data for suspense ··· 134 136 sharedOps$, 135 137 filter(op => isCached(op)), 136 138 map(op => { 137 - const serialized = data[op.key]; 139 + const serialized = data[op.key]!; 138 140 return deserializeResult(op, serialized); 139 141 }) 140 142 ); ··· 159 161 return merge([forwardedOps$, cachedOps$]); 160 162 }; 161 163 162 - ssr.restoreData = (restore: SSRData) => Object.assign(data, restore); 163 - ssr.extractData = () => Object.assign({}, data); 164 + ssr.restoreData = (restore: SSRData) => { 165 + for (const key in restore) { 166 + // We only restore data that hasn't been previously invalidated 167 + if (data[key] !== null) { 168 + data[key] = restore[key]; 169 + } 170 + } 171 + }; 172 + 173 + ssr.extractData = () => { 174 + const result: SSRData = {}; 175 + for (const key in data) if (data[key] != null) result[key] = data[key]!; 176 + return result; 177 + }; 164 178 165 179 if (params && params.initialState) { 166 180 ssr.restoreData(params.initialState);