UNPKG

monobank

Version:
244 lines (219 loc) 8.05 kB
'use strict'; const makeRequest = require('./makeRequest'); const utils = require('./utils'); function makeAutoPaginationMethods(self, requestArgs, spec, firstPagePromise) { const promiseCache = {currentPromise: null}; let listPromise = firstPagePromise; let i = 0; function iterate(listResult) { if ( !( listResult && listResult.data && typeof listResult.data.length === 'number' ) ) { throw Error( 'Unexpected: Stripe API response does not have a well-formed `data` array.' ); } if (i < listResult.data.length) { const value = listResult.data[i]; i += 1; return {value, done: false}; } else if (listResult.has_more) { // Reset counter, request next page, and recurse. i = 0; const lastId = getLastId(listResult); listPromise = makeRequest(self, requestArgs, spec, { starting_after: lastId, }); return listPromise.then(iterate); } return {value: undefined, done: true}; } function asyncIteratorNext() { return memoizedPromise(promiseCache, (resolve, reject) => { return listPromise .then(iterate) .then(resolve) .catch(reject); }); } const autoPagingEach = makeAutoPagingEach(asyncIteratorNext); const autoPagingToArray = makeAutoPagingToArray(autoPagingEach); const autoPaginationMethods = { autoPagingEach, autoPagingToArray, // Async iterator functions: next: asyncIteratorNext, return: () => { // This is required for `break`. return {}; }, [getAsyncIteratorSymbol()]: () => { return autoPaginationMethods; }, }; return autoPaginationMethods; } module.exports.makeAutoPaginationMethods = makeAutoPaginationMethods; /** * ---------------- * Private Helpers: * ---------------- */ function getAsyncIteratorSymbol() { if (typeof Symbol !== 'undefined' && Symbol.asyncIterator) { return Symbol.asyncIterator; } // Follow the convention from libraries like iterall: https://github.com/leebyron/iterall#asynciterator-1 return '@@asyncIterator'; } function getDoneCallback(args) { if (args.length < 2) { return undefined; } const onDone = args[1]; if (typeof onDone !== 'function') { throw Error( `The second argument to autoPagingEach, if present, must be a callback function; receieved ${typeof onDone}` ); } return onDone; } /** * We allow four forms of the `onItem` callback (the middle two being equivalent), * * 1. `.autoPagingEach((item) => { doSomething(item); return false; });` * 2. `.autoPagingEach(async (item) => { await doSomething(item); return false; });` * 3. `.autoPagingEach((item) => doSomething(item).then(() => false));` * 4. `.autoPagingEach((item, next) => { doSomething(item); next(false); });` * * In addition to standard validation, this helper * coalesces the former forms into the latter form. */ function getItemCallback(args) { if (args.length === 0) { return undefined; } const onItem = args[0]; if (typeof onItem !== 'function') { throw Error( `The first argument to autoPagingEach, if present, must be a callback function; receieved ${typeof onItem}` ); } // 4. `.autoPagingEach((item, next) => { doSomething(item); next(false); });` if (onItem.length === 2) { return onItem; } if (onItem.length > 2) { throw Error( `The \`onItem\` callback function passed to autoPagingEach must accept at most two arguments; got ${onItem}` ); } // This magically handles all three of these usecases (the latter two being functionally identical): // 1. `.autoPagingEach((item) => { doSomething(item); return false; });` // 2. `.autoPagingEach(async (item) => { await doSomething(item); return false; });` // 3. `.autoPagingEach((item) => doSomething(item).then(() => false));` return function _onItem(item, next) { const shouldContinue = onItem(item); next(shouldContinue); }; } function getLastId(listResult) { const lastIdx = listResult.data.length - 1; const lastItem = listResult.data[lastIdx]; const lastId = lastItem && lastItem.id; if (!lastId) { throw Error( 'Unexpected: No `id` found on the last item while auto-paging a list.' ); } return lastId; } /** * If a user calls `.next()` multiple times in parallel, * return the same result until something has resolved * to prevent page-turning race conditions. */ function memoizedPromise(promiseCache, cb) { if (promiseCache.currentPromise) { return promiseCache.currentPromise; } promiseCache.currentPromise = new Promise(cb).then((ret) => { promiseCache.currentPromise = undefined; return ret; }); return promiseCache.currentPromise; } function makeAutoPagingEach(asyncIteratorNext) { return function autoPagingEach(/* onItem?, onDone? */) { const args = [].slice.call(arguments); const onItem = getItemCallback(args); const onDone = getDoneCallback(args); if (args.length > 2) { throw Error('autoPagingEach takes up to two arguments; received:', args); } const autoPagePromise = wrapAsyncIteratorWithCallback( asyncIteratorNext, onItem ); return utils.callbackifyPromiseWithTimeout(autoPagePromise, onDone); }; } function makeAutoPagingToArray(autoPagingEach) { return function autoPagingToArray(opts, onDone) { const limit = opts && opts.limit; if (!limit) { throw Error( 'You must pass a `limit` option to autoPagingToArray, eg; `autoPagingToArray({limit: 1000});`.' ); } if (limit > 10000) { throw Error( 'You cannot specify a limit of more than 10,000 items to fetch in `autoPagingToArray`; use `autoPagingEach` to iterate through longer lists.' ); } const promise = new Promise((resolve, reject) => { const items = []; autoPagingEach((item) => { items.push(item); if (items.length >= limit) { return false; } }) .then(() => { resolve(items); }) .catch(reject); }); return utils.callbackifyPromiseWithTimeout(promise, onDone); }; } function wrapAsyncIteratorWithCallback(asyncIteratorNext, onItem) { return new Promise((resolve, reject) => { function handleIteration(iterResult) { if (iterResult.done) { resolve(); return; } const item = iterResult.value; return new Promise((next) => { // Bit confusing, perhaps; we pass a `resolve` fn // to the user, so they can decide when and if to continue. // They can return false, or a promise which resolves to false, to break. onItem(item, next); }).then((shouldContinue) => { if (shouldContinue === false) { return handleIteration({done: true}); } else { return asyncIteratorNext().then(handleIteration); } }); } asyncIteratorNext() .then(handleIteration) .catch(reject); }); }