mws-zodane-advanced
Version:
fixed throtal resend
202 lines (183 loc) • 8.48 kB
JavaScript
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
};