Skip to content

Add support for ES6 style async boot scripts (2.x)#252

Merged
bajtos merged 1 commit intostrongloop:2.xfrom
lehni:feature/async-boot-scripts-2.x
Jul 31, 2017
Merged

Add support for ES6 style async boot scripts (2.x)#252
bajtos merged 1 commit intostrongloop:2.xfrom
lehni:feature/async-boot-scripts-2.x

Conversation

@lehni
Copy link
Copy Markdown
Contributor

@lehni lehni commented Jul 24, 2017

Description

This PR allows boot scripts to be defined in the ES6 async fashion, without the need for passing callbacks:

module.exports = async function(app) {
   ...
};

Related issues

  • None

Checklist

  • New tests added or existing tests modified to cover all changes
  • Code conforms with the style
    guide

@slnode
Copy link
Copy Markdown

slnode commented Jul 24, 2017

Can one of the admins verify this patch? To accept patch and trigger a build add comment ".ok\W+to\W+test."

@slnode
Copy link
Copy Markdown

slnode commented Jul 24, 2017

Can one of the admins verify this patch?

2 similar comments
@slnode
Copy link
Copy Markdown

slnode commented Jul 24, 2017

Can one of the admins verify this patch?

@slnode
Copy link
Copy Markdown

slnode commented Jul 24, 2017

Can one of the admins verify this patch?

@bajtos
Copy link
Copy Markdown
Member

bajtos commented Jul 26, 2017

@slnode ok to test

Copy link
Copy Markdown
Member

@bajtos bajtos left a comment

Choose a reason for hiding this comment

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

Hello @lehni, thank you for the pull request.

First of all, please add a test to verify your implementation.

Secondly, we usually try to support any function returning a promise, not only async functions. The trick is to check whether the function call returned an object containing a then method.

var cb = function(err) {
  debug('Async function finished %s', f.path);
  done(err);
};

debug('Starting function %s', f.path);
var result = f.func(app, cb);
if (result && typeof result.then === 'function') {
  result.then(ok => cb(), cb);
  return;
} else if (f.func.length < 2) {
  debug('Sync function finished %s', f.path);
  done();
}

lib/executor.js Outdated
});
};
if (isAsync) {
f.func(app).then(cb.bind(null, null)).catch(cb);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If cb(null) throws an error, then .catch() will invoke cb again with this error - that's not correct IMO. You should pass the error-handling-callback as the second argument of .then():

f.func(app).then(cb.bind(null, null), cb);

@bajtos bajtos added the feature label Jul 26, 2017
@bajtos bajtos self-assigned this Jul 26, 2017
@bajtos
Copy link
Copy Markdown
Member

bajtos commented Jul 26, 2017

This is a great feature, I am looking forward to get it added! 👍

@lehni
Copy link
Copy Markdown
Contributor Author

lehni commented Jul 27, 2017

@bajtos I will work on this improvements and unit test for them. In the meantime, can you tell me which branch I should work on? Since there are two active branches (2.x and master), and I am still using 2.x for LB 3 (master doesn't seem to load mixins correctly, I meant to create a seperate issue for this), I guess I'll work on 2.x first and then port to master when it landed?

@bajtos
Copy link
Copy Markdown
Member

bajtos commented Jul 27, 2017

I guess I'll work on 2.x first and then port to master when it landed?

Yes please, this works best for me.

@lehni
Copy link
Copy Markdown
Contributor Author

lehni commented Jul 28, 2017

@bajtos I've pushed a new version based on your input, along with some unit tests. PTAL

@lehni
Copy link
Copy Markdown
Contributor Author

lehni commented Jul 28, 2017

Two more things:

  1. I needed to use an anonymous function instead of the =>, because eslint was complaining about it. I tried to update eslint to a version that supports it, but that triggered a whole row of other linting issues, so I stuck with the function for now

  2. The tests reference the Promise object. I am not sure if that's fine? If not, then I can keep it all to thenable objects.

Copy link
Copy Markdown
Member

@bajtos bajtos left a comment

Choose a reason for hiding this comment

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

I needed to use an anonymous function instead of the =>, because eslint was complaining about it. I tried to update eslint to a version that supports it, but that triggered a whole row of other linting issues, so I stuck with the function for now

We are trying to keep support for Node.js 0.x versions in the 2.x branch for backwards compatibility, ES6 constructs cannot be used here. This applies to most 2.x branches in loopback projects (strong-remoting, loopback-datasource-juggler, etc.). See strongloop/loopback#2807 (comment), you may want to read through the discussion that lead us to the current approach.

The tests reference the Promise object. I am not sure if that's fine? If not, then I can keep it all to thenable objects.

Sure, that's fine.

// License text available at https://opensource.org/licenses/MIT

module.exports = function(app) {
return Promise.reject('Promise.reject()')
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Our practice is to reject Promises with Error instances.

return Promise.reject(new Error('Promise.reject()'));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Also please add .js extension to the filename. I am surprised your new tests are passing despite this problem?

Copy link
Copy Markdown
Contributor Author

@lehni lehni Jul 28, 2017

Choose a reason for hiding this comment

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

I am using a bit of a workaround with scriptExtensions set to '.reject' for this test, so the failing boot script does not get loaded in all other tests, but I can then explicitly request it for that single test and produce the rejection. It's not super clean but I couldn't think of another way to achieve this goal without creating another full app (similar to the simple-app folder)... If I add .js, then it would be loaded in all boot script tests, and fail each time.

I hope it's fine?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Actually, I think I have a better idea for how to control this. I'll do the same to test for sync exceptions also.

lib/executor.js Outdated
result = f.func(app, cb);
} catch (err) {
debug('Sync function failed %s', f.path, err);
error = err;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AFAICT, error is not used. Can we remove it please?

Also when the function exported by a boot script fails, the error should be reported back/up to the caller of boot(app). I think you should call return done(err) here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It was late last night... My bad. Will address it now.

@bajtos
Copy link
Copy Markdown
Member

bajtos commented Jul 28, 2017

@lehni the CI is failing on Node.js 0.10, see https://travis-ci.org/strongloop/loopback-boot/jobs/258394078. It may be caused by the fact that Node.js 0.10 does not have a built-in Promise object, in which case you can add bluebird as a dev-dependency and use bluebird Promise instead of the built-in one in the newly added boot scripts. Also because your new code is silently swallowing errors thrown by boot scripts, the CI output is not making the problem obvious.

@lehni
Copy link
Copy Markdown
Contributor Author

lehni commented Jul 28, 2017

@bajtos oops, yeah that's why the errors didn't make sense to me probably. I will look into both issues right now. Stil learning :)

@lehni
Copy link
Copy Markdown
Contributor Author

lehni commented Jul 28, 2017

@bajtos I think this is it. I responded to all your feedback and added a test for errors thrown from boot scripts. And it looks like it's passing all tests!

Copy link
Copy Markdown
Member

@bajtos bajtos left a comment

Choose a reason for hiding this comment

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

Almost there :) I'll fix cb && cb() myself to speed the process up.

lib/executor.js Outdated
try {
var result = f.func(app, cb);
if (result && typeof result.then === 'function') {
result.then(function() { cb && cb(); }, cb);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

cb is always defined, the check cb && is redundant.

describe('with boot script returning a rejected promise', function() {
before(function() {
// Tell simple-app/boot/reject.js to return a rejected promise
process.rejectPromise = true;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It would be probably simpler to create a different application (a set of instructions) for each of these error cases. Having said that, your approach with process flags is ok too.

@bajtos bajtos merged commit 7f34b0d into strongloop:2.x Jul 31, 2017
@bajtos
Copy link
Copy Markdown
Member

bajtos commented Jul 31, 2017

Landed, thank you for the contribution!

@bajtos
Copy link
Copy Markdown
Member

bajtos commented Jul 31, 2017

Released as loopback-boot@2.26.0. Enjoy 🎉

@lehni
Copy link
Copy Markdown
Contributor Author

lehni commented Jul 31, 2017

Very cool! That went faster than I'd imagined. Thanks!

@lehni
Copy link
Copy Markdown
Contributor Author

lehni commented Jul 31, 2017

Shall I port this to #253 now?

@slnode
Copy link
Copy Markdown

slnode commented Jul 31, 2017

Can one of the admins verify this patch?

@lehni
Copy link
Copy Markdown
Contributor Author

lehni commented Aug 1, 2017

@bajtos the reason for cb && cb() was in this comment above:

    var cb = function(err) {
      done(err);
      // Make sure done() isn't called twice, e.g. if a script returns a
      // thenable object and also calls the passed callback.
      cb = null;
    };

I believe it's still valid?

@bajtos
Copy link
Copy Markdown
Member

bajtos commented Aug 3, 2017

the reason for cb && cb() was in this comment above:
(...)
I believe it's still valid?

@lehni Ouch! You are right. I think a better solution is to set cb to a no-op function instead of null. Would you mind contributing this change in a new pull request? It would be great to add a unit-test with a script returning both a thenable object and calling the callback.

Shall I port this to #253 now?

Yes please 👍

@lehni
Copy link
Copy Markdown
Contributor Author

lehni commented Aug 16, 2017

@bajtos I will wait for #260 before resolving #253, and will then roll both #252 and #260 into it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants