-
Notifications
You must be signed in to change notification settings - Fork 36
New Registry Proposal
There are a number of deficiencies with the current service registry (as of BRJS v0.15.4):
- It doesn't allow services to be provided asynchronously.
- It has two distinct ways of creating services (lazy automatic creation for zero-arg constructors and explicit injection for non zero-arg constructors) and these two mechanisms don't always play nicely together, leading to bootstrapping issues.
-
require()
is used to invokeServiceRegistry.get()
behind the scenes, which may make it incompatible with ES2016 modules (where modules are permanently cached), assuming ES2016 doesn't end up inheriting theSystem.set()
method that the polyfills have.
BRJS currently includes a declarative IoC mechanism known as aliasing instead of using a simpler programmatic IoC mechanism (e.g. getService()
and setService()
). This is needed because:
- A programmatic IoC mechanism where the developer is left to register all services would regularly lead to backwardly incompatible library updates as libraries that developers use introduce new services, forcing developers to update every aspect, workbench and test-suite that makes use of an updated library.
- A programmatic IoC mechanism that uses helper classes to prevent backwardly incompatible breakages introduces the unwanted side effect that all apps become dependent on all services and their transitive dependencies.
However, it has been noted that once there is widespread support for ES6 modules and HTTP/2 within all supported browsers (and the es6-module-loader polyfill makes the former available even in IE8) then it becomes possible to stop bundling source modules altogether, and leave the browser to dynamically load the source modules an app needs. Furthermore, these technologies make it possible to switch to a simpler programmatic IoC mechanism, since helper classes will then be able to refer to the default implementations of classes that could be used without having those dependencies loaded unless they are actually needed.
For the foreseeable future though, it will continue to be necessary for BRJS to determine an app's dependencies using source-code scanning alone, so that uniquely identifiable alias strings will continue to be necessary. Additionally, since having both injected and automatically created dependencies often leads to bootstrapping issues, any new APIs for programmatic injection should only allow the injection of the service provider, and not the service itself.
Although only tangentially related, it's worth addressing how future ES6 and NPM compatibility might work. At present we bundle all source code together and deliver as a set of define()
invocations that are processed by browser-modules, the BRJS implementation of CommonJS.
NPM Support could be achieved by using browserify to emit a bundle of all the required NPM modules, where browserify's require()
is subsequently replaced by browser-modules's require()
, but stored and used as a back-up for modules that aren't available as part of the BRJS bundle, as described in the original NPM Integration Strawman.
By using babelify to bundle the NPM modules it becomes even easier for browser-modules to integrate with NPM since we no longer have two libraries attempting to define require()
, plus developers would be able to start writing any NPM modules in ES6.
By using Babel to transpile any BRJS modules, NPM modules and BRJS modules would both start to share the same bundle format, potentially allowing us to drop support for browser-modules in favor of System.register()
.
However, for this to happen, a replacement for sub-realms would be needed, as this is an important transitioning tool for people converting namespaced code (where you can easily replace packages and classes within tests) to CommonJs, where you otherwise wouldn't be able to. This could potentially be achieved by modifying ES6 Module Loader to have a comparable feature, though this might encourage people writing new code rely on a feature that we should no longer encourage, given that it's also incompatible with native ES6 module support (again due to caching).
Any new APIs we begin to offer need to either be compatible with the existing APIs, so that it's possible to proxy methods from the old API to the new API, or otherwise should only require trivial changes to be made by developers when transitioning their code.
Services can be requested from the registry like this:
var registry = require('br/registry');
var logService = registry.require('service!log-service');
Here, we are using a generic registry rather than a service-registry, so that we can continue scanning for easy to recognize identifiers like service!log-service
on the server, and so that we can also limit ourselves to scanning for require()
statements in CommonJs code, which we already have to do to support CommonJS.
Notice also how registry.require()
is a synchronous operation, even though this proposal supports asynchronous service provision. This is possible because it's left to the application to ensure that all of the services that the app needs are loaded before the relevant require()
statements are invoked — this includes all transitive dependencies of the app too.
An application can pre-fetch all of its dependent services like this:
registry.fetchAll().then(function() {
// start app here...
});
or can fetch a limited sub-set of the required services using code like this:
registry.fetch(['service1', 'service2']).then(function() {
// start app here...
});
In this way, apps could choose to load only the services needed for the app container at app start-up, and then might load other required services on a per-component basis, so that each component starts up as soon as the services it depends on are available.
For backwards compatibility we will need to continue supporting aliases that point at service constructors, but we will also start supporting services provided using promisified service-factories. To begin with, here's an example of a MyService
service provided as a constructor, but which happens to depend on the logical services service!service1
and service!service2
:
var registry = require('br/registry');
var service1, service2;
function MyService() {
service1 = registry.require('service!service1');
service2 = registry.require('service!service2');
}
MyService.dependentServices = ['service!service1', 'service!service2'];
module.exports = MyService;
Here, we're relying on the registry to automatically construct and promisify MyService
, using fetch()
to ensure that the services specified specified within MyService.dependentServices
have first been resolved.
Developers needing to specify the constructor arguments of such a service would need to write an additional JavaScript module like this:
var registry = require('br/registry');
var MyService = require('./MyService');
module.exports = registry.fetch(MyService.dependentServices).then(function() {
return Promise.resolve(new MyService('some-arg'));
});
and point the service alias to this module instead of MyService
.
On the other hand, asynchronously provisioned services can not be delivered using service constructors, and must instead be delivered as promisified service factories, for example:
var registry = require('br/registry');
function UserService() {
}
module.exports = fetch('user-data').then(function(userData) {
return new UserService(userData);
});
Services that are both provisioned asynchronously and that depend on other services should make use of Promise.all()
to wait until both their input dependencies have been fetched and their constructor arguments are available before constructing the service.
Existing service-modules currently provide service-constructors rather than promisified service-instances, and so a service adapter could be provided to the registry so that it does the right thing. Given that we are using a general registry rather than a service registry, we'll need adaptors anyway to ensure something sensible happens for each registry adaptation (currently service!
and alias!
).
The service
adaptor might look like this:
var registry = require('br/registry');
var aliasRegistry = require('../AliasRegistry');
function ServiceAdaptor() {
this.registry = {};
}
ServiceAdaptor.prototype.require = function(serviceName) {
if(!aliasRegistry.isAlias(serviceName)) throw new ReferenceError("Unkown alias: '" + serviceName + "'");
var promise;
if(serviceName in this.registry) {
promise = Promise.resolve(this.registry[serviceName]);
}
else {
var resolvedAlias = aliasRegistry.getClass(serviceName);
if(resolvedAlias instanceof Promise) {
promise = resolvedAlias;
}
else {
promise = registry.fetch(resolvedAlias.dependentServices || []).then(function() {
this.registry[serviceName] = new resolvedAlias();
return Promise.resolve(this.registry[serviceName]);
}.bind(this));
}
}
return promise;
};
registry.setAdaptor('service', new ServiceAdaptor());
By invoking setAdaptor()
a second time the original adaptor could be replaced, which would allow CaplinTrader to provide a more complex adaptor that could also handle services that implement DelayedReadinessService
.
We will need to continue offering something akin to the ServiceRegistry.registerService()
method so that people can register services on the client. This could be done by adding the following methods to ServiceAdaptor
:
ServiceAdaptor.prototype.registerService = function(serviceName, service) {
this.registry[serviceName] = service;
};
ServiceAdaptor.prototype.clearServices = function() {
this.registry = {};
};
which would then allow people to write code like this in their tests:
registry.adaptor('service').registerService('log-service', new ConsoleLogService());
Although it will always be useful to inject services for tests that want a different service to what is used for the remainder of the test suite (which can be configured within aliases.xml
), existing applications should begin to replace uses of registerService()
within the app initialization code with service factories.
For example, an app that has this in it's initialization code:
require('br/registry').adaptor('service').registerService('log-service', new InMemoryLogService(512));
should instead create a module that like this:
module.exports = Promise.resolve(new InMemoryLogService(512));
and point the log-service
alias at this module.
Both BRJS and CaplinTrader have implementations of ServiceRegistry
(i.e. br/ServiceRegistry
and caplin/core/ServiceRegistry
) that would need to be updated to proxy through to the new br/registry
module. I've looked at the code in both classes and can't see any obvious reason why this couldn't be done.