ServiceLayer
Service Layer
Section titled âService LayerâThis document contains details about the implementation of the Open Pioneer Trails runtimeâs service layer. It should aid developers to gain an understanding of the implementation in order to maintain or extend it.
The service layer provides a generic dependency injection mechanism in which services can be defined. Services may reference each other through references, forming a graph of services.
See Design for some background information.
Overview
Section titled âOverviewâThe service layerâs implementation lives in the package @open-pioneer/runtime, mostly in the services directory.
It is heavily inspired by the Spring framework.
The most important classes and modules are:
-
class ServiceLayerResponsible for maintaining the service lifecycle. This involves starting all required services, connecting them via references and destroying them again when the system shuts down.
-
function verifyDependenciesThe service layer will verify the servicesâ dependency graph before starting them by calling this function. It will visit all services to ensure that their dependencies can be fulfilled and that no cycles occur. On error, the method will output sensible error messages that can be easily understood by the developer.
In its current form, the function essentially ensures that the dependency graph is an acyclic graph.
The return value of
verifyDependenciesis used by the service layer. It contains the service index (mapping interface names, qualifiers etc. to services) and the verified dependency information (mapping services to other referenced services). The service layer will use the index to support dynamic service lookups (e.g. for the UI) and the dependency information during service construction to setup service references. -
class PackageRepr/ServiceReprRepresents a package / service at runtime. It must be noted that these classes are stateful: their state will reflect the changes made by the service layer. For example, a
ServiceReprâsinstanceattribute will hold the actual service instance once it has been created. As a consequence, they cannot be reused or shared between multiple application instances. -
function createPackagesParses metadata into instances of
PackageReprandServiceRepr. This transforms the well defined metadata format (input to the service layer) into the internal runtime representation. Package properties are also handled here: properties defined by the application are merged with the default values declared in the individual packages.
Metadata format
Section titled âMetadata formatâThe metadata format is currently maintained in the runtime package at @open-pioneer/runtime/metadata.
It describes packages, services and their associated metadata (such as references, provided interfaces, etc.).
In most instances, the metadata will be autogenerated by build tools.
However, it can also be provided manually (see tests).
The metadata format should be stable: it is part of the public interface. When the metadata format is altered, the build tooling will (most likely) have to be updated as well.
Service Layer Lifecycle
Section titled âService Layer Lifecycleâ-
Construction
The service layer is constructed from a set of
PackageRepr. It will immediately verify and index the services in the dependency graph. The construction is currently triggered from the custom element class when it is connected to the DOM.During its construction, the service layer will first verify the entire service graph (needed and unneeded services). It will then compute the set of actually required services. This set is currently made up of the services required by the UI (taking from package metadata) and the services required by the framework (constructor parameter). Only those services (and their transitive dependencies) will actually be started.
-
start
The
start()method is called to launch all required services.createServiceis recursive: a serviceâs dependencies must be created before the service itself can be created.The process to construct a service
sis as follows:- Has
salready been constructed? Returns.instanceand increment its reference count. - Is the service in any other state than
not-constructed? Throw an error (this is a sanity check, sinceverifyDependencyshould have caught the issue). - Call
createService(d)for every servicedrequired bysand store the instance reference. - Finally create the service
sby passing the required references and other parameters into its constructor. Note that this initializes the serviceâs reference count to1.
start()tracks the number of times a service is being used as a dependency. This is needed during shutdown. - Has
-
The application runs. Services may call each otherâs methods.
-
destroy
The
destroy()is being called on the service layer. This currently happens when the custom component is removed from the DOM.Destruction reverses the process in
start().destroyService(s)is called for every service:- Is the service already destroyed? Do nothing and return.
- Remove a reference from
s. It is no longer used and can be safely destroyed if the count reaches zero. - Call
destroyService(d)for every dependency ofs. It is important to destroy the dependencies after the service has been destroyed, because the serviceâs destructor may still reference its dependencies.
Additional features
Section titled âAdditional featuresâDisambiguate reference using a qualifier
Section titled âDisambiguate reference using a qualifierâThe service layer implements 1-to-1 or 1-to-n dependencies. This also means that an interface can be implemented by multiple services at the same time.
1-to-1 dependencies are the common case: a service references another single service that implements an interface. The service layer throws an error when the reference cannot be resolved to an exact service. The error conditions are:
- The interface is not implemented at all
- The interface is implemented multiple times but the reference is not qualified
A âqualifierâ (a simple string tag in its current form) can be used to disambiguate the reference.
Referencing all services implementing an interface
Section titled âReferencing all services implementing an interfaceâServices can reference all services implementing a certain interface. This feature will most likely be used to implement extension points where a service may customize the implementation of another service.
When 1-to-n references are resolved, all services implementing the requested interface are gathered into an array which is then injected into the serviceâs constructor instead of the usual, single instance.
Verification (e.g. cycle detection) has been extended to support this feature. Note that it is not an error if there are zero implementations of an interface: the array will simply be empty.
Runtime lookups
Section titled âRuntime lookupsâThe service layer provides an API to retrieve interface implementations at runtime (getService and getServices).
This is a required building block for the integration with react components: when a react component calls useService(...),
a dynamic lookup will be triggered (the result can be cached).
An error will be thrown if the dependency on that service has not been declared in the packageâs metadata (ui.references).
Limitations
Section titled âLimitationsâCycles are forbidden
Section titled âCycles are forbiddenâA service cannot depend on itself (directly or indirectly). Such a thing would be impossible to implement (reasonably) because a dependency should be fully initialized before it is being used.
We currently verify that there are no cycles through static analysis.
This should remain the default case: most references to other services should be static.
Tooling such as the verifyDependencies function can then provide useful analysis to prevent (or diagnose) common errors.
The restriction can still be relaxed when a service depends on itself statically but is only dynamically used once fully constructed; in other words, if the cycle is âresolvedâ once the first service method is actually being called.
The way this could be implemented is by declaring a dependency as lazy. A reference would still be injected into the serviceâs constructor, but it will only be a proxy object that must not be used in a cycle. Such a reference cannot be verified statically and should therefore only be used at carefully chosen locations to break cycles.
Example:
class MyService { constructor({ references }) { const actuallyMyself = references.myself; // not a MyService, but a Lazy<MyService> this.actuallyMyself = actuallyMyself;
// Would throw because myself has not been fully constructed yet! actuallyMyself.get().foo(); }
bar() { // Works because by now the service has been constructed. // This requires runtime checks inside get(). this.actuallyMyself.get().foo(); }
foo() { console.log("Hello World"); }}