Skip to content

Commit

Permalink
fix(graphcache): Replace selection iterator implementation for JSC me…
Browse files Browse the repository at this point in the history
…mory reduction (#3693)
  • Loading branch information
kitten authored Oct 19, 2024
1 parent ffaded6 commit 4641c39
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 91 deletions.
5 changes: 5 additions & 0 deletions .changeset/tender-moons-watch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@urql/exchange-graphcache': patch
---

Update selection iterator implementation for JSC memory reduction
10 changes: 5 additions & 5 deletions exchanges/graphcache/src/operations/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import { warn, pushDebugNode, popDebugNode } from '../helpers/help';

import type { Context } from './shared';
import {
makeSelectionIterator,
SelectionIterator,
ensureData,
makeContext,
updateContext,
Expand Down Expand Up @@ -142,7 +142,7 @@ const readRoot = (
return input;
}

const iterate = makeSelectionIterator(
const selection = new SelectionIterator(
entityKey,
entityKey,
false,
Expand All @@ -154,7 +154,7 @@ const readRoot = (
let node: FormattedNode<FieldNode> | 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
Expand Down Expand Up @@ -387,7 +387,7 @@ const readSelection = (
return;
}

const iterate = makeSelectionIterator(
const selection = new SelectionIterator(
typename,
entityKey,
false,
Expand All @@ -402,7 +402,7 @@ const readSelection = (
let node: FormattedNode<FieldNode> | 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);
Expand Down
24 changes: 12 additions & 12 deletions exchanges/graphcache/src/operations/shared.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
Expand All @@ -21,7 +21,7 @@ const selectionOfDocument = (

const ctx = {} as any;

describe('makeSelectionIterator', () => {
describe('SelectionIterator', () => {
it('emits all fields', () => {
const selection = selectionOfDocument(gql`
{
Expand All @@ -30,7 +30,7 @@ describe('makeSelectionIterator', () => {
c
}
`);
const iterate = makeSelectionIterator(
const iterate = new SelectionIterator(
'Query',
'Query',
false,
Expand All @@ -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(`
[
Expand Down Expand Up @@ -90,7 +90,7 @@ describe('makeSelectionIterator', () => {
}
`);

const iterate = makeSelectionIterator(
const iterate = new SelectionIterator(
'Query',
'Query',
false,
Expand All @@ -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('[]');
});
Expand All @@ -121,7 +121,7 @@ describe('makeSelectionIterator', () => {
}
`);

const iterate = makeSelectionIterator(
const iterate = new SelectionIterator(
'Query',
'Query',
false,
Expand All @@ -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(`
[
Expand Down Expand Up @@ -207,7 +207,7 @@ describe('makeSelectionIterator', () => {
}
`);

const iterate = makeSelectionIterator(
const iterate = new SelectionIterator(
'Query',
'Query',
false,
Expand All @@ -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
Expand All @@ -243,7 +243,7 @@ describe('makeSelectionIterator', () => {
}
`);

const iterate = makeSelectionIterator(
const iterate = new SelectionIterator(
'Query',
'Query',
true,
Expand All @@ -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]);
});
});
160 changes: 89 additions & 71 deletions exchanges/graphcache/src/operations/shared.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type { CombinedError, ErrorLike, FormattedNode } from '@urql/core';

import type {
FieldNode,
InlineFragmentNode,
FragmentDefinitionNode,
} from '@0no-co/graphql.web';
Expand Down Expand Up @@ -161,114 +160,133 @@ const isFragmentHeuristicallyMatching = (
});
};

interface SelectionIterator {
(): FormattedNode<FieldNode> | undefined;
}
export class SelectionIterator {
typename: undefined | string;
entityKey: string;
ctx: Context;
stack: {
selectionSet: FormattedNode<SelectionSet>;
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<SelectionSet>,
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<SelectionSet>,
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<SelectionSet>,
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<SelectionSet>,
ctx: Context
);

export function makeSelectionIterator(
typename: undefined | string,
entityKey: string,
_defer: boolean,
_optional: boolean | undefined,
selectionSet: FormattedNode<SelectionSet>,
ctx: Context
): SelectionIterator {
let child: SelectionIterator | void;
let index = 0;
constructor(
typename: undefined | string,
entityKey: string,
_defer: boolean,
_optional: boolean | undefined,
selectionSet: FormattedNode<SelectionSet>,
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<FieldNode> | 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) => {
Expand Down
6 changes: 3 additions & 3 deletions exchanges/graphcache/src/operations/write.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import * as InMemoryData from '../store/data';

import type { Context } from './shared';
import {
makeSelectionIterator,
SelectionIterator,
ensureData,
makeContext,
updateContext,
Expand Down Expand Up @@ -237,7 +237,7 @@ const writeSelection = (
}

const updates = ctx.store.updates[typename];
const iterate = makeSelectionIterator(
const selection = new SelectionIterator(
typename,
entityKey || typename,
false,
Expand All @@ -247,7 +247,7 @@ const writeSelection = (
);

let node: FormattedNode<FieldNode> | void;
while ((node = iterate())) {
while ((node = selection.next())) {
const fieldName = getName(node);
const fieldArgs = getFieldArguments(node, ctx.variables);
const fieldKey = keyOfField(fieldName, fieldArgs);
Expand Down

0 comments on commit 4641c39

Please sign in to comment.