Skip to content

New Registry Proposal

Dominic Chambers edited this page May 1, 2015 · 2 revisions

Introduction

There are a number of deficiencies with the current service registry (as of BRJS v0.15.4):

  1. It doesn't allow services to be provided asynchronously.
  2. 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.
  3. require() is used to invoke ServiceRegistry.get() behind the scenes, which may make it incompatible with ES2016 modules (where modules are permanently cached), assuming ES2016 doesn't end up inheriting the System.set() method that the polyfills have.

Declarative Vs Programmatic IoC

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:

  1. 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.
  2. 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.

ES6 + NPM Compatibility

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

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.

ES6 NPM Support

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.

ES6 BRJS Support

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).

Backwards Compatibility

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.

Registry Proposal

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.

Service Provision

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.

Registry Adaptors

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.

Service Injection

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());

Reducing Usage Of Service Injection

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.

ServiceRegistry Compatibility

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.

Clone this wiki locally