Skip to content

Commit a13abf7

Browse files
authored
Merge pull request #846 from apollographql/subscriptions
Subscriptions support
2 parents 65ab0a8 + 5f25f53 commit a13abf7

10 files changed

+1202
-51
lines changed

src/execution/execute.js

Lines changed: 67 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ import type {
8080
* Namely, schema of the type system that is currently executing,
8181
* and the fragments defined in the query document
8282
*/
83-
type ExecutionContext = {
83+
export type ExecutionContext = {
8484
schema: GraphQLSchema;
8585
fragments: {[key: string]: FragmentDefinitionNode};
8686
rootValue: mixed;
@@ -117,22 +117,6 @@ export function execute(
117117
variableValues?: ?{[key: string]: mixed},
118118
operationName?: ?string
119119
): Promise<ExecutionResult> {
120-
invariant(schema, 'Must provide schema');
121-
invariant(document, 'Must provide document');
122-
invariant(
123-
schema instanceof GraphQLSchema,
124-
'Schema must be an instance of GraphQLSchema. Also ensure that there are ' +
125-
'not multiple versions of GraphQL installed in your node_modules directory.'
126-
);
127-
128-
// Variables, if provided, must be an object.
129-
invariant(
130-
!variableValues || typeof variableValues === 'object',
131-
'Variables must be provided as an Object where each property is a ' +
132-
'variable value. Perhaps look to see if an unparsed JSON string ' +
133-
'was provided.'
134-
);
135-
136120
// If a valid context cannot be created due to incorrect arguments,
137121
// this will throw an error.
138122
const context = buildExecutionContext(
@@ -183,8 +167,11 @@ export function responsePathAsArray(
183167
return flattened.reverse();
184168
}
185169

186-
187-
function addPath(prev: ResponsePath, key: string | number) {
170+
/**
171+
* Given a ResponsePath and a key, return a new ResponsePath containing the
172+
* new key.
173+
*/
174+
export function addPath(prev: ResponsePath, key: string | number) {
188175
return { prev, key };
189176
}
190177

@@ -194,14 +181,30 @@ function addPath(prev: ResponsePath, key: string | number) {
194181
*
195182
* Throws a GraphQLError if a valid execution context cannot be created.
196183
*/
197-
function buildExecutionContext(
184+
export function buildExecutionContext(
198185
schema: GraphQLSchema,
199186
document: DocumentNode,
200187
rootValue: mixed,
201188
contextValue: mixed,
202189
rawVariableValues: ?{[key: string]: mixed},
203190
operationName: ?string
204191
): ExecutionContext {
192+
invariant(schema, 'Must provide schema');
193+
invariant(document, 'Must provide document');
194+
invariant(
195+
schema instanceof GraphQLSchema,
196+
'Schema must be an instance of GraphQLSchema. Also ensure that there are ' +
197+
'not multiple versions of GraphQL installed in your node_modules directory.'
198+
);
199+
200+
// Variables, if provided, must be an object.
201+
invariant(
202+
!rawVariableValues || typeof rawVariableValues === 'object',
203+
'Variables must be provided as an Object where each property is a ' +
204+
'variable value. Perhaps look to see if an unparsed JSON string ' +
205+
'was provided.'
206+
);
207+
205208
const errors: Array<GraphQLError> = [];
206209
let operation: ?OperationDefinitionNode;
207210
const fragments: {[name: string]: FragmentDefinitionNode} =
@@ -280,7 +283,7 @@ function executeOperation(
280283
/**
281284
* Extracts the root type of the operation from the schema.
282285
*/
283-
function getOperationRootType(
286+
export function getOperationRootType(
284287
schema: GraphQLSchema,
285288
operation: OperationDefinitionNode
286289
): GraphQLObjectType {
@@ -408,7 +411,7 @@ function executeFields(
408411
* returns an Interface or Union type, the "runtime type" will be the actual
409412
* Object type returned by that field.
410413
*/
411-
function collectFields(
414+
export function collectFields(
412415
exeContext: ExecutionContext,
413416
runtimeType: GraphQLObjectType,
414417
selectionSet: SelectionSetNode,
@@ -577,60 +580,68 @@ function resolveField(
577580
return;
578581
}
579582

580-
const returnType = fieldDef.type;
581583
const resolveFn = fieldDef.resolve || defaultFieldResolver;
582584

583-
// The resolve function's optional third argument is a context value that
584-
// is provided to every resolve function within an execution. It is commonly
585-
// used to represent an authenticated user, or request-specific caches.
586-
const context = exeContext.contextValue;
587-
588-
// The resolve function's optional fourth argument is a collection of
589-
// information about the current execution state.
590-
const info: GraphQLResolveInfo = {
591-
fieldName,
585+
const info = buildResolveInfo(
586+
exeContext,
587+
fieldDef,
592588
fieldNodes,
593-
returnType,
594589
parentType,
595-
path,
596-
schema: exeContext.schema,
597-
fragments: exeContext.fragments,
598-
rootValue: exeContext.rootValue,
599-
operation: exeContext.operation,
600-
variableValues: exeContext.variableValues,
601-
};
590+
path
591+
);
602592

603593
// Get the resolve function, regardless of if its result is normal
604594
// or abrupt (error).
605-
const result = resolveOrError(
595+
const result = resolveFieldValueOrError(
606596
exeContext,
607597
fieldDef,
608-
fieldNode,
598+
fieldNodes,
609599
resolveFn,
610600
source,
611-
context,
612601
info
613602
);
614603

615604
return completeValueCatchingError(
616605
exeContext,
617-
returnType,
606+
fieldDef.type,
618607
fieldNodes,
619608
info,
620609
path,
621610
result
622611
);
623612
}
624613

614+
export function buildResolveInfo(
615+
exeContext: ExecutionContext,
616+
fieldDef: GraphQLField<*, *>,
617+
fieldNodes: Array<FieldNode>,
618+
parentType: GraphQLObjectType,
619+
path: ResponsePath
620+
): GraphQLResolveInfo {
621+
// The resolve function's optional fourth argument is a collection of
622+
// information about the current execution state.
623+
return {
624+
fieldName: fieldNodes[0].name.value,
625+
fieldNodes,
626+
returnType: fieldDef.type,
627+
parentType,
628+
path,
629+
schema: exeContext.schema,
630+
fragments: exeContext.fragments,
631+
rootValue: exeContext.rootValue,
632+
operation: exeContext.operation,
633+
variableValues: exeContext.variableValues,
634+
};
635+
}
636+
625637
// Isolates the "ReturnOrAbrupt" behavior to not de-opt the `resolveField`
626638
// function. Returns the result of resolveFn or the abrupt-return Error object.
627-
function resolveOrError<TSource, TContext>(
639+
export function resolveFieldValueOrError<TSource>(
628640
exeContext: ExecutionContext,
629-
fieldDef: GraphQLField<TSource, TContext>,
630-
fieldNode: FieldNode,
631-
resolveFn: GraphQLFieldResolver<TSource, TContext>,
641+
fieldDef: GraphQLField<TSource, *>,
642+
fieldNodes: Array<FieldNode>,
643+
resolveFn: GraphQLFieldResolver<TSource, *>,
632644
source: TSource,
633-
context: TContext,
634645
info: GraphQLResolveInfo
635646
): Error | mixed {
636647
try {
@@ -639,10 +650,15 @@ function resolveOrError<TSource, TContext>(
639650
// TODO: find a way to memoize, in case this field is within a List type.
640651
const args = getArgumentValues(
641652
fieldDef,
642-
fieldNode,
653+
fieldNodes[0],
643654
exeContext.variableValues
644655
);
645656

657+
// The resolve function's optional third argument is a context value that
658+
// is provided to every resolve function within an execution. It is commonly
659+
// used to represent an authenticated user, or request-specific caches.
660+
const context = exeContext.contextValue;
661+
646662
return resolveFn(source, args, context, info);
647663
} catch (error) {
648664
// Sometimes a non-error is thrown, wrap it as an Error for a
@@ -1178,7 +1194,7 @@ function getPromise<T>(value: Promise<T> | mixed): Promise<T> | void {
11781194
* added to the query type, but that would require mutating type
11791195
* definitions, which would cause issues.
11801196
*/
1181-
function getFieldDef(
1197+
export function getFieldDef(
11821198
schema: GraphQLSchema,
11831199
parentType: GraphQLObjectType,
11841200
fieldName: string

src/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,7 @@ export type {
243243
ExecutionResult,
244244
} from './execution';
245245

246+
export { subscribe, createSubscriptionSourceEventStream } from './subscription';
246247

247248
// Validate GraphQL queries.
248249
export {
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/**
2+
* Copyright (c) 2017, Facebook, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*/
9+
10+
import { expect } from 'chai';
11+
import { describe, it } from 'mocha';
12+
13+
import EventEmitter from 'events';
14+
import eventEmitterAsyncIterator from './eventEmitterAsyncIterator';
15+
16+
describe('eventEmitterAsyncIterator', () => {
17+
18+
it('subscribe async-iterator mock', async () => {
19+
// Create an AsyncIterator from an EventEmitter
20+
const emitter = new EventEmitter();
21+
const iterator = eventEmitterAsyncIterator(emitter, 'publish');
22+
23+
// Queue up publishes
24+
expect(emitter.emit('publish', 'Apple')).to.equal(true);
25+
expect(emitter.emit('publish', 'Banana')).to.equal(true);
26+
27+
// Read payloads
28+
expect(await iterator.next()).to.deep.equal(
29+
{ done: false, value: 'Apple' }
30+
);
31+
expect(await iterator.next()).to.deep.equal(
32+
{ done: false, value: 'Banana' }
33+
);
34+
35+
// Read ahead
36+
const i3 = iterator.next().then(x => x);
37+
const i4 = iterator.next().then(x => x);
38+
39+
// Publish
40+
expect(emitter.emit('publish', 'Coconut')).to.equal(true);
41+
expect(emitter.emit('publish', 'Durian')).to.equal(true);
42+
43+
// Await out of order to get correct results
44+
expect(await i4).to.deep.equal({ done: false, value: 'Durian'});
45+
expect(await i3).to.deep.equal({ done: false, value: 'Coconut'});
46+
47+
// Read ahead
48+
const i5 = iterator.next().then(x => x);
49+
50+
// Terminate emitter
51+
await iterator.return();
52+
53+
// Publish is not caught after terminate
54+
expect(emitter.emit('publish', 'Fig')).to.equal(false);
55+
56+
// Find that cancelled read-ahead got a "done" result
57+
expect(await i5).to.deep.equal({ done: true, value: undefined });
58+
59+
// And next returns empty completion value
60+
expect(await iterator.next()).to.deep.equal(
61+
{ done: true, value: undefined }
62+
);
63+
});
64+
});
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/**
2+
* Copyright (c) 2017, Facebook, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*
9+
* @flow
10+
*/
11+
12+
import type EventEmitter from 'events';
13+
import { $$asyncIterator } from 'iterall';
14+
15+
/**
16+
* Create an AsyncIterator from an EventEmitter. Useful for mocking a
17+
* PubSub system for tests.
18+
*/
19+
export default function eventEmitterAsyncIterator(
20+
eventEmitter: EventEmitter,
21+
eventName: string
22+
): AsyncIterator<mixed> {
23+
const pullQueue = [];
24+
const pushQueue = [];
25+
let listening = true;
26+
eventEmitter.addListener(eventName, pushValue);
27+
28+
function pushValue(event) {
29+
if (pullQueue.length !== 0) {
30+
pullQueue.shift()({ value: event, done: false });
31+
} else {
32+
pushQueue.push(event);
33+
}
34+
}
35+
36+
function pullValue() {
37+
return new Promise(resolve => {
38+
if (pushQueue.length !== 0) {
39+
resolve({ value: pushQueue.shift(), done: false });
40+
} else {
41+
pullQueue.push(resolve);
42+
}
43+
});
44+
}
45+
46+
function emptyQueue() {
47+
if (listening) {
48+
listening = false;
49+
eventEmitter.removeListener(eventName, pushValue);
50+
pullQueue.forEach(resolve => resolve({ value: undefined, done: true }));
51+
pullQueue.length = 0;
52+
pushQueue.length = 0;
53+
}
54+
}
55+
56+
return {
57+
next() {
58+
return listening ? pullValue() : this.return();
59+
},
60+
return() {
61+
emptyQueue();
62+
return Promise.resolve({ value: undefined, done: true });
63+
},
64+
throw(error) {
65+
emptyQueue();
66+
return Promise.reject(error);
67+
},
68+
[$$asyncIterator]() {
69+
return this;
70+
},
71+
};
72+
}

0 commit comments

Comments
 (0)