Skip to content

Commit

Permalink
SFX-99: Add Core module with registration mechanism (#5)
Browse files Browse the repository at this point in the history
Core is the backbone of the entire SF-X system. It manages plugins and
allows them to communicate with each other. The core package defines
interfaces for plugins.

Exported from the `@sfx/core` package are the `Core` class and the
`Plugin`, `PluginMetadata` and `PluginRegistry` interfaces.

A registration mechanism is currently implemented. Unregistration will
be added separately. The interface and semantics of the process are
documented in the class and interface.
  • Loading branch information
jmbtutor authored Jul 5, 2019
1 parent 7fa8de2 commit 367de56
Show file tree
Hide file tree
Showing 21 changed files with 783 additions and 1 deletion.
2 changes: 1 addition & 1 deletion .mocharc.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@
"source-map-support/register"
],
"recursive": true,
"spec": ["test/setup.ts", "test/unit/common/*.test.ts", "test/unit/node/*.test.ts"]
"spec": ["test/setup.ts", "test/unit/common/**/*.test.ts", "test/unit/node/**/*.test.ts"]
}
11 changes: 11 additions & 0 deletions packages/@sfx/core/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Changelog
All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
### Added
- SFX-99: Added the `Core` module and related plugin interfaces.
- The Core module manages the registration and unregistration of plugins.
- The package also exports the `Plugin`, `PluginMetadata`, and `PluginRegistry` interfaces.
27 changes: 27 additions & 0 deletions packages/@sfx/core/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# SF-X Core

This package contains the SF-X Core class and related plugin interfaces.

## Usage

To use Core, simply instantiate it:

```js
const core = new Core();
```

## Registering a plugin

To register one or more plugins with Core, instantiate the plugins, then
pass them to `Core.register()`:

```js
const core = new Core();
const pluginA = new PluginA();
const pluginB = new PluginB();

core.register([pluginA, pluginB]);
```

A plugin may take configuration options in its constructor. Refer to the
plugin's documentation for details.
1 change: 1 addition & 0 deletions packages/@sfx/core/karma.conf.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('../../../karma.conf.js');
24 changes: 24 additions & 0 deletions packages/@sfx/core/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "@sfx/core",
"version": "0.0.0",
"description": "SF-X Core",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"esnext": "esnext/index.js",
"scripts": {
"build": "../../../scripts/build.sh",
"dev": "nodemon --config ../../../nodemon.json --exec npm run build",
"test": "nyc mocha",
"tdd": "nodemon --config ../../../nodemon.test.json --exec npm test",
"test:browser": "karma start karma.conf.js",
"tdd:browser": "karma start karma.conf.js --no-single-run"
},
"repository": {
"type": "git",
"url": "https://github.com/groupby/sfx-logic.git",
"directory": "packages/@sfx/core"
},
"author": "",
"license": "MIT",
"sideEffects": false
}
60 changes: 60 additions & 0 deletions packages/@sfx/core/src/core.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Plugin, PluginRegistry } from './plugin';
import {
calculateMissingDependencies,
initPlugins,
readyPlugins,
registerPlugins,
} from './utils/core';

/**
* The core of the SF-X plugin system. This entity is responsible for
* managing plugins and provides a mechanism for plugins to communicate
* with each other.
*/
export default class Core {
/**
* The plugin registry. This object is a dictionary containing plugin
* names as keys and their corresponding exposed values as values.
*
* The preferred way to access plugins in the registry is through
* another plugin. Accessing the registry through Core outside of a
* plugin is discouraged.
*/
registry: PluginRegistry = Object.create(null);

/**
* Register one or more plugins with Core.
*
* Plugins given here are registered as a batch: the next lifecycle
* event does not occur until all plugins in this batch have
* experienced the current lifecycle event.
*
* The registration lifecycle is as follows:
*
* 1. **Registration:** The registry is populated with each plugin's
* name and the value that it wants to expose. Plugins cannot
* assume that other plugins have been registered.
* 2. **Initialization:** Plugins perform setup tasks. Plugins are
* aware of the existence of other plugins, but should not make use
* of their functionality as they may not yet be initialized.
* 3. **Ready:** Registration is complete. All plugins have been
* initialized, and plugins may make use of other plugins.
*
* This function ensures that all plugin dependencies are available
* before proceeding to register the plugins. Circular dependencies
* are supported. If dependencies are missing, an error will be
* thrown.
*
* @param plugins An array of plugin instances to register.
*/
register(plugins: Plugin[]) {
const missingDependencies = calculateMissingDependencies(plugins, this.registry);
if (missingDependencies.length) {
throw new Error('Missing dependencies: ' + missingDependencies.join(', '));
}

registerPlugins(plugins, this.registry);
initPlugins(plugins);
readyPlugins(plugins);
}
}
2 changes: 2 additions & 0 deletions packages/@sfx/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as Core } from './core';
export { Plugin, PluginMetadata, PluginRegistry } from './plugin';
83 changes: 83 additions & 0 deletions packages/@sfx/core/src/plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/**
* A plugin for use with the SF-X Core. Plugins supply functionality to
* the otherwise function-less Core. Plugins may depend on other
* plugins.
*
* A plugin must conform to this interface to be registered with Core,
* but it may define other methods and properties for internal use.
*/
export interface Plugin {
/**
* Plugin metadata. This object provides Core with the information
* necessary to register the plugin.
*
* Plugin authors may consider using a getter to implement this
* property.
*/
metadata: PluginMetadata;

/**
* The callback for the registration phase of the lifecycle. The
* plugin will receive a reference to the plugin registry (the object
* through which other plugins can be accessed) here, and the function
* is expected to return a value that will be exposed through the same
* registry. It is recommended to store a reference to both values in
* the plugin for later retrieval.
*
* The plugin cannot assume that other plugins are registered in this
* function.
*
* @param plugins The plugin registry containing all other plugins.
* @returns The value to expose in the registry.
*/
register: (plugins: PluginRegistry) => any;

/**
* The callback for the initialization phase of the lifecycle. The
* plugin should do as much setup as it can in this function.
*
* In this function, the plugin can assume that other plugins have
* been registered and are accessible through the plugin registry
* object given in the `register` function, but it cannot assume that
* the other plugins have been initialized. In other words, the plugin
* will know which plugins are available, but it cannot use them.
*/
init?: () => void;

/**
* The callback for the ready phase of the lifecycle.
*
* In this function, the plugin can assume that other plugins have
* been initialized and may safely use other plugins.
*/
ready?: () => void;
}

/**
* Plugin metadata. Core will use the properties specified in this
* interface during the registration process.
*/
export interface PluginMetadata {
/**
* The name of the plugin. This name will be used as the key in the
* registry, so this name should be unique. By convention, this name
* should be a valid JavaScript identifier and be written in
* snake_case.
*/
name: string;

/**
* The names of all the plugins that this plugin has a hard dependency
* on (i.e. these plugins must be present for this plugin to
* function).
*/
depends: string[];
}

/**
* The type of the plugin registry. Each key/value pair corresponds to
* the name of a plugin and its exposed value.
*/
export interface PluginRegistry {
[key: string]: any;
}
74 changes: 74 additions & 0 deletions packages/@sfx/core/src/utils/core.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { Plugin, PluginRegistry } from '../plugin';

/**
* Calculates the missing dependencies of the given plugins. The given
* plugins and the plugins in the registry are eligible to satisfy
* dependencies.
*
* @param plugins The plugins whose dependencies should be checked.
* @param registry The plugin registry containing all registered plugins.
* @returns An array of names of missing plugins.
*/
export function calculateMissingDependencies(plugins: Plugin[], registry: PluginRegistry): string[] {
const available = [
...Object.keys(registry),
...plugins.map(({ metadata: { name }}) => name),
];
const required = plugins.reduce((memo, plugin) => {
return [...memo, ...plugin.metadata.depends];
}, []);
const availableSet = new Set(available);
const requiredSet = new Set(required);
const difference = new Set(Array.from(requiredSet).filter((p) => !availableSet.has(p)));

return Array.from(difference.values()).sort();
}

/**
* Calls the `register` function of each plugin. The values returned by
* each plugin are added to the given registry.
*
* @param plugins The plugins to register.
* @param registry The registry into which to add the plugins.
* @returns An object containing the keys and values of the new items
* added to the registry.
*/
export function registerPlugins(plugins: Plugin[], registry: PluginRegistry): PluginRegistry {
const newlyRegistered = Object.create(null);

plugins.forEach((plugin) => {
const exposedValue = plugin.register(Object.create(registry));
const { name } = plugin.metadata;

newlyRegistered[name] = exposedValue;
});

Object.assign(registry, newlyRegistered);
return newlyRegistered;
}

/**
* Calls the optional `init` function of each plugin.
*
* @param plugins The plugins to initialize.
*/
export function initPlugins(plugins: Plugin[]) {
plugins.forEach((plugin) => {
if (typeof plugin.init === 'function') {
plugin.init();
}
});
}

/**
* Calls the optional `ready` function of each plugin.
*
* @param plugins The plugins to ready.
*/
export function readyPlugins(plugins: Plugin[]) {
plugins.forEach((plugin) => {
if (typeof plugin.ready === 'function') {
plugin.ready();
}
});
}
1 change: 1 addition & 0 deletions packages/@sfx/core/test/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import '../../../../test/setup';
Empty file.
83 changes: 83 additions & 0 deletions packages/@sfx/core/test/unit/common/core.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { expect, sinon, stub } from '../../utils';
import Core from '../../../src/core';
import * as CoreUtils from '../../../src/utils/core';

describe('Core', () => {
let core: Core;

beforeEach(() => {
core = new Core();
});

describe('constructor()', () => {
it('should create an empty null-prototype plugins registry object', () => {
expect(core.registry).to.be.empty;
expect(Object.getPrototypeOf(core.registry)).to.be.null;
});
});

describe('register()', () => {
let calculateMissingDependencies;
let registerPlugins;
let initPlugins;
let readyPlugins;

beforeEach(() => {
calculateMissingDependencies = stub(CoreUtils, 'calculateMissingDependencies');
registerPlugins = stub(CoreUtils, 'registerPlugins');
initPlugins = stub(CoreUtils, 'initPlugins');
readyPlugins = stub(CoreUtils, 'readyPlugins');
});

it('should throw if dependencies are not met', () => {
const plugins: any = [
{
metadata: {
name: 'a',
depends: ['x'],
},
},
{
metadata: {
name: 'b',
depends: ['a'],
},
},
];
calculateMissingDependencies.returns(['x']);
Object.assign(core.registry, { m: 'mm' });

expect(() => core.register(plugins)).to.throw();
expect(calculateMissingDependencies).to.be.calledWith(plugins, sinon.match(core.registry));
});

it('should call the lifecycle events in order on the plugins', () => {
const plugins: any = [
{
metadata: {
name: 'a',
depends: [],
},
},
{
metadata: {
name: 'b',
depends: [],
},
},
];
calculateMissingDependencies.returns([]);

core.register(plugins);

expect(registerPlugins).to.be.calledWith(plugins, sinon.match(core.registry));
expect(initPlugins).to.be.calledWith(plugins);
expect(readyPlugins).to.be.calledWith(plugins);
sinon.assert.callOrder(
registerPlugins,
initPlugins,
readyPlugins
);
});
});
});
Loading

0 comments on commit 367de56

Please sign in to comment.