Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(graphcache): Replace selection iterator implementation for JSC memory reduction #3693

Merged
merged 4 commits into from
Oct 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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