-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
SFX-99: Add Core module with registration mechanism (#5)
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
Showing
21 changed files
with
783 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
module.exports = require('../../../karma.conf.js'); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
import '../../../../test/setup'; |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.