UNPKG

react-relay-network-modern

Version:

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

179 lines (173 loc) 6.97 kB
const _excluded = ["headersOrThunk"]; function _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; } function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; } /* eslint-disable no-param-reassign */ import { isFunction } from '../utils'; import RelayRequestBatch from '../RelayRequestBatch'; import RelayRequest from '../RelayRequest'; import { RRNLBatchMiddlewareError } from './batch'; // Max out at roughly 100kb (express-graphql imposed max) const DEFAULT_BATCH_SIZE = 102400; export default function legacyBatchMiddleware(options) { 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); } return passThroughBatch(req, next, { batchTimeout, batchUrl, singleton, maxBatchSize, fetchOpts }); }; } function passThroughBatch(req, next, opts) { const { singleton } = opts; const bodyLength = req.fetchOpts.body.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 relayReqId = req.getID(); const { requestMap } = singleton.batcher; const 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: [] }; if (requestMap[relayReqId]) { /* 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 */ requestMap[relayReqId].duplicates.push(requestWrapper); } else { requestMap[relayReqId] = requestWrapper; } }); } function prepareNewBatcher(next, opts) { const batcher = { bodySize: 2, // account for '[]' requestMap: {}, acceptRequests: true }; setTimeout(() => { batcher.acceptRequests = false; sendRequests(batcher.requestMap, next, opts).then(() => finalizeUncompleted(batcher.requestMap)).catch(() => finalizeUncompleted(batcher.requestMap)); }, opts.batchTimeout); return batcher; } async function sendRequests(requestMap, next, opts) { const ids = Object.keys(requestMap); if (ids.length === 1) { // SEND AS SINGLE QUERY const request = requestMap[ids[0]]; const res = await next(request.req); request.completeOk(res); request.duplicates.forEach(r => r.completeOk(res)); return res; } else if (ids.length > 1) { // SEND AS BATCHED QUERY const batchRequest = new RelayRequestBatch(ids.map(id => requestMap[id].req)); // $FlowFixMe const url = await (isFunction(opts.batchUrl) ? opts.batchUrl(requestMap) : opts.batchUrl); batchRequest.setFetchOption('url', url); const _opts$fetchOpts = opts.fetchOpts, { headersOrThunk } = _opts$fetchOpts, fetchOpts = _objectWithoutProperties(_opts$fetchOpts, _excluded); batchRequest.setFetchOptions(fetchOpts); if (headersOrThunk) { const headers = await (isFunction(headersOrThunk) ? headersOrThunk(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 => { if (!payload) return; const request = requestMap[payload.id]; if (request) { const res = createSingleResponse(batchResponse, payload); request.completeOk(res); } }); return batchResponse; } catch (e) { ids.forEach(id => { requestMap[id].completeErr(e); }); } } return Promise.resolve(); } // check that server returns responses for all requests function finalizeUncompleted(requestMap) { Object.keys(requestMap).forEach(id => { const request = requestMap[id]; if (!request.done) { request.completeErr(new RRNLBatchMiddlewareError(`Server does not return response for request with id ${id} \n` + `Response should have following shape { "id": "${id}", "data": {} }`)); } }); } function createSingleResponse(batchResponse, json) { // Fallback for graphql-graphene and apollo-server batch responses const data = json.payload || json; const res = batchResponse.clone(); res.processJsonData(data); return res; }