This is part 4 of 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 {
this.app.log.warning("[kernel-dummy-plugin] REGISTER");
} public async boot(): Promise {
this.app.log.warning("[kernel-dummy-plugin] BOOT");
} public async dispose(): Promise {
this.app.log.warning("[kernel-dummy-plugin] DISPOSE");
} public async enableWhen(): Promise {
const { data } = this.app
.resolve("state")
.getStore()
.getLastBlock();return data.height % 2 === 0;
} public async disableWhen(): Promise {
const { data } = this.app
.resolve("state")
.getStore()
.getLastBlock();return data.height % 3 === 0;
}
}
- When the application bootstrappers call register we log a message.
- When the application bootstrappers call boot we log a message.
- When the application bootstrappers call dispose we log a message.
- The plugin will be enabled when the height of the last received block is divisible by 2.
- The plugin will be enabled when the height of the last received block is divisible by 3.
You might’ve 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(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(Container.Identifiers.TriggerService).call("validateBlock", someBlock);
- We bind a trigger called validateBlock that performs the validation of a block.
- 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.
- 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(Base: TBase) {
return class extends Base {
timestamp = new Date("2019-09-01");
};
}// Types
type AnyFunction = (...input: any[]) => T;
type Mixin = InstanceType;type TTimestamped = Mixin;
type MixinBlock = TTimestamped & Block;// Example
app.mixins.set("timestamped", Timestamped);
const block: MixinBlock = new (app.mixins.apply("timestamped", Block))();
expect(block.timestamp).toEqual(new Date("2019-09-01"));
- We define a few types that will help TypeScript understand what methods the object that was created by the mixin contain.
- We register the
Timestamped
function as a mixin with the name oftimestamped.
- We apply the
timestamped
mixin to theBlock
class to create a new variant of it that contains thetimestamp
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.
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:
- Part 1: Infrastructure
- Part 2: Bootstrap & Events
- Part 3: Kernel & Services
- Part 4: Extensibility*(you are currently reading it)*
- Part 5: Maintainability & Testability
- Part 6: Tooling