UNPKG

react-relay-network-modern

Version:

Network Layer for React Relay and Express (Batch Queries, AuthToken, Logging, Retry)

263 lines (224 loc) 8.06 kB
/* @flow */ /* eslint-disable no-param-reassign */ import { isFunction } from '../utils'; import RelayRequestBatch from '../RelayRequestBatch'; import RelayRequest from '../RelayRequest'; import type RelayResponse from '../RelayResponse'; import type { Middleware, FetchOpts } from '../definition'; import RRNLError from '../RRNLError'; // Max out at roughly 100kb (express-graphql imposed max) const DEFAULT_BATCH_SIZE = 102400; type Headers = { [name: string]: string }; export type BatchMiddlewareOpts = {| batchUrl?: | string | Promise<string> | ((requestList: RequestWrapper[]) => string | Promise<string>), batchTimeout?: number, maxBatchSize?: number, allowMutations?: boolean, method?: 'POST' | 'GET', headers?: Headers | Promise<Headers> | ((req: RelayRequestBatch) => Headers | Promise<Headers>), // Avaliable request modes in fetch options. For details see https://fetch.spec.whatwg.org/#requests credentials?: $PropertyType<FetchOpts, 'credentials'>, mode?: $PropertyType<FetchOpts, 'mode'>, cache?: $PropertyType<FetchOpts, 'cache'>, redirect?: $PropertyType<FetchOpts, 'redirect'>, |}; export type RequestWrapper = {| req: RelayRequest, completeOk: (res: Object) => void, completeErr: (e: Error) => void, done: boolean, duplicates: Array<RequestWrapper>, |}; type Batcher = { bodySize: number, requestList: RequestWrapper[], acceptRequests: boolean, }; export class RRNLBatchMiddlewareError extends RRNLError { constructor(msg: string) { super(msg); this.name = 'RRNLBatchMiddlewareError'; } } export default function batchMiddleware(options?: BatchMiddlewareOpts): Middleware { const opts = options || {}; const batchTimeout = opts.batchTimeout || 0; // 0 is the same as nextTick in nodeJS const allowMutations = opts.allowMutations || false; const batchUrl = opts.batchUrl || '/graphql/batch'; const maxBatchSize = opts.maxBatchSize || DEFAULT_BATCH_SIZE; const singleton = {}; const fetchOpts = {}; if (opts.method) fetchOpts.method = opts.method; if (opts.credentials) fetchOpts.credentials = opts.credentials; if (opts.mode) fetchOpts.mode = opts.mode; if (opts.cache) fetchOpts.cache = opts.cache; if (opts.redirect) fetchOpts.redirect = opts.redirect; if (opts.headers) fetchOpts.headersOrThunk = opts.headers; return (next) => (req) => { // do not batch mutations unless allowMutations = true if (req.isMutation() && !allowMutations) { return next(req); } if (!(req instanceof RelayRequest)) { throw new RRNLBatchMiddlewareError( 'Relay batch middleware accepts only simple RelayRequest. Did you add batchMiddleware twice?' ); } // req with FormData can not be batched if (req.isFormData()) { return next(req); } // skip batching if request explicitly opts out if (req.cacheConfig.skipBatch) { return next(req); } return passThroughBatch(req, next, { batchTimeout, batchUrl, singleton, maxBatchSize, fetchOpts, }); }; } function passThroughBatch(req: RelayRequest, next, opts) { const { singleton } = opts; const bodyLength = (req.getBody(): any).length; if (!bodyLength) { return next(req); } if (!singleton.batcher || !singleton.batcher.acceptRequests) { singleton.batcher = prepareNewBatcher(next, opts); } if (singleton.batcher.bodySize + bodyLength + 1 > opts.maxBatchSize) { singleton.batcher = prepareNewBatcher(next, opts); } // +1 accounts for tailing comma after joining singleton.batcher.bodySize += bodyLength + 1; // queue request return new Promise((resolve, reject) => { const { requestList } = singleton.batcher; const requestWrapper: RequestWrapper = { req, completeOk: (res) => { requestWrapper.done = true; resolve(res); requestWrapper.duplicates.forEach((r) => r.completeOk(res)); }, completeErr: (err) => { requestWrapper.done = true; reject(err); requestWrapper.duplicates.forEach((r) => r.completeErr(err)); }, done: false, duplicates: [], }; const duplicateIndex = requestList.findIndex( (wrapper) => req.getBody() === wrapper.req.getBody() ); if (duplicateIndex !== -1) { /* I've run into a scenario with Relay Classic where if you have 2 components that make the exact same query, Relay will dedup the queries and reuse the request ids but still make 2 requests. The batch code then loses track of all the duplicate requests being made and never resolves or rejects the duplicate requests https://github.com/nodkz/react-relay-network-layer/pull/52 */ requestList[duplicateIndex].duplicates.push(requestWrapper); } else { requestList.push(requestWrapper); } }); } function prepareNewBatcher(next, opts): Batcher { const batcher: Batcher = { bodySize: 2, // account for '[]' requestList: [], acceptRequests: true, }; setTimeout(() => { batcher.acceptRequests = false; sendRequests(batcher.requestList, next, opts) .then(() => finalizeUncompleted(batcher.requestList)) .catch((e) => { if (e && e.name === 'AbortError') { finalizeCanceled(batcher.requestList, e); } else { finalizeUncompleted(batcher.requestList); } }); }, opts.batchTimeout); return batcher; } async function sendRequests(requestList: RequestWrapper[], next, opts) { if (requestList.length === 1) { // SEND AS SINGLE QUERY const wrapper = requestList[0]; const res = await next(wrapper.req); wrapper.completeOk(res); wrapper.duplicates.forEach((r) => r.completeOk(res)); return res; } else if (requestList.length > 1) { // SEND AS BATCHED QUERY const batchRequest = new RelayRequestBatch(requestList.map((wrapper) => wrapper.req)); // $FlowFixMe const url = await (isFunction(opts.batchUrl) ? opts.batchUrl(requestList) : opts.batchUrl); batchRequest.setFetchOption('url', url); const { headersOrThunk, ...fetchOpts } = opts.fetchOpts; batchRequest.setFetchOptions(fetchOpts); if (headersOrThunk) { const headers = await (isFunction(headersOrThunk) ? (headersOrThunk: any)(batchRequest) : headersOrThunk); batchRequest.setFetchOption('headers', headers); } try { const batchResponse = await next(batchRequest); if (!batchResponse || !Array.isArray(batchResponse.json)) { throw new RRNLBatchMiddlewareError( 'Wrong response from server. Did your server support batch request?' ); } batchResponse.json.forEach((payload: any, index) => { if (!payload) return; const request = requestList[index]; if (request) { const res = createSingleResponse(batchResponse, payload); request.completeOk(res); } }); return batchResponse; } catch (e) { requestList.forEach((request) => request.completeErr(e)); } } return Promise.resolve(); } // check that server returns responses for all requests function finalizeCanceled(requestList: RequestWrapper[], error: Error) { requestList.forEach((request) => request.completeErr(error)); } // check that server returns responses for all requests function finalizeUncompleted(requestList: RequestWrapper[]) { requestList.forEach((request, index) => { if (!request.done) { request.completeErr( new RRNLBatchMiddlewareError( `Server does not return response for request at index ${index}.\n` + `Response should have an array with ${requestList.length} item(s).` ) ); } }); } function createSingleResponse(batchResponse: RelayResponse, json: any): RelayResponse { // Fallback for graphql-graphene and apollo-server batch responses const data = json.payload || json; const res = batchResponse.clone(); res.processJsonData(data); return res; }