Skip to content
This repository has been archived by the owner on Apr 13, 2023. It is now read-only.

Subs #256

Merged
merged 3 commits into from
Oct 8, 2016
Merged

Subs #256

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
1 change: 1 addition & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Expect active development and potentially significant breaking changes in the `0
- Feature: Remove nested imports for apollo-client. Making local development eaiser. [#234](https://github.com/apollostack/react-apollo/pull/234)
- Feature: Move types to dev deps [#251](https://github.com/apollostack/react-apollo/pull/251)
- Feature: New method for skipping queries which bypasses HOC internals [#253](https://github.com/apollostack/react-apollo/pull/253)
- Feature: Integrated subscriptions! [#256](https://github.com/apollostack/react-apollo/pull/256)

### v0.5.7

Expand Down
100 changes: 63 additions & 37 deletions src/graphql.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -142,14 +142,8 @@ export default function graphql(
const graphQLDisplayName = `Apollo(${getDisplayName(WrappedComponent)})`;

function calculateFragments(fragments): FragmentDefinition[] {
if (!fragments && !operation.fragments.length) {
return fragments;
}

if (!fragments) {
return fragments = flatten([...operation.fragments]);
}

if (!fragments && !operation.fragments.length) return fragments;
if (!fragments) return fragments = flatten([...operation.fragments]);
return flatten([...fragments, ...operation.fragments]);
}

Expand Down Expand Up @@ -189,7 +183,10 @@ export default function graphql(

function fetchData(props, { client }) {
if (mapPropsToSkip(props)) return;
if (operation.type === DocumentType.Mutation) return false;
if (
operation.type === DocumentType.Mutation || operation.type === DocumentType.Subscription
) return false;

const opts = calculateOptions(props) as any;
opts.query = document;

Expand Down Expand Up @@ -302,14 +299,15 @@ export default function graphql(

componentWillUnmount() {
if (this.type === DocumentType.Query) this.unsubscribeFromQuery();
if (this.type === DocumentType.Subscription) this.unsubscribeFromQuery();

this.hasMounted = false;
}

calculateOptions(props, newProps?) { return calculateOptions(props, newProps); };

calculateResultProps(result) {
let name = this.type === DocumentType.Query ? 'data' : 'mutate';
let name = this.type === DocumentType.Mutation ? 'mutate' : 'data';
if (operationOptions.name) name = operationOptions.name;

const newResult = { [name]: result, ownProps: this.props };
Expand Down Expand Up @@ -369,7 +367,7 @@ export default function graphql(
}

subscribeToQuery(props): boolean {
const { watchQuery } = this.client;
const { watchQuery, subscribe } = this.client;
const opts = calculateOptions(props) as QueryOptions;
if (opts.skip) return;

Expand Down Expand Up @@ -416,8 +414,9 @@ export default function graphql(

const queryOptions: WatchQueryOptions = assign({ query: document }, opts);
queryOptions.fragments = calculateFragments(queryOptions.fragments);
const observableQuery = watchQuery(queryOptions);
const { queryId } = observableQuery;
// tslint:disable-next-line
const observableQuery = this.type === DocumentType.Subscription ? subscribe(queryOptions) : watchQuery(queryOptions);
const { queryId } = (observableQuery as any);

// the shape of the query has changed
if (previousQuery.queryId && previousQuery.queryId !== queryId) {
Expand All @@ -436,7 +435,7 @@ export default function graphql(
}
}

handleQueryData(observableQuery: ObservableQuery, { variables }: WatchQueryOptions): void {
handleQueryData(observableQuery: any, { variables }: WatchQueryOptions): void {
// bind each handle to updating and rerendering when data
// has been recieved
let refetch,
Expand All @@ -446,10 +445,20 @@ export default function graphql(
updateQuery,
oldData = {};

const next = ({ data = oldData, loading, error }: any) => {
const { queryId } = observableQuery;

let initialVariables = this.client.queryManager.getApolloState().queries[queryId].variables;
const next = (result) => {
if (this.type === DocumentType.Subscription) {
result = {
data: result,
loading: false,
error: null,
};
}
const { data = oldData, loading, error }: any = result;
let initialVariables = {};
if (this.type !== DocumentType.Subscription) {
const { queryId } = observableQuery;
initialVariables = this.client.queryManager.getApolloState().queries[queryId].variables;
}

const resultKeyConflict: boolean = (
'errors' in data ||
Expand All @@ -475,16 +484,24 @@ export default function graphql(

// cache the changed data for next check
oldData = assign({}, data);
this.data = assign({
if (this.type === DocumentType.Subscription) {
this.data = assign({
variables: this.data.variables || initialVariables,
loading,
error,
}, data);
} else {
this.data = assign({
variables: this.data.variables || initialVariables,
loading,
refetch,
startPolling,
stopPolling,
fetchMore,
error,
updateQuery,
}, data);
loading,
refetch,
startPolling,
stopPolling,
fetchMore,
error,
updateQuery,
}, data);
}

this.forceRenderChildren();
};
Expand Down Expand Up @@ -525,17 +542,26 @@ export default function graphql(
*/
this.querySubscription = observableQuery.subscribe({ next, error: handleError });

refetch = createBoundRefetch((this.queryObservable as any).refetch);
fetchMore = createBoundRefetch((this.queryObservable as any).fetchMore);
startPolling = (this.queryObservable as any).startPolling;
stopPolling = (this.queryObservable as any).stopPolling;
updateQuery = (this.queryObservable as any).updateQuery;
if (this.type === DocumentType.Query) {
refetch = createBoundRefetch((this.queryObservable as any).refetch);
fetchMore = createBoundRefetch((this.queryObservable as any).fetchMore);
startPolling = (this.queryObservable as any).startPolling;
stopPolling = (this.queryObservable as any).stopPolling;
updateQuery = (this.queryObservable as any).updateQuery;

// XXX the tests seem to be keeping the error around?
delete this.data.error;
this.data = assign(this.data, {
refetch, startPolling, stopPolling, fetchMore, updateQuery, variables,
});
}

if (this.type === DocumentType.Subscription) {
// XXX the tests seem to be keeping the error around?
delete this.data.error;
this.data = assign(this.data, { variables });
}

// XXX the tests seem to be keeping the error around?
delete this.data.error;
this.data = assign(this.data, {
refetch, startPolling, stopPolling, fetchMore, updateQuery, variables,
});
}

forceRenderChildren() {
Expand Down
22 changes: 15 additions & 7 deletions src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import invariant = require('invariant');
export enum DocumentType {
Query,
Mutation,
Subscription,
}

export interface IDocumentDefinition {
Expand All @@ -24,7 +25,7 @@ export interface IDocumentDefinition {
// the parser is mainly a safety check for the HOC
export function parser(document: Document): IDocumentDefinition {
// variables
let fragments, queries, mutations, variables, definitions, type, name;
let fragments, queries, mutations, subscriptions, variables, definitions, type, name;


/*
Expand Down Expand Up @@ -54,26 +55,33 @@ export function parser(document: Document): IDocumentDefinition {
(x: OperationDefinition) => x.kind === 'OperationDefinition' && x.operation === 'mutation'
);

if (fragments.length && (!queries.length || !mutations.length)) {
subscriptions = document.definitions.filter(
(x: OperationDefinition) => x.kind === 'OperationDefinition' && x.operation === 'subscription'
);

if (fragments.length && (!queries.length || !mutations.length || !subscriptions.length)) {
invariant(true,
`Passing only a fragment to 'graphql' is not yet supported. You must include a query or mutation as well`
`Passing only a fragment to 'graphql' is not yet supported. You must include a query, subscription or mutation as well`
);
}

if (queries.length && mutations.length) {
invariant((queries.length && mutations.length),
if (queries.length && mutations.length && mutations.length) {
invariant(((queries.length + mutations.length + mutations.length) > 1),
// tslint:disable-line
`react-apollo only supports a query or a mutation per HOC. ${document} had ${queries.length} queries and ${mutations.length} muations. You can use 'combineOperations' to join multiple operation types to a component`
`react-apollo only supports a query, subscription, or a mutation per HOC. ${document} had ${queries.length} queries, ${subscriptions.length} subscriptions and ${mutations.length} muations. You can use 'compose' to join multiple operation types to a component`
);
}

type = queries.length ? DocumentType.Query : DocumentType.Mutation;
if (!queries.length && !mutations.length) type = DocumentType.Subscription;

definitions = queries.length ? queries : mutations;
if (!queries.length && !mutations.length) definitions = subscriptions;

if (definitions.length !== 1) {
invariant((definitions.length !== 1),
// tslint:disable-line
`react-apollo only supports one defintion per HOC. ${document} had ${definitions.length} definitions. You can use 'combineOperations' to join multiple operation types to a component`
`react-apollo only supports one defintion per HOC. ${document} had ${definitions.length} definitions. You can use 'compose' to join multiple operation types to a component`
);
}

Expand Down
92 changes: 92 additions & 0 deletions test/mocks/mockNetworkInterface.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
NetworkInterface,
Request,
SubscriptionNetworkInterface,
} from 'apollo-client/networkInterface';

import {
Expand All @@ -17,6 +18,12 @@ export default function mockNetworkInterface(
return new MockNetworkInterface(...mockedResponses);
}

export function mockSubscriptionNetworkInterface(
mockedSubscriptions: MockedSubscription[], ...mockedResponses: MockedResponse[]
): MockSubscriptionNetworkInterface {
return new MockSubscriptionNetworkInterface(mockedSubscriptions, ...mockedResponses);
}

export interface ParsedRequest {
variables?: Object;
query?: Document;
Expand All @@ -31,6 +38,18 @@ export interface MockedResponse {
newData?: () => any;
}

export interface MockedSubscriptionResult {
result?: GraphQLResult;
error?: Error;
delay?: number;
}

export interface MockedSubscription {
request: ParsedRequest;
results?: MockedSubscriptionResult[];
id?: number;
}

export class MockNetworkInterface implements NetworkInterface {
private mockedResponsesByKey: { [key: string]: MockedResponse[] } = {};

Expand Down Expand Up @@ -87,6 +106,79 @@ export class MockNetworkInterface implements NetworkInterface {
}
}

export class MockSubscriptionNetworkInterface extends MockNetworkInterface implements SubscriptionNetworkInterface {
public mockedSubscriptionsByKey: { [key: string ]: MockedSubscription[] } = {};
public mockedSubscriptionsById: { [id: number]: MockedSubscription} = {};
public handlersById: {[id: number]: (error: any, result: any) => void} = {};
public subId: number;

constructor(mockedSubscriptions: MockedSubscription[], mockedResponses: MockedResponse[]) {
super(...mockedResponses);
this.subId = 0;
mockedSubscriptions.forEach((sub) => {
this.addMockedSubscription(sub);
});
}
public generateSubscriptionId() {
const requestId = this.subId;
this.subId++;
return requestId;
}

public addMockedSubscription(mockedSubscription: MockedSubscription) {
const key = requestToKey(mockedSubscription.request);
if (mockedSubscription.id === undefined) {
mockedSubscription.id = this.generateSubscriptionId();
}

let mockedSubs = this.mockedSubscriptionsByKey[key];
if (!mockedSubs) {
mockedSubs = [];
this.mockedSubscriptionsByKey[key] = mockedSubs;
}
mockedSubs.push(mockedSubscription);
}

public subscribe(request: Request, handler: (error: any, result: any) => void): number {
const parsedRequest: ParsedRequest = {
query: request.query,
variables: request.variables,
debugName: request.debugName,
};
const key = requestToKey(parsedRequest);
if (this.mockedSubscriptionsByKey.hasOwnProperty(key)) {
const subscription = this.mockedSubscriptionsByKey[key].shift();
this.handlersById[subscription.id] = handler;
this.mockedSubscriptionsById[subscription.id] = subscription;
return subscription.id;
} else {
throw new Error('Network interface does not have subscription associated with this request.');
}

};

public fireResult(id: number) {
const handler = this.handlersById[id];
if (this.mockedSubscriptionsById.hasOwnProperty(id.toString())) {
const subscription = this.mockedSubscriptionsById[id];
if (subscription.results.length === 0) {
throw new Error(`No more mocked subscription responses for the query: ` +
`${print(subscription.request.query)}, variables: ${JSON.stringify(subscription.request.variables)}`);
}
const response = subscription.results.shift();
setTimeout(() => {
handler(response.error, response.result);
}, response.delay ? response.delay : 0);
} else {
throw new Error('Network interface does not have subscription associated with this id.');
}
}

public unsubscribe(id: number) {
delete this.mockedSubscriptionsById[id];
}
}

function requestToKey(request: ParsedRequest): string {
const queryString = request.query && print(request.query);
return JSON.stringify({
Expand Down
Loading