mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
Introduce @kbn/system-loader (#17595)
* Introduce @kbn/plugin-system * Throw if plugin exposes a promise from 'start' * TS updates * rename * Better error if missing plugin * Api to add multiple specs at the same time * isPromise prettier-ified * Rename 'plugin' to 'system' * Metadata + some cleanups * Make it possible to type system metadata * Throw if stop is async * Add tests for System class
This commit is contained in:
parent
842ed488c5
commit
d9f34f704e
15 changed files with 938 additions and 0 deletions
16
packages/kbn-system-loader/package.json
Normal file
16
packages/kbn-system-loader/package.json
Normal file
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"name": "@kbn/system-loader",
|
||||
"version": "1.0.0",
|
||||
"license": "Apache-2.0",
|
||||
"private": true,
|
||||
"main": "target/index.js",
|
||||
"typings": "target/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"kbn:bootstrap": "yarn build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^22.2.2",
|
||||
"typescript": "^2.8.1"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`throws if start returns a promise 1`] = `"A promise was returned when starting [foo], but systems must start synchronously and return either return undefined or the contract they expose to other systems."`;
|
||||
|
||||
exports[`throws if stop returns a promise 1`] = `"A promise was returned when stopping [foo], but systems must stop synchronously."`;
|
|
@ -0,0 +1,5 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`throws if adding that has the same name as a system that's already added 1`] = `"a system named [foo] has already been added"`;
|
||||
|
||||
exports[`throws if starting a system that depends on a system that's not present 1`] = `"System [foo] depends on [does-not-exist], which is not present"`;
|
|
@ -0,0 +1,3 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`throws if ordering does not succeed 1`] = `"Topological ordering did not complete, these edges could not be ordered: [[\\"a\\",[\\"b\\"]],[\\"b\\",[\\"c\\"]],[\\"c\\",[\\"a\\"]],[\\"f\\",[\\"g\\"]]]"`;
|
3
packages/kbn-system-loader/src/index.ts
Normal file
3
packages/kbn-system-loader/src/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export { SystemLoader } from './system_loader';
|
||||
export { System } from './system';
|
||||
export { KibanaSystem } from './system_types';
|
24
packages/kbn-system-loader/src/sorted_systems.ts
Normal file
24
packages/kbn-system-loader/src/sorted_systems.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
import { System } from './system';
|
||||
import { SystemName } from './system_types';
|
||||
import { topologicalSort } from './topological_sort';
|
||||
|
||||
// We need this helper for the types to be correct when creating Map
|
||||
// (otherwise it assumes an array of A|B instead of a tuple [A,B])
|
||||
const toTuple = <A, B>(a: A, b: B): [A, B] => [a, b];
|
||||
|
||||
function toSortable(systems: Map<SystemName, System<any, any, any, any>>) {
|
||||
const dependenciesBySystem = [...systems.entries()].map(([name, system]) =>
|
||||
toTuple(name, system.dependencies || [])
|
||||
);
|
||||
return new Map(dependenciesBySystem);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts systems in topological order based on dependencies
|
||||
*/
|
||||
export function getSortedSystemNames(
|
||||
systems: Map<SystemName, System<any, any, any, any>>
|
||||
) {
|
||||
const sorted = topologicalSort(toSortable(systems));
|
||||
return [...sorted];
|
||||
}
|
74
packages/kbn-system-loader/src/system.test.ts
Normal file
74
packages/kbn-system-loader/src/system.test.ts
Normal file
|
@ -0,0 +1,74 @@
|
|||
import { System } from './system';
|
||||
import { KibanaSystem } from './system_types';
|
||||
|
||||
test('can get exposed values after starting', () => {
|
||||
type CoreType = { bar: string };
|
||||
type DepsType = { quux: string };
|
||||
type ExposedType = {
|
||||
core: CoreType;
|
||||
deps: DepsType;
|
||||
};
|
||||
|
||||
class FooSystem extends KibanaSystem<CoreType, DepsType, ExposedType> {
|
||||
start() {
|
||||
return {
|
||||
core: this.kibana,
|
||||
deps: this.deps,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const system = new System('foo', {
|
||||
implementation: FooSystem,
|
||||
});
|
||||
|
||||
system.start(
|
||||
{
|
||||
bar: 'bar',
|
||||
},
|
||||
{
|
||||
quux: 'quux',
|
||||
}
|
||||
);
|
||||
|
||||
expect(system.getExposedValues()).toEqual({
|
||||
core: { bar: 'bar' },
|
||||
deps: { quux: 'quux' },
|
||||
});
|
||||
});
|
||||
|
||||
test('throws if start returns a promise', () => {
|
||||
class FooSystem extends KibanaSystem<any, any, any> {
|
||||
async start() {
|
||||
return 'foo';
|
||||
}
|
||||
}
|
||||
|
||||
const system = new System('foo', {
|
||||
implementation: FooSystem,
|
||||
});
|
||||
|
||||
expect(() => {
|
||||
system.start({}, {});
|
||||
}).toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
|
||||
test('throws if stop returns a promise', () => {
|
||||
class FooSystem extends KibanaSystem<any, any, any> {
|
||||
start() {}
|
||||
|
||||
async stop() {
|
||||
return 'stop';
|
||||
}
|
||||
}
|
||||
|
||||
const system = new System('foo', {
|
||||
implementation: FooSystem,
|
||||
});
|
||||
|
||||
system.start({}, {});
|
||||
|
||||
expect(() => {
|
||||
system.stop();
|
||||
}).toThrowErrorMatchingSnapshot();
|
||||
});
|
81
packages/kbn-system-loader/src/system.ts
Normal file
81
packages/kbn-system-loader/src/system.ts
Normal file
|
@ -0,0 +1,81 @@
|
|||
import {
|
||||
SystemsType,
|
||||
SystemName,
|
||||
SystemMetadata,
|
||||
KibanaSystemClassStatic,
|
||||
KibanaSystem,
|
||||
} from './system_types';
|
||||
|
||||
function isPromise(obj: any) {
|
||||
return (
|
||||
obj != null && typeof obj === 'object' && typeof obj.then === 'function'
|
||||
);
|
||||
}
|
||||
|
||||
export class System<C, M extends SystemMetadata, D extends SystemsType, E> {
|
||||
readonly name: SystemName;
|
||||
readonly dependencies: SystemName[];
|
||||
readonly metadata?: M;
|
||||
|
||||
private readonly _systemClass: KibanaSystemClassStatic<C, D, E>;
|
||||
private _systemInstance?: KibanaSystem<C, D, E>;
|
||||
private _exposedValues?: E;
|
||||
|
||||
constructor(
|
||||
name: SystemName,
|
||||
config: {
|
||||
metadata?: M;
|
||||
dependencies?: SystemName[];
|
||||
implementation: KibanaSystemClassStatic<C, D, E>;
|
||||
}
|
||||
) {
|
||||
this.name = name;
|
||||
this.dependencies = config.dependencies || [];
|
||||
this.metadata = config.metadata;
|
||||
this._systemClass = config.implementation;
|
||||
}
|
||||
|
||||
getExposedValues(): E {
|
||||
if (this._systemInstance === undefined) {
|
||||
throw new Error(
|
||||
'trying to get the exposed value of a system that is NOT running'
|
||||
);
|
||||
}
|
||||
|
||||
return this._exposedValues!;
|
||||
}
|
||||
|
||||
start(kibanaValues: C, dependenciesValues: D) {
|
||||
this._systemInstance = new this._systemClass(
|
||||
kibanaValues,
|
||||
dependenciesValues
|
||||
);
|
||||
const exposedValues = this._systemInstance.start();
|
||||
|
||||
if (isPromise(exposedValues)) {
|
||||
throw new Error(
|
||||
`A promise was returned when starting [${
|
||||
this.name
|
||||
}], but systems must start synchronously and return either return undefined or the contract they expose to other systems.`
|
||||
);
|
||||
}
|
||||
|
||||
this._exposedValues =
|
||||
exposedValues === undefined ? ({} as E) : exposedValues;
|
||||
}
|
||||
|
||||
stop() {
|
||||
const stoppedResponse = this._systemInstance && this._systemInstance.stop();
|
||||
|
||||
this._exposedValues = undefined;
|
||||
this._systemInstance = undefined;
|
||||
|
||||
if (isPromise(stoppedResponse)) {
|
||||
throw new Error(
|
||||
`A promise was returned when stopping [${
|
||||
this.name
|
||||
}], but systems must stop synchronously.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
474
packages/kbn-system-loader/src/system_loader.test.ts
Normal file
474
packages/kbn-system-loader/src/system_loader.test.ts
Normal file
|
@ -0,0 +1,474 @@
|
|||
import { System } from './system';
|
||||
import { KibanaSystem } from './system_types';
|
||||
import { SystemLoader, KibanaSystemApiFactory } from './system_loader';
|
||||
|
||||
// To make types simpler in the tests
|
||||
type CoreType = void;
|
||||
const createCoreValues = () => {};
|
||||
|
||||
test('starts system with core api', () => {
|
||||
expect.assertions(1);
|
||||
|
||||
type KibanaCoreApi = { fromCore: boolean; name: string };
|
||||
type Metadata = { configPath?: string };
|
||||
|
||||
class FooSystem extends KibanaSystem<KibanaCoreApi, {}> {
|
||||
start() {
|
||||
expect(this.kibana).toEqual({
|
||||
name: 'foo',
|
||||
fromCore: true,
|
||||
metadata: {
|
||||
configPath: 'config.path.foo',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const foo = new System('foo', {
|
||||
metadata: {
|
||||
configPath: 'config.path.foo',
|
||||
},
|
||||
implementation: FooSystem,
|
||||
});
|
||||
|
||||
const createSystemApi: KibanaSystemApiFactory<KibanaCoreApi, Metadata> = (
|
||||
name,
|
||||
metadata
|
||||
) => {
|
||||
return {
|
||||
name,
|
||||
metadata,
|
||||
fromCore: true,
|
||||
};
|
||||
};
|
||||
|
||||
const systems = new SystemLoader(createSystemApi);
|
||||
systems.addSystem(foo);
|
||||
|
||||
systems.startSystems();
|
||||
});
|
||||
|
||||
test('system can expose a value', () => {
|
||||
expect.assertions(1);
|
||||
|
||||
type Foo = {
|
||||
foo: {
|
||||
value: string;
|
||||
};
|
||||
};
|
||||
|
||||
class FooSystem extends KibanaSystem<CoreType, {}, Foo['foo']> {
|
||||
start() {
|
||||
return {
|
||||
value: 'my-value',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class BarSystem extends KibanaSystem<CoreType, Foo> {
|
||||
start() {
|
||||
expect(this.deps.foo).toEqual({ value: 'my-value' });
|
||||
}
|
||||
}
|
||||
|
||||
const foo = new System('foo', {
|
||||
implementation: FooSystem,
|
||||
});
|
||||
|
||||
const bar = new System('bar', {
|
||||
dependencies: ['foo'],
|
||||
implementation: BarSystem,
|
||||
});
|
||||
|
||||
const systems = new SystemLoader(createCoreValues);
|
||||
systems.addSystem(foo);
|
||||
systems.addSystem(bar);
|
||||
systems.startSystems();
|
||||
});
|
||||
|
||||
test('system can expose a function', () => {
|
||||
expect.assertions(2);
|
||||
|
||||
type Foo = {
|
||||
foo: {
|
||||
fn: (val: string) => string;
|
||||
};
|
||||
};
|
||||
|
||||
class FooSystem extends KibanaSystem<CoreType, {}, Foo['foo']> {
|
||||
start(): Foo['foo'] {
|
||||
return {
|
||||
fn: val => `test-${val}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class BarSystem extends KibanaSystem<CoreType, Foo> {
|
||||
start() {
|
||||
expect(this.deps.foo).toBeDefined();
|
||||
expect(this.deps.foo.fn('some-value')).toBe('test-some-value');
|
||||
}
|
||||
}
|
||||
|
||||
const foo = new System('foo', {
|
||||
implementation: FooSystem,
|
||||
});
|
||||
|
||||
const bar = new System('bar', {
|
||||
dependencies: ['foo'],
|
||||
implementation: BarSystem,
|
||||
});
|
||||
|
||||
const systems = new SystemLoader(createCoreValues);
|
||||
systems.addSystem(foo);
|
||||
systems.addSystem(bar);
|
||||
systems.startSystems();
|
||||
});
|
||||
|
||||
test('can expose value with same name across multiple systems', () => {
|
||||
expect.assertions(2);
|
||||
|
||||
type Foo = {
|
||||
foo: {
|
||||
value: string;
|
||||
};
|
||||
};
|
||||
|
||||
type Bar = {
|
||||
bar: {
|
||||
value: string;
|
||||
};
|
||||
};
|
||||
|
||||
class FooSystem extends KibanaSystem<CoreType, {}, Foo['foo']> {
|
||||
start(): Foo['foo'] {
|
||||
return {
|
||||
value: 'value-foo',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class BarSystem extends KibanaSystem<CoreType, {}, Bar['bar']> {
|
||||
start(): Bar['bar'] {
|
||||
return {
|
||||
value: 'value-bar',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class QuuxSystem extends KibanaSystem<CoreType, Foo & Bar> {
|
||||
start() {
|
||||
expect(this.deps.foo).toEqual({ value: 'value-foo' });
|
||||
expect(this.deps.bar).toEqual({ value: 'value-bar' });
|
||||
}
|
||||
}
|
||||
|
||||
const foo = new System('foo', {
|
||||
implementation: FooSystem,
|
||||
});
|
||||
|
||||
const bar = new System('bar', {
|
||||
implementation: BarSystem,
|
||||
});
|
||||
|
||||
const quux = new System('quux', {
|
||||
dependencies: ['foo', 'bar'],
|
||||
implementation: QuuxSystem,
|
||||
});
|
||||
|
||||
const systems = new SystemLoader(createCoreValues);
|
||||
systems.addSystem(foo);
|
||||
systems.addSystem(bar);
|
||||
systems.addSystem(quux);
|
||||
systems.startSystems();
|
||||
});
|
||||
|
||||
test('receives values from dependencies but not transitive dependencies', () => {
|
||||
expect.assertions(3);
|
||||
|
||||
type Grandchild = {
|
||||
grandchild: {
|
||||
value: string;
|
||||
};
|
||||
};
|
||||
|
||||
type Child = {
|
||||
child: {
|
||||
value: string;
|
||||
};
|
||||
};
|
||||
|
||||
class GrandchildSystem extends KibanaSystem<
|
||||
CoreType,
|
||||
{},
|
||||
Grandchild['grandchild']
|
||||
> {
|
||||
start() {
|
||||
return {
|
||||
value: 'grandchild',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class ChildSystem extends KibanaSystem<CoreType, Grandchild, Child['child']> {
|
||||
start() {
|
||||
expect(this.deps.grandchild).toEqual({ value: 'grandchild' });
|
||||
|
||||
return {
|
||||
value: 'child',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class ParentSystem extends KibanaSystem<CoreType, Grandchild & Child> {
|
||||
start() {
|
||||
expect(this.deps.child).toEqual({ value: 'child' });
|
||||
expect(this.deps.grandchild).toBeUndefined();
|
||||
}
|
||||
}
|
||||
|
||||
const grandchild = new System('grandchild', {
|
||||
implementation: GrandchildSystem,
|
||||
});
|
||||
|
||||
const child = new System('child', {
|
||||
dependencies: ['grandchild'],
|
||||
implementation: ChildSystem,
|
||||
});
|
||||
|
||||
const parent = new System('parent', {
|
||||
dependencies: ['child'],
|
||||
implementation: ParentSystem,
|
||||
});
|
||||
|
||||
const systems = new SystemLoader(createCoreValues);
|
||||
systems.addSystem(grandchild);
|
||||
systems.addSystem(child);
|
||||
systems.addSystem(parent);
|
||||
systems.startSystems();
|
||||
});
|
||||
|
||||
test('keeps reference on registered value', () => {
|
||||
expect.assertions(1);
|
||||
|
||||
type Child = {
|
||||
child: {
|
||||
value: {};
|
||||
};
|
||||
};
|
||||
|
||||
const myRef = {};
|
||||
|
||||
class ChildSystem extends KibanaSystem<CoreType, {}, Child['child']> {
|
||||
start() {
|
||||
return {
|
||||
value: myRef,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class ParentSystem extends KibanaSystem<CoreType, Child> {
|
||||
start() {
|
||||
expect(this.deps.child.value).toBe(myRef);
|
||||
}
|
||||
}
|
||||
|
||||
const child = new System('child', {
|
||||
implementation: ChildSystem,
|
||||
});
|
||||
|
||||
const parent = new System('parent', {
|
||||
dependencies: ['child'],
|
||||
implementation: ParentSystem,
|
||||
});
|
||||
|
||||
const systems = new SystemLoader(createCoreValues);
|
||||
systems.addSystem(child);
|
||||
systems.addSystem(parent);
|
||||
systems.startSystems();
|
||||
});
|
||||
|
||||
test('can register multiple values in single system', () => {
|
||||
expect.assertions(1);
|
||||
|
||||
type Child = {
|
||||
child: {
|
||||
value1: number;
|
||||
value2: number;
|
||||
};
|
||||
};
|
||||
|
||||
class ChildSystem extends KibanaSystem<CoreType, {}, Child['child']> {
|
||||
start() {
|
||||
return {
|
||||
value1: 1,
|
||||
value2: 2,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class ParentSystem extends KibanaSystem<CoreType, Child> {
|
||||
start() {
|
||||
expect(this.deps.child).toEqual({
|
||||
value1: 1,
|
||||
value2: 2,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const child = new System('child', {
|
||||
implementation: ChildSystem,
|
||||
});
|
||||
|
||||
const parent = new System('parent', {
|
||||
dependencies: ['child'],
|
||||
implementation: ParentSystem,
|
||||
});
|
||||
|
||||
const systems = new SystemLoader(createCoreValues);
|
||||
systems.addSystem(child);
|
||||
systems.addSystem(parent);
|
||||
systems.startSystems();
|
||||
});
|
||||
|
||||
test("throws if starting a system that depends on a system that's not present", () => {
|
||||
class FooSystem extends KibanaSystem<CoreType, {}> {
|
||||
start() {}
|
||||
}
|
||||
|
||||
const foo = new System('foo', {
|
||||
dependencies: ['does-not-exist'],
|
||||
implementation: FooSystem,
|
||||
});
|
||||
|
||||
const systems = new SystemLoader(createCoreValues);
|
||||
|
||||
systems.addSystem(foo);
|
||||
|
||||
expect(() => {
|
||||
systems.startSystems();
|
||||
}).toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
|
||||
test("throws if adding that has the same name as a system that's already added", () => {
|
||||
class FooSystem extends KibanaSystem<CoreType, {}> {
|
||||
start() {}
|
||||
}
|
||||
|
||||
const foo = new System('foo', {
|
||||
implementation: FooSystem,
|
||||
});
|
||||
|
||||
const systems = new SystemLoader(createCoreValues);
|
||||
|
||||
systems.addSystem(foo);
|
||||
expect(() => {
|
||||
systems.addSystem(foo);
|
||||
}).toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
|
||||
test('stops systems in reverse order of their starting order', () => {
|
||||
const events: string[] = [];
|
||||
|
||||
class FooSystem extends KibanaSystem<CoreType, {}> {
|
||||
start() {
|
||||
events.push('start foo');
|
||||
}
|
||||
stop() {
|
||||
events.push('stop foo');
|
||||
}
|
||||
}
|
||||
|
||||
class BarSystem extends KibanaSystem<CoreType, {}> {
|
||||
start() {
|
||||
events.push('start bar');
|
||||
}
|
||||
stop() {
|
||||
events.push('stop bar');
|
||||
}
|
||||
}
|
||||
|
||||
const foo = new System('foo', {
|
||||
implementation: FooSystem,
|
||||
});
|
||||
const bar = new System('bar', {
|
||||
implementation: BarSystem,
|
||||
});
|
||||
|
||||
const systems = new SystemLoader(createCoreValues);
|
||||
|
||||
systems.addSystem(foo);
|
||||
systems.addSystem(bar);
|
||||
|
||||
systems.startSystems();
|
||||
systems.stopSystems();
|
||||
|
||||
expect(events).toEqual(['start bar', 'start foo', 'stop foo', 'stop bar']);
|
||||
});
|
||||
|
||||
test('can add systems before adding its dependencies', () => {
|
||||
expect.assertions(1);
|
||||
|
||||
type Foo = {
|
||||
foo: string;
|
||||
};
|
||||
|
||||
class FooSystem extends KibanaSystem<CoreType, {}, Foo['foo']> {
|
||||
start() {
|
||||
return 'value';
|
||||
}
|
||||
}
|
||||
|
||||
class BarSystem extends KibanaSystem<CoreType, Foo> {
|
||||
start() {
|
||||
expect(this.deps.foo).toBe('value');
|
||||
}
|
||||
}
|
||||
|
||||
const foo = new System('foo', {
|
||||
implementation: FooSystem,
|
||||
});
|
||||
|
||||
const bar = new System('bar', {
|
||||
dependencies: ['foo'],
|
||||
implementation: BarSystem,
|
||||
});
|
||||
|
||||
const systems = new SystemLoader(createCoreValues);
|
||||
// `bar` depends on `foo`, but we add it first
|
||||
systems.addSystem(bar);
|
||||
systems.addSystem(foo);
|
||||
systems.startSystems();
|
||||
});
|
||||
|
||||
test('can add multiple system specs at the same time', () => {
|
||||
expect.assertions(1);
|
||||
|
||||
const spy = jest.fn();
|
||||
|
||||
class FooSystem extends KibanaSystem<CoreType, {}> {
|
||||
start() {
|
||||
spy();
|
||||
}
|
||||
}
|
||||
|
||||
class BarSystem extends KibanaSystem<CoreType, {}> {
|
||||
start() {
|
||||
spy();
|
||||
}
|
||||
}
|
||||
|
||||
const foo = new System('foo', {
|
||||
implementation: FooSystem,
|
||||
});
|
||||
|
||||
const bar = new System('bar', {
|
||||
implementation: BarSystem,
|
||||
});
|
||||
|
||||
const systems = new SystemLoader(createCoreValues);
|
||||
systems.addSystems([foo, bar]);
|
||||
systems.startSystems();
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(2);
|
||||
});
|
93
packages/kbn-system-loader/src/system_loader.ts
Normal file
93
packages/kbn-system-loader/src/system_loader.ts
Normal file
|
@ -0,0 +1,93 @@
|
|||
import { System } from './system';
|
||||
import { SystemName, SystemMetadata, SystemsType } from './system_types';
|
||||
import { getSortedSystemNames } from './sorted_systems';
|
||||
|
||||
export type KibanaSystemApiFactory<C, M> = (
|
||||
name: SystemName,
|
||||
metadata?: M
|
||||
) => C;
|
||||
|
||||
export class SystemLoader<C, M extends SystemMetadata> {
|
||||
private readonly _systems = new Map<SystemName, System<C, M, any, any>>();
|
||||
private _startedSystems: SystemName[] = [];
|
||||
|
||||
constructor(
|
||||
/**
|
||||
* Creates the Kibana system api for each system. It is called with
|
||||
* information about a system before it's started, and the return value will
|
||||
* be injected into the system at startup.
|
||||
*/
|
||||
private readonly _kibanaSystemApiFactory: KibanaSystemApiFactory<C, M>
|
||||
) {}
|
||||
|
||||
addSystems(systemSpecs: System<C, M, any, any>[]) {
|
||||
systemSpecs.forEach(systemSpec => {
|
||||
this.addSystem(systemSpec);
|
||||
});
|
||||
}
|
||||
|
||||
addSystem<D extends SystemsType, E = void>(system: System<C, M, D, E>) {
|
||||
if (this._systems.has(system.name)) {
|
||||
throw new Error(`a system named [${system.name}] has already been added`);
|
||||
}
|
||||
|
||||
this._systems.set(system.name, system);
|
||||
}
|
||||
|
||||
startSystems() {
|
||||
this._ensureAllSystemDependenciesCanBeResolved();
|
||||
|
||||
getSortedSystemNames(this._systems)
|
||||
.map(systemName => this._systems.get(systemName)!)
|
||||
.forEach(systemSpec => {
|
||||
this.startSystem(systemSpec);
|
||||
});
|
||||
}
|
||||
|
||||
private _ensureAllSystemDependenciesCanBeResolved() {
|
||||
for (const [systemName, system] of this._systems) {
|
||||
for (const systemDependency of system.dependencies) {
|
||||
if (!this._systems.has(systemDependency)) {
|
||||
throw new Error(
|
||||
`System [${systemName}] depends on [${systemDependency}], which is not present`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private startSystem<D extends SystemsType, E = void>(
|
||||
system: System<C, M, D, E>
|
||||
) {
|
||||
const dependenciesValues = {} as D;
|
||||
|
||||
for (const dependency of system.dependencies) {
|
||||
dependenciesValues[dependency] = this._systems
|
||||
.get(dependency)!
|
||||
.getExposedValues();
|
||||
}
|
||||
|
||||
const kibanaSystemApi = this._kibanaSystemApiFactory(
|
||||
system.name,
|
||||
system.metadata
|
||||
);
|
||||
|
||||
system.start(kibanaSystemApi, dependenciesValues);
|
||||
this._startedSystems.push(system.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop all systems in the reverse order of when they were started
|
||||
*/
|
||||
stopSystems() {
|
||||
this._startedSystems
|
||||
.map(systemName => this._systems.get(systemName)!)
|
||||
.reverse()
|
||||
.forEach(system => {
|
||||
system.stop();
|
||||
this._systems.delete(system.name);
|
||||
});
|
||||
|
||||
this._startedSystems = [];
|
||||
}
|
||||
}
|
32
packages/kbn-system-loader/src/system_types.ts
Normal file
32
packages/kbn-system-loader/src/system_types.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
export type SystemName = string;
|
||||
export type SystemMetadata = {
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
export type SystemsType = {
|
||||
[systemName: string]: any;
|
||||
};
|
||||
|
||||
export abstract class KibanaSystem<C, D extends SystemsType, E = void> {
|
||||
constructor(readonly kibana: C, readonly deps: D) {}
|
||||
|
||||
abstract start(): E;
|
||||
|
||||
stop() {
|
||||
// default implementation of stop does nothing
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the "static side" of the Kibana system class.
|
||||
*
|
||||
* When a class implements an interface, only the instance side of the class is
|
||||
* checked, so you can't include static methods there. Because of that we have
|
||||
* a separate interface for the static side, which we can use to specify that we
|
||||
* want a _class_ (not an instance) that matches this interface.
|
||||
*
|
||||
* See https://www.typescriptlang.org/docs/handbook/interfaces.html#difference-between-the-static-and-instance-sides-of-classes
|
||||
*/
|
||||
export interface KibanaSystemClassStatic<C, D extends SystemsType, E = void> {
|
||||
new (kibana: C, deps: D): KibanaSystem<C, D, E>;
|
||||
}
|
46
packages/kbn-system-loader/src/topological_sort.test.ts
Normal file
46
packages/kbn-system-loader/src/topological_sort.test.ts
Normal file
|
@ -0,0 +1,46 @@
|
|||
import { topologicalSort } from './topological_sort';
|
||||
|
||||
test('returns a topologically ordered sequence', () => {
|
||||
const nodes = new Map([
|
||||
['a', []],
|
||||
['b', ['a']],
|
||||
['c', ['a', 'b']],
|
||||
['d', ['a']],
|
||||
]);
|
||||
|
||||
let sorted = topologicalSort(nodes);
|
||||
|
||||
expect(sorted).toBeDefined();
|
||||
|
||||
expect([...sorted!]).toEqual(['a', 'd', 'b', 'c']);
|
||||
});
|
||||
|
||||
test('handles multiple "roots" with no deps', () => {
|
||||
const nodes = new Map([
|
||||
['a', []],
|
||||
['b', []],
|
||||
['c', ['a', 'b']],
|
||||
['d', ['a']],
|
||||
]);
|
||||
|
||||
let sorted = topologicalSort(nodes);
|
||||
|
||||
expect(sorted).toBeDefined();
|
||||
|
||||
expect([...sorted!]).toEqual(['b', 'a', 'd', 'c']);
|
||||
});
|
||||
|
||||
test('throws if ordering does not succeed', () => {
|
||||
const nodes = new Map([
|
||||
['a', ['b']],
|
||||
['b', ['c']],
|
||||
['c', ['a', 'd']], // cycles back to 'a'
|
||||
['d', []],
|
||||
['e', ['d']],
|
||||
['f', ['g']], // 'g' does not 'exist'
|
||||
]);
|
||||
|
||||
expect(() => {
|
||||
topologicalSort(nodes);
|
||||
}).toThrowErrorMatchingSnapshot();
|
||||
});
|
59
packages/kbn-system-loader/src/topological_sort.ts
Normal file
59
packages/kbn-system-loader/src/topological_sort.ts
Normal file
|
@ -0,0 +1,59 @@
|
|||
/**
|
||||
* A topological ordering is possible if and only if the graph has no directed
|
||||
* cycles, that is, if it is a directed acyclic graph (DAG). If the input cannot
|
||||
* be ordered an error is thrown.
|
||||
*
|
||||
* Uses Kahn's Algorithm to sort the graph.
|
||||
*
|
||||
* @param graph A directed acyclic graph with vertices as keys and outgoing
|
||||
* edges as values.
|
||||
*/
|
||||
export function topologicalSort<T>(graph: Map<T, T[]>) {
|
||||
const sorted = new Set<T>();
|
||||
|
||||
// if (graph.size === 0) {
|
||||
// return sorted;
|
||||
// }
|
||||
|
||||
// We clone the graph so we can remove handled nodes while we perform the
|
||||
// topological ordering. If the cloned graph is _not_ empty at the end, we
|
||||
// know we were not able to topologically order the graph.
|
||||
const clonedGraph = new Map(graph.entries());
|
||||
|
||||
// First, find a list of "start nodes" which have no outgoing edges. At least
|
||||
// one such node must exist in a non-empty acyclic graph.
|
||||
const nodesWithNoEdges = [...clonedGraph.keys()].filter(name => {
|
||||
const edges = clonedGraph.get(name) as T[];
|
||||
return edges.length === 0;
|
||||
});
|
||||
|
||||
while (nodesWithNoEdges.length > 0) {
|
||||
const processingNode = nodesWithNoEdges.pop() as T;
|
||||
|
||||
// We know this node has no edges, so we can remove it
|
||||
clonedGraph.delete(processingNode);
|
||||
|
||||
sorted.add(processingNode);
|
||||
|
||||
// Go through all nodes and remove all edges into `node`
|
||||
[...clonedGraph.keys()].forEach(node => {
|
||||
const edges = clonedGraph.get(node) as T[];
|
||||
const newEdges = edges.filter(edge => edge !== processingNode);
|
||||
|
||||
clonedGraph.set(node, newEdges);
|
||||
|
||||
if (newEdges.length === 0) {
|
||||
nodesWithNoEdges.push(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (clonedGraph.size > 0) {
|
||||
const edgesLeft = JSON.stringify([...clonedGraph.entries()]);
|
||||
throw new Error(
|
||||
`Topological ordering did not complete, these edges could not be ordered: ${edgesLeft}`
|
||||
);
|
||||
}
|
||||
|
||||
return sorted;
|
||||
}
|
12
packages/kbn-system-loader/tsconfig.json
Normal file
12
packages/kbn-system-loader/tsconfig.json
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es2017",
|
||||
"declaration": true,
|
||||
"outDir": "./target"
|
||||
},
|
||||
"include": [
|
||||
"./src/**/*.ts"
|
||||
]
|
||||
}
|
11
packages/kbn-system-loader/yarn.lock
Normal file
11
packages/kbn-system-loader/yarn.lock
Normal file
|
@ -0,0 +1,11 @@
|
|||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@types/jest@^22.2.2":
|
||||
version "22.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-22.2.2.tgz#afe5dacbd00d65325f52da0ed3e76e259629ac9d"
|
||||
|
||||
typescript@^2.8.1:
|
||||
version "2.8.1"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.8.1.tgz#6160e4f8f195d5ba81d4876f9c0cc1fbc0820624"
|
Loading…
Add table
Add a link
Reference in a new issue