Skip to content

Commit

Permalink
introduce OrderedSet
Browse files Browse the repository at this point in the history
to optimize comparisons and insertion into the set-indexed map
  • Loading branch information
yaacovCR committed Aug 24, 2023
1 parent 4ff4d05 commit ad7dbff
Show file tree
Hide file tree
Showing 5 changed files with 137 additions and 35 deletions.
18 changes: 10 additions & 8 deletions src/execution/collectFields.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { AccumulatorMap } from '../jsutils/AccumulatorMap.js';
import { getBySet } from '../jsutils/getBySet.js';
import { invariant } from '../jsutils/invariant.js';
import { isSameSet } from '../jsutils/isSameSet.js';
import type { ObjMap } from '../jsutils/ObjMap.js';
import type { ReadonlyOrderedSet } from '../jsutils/OrderedSet.js';
import { OrderedSet } from '../jsutils/OrderedSet.js';

import type {
FieldNode,
Expand Down Expand Up @@ -33,11 +33,13 @@ export interface DeferUsage {
ancestors: ReadonlyArray<Target>;
}

export const NON_DEFERRED_TARGET_SET: TargetSet = new Set<Target>([undefined]);
export const NON_DEFERRED_TARGET_SET = new OrderedSet<Target>([
undefined,
]).freeze();

export type Target = DeferUsage | undefined;
export type TargetSet = ReadonlySet<Target>;
export type DeferUsageSet = ReadonlySet<DeferUsage>;
export type TargetSet = ReadonlyOrderedSet<Target>;
export type DeferUsageSet = ReadonlyOrderedSet<DeferUsage>;

export interface FieldDetails {
node: FieldNode;
Expand Down Expand Up @@ -430,13 +432,13 @@ function getTargetSetDetails(
}
}

const maskingTargets: TargetSet = new Set<Target>(maskingTargetList);
if (isSameSet(maskingTargets, parentTargets)) {
const maskingTargets = new OrderedSet(maskingTargetList).freeze();
if (maskingTargets === parentTargets) {
parentTargetKeys.add(responseKey);
continue;
}

let targetSetDetails = getBySet(targetSetDetailsMap, maskingTargets);
let targetSetDetails = targetSetDetailsMap.get(maskingTargets);
if (targetSetDetails === undefined) {
targetSetDetails = {
keys: new Set(),
Expand Down
93 changes: 93 additions & 0 deletions src/jsutils/OrderedSet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
const setContainingUndefined = new Set([undefined]);
const setsContainingOneItem = new WeakMap<object, Set<object | undefined>>();
const setsAppendedByUndefined = new WeakMap<
ReadonlySet<object | undefined>,
Set<object | undefined>
>();
const setsAppendedByDefined = new WeakMap<
ReadonlySet<object | undefined>,
WeakMap<object, Set<object | undefined>>
>();

function createOrderedSet<T extends object | undefined>(
item: T,
): ReadonlySet<T | undefined> {
if (item === undefined) {
return setContainingUndefined;
}

let set = setsContainingOneItem.get(item);
if (set === undefined) {
set = new Set([item]);
set.add(item);
setsContainingOneItem.set(item, set);
}
return set as ReadonlyOrderedSet<T | undefined>;
}

function appendToOrderedSet<T extends object | undefined>(
set: ReadonlySet<T | undefined>,
item: T | undefined,
): ReadonlySet<T | undefined> {
if (set.has(item)) {
return set;
}

if (item === undefined) {
let appendedSet = setsAppendedByUndefined.get(set);
if (appendedSet === undefined) {
appendedSet = new Set(set);
appendedSet.add(undefined);
setsAppendedByUndefined.set(set, appendedSet);
}
return appendedSet as ReadonlySet<T | undefined>;
}

let appendedSets = setsAppendedByDefined.get(set);
if (appendedSets === undefined) {
appendedSets = new WeakMap();
setsAppendedByDefined.set(set, appendedSets);
const appendedSet = new Set(set);
appendedSet.add(item);
appendedSets.set(item, appendedSet);
return appendedSet as ReadonlySet<T | undefined>;
}

let appendedSet: Set<object | undefined> | undefined = appendedSets.get(item);
if (appendedSet === undefined) {
appendedSet = new Set<object | undefined>(set);
appendedSet.add(item);
appendedSets.set(item, appendedSet);
}

return appendedSet as ReadonlySet<T | undefined>;
}

export type ReadonlyOrderedSet<T> = ReadonlySet<T>;

const emptySet = new Set();

/**
* A set that when frozen can be directly compared for equality.
*
* Sets are limited to JSON serializable values.
*
* @internal
*/
export class OrderedSet<T extends object | undefined> {
_set: ReadonlySet<T | undefined> = emptySet as ReadonlySet<T>;
constructor(items: Iterable<T>) {
for (const item of items) {
if (this._set === emptySet) {
this._set = createOrderedSet(item);
continue;
}

this._set = appendToOrderedSet(this._set, item);
}
}

freeze(): ReadonlyOrderedSet<T> {
return this._set as ReadonlyOrderedSet<T>;
}
}
34 changes: 34 additions & 0 deletions src/jsutils/__tests__/OrderedSet-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { expect } from 'chai';
import { describe, it } from 'mocha';

import { OrderedSet } from '../OrderedSet.js';

describe('OrderedSet', () => {
it('empty sets are equal', () => {
const orderedSetA = new OrderedSet([]).freeze();
const orderedSetB = new OrderedSet([]).freeze();

expect(orderedSetA).to.equal(orderedSetB);
});

it('sets with members in different orders or numbers are equal', () => {
const a = { a: 'a' };
const b = { b: 'b' };
const c = { c: 'c' };
const orderedSetA = new OrderedSet([a, b, c, a, undefined]).freeze();
const orderedSetB = new OrderedSet([undefined, b, a, b, c]).freeze();

expect(orderedSetA).to.not.equal(orderedSetB);
});

it('sets with members in different orders or numbers are equal', () => {
const a = { a: 'a' };
const b = { b: 'b' };
const c = { c: 'c' };
const d = { c: 'd' };
const orderedSetA = new OrderedSet([a, b, c, a, undefined]).freeze();
const orderedSetB = new OrderedSet([undefined, b, a, b, d]).freeze();

expect(orderedSetA).to.not.equal(orderedSetB);
});
});
13 changes: 0 additions & 13 deletions src/jsutils/getBySet.ts

This file was deleted.

14 changes: 0 additions & 14 deletions src/jsutils/isSameSet.ts

This file was deleted.

0 comments on commit ad7dbff

Please sign in to comment.