Skip to content
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
3 changes: 2 additions & 1 deletion src/execution/incremental/IncrementalExecutor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type { ObjMap } from '../../jsutils/ObjMap.ts';
import type { Path } from '../../jsutils/Path.ts';
import { addPath, pathToArray } from '../../jsutils/Path.ts';
import type { PromiseOrValue } from '../../jsutils/PromiseOrValue.ts';
import type { SetMap } from '../../jsutils/SetMap.ts';

import type {
GraphQLError,
Expand Down Expand Up @@ -597,7 +598,7 @@ export class IncrementalExecutor<
parentType: GraphQLObjectType,
sourceValue: unknown,
path: Path | undefined,
newGroupedFieldSets: Map<DeferUsageSet, GroupedFieldSet>,
newGroupedFieldSets: SetMap<DeferUsage, GroupedFieldSet>,
deliveryGroupMap: ReadonlyMap<DeferUsage, DeliveryGroup>,
): void {
const createSubExecutor = this.getCreateSubExecutor();
Expand Down
16 changes: 6 additions & 10 deletions src/execution/incremental/buildExecutionPlan.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getBySet } from '../../jsutils/getBySet.ts';
import { isSameSet } from '../../jsutils/isSameSet.ts';
import { SetMap } from '../../jsutils/SetMap.ts';

import type {
DeferUsage,
Expand All @@ -13,7 +13,7 @@ export type DeferUsageSet = ReadonlySet<DeferUsage>;
/** @internal */
export interface ExecutionPlan {
groupedFieldSet: GroupedFieldSet;
newGroupedFieldSets: Map<DeferUsageSet, GroupedFieldSet>;
newGroupedFieldSets: SetMap<DeferUsage, GroupedFieldSet>;
}

/** @internal */
Expand All @@ -22,8 +22,8 @@ export function buildExecutionPlan(
parentDeferUsages: DeferUsageSet = new Set<DeferUsage>(),
): ExecutionPlan {
const groupedFieldSet = new Map<string, FieldDetailsList>();
const newGroupedFieldSets = new Map<
DeferUsageSet,
const newGroupedFieldSets = new SetMap<
DeferUsage,
Map<string, FieldDetailsList>
>();
for (const [responseKey, fieldDetailsList] of originalGroupedFieldSet) {
Expand All @@ -34,14 +34,10 @@ export function buildExecutionPlan(
continue;
}

let newGroupedFieldSet = getBySet(
newGroupedFieldSets,
const newGroupedFieldSet = newGroupedFieldSets.getOrInsertComputed(
filteredDeferUsageSet,
() => new Map(),
);
if (newGroupedFieldSet === undefined) {
newGroupedFieldSet = new Map();
newGroupedFieldSets.set(filteredDeferUsageSet, newGroupedFieldSet);
}
newGroupedFieldSet.set(responseKey, fieldDetailsList);
}

Expand Down
15 changes: 7 additions & 8 deletions src/execution/legacyIncremental/BranchingIncrementalExecutor.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
/** @category Legacy Incremental Execution */

import { AccumulatorMap } from '../../jsutils/AccumulatorMap.ts';
import { getBySet } from '../../jsutils/getBySet.ts';
import { invariant } from '../../jsutils/invariant.ts';
import { isSameSet } from '../../jsutils/isSameSet.ts';
import { memoize1 } from '../../jsutils/memoize1.ts';
import { memoize2 } from '../../jsutils/memoize2.ts';
import type { ObjMap } from '../../jsutils/ObjMap.ts';
import { SetMap } from '../../jsutils/SetMap.ts';

import type {
GraphQLError,
Expand Down Expand Up @@ -344,8 +344,8 @@ function buildBranchingExecutionPlan(
): ExecutionPlan {
const groupedFieldSet = new AccumulatorMap<string, FieldDetails>();

const newGroupedFieldSets = new Map<
DeferUsageSet,
const newGroupedFieldSets = new SetMap<
DeferUsage,
AccumulatorMap<string, FieldDetails>
>();

Expand All @@ -359,11 +359,10 @@ function buildBranchingExecutionPlan(
if (isSameSet(parentDeferUsages, deferUsageSet)) {
groupedFieldSet.add(responseKey, fieldDetails);
} else {
let newGroupedFieldSet = getBySet(newGroupedFieldSets, deferUsageSet);
if (newGroupedFieldSet === undefined) {
newGroupedFieldSet = new AccumulatorMap();
newGroupedFieldSets.set(deferUsageSet, newGroupedFieldSet);
}
const newGroupedFieldSet = newGroupedFieldSets.getOrInsertComputed(
deferUsageSet,
() => new AccumulatorMap(),
);
newGroupedFieldSet.add(responseKey, fieldDetails);
}
}
Expand Down
124 changes: 124 additions & 0 deletions src/jsutils/SetMap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
interface SetMapEntry<T, V> {
members: ReadonlySet<T>;
value: V;
}

/**
* Maps an order-independent set of member identities to a value. Unlike a
* `Map<ReadonlySet<T>, V>`, two sets containing the same members address the
* same entry even when they are different `Set` objects.
*
* Each member receives a map-local numeric ID. Sorting those IDs produces an
* exact, order-independent key.
*
* Iteration follows insertion order and returns the first `Set` object stored
* for each key. Key sets must not be mutated after insertion.
*
* @internal
* @typeParam T - A member of a map key.
* @typeParam V - The value associated with a set of members.
*/
export class SetMap<T, V> {
// Retains the first Set object for each key and preserves insertion order.
_entries: Map<string, SetMapEntry<T, V>>;
_memberIds: Map<T, number>;
_nextMemberId: number;

constructor() {
this._entries = new Map();
this._memberIds = new Map();
this._nextMemberId = 0;
}

get size(): number {
return this._entries.size;
}

get(members: ReadonlySet<T>): V | undefined {
return this._find(members)?.value;
}

getOrInsert(members: ReadonlySet<T>, value: V): V {
const entry = this._find(members);
if (entry !== undefined) {
return entry.value;
}

this._create(members, value);
return value;
}

getOrInsertComputed(
members: ReadonlySet<T>,
computeValue: (members: ReadonlySet<T>) => V,
): V {
const entry = this._find(members);
if (entry !== undefined) {
return entry.value;
}

const value = computeValue(members);
this.set(members, value);
return value;
}

has(members: ReadonlySet<T>): boolean {
return this._find(members) !== undefined;
}

set(members: ReadonlySet<T>, value: V): this {
const entry = this._find(members);
if (entry !== undefined) {
entry.value = value;
return this;
}

this._create(members, value);
return this;
}

*keys(): IterableIterator<ReadonlySet<T>> {
for (const entry of this._entries.values()) {
yield entry.members;
}
}

*values(): IterableIterator<V> {
for (const entry of this._entries.values()) {
yield entry.value;
}
}

*entries(): IterableIterator<[ReadonlySet<T>, V]> {
for (const entry of this._entries.values()) {
yield [entry.members, entry.value];
}
}

[Symbol.iterator](): IterableIterator<[ReadonlySet<T>, V]> {
return this.entries();
}

_find(members: ReadonlySet<T>): SetMapEntry<T, V> | undefined {
return this._entries.get(this._key(members));
}

_create(members: ReadonlySet<T>, value: V): void {
const entry = { members, value };
this._entries.set(this._key(members), entry);
}

_key(members: ReadonlySet<T>): string {
const ids = [];
for (const member of members) {
let id = this._memberIds.get(member);
if (id === undefined) {
id = this._nextMemberId++;
this._memberIds.set(member, id);
}
ids.push(id);
}
ids.sort((id1, id2) => id1 - id2);
return ids.join(',');
}
}
120 changes: 120 additions & 0 deletions src/jsutils/__tests__/SetMap-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { describe, it } from 'node:test';

import { expect } from 'chai';

import { spyOn } from '../../__testUtils__/spyOn.ts';

import { SetMap } from '../SetMap.ts';

describe('SetMap', () => {
it('maps exact sets regardless of insertion order', () => {
const cache = new SetMap<object, number>();
const a = {};
const b = {};

cache.set(new Set([a, b]), 0);

expect(cache.get(new Set([a, b]))).to.equal(0);
expect(cache.get(new Set([b, a]))).to.equal(0);
});

it('distinguishes sets which share members', () => {
const cache = new SetMap<object, number>();
const a = {};
const b = {};
const c = {};

cache.set(new Set([a, b]), 0);
cache.set(new Set([a, c]), 1);
cache.set(new Set([a, b, c]), 2);
cache.set(new Set([c, b]), 3);

expect(cache.get(new Set([a, b]))).to.equal(0);
expect(cache.get(new Set([a, c]))).to.equal(1);
expect(cache.get(new Set([a, b, c]))).to.equal(2);
expect(cache.get(new Set([c, b]))).to.equal(3);
});

it('maps the empty set', () => {
const cache = new SetMap<object, number>();

expect(cache.get(new Set())).to.equal(undefined);
cache.set(new Set(), 0);
expect(cache.get(new Set())).to.equal(0);
});

it('supports undefined values', () => {
const cache = new SetMap<object, undefined>();
const member = {};

cache.set(new Set([member]), undefined);

expect(cache.get(new Set([member]))).to.equal(undefined);
expect(cache.has(new Set([member]))).to.equal(true);
});

it('iterates canonical keys and values in insertion order', () => {
const cache = new SetMap<object, number>();
const a = {};
const b = {};
const first = new Set([a]);
const second = new Set([a, b]);

cache.set(first, 1);
cache.set(second, 2);
cache.set(new Set([a]), 3);

expect(cache.size).to.equal(2);
expect(Array.from(cache.keys())).to.deep.equal([first, second]);
expect(Array.from(cache.values())).to.deep.equal([3, 2]);
expect(Array.from(cache)).to.deep.equal([
[first, 3],
[second, 2],
]);
});

it('can insert values on demand', () => {
const cache = new SetMap<object, number>();
const member = {};

expect(cache.getOrInsert(new Set([member]), 0)).to.equal(0);
expect(cache.getOrInsert(new Set([member]), 1)).to.equal(0);
});

it('can compute values on demand', () => {
const cache = new SetMap<object, number>();
const member = {};
const compute = spyOn(() => 0);

expect(cache.getOrInsertComputed(new Set([member]), compute)).to.equal(0);
expect(cache.getOrInsertComputed(new Set([member]), compute)).to.equal(0);
expect(compute.callCount).to.equal(1);
});

it('does not recreate undefined values', () => {
const cache = new SetMap<object, undefined>();
const member = {};
const compute = spyOn(() => undefined);

expect(cache.getOrInsertComputed(new Set([member]), compute)).to.equal(
undefined,
);
expect(cache.getOrInsertComputed(new Set([member]), compute)).to.equal(
undefined,
);
expect(compute.callCount).to.equal(1);
});

it('updates values inserted during computation', () => {
const cache = new SetMap<object, number>();
const member = {};

expect(
cache.getOrInsertComputed(new Set([member]), (members) => {
cache.set(members, 1);
return 2;
}),
).to.equal(2);
expect(cache.get(new Set([member]))).to.equal(2);
});
});
14 changes: 0 additions & 14 deletions src/jsutils/getBySet.ts

This file was deleted.

Loading