diff --git a/.changeset/tender-moons-watch.md b/.changeset/tender-moons-watch.md new file mode 100644 index 0000000000..65976c582f --- /dev/null +++ b/.changeset/tender-moons-watch.md @@ -0,0 +1,5 @@ +--- +'@urql/exchange-graphcache': patch +--- + +Update selection iterator implementation for JSC memory reduction diff --git a/exchanges/graphcache/src/operations/query.ts b/exchanges/graphcache/src/operations/query.ts index 3ad90a2ecf..f8ed04c981 100644 --- a/exchanges/graphcache/src/operations/query.ts +++ b/exchanges/graphcache/src/operations/query.ts @@ -37,7 +37,7 @@ import { warn, pushDebugNode, popDebugNode } from '../helpers/help'; import type { Context } from './shared'; import { - makeSelectionIterator, + SelectionIterator, ensureData, makeContext, updateContext, @@ -142,7 +142,7 @@ const readRoot = ( return input; } - const iterate = makeSelectionIterator( + const selection = new SelectionIterator( entityKey, entityKey, false, @@ -154,7 +154,7 @@ const readRoot = ( let node: FormattedNode | void; let hasChanged = InMemoryData.currentForeignData; const output = InMemoryData.makeData(input); - while ((node = iterate())) { + while ((node = selection.next())) { const fieldAlias = getFieldAlias(node); const fieldValue = input[fieldAlias]; // Add the current alias to the walked path before processing the field's value @@ -387,7 +387,7 @@ const readSelection = ( return; } - const iterate = makeSelectionIterator( + const selection = new SelectionIterator( typename, entityKey, false, @@ -402,7 +402,7 @@ const readSelection = ( let node: FormattedNode | void; const hasPartials = ctx.partial; const output = InMemoryData.makeData(input); - while ((node = iterate()) !== undefined) { + while ((node = selection.next()) !== undefined) { // Derive the needed data from our node. const fieldName = getName(node); const fieldArgs = getFieldArguments(node, ctx.variables); diff --git a/exchanges/graphcache/src/operations/shared.test.ts b/exchanges/graphcache/src/operations/shared.test.ts index 6b331949f9..711d95938c 100644 --- a/exchanges/graphcache/src/operations/shared.test.ts +++ b/exchanges/graphcache/src/operations/shared.test.ts @@ -7,7 +7,7 @@ import { } from '@urql/core'; import { FieldNode } from '@0no-co/graphql.web'; -import { makeSelectionIterator, deferRef } from './shared'; +import { SelectionIterator, deferRef } from './shared'; import { SelectionSet } from '../ast'; const selectionOfDocument = ( @@ -21,7 +21,7 @@ const selectionOfDocument = ( const ctx = {} as any; -describe('makeSelectionIterator', () => { +describe('SelectionIterator', () => { it('emits all fields', () => { const selection = selectionOfDocument(gql` { @@ -30,7 +30,7 @@ describe('makeSelectionIterator', () => { c } `); - const iterate = makeSelectionIterator( + const iterate = new SelectionIterator( 'Query', 'Query', false, @@ -41,7 +41,7 @@ describe('makeSelectionIterator', () => { const result: FieldNode[] = []; let node: FieldNode | void; - while ((node = iterate())) result.push(node); + while ((node = iterate.next())) result.push(node); expect(result).toMatchInlineSnapshot(` [ @@ -90,7 +90,7 @@ describe('makeSelectionIterator', () => { } `); - const iterate = makeSelectionIterator( + const iterate = new SelectionIterator( 'Query', 'Query', false, @@ -101,7 +101,7 @@ describe('makeSelectionIterator', () => { const result: FieldNode[] = []; let node: FieldNode | void; - while ((node = iterate())) result.push(node); + while ((node = iterate.next())) result.push(node); expect(result).toMatchInlineSnapshot('[]'); }); @@ -121,7 +121,7 @@ describe('makeSelectionIterator', () => { } `); - const iterate = makeSelectionIterator( + const iterate = new SelectionIterator( 'Query', 'Query', false, @@ -132,7 +132,7 @@ describe('makeSelectionIterator', () => { const result: FieldNode[] = []; let node: FieldNode | void; - while ((node = iterate())) result.push(node); + while ((node = iterate.next())) result.push(node); expect(result).toMatchInlineSnapshot(` [ @@ -207,7 +207,7 @@ describe('makeSelectionIterator', () => { } `); - const iterate = makeSelectionIterator( + const iterate = new SelectionIterator( 'Query', 'Query', false, @@ -217,7 +217,7 @@ describe('makeSelectionIterator', () => { ); const deferred: boolean[] = []; - while (iterate()) deferred.push(deferRef); + while (iterate.next()) deferred.push(deferRef); expect(deferred).toEqual([ false, // a true, // b @@ -243,7 +243,7 @@ describe('makeSelectionIterator', () => { } `); - const iterate = makeSelectionIterator( + const iterate = new SelectionIterator( 'Query', 'Query', true, @@ -253,7 +253,7 @@ describe('makeSelectionIterator', () => { ); const deferred: boolean[] = []; - while (iterate()) deferred.push(deferRef); + while (iterate.next()) deferred.push(deferRef); expect(deferred).toEqual([true, true, true]); }); }); diff --git a/exchanges/graphcache/src/operations/shared.ts b/exchanges/graphcache/src/operations/shared.ts index bb1893a3fe..3bfa1560ef 100644 --- a/exchanges/graphcache/src/operations/shared.ts +++ b/exchanges/graphcache/src/operations/shared.ts @@ -1,7 +1,6 @@ import type { CombinedError, ErrorLike, FormattedNode } from '@urql/core'; import type { - FieldNode, InlineFragmentNode, FragmentDefinitionNode, } from '@0no-co/graphql.web'; @@ -161,114 +160,133 @@ const isFragmentHeuristicallyMatching = ( }); }; -interface SelectionIterator { - (): FormattedNode | undefined; -} +export class SelectionIterator { + typename: undefined | string; + entityKey: string; + ctx: Context; + stack: { + selectionSet: FormattedNode; + index: number; + defer: boolean; + optional: boolean | undefined; + }[]; -// NOTE: Outside of this file, we expect `_defer` to always be reset to `false` -export function makeSelectionIterator( - typename: undefined | string, - entityKey: string, - _defer: false, - _optional: undefined, - selectionSet: FormattedNode, - ctx: Context -): SelectionIterator; -// NOTE: Inside this file we expect the state to be recursively passed on -export function makeSelectionIterator( - typename: undefined | string, - entityKey: string, - _defer: boolean, - _optional: undefined | boolean, - selectionSet: FormattedNode, - ctx: Context -): SelectionIterator; + // NOTE: Outside of this file, we expect `_defer` to always be reset to `false` + constructor( + typename: undefined | string, + entityKey: string, + _defer: false, + _optional: undefined, + selectionSet: FormattedNode, + ctx: Context + ); + // NOTE: Inside this file we expect the state to be recursively passed on + constructor( + typename: undefined | string, + entityKey: string, + _defer: boolean, + _optional: undefined | boolean, + selectionSet: FormattedNode, + ctx: Context + ); -export function makeSelectionIterator( - typename: undefined | string, - entityKey: string, - _defer: boolean, - _optional: boolean | undefined, - selectionSet: FormattedNode, - ctx: Context -): SelectionIterator { - let child: SelectionIterator | void; - let index = 0; + constructor( + typename: undefined | string, + entityKey: string, + _defer: boolean, + _optional: boolean | undefined, + selectionSet: FormattedNode, + ctx: Context + ) { + this.typename = typename; + this.entityKey = entityKey; + this.ctx = ctx; + this.stack = [ + { + selectionSet, + index: 0, + defer: _defer, + optional: _optional, + }, + ]; + } - return function next() { - let node: FormattedNode | undefined; - while (child || index < selectionSet.length) { - node = undefined; - deferRef = _defer; - optionalRef = _optional; - if (child) { - if ((node = child())) { - return node; - } else { - child = undefined; - if (process.env.NODE_ENV !== 'production') popDebugNode(); - } - } else { - const select = selectionSet[index++]; - if (!shouldInclude(select, ctx.variables)) { + next() { + while (this.stack.length > 0) { + let state = this.stack[this.stack.length - 1]; + while (state.index < state.selectionSet.length) { + const select = state.selectionSet[state.index++]; + if (!shouldInclude(select, this.ctx.variables)) { /*noop*/ } else if (select.kind !== Kind.FIELD) { // A fragment is either referred to by FragmentSpread or inline const fragment = select.kind !== Kind.INLINE_FRAGMENT - ? ctx.fragments[getName(select)] + ? this.ctx.fragments[getName(select)] : select; if (fragment) { const isMatching = !fragment.typeCondition || - (ctx.store.schema - ? isInterfaceOfType(ctx.store.schema, fragment, typename) + (this.ctx.store.schema + ? isInterfaceOfType( + this.ctx.store.schema, + fragment, + this.typename + ) : (currentOperation === 'read' && isFragmentMatching( fragment.typeCondition.name.value, - typename + this.typename )) || isFragmentHeuristicallyMatching( fragment, - typename, - entityKey, - ctx.variables, - ctx.store.logger + this.typename, + this.entityKey, + this.ctx.variables, + this.ctx.store.logger )); - if ( isMatching || - (currentOperation === 'write' && !ctx.store.schema) + (currentOperation === 'write' && !this.ctx.store.schema) ) { if (process.env.NODE_ENV !== 'production') - pushDebugNode(typename, fragment); + pushDebugNode(this.typename, fragment); const isFragmentOptional = isOptional(select); if ( isMatching && fragment.typeCondition && - typename !== fragment.typeCondition.name.value + this.typename !== fragment.typeCondition.name.value ) { - writeConcreteType(fragment.typeCondition.name.value, typename!); + writeConcreteType( + fragment.typeCondition.name.value, + this.typename! + ); } - child = makeSelectionIterator( - typename, - entityKey, - _defer || isDeferred(select, ctx.variables), - isFragmentOptional !== undefined - ? isFragmentOptional - : _optional, - getSelectionSet(fragment), - ctx + this.stack.push( + (state = { + selectionSet: getSelectionSet(fragment), + index: 0, + defer: state.defer || isDeferred(select, this.ctx.variables), + optional: + isFragmentOptional !== undefined + ? isFragmentOptional + : state.optional, + }) ); } } } else if (currentOperation === 'write' || !select._generated) { + deferRef = state.defer; + optionalRef = state.optional; return select; } } + this.stack.pop(); + if (process.env.NODE_ENV !== 'production') popDebugNode(); } - }; + return undefined; + } } const isFragmentMatching = (typeCondition: string, typename: string | void) => { diff --git a/exchanges/graphcache/src/operations/write.ts b/exchanges/graphcache/src/operations/write.ts index 2f6fc74430..1e49fbd9e8 100644 --- a/exchanges/graphcache/src/operations/write.ts +++ b/exchanges/graphcache/src/operations/write.ts @@ -39,7 +39,7 @@ import * as InMemoryData from '../store/data'; import type { Context } from './shared'; import { - makeSelectionIterator, + SelectionIterator, ensureData, makeContext, updateContext, @@ -237,7 +237,7 @@ const writeSelection = ( } const updates = ctx.store.updates[typename]; - const iterate = makeSelectionIterator( + const selection = new SelectionIterator( typename, entityKey || typename, false, @@ -247,7 +247,7 @@ const writeSelection = ( ); let node: FormattedNode | void; - while ((node = iterate())) { + while ((node = selection.next())) { const fieldName = getName(node); const fieldArgs = getFieldArguments(node, ctx.variables); const fieldKey = keyOfField(fieldName, fieldArgs);