-
Notifications
You must be signed in to change notification settings - Fork 751
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Preliminary implementation of autoPagingEach
- Loading branch information
1 parent
5109070
commit 7efc7ea
Showing
9 changed files
with
312 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
*.node10.js |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
'use strict'; | ||
|
||
var makeRequest = require('./makeRequest'); | ||
|
||
function getItemCallback(args) { | ||
if (args.length === 0) { | ||
return undefined; | ||
} | ||
var onItem = args[0]; | ||
if (typeof onItem !== 'function') { | ||
throw Error('The first argument to autoPagingEach, if present, must be a callback function; receieved ' + typeof onItem); | ||
} | ||
|
||
// `.autoPagingEach((item, next) => { doSomething(item); next(); });` | ||
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); | ||
} | ||
|
||
// API compat; turn this: | ||
// .autoPagingEach((item) => { doSomething(item); return false; }); | ||
// into this: | ||
// .autoPagingEach((item) => { doSomething(item); next(false); }); | ||
return function(item, next) { | ||
var shouldContinue = onItem(item); | ||
next(shouldContinue); | ||
}; | ||
} | ||
|
||
function getDoneCallback(args) { | ||
if (args.length < 2) { | ||
return undefined; | ||
} | ||
var 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; | ||
} | ||
|
||
function wrapAsyncIteratorWithCallback(asyncIteratorNext, onItem) { | ||
return new Promise(function(resolve, reject) { | ||
function handleIteration(iterResult) { | ||
if (iterResult.done) { | ||
resolve(); | ||
return; | ||
} | ||
|
||
var item = iterResult.value; | ||
return new Promise(function(next) { | ||
// Bit confusing, perhaps; we pass a `resolve` fn | ||
// to the user, so they can decide when and if to continue. | ||
onItem(item, next); | ||
}).then(function(shouldContinue) { | ||
if (shouldContinue === false) { | ||
return handleIteration({done: true}); | ||
} else { | ||
return asyncIteratorNext().then(handleIteration); | ||
} | ||
}); | ||
} | ||
|
||
asyncIteratorNext().then(handleIteration).catch(reject); | ||
}); | ||
} | ||
|
||
function autoPagingEach(self, requestArgs, spec, firstPagePromise) { | ||
return function autoPagingEach(/* onItem?, onDone? */) { | ||
var args = [].slice.call(arguments); | ||
var onItem = getItemCallback(args); | ||
var onDone = getDoneCallback(args); | ||
if (args.length > 2) { | ||
throw Error('autoPagingEach takes up to two arguments; received:', args); | ||
} | ||
|
||
function requestNextPage(listResult) { | ||
var lastIdx = listResult.data.length - 1; | ||
var lastItem = listResult.data[lastIdx]; | ||
var lastId = lastItem && lastItem.id; | ||
if (!lastId) { | ||
throw Error('Unexpected: No `id` found on the last item while auto-paging a list.'); | ||
} | ||
return makeRequest(self, requestArgs, spec, {starting_after: lastId}); | ||
} | ||
|
||
// If a user calls `.next()` multiple times in parallel, | ||
// return the same result until something has resolved | ||
// to prevent page-turning race conditions. | ||
var currentPromise; | ||
function memoizedPromise(cb) { | ||
if (currentPromise) { | ||
return currentPromise; | ||
} | ||
currentPromise = new Promise(cb).then(function(ret) { | ||
currentPromise = undefined; | ||
return ret; | ||
}); | ||
return currentPromise; | ||
} | ||
|
||
// Iterator state. | ||
var listPromise = firstPagePromise; | ||
var i = 0; | ||
|
||
function iterate(listResult) { | ||
if (i < listResult.data.length) { | ||
var value = listResult.data[i]; | ||
i += 1; | ||
return {value: value, done: false}; | ||
} else if (listResult.has_more) { | ||
// Reset counter, request next page, and recurse. | ||
i = 0; | ||
listPromise = requestNextPage(listResult); | ||
return listPromise.then(iterate); | ||
} | ||
return {done: true}; | ||
} | ||
|
||
function asyncIteratorNext() { | ||
return memoizedPromise(function(resolve, reject) { | ||
return listPromise | ||
.then(iterate) | ||
.catch(reject) | ||
.then(resolve); | ||
}); | ||
} | ||
|
||
// Bifurcate API for those using callbacks vs. those using async iterators. | ||
if (onItem) { | ||
var autoPagePromise = wrapAsyncIteratorWithCallback(asyncIteratorNext, onItem); | ||
return self.wrapTimeout(autoPagePromise, onDone); | ||
} else { | ||
var iterator = { | ||
next: asyncIteratorNext, | ||
return: function() { | ||
// This is required for `break`. | ||
return {}; | ||
}, | ||
} | ||
if (typeof Symbol !== 'undefined' && Symbol.asyncIterator) { | ||
iterator[Symbol.asyncIterator] = function() { | ||
return iterator; | ||
} | ||
} | ||
return iterator; | ||
} | ||
}; | ||
} | ||
|
||
module.exports = autoPagingEach; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
'use strict'; | ||
|
||
/* eslint-disable callback-return */ | ||
|
||
var testUtils = require('./testUtils'); | ||
var stripe = require('../lib/stripe')( | ||
testUtils.getUserStripeKey(), | ||
'latest' | ||
); | ||
|
||
var expect = require('chai').expect; | ||
|
||
var LIMIT = 7; | ||
|
||
describe('auto pagination', function() { | ||
this.timeout(20000); | ||
|
||
var realCustomerIds; | ||
before(function() { | ||
return new Promise(function(resolve) { | ||
stripe.customers.list({limit: LIMIT}).then(function(customers) { | ||
realCustomerIds = customers.data.map(function(item) { | ||
return item.id; | ||
}); | ||
resolve(); | ||
}); | ||
}); | ||
}); | ||
|
||
describe('callbacks', function() { | ||
it('lets you call `next()` to iterate and `next(false)` to break', function() { | ||
return expect(new Promise(function(resolve, reject) { | ||
var customerIds = []; | ||
function onCustomer(customer, next) { | ||
customerIds.push(customer.id); | ||
if (customerIds.length >= LIMIT) { | ||
next(false); | ||
} else { | ||
next(); | ||
} | ||
} | ||
function onDone(err) { | ||
resolve(customerIds); | ||
} | ||
|
||
stripe.customers.list({limit: 3}).autoPagingEach(onCustomer, onDone); | ||
})).to.eventually.deep.equal(realCustomerIds); | ||
}); | ||
|
||
it('lets you ignore the second arg and `return false` to break', function() { | ||
return expect(new Promise(function(resolve, reject) { | ||
var customerIds = []; | ||
function onCustomer(customer) { | ||
customerIds.push(customer.id); | ||
if (customerIds.length >= LIMIT) { | ||
return false; | ||
} | ||
} | ||
function onDone(err) { | ||
resolve(customerIds); | ||
} | ||
|
||
stripe.customers.list({limit: 3}).autoPagingEach(onCustomer, onDone); | ||
})).to.eventually.deep.equal(realCustomerIds); | ||
}); | ||
}); | ||
|
||
describe('async iterators', function() { | ||
// `for await` throws a syntax error everywhere but node 10, | ||
// so we must conditionally require it. | ||
if (typeof Symbol !== 'undefined' && Symbol.asyncIterator) { | ||
var forAwaitUntil = require('./forAwait.node10').forAwaitUntil; | ||
|
||
it('works with `for await` when that feature exists', function() { | ||
return expect(new Promise(function(resolve, reject) { | ||
forAwaitUntil(stripe.customers.list({limit: 3}).autoPagingEach(), LIMIT).then(function(customers) { | ||
resolve(customers.map(function(customer) { return customer.id; })); | ||
}); | ||
})).to.eventually.deep.equal(realCustomerIds); | ||
}); | ||
} | ||
|
||
it('works when you call it sequentially', function() { | ||
return expect(new Promise(function(resolve, reject) { | ||
var iter = stripe.customers.list({limit: 3}).autoPagingEach(); | ||
|
||
var customerIds = []; | ||
function handleIter(result) { | ||
customerIds.push(result.value.id); | ||
if (customerIds.length < 7) { | ||
return iter.next().then(handleIter); | ||
} | ||
} | ||
iter.next().then(handleIter).then(function() { | ||
resolve(customerIds); | ||
}); | ||
})).to.eventually.deep.equal(realCustomerIds); | ||
}); | ||
|
||
it('gives you the same result each time when you call it multiple times in parallel', function() { | ||
return expect(new Promise(function(resolve, reject) { | ||
var iter = stripe.customers.list({limit: 3}).autoPagingEach(); | ||
|
||
var customerIds = [] | ||
function handleIter(result) { | ||
customerIds.push(result.value.id); | ||
} | ||
|
||
Promise.all([ | ||
iter.next().then(handleIter), | ||
iter.next().then(handleIter).then(function() { | ||
return Promise.all([ | ||
iter.next().then(handleIter), | ||
iter.next().then(handleIter), | ||
]) | ||
}).then(function() { | ||
return Promise.all([ | ||
iter.next().then(handleIter), | ||
iter.next().then(handleIter), | ||
]) | ||
}).then(function() { | ||
return Promise.all([ | ||
iter.next().then(handleIter), | ||
iter.next().then(handleIter), | ||
]) | ||
}) | ||
]).then(function() { | ||
resolve(customerIds); | ||
}); | ||
})).to.eventually.deep.equal(realCustomerIds.slice(0, 4).reduce(function(acc, x) { | ||
acc.push(x); | ||
acc.push(x); | ||
return acc; | ||
}, [])); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
'use strict'; | ||
|
||
module.exports.forAwaitUntil = async function forAwaitUntil(iterator, limit) { | ||
const items = []; | ||
for await (const item of iterator) { | ||
items.push(item); | ||
if (items.length >= limit) { | ||
break; | ||
} | ||
} | ||
return items; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,2 @@ | ||
--bail | ||
--recursive | ||
**/*.spec.js |