-
Notifications
You must be signed in to change notification settings - Fork 751
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Auto-Pagination #496
Auto-Pagination #496
Conversation
7efc7ea
to
1877d7a
Compare
What does that mean? Is this to have some kind of 'output' from the whole thing? If so, I think it's probably not worth doing, tbh; if someone wants to collect 'em all, they can do so, but per comments in 225, I think it's a bit risky for us to keep track of this - and I don't think any of the other libraries do either? |
Yeah, I feel that. Pretty on the fence myself. I'm basically picturing a lot of users writing their own wrappers to do exactly this, though, which makes me cringe a bit, especially given that it's a lot less straightforward/pleasant than in any of the other languages. Plus, for-loops and iteratively pushing into an array have (somewhat rightfully) fallen out of style in the JS community, in favor of As a first pass implementation, I have: requestPromise.autoPagingToArray = function(onDone) {
var promise = new Promise(function(resolve) {
var items = [];
requestPromise.autoPagingEach(function(item) {
items.push(item);
}).then(function() {
resolve(items);
});
});
return self.wrapTimeout(promise, onDone);
} ... though I'm leaning towards a mandatory Frankly, looking at the above code, I think we'd ~need to either include a copy/pastable version of that in our docs, or just include the helper directly. |
Okay, added an implementation of Using the tests as an example, comparing the ~simplest case of stripe-node/test/autoPagination.spec.js Lines 52 to 63 in 173eb65
to the equivalent with autoPagingToArray :stripe-node/test/autoPagination.spec.js Lines 141 to 145 in 173eb65
And also looking at the 10-line boilerplate wrapper we'd need to recommend for casual use, I'm personally leaning towards including this in some form. My impression is that none of our other languages have such non-idiomatic/ugly code required to for happy-path usecases. EDIT: for example, node requires two callbacks (or a callback and a promise) to handle each item and then do stuff when it's done. In every other language, you can just write code after an idiomatic for-loop, or in ruby's case just chain after I'm assuming here that a large percentage of paginated list requests against the Stripe API have fewer than 10k results (eg; ~small users poking around at ~small collections), and that 10k results could easily fit within memory & rate limits. I'm also assuming that users ~frequently open up a node console to poke around at things in an exploratory fashion. Definitely open to thoughts on this, and bikeshedding re; the API. These are just my hunches. |
Refactor out StripeResource#wrapTimeout as utils.callbackifyPromise
4eb125c
to
204aaee
Compare
c5eb2d6
to
4f410af
Compare
4f410af
to
c7473a1
Compare
c7473a1
to
3cf98f5
Compare
r? @brandur-stripe @jlomas-stripe |
Hey @rattrayalex-stripe! I'll try to take a peek at this today or tomorrow. Thanks so much for taking this on! \o/ |
@rattrayalex-stripe I think the test for "lets you call |
sweet, thanks @jlomas-stripe ! re; Separately, re; moving I could have moved the |
By the way, @romain-stripe had suggested |
Thanks mocha
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice one Alex!
I'd probably +1 limit
over max
on autoPagingToArray
— even though it's a bit overloaded in being the same name as on list endpoints, the function is similar enough that it's pretty easy to understand. If they were different, it would be a little surprising.
lib/utils.js
Outdated
@@ -227,6 +227,19 @@ var utils = module.exports = { | |||
} | |||
return false; | |||
}, | |||
|
|||
callbackifyPromise: function(promise, callback) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't the old name for this (wrapTimeout
) better? It basically seems to take a callback and put it in a timeout block.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmm, it happens to use a timeout to achieve a secondary goal of letting a promise instead call a callback, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, agreed, but it seems like the timeout piece is kind of important here. callbackifyPromise
reads like a promise utility function or something to me. Maybe callbackifyPromiseWithTimeout
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SGTM, will do on Monday
lib/resources/Accounts.js
Outdated
@@ -14,6 +14,7 @@ module.exports = StripeResource.extend({ | |||
list: stripeMethod({ | |||
method: 'GET', | |||
path: 'accounts', | |||
autoPageable: true, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So the idea is that we still need to add this parameter to a few dozen other list calls elsewhere right?
The autoPageable
parameter seems a little unusual to me given that (1) only lists are pageable, and (2) all lists should be pageable. What do you think about naming this something like methodType: 'list'
or something along those lines instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, looks like I missed methods like listTransactions
, etc – looks like there's 13 of those.
I like methodType: 'list'
a little better too, let's go with that. My thinking was "what if there's a list that isn't pageable" but that's pretty silly, since we try hard to avoid that (and I don't think there are current instances of such behavior).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
✅
lib/autoPagination.js
Outdated
var makeRequest = require('./makeRequest'); | ||
var utils = require('./utils'); | ||
|
||
function getItemCallback(args) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you think that we could make an effort to organize this file a little bit more? Either:
- Functions listed alphabetically.
- Functions listed by category with exported on the top (so you drop into the file and see the most important ones first) and unexported below, and alphabetically within each category.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sure, I like (2)!
It's a bit of a smell in JS to call functions before they're defined, imo (it's not possible with arrow functions like const fn = () =>
, only function declarations like we have here) but given that we're likely to stay with ES5 source code for some time, I'm happy to make a silver lining out of it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
✅
lib/autoPagination.js
Outdated
throw Error('The `onItem` callback function passed to autoPagingEach must accept at most two arguments; got ' + onItem); | ||
} | ||
|
||
// API compat; turn this: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you add a little more flavor to this comment? What API is this trying to be compatible with?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
✅
lib/autoPagination.js
Outdated
} else if (listResult.has_more) { | ||
// Reset counter, request next page, and recurse. | ||
i = 0; | ||
listPromise = requestNextPage(listResult); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there any potential harm to going to an arbitrary stack depth like this? Like if we iterated a thousand pages, do we have a thousand functions sitting on the stack and the potential to overflow? I know some languages do tail call optimization for this kind of situation, and it looks like newer versions of JS have it, but not sure if we should expect it to kick in here or not.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, interesting. I wasn't sure about this but it looks like recursive promises are handled by the promise implementation, not the call stack:
That recursive call is asynchronous and so it does not abuse the call stack by definition.
Which I got from this post, ironically about how the promise implementation leaks memory (fortunately, at rates which don't seem problematic – I think it's not until you get well into the millions at a minimum).
So I think we're good here; thanks for checking
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
K, thanks for looking into that.
lib/autoPagination.js
Outdated
}); | ||
} | ||
|
||
function autoPagingEach(self, requestArgs, spec, firstPagePromise) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since these are new methods and fairly non-obvious to figure out how to use based on the function signature, could we try adding some basic documentation blocks for autoPagingEach
and autoPagingToArray
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure; for users or maintainers? I assume users?
Okay, most feedback addressed (except for docs which is locally WIP). Actually, in writing docs for this (which I started in docstrings but would like to move to the readme), I'm leaning towards moving from this: for await (const customer of stripe.customers.list().autoPagingEach()) {} to this: for await (const customer of stripe.customers.list()) {} which will enable getting rid of a hack or two and should be cleaner for users. I have a WIP on that but will need to finish on Monday. Thoughts? |
@rattrayalex-stripe How would that work, exactly? Is it, like ... |
Was wondering the same thing! :) The invocation certainly looks incredibly clean if we can get there. |
yeah, instead of
... which I'll probably wrap into a mutative helper packed in |
a3fcfbe
to
533239a
Compare
Alright, I think this is ready for another look! |
README.md
Outdated
@@ -216,6 +216,87 @@ stripe.setAppInfo({ | |||
|
|||
This information is passed along when the library makes calls to the Stripe API. | |||
|
|||
### Auto-pagination | |||
|
|||
As of stripe-node 6.11.0, you may auto-paginate list api methods. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(I assume I should bump the version separately from this PR; is it safe to assume this'll go out as 6.11.0?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah probably. We can always fix it later too!
FWIW, I ran a back-of-napkin |
README.md
Outdated
@@ -216,6 +216,87 @@ stripe.setAppInfo({ | |||
|
|||
This information is passed along when the library makes calls to the Stripe API. | |||
|
|||
### Auto-pagination | |||
|
|||
As of stripe-node 6.11.0, you may auto-paginate list api methods. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you capitalize to "API" given this is user facing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmm, actually I think I'll just remove the word since it's used differently than the next sentence: you may auto-paginate list methods
. sgty?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep! SG.
README.md
Outdated
### Auto-pagination | ||
|
||
As of stripe-node 6.11.0, you may auto-paginate list api methods. | ||
We provide a few different API's for this to aid with a variety of node versions and styles. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry the nit, but I think we usually use "APIs" (see examples from Google). There's a little disagreement, but without an apostrophe is usually considered the right way to pluralize an acronym.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice, yes, thanks!
README.md
Outdated
|
||
#### Async iterators (`for-await-of`) | ||
|
||
If you are in a node environment that has support for [async iteration](https://github.com/tc39/proposal-async-iteration#the-async-iteration-statement-for-await-of), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we pluralize "Node" here like it is below?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assume you meant capitalize 😄 but yes, good catch!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Haha, oops, yes.
Awesome. Left a few minor comments on README wording, but LGTM. |
I wonder if it's worth including some notes either in the code or the documentation about this, and when you might use one approach over the other? |
Awesome, thanks for the eagle-eyes @brandur-stripe ! Updated. Does the content feel good? @jlomas-stripe that's a very good point... I think we should really add docs at |
Yep, looks great! We can always iterate if we find something missing/unclear later. |
Great, thanks! Sounds like this is approved; marking as such. Will merge & release this afternoon. |
Addresses #225
Work remaining:
has_more: false
)..then
on the.list()
and its.autoPagingEach()
, passing a callback and attempting to use the result in an iterator, etc).autoPageable
to all the places that I should have (I just greppedlist:
)..autoPagingToArray
helper of some kind.cc @ob-stripe @jlomas-stripe @brandur-stripe – this is ready for preliminary eyes if you're interested. I included some refactor work, so viewing commit-by-commit may aid your sanity.