Skip to content

Commit

Permalink
Merge pull request #531 from apollostack/transport_batching
Browse files Browse the repository at this point in the history
Transport-level query batching
  • Loading branch information
helfer authored Aug 30, 2016
2 parents 04dc479 + 01e03e3 commit 1711d7a
Show file tree
Hide file tree
Showing 7 changed files with 561 additions and 39 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Expect active development and potentially significant breaking changes in the `0
- Fixed issue with error passing with query merging. [PR #589](https://github.com/apollostack/apollo-client/pull/589) and [Issue #551](https://github.com/apollostack/apollo-client/issues/551).
- [Experimental] Change subscription API to `subscribe` function on Apollo Client instance, and remove `fetchMore`-style API temporarily.
- Fixed an issue with batching and variables used in directives. [PR #584](https://github.com/apollostack/apollo-client/pull/584) and [Issue #577](https://github.com/apollostack/apollo-client/issues/577).
- Implemented transport-level batching the way it is currently supported within Apollo Server. [PR #531](https://github.com/apollostack/apollo-client/pull/531) and [Issue #505](https://github.com/apollostack/apollo-client/issues/505).

### v0.4.13

Expand Down
95 changes: 95 additions & 0 deletions src/batchedNetworkInterface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import {
HTTPFetchNetworkInterface,
RequestAndOptions,
Request,
printRequest,
BatchedNetworkInterface,
} from './networkInterface';

import {
GraphQLResult,
} from 'graphql';

import 'whatwg-fetch';

import assign = require('lodash.assign');

// An implementation of the network interface that operates over HTTP and batches
// together requests over the HTTP transport. Note that this implementation will only work correctly
// for GraphQL server implementations that support batching. If such a server is not available, you
// should see `addQueryMerging` instead.
export class HTTPBatchedNetworkInterface extends HTTPFetchNetworkInterface implements BatchedNetworkInterface {
constructor(uri: string, opts: RequestInit) {
super(uri, opts);
};

public batchedFetchFromRemoteEndpoint(
requestsAndOptions: RequestAndOptions[]
): Promise<IResponse> {
const options: RequestInit = {};

// Combine all of the options given by the middleware into one object.
requestsAndOptions.forEach((requestAndOptions) => {
assign(options, requestAndOptions.options);
});

// Serialize the requests to strings of JSON
const printedRequests = requestsAndOptions.map(({ request }) => {
return printRequest(request);
});

return fetch(this._uri, assign({}, this._opts, options, {
body: JSON.stringify(printedRequests),
headers: assign({}, options.headers, {
Accept: '*/*',
'Content-Type': 'application/json',
}),
method: 'POST',
}));
};

public batchQuery(requests: Request[]): Promise<GraphQLResult[]> {
const options = assign({}, this._opts);

// Apply the middlewares to each of the requests
const middlewarePromises: Promise<RequestAndOptions>[] = [];
requests.forEach((request) => {
middlewarePromises.push(this.applyMiddlewares({
request,
options,
}));
});

return new Promise((resolve, reject) => {
Promise.all(middlewarePromises).then((requestsAndOptions: RequestAndOptions[]) => {
return this.batchedFetchFromRemoteEndpoint(requestsAndOptions)
.then(result => {
return result.json();
})
.then(responses => {
const afterwaresPromises = responses.map((response: IResponse, index: number) => {
return this.applyAfterwares({
response,
options: requestsAndOptions[index].options,
});
});

Promise.all(afterwaresPromises).then((responsesAndOptions: {
response: IResponse,
options: RequestInit,
}[]) => {
const results: Array<IResponse> = [];
responsesAndOptions.forEach(({ response }) => {
results.push(response);
});
resolve(results);
}).catch((error) => {
reject(error);
});
});
}).catch((error) => {
reject(error);
});
});
}
}
117 changes: 78 additions & 39 deletions src/networkInterface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,21 +98,28 @@ export function printRequest(request: Request): PrintedRequest {
return printedRequest;
}

export function createNetworkInterface(uri: string, opts: RequestInit = {}): HTTPNetworkInterface {
if (!uri) {
throw new Error('A remote enpdoint is required for a network layer');
}
export class HTTPFetchNetworkInterface implements NetworkInterface {
public _uri: string;
public _opts: RequestInit;
public _middlewares: MiddlewareInterface[];
public _afterwares: AfterwareInterface[];

if (!isString(uri)) {
throw new Error('Remote endpoint must be a string');
}
constructor(uri: string, opts: RequestInit = {}) {
if (!uri) {
throw new Error('A remote enpdoint is required for a network layer');
}

if (!isString(uri)) {
throw new Error('Remote endpoint must be a string');
}

const _uri: string = uri;
const _opts: RequestInit = assign({}, opts);
const _middlewares: MiddlewareInterface[] = [];
const _afterwares: AfterwareInterface[] = [];
this._uri = uri;
this._opts = assign({}, opts);
this._middlewares = [];
this._afterwares = [];
}

function applyMiddlewares({
public applyMiddlewares({
request,
options,
}: RequestAndOptions): Promise<RequestAndOptions> {
Expand All @@ -133,11 +140,11 @@ export function createNetworkInterface(uri: string, opts: RequestInit = {}): HTT
};

// iterate through middlewares using next callback
queue([..._middlewares], this);
queue([...this._middlewares], this);
});
}

function applyAfterwares({
public applyAfterwares({
response,
options,
}: ResponseAndOptions): Promise<ResponseAndOptions> {
Expand All @@ -158,15 +165,15 @@ export function createNetworkInterface(uri: string, opts: RequestInit = {}): HTT
};

// iterate through afterwares using next callback
queue([..._afterwares], this);
queue([...this._afterwares], this);
});
}

function fetchFromRemoteEndpoint({
public fetchFromRemoteEndpoint({
request,
options,
}: RequestAndOptions): Promise<IResponse> {
return fetch(uri, assign({}, _opts, options, {
return fetch(this._uri, assign({}, this._opts, options, {
body: JSON.stringify(printRequest(request)),
headers: assign({}, options.headers, {
Accept: '*/*',
Expand All @@ -176,21 +183,21 @@ export function createNetworkInterface(uri: string, opts: RequestInit = {}): HTT
}));
};

function query(request: Request): Promise<GraphQLResult> {
const options = assign({}, _opts);
public query(request: Request): Promise<GraphQLResult> {
const options = assign({}, this._opts);

return applyMiddlewares({
return this.applyMiddlewares({
request,
options,
}).then(fetchFromRemoteEndpoint)
}).then(this.fetchFromRemoteEndpoint.bind(this))
.then(response => {
applyAfterwares({
response,
this.applyAfterwares({
response: response as IResponse,
options,
});
return response;
})
.then(result => result.json())
.then(result => (result as IResponse).json())
.then((payload: GraphQLResult) => {
if (!payload.hasOwnProperty('data') && !payload.hasOwnProperty('errors')) {
throw new Error(
Expand All @@ -202,35 +209,67 @@ export function createNetworkInterface(uri: string, opts: RequestInit = {}): HTT
});
};

function use(middlewares: MiddlewareInterface[]) {
public use(middlewares: MiddlewareInterface[]) {
middlewares.map((middleware) => {
if (typeof middleware.applyMiddleware === 'function') {
_middlewares.push(middleware);
this._middlewares.push(middleware);
} else {
throw new Error('Middleware must implement the applyMiddleware function');
}
});
}

function useAfter(afterwares: AfterwareInterface[]) {
public useAfter(afterwares: AfterwareInterface[]) {
afterwares.map(afterware => {
if (typeof afterware.applyAfterware === 'function') {
_afterwares.push(afterware);
this._afterwares.push(afterware);
} else {
throw new Error('Afterware must implement the applyAfterware function');
}
});
}
}

// createNetworkInterface has batching ability by default, which is not used unless the
// `shouldBatch` option is passed to apollo-client
return addQueryMerging({
_uri,
_opts,
_middlewares,
_afterwares,
query,
use,
useAfter,
}) as HTTPNetworkInterface;
// This import has to be placed here due to a bug in TypeScript:
// https://github.com/Microsoft/TypeScript/issues/21
import {
HTTPBatchedNetworkInterface,
} from './batchedNetworkInterface';


export interface NetworkInterfaceOptions {
uri: string;
opts?: RequestInit;
transportBatching?: boolean;
}

// This function is written to preserve backwards compatibility.
// Specifically, there are two ways of calling `createNetworkInterface`:
// 1. createNetworkInterface(uri: string, opts: RequestInit)
// 2. createnetworkInterface({ uri: string, opts: RequestInit, transportBatching: boolean})
// where the latter is preferred over the former.
//
// XXX remove this backward compatibility feature by 1.0
export function createNetworkInterface(
interfaceOpts: (NetworkInterfaceOptions | string),
backOpts: RequestInit = {}
): HTTPNetworkInterface {
if (isString(interfaceOpts) || !interfaceOpts) {
const uri = interfaceOpts as string;
return addQueryMerging(new HTTPFetchNetworkInterface(uri, backOpts)) as HTTPNetworkInterface;
} else {
const {
transportBatching = false,
opts = {},
uri,
} = interfaceOpts as NetworkInterfaceOptions;
if (transportBatching) {
// We can use transport batching rather than query merging.
return new HTTPBatchedNetworkInterface(uri, opts) as HTTPNetworkInterface;
} else {
// createNetworkInterface has batching ability by default through query merging,
// which is not used unless the `shouldBatch` option is passed to apollo-client.
return addQueryMerging(new HTTPFetchNetworkInterface(uri, opts)) as HTTPNetworkInterface;
}
}
}
Loading

0 comments on commit 1711d7a

Please sign in to comment.