UNPKG

mws-zodane-advanced

Version:

fixed throtal resend

202 lines (183 loc) 8.48 kB
const MWS = require('mws-simple'); const { MWS_ENDPOINTS } = require('./constants'); const sleep = require('./util/sleep'); const fs = require('fs'); const errors = require('./errors.js'); /* * TODO: split the "init" function out of the "callEndpoint" file, difficulty: callEndpoint * depends on having init modify the mws value, which doesn't carry between module pieces in that * case -- exporting 'mws' to callEndpoint just results in null every time it's accessed, even * when requiring it inline in code, rather than at the top of the file. Ugh. Big design * problem, that is a holdover from when the entire module was all located in a single file. */ /** holds the mws-simple reference after init() is called */ /** * Initialize mws-advanced with your MWS access keys, merchantId, optionally authtoken, host, port * If accessKeyId, secretAccessKey, and/or merchantId are not provided, they will be read from * the environment variables MWS_ACESS_KEY, MWS_SECRET_ACCESS_KEY, and MWS_MERCHANT_ID respectively * * @public * @example * const mws = MWS.init({ region: 'NA', accessKeyId: '1234', secretAccessKey: '2345', merchantId: '1234567890' }); * const mws = MWS.init({ region: 'EU', accessKeyId, ... }); * const mws = MWS.init({ authToken: 'qwerty', accessKeyId, ...}); * const mws = MWS.init({ host: 'alternate-mws-server.com', accessKeyId, ... }); * @param {object} config Contains your MWS Access Keys/Tokens and options to configure the API * @param {string} [config.accessKeyId=process.env.MWS_ACCESS_KEY] Your MWS Access Key * @param {string} [config.secretAccessKey=process.env.MWS_SECRET_ACCESS_KEY] Your MWS Secret Access Key * @param {string} [config.merchantId=process.env.MWS_MERCHANT_ID] Your MWS Merchant ID * @param {string} [config.authToken] If making a call for a third party account, the Auth Token provided * for the third party account * @param {string} [config.region='NA'] One of the Amazon regions as specified in https://docs.developer.amazonservices.com/en_US/dev_guide/DG_Endpoints.html * @param {string} [config.host='mws.amazonservices.com'] Set MWS host server name, see https://docs.developer.amazonservices.com/en_US/dev_guide/DG_Endpoints.html * @param {number} [config.port=443] Set MWS host port * @returns {mws-simple} - The mws-simple instance used to communicate with the API */ let mws; const init = ({ region = 'NA', // marketplace = 'US', // TODO: maybe someday we will want to keep track of a 'default' marketplace. accessKeyId = process.env.MWS_ACCESS_KEY, secretAccessKey = process.env.MWS_SECRET_ACCESS_KEY, merchantId = process.env.MWS_MERCHANT_ID, authToken, host = MWS_ENDPOINTS[region] || 'mws.amazonservices.com', port, } = {}) => { mws = new MWS({ accessKeyId, secretAccessKey, merchantId, authToken, host, port, }); return mws; }; const { flattenResult } = require('./flatten-result'); const { validateAndTransformParameters } = require('./validation'); const { digResponseResult } = require('./dig-response-result.js'); // TODO: add Subscriptions and Recommendations categories const { feeds, finances, inbound, inventory, outbound, merchFulfillment, orders, products, sellers, reports, } = require('./endpoints'); // TODO: THIS MAY BE A BAD IDEA CONSIDERING THAT SOME CATEGORIES SHARE SOME ENDPOINT NAMES // TODO: WE MAY NEED TO COME UP WITH A WAY TO DEAL WITH THAT. /** simple flat list of all the endpoints required from individual modules above */ const endpoints = { ...feeds, ...finances, ...inbound, ...inventory, ...merchFulfillment, ...orders, ...outbound, ...products, ...sellers, ...reports, }; /** * Return a promise for making the desired request, flattening the response out to something * that will hopefully make a little more sense. * * @async * @private * @param {any} requestData * @param {object} [opt] - options * @param {boolean} [opt.noFlatten] - do not run flattenResult on the output */ const requestPromise = (requestData, opt = {}, mm) => new Promise((resolve, reject) => { // eslint-disable-next-line global-require mm.request(requestData, (err, result) => { if (err) { reject(err); } else { resolve(opt.noFlatten ? result : flattenResult(result)); } }); }); /** * Call a known endpoint at MWS, returning the raw data from the function. Parameters are * transformed and validated according to the rules defined in lib/endpoints * * @async * @public * @param {string} name - name of MWS API function to call * @param {object} [callOptions] - named hash object of the parameters to pass to the API * @param {object} [opt] - options for callEndpoint * @param {boolean} [opt.noFlatten] - do not flatten results * @param {boolean} [opt.returnRaw] - return only the raw data (may or may not be flattened) * @param {string} [opt.saveRaw] - filename to save raw data to (may or may not be flattened) * @param {string} [opt.saveParsed] - filename to save final parsed data to (not compatible with returnRaw, since parsing won't happen) * @returns {any} - Results of the call to MWS */ const callEndpoint = async (name, callOptions = {}, opt = {}, mm) => { // console.log('endpoints:', endpoints); const endpoint = endpoints[name]; if (!endpoint) { throw new errors.InvalidUsage('No endpoint name supplied to callEndpoint'); } const newOptions = endpoint.params ? validateAndTransformParameters(endpoint.params, callOptions) : callOptions; const queryOptions = { ...newOptions, Action: endpoint.action, Version: endpoint.version, }; const params = { path: `/${endpoint.category}/${endpoint.version}`, query: queryOptions, }; try { const result = await requestPromise(params, { noFlatten: opt.noFlatten }, mm); if (opt.saveRaw) { fs.writeFileSync(opt.saveRaw, JSON.stringify(result)); } if (opt.returnRaw) { return result; } const digResult = digResponseResult(name, result); if (opt.saveParsed) { fs.writeFileSync(opt.saveParsed, JSON.stringify(digResult)); } return digResult; } catch (err) { if (err instanceof MWS.ServerError) { if (err.code === 503) { console.warn('***** Error 503 .. throttling?', name); let ms = 2000; if (!endpoint.throttle) { console.warn(`***** throttle information missing for API ${name}`); console.warn('***** ', JSON.stringify(endpoint)); } else { // restoreRate = how many you get back in a minute of time. there's an assumption here // that you're spamming a lot of requests if you're hitting throttling, and this // throttle management system is first version, here -- so we're going to throttle it // not to the minimum possible time frame, but to the length of time that it will take // to restore your entire maxInFlight -- assuming you're probably going to hit the max // again as soon as we start letting more requests in. // ultimately, an upgraded version of this throttling handler would keep track of how // many of every request are in flight, and try to throttle it to the minimum amount // of time that you can go, automatically throttling future calls without having to // even encounter the 503 from the server. // that day is not today. it might be tomorrow. maybe next weekend. i don't know. :-) const restoreMaxInFlightMin = (endpoint.throttle.maxInFlight / endpoint.throttle.restoreRate) * (callOptions.IdList ? callOptions.IdList.length : 1); ms = (restoreMaxInFlightMin * 60 * 1000) + 100; } console.warn(`***** trying again in ${ms}ms`); await sleep(ms + 100); return callEndpoint(name, callOptions, opt, mm); } } throw err; } }; module.exports = { getMws () { return mws; }, init, callEndpoint };