paint-brush
Let's Explore ARK Core v3: Extensibility [Part 4]by@ark.io
176 reads

Let's Explore ARK Core v3: Extensibility [Part 4]

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

Too Long; Didn't Read

Let's Explore ARK Core v3: Extensibility [Part 4] is part of the Let’s Explore ArK Core series which documents the development of the next major release of ARK. We'll take a look at the arguably highest impact changes for package developers which make several improvements to the extensibility and testability of packages for Core. In part 3 we explained what they do and how they are executed but the basic gist of them is to be isolated entities with a single responsibility, ensure that a plugin is properly registered, booted and disposed.

Company Mentioned

Mention Thumbnail
featured image - Let's Explore ARK Core v3: Extensibility [Part 4]
ARK.io HackerNoon profile picture
This is Part 4 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 previous part, we have laid the foundation to understand how the new Core infrastructure looks and getting an understanding of how it works. Today we’ll take a look at the arguably highest impact changes for package developers which make several improvements to the extensibility and testability of packages for Core.

No special treatment for plugins

In Core 2.0 plugins were treated as a type of special entity inside the container. This is no longer the case in Core 3.0, everything that is bound to the container is treated the same and is just another binding.

Plugins now expose themselves to Core through “service providers”. In part 3 we explained what they do and how they are executed but the basic gist of them is to be isolated entities with a single responsibility, provide information about a plugin and ensure that it is properly registered, booted and disposed to avoid unwanted side-effects like ghost processes or hanging database connections after Core has already been stopped.

The Basics

Most of the following has already been covered in Part 2 but lets quickly refresh our minds about service providers as they are at the heart of it all.

Service Providers contain 3 methods that control how a plugin interacts with Core while it starts. Those methods are register, boot and dispose which take care of registering bindings with the container, booting any services that were registered and finally disposing of those services when the Core process terminated.

Conditional Plugins

A new feature that comes with the revamped plugin system of Core 3.0 is the ability to conditionally enable and disable plugins at runtime. Use-cases for this feature would be to enable or disable a plugin after a given height (similar to how milestones work), require some other plugins to be installed or require that a node has forked to perform some recovery tasks.

Let's have a look at an example service provider that implements the enableWhen and disableWhen methods and break it down into pieces.

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

export class ServiceProvider extends Providers.ServiceProvider {
    public async register(): Promise<void> {
        this.app.log.warning("[kernel-dummy-plugin] REGISTER");
    }

    public async boot(): Promise<void> {
        this.app.log.warning("[kernel-dummy-plugin] BOOT");
    }

    public async dispose(): Promise<void> {
        this.app.log.warning("[kernel-dummy-plugin] DISPOSE");
    }

    public async enableWhen(): Promise<boolean> {
        const { data } = this.app
            .resolve<StateService>("state")
            .getStore()
            .getLastBlock();

        return data.height % 2 === 0;
    }

    public async disableWhen(): Promise<boolean> {
        const { data } = this.app
            .resolve<StateService>("state")
            .getStore()
            .getLastBlock();

        return data.height % 3 === 0;
    }
}
  1. When the application bootstrappers call register we log a message.
  2. When the application bootstrappers call boot we log a message.
  3. When the application bootstrappers call dispose we log a message.
  4. The plugin will be enabled when the height of the last received block is divisible by 2.
  5. The plugin will be enabled when the height of the last received block is divisible by 3.

You might have noticed that there is a collision with values being divisible by both 2 and 3, like for example 6. This won’t cause any issues as the boot is only called if a plugin is not already booted, the same goes for the dispose method which is only called if a plugin is booted.

A common use-case for conditional enabling and disabling could be a plugin that fixes the state of a node if it forks and once new blocks are received it disables itself again.

Triggers & Hooks

Triggers are a new feature that will make it a lot easier for forks and bridgechains to modify how certain things behave without ever having to touch Core implementations, all your modifications will be done through your plugins that run in combination with the official stock Core available via 

@arkecosystem/core
.

In simple terms, triggers are just functional bindings in the container that can be easily rebound by plugins to alter how Core performs specific tasks. An example of this would be how a block is validated, let's have a look at what that would look like and how you could alter this behavior with your own plugin.

app
    .get<Services.Triggers.TriggerService>(Container.Identifiers.TriggerService)
    .bind("validateBlock", (data: Block) => {
        console.log('I will validate the block')
    })
    .before(() => {
        console.log('I run before a block is validated')
    })
    .error(() => {
        console.log('I run if block validation fails with an uncaught exception')
    })
    .after(() => {
        console.log('I run after a block is validated')
    });

const isValid: boolean = await app.get<Services.Triggers.TriggerService>(Container.Identifiers.TriggerService).call("validateBlock", someBlock);
  1. We bind a trigger called validateBlock that performs the validation of a block.
  2. We register a before hook that executes before the trigger we created via bind is executed.We register an error hook that executes when there is an uncaught exception in the trigger we created via bind is executed.
  3. We register an after hook that executes after the trigger we created via bind is executed.

This very basic example illustrates how we can take advantage of triggers and hooks. If you would want to use your own implementation of block validation, all you have to do is to register a trigger with the same name and it will be overwritten and used by Core.

Since triggers are bound to the container like anything else in Core they are easy to work with in tests as you can simply bind them to a dummy container whenever you need to, without having to spin up a full application instance.

Mixins

A big flaw of inheritance in the world of OOP is that it is limited to one level. This means you cannot extend more than a single class, which can get messy quickly as you end up with large classes that have multiple responsibilities.

Languages like PHP have something called Traits which works around this issue by allowing you to move methods into entities with a single responsibility. Those traits are easily reusable and could be things like HasComments or HasReviews which could be shared between entities like Movie, Project, Post without having to duplicate the implementation.

TypeScript has something called mixins. They act somewhat like traits, with the major difference being that under the hood they extend an object and return that. The result of that is that rather than simply adding some methods, a completely new object is created that contains the old and new methods.

Let's break down the following example to understand how mixins inside of Core work and the pros and cons that come with them.

class Block {}

function Timestamped<TBase extends Constructor>(Base: TBase) {
    return class extends Base {
        timestamp = new Date("2019-09-01");
    };
}

// Types
type AnyFunction<T = any> = (...input: any[]) => T;
type Mixin<T extends AnyFunction> = InstanceType<ReturnType<T>>;
type TTimestamped = Mixin<typeof Timestamped>;
type MixinBlock = TTimestamped & Block;

// Example
app.mixins.set("timestamped", Timestamped);

const block: MixinBlock = new (app.mixins.apply<MixinBlock>("timestamped", Block))();

expect(block.timestamp).toEqual(new Date("2019-09-01"));
  1. We define a few types that will help TypeScript understand what methods the object that was created by the mixin contain.
  2. We register the 
    Timestamped
     function as a mixin with the name of timestamped.
  3. We apply the 
    timestamped
     mixin to the 
    Block
     class to create a new variant of it that contains the 
    timestamp
     property with the current date, instantiate a new block instance to make assertions on it.

Mixins are a powerful tool to make use of composition over inheritance but they should be used with caution, like everything, or you’ll end up with the same issues that come with the excessive use of inheritance.

What's Next?

This concludes Part 4 of the ARK Core Adventures series. In the next part, we will delve into how ARK Core 3.0 has made several improvements to internal testing and what new testing tools have come out of those.

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.

Previously published at https://blog.ark.io/lets-explore-core-part-4-extensibility-c60522f1b700