Introduction

In part 1 & 2 of this series, we gave a rough overview of how the Core infrastructure and application bootstrapping process was revamped. Today we’ll learn what services inside of Core are, why they were introduced and what their job is to keep Core running.

What is a service?

Before starting we should set the stage by defining what a service inside of Core is. In simple terms, a service is a self-contained feature that only exposes a small chunk of the total business logic that makes up Core. For example, a log service, cache service, database service, transaction service, etc. — all of those are important building blocks of Core and should be easy to modify, maintain and test.

Why were services introduced?

Services aren’t actually all new in Core 3.0 but rather a rework of how certain plugins operated in Core 2.0. Core 2.0 had one major issue that was the result of the late container and plugin system introduction, it was so heavily fragmented to the point where required plugins for functionality like loggers existed. You would always be required to include those plugins when bundling Core instead of them being part of a single package that you require in your own packages and have everything ready.

How were services introduced?

In Core 3.0 we introduced a new package called core-kernel which is the amalgamation of various packages that have previously existed in Core 2.0. Examples of those would be core-container , core-logger , core-event-emitter and more.

The core-kernel package is the heart of Core 3.0 with the goal of resolving a lot of pain points from previous versions, improve DX, reducing boilerplate for package developers and lastly to reduce the fragmentation that plagues Core 2.0.

Part of core-kernel is the services directory which comes with a variety of services out of the box to reduce the boilerplate needed for the development of new packages and reduce duplication in existing ones.

Actions

Cache

Config

Events

Filesystem

Log

Mixins

Queue

Schedule

Validation

We’ll go more in-depth into those services in a later part of this series.

Registering your own services

Registering your own services is as easy as can be and only takes a few lines of code due to the abstractions that come out of the box for package developers. Lets first have a look at the abstract ServiceProvider that comes with Core to reduce the necessary boilerplate for service registrations.

@injectable()

export abstract class ServiceProvider {

/**

* The application instance.

*/

@inject(Identifiers.Application)

protected readonly app: Kernel.Application; /**

* The application instance.

*/

private packageConfiguration: PackageConfiguration; /**

* The loaded manifest.

*/

private packageManifest: PackageManifest; /**

* Register the service provider.

*/

public abstract async register(): Promise<void>; /**

* Boot the service provider.

*/

public async boot(): Promise<void> {

//

} /**

* Dispose the service provider.

*/

public async dispose(): Promise<void> {

//

} /**

* Get the manifest of the service provider.

*/

public manifest(): PackageManifest {

return this.packageManifest;

} /**

* Set the manifest of the service provider.

*/

public setManifest(manifest: PackageManifest): void {

this.packageManifest = manifest;

} /**

* Get the name of the service provider.

*/

public name(): string | undefined {

if (this.packageManifest) {

return this.packageManifest.get("name");

} return undefined;

} /**

* Get the version of the service provider.

*

* @returns {string}

* @memberof ServiceProvider

*/

public version(): string | undefined {

if (this.packageManifest) {

return this.packageManifest.get("version");

} return undefined;

} /**

* Get the configuration of the service provider.

*/

public config(): PackageConfiguration {

return this.packageConfiguration;

} /**

* Set the configuration of the service provider.

*/

public setConfig(config: PackageConfiguration): void {

this.packageConfiguration = config;

} /**

* Get the configuration defaults of the service provider.

*/

public configDefaults(): JsonObject {

return {};

} /**

* Get the configuration schema of the service provider.

*/

public configSchema(): object {

return {};

} /**

* Get the dependencies of the service provider.

*/

public dependencies(): Kernel.PackageDependency[] {

return [];

} /**

* Enable the service provider when the given conditions are met.

*/

public async enableWhen(): Promise<boolean> {

return true;

} /**

* Disable the service provider when the given conditions are met.

*/

public async disableWhen(): Promise<boolean> {

return false;

} /**

* Determine if the package is required, which influences how bootstrapping errors are handled.

*/

public async required(): Promise<boolean> {

return false;

}

}

This is quite a bit of code so let's break it down into digestible parts.

The abstract register method is called by the bootstrapper classes that are responsible for registering services. This method should only register things, as its name indicates, and not start anything like HTTP servers. The boot method is called by the bootstrapper classes that are responsible for booting services. This should act based on what happened in the register method, i.e. start an HTTP server. The dispose method is called by the bootstrapper classes that are responsible for disposing of services. This should act based on what happened in the boot method, i.e. stop an HTTP server. The manifest method grants access to the package.json of a package to gather information like name or version. The setManifest is called by the bootstrapper classes that are responsible for registering services. The package.json file of a package will be automatically loaded, parsed and finally stored through this method. The name method returns the contents of the name property inside the package.json file of a package. The version method returns the contents of the version property inside the package.json file of a package. The config method grants access to the configuration of a package after it has been validated and normalized. The setConfig method is called by the bootstrapper classes that are responsible for registering services. The configuration will be validated, normalized and finally stored through this method. The configDefaults method is called by the bootstrapper classes that are responsible for registering services. The return value of this method will be merged with the user-supplied configuration to ensure all values are available. The configSchema method is called by the bootstrapper classes that are responsible for registering services. This method has to return a @hapi/joi schema that will be used to validate and normalize the configuration. The dependencies method is called by the bootstrapper classes that are responsible for registering services. The return value of this method has to be an array of objects that contain information like names and version constraints. The enableWhen / disableWhen methods are called by the bootstrapper classes when a block is applied and are responsible for (de)registering services. These methods have to return a boolean value that determines when to enable or disable a service. The required method is called by the bootstrapper classes that are responsible for registering services. This method has to return a boolean value that determines whether or not a service is required. Required plugins receive stricter error handling and any errors during registration or booting result in process termination.

That’s the functionality a service provider comes with out of the box but the only methods you’ll interact with in most cases are register, boot and dispose. Let us take a look at an example service provider to illustrate their use.

import { Providers } from "@arkecosystem/core-kernel";

import { Server } from "@hapi/hapi"; export class ServiceProvider extends Providers.ServiceProvider {

public async register(): Promise<void> {

this.app.bind<Server>("api").toConstantValue(new Server());

} public async boot(): Promise<void> {

await this.app.get<Server>("api").start();

} public async dispose(): Promise<void> {

await this.app.get<Server>("api").stop();

}

}

The register method binds a new instance of a hapi.js server to the container without starting it. The boot method retrieves the previously registered server from the container and calls the start method on it. The dispose method retrieves the previously registered server from the container and calls the stop method on it.

As you can see it’s pretty easy to register your own services without much hassle and everything is clearly named, let's end with listing some of the benefits of this new architecture compared to Core 2.0.

Benefits of new service provider lifecycle

Clear separation of responsibilities during application bootstrap.

Easy testing due to a clear separation of responsibilities.

Ability to enable and disable packages at runtime without a full teardown.

The by far biggest benefit for package developers is that it is now possible to alter or extend other packages due to how the application bootstrapping now works. An example of this would be a plugin that adds new routes or plugins to the core-api package before the server is started, all it would take is to resolve the hapi.js server from the container in the register method and call the usual methods provided by hapi.js on the resolved value.

This removes the need to spin up your own HTTP server if you just need 1–2 extra API endpoints. Being able to do modify other plugins before they are launched will provide developers with greater control and possibilities to modify how core behaves.

What’s next?

This concludes Part 3 of the Let’s Explore ARK Core blog series. In the next part, we will delve into how ARK Core 3.0 is more extensible than ever and how you can take advantage of this to reduce your time spent on developing packages.

Let’s Explore Core Series

If you have missed other Let’s Explore Core series post, you can read them by following the links below: