Skip to content

Commit

Permalink
Remove 'iterall' as dependency. 'graphql' becomes zero dependen… (#2364)
Browse files Browse the repository at this point in the history
We used only couple functions from 'iterall' and it's the only
dependency we have at the moment.
Having no dependencies significantly simplify ESM builds and Deno
support in future
Partially resolves: #2277

Bonus: All supported Node versions already natively support Array.from
so it should improve perfomance
  • Loading branch information
IvanGoncharov committed Jan 20, 2020
1 parent c90c9a3 commit bde5e4a
Show file tree
Hide file tree
Showing 12 changed files with 216 additions and 35 deletions.
3 changes: 2 additions & 1 deletion cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
"words": [
"jsutils",
"tsutils",
"iterall",
"noflow",

// Different names used inside tests
Expand Down Expand Up @@ -51,6 +50,7 @@
"filepaths",
"hardcoded",
"heredoc",
"iteratable",
"lexable",
"lexed",
"lexes",
Expand All @@ -63,6 +63,7 @@
"nullability",
"nullish",
"passthrough",
"polyfilled",
"promisify",
"pubsub",
"punctuator",
Expand Down
4 changes: 1 addition & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,7 @@
"version": "node resources/gen-version.js && npm test && git add src/version.js",
"gitpublish": ". ./resources/gitpublish.sh"
},
"dependencies": {
"iterall": "^1.3.0"
},
"dependencies": {},
"devDependencies": {
"@babel/core": "7.6.2",
"@babel/plugin-transform-flow-strip-types": "7.4.4",
Expand Down
9 changes: 5 additions & 4 deletions src/execution/execute.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// @flow strict

import { forEach, isCollection } from 'iterall';
import arrayFrom from '../polyfills/arrayFrom';

import inspect from '../jsutils/inspect';
import memoize3 from '../jsutils/memoize3';
Expand All @@ -11,6 +11,7 @@ import isNullish from '../jsutils/isNullish';
import isPromise from '../jsutils/isPromise';
import { type ObjMap } from '../jsutils/ObjMap';
import isObjectLike from '../jsutils/isObjectLike';
import isCollection from '../jsutils/isCollection';
import promiseReduce from '../jsutils/promiseReduce';
import promiseForObject from '../jsutils/promiseForObject';
import { type PromiseOrValue } from '../jsutils/PromiseOrValue';
Expand Down Expand Up @@ -910,8 +911,7 @@ function completeListValue(
// where the list contains no Promises by avoiding creating another Promise.
const itemType = returnType.ofType;
let containsPromise = false;
const completedResults = [];
forEach((result: any), (item, index) => {
const completedResults = arrayFrom(result, (item, index) => {
// No need to modify the info object containing the path,
// since from here on it is not ever accessed by resolver functions.
const fieldPath = addPath(path, index);
Expand All @@ -927,7 +927,8 @@ function completeListValue(
if (!containsPromise && isPromise(completedItem)) {
containsPromise = true;
}
completedResults.push(completedItem);

return completedItem;
});

return containsPromise ? Promise.all(completedResults) : completedResults;
Expand Down
70 changes: 70 additions & 0 deletions src/jsutils/__tests__/isCollection-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// @flow strict

import { expect } from 'chai';
import { describe, it } from 'mocha';

import identityFunc from '../identityFunc';
import isCollection from '../isCollection';

describe('isCollection', () => {
it('should return `true` for collections', () => {
expect(isCollection([])).to.equal(true);
expect(isCollection(new Int8Array(1))).to.equal(true);

// eslint-disable-next-line no-new-wrappers
expect(isCollection(new String('ABC'))).to.equal(true);

function getArguments() {
return arguments;
}
expect(isCollection(getArguments())).to.equal(true);

const arrayLike = {
length: 3,
'0': 'Alpha',
'1': 'Bravo',
'2': 'Charlie',
};
expect(isCollection(arrayLike)).to.equal(true);

const iterator = { [Symbol.iterator]: identityFunc };
expect(isCollection(iterator)).to.equal(true);

// istanbul ignore next
function* generatorFunc() {
/* do nothing */
}
expect(isCollection(generatorFunc())).to.equal(true);
});

it('should return `false` for non-collections', () => {
expect(isCollection(null)).to.equal(false);
expect(isCollection(undefined)).to.equal(false);

expect(isCollection('ABC')).to.equal(false);
expect(isCollection('0')).to.equal(false);
expect(isCollection('')).to.equal(false);

expect(isCollection(1)).to.equal(false);
expect(isCollection(0)).to.equal(false);
expect(isCollection(NaN)).to.equal(false);
// eslint-disable-next-line no-new-wrappers
expect(isCollection(new Number(123))).to.equal(false);

expect(isCollection(true)).to.equal(false);
expect(isCollection(false)).to.equal(false);
// eslint-disable-next-line no-new-wrappers
expect(isCollection(new Boolean(true))).to.equal(false);

expect(isCollection({})).to.equal(false);
expect(isCollection({ iterable: true })).to.equal(false);

const iteratorWithoutSymbol = { next: identityFunc };
expect(isCollection(iteratorWithoutSymbol)).to.equal(false);

const iteratorWithInvalidTypedSymbol = {
[Symbol.iterator]: { next: identityFunc },
};
expect(isCollection(iteratorWithInvalidTypedSymbol)).to.equal(false);
});
});
39 changes: 39 additions & 0 deletions src/jsutils/isCollection.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// @flow strict

import { SYMBOL_ITERATOR } from '../polyfills/symbols';

/**
* Returns true if the provided object is an Object (i.e. not a string literal)
* and is either Iterable or Array-like.
*
* This may be used in place of [Array.isArray()][isArray] to determine if an
* object should be iterated-over. It always excludes string literals and
* includes Arrays (regardless of if it is Iterable). It also includes other
* Array-like objects such as NodeList, TypedArray, and Buffer.
*
* @example
*
* isCollection([ 1, 2, 3 ]) // true
* isCollection('ABC') // false
* isCollection({ length: 1, 0: 'Alpha' }) // true
* isCollection({ key: 'value' }) // false
* isCollection(new Map()) // true
*
* @param obj
* An Object value which might implement the Iterable or Array-like protocols.
* @return {boolean} true if Iterable or Array-like Object.
*/
export default function isCollection(obj: mixed): boolean {
if (obj == null || typeof obj !== 'object') {
return false;
}

// Is Array like?
const length = obj.length;
if (typeof length === 'number' && length >= 0 && length % 1 === 0) {
return true;
}

// Is Iterable?
return typeof obj[SYMBOL_ITERATOR] === 'function';
}
57 changes: 57 additions & 0 deletions src/polyfills/arrayFrom.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// @flow strict

import { SYMBOL_ITERATOR } from './symbols';

declare function arrayFrom<T: mixed>(
arrayLike: mixed,
mapFn?: (elem: mixed, index: number) => T,
thisArg?: mixed,
): Array<T>;

/* eslint-disable no-redeclare */
// $FlowFixMe
const arrayFrom =
Array.from ||
function(obj, mapFn, thisArg) {
if (obj == null) {
throw new TypeError(
'Array.from requires an array-like object - not null or undefined',
);
}

// Is Iterable?
const iteratorMethod = obj[SYMBOL_ITERATOR];
if (typeof iteratorMethod === 'function') {
const iterator = iteratorMethod.call(obj);
const result = [];
let step;

for (let i = 0; !(step = iterator.next()).done; ++i) {
result.push(mapFn.call(thisArg, step.value, i));
// Infinite Iterators could cause forEach to run forever.
// After a very large number of iterations, produce an error.
/* istanbul ignore if */
if (i > 9999999) {
throw new TypeError('Near-infinite iteration.');
}
}
return result;
}

// Is Array like?
const length = obj.length;
if (typeof length === 'number' && length >= 0 && length % 1 === 0) {
const result = [];

for (let i = 0; i < length; ++i) {
if (Object.prototype.hasOwnProperty.call(obj, i)) {
result.push(mapFn.call(thisArg, obj[i], i));
}
}
return result;
}

return [];
};

export default arrayFrom;
12 changes: 12 additions & 0 deletions src/polyfills/symbols.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// @flow strict

// In ES2015 (or a polyfilled) environment, this will be Symbol.iterator
/* istanbul ignore next (See: https://github.com/graphql/graphql-js/issues/2317) */
export const SYMBOL_ITERATOR: string =
typeof Symbol === 'function' ? Symbol.iterator : '@@iterator';

// In ES2017 (or a polyfilled) environment, this will be Symbol.asyncIterator
/* istanbul ignore next (See: https://github.com/graphql/graphql-js/issues/2317) */
export const SYMBOL_ASYNC_ITERATOR: string =
// $FlowFixMe Flow doesn't define `Symbol.asyncIterator` yet
typeof Symbol === 'function' ? Symbol.asyncIterator : '@@asyncIterator';
8 changes: 5 additions & 3 deletions src/subscription/mapAsyncIterator.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// @flow strict

import { $$asyncIterator, getAsyncIterator } from 'iterall';
import { SYMBOL_ASYNC_ITERATOR } from '../polyfills/symbols';

import { type PromiseOrValue } from '../jsutils/PromiseOrValue';

Expand All @@ -13,7 +13,9 @@ export default function mapAsyncIterator<T, U>(
callback: T => PromiseOrValue<U>,
rejectCallback?: any => PromiseOrValue<U>,
): AsyncGenerator<U, void, void> {
const iterator = getAsyncIterator(iterable);
// $FlowFixMe
const iteratorMethod = iterable[SYMBOL_ASYNC_ITERATOR];
const iterator: AsyncIterator<T> = iteratorMethod.call(iterable);
let $return;
let abruptClose;
// $FlowFixMe(>=0.68.0)
Expand Down Expand Up @@ -57,7 +59,7 @@ export default function mapAsyncIterator<T, U>(
}
return Promise.reject(error).catch(abruptClose);
},
[$$asyncIterator]() {
[SYMBOL_ASYNC_ITERATOR]() {
return this;
},
}: any);
Expand Down
14 changes: 13 additions & 1 deletion src/subscription/subscribe.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// @flow strict

import { isAsyncIterable } from 'iterall';
import { SYMBOL_ASYNC_ITERATOR } from '../polyfills/symbols';

import inspect from '../jsutils/inspect';
import { addPath, pathToArray } from '../jsutils/Path';
Expand Down Expand Up @@ -298,3 +298,15 @@ export function createSourceEventStream(
: Promise.reject(error);
}
}

/**
* Returns true if the provided object implements the AsyncIterator protocol via
* either implementing a `Symbol.asyncIterator` or `"@@asyncIterator"` method.
*/
function isAsyncIterable(maybeAsyncIterable: mixed): boolean {
if (maybeAsyncIterable == null || typeof maybeAsyncIterable !== 'object') {
return false;
}

return typeof maybeAsyncIterable[SYMBOL_ASYNC_ITERATOR] === 'function';
}
12 changes: 7 additions & 5 deletions src/utilities/astFromValue.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
// @flow strict

import { forEach, isCollection } from 'iterall';

import arrayFrom from '../polyfills/arrayFrom';
import objectValues from '../polyfills/objectValues';

import inspect from '../jsutils/inspect';
import invariant from '../jsutils/invariant';
import isNullish from '../jsutils/isNullish';
import isInvalid from '../jsutils/isInvalid';
import isObjectLike from '../jsutils/isObjectLike';
import isCollection from '../jsutils/isCollection';

import { Kind } from '../language/kinds';
import { type ValueNode } from '../language/ast';
Expand Down Expand Up @@ -69,12 +69,14 @@ export function astFromValue(value: mixed, type: GraphQLInputType): ?ValueNode {
const itemType = type.ofType;
if (isCollection(value)) {
const valuesNodes = [];
forEach((value: any), item => {
// Since we transpile for-of in loose mode it doesn't support iterators
// and it's required to first convert iteratable into array
for (const item of arrayFrom(value)) {
const itemNode = astFromValue(item, itemType);
if (itemNode) {
if (itemNode != null) {
valuesNodes.push(itemNode);
}
});
}
return { kind: Kind.LIST, values: valuesNodes };
}
return astFromValue(value, itemType);
Expand Down
18 changes: 5 additions & 13 deletions src/utilities/coerceInputValue.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
// @flow strict

import { forEach, isCollection } from 'iterall';

import arrayFrom from '../polyfills/arrayFrom';
import objectValues from '../polyfills/objectValues';

import inspect from '../jsutils/inspect';
import invariant from '../jsutils/invariant';
import didYouMean from '../jsutils/didYouMean';
import isObjectLike from '../jsutils/isObjectLike';
import isCollection from '../jsutils/isCollection';
import suggestionList from '../jsutils/suggestionList';
import printPathArray from '../jsutils/printPathArray';
import { type Path, addPath, pathToArray } from '../jsutils/Path';
Expand Down Expand Up @@ -79,18 +79,10 @@ function coerceInputValueImpl(
if (isListType(type)) {
const itemType = type.ofType;
if (isCollection(inputValue)) {
const coercedValue = [];
forEach((inputValue: any), (itemValue, index) => {
coercedValue.push(
coerceInputValueImpl(
itemValue,
itemType,
onError,
addPath(path, index),
),
);
return arrayFrom(inputValue, (itemValue, index) => {
const itemPath = addPath(path, index);
return coerceInputValueImpl(itemValue, itemType, onError, itemPath);
});
return coercedValue;
}
// Lists accept a non-list value as a list of one.
return [coerceInputValueImpl(inputValue, itemType, onError, path)];
Expand Down
5 changes: 0 additions & 5 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2426,11 +2426,6 @@ iterable-to-stream@^1.0.1:
resolved "https://registry.yarnpkg.com/iterable-to-stream/-/iterable-to-stream-1.0.1.tgz#37e86baacf6b1a0e9233dad4eb526d0423d08bf3"
integrity sha512-O62gD5ADMUGtJoOoM9U6LQ7i4byPXUNoHJ6mqsmkQJcom331ZJGDApWgDESWyBMEHEJRjtHozgIiTzYo9RU4UA==

iterall@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.3.0.tgz#afcb08492e2915cbd8a0884eb93a8c94d0d72fea"
integrity sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg==

js-levenshtein@^1.1.3:
version "1.1.6"
resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.6.tgz#c6cee58eb3550372df8deb85fad5ce66ce01d59d"
Expand Down

0 comments on commit bde5e4a

Please sign in to comment.