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(graphcache): Replace selection iterator implementation for JSC memory reduction (#3693)

authored by kitten.sh and committed by

GitHub 4641c394 ffaded60

+114 -91
+5
.changeset/tender-moons-watch.md
··· 1 + --- 2 + '@urql/exchange-graphcache': patch 3 + --- 4 + 5 + Update selection iterator implementation for JSC memory reduction
+5 -5
exchanges/graphcache/src/operations/query.ts
··· 37 37 38 38 import type { Context } from './shared'; 39 39 import { 40 - makeSelectionIterator, 40 + SelectionIterator, 41 41 ensureData, 42 42 makeContext, 43 43 updateContext, ··· 142 142 return input; 143 143 } 144 144 145 - const iterate = makeSelectionIterator( 145 + const selection = new SelectionIterator( 146 146 entityKey, 147 147 entityKey, 148 148 false, ··· 154 154 let node: FormattedNode<FieldNode> | void; 155 155 let hasChanged = InMemoryData.currentForeignData; 156 156 const output = InMemoryData.makeData(input); 157 - while ((node = iterate())) { 157 + while ((node = selection.next())) { 158 158 const fieldAlias = getFieldAlias(node); 159 159 const fieldValue = input[fieldAlias]; 160 160 // Add the current alias to the walked path before processing the field's value ··· 387 387 return; 388 388 } 389 389 390 - const iterate = makeSelectionIterator( 390 + const selection = new SelectionIterator( 391 391 typename, 392 392 entityKey, 393 393 false, ··· 402 402 let node: FormattedNode<FieldNode> | void; 403 403 const hasPartials = ctx.partial; 404 404 const output = InMemoryData.makeData(input); 405 - while ((node = iterate()) !== undefined) { 405 + while ((node = selection.next()) !== undefined) { 406 406 // Derive the needed data from our node. 407 407 const fieldName = getName(node); 408 408 const fieldArgs = getFieldArguments(node, ctx.variables);
+12 -12
exchanges/graphcache/src/operations/shared.test.ts
··· 7 7 } from '@urql/core'; 8 8 import { FieldNode } from '@0no-co/graphql.web'; 9 9 10 - import { makeSelectionIterator, deferRef } from './shared'; 10 + import { SelectionIterator, deferRef } from './shared'; 11 11 import { SelectionSet } from '../ast'; 12 12 13 13 const selectionOfDocument = ( ··· 21 21 22 22 const ctx = {} as any; 23 23 24 - describe('makeSelectionIterator', () => { 24 + describe('SelectionIterator', () => { 25 25 it('emits all fields', () => { 26 26 const selection = selectionOfDocument(gql` 27 27 { ··· 30 30 c 31 31 } 32 32 `); 33 - const iterate = makeSelectionIterator( 33 + const iterate = new SelectionIterator( 34 34 'Query', 35 35 'Query', 36 36 false, ··· 41 41 const result: FieldNode[] = []; 42 42 43 43 let node: FieldNode | void; 44 - while ((node = iterate())) result.push(node); 44 + while ((node = iterate.next())) result.push(node); 45 45 46 46 expect(result).toMatchInlineSnapshot(` 47 47 [ ··· 90 90 } 91 91 `); 92 92 93 - const iterate = makeSelectionIterator( 93 + const iterate = new SelectionIterator( 94 94 'Query', 95 95 'Query', 96 96 false, ··· 101 101 const result: FieldNode[] = []; 102 102 103 103 let node: FieldNode | void; 104 - while ((node = iterate())) result.push(node); 104 + while ((node = iterate.next())) result.push(node); 105 105 106 106 expect(result).toMatchInlineSnapshot('[]'); 107 107 }); ··· 121 121 } 122 122 `); 123 123 124 - const iterate = makeSelectionIterator( 124 + const iterate = new SelectionIterator( 125 125 'Query', 126 126 'Query', 127 127 false, ··· 132 132 const result: FieldNode[] = []; 133 133 134 134 let node: FieldNode | void; 135 - while ((node = iterate())) result.push(node); 135 + while ((node = iterate.next())) result.push(node); 136 136 137 137 expect(result).toMatchInlineSnapshot(` 138 138 [ ··· 207 207 } 208 208 `); 209 209 210 - const iterate = makeSelectionIterator( 210 + const iterate = new SelectionIterator( 211 211 'Query', 212 212 'Query', 213 213 false, ··· 217 217 ); 218 218 219 219 const deferred: boolean[] = []; 220 - while (iterate()) deferred.push(deferRef); 220 + while (iterate.next()) deferred.push(deferRef); 221 221 expect(deferred).toEqual([ 222 222 false, // a 223 223 true, // b ··· 243 243 } 244 244 `); 245 245 246 - const iterate = makeSelectionIterator( 246 + const iterate = new SelectionIterator( 247 247 'Query', 248 248 'Query', 249 249 true, ··· 253 253 ); 254 254 255 255 const deferred: boolean[] = []; 256 - while (iterate()) deferred.push(deferRef); 256 + while (iterate.next()) deferred.push(deferRef); 257 257 expect(deferred).toEqual([true, true, true]); 258 258 }); 259 259 });
+89 -71
exchanges/graphcache/src/operations/shared.ts
··· 1 1 import type { CombinedError, ErrorLike, FormattedNode } from '@urql/core'; 2 2 3 3 import type { 4 - FieldNode, 5 4 InlineFragmentNode, 6 5 FragmentDefinitionNode, 7 6 } from '@0no-co/graphql.web'; ··· 161 160 }); 162 161 }; 163 162 164 - interface SelectionIterator { 165 - (): FormattedNode<FieldNode> | undefined; 166 - } 163 + export class SelectionIterator { 164 + typename: undefined | string; 165 + entityKey: string; 166 + ctx: Context; 167 + stack: { 168 + selectionSet: FormattedNode<SelectionSet>; 169 + index: number; 170 + defer: boolean; 171 + optional: boolean | undefined; 172 + }[]; 167 173 168 - // NOTE: Outside of this file, we expect `_defer` to always be reset to `false` 169 - export function makeSelectionIterator( 170 - typename: undefined | string, 171 - entityKey: string, 172 - _defer: false, 173 - _optional: undefined, 174 - selectionSet: FormattedNode<SelectionSet>, 175 - ctx: Context 176 - ): SelectionIterator; 177 - // NOTE: Inside this file we expect the state to be recursively passed on 178 - export function makeSelectionIterator( 179 - typename: undefined | string, 180 - entityKey: string, 181 - _defer: boolean, 182 - _optional: undefined | boolean, 183 - selectionSet: FormattedNode<SelectionSet>, 184 - ctx: Context 185 - ): SelectionIterator; 174 + // NOTE: Outside of this file, we expect `_defer` to always be reset to `false` 175 + constructor( 176 + typename: undefined | string, 177 + entityKey: string, 178 + _defer: false, 179 + _optional: undefined, 180 + selectionSet: FormattedNode<SelectionSet>, 181 + ctx: Context 182 + ); 183 + // NOTE: Inside this file we expect the state to be recursively passed on 184 + constructor( 185 + typename: undefined | string, 186 + entityKey: string, 187 + _defer: boolean, 188 + _optional: undefined | boolean, 189 + selectionSet: FormattedNode<SelectionSet>, 190 + ctx: Context 191 + ); 186 192 187 - export function makeSelectionIterator( 188 - typename: undefined | string, 189 - entityKey: string, 190 - _defer: boolean, 191 - _optional: boolean | undefined, 192 - selectionSet: FormattedNode<SelectionSet>, 193 - ctx: Context 194 - ): SelectionIterator { 195 - let child: SelectionIterator | void; 196 - let index = 0; 193 + constructor( 194 + typename: undefined | string, 195 + entityKey: string, 196 + _defer: boolean, 197 + _optional: boolean | undefined, 198 + selectionSet: FormattedNode<SelectionSet>, 199 + ctx: Context 200 + ) { 201 + this.typename = typename; 202 + this.entityKey = entityKey; 203 + this.ctx = ctx; 204 + this.stack = [ 205 + { 206 + selectionSet, 207 + index: 0, 208 + defer: _defer, 209 + optional: _optional, 210 + }, 211 + ]; 212 + } 197 213 198 - return function next() { 199 - let node: FormattedNode<FieldNode> | undefined; 200 - while (child || index < selectionSet.length) { 201 - node = undefined; 202 - deferRef = _defer; 203 - optionalRef = _optional; 204 - if (child) { 205 - if ((node = child())) { 206 - return node; 207 - } else { 208 - child = undefined; 209 - if (process.env.NODE_ENV !== 'production') popDebugNode(); 210 - } 211 - } else { 212 - const select = selectionSet[index++]; 213 - if (!shouldInclude(select, ctx.variables)) { 214 + next() { 215 + while (this.stack.length > 0) { 216 + let state = this.stack[this.stack.length - 1]; 217 + while (state.index < state.selectionSet.length) { 218 + const select = state.selectionSet[state.index++]; 219 + if (!shouldInclude(select, this.ctx.variables)) { 214 220 /*noop*/ 215 221 } else if (select.kind !== Kind.FIELD) { 216 222 // A fragment is either referred to by FragmentSpread or inline 217 223 const fragment = 218 224 select.kind !== Kind.INLINE_FRAGMENT 219 - ? ctx.fragments[getName(select)] 225 + ? this.ctx.fragments[getName(select)] 220 226 : select; 221 227 if (fragment) { 222 228 const isMatching = 223 229 !fragment.typeCondition || 224 - (ctx.store.schema 225 - ? isInterfaceOfType(ctx.store.schema, fragment, typename) 230 + (this.ctx.store.schema 231 + ? isInterfaceOfType( 232 + this.ctx.store.schema, 233 + fragment, 234 + this.typename 235 + ) 226 236 : (currentOperation === 'read' && 227 237 isFragmentMatching( 228 238 fragment.typeCondition.name.value, 229 - typename 239 + this.typename 230 240 )) || 231 241 isFragmentHeuristicallyMatching( 232 242 fragment, 233 - typename, 234 - entityKey, 235 - ctx.variables, 236 - ctx.store.logger 243 + this.typename, 244 + this.entityKey, 245 + this.ctx.variables, 246 + this.ctx.store.logger 237 247 )); 238 - 239 248 if ( 240 249 isMatching || 241 - (currentOperation === 'write' && !ctx.store.schema) 250 + (currentOperation === 'write' && !this.ctx.store.schema) 242 251 ) { 243 252 if (process.env.NODE_ENV !== 'production') 244 - pushDebugNode(typename, fragment); 253 + pushDebugNode(this.typename, fragment); 245 254 const isFragmentOptional = isOptional(select); 246 255 if ( 247 256 isMatching && 248 257 fragment.typeCondition && 249 - typename !== fragment.typeCondition.name.value 258 + this.typename !== fragment.typeCondition.name.value 250 259 ) { 251 - writeConcreteType(fragment.typeCondition.name.value, typename!); 260 + writeConcreteType( 261 + fragment.typeCondition.name.value, 262 + this.typename! 263 + ); 252 264 } 253 265 254 - child = makeSelectionIterator( 255 - typename, 256 - entityKey, 257 - _defer || isDeferred(select, ctx.variables), 258 - isFragmentOptional !== undefined 259 - ? isFragmentOptional 260 - : _optional, 261 - getSelectionSet(fragment), 262 - ctx 266 + this.stack.push( 267 + (state = { 268 + selectionSet: getSelectionSet(fragment), 269 + index: 0, 270 + defer: state.defer || isDeferred(select, this.ctx.variables), 271 + optional: 272 + isFragmentOptional !== undefined 273 + ? isFragmentOptional 274 + : state.optional, 275 + }) 263 276 ); 264 277 } 265 278 } 266 279 } else if (currentOperation === 'write' || !select._generated) { 280 + deferRef = state.defer; 281 + optionalRef = state.optional; 267 282 return select; 268 283 } 269 284 } 285 + this.stack.pop(); 286 + if (process.env.NODE_ENV !== 'production') popDebugNode(); 270 287 } 271 - }; 288 + return undefined; 289 + } 272 290 } 273 291 274 292 const isFragmentMatching = (typeCondition: string, typename: string | void) => {
+3 -3
exchanges/graphcache/src/operations/write.ts
··· 39 39 40 40 import type { Context } from './shared'; 41 41 import { 42 - makeSelectionIterator, 42 + SelectionIterator, 43 43 ensureData, 44 44 makeContext, 45 45 updateContext, ··· 237 237 } 238 238 239 239 const updates = ctx.store.updates[typename]; 240 - const iterate = makeSelectionIterator( 240 + const selection = new SelectionIterator( 241 241 typename, 242 242 entityKey || typename, 243 243 false, ··· 247 247 ); 248 248 249 249 let node: FormattedNode<FieldNode> | void; 250 - while ((node = iterate())) { 250 + while ((node = selection.next())) { 251 251 const fieldName = getName(node); 252 252 const fieldArgs = getFieldArguments(node, ctx.variables); 253 253 const fieldKey = keyOfField(fieldName, fieldArgs);