UNPKG

7.05 kBJavaScriptView Raw
1'use strict';
2
3const makeRequest = require('./makeRequest');
4const utils = require('./utils');
5
6function makeAutoPaginationMethods(self, requestArgs, spec, firstPagePromise) {
7 const promiseCache = {currentPromise: null};
8 let listPromise = firstPagePromise;
9 let i = 0;
10
11 function iterate(listResult) {
12 if (
13 !(
14 listResult &&
15 listResult.data &&
16 typeof listResult.data.length === 'number'
17 )
18 ) {
19 throw Error(
20 'Unexpected: Stripe API response does not have a well-formed `data` array.'
21 );
22 }
23
24 if (i < listResult.data.length) {
25 const value = listResult.data[i];
26 i += 1;
27 return {value, done: false};
28 } else if (listResult.has_more) {
29 // Reset counter, request next page, and recurse.
30 i = 0;
31 const lastId = getLastId(listResult);
32 listPromise = makeRequest(self, requestArgs, spec, {
33 starting_after: lastId,
34 });
35 return listPromise.then(iterate);
36 }
37 return {value: undefined, done: true};
38 }
39
40 function asyncIteratorNext() {
41 return memoizedPromise(promiseCache, (resolve, reject) => {
42 return listPromise
43 .then(iterate)
44 .then(resolve)
45 .catch(reject);
46 });
47 }
48
49 const autoPagingEach = makeAutoPagingEach(asyncIteratorNext);
50 const autoPagingToArray = makeAutoPagingToArray(autoPagingEach);
51
52 const autoPaginationMethods = {
53 autoPagingEach,
54 autoPagingToArray,
55
56 // Async iterator functions:
57 next: asyncIteratorNext,
58 return: () => {
59 // This is required for `break`.
60 return {};
61 },
62 [getAsyncIteratorSymbol()]: () => {
63 return autoPaginationMethods;
64 },
65 };
66 return autoPaginationMethods;
67}
68
69module.exports.makeAutoPaginationMethods = makeAutoPaginationMethods;
70
71/**
72 * ----------------
73 * Private Helpers:
74 * ----------------
75 */
76
77function getAsyncIteratorSymbol() {
78 if (typeof Symbol !== 'undefined' && Symbol.asyncIterator) {
79 return Symbol.asyncIterator;
80 }
81 // Follow the convention from libraries like iterall: https://github.com/leebyron/iterall#asynciterator-1
82 return '@@asyncIterator';
83}
84
85function getDoneCallback(args) {
86 if (args.length < 2) {
87 return undefined;
88 }
89 const onDone = args[1];
90 if (typeof onDone !== 'function') {
91 throw Error(
92 `The second argument to autoPagingEach, if present, must be a callback function; receieved ${typeof onDone}`
93 );
94 }
95 return onDone;
96}
97
98/**
99 * We allow four forms of the `onItem` callback (the middle two being equivalent),
100 *
101 * 1. `.autoPagingEach((item) => { doSomething(item); return false; });`
102 * 2. `.autoPagingEach(async (item) => { await doSomething(item); return false; });`
103 * 3. `.autoPagingEach((item) => doSomething(item).then(() => false));`
104 * 4. `.autoPagingEach((item, next) => { doSomething(item); next(false); });`
105 *
106 * In addition to standard validation, this helper
107 * coalesces the former forms into the latter form.
108 */
109function getItemCallback(args) {
110 if (args.length === 0) {
111 return undefined;
112 }
113 const onItem = args[0];
114 if (typeof onItem !== 'function') {
115 throw Error(
116 `The first argument to autoPagingEach, if present, must be a callback function; receieved ${typeof onItem}`
117 );
118 }
119
120 // 4. `.autoPagingEach((item, next) => { doSomething(item); next(false); });`
121 if (onItem.length === 2) {
122 return onItem;
123 }
124
125 if (onItem.length > 2) {
126 throw Error(
127 `The \`onItem\` callback function passed to autoPagingEach must accept at most two arguments; got ${onItem}`
128 );
129 }
130
131 // This magically handles all three of these usecases (the latter two being functionally identical):
132 // 1. `.autoPagingEach((item) => { doSomething(item); return false; });`
133 // 2. `.autoPagingEach(async (item) => { await doSomething(item); return false; });`
134 // 3. `.autoPagingEach((item) => doSomething(item).then(() => false));`
135 return function _onItem(item, next) {
136 const shouldContinue = onItem(item);
137 next(shouldContinue);
138 };
139}
140
141function getLastId(listResult) {
142 const lastIdx = listResult.data.length - 1;
143 const lastItem = listResult.data[lastIdx];
144 const lastId = lastItem && lastItem.id;
145 if (!lastId) {
146 throw Error(
147 'Unexpected: No `id` found on the last item while auto-paging a list.'
148 );
149 }
150 return lastId;
151}
152
153/**
154 * If a user calls `.next()` multiple times in parallel,
155 * return the same result until something has resolved
156 * to prevent page-turning race conditions.
157 */
158function memoizedPromise(promiseCache, cb) {
159 if (promiseCache.currentPromise) {
160 return promiseCache.currentPromise;
161 }
162 promiseCache.currentPromise = new Promise(cb).then((ret) => {
163 promiseCache.currentPromise = undefined;
164 return ret;
165 });
166 return promiseCache.currentPromise;
167}
168
169function makeAutoPagingEach(asyncIteratorNext) {
170 return function autoPagingEach(/* onItem?, onDone? */) {
171 const args = [].slice.call(arguments);
172 const onItem = getItemCallback(args);
173 const onDone = getDoneCallback(args);
174 if (args.length > 2) {
175 throw Error('autoPagingEach takes up to two arguments; received:', args);
176 }
177
178 const autoPagePromise = wrapAsyncIteratorWithCallback(
179 asyncIteratorNext,
180 onItem
181 );
182 return utils.callbackifyPromiseWithTimeout(autoPagePromise, onDone);
183 };
184}
185
186function makeAutoPagingToArray(autoPagingEach) {
187 return function autoPagingToArray(opts, onDone) {
188 const limit = opts && opts.limit;
189 if (!limit) {
190 throw Error(
191 'You must pass a `limit` option to autoPagingToArray, e.g., `autoPagingToArray({limit: 1000});`.'
192 );
193 }
194 if (limit > 10000) {
195 throw Error(
196 'You cannot specify a limit of more than 10,000 items to fetch in `autoPagingToArray`; use `autoPagingEach` to iterate through longer lists.'
197 );
198 }
199 const promise = new Promise((resolve, reject) => {
200 const items = [];
201 autoPagingEach((item) => {
202 items.push(item);
203 if (items.length >= limit) {
204 return false;
205 }
206 })
207 .then(() => {
208 resolve(items);
209 })
210 .catch(reject);
211 });
212 return utils.callbackifyPromiseWithTimeout(promise, onDone);
213 };
214}
215
216function wrapAsyncIteratorWithCallback(asyncIteratorNext, onItem) {
217 return new Promise((resolve, reject) => {
218 function handleIteration(iterResult) {
219 if (iterResult.done) {
220 resolve();
221 return;
222 }
223
224 const item = iterResult.value;
225 return new Promise((next) => {
226 // Bit confusing, perhaps; we pass a `resolve` fn
227 // to the user, so they can decide when and if to continue.
228 // They can return false, or a promise which resolves to false, to break.
229 onItem(item, next);
230 }).then((shouldContinue) => {
231 if (shouldContinue === false) {
232 return handleIteration({done: true});
233 } else {
234 return asyncIteratorNext().then(handleIteration);
235 }
236 });
237 }
238
239 asyncIteratorNext()
240 .then(handleIteration)
241 .catch(reject);
242 });
243}