Skip to content
This repository has been archived by the owner on Aug 20, 2020. It is now read-only.

Commit

Permalink
batching support for apollo-fetch with middleware and afterware
Browse files Browse the repository at this point in the history
  • Loading branch information
Evans Hauser committed Jul 22, 2017
1 parent 3080f0e commit dea26a6
Show file tree
Hide file tree
Showing 4 changed files with 573 additions and 297 deletions.
254 changes: 144 additions & 110 deletions packages/apollo-fetch/src/apollo-fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ import {
FetchResult,
RequestAndOptions,
ResponseAndOptions,
RequestsAndOptions,
AfterwareInterface,
MiddlewareInterface,
BatchedAfterwareInterface,
BatchedMiddlewareInterface,
FetchOptions,
ApolloFetch,
ParsedResponse,
Expand All @@ -12,134 +15,150 @@ import {
} from './types';
import 'isomorphic-fetch';

export function createApolloFetch(params: FetchOptions = {}): ApolloFetch {
const {uri, customFetch} = params;

const _uri = uri || '/graphql';
const _middlewares = [];
const _afterwares = [];
type WareStack = MiddlewareInterface[] | BatchedMiddlewareInterface[] | AfterwareInterface[] | BatchedAfterwareInterface[];

const applyMiddlewares = (requestAndOptions: RequestAndOptions): Promise<RequestAndOptions> => {
return new Promise((resolve, reject) => {
const { request, options } = requestAndOptions;
const buildMiddlewareStack = (funcs: MiddlewareInterface[], scope: any) => {
const next = () => {
if (funcs.length > 0) {
const f = funcs.shift();
if (f) {
f.apply(scope, [{ request, options }, next]);
}
} else {
resolve({
request,
options,
});
}
};
next();
};
function buildWareStack<M>(funcs: WareStack, modifiedObject: M, resolve) {
const next = () => {
if (funcs.length > 0) {
const f = funcs.shift();
if (f) {
f.apply(this, [modifiedObject, next]);
}
} else {
resolve(modifiedObject);
}
};
next();
}

buildMiddlewareStack([..._middlewares], this);
});
export function constructDefaultOptions(request: GraphQLRequest | GraphQLRequest[], options: RequestInit) {
let body;
try {
body = JSON.stringify(request);
} catch (e) {
throw new Error(`Network request failed. Payload is not serializable: ${e.message}`);
}

return {
body,
method: 'POST',
...options,
headers: {
Accept: '*/*',
'Content-Type': 'application/json',
...(options.headers || []),
},
};
}

function throwHttpError(response, error) {
let httpError;
if (response && response.status >= 300) {
httpError = new Error(`Network request failed with status ${response.status} - "${response.statusText}"`);
} else {
httpError = new Error(`Network request failed to return valid JSON`);
}
(httpError as any).response = response;
(httpError as any).parseError = error;

throw httpError as FetchError;
}

const applyAfterwares = ({response, options}: ResponseAndOptions): Promise<ResponseAndOptions> => {
return new Promise((resolve, reject) => {
// Declare responseObject so that afterware can mutate it.
const responseObject = {response, options};
const buildAfterwareStack = (funcs: AfterwareInterface[], scope: any) => {
const next = () => {
if (funcs.length > 0) {
const f = funcs.shift();
if (f) {
f.apply(scope, [responseObject, next]);
}
} else {
resolve(responseObject);
}
};
next();
};
function throwBatchError(response) {
let httpError = new Error(`A batched Operation of responses for `);
(httpError as any).response = response;

// iterate through afterwares using next callback
buildAfterwareStack([..._afterwares], this);
});
};
throw httpError as FetchError;
}

const callFetch = ({ request, options }) => {
let body;
try {
body = JSON.stringify(request);
} catch (e) {
throw new Error(`Network request failed. Payload is not serializable: ${e.message}`);
}
export function createApolloFetch(params: FetchOptions = {}): ApolloFetch {
const {constructOptions, customFetch} = params;

const opts = {
body,
method: 'POST',
...options,
headers: {
Accept: '*/*',
'Content-Type': 'application/json',
...(options.headers || []),
},
};
return customFetch ? customFetch(_uri, opts) : fetch(_uri, opts);
};
const _uri = params.uri || '/graphql';
const middlewares = [];
const batchedMiddlewares = [];
const afterwares = [];
const batchedAfterwares = [];

const throwHttpError = (response, error) => {
let httpError;
if (response && response.status >= 300) {
httpError = new Error(`Network request failed with status ${response.status} - "${response.statusText}"`);
} else {
httpError = new Error(`Network request failed to return valid JSON`);
}
(httpError as any).response = response;
(httpError as any).parseError = error;

throw httpError as FetchError;
const applyMiddlewares = (
requestAndOptions: RequestAndOptions | RequestsAndOptions,
batched: boolean,
): Promise<RequestAndOptions | RequestsAndOptions> => {
return new Promise((resolve, reject) => {
if (batched) {
buildWareStack([...batchedMiddlewares], requestAndOptions, resolve);
} else {
buildWareStack([...middlewares], requestAndOptions, resolve);
}
});
};

const applyAfterwares = (responseObject: ResponseAndOptions, batched: boolean): Promise<ResponseAndOptions> => {
return new Promise((resolve, reject) => {
if (batched) {
buildWareStack([...batchedAfterwares], responseObject, resolve);
} else {
buildWareStack([...afterwares], responseObject, resolve);
}
});
};

const apolloFetch: ApolloFetch = <ApolloFetch>Object.assign(
function (request: GraphQLRequest): Promise<FetchResult> {
const options = {};
function (request: GraphQLRequest | GraphQLRequest[]): Promise<FetchResult | FetchResult[]> {
let options = {};
let parseError;

return applyMiddlewares({
const batched = Array.isArray(request);

const requestObject = <RequestAndOptions | RequestsAndOptions>{
request,
options,
})
.then( callFetch )
.then( response => response.text().then( raw => {
try {
const parsed = JSON.parse(raw);
return <ParsedResponse>{ ...response, raw, parsed };
} catch (e) {
parseError = e;

//pass parsed raw response onto afterware
return <ParsedResponse>{ ...response, raw };
};

return applyMiddlewares(requestObject, batched)
.then(({ request: req, options: opt }) => (constructOptions || constructDefaultOptions)(req, opt))
.then( opts => {
options = {...opts};
return (customFetch || fetch) (_uri, options);
})
.then( response => response.text().then( raw => {
try {
const parsed = JSON.parse(raw);
return <ParsedResponse>{ ...response, raw, parsed };
} catch (e) {
parseError = e;

//pass parsed raw response onto afterware
return <ParsedResponse>{ ...response, raw };
}
}),
//.catch() this should never happen: https://developer.mozilla.org/en-US/docs/Web/API/Body/text
)
.then(response => applyAfterwares({
response,
options,
}, batched))
.then(({ response }) => {
if (response.parsed) {
if (batched) {
if (Array.isArray(response.parsed)) {
return response.parsed as FetchResult[];
} else {
throwBatchError(response);
}
} else {
return { ...response.parsed };
}
} else {
throwHttpError(response, parseError);
}
}),
//.catch() this should never happen: https://developer.mozilla.org/en-US/docs/Web/API/Body/text
)
.then(response => applyAfterwares({
response,
options,
}))
.then(({ response }) => {
if (response.parsed) {
return { ...response.parsed };
} else {
throwHttpError(response, parseError);
}
});
});
},
{
use: (middleware: MiddlewareInterface) => {
if (typeof middleware === 'function') {
_middlewares.push(middleware);
middlewares.push(middleware);
} else {
throw new Error('Middleware must be a function');
}
Expand All @@ -148,7 +167,25 @@ export function createApolloFetch(params: FetchOptions = {}): ApolloFetch {
},
useAfter: (afterware: AfterwareInterface) => {
if (typeof afterware === 'function') {
_afterwares.push(afterware);
afterwares.push(afterware);
} else {
throw new Error('Afterware must be a function');
}

return apolloFetch;
},
batchUse: (middleware: BatchedMiddlewareInterface) => {
if (typeof middleware === 'function') {
batchedMiddlewares.push(middleware);
} else {
throw new Error('Middleware must be a function');
}

return apolloFetch;
},
batchUseAfter: (afterware: BatchedAfterwareInterface) => {
if (typeof afterware === 'function') {
batchedAfterwares.push(afterware);
} else {
throw new Error('Afterware must be a function');
}
Expand All @@ -160,6 +197,3 @@ export function createApolloFetch(params: FetchOptions = {}): ApolloFetch {

return apolloFetch as ApolloFetch;
}

const apolloFetch = createApolloFetch();
export { apolloFetch };
4 changes: 4 additions & 0 deletions packages/apollo-fetch/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@ import {

import {
createApolloFetch,
constructDefaultOptions,
} from './apollo-fetch';

export {
createApolloFetch,
constructDefaultOptions,

//Types
ApolloFetch,
GraphQLRequest,
};
11 changes: 11 additions & 0 deletions packages/apollo-fetch/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
export interface ApolloFetch {
(operation: GraphQLRequest): Promise<FetchResult>;
(operation: GraphQLRequest[]): Promise<FetchResult[]>;
use: (middlewares: MiddlewareInterface) => ApolloFetch;
useAfter: (afterwares: AfterwareInterface) => ApolloFetch;
batchUse: (middlewares: BatchedMiddlewareInterface) => ApolloFetch;
batchUseAfter: (afterwares: BatchedAfterwareInterface) => ApolloFetch;
}

export interface GraphQLRequest {
Expand All @@ -17,13 +20,20 @@ export interface FetchResult {
}

export type MiddlewareInterface = (request: RequestAndOptions, next: Function) => void;
export type BatchedMiddlewareInterface = (request: RequestsAndOptions, next: Function) => void;

export interface RequestAndOptions {
request: GraphQLRequest;
options: RequestInit;
}

export interface RequestsAndOptions {
request: GraphQLRequest[];
options: RequestInit;
}

export type AfterwareInterface = (response: ResponseAndOptions, next: Function) => void;
export type BatchedAfterwareInterface = (response: ResponseAndOptions, next: Function) => void;

export interface ResponseAndOptions {
response: ParsedResponse;
Expand All @@ -38,6 +48,7 @@ export interface ParsedResponse extends Response {
export interface FetchOptions {
uri?: string;
customFetch?: (request: RequestInfo, init: RequestInit) => Promise<Response>;
constructOptions?: (request: GraphQLRequest | GraphQLRequest[], options: RequestInit) => RequestInit;
}

export interface FetchError extends Error {
Expand Down
Loading

0 comments on commit dea26a6

Please sign in to comment.