Skip to content
This repository has been archived by the owner on Jan 20, 2020. It is now read-only.

Promisify #60

Closed
wants to merge 19 commits into from
Closed

Promisify #60

wants to merge 19 commits into from

Conversation

nikulis
Copy link
Contributor

@nikulis nikulis commented Jun 9, 2017

Here's my first pass at adding in promise support re: #41.

The main area of interest for this PR is the tests directory (re: discussion on #55), where I've reorganized the test suites and added tests that exemplify uses of both callbacks and promises where applicable.

When I started work on this, I had not yet realized that the blocking performance regressions on #40 were due to how tests were running instead of introduced code complexity, so I did my own port to ES2015/17 (also just to get myself familiar with the codebase). I don't mean to replace the hard work others have done on #40, and would be happy to port my changes over to that branch after the maintainers have decided on Node version compatibility and updated the CI to build accordingly.

- Refactor library codebase to use native ES6 syntax, including classes
- Remove dependency on lodash functions
- Update dependencies to latest versions
- Refactor test code to use `const`, arrow functions where possible
- Normalize codebase formatting using prettier
Added tests to ensure that, for methods which support callbacks
and return promises, that both the callback and promise give the
expected behavior.

Reorganized test suite to use `tdd` test ui and use mocha's `spec`
reporter for better printout of test hierarchy.
…c.js`.

Also move mocks JSON into a new folder
callback = orderID;
callback(new Error('must provide an orderID or consider cancelOrders'));
return;
if(!orderID || typeof orderID == 'function') {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the typeof 'function' check necessary here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think their original intent was to ensure that the first argument was an orderID instead of a callback. So if orderID were a function, then !orderID === true, so you have to also check to make sure it's not a function.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ahh yeah right, I didn't think about it enough 👍

return prototype.get.call(self, ['accounts'], callback);
};
getAccounts(callback) {
return this.get(['accounts'], callback);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thoughts on using arrow syntax in classes?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How so? If you mean something like

class Whatever {
  getAccounts: callback => this.get(['accounts'], callback);
}

I'm pretty sure that's invalid JS syntax.

Copy link
Contributor

@awitherow awitherow Jun 11, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

class whatever {
    func = prop => this.otherfunc()
}

works?

Copy link
Contributor Author

@nikulis nikulis Jun 11, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not until ES Class Fields are finalized (currently stage 2). Would (/ will?) be nice though.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Jumping between babelified React Native, React and Node can get confusing 😅

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel your pain!


args = args || {}
if (!callback && (typeof args === 'function')) {
async cancelAllOrders(args = {}, callback) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😍

return;
}
totalDeletedOrders.push(...data);
} while (data.length);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

elegant while!

callback(new Error('must provide an orderID or consider getOrders'));
return;
getOrder(orderID, callback) {
if (!orderID || typeof orderID == 'function') {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

again on the use of the orderID function check.

args = args || {}
if (!callback && (typeof args === 'function')) {
getProductOrderBook(args = {}, callback) {
if (!callback && typeof args === 'function') {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These couple lines of code are duplicated so much in this repo in general, thoughts on what could be done to reduce that in a functional way?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In terms of optimizing syntactic aesthetics, decorators are the first thing I think of,

@optionalArgs
someFunction(args, otherParam, callback) {
  /* ... */
}

But even though decorators are in Stage 2, there isn't great out-of-the-box support for transpiling them quite yet, including in the latest version of babel.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah definitely something for the future 🌈 🦄

// rate-limited, try again
setTimeout(() => {
fetchTrades.call(this, stream, tradesFrom, tradesTo, shouldStop);
}, 900);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why 900? Just curious about the setTimeout.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure, that's the timeout that was used previously.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm interesting. 👍

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rate limit is per second, this skips to the next second. Not sure if this is the ideal solution, we could also just trigger a couple more requests.

var self = this;
self.emit('open');
onOpen() {
this.emit('open');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

confused at this here... onOpen is called when 'open' is emitted, and then it immediately emits 'open' again?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, now that you point that out, I'm confused as well. This was the previous behavior. Thoughts @fb55?

@nikulis nikulis mentioned this pull request Jun 10, 2017
Copy link
Contributor

@fb55 fb55 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the long wait — this is looking really promising (no pun intended). I left some comments, but this should be good to go soon.

How do you feel about integrating #58 (with default whitespace settings, I didn't see that we were using two spaces here)?

edit: Also, you will need to set the minimum node version for circle to pass.

do {
// Have to use a special promise to get response as well as data
// for later passing to the callback;
[response, data] = await new Promise((resolve, reject) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer to not use async/await in this library for now (node@6 should be the target).

this.delete(['orders'], opts, (err, response, data) => {
if (err) reject(err);
resolve([response, data]);
}).catch(() => {}); // silently ignore, rejection is already handled in callback
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally this would be appended every time we pass a callback.

if (!callback) {
throw "Must supply a callback"
}
if (!callback) callback = () => {};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's handle this in makeRequestCallback

if (!started) {
started = true;
fetchTrades(self, rs, tradesFrom, tradesTo, shouldStop, 0);
fetchTrades.bind(this)(rs, tradesFrom, tradesTo, shouldStop, 0);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.call

Copy link
Contributor

@fb55 fb55 Jul 13, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use fetchTrades.call here.

// rate-limited, try again
setTimeout(() => {
fetchTrades.call(this, stream, tradesFrom, tradesTo, shouldStop);
}, 900);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rate limit is per second, this skips to the next second. Not sure if this is the ideal solution, we could also just trigger a couple more requests.

const EventEmitter = require('events').EventEmitter;
const Websocket = require('ws');
const Utils = require('../utilities.js');
const signRequest = require('../../lib/request_signer').signRequest;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Destructuring for EventEmitter and signRequest — should also be enforced by eslint (prefer-destructuring).

This was referenced Jun 15, 2017
- Add engine `node >= 6.0.0`
  - Refactor out (async/await) --> es2015
-`extend:eslint-recommended`
  - Refactor a bit of code to match
@nikulis nikulis mentioned this pull request Jun 20, 2017
@nikulis
Copy link
Contributor Author

nikulis commented Jul 4, 2017

Hmm, CircleCI seems to have independently fallen down on this one ("An error occurred while fetching information about this build from GitHub.").

I spun up my own Circle instance and it seems to be working fine, though. Can anyone with access go in and trigger a re-build?

@nikulis
Copy link
Contributor Author

nikulis commented Jul 4, 2017

Side note: It seems that Circle doesn't read node engine settings from package.json, so we have to use a circle.yml config file for now.

Also introduce a few wording, stylistic tweaks.

Correct markdown syntax
Also add relevant test case

This addresses coinbase#23 and coinbase#66
If a client method is invoked with a provided callback, the ultimately-
returned promise will never be fulfilled (neither resolved nor rejected).

This prevents potential `UnhandledPromiseRejectionWarning`s, which will
cause future versions of Node to terminate.
@nikulis
Copy link
Contributor Author

nikulis commented Jul 12, 2017

Just checking in - would anyone have time to review these newer changes?

If all is well, I propose merging this branch and getting a new release out very soon, possibly with some other addressed issues as well. Thinking towards the future, I also still have nikulis#1 open (based on this branch) which implements the more standard two-argument callback API (major breaking change).

There are many other open issues that I would like to work on, but I am trying to limit the scope of this PR itself. If the maintainers are interested in moving forward with this PR (as is my impression), I think it would be valuable to get it through soon; there seem to be many community members submitting PRs based off of the current master which would all need significant refactoring in order to apply here, and I think it would be fairer to them to not have to duplicate so much work going forward. That being said, I'm always happy to help with related tasks too :)

Thanks!

Copy link
Contributor

@fb55 fb55 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry that it took this long to do another review. I very much like where this is going, and I agree this should land soon to avoid merge conflicts.

We should revisit the promise interface, plus there is one remaining bug (a missing .bind). Still, this should be good to go very soon.

README.md Outdated

try {

let products = await publicClient.getProducts();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should standardise to either use let or const in this file. I'd opt for const.

opts = opts || {};
if (!callback && (typeof opts === 'function')) {
request(method, uriParts, opts = {}, callback) {
if (!callback && typeof opts == 'function') {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

=== for typeof checks

callback(err, response, data);
return;
}
if (err) reject(err);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if/else should always have blocks.

callback(err, response, data);
if (typeof callback == 'function') {
callback(err, response, data);
return;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should still fulfil the promise. What do you think about

  1. Having the signed request simply call the unsigned variant (super.request)
  2. Use superagent's promises directly:
    • Create the promise, store a reference
    • If there is a callback, add handling for it in a then callback
    • Return the original promise (not the one with error handling applied) — this will ensure that errors are always forwarded

if (!started) {
started = true;
fetchTrades(self, rs, tradesFrom, tradesTo, shouldStop, 0);
fetchTrades.bind(this)(rs, tradesFrom, tradesTo, shouldStop, 0);
Copy link
Contributor

@fb55 fb55 Jul 13, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use fetchTrades.call here.

test('subscribes to the default product (BTC-USD) if undefined', function(done) {
var server = testserver(++port, function() {
var websocketClient = new Gdax.WebsocketClient(null, 'ws://localhost:' + port);
test('subscribes to the default product (BTC-USD) if undefined', function(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of these should be switched to arrow functions (eslint can also take care of that — only named functions should still be allowed).

.eslintrc.json Outdated
"root": true,
"extends": "eslint:recommended",
"env": {
"commonjs": true,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pretty sure this is implied by node

.eslintrc.json Outdated
"ecmaVersion": 2015
},
"rules": {
"constructor-super": "warn",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of these should error.

var callback = function(err, response, data) {
// your code here.
};

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps add some description here: "Promises can be used with callbacks, as well as using async/await in modern environments." (or something similar).

if (err) reject(err);
if (data === null)
reject(new Error('Response could not be parsed as JSON'));
else resolve(data);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been looking into replacing parts of this library with swagger-codegen. Their promise interface returns a {data, response} object — I guess we should adopt that as well. While I like the simplicity of this API, there probably are cases where you want the request object (for headers, status codes etc.), plus an object would allow for future extensions.

All eslint rules now error instead of warn

- Add eslint rule `curly: all` - require block statements for control
  structures (`if`/`else`, etc.)
- Add eslint rule `eqeqeq` - only strict equality operators
- Add eslint rule `func-names: always` - no anonymous `function`
  expressions
- Add eslint rule `prefer-arrow-callback: { allow-named-functions: true }`

Add `eslint` to list of `lint-staged` actions to be enforced before
commits
Remove duplicate request logic from `AuthenticatedClient`; delegate to
`super` request method
Add some more descriptive text

Use `const` over `let` in code examples
Refactor to use promise interface on public/authed clients'
`.getProductOrderBook`

Update test mock ws server to respond with expected format (with
`product_id` data)

Remove unnecessary call to `Util.determineProductIds`
return prototype.get.call(self, ['products'], callback);
};
if (callback) {
p.catch(() => {});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is definitely sensible behaviour, although I'd simply add listeners to the promise in case there is a callback. Still return the original promise (not the one with the callbacks applied) so that nothing needs to be returned from the callback.

We could also get rid of makeRequestCallback in favour of superagent promises — how do you feel about that?

@fb55
Copy link
Contributor

fb55 commented Jul 17, 2017

One comment, but that can be iterated upon once this is merged. There is a merge conflict with the package.json file — once that's resolved I'm happy to merge. Thanks a lot for all the work, this is amazing.

@fb55 fb55 closed this in a146d93 Jul 26, 2017
@fb55
Copy link
Contributor

fb55 commented Jul 26, 2017

Finally got around to merging this. Thanks a lot @awitherow and especially @nikulis! I'd like to land the superagent switch before publishing it — @nikulis do you want to open a PR with the code you already have?

@awitherow
Copy link
Contributor

Thanks to @nikulis I was just giving some insight! He did the 💪. 👏

@nikulis
Copy link
Contributor Author

nikulis commented Jul 29, 2017

I was holding off on superagent for this one because those using v0.4.x might be relying upon the currently-exposed response object (2nd callback parameter) that the request library returns. I've personally never used this in my own projects, so I'd have to look into whether the response object could be easily reconstructed using the information that superagent returns. If so, I can

I did switch to superagent with the new two-parameter callback API in nikulis#1. That branch is a little stale atm, so I'm about to work on getting it up-to-date before I open a new PR in this repo. (Just got back online after a few days off 😅 )

And yes, thank you to all of you, too, for providing all the wonderful feedback and critiques!

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants