UNPKG

@onfleet/node-onfleet

Version:

Onfleet's Node.js API Wrapper Package

183 lines (164 loc) 5.18 kB
/* eslint-disable no-console */ import Bottleneck from 'bottleneck'; import fetch from 'node-fetch'; import * as constants from './constants.js'; import * as util from './util.js'; import { HttpError, PermissionError, RateLimitError, ServiceError, } from './error.js'; // Create new rate limiter using defined constants const limiter = new Bottleneck({ reservoir: constants.LIMITER_RESERVOIR, maxConcurrent: constants.LIMITER_MAX_CONCURRENT, minTime: constants.LIMITER_MIN_TIME, }); // Rate reservoir refresh on every response // New rate is determined by x-ratelimit-remaining in headers const reassignRate = (newRate) => { if (newRate > 0) { limiter.updateSettings({ reservoir: newRate, }); } }; const wait = async (ms) => { console.log('Waiting due to rate limiting'); return new Promise((resolve) => { setTimeout(resolve, ms); }); }; // On reservoir depletion, we wait 10000ms and reset the rate again (20 req/second limitation) limiter.on('depleted', async (empty) => { if (!empty) { await wait(constants.LIMITER_WAIT_UPON_DEPLETION); reassignRate(constants.LIMITER_RESERVOIR); } }); /** * The Method Factory * @desc configures the actual method for each CRUD operations * @returns a promise containing the response from an HTTP request */ const Methods = async (key, api, ...args) => { const { path, altPath, method, queryParams, deliveryManifestObject, timeoutInMilliseconds, } = key; const operations = method; // Instead of using ['method'], we directly assign `method` let url = `${api.api.baseUrl}${path}`; let body = ''; let hasBody = false; // No arguments if (args.length === 0 && operations === 'GET' && altPath) { url = `${api.api.baseUrl}${altPath}`; } // 1 or more arguments if (args.length >= 1 && ['GET', 'DELETE', 'PUT'].includes(operations)) { if (['name', 'shortId', 'phone', 'workers', 'organizations', 'teams'].includes(args[1])) { url = util.replaceWithEndpointAndParam(url, args[1], args[0]); } else if (util.isBase64Encoded(args[0])) { url = util.replaceWithId(url, args[0]); } else { url = `${api.api.baseUrl}${altPath}`; } if (operations === 'PUT') { body = args[1]; hasBody = true; } } if (['PUT', 'DELETE'].includes(operations) && url.includes('customFields') && Array.isArray(args)) { body = args[0]; // eslint-disable-line hasBody = true; } // POST Prep - 3 different cases if (operations === 'POST') { if (util.isBase64Encoded(args[0])) { url = util.replaceWithId(url, args[0]); if (args[1]) { body = args[1]; hasBody = true; } } else { body = args[0]; hasBody = true; } } // Query Params extension if (queryParams) { for (const element of args) { if (util.isQueryParam(element)) { url = util.appendQueryParameters(url, element); } } } // Reference https://docs.onfleet.com/reference/delivery-manifest if (deliveryManifestObject && args && args.length > 0) { args.forEach((item) => { if (item.hubId && item.workerId) { body = { path: `providers/manifest/generate?hubId=${item.hubId}&workerId=${item.workerId}`, method: "GET", }; hasBody = true; } if (item.googleApiKey) { api.api.headers["X-API-Key"] = `Google ${item.googleApiKey}`; } if (item.startDate || item.endDate) { const queryParams = {}; if (item.startDate) queryParams.startDate = item.startDate; if (item.endDate) queryParams.endDate = item.endDate; url = util.appendQueryParameters(url, queryParams); } }); } // Send the HTTP request through the rate limiter try { const res = await limiter.schedule(() => fetch(url, { method: operations, headers: api.api.headers, timeout: timeoutInMilliseconds, body: hasBody ? JSON.stringify(body) : undefined, })); // For every request, we compare the reservoir with the remaining rate limit in the header const reservoir = await limiter.currentReservoir(); const rateLimitRemaining = res.headers.get('x-ratelimit-remaining'); if (reservoir < rateLimitRemaining) { reassignRate(rateLimitRemaining); } if (res.ok) { if (operations === 'DELETE') { return res.status; } return res.json().catch(() => res.status); } const error = await res.json(); const errorCode = error.message.error; const errorInfo = [ error.message.message, errorCode, error.message.cause, error.message.request, ]; if (errorCode === 2300) { throw new RateLimitError(...errorInfo); } else if (errorCode >= 1100 && errorCode <= 1108) { throw new PermissionError(...errorInfo); } else if (errorCode >= 2500) { throw new ServiceError(...errorInfo); } else if (errorCode === 2218) { // Precondition error for Auto-Dispatch throw new ServiceError(...errorInfo); } throw new HttpError(...errorInfo); } catch (error) { throw error; } }; export default Methods;