Skip to content

[SEMVER-MAJOR] Refactor for modular and pluggable design#181

Merged
davidcheung merged 1 commit intomasterfrom
feature/extensibility
Nov 22, 2016
Merged

[SEMVER-MAJOR] Refactor for modular and pluggable design#181
davidcheung merged 1 commit intomasterfrom
feature/extensibility

Conversation

@raymondfeng
Copy link
Copy Markdown
Member

@raymondfeng raymondfeng commented Apr 8, 2016

connect to strongloop-internal/scrum-loopback/issues/1136

The current boot process consists of three steps:

  • load
  • compile
  • execute

It hard-codes logic to handle various artifact types:

  • data source
  • model
  • mixin
  • middleware
  • boot script

The problems with the current design/implementation are:

  • lack of extensibility
  • no clean separation of different types of artifacts

The PR proposes the following changes:

  • refactor logic of processing artifacts into their own classes
  • introduce Container as the main class for bootstrapping and build a registry of handlers during boot to organize them by a hierarchy denoted by path
  • adopt middleware like registration and invocation
    • container.use(path, handler)
    • container.run(context, done)
  • allow more phases during boot

- explore dependency injection

Connect to #188

Update 16/11/16:

  • needs review by @raymondfeng, maybe @strongloop/squad-epitome can help out?

@bajtos
Copy link
Copy Markdown
Member

bajtos commented Apr 12, 2016

Hey @raymondfeng, the proposal looks good from high-level view. I have few questions for you:

  1. How do you envision supporting browserify scenario, where there are two steps run in different environments:
  • the first step (compile + browserify) is run in Node.js on a dev machine (or build server) and creates a set of instructions for the second step,
  • and the second steps executes these instructions in a browser.
  1. In the current version, bootstrapping of models has two phases: in the first phase, all models are defined in juggler's model registry. In the second phase, which is run after datasources are created, the models are attached to datasources and registered with app's RemoteObject instance to expose them via public (REST) API. Does your proposal take this into account?

  2. It is important to preserve the order in which different artifacts are created. For example, to attach models to datasources, datasources must be already setup; boot scripts should be generally run at the very end, when everything else is already set up. How do you propose to define this ordering?

adopt middleware like registration and invocation

container.use(path, handler)
container.run(context, done)

What is the purpose of this feature? Could you please provide examples where one would use this feature and how the code will look like?

  1. allow more phases during boot

Again, could you please show some examples of how this would be used?

  1. build a registry of handlers during boot to organize them by a hierarchy denoted by path

From your proposal, it is not clear to me what these paths will be used for. Could you please elaborate a bit more? How will these paths relate to the need of having a well-defined order in which handlers are executed?

@superkhau
Copy link
Copy Markdown
Contributor

superkhau commented Apr 14, 2016

While we're at it, @davidcheung brought up a good point about config.json vs config.js inconsistencies with our configurations. We should have the new loopback-boot respect base.js files too (config.js, model-config.js, datasources.js) etc.

See strongloop/loopback#2103

@raymondfeng raymondfeng force-pushed the feature/extensibility branch 3 times, most recently from 079b837 to 5c35225 Compare April 19, 2016 00:08
@pbhadauria2000
Copy link
Copy Markdown

pbhadauria2000 commented Apr 19, 2016

Can you also make the DYNAMIC_CONFIG_PARAM regex to a bit flexible. Something like /(.)${(\w+)}(.)/ and then you can do configVariable = match(1) + configVariable + match(3)

@raymondfeng raymondfeng force-pushed the feature/extensibility branch 10 times, most recently from 3ba32c0 to 1f81231 Compare April 26, 2016 18:17
@raymondfeng raymondfeng changed the title [WIP] Initial PoC to refactor loopback-boot (DO NOT MERGE) Initial PoC to refactor loopback-boot Apr 26, 2016
@raymondfeng
Copy link
Copy Markdown
Member Author

Connect to #188

@superkhau superkhau mentioned this pull request Apr 27, 2016
@raymondfeng raymondfeng force-pushed the feature/extensibility branch 2 times, most recently from 8555f13 to 41c95cd Compare April 28, 2016 06:53
@raymondfeng
Copy link
Copy Markdown
Member Author

@pbhadauria2000

Can you also make the DYNAMIC_CONFIG_PARAM regex to a bit flexible. Something like /(.)${(\w+)}(.)/ and then you can do configVariable = match(1) + configVariable + match(3)

Can you provide an example?

@pbhadauria2000
Copy link
Copy Markdown

@raymondfeng

I would like to load sampleData files for memory datasource from different folders based on variables in config.json. Example: file: "samples{$dataFolder}\product.json". In this example, I can keep the value of the variable "dataFolder" defined in config which could be environment specific.

*/

exports = module.exports = function bootBrowserApp(app, options) {
exports = module.exports = function bootBrowserApp(app, options, callback) {
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.

Since this is for LoopBack Next, can we drop support for callbacks and switch exclusively to Promise-based APIs here in loopback-boot?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I want to do this incrementally. The current implementation is ready for LoopBack 3.0. We can further clean it up for LoopBack.next if we decide to drop callback style.

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.

Makes sense.

@bajtos
Copy link
Copy Markdown
Member

bajtos commented Sep 16, 2016

@davidcheung once #211 is landed, please add a description to 3.0-RELEASE-NOTES.md as part of this pull request (see #181 (comment) for starter).

@davidcheung davidcheung force-pushed the feature/extensibility branch 2 times, most recently from 96356e5 to c032849 Compare September 19, 2016 20:55
@davidcheung
Copy link
Copy Markdown
Contributor

@slnode test please

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.

@davidcheung I have reviewed the changes as well as I could. I did not review the overall design again, as I have already commented on it earlier.

Besides the comments listed below, this pull request needs an entry in 3.0-RELEASE-NOTES.md describing the breaking changes.

I think it would be worthwhile to describe the new features in 3.0-RELEASE-NOTES too (the modular design and how to build 3rd party boot plugins). In my experience, the process of writing the documentation usually discover missing pieces and/or rough edges. However, I am fine with leaving the description of the new features out of scope of this pull request.

var debug = require('debug')('loopback:boot:bootstrapper');
var Promise = global.Promise || require('bluebird');

module.exports = function(options) {
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.

I personally prefer to export Bootstrapper directly.

module.exports = Bootstrapper;

When I look at the code above, I see 2 places with require('...boostrapper').Bootstrapper and only one place calling the exported function directly.

try {
loadAndRegisterPlugins(self, options);
} catch (err) {
debug(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.

Please include a message explaining what went wrong, e.g.

debug('Cannot load & register plugins: %s', err.stack || err);

var path = require('path');
var pluginLoader = require('./plugin-loader');
var debug = require('debug')('loopback:boot:bootstrapper');
var Promise = global.Promise || require('bluebird');
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.

In LoopBack 3.x, we are always using bluebird as our promise implementation. I am proposing to do the same in loopback-boot for consistency.

var Promise = require('bluebird');

} else {
result = plugin.handler[phase](context);
if (typeof Promise !== 'undefined') {
Promise.resolve(result).then(function(value) {
Copy link
Copy Markdown
Member

@bajtos bajtos Oct 11, 2016

Choose a reason for hiding this comment

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

Is this trying to implement support for handler functions returning a promise? Here is the version that we use elsewhere in LoopBack (example, it is addressing many subtle edge cases that are handled incorrectly by this code.

if (result && typeof result.then === 'function') {
  result.then(
    function onPluginPhaseResolved(value) {
      done(null, value); 
    },
    function onPluginPhaseRejected(err) {
      debug('Unable to invoke %s.%s()', plugin.name, phase, err); 
      done(err); 
    });
} else {
  done(null, result);
}

};

function pluginIteratorFactory(context, phase) {
return function(plugin, done) {
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.

Please use named functions wherever possible, e.g.

return function executePluginPhase(plugin, done) {
  // ...
};

expect(app.dummyComponentOptions).to.eql({ option: 'value' });

done();
var app = executeBundledApp(bundlePath, function(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.

unhandled err

printContextLogs(context);
done(err);
});
return app;
Copy link
Copy Markdown
Member

@bajtos bajtos Oct 11, 2016

Choose a reason for hiding this comment

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

An async function that also returns a value in sync way is very weird. Please pass app to the callback.

app.start(function(err) {
  printContextLogs(context);
  done(err, app);
});

}), function(err, context) {
if (err) return done(err);
expect(app.models.Customer._modelsWhenAttached).
to.include('UniqueName');
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.

expect(app..._modelsWhenAttached)
 .to.include('UniqueName');

function(err) {
expect(function() {
if (err) throw err;
}).to.throw(/Cannot find module \'doesnt-exist\'/);
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.

Huh?? This is overcomplicated, just use something like the following:

expect(err && err.message).to.match(/Cannot find module \'doesnt-exist\'/);

]);

// bar finished happens in the next tick
// barSync executed after bar finished
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.

I think this change needs to be clearly explained in 3.0-RELEASE-NOTES.md.

@bajtos bajtos changed the title [SEMVER-MAJOR] Initial PoC to refactor loopback-boot [SEMVER-MAJOR] Refactor for modular and pluggable design Oct 11, 2016
@davidcheung davidcheung force-pushed the feature/extensibility branch from 22ceee8 to 8138232 Compare October 26, 2016 22:39
@davidcheung davidcheung force-pushed the feature/extensibility branch 2 times, most recently from eb391c4 to 70af0d3 Compare November 11, 2016 21:40
@davidcheung
Copy link
Copy Markdown
Contributor

@bajtos can you PTAL, the changes are mostly logically separated into their own commit, but probably still quite difficult to navigate due to size of PR :(

@davidcheung
Copy link
Copy Markdown
Contributor

I think it would be worthwhile to describe the new features in 3.0-RELEASE-NOTES too (the modular design and how to build 3rd party boot plugins). In my experience, the process of writing the documentation usually discover missing pieces and/or rough edges. However, I am fine with leaving the description of the new features out of scope of this pull request.

i have only included one change in the release-note.md, will open another issue to document the modular design, and how to create your own plugin.

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 have reviewed the new commits, left two nit-picks, the rest looks good to me.

After this patch is landed, could you @davidcheung please upgrade eslint-config-loopback to v5 and fix linter errors?

});

var sortedNames = toposort(edges);
var _sortedNames = toposort(edges);
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.

We usually don't prefix variable names with _.

lib/utils.js Outdated
var files = tryReadDir(sourceDir);
var fixedFile = fixFileExtension(resolvedPath, files, false);
return (fixedFile === undefined ? resolvedPath : fixedFile);
var _fixedFile = fixFileExtension(resolvedPath, files, false);
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.

We usually don't prefix variable names with _.

@bajtos
Copy link
Copy Markdown
Member

bajtos commented Nov 22, 2016

@davidcheung also please don't forget to squash the commits before landing!

@bajtos bajtos assigned davidcheung and unassigned raymondfeng and bajtos Nov 22, 2016
@davidcheung davidcheung force-pushed the feature/extensibility branch 2 times, most recently from d531771 to 64a62ba Compare November 22, 2016 17:20
- refactor logic of processing artifacts into their own classes
- introduce Container as the main class for bootstrapping and build a
  registry of handlers during boot to organize them by a hierarchy
  denoted by path
- adopt middleware like registration and invocation
- container.use(path, handler)
- container.run(context, done)
- allow more phases during boot
- boot is now asynchronous
@davidcheung davidcheung force-pushed the feature/extensibility branch from 64a62ba to ac1571c Compare November 22, 2016 18:40
@davidcheung davidcheung merged commit f7c9cbc into master Nov 22, 2016
@davidcheung davidcheung deleted the feature/extensibility branch November 22, 2016 19:24
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.

6 participants