paint-brush
Let's Explore ARK Core: Kernel & Services [Part 3]by@ark.io
117 reads

Let's Explore ARK Core: Kernel & Services [Part 3]

by ARK.ioApril 26th, 2020
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

This is Part 3 of 6 in the Let’s Explore ARK Core series which documents the development of the next major release of Core. We'll learn what services inside of Core are, why they were introduced and what their job is to keep Core running. A service is a self-contained feature that only exposes a small chunk of the total business logic that makes up Core. Core 2.0 had one major issue that was the result of the late container and plugin system introduction, it was so heavily fragmented that required plugins for functionality like loggers existed.

Company Mentioned

Mention Thumbnail
featured image - Let's Explore ARK Core: Kernel & Services [Part 3]
ARK.io HackerNoon profile picture

This is Part 3 of 6 in the Let’s Explore ARK Core series which documents the development of the next major release of ARK Core, alongside some tips & tricks on how to get started with contributing and building your next idea today.

Introduction

In the Part 1 and Part 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.

ARK Core v3.0 Github Repository

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

I Want To Help With Development

That's great news! If you want to help out, our GitHub repositories are wide open, but that is not all, we also have special Monthly Development GitHub Bounties on-going where you can earn money for every valid and merged Pull-Request. 

To learn more about the program please read our Bounty Program Guidelines blog post and to learn more about ARK Core and blockchain visit our Learning Hub.