diff --git a/docs/development/core/public/kibana-plugin-public.contextcontainer.createhandler.md b/docs/development/core/public/kibana-plugin-public.contextcontainer.createhandler.md
new file mode 100644
index 000000000000000..899b35c07e6ec69
--- /dev/null
+++ b/docs/development/core/public/kibana-plugin-public.contextcontainer.createhandler.md
@@ -0,0 +1,28 @@
+
+
+[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ContextContainer](./kibana-plugin-public.contextcontainer.md) > [createHandler](./kibana-plugin-public.contextcontainer.createhandler.md)
+
+## ContextContainer.createHandler() method
+
+Create a new handler function pre-wired to context for the plugin.
+
+Signature:
+
+```typescript
+createHandler(handler: Handler): (...rest: THandlerParameters) => Promisify;
+```
+
+## Parameters
+
+| Parameter | Type | Description |
+| --- | --- | --- |
+| handler | Handler<TContext, THandlerReturn, THandlerParameters>
| |
+
+Returns:
+
+`(...rest: THandlerParameters) => Promisify`
+
+## Remarks
+
+This must be called when the handler is registered by the consuming plugin. If this is called later in the lifecycle it will throw an exception.
+
diff --git a/docs/development/core/public/kibana-plugin-public.contextcontainer.md b/docs/development/core/public/kibana-plugin-public.contextcontainer.md
new file mode 100644
index 000000000000000..8b1ae8c9e96ada5
--- /dev/null
+++ b/docs/development/core/public/kibana-plugin-public.contextcontainer.md
@@ -0,0 +1,27 @@
+
+
+[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ContextContainer](./kibana-plugin-public.contextcontainer.md)
+
+## ContextContainer interface
+
+An object that handles registration of context providers and building of new context objects.
+
+Signature:
+
+```typescript
+export interface ContextContainer
+```
+
+## Methods
+
+| Method | Description |
+| --- | --- |
+| [createHandler(handler)](./kibana-plugin-public.contextcontainer.createhandler.md) | Create a new handler function pre-wired to context for the plugin. |
+| [registerContext(contextName, provider)](./kibana-plugin-public.contextcontainer.registercontext.md) | Register a new context provider. Throws an exception if more than one provider is registered for the same context key. |
+
+## Remarks
+
+A `ContextContainer` can be used by any Core service or plugin (known as the "service owner") which wishes to expose APIs in a handler function. The container object will manage registering context providers and building a context object for a handler with all of the contexts that should be exposed to the handler's plugin. This is dependent on the dependencies that the handler's plugin declares.
+
+Contexts providers are executed in the order they were registered. Each provider gets access to context values provided by any plugins that it depends on.
+
diff --git a/docs/development/core/public/kibana-plugin-public.contextcontainer.registercontext.md b/docs/development/core/public/kibana-plugin-public.contextcontainer.registercontext.md
new file mode 100644
index 000000000000000..22047b66d02151a
--- /dev/null
+++ b/docs/development/core/public/kibana-plugin-public.contextcontainer.registercontext.md
@@ -0,0 +1,27 @@
+
+
+[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ContextContainer](./kibana-plugin-public.contextcontainer.md) > [registerContext](./kibana-plugin-public.contextcontainer.registercontext.md)
+
+## ContextContainer.registerContext() method
+
+Register a new context provider. Throws an exception if more than one provider is registered for the same context key.
+
+Signature:
+
+```typescript
+registerContext(contextName: TContextName, provider: ContextProvider): this;
+```
+
+## Parameters
+
+| Parameter | Type | Description |
+| --- | --- | --- |
+| contextName | TContextName
| The key of the TContext
object this provider supplies the value for. |
+| provider | ContextProvider<TContext, TContextName, THandlerParameters>
| A [ContextProvider](./kibana-plugin-public.contextprovider.md) to be called each time a new context is created. |
+
+Returns:
+
+`this`
+
+The `ContextContainer` for method chaining.
+
diff --git a/docs/development/core/public/kibana-plugin-public.contextprovider.md b/docs/development/core/public/kibana-plugin-public.contextprovider.md
new file mode 100644
index 000000000000000..2e16e0568c90a4f
--- /dev/null
+++ b/docs/development/core/public/kibana-plugin-public.contextprovider.md
@@ -0,0 +1,18 @@
+
+
+[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ContextProvider](./kibana-plugin-public.contextprovider.md)
+
+## ContextProvider type
+
+A function that returns a context value for a specific key of given context type.
+
+Signature:
+
+```typescript
+export declare type ContextProvider = (context: Partial, ...rest: TProviderParameters) => Promise | TContext[TContextName];
+```
+
+## Remarks
+
+This function will be called each time a new context is built for a handler invocation.
+
diff --git a/docs/development/core/public/kibana-plugin-public.contextsetup.createcontextcontainer.md b/docs/development/core/public/kibana-plugin-public.contextsetup.createcontextcontainer.md
new file mode 100644
index 000000000000000..88c661228cdc000
--- /dev/null
+++ b/docs/development/core/public/kibana-plugin-public.contextsetup.createcontextcontainer.md
@@ -0,0 +1,17 @@
+
+
+[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ContextSetup](./kibana-plugin-public.contextsetup.md) > [createContextContainer](./kibana-plugin-public.contextsetup.createcontextcontainer.md)
+
+## ContextSetup.createContextContainer() method
+
+Creates a new [ContextContainer](./kibana-plugin-public.contextcontainer.md) for a service owner.
+
+Signature:
+
+```typescript
+createContextContainer(): ContextContainer;
+```
+Returns:
+
+`ContextContainer`
+
diff --git a/docs/development/core/public/kibana-plugin-public.contextsetup.md b/docs/development/core/public/kibana-plugin-public.contextsetup.md
new file mode 100644
index 000000000000000..f1c8db5c8fd231f
--- /dev/null
+++ b/docs/development/core/public/kibana-plugin-public.contextsetup.md
@@ -0,0 +1,52 @@
+
+
+[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ContextSetup](./kibana-plugin-public.contextsetup.md)
+
+## ContextSetup interface
+
+An object that handles registration of context providers and building of new context objects.
+
+Signature:
+
+```typescript
+export interface ContextSetup
+```
+
+## Methods
+
+| Method | Description |
+| --- | --- |
+| [createContextContainer()](./kibana-plugin-public.contextsetup.createcontextcontainer.md) | Creates a new [ContextContainer](./kibana-plugin-public.contextcontainer.md) for a service owner. |
+
+## Remarks
+
+A `ContextContainer` can be used by any Core service or plugin (known as the "service owner") which wishes to expose APIs in a handler function. The container object will manage registering context providers and building a context object for a handler with all of the contexts that should be exposed to the handler's plugin. This is dependent on the dependencies that the handler's plugin declares.
+
+Contexts providers are executed in the order they were registered. Each provider gets access to context values provided by any plugins that it depends on.
+
+## Example
+
+How to create your own context
+
+```ts
+class MyPlugin {
+ setup(core) {
+ this.myHandlers = new Map();
+ this.contextContainer = core.createContextContainer();
+ return {
+ registerContext: this.contextContainer.register,
+ registerHandler: (endpoint, handler) =>
+ // `createHandler` must be called immediately.
+ this.myHandlers.set(endpoint, this.contextContainer.createHandler(handler)),
+ };
+ }
+
+ start() {
+ return {
+ registerContext: this.contextContainer.register,
+ };
+ }
+}
+
+```
+
diff --git a/docs/development/core/public/kibana-plugin-public.coresetup.context.md b/docs/development/core/public/kibana-plugin-public.coresetup.context.md
new file mode 100644
index 000000000000000..dbb1c6d127a505b
--- /dev/null
+++ b/docs/development/core/public/kibana-plugin-public.coresetup.context.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [CoreSetup](./kibana-plugin-public.coresetup.md) > [context](./kibana-plugin-public.coresetup.context.md)
+
+## CoreSetup.context property
+
+[ContextSetup](./kibana-plugin-public.contextsetup.md)
+
+Signature:
+
+```typescript
+context: Pick;
+```
diff --git a/docs/development/core/public/kibana-plugin-public.coresetup.md b/docs/development/core/public/kibana-plugin-public.coresetup.md
index 5bbd54a2561a31d..529a36c6e73683f 100644
--- a/docs/development/core/public/kibana-plugin-public.coresetup.md
+++ b/docs/development/core/public/kibana-plugin-public.coresetup.md
@@ -16,6 +16,7 @@ export interface CoreSetup
| Property | Type | Description |
| --- | --- | --- |
+| [context](./kibana-plugin-public.coresetup.context.md) | Pick<ContextSetup, 'createContextContainer'>
| [ContextSetup](./kibana-plugin-public.contextsetup.md) |
| [fatalErrors](./kibana-plugin-public.coresetup.fatalerrors.md) | FatalErrorsSetup
| [FatalErrorsSetup](./kibana-plugin-public.fatalerrorssetup.md) |
| [http](./kibana-plugin-public.coresetup.http.md) | HttpSetup
| [HttpSetup](./kibana-plugin-public.httpsetup.md) |
| [notifications](./kibana-plugin-public.coresetup.notifications.md) | NotificationsSetup
| [NotificationsSetup](./kibana-plugin-public.notificationssetup.md) |
diff --git a/docs/development/core/public/kibana-plugin-public.handler.md b/docs/development/core/public/kibana-plugin-public.handler.md
new file mode 100644
index 000000000000000..856d2d580757369
--- /dev/null
+++ b/docs/development/core/public/kibana-plugin-public.handler.md
@@ -0,0 +1,18 @@
+
+
+[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [Handler](./kibana-plugin-public.handler.md)
+
+## Handler type
+
+A function registered by a plugin to perform some action.
+
+Signature:
+
+```typescript
+export declare type Handler = (context: TContext, ...rest: THandlerParameters) => TReturn;
+```
+
+## Remarks
+
+A new `TContext` will be built for each handler before invoking.
+
diff --git a/docs/development/core/public/kibana-plugin-public.md b/docs/development/core/public/kibana-plugin-public.md
index 98b6a8703f54358..15e300e689d2d6b 100644
--- a/docs/development/core/public/kibana-plugin-public.md
+++ b/docs/development/core/public/kibana-plugin-public.md
@@ -34,6 +34,8 @@ The plugin integrates with the core system via lifecycle events: `setup`
| [ChromeRecentlyAccessed](./kibana-plugin-public.chromerecentlyaccessed.md) | [APIs](./kibana-plugin-public.chromerecentlyaccessed.md) for recently accessed history. |
| [ChromeRecentlyAccessedHistoryItem](./kibana-plugin-public.chromerecentlyaccessedhistoryitem.md) | |
| [ChromeStart](./kibana-plugin-public.chromestart.md) | ChromeStart allows plugins to customize the global chrome header UI and enrich the UX with additional information about the current location of the browser. |
+| [ContextContainer](./kibana-plugin-public.contextcontainer.md) | An object that handles registration of context providers and building of new context objects. |
+| [ContextSetup](./kibana-plugin-public.contextsetup.md) | An object that handles registration of context providers and building of new context objects. |
| [CoreSetup](./kibana-plugin-public.coresetup.md) | Core services exposed to the Plugin
setup lifecycle |
| [CoreStart](./kibana-plugin-public.corestart.md) | Core services exposed to the Plugin
start lifecycle |
| [DocLinksStart](./kibana-plugin-public.doclinksstart.md) | |
@@ -58,6 +60,8 @@ The plugin integrates with the core system via lifecycle events: `setup`
| --- | --- |
| [ChromeHelpExtension](./kibana-plugin-public.chromehelpextension.md) | |
| [ChromeNavLinkUpdateableFields](./kibana-plugin-public.chromenavlinkupdateablefields.md) | |
+| [ContextProvider](./kibana-plugin-public.contextprovider.md) | A function that returns a context value for a specific key of given context type. |
+| [Handler](./kibana-plugin-public.handler.md) | A function registered by a plugin to perform some action. |
| [HttpSetup](./kibana-plugin-public.httpsetup.md) | |
| [HttpStart](./kibana-plugin-public.httpstart.md) | |
| [PluginInitializer](./kibana-plugin-public.plugininitializer.md) | The plugin
export at the root of a plugin's public
directory should conform to this interface. |
diff --git a/src/core/public/context/context.mock.ts b/src/core/public/context/context.mock.ts
new file mode 100644
index 000000000000000..3ff6ea280276345
--- /dev/null
+++ b/src/core/public/context/context.mock.ts
@@ -0,0 +1,37 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { ContextContainerImplementation } from './context';
+
+export type ContextContainerMock = jest.Mocked<
+ PublicMethodsOf>
+>;
+
+const createContextMock = () => {
+ const contextMock: ContextContainerMock = {
+ registerContext: jest.fn(),
+ createHandler: jest.fn(),
+ setCurrentPlugin: jest.fn(),
+ };
+ return contextMock;
+};
+
+export const contextMock = {
+ create: createContextMock,
+};
diff --git a/src/core/public/context/context.test.ts b/src/core/public/context/context.test.ts
new file mode 100644
index 000000000000000..8027acd7aa19608
--- /dev/null
+++ b/src/core/public/context/context.test.ts
@@ -0,0 +1,212 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { PluginName } from '../../server';
+import { ContextContainerImplementation } from './context';
+
+const plugins: ReadonlyMap = new Map([
+ ['pluginA', []],
+ ['pluginB', ['pluginA']],
+ ['pluginC', ['pluginA', 'pluginB']],
+ ['pluginD', []],
+]);
+
+interface MyContext {
+ core1: string;
+ core2: number;
+ ctxFromA: string;
+ ctxFromB: number;
+ ctxFromC: boolean;
+ ctxFromD: object;
+ baseCtx: number;
+}
+
+describe('ContextContainer', () => {
+ it('does not allow the same context to be registered twice', () => {
+ const contextContainer = new ContextContainerImplementation(plugins);
+ contextContainer.registerContext('ctxFromA', () => 'aString');
+
+ expect(() =>
+ contextContainer.registerContext('ctxFromA', () => 'aString')
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"Context provider for ctxFromA has already been registered."`
+ );
+ });
+
+ describe('context building', () => {
+ it('resolves dependencies', async () => {
+ const contextContainer = new ContextContainerImplementation(plugins);
+ expect.assertions(8);
+ contextContainer.registerContext('core1', context => {
+ expect(context).toEqual({});
+ return 'core';
+ });
+
+ contextContainer.setCurrentPlugin('pluginA');
+ contextContainer.registerContext('ctxFromA', context => {
+ expect(context).toEqual({ core1: 'core' });
+ return 'aString';
+ });
+ contextContainer.setCurrentPlugin('pluginB');
+ contextContainer.registerContext('ctxFromB', context => {
+ expect(context).toEqual({ core1: 'core', ctxFromA: 'aString' });
+ return 299;
+ });
+ contextContainer.setCurrentPlugin('pluginC');
+ contextContainer.registerContext('ctxFromC', context => {
+ expect(context).toEqual({ core1: 'core', ctxFromA: 'aString', ctxFromB: 299 });
+ return false;
+ });
+ contextContainer.setCurrentPlugin('pluginD');
+ contextContainer.registerContext('ctxFromD', context => {
+ expect(context).toEqual({ core1: 'core' });
+ return {};
+ });
+
+ contextContainer.setCurrentPlugin('pluginC');
+ const rawHandler1 = jest.fn(() => 'handler1');
+ const handler1 = contextContainer.createHandler(rawHandler1);
+
+ contextContainer.setCurrentPlugin('pluginD');
+ const rawHandler2 = jest.fn(() => 'handler2');
+ const handler2 = contextContainer.createHandler(rawHandler2);
+
+ contextContainer.setCurrentPlugin(undefined);
+
+ await handler1();
+ await handler2();
+
+ // Should have context from pluginC, its deps, and core
+ expect(rawHandler1).toHaveBeenCalledWith({
+ core1: 'core',
+ ctxFromA: 'aString',
+ ctxFromB: 299,
+ ctxFromC: false,
+ });
+
+ // Should have context from pluginD, and core
+ expect(rawHandler2).toHaveBeenCalledWith({
+ core1: 'core',
+ ctxFromD: {},
+ });
+ });
+
+ it('exposes all previously registered context to Core providers', async () => {
+ expect.assertions(4);
+ const contextContainer = new ContextContainerImplementation(plugins);
+
+ contextContainer
+ .registerContext('core1', context => {
+ expect(context).toEqual({});
+ return 'core';
+ })
+ .registerContext('core2', context => {
+ expect(context).toEqual({ core1: 'core' });
+ return 101;
+ });
+
+ contextContainer.setCurrentPlugin('pluginA');
+ const rawHandler1 = jest.fn(() => 'handler1');
+ const handler1 = contextContainer.createHandler(rawHandler1);
+
+ expect(await handler1()).toEqual('handler1');
+
+ // If no context is registered for pluginA, only core contexts should be exposed
+ expect(rawHandler1).toHaveBeenCalledWith({
+ core1: 'core',
+ core2: 101,
+ });
+ });
+
+ it('passes additional arguments to providers', async () => {
+ expect.assertions(6);
+ const contextContainer = new ContextContainerImplementation<
+ MyContext,
+ string,
+ [string, number]
+ >(plugins);
+
+ contextContainer.registerContext('core1', (context, str, num) => {
+ expect(str).toEqual('passed string');
+ expect(num).toEqual(77);
+ return `core ${str}`;
+ });
+
+ contextContainer.setCurrentPlugin('pluginD');
+ contextContainer.registerContext('ctxFromD', (context, str, num) => {
+ expect(str).toEqual('passed string');
+ expect(num).toEqual(77);
+ return {
+ num: 77,
+ };
+ });
+
+ const rawHandler1 = jest.fn(() => 'handler1');
+ const handler1 = contextContainer.createHandler(rawHandler1);
+
+ expect(await handler1('passed string', 77)).toEqual('handler1');
+
+ expect(rawHandler1).toHaveBeenCalledWith(
+ {
+ core1: 'core passed string',
+ ctxFromD: {
+ num: 77,
+ },
+ },
+ 'passed string',
+ 77
+ );
+ });
+ });
+
+ describe('createHandler', () => {
+ it('throws error if registered outside plugin', async () => {
+ const contextContainer = new ContextContainerImplementation(plugins);
+ contextContainer.setCurrentPlugin(undefined);
+ await expect(() =>
+ contextContainer.createHandler(jest.fn())
+ ).toThrowErrorMatchingInlineSnapshot(`"Cannot create handlers outside a plugin!"`);
+ });
+
+ it('returns value from original handler', async () => {
+ const contextContainer = new ContextContainerImplementation(plugins);
+
+ contextContainer.setCurrentPlugin('pluginA');
+ const rawHandler1 = jest.fn(() => 'handler1');
+ const handler1 = contextContainer.createHandler(rawHandler1);
+
+ expect(await handler1()).toEqual('handler1');
+ });
+
+ it('passes additional arguments to handlers', async () => {
+ const contextContainer = new ContextContainerImplementation<
+ MyContext,
+ string,
+ [string, number]
+ >(plugins);
+
+ contextContainer.setCurrentPlugin('pluginA');
+ const rawHandler1 = jest.fn(() => 'handler1');
+ const handler1 = contextContainer.createHandler(rawHandler1);
+
+ await handler1('passed string', 77);
+ expect(rawHandler1).toHaveBeenCalledWith({}, 'passed string', 77);
+ });
+ });
+});
diff --git a/src/core/public/context/context.ts b/src/core/public/context/context.ts
new file mode 100644
index 000000000000000..669f70ffeacb95d
--- /dev/null
+++ b/src/core/public/context/context.ts
@@ -0,0 +1,222 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { flatten } from 'lodash';
+import { PluginName } from '../../server';
+import { pick } from '../../utils';
+
+/**
+ * A function that returns a context value for a specific key of given context type.
+ *
+ * @remarks
+ * This function will be called each time a new context is built for a handler invocation.
+ *
+ * @param context - A partial context object containing only the keys for values provided by plugin dependencies
+ * @param rest - Additional parameters provided by the service owner of this context
+ * @returns The context value associated with this key. May also return a Promise.
+ *
+ * @public
+ */
+export type ContextProvider<
+ TContext extends {},
+ TContextName extends keyof TContext,
+ TProviderParameters extends any[] = []
+> = (
+ context: Partial,
+ ...rest: TProviderParameters
+) => Promise | TContext[TContextName];
+
+/**
+ * A function registered by a plugin to perform some action.
+ *
+ * @remarks
+ * A new `TContext` will be built for each handler before invoking.
+ *
+ * @public
+ */
+export type Handler = (
+ context: TContext,
+ ...rest: THandlerParameters
+) => TReturn;
+
+type Promisify = T extends Promise ? Promise : Promise;
+
+/**
+ * An object that handles registration of context providers and building of new context objects.
+ *
+ * @remarks
+ * A `ContextContainer` can be used by any Core service or plugin (known as the "service owner") which wishes to expose
+ * APIs in a handler function. The container object will manage registering context providers and building a context
+ * object for a handler with all of the contexts that should be exposed to the handler's plugin. This is dependent on
+ * the dependencies that the handler's plugin declares.
+ *
+ * Contexts providers are executed in the order they were registered. Each provider gets access to context values
+ * provided by any plugins that it depends on.
+ *
+ *
+ * @public
+ */
+export interface ContextContainer<
+ TContext extends {},
+ THandlerReturn,
+ THandlerParameters extends any[] = []
+> {
+ /**
+ * Register a new context provider. Throws an exception if more than one provider is registered for the same context
+ * key.
+ *
+ * @param contextName - The key of the `TContext` object this provider supplies the value for.
+ * @param provider - A {@link ContextProvider} to be called each time a new context is created.
+ * @returns The `ContextContainer` for method chaining.
+ */
+ registerContext(
+ contextName: TContextName,
+ provider: ContextProvider
+ ): this;
+
+ /**
+ * Create a new handler function pre-wired to context for the plugin.
+ *
+ * @remarks
+ * This must be called when the handler is registered by the consuming plugin. If this is called later in the
+ * lifecycle it will throw an exception.
+ *
+ * @param handler
+ */
+ createHandler(
+ handler: Handler
+ ): (...rest: THandlerParameters) => Promisify;
+}
+
+/** @internal */
+export class ContextContainerImplementation<
+ TContext extends {},
+ THandlerReturn,
+ THandlerParameters extends any[] = []
+> implements ContextContainer {
+ private currentPlugin?: string;
+
+ /**
+ * Used to map contexts to their providers and associated plugin. In registration order which is tightly coupled to
+ * plugin load order.
+ */
+ private readonly contextProviders = new Map<
+ keyof TContext,
+ {
+ provider: ContextProvider;
+ plugin?: PluginName;
+ }
+ >();
+ /** Used to keep track of which plugins registered which contexts for dependency resolution. */
+ private readonly contextNamesByPlugin = new Map>();
+
+ /**
+ * @param pluginDependencies - A map of plugins to an array of their dependencies.
+ */
+ constructor(private readonly pluginDependencies: ReadonlyMap) {}
+
+ public registerContext = (
+ contextName: TContextName,
+ provider: ContextProvider
+ ): this => {
+ if (this.contextProviders.has(contextName)) {
+ throw new Error(`Context provider for ${contextName} has already been registered.`);
+ }
+
+ const plugin = this.currentPlugin;
+ this.contextProviders.set(contextName, { provider, plugin });
+
+ if (plugin) {
+ this.contextNamesByPlugin.set(plugin, [
+ ...(this.contextNamesByPlugin.get(plugin) || []),
+ contextName,
+ ]);
+ }
+
+ return this;
+ };
+
+ public createHandler = (handler: Handler) => {
+ const plugin = this.currentPlugin;
+ if (!plugin) {
+ throw new Error(`Cannot create handlers outside a plugin!`);
+ } else if (!this.pluginDependencies.has(plugin)) {
+ throw new Error(`Cannot create handler for unknown plugin: ${plugin}`);
+ }
+
+ return (async (...args: THandlerParameters) => {
+ const context = await this.buildContext(plugin, {}, ...args);
+ return handler(context, ...args);
+ }) as (...args: THandlerParameters) => Promisify;
+ };
+
+ private async buildContext(
+ pluginName: PluginName,
+ baseContext: Partial = {},
+ ...contextArgs: THandlerParameters
+ ): Promise {
+ const ownerContextNames = [...this.contextProviders]
+ .filter(([name, { plugin }]) => plugin === undefined)
+ .map(([name]) => name);
+ const contextNamesForPlugin = (plug: PluginName): Set => {
+ const pluginDeps = this.pluginDependencies.get(plug);
+ if (!pluginDeps) {
+ // This should be impossible, but just in case.
+ throw new Error(`Cannot create context for unknown plugin: ${pluginName}`);
+ }
+
+ return new Set([
+ // Owner contexts
+ ...ownerContextNames,
+ // Contexts calling plugin created
+ ...(this.contextNamesByPlugin.get(pluginName) || []),
+ // Contexts calling plugin's dependencies created
+ ...flatten(pluginDeps.map(p => this.contextNamesByPlugin.get(p) || [])),
+ ]);
+ };
+
+ const contextsToBuild = contextNamesForPlugin(pluginName);
+
+ return [...this.contextProviders]
+ .filter(([contextName]) => contextsToBuild.has(contextName))
+ .reduce(
+ async (contextPromise, [contextName, { provider, plugin }]) => {
+ const resolvedContext = await contextPromise;
+
+ // If the provider is not from a plugin, give access to the entire
+ // context built so far (this is only possible for providers registered
+ // by the service owner).
+ const exposedContext = plugin
+ ? pick(resolvedContext, [...contextNamesForPlugin(plugin)])
+ : resolvedContext;
+
+ return {
+ ...resolvedContext,
+ [contextName]: await provider(exposedContext as Partial, ...contextArgs),
+ };
+ },
+ Promise.resolve(baseContext) as Promise
+ );
+ }
+
+ /** @internal */
+ public setCurrentPlugin(plugin?: string) {
+ this.currentPlugin = plugin;
+ }
+}
diff --git a/src/core/public/context/context_service.mock.ts b/src/core/public/context/context_service.mock.ts
new file mode 100644
index 000000000000000..fae14cd1ccae6c3
--- /dev/null
+++ b/src/core/public/context/context_service.mock.ts
@@ -0,0 +1,53 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { ContextStart, ContextService, ContextSetup } from './context_service';
+import { contextMock } from './context.mock';
+
+const createSetupContractMock = () => {
+ const setupContract: jest.Mocked = {
+ createContextContainer: jest.fn().mockImplementation(() => contextMock.create()),
+ setCurrentPlugin: jest.fn(),
+ };
+ return setupContract;
+};
+
+const createStartContractMock = () => {
+ const startContract: jest.Mocked = {
+ setCurrentPlugin: jest.fn(),
+ };
+ return startContract;
+};
+
+type ContextServiceContract = PublicMethodsOf;
+const createMock = () => {
+ const mocked: jest.Mocked = {
+ setup: jest.fn(),
+ start: jest.fn(),
+ };
+ mocked.setup.mockReturnValue(createSetupContractMock());
+ mocked.start.mockReturnValue(createStartContractMock());
+ return mocked;
+};
+
+export const contextServiceMock = {
+ create: createMock,
+ createSetupContract: createSetupContractMock,
+ createStartContract: createStartContractMock,
+};
diff --git a/src/core/public/context/context_service.test.mocks.ts b/src/core/public/context/context_service.test.mocks.ts
new file mode 100644
index 000000000000000..d0bcb201b11ce55
--- /dev/null
+++ b/src/core/public/context/context_service.test.mocks.ts
@@ -0,0 +1,25 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { contextMock } from './context.mock';
+
+export const MockContextConstructor = jest.fn(contextMock.create);
+jest.doMock('./context', () => ({
+ ContextContainerImplementation: MockContextConstructor,
+}));
diff --git a/src/core/public/context/context_service.test.ts b/src/core/public/context/context_service.test.ts
new file mode 100644
index 000000000000000..467341083279a21
--- /dev/null
+++ b/src/core/public/context/context_service.test.ts
@@ -0,0 +1,104 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+// import { ContextContainerImplementation } from './context';
+
+import { MockContextConstructor } from './context_service.test.mocks';
+import { ContextService } from './context_service';
+import { ContextContainerMock } from './context.mock';
+
+const pluginDependencies = new Map([
+ ['pluginA', []],
+ ['pluginB', ['pluginA']],
+ ['pluginC', ['pluginB']],
+]);
+
+describe('ContextService', () => {
+ describe('#setup()', () => {
+ test('createContextContainer returns a new container configured with pluginDependencies', () => {
+ const service = new ContextService();
+ const setup = service.setup({ pluginDependencies });
+ expect(setup.createContextContainer()).toBeDefined();
+ expect(MockContextConstructor).toHaveBeenCalledWith(pluginDependencies);
+ });
+
+ test('setCurrentPlugin does not fail if there are no contianers', () => {
+ const service = new ContextService();
+ const setup = service.setup({ pluginDependencies });
+ expect(() => setup.setCurrentPlugin('pluginA')).not.toThrow();
+ });
+
+ test('setCurrentPlugin calls on all context containers', () => {
+ const service = new ContextService();
+ const setup = service.setup({ pluginDependencies });
+ const container1 = setup.createContextContainer() as ContextContainerMock;
+ const container2 = setup.createContextContainer() as ContextContainerMock;
+ const container3 = setup.createContextContainer() as ContextContainerMock;
+
+ setup.setCurrentPlugin('pluginA');
+ expect(container1.setCurrentPlugin).toHaveBeenCalledWith('pluginA');
+ expect(container2.setCurrentPlugin).toHaveBeenCalledWith('pluginA');
+ expect(container3.setCurrentPlugin).toHaveBeenCalledWith('pluginA');
+
+ setup.setCurrentPlugin('pluginB');
+ expect(container1.setCurrentPlugin).toHaveBeenCalledWith('pluginB');
+ expect(container2.setCurrentPlugin).toHaveBeenCalledWith('pluginB');
+ expect(container3.setCurrentPlugin).toHaveBeenCalledWith('pluginB');
+
+ setup.setCurrentPlugin(undefined);
+ expect(container1.setCurrentPlugin).toHaveBeenCalledWith(undefined);
+ expect(container2.setCurrentPlugin).toHaveBeenCalledWith(undefined);
+ expect(container3.setCurrentPlugin).toHaveBeenCalledWith(undefined);
+ });
+ });
+
+ describe('#start()', () => {
+ test('setCurrentPlugin does not fail if there are no contianers', () => {
+ const service = new ContextService();
+ service.setup({ pluginDependencies });
+ const start = service.start();
+ expect(() => start.setCurrentPlugin('pluginA')).not.toThrow();
+ });
+
+ test('setCurrentPlugin calls on all context containers', () => {
+ const service = new ContextService();
+ const setup = service.setup({ pluginDependencies });
+ const container1 = setup.createContextContainer() as ContextContainerMock;
+ const container2 = setup.createContextContainer() as ContextContainerMock;
+ const container3 = setup.createContextContainer() as ContextContainerMock;
+
+ const start = service.start();
+
+ start.setCurrentPlugin('pluginA');
+ expect(container1.setCurrentPlugin).toHaveBeenCalledWith('pluginA');
+ expect(container2.setCurrentPlugin).toHaveBeenCalledWith('pluginA');
+ expect(container3.setCurrentPlugin).toHaveBeenCalledWith('pluginA');
+
+ start.setCurrentPlugin('pluginB');
+ expect(container1.setCurrentPlugin).toHaveBeenCalledWith('pluginB');
+ expect(container2.setCurrentPlugin).toHaveBeenCalledWith('pluginB');
+ expect(container3.setCurrentPlugin).toHaveBeenCalledWith('pluginB');
+
+ start.setCurrentPlugin(undefined);
+ expect(container1.setCurrentPlugin).toHaveBeenCalledWith(undefined);
+ expect(container2.setCurrentPlugin).toHaveBeenCalledWith(undefined);
+ expect(container3.setCurrentPlugin).toHaveBeenCalledWith(undefined);
+ });
+ });
+});
diff --git a/src/core/public/context/context_service.ts b/src/core/public/context/context_service.ts
new file mode 100644
index 000000000000000..c88198bb7b27110
--- /dev/null
+++ b/src/core/public/context/context_service.ts
@@ -0,0 +1,112 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { ContextContainer, ContextContainerImplementation } from './context';
+
+interface StartDeps {
+ pluginDependencies: ReadonlyMap;
+}
+
+/** @internal */
+export class ContextService {
+ private readonly containers = new Set>();
+
+ public setup({ pluginDependencies }: StartDeps): ContextSetup {
+ return {
+ setCurrentPlugin: this.setCurrentPlugin.bind(this),
+ createContextContainer: <
+ TContext extends {},
+ THandlerReturn,
+ THandlerParameters extends any[] = []
+ >() => {
+ const newContainer = new ContextContainerImplementation<
+ TContext,
+ THandlerReturn,
+ THandlerParameters
+ >(pluginDependencies);
+
+ this.containers.add(newContainer);
+ return newContainer;
+ },
+ };
+ }
+
+ public start(): ContextStart {
+ return {
+ setCurrentPlugin: this.setCurrentPlugin.bind(this),
+ };
+ }
+
+ private setCurrentPlugin(plugin?: string) {
+ [...this.containers].forEach(container => container.setCurrentPlugin(plugin));
+ }
+}
+
+/**
+ * {@inheritdoc ContextContainer}
+ *
+ * @example
+ * How to create your own context
+ * ```ts
+ * class MyPlugin {
+ * setup(core) {
+ * this.myHandlers = new Map();
+ * this.contextContainer = core.createContextContainer();
+ * return {
+ * registerContext: this.contextContainer.register,
+ * registerHandler: (endpoint, handler) =>
+ * // `createHandler` must be called immediately.
+ * this.myHandlers.set(endpoint, this.contextContainer.createHandler(handler)),
+ * };
+ * }
+ *
+ * start() {
+ * return {
+ * registerContext: this.contextContainer.register,
+ * };
+ * }
+ * }
+ * ```
+ *
+ * @public
+ */
+export interface ContextSetup {
+ /**
+ * Must be called by the PluginsService during each plugin's lifecycle methods.
+ * @internal
+ */
+ setCurrentPlugin(plugin?: string): void;
+
+ /**
+ * Creates a new {@link ContextContainer} for a service owner.
+ */
+ createContextContainer<
+ TContext extends {},
+ THandlerReturn,
+ THandlerParmaters extends any[] = []
+ >(): ContextContainer;
+}
+
+/** @internal */
+export interface ContextStart {
+ /**
+ * @internal
+ */
+ setCurrentPlugin(plugin?: string): void;
+}
diff --git a/src/core/public/context/index.ts b/src/core/public/context/index.ts
new file mode 100644
index 000000000000000..270d8563cad8e08
--- /dev/null
+++ b/src/core/public/context/index.ts
@@ -0,0 +1,21 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export { ContextService, ContextSetup, ContextStart } from './context_service';
+export { ContextContainer, ContextProvider, Handler } from './context';
diff --git a/src/core/public/core_system.test.mocks.ts b/src/core/public/core_system.test.mocks.ts
index 4a96214b3e5defd..d2494badfacdb56 100644
--- a/src/core/public/core_system.test.mocks.ts
+++ b/src/core/public/core_system.test.mocks.ts
@@ -30,6 +30,7 @@ import { pluginsServiceMock } from './plugins/plugins_service.mock';
import { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock';
import { docLinksServiceMock } from './doc_links/doc_links_service.mock';
import { renderingServiceMock } from './rendering/rendering_service.mock';
+import { contextServiceMock } from './context/context_service.mock';
export const MockLegacyPlatformService = legacyPlatformServiceMock.create();
export const LegacyPlatformServiceConstructor = jest
@@ -120,3 +121,9 @@ export const RenderingServiceConstructor = jest.fn().mockImplementation(() => Mo
jest.doMock('./rendering', () => ({
RenderingService: RenderingServiceConstructor,
}));
+
+export const MockContextService = contextServiceMock.create();
+export const ContextServiceConstructor = jest.fn().mockImplementation(() => MockContextService);
+jest.doMock('./context', () => ({
+ ContextService: ContextServiceConstructor,
+}));
diff --git a/src/core/public/core_system.test.ts b/src/core/public/core_system.test.ts
index 044a40b27599364..ca26b6fe45dbadf 100644
--- a/src/core/public/core_system.test.ts
+++ b/src/core/public/core_system.test.ts
@@ -41,6 +41,7 @@ import {
MockDocLinksService,
MockRenderingService,
RenderingServiceConstructor,
+ MockContextService,
} from './core_system.test.mocks';
import { CoreSystem } from './core_system';
@@ -216,6 +217,11 @@ describe('#start()', () => {
expect(MockApplicationService.start).toHaveBeenCalledTimes(1);
});
+ it('calls context#start()', async () => {
+ await startCore();
+ expect(MockContextService.start).toHaveBeenCalledTimes(1);
+ });
+
it('calls docLinks#start()', async () => {
await startCore();
expect(MockDocLinksService.start).toHaveBeenCalledTimes(1);
diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts
index f3f466df8a78e4c..d8de15d73b1cc43 100644
--- a/src/core/public/core_system.ts
+++ b/src/core/public/core_system.ts
@@ -34,6 +34,7 @@ import { ApplicationService } from './application';
import { mapToObject } from '../utils/';
import { DocLinksService } from './doc_links';
import { RenderingService } from './rendering';
+import { ContextService } from './context';
interface Params {
rootDomElement: HTMLElement;
@@ -69,6 +70,7 @@ export class CoreSystem {
private readonly application: ApplicationService;
private readonly docLinks: DocLinksService;
private readonly rendering: RenderingService;
+ private readonly context: ContextService;
private readonly rootDomElement: HTMLElement;
private fatalErrorsSetup: FatalErrorsSetup | null = null;
@@ -103,6 +105,7 @@ export class CoreSystem {
this.chrome = new ChromeService({ browserSupportsCsp });
this.docLinks = new DocLinksService();
this.rendering = new RenderingService();
+ this.context = new ContextService();
const core: CoreContext = {};
this.plugins = new PluginsService(core);
@@ -127,8 +130,12 @@ export class CoreSystem {
const notifications = this.notifications.setup({ uiSettings });
const application = this.application.setup();
+ const pluginDependencies = this.plugins.setPluginDependencies(injectedMetadata.getPlugins());
+ const context = this.context.setup({ pluginDependencies });
+
const core: InternalCoreSetup = {
application,
+ context,
fatalErrors: this.fatalErrorsSetup,
http,
injectedMetadata,
@@ -159,6 +166,7 @@ export class CoreSystem {
const http = await this.http.start({ injectedMetadata, fatalErrors: this.fatalErrorsSetup });
const i18n = await this.i18n.start();
const application = await this.application.start({ injectedMetadata });
+ const context = await this.context.start();
const coreUiTargetDomElement = document.createElement('div');
coreUiTargetDomElement.id = 'kibana-body';
@@ -190,6 +198,7 @@ export class CoreSystem {
const core: InternalCoreStart = {
application,
chrome,
+ context,
docLinks,
http,
i18n,
diff --git a/src/core/public/index.ts b/src/core/public/index.ts
index 2a88ebf86ab0c55..66b69bbc289778e 100644
--- a/src/core/public/index.ts
+++ b/src/core/public/index.ts
@@ -66,6 +66,7 @@ import { Plugin, PluginInitializer, PluginInitializerContext } from './plugins';
import { UiSettingsClient, UiSettingsState, UiSettingsClientContract } from './ui_settings';
import { ApplicationSetup, Capabilities, ApplicationStart } from './application';
import { DocLinksStart } from './doc_links';
+import { ContextContainer, ContextProvider, ContextSetup, ContextStart, Handler } from './context';
export { CoreContext, CoreSystem } from './core_system';
export { RecursiveReadonly } from '../utils';
@@ -80,6 +81,8 @@ export { RecursiveReadonly } from '../utils';
* https://github.com/Microsoft/web-build-tools/issues/1237
*/
export interface CoreSetup {
+ /** {@link ContextSetup} */
+ context: Pick;
/** {@link FatalErrorsSetup} */
fatalErrors: FatalErrorsSetup;
/** {@link HttpSetup} */
@@ -121,12 +124,14 @@ export interface CoreStart {
/** @internal */
export interface InternalCoreSetup extends CoreSetup {
application: ApplicationSetup;
+ context: ContextSetup;
injectedMetadata: InjectedMetadataSetup;
}
/** @internal */
export interface InternalCoreStart extends CoreStart {
application: ApplicationStart;
+ context: ContextStart;
injectedMetadata: InjectedMetadataStart;
}
@@ -146,10 +151,14 @@ export {
ChromeRecentlyAccessed,
ChromeRecentlyAccessedHistoryItem,
ChromeStart,
+ ContextContainer,
+ ContextProvider,
+ ContextSetup,
DocLinksStart,
ErrorToastOptions,
FatalErrorInfo,
FatalErrorsSetup,
+ Handler,
HttpInterceptor,
HttpServiceBase,
HttpSetup,
diff --git a/src/core/public/legacy/legacy_service.test.ts b/src/core/public/legacy/legacy_service.test.ts
index a514bf71540550e..76a05f8d03ae712 100644
--- a/src/core/public/legacy/legacy_service.test.ts
+++ b/src/core/public/legacy/legacy_service.test.ts
@@ -58,8 +58,10 @@ import { uiSettingsServiceMock } from '../ui_settings/ui_settings_service.mock';
import { LegacyPlatformService } from './legacy_service';
import { applicationServiceMock } from '../application/application_service.mock';
import { docLinksServiceMock } from '../doc_links/doc_links_service.mock';
+import { contextServiceMock } from '../context/context_service.mock';
const applicationSetup = applicationServiceMock.createSetupContract();
+const contextSetup = contextServiceMock.createSetupContract();
const fatalErrorsSetup = fatalErrorsServiceMock.createSetupContract();
const httpSetup = httpServiceMock.createSetupContract();
const injectedMetadataSetup = injectedMetadataServiceMock.createSetupContract();
@@ -75,6 +77,7 @@ const defaultParams = {
const defaultSetupDeps = {
core: {
application: applicationSetup,
+ context: contextSetup,
fatalErrors: fatalErrorsSetup,
injectedMetadata: injectedMetadataSetup,
notifications: notificationsSetup,
@@ -85,6 +88,7 @@ const defaultSetupDeps = {
};
const applicationStart = applicationServiceMock.createStartContract();
+const contextStart = contextServiceMock.createStartContract();
const docLinksStart = docLinksServiceMock.createStartContract();
const httpStart = httpServiceMock.createStartContract();
const chromeStart = chromeServiceMock.createStartContract();
@@ -97,6 +101,7 @@ const uiSettingsStart = uiSettingsServiceMock.createStartContract();
const defaultStartDeps = {
core: {
application: applicationStart,
+ context: contextStart,
docLinks: docLinksStart,
http: httpStart,
chrome: chromeStart,
diff --git a/src/core/public/mocks.ts b/src/core/public/mocks.ts
index b1312eaa228d214..3682d86168dcd2d 100644
--- a/src/core/public/mocks.ts
+++ b/src/core/public/mocks.ts
@@ -26,6 +26,7 @@ import { i18nServiceMock } from './i18n/i18n_service.mock';
import { notificationServiceMock } from './notifications/notifications_service.mock';
import { overlayServiceMock } from './overlays/overlay_service.mock';
import { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock';
+import { contextServiceMock } from './context/context_service.mock';
export { chromeServiceMock } from './chrome/chrome_service.mock';
export { docLinksServiceMock } from './doc_links/doc_links_service.mock';
@@ -40,6 +41,7 @@ export { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock';
function createCoreSetupMock() {
const mock: MockedKeys = {
+ context: contextServiceMock.createSetupContract(),
fatalErrors: fatalErrorsServiceMock.createSetupContract(),
http: httpServiceMock.createSetupContract(),
notifications: notificationServiceMock.createSetupContract(),
diff --git a/src/core/public/plugins/plugin_context.ts b/src/core/public/plugins/plugin_context.ts
index bc77b139a86dc50..557b79b6931a207 100644
--- a/src/core/public/plugins/plugin_context.ts
+++ b/src/core/public/plugins/plugin_context.ts
@@ -69,8 +69,9 @@ export function createPluginSetupContext<
plugin: PluginWrapper
): CoreSetup {
return {
- http: deps.http,
+ context: omit(deps.context, 'setCurrentPlugin'),
fatalErrors: deps.fatalErrors,
+ http: deps.http,
notifications: deps.notifications,
uiSettings: deps.uiSettings,
};
diff --git a/src/core/public/plugins/plugins_service.mock.ts b/src/core/public/plugins/plugins_service.mock.ts
index 4df57b05fda30b9..baadfd1336912d1 100644
--- a/src/core/public/plugins/plugins_service.mock.ts
+++ b/src/core/public/plugins/plugins_service.mock.ts
@@ -38,6 +38,7 @@ const createStartContractMock = () => {
type PluginsServiceContract = PublicMethodsOf;
const createMock = () => {
const mocked: jest.Mocked = {
+ setPluginDependencies: jest.fn(),
setup: jest.fn(),
start: jest.fn(),
stop: jest.fn(),
diff --git a/src/core/public/plugins/plugins_service.test.ts b/src/core/public/plugins/plugins_service.test.ts
index 55e91bde27cb09b..ef8ecd9348ca819 100644
--- a/src/core/public/plugins/plugins_service.test.ts
+++ b/src/core/public/plugins/plugins_service.test.ts
@@ -43,6 +43,7 @@ import { injectedMetadataServiceMock } from '../injected_metadata/injected_metad
import { httpServiceMock } from '../http/http_service.mock';
import { CoreSetup, CoreStart } from '..';
import { docLinksServiceMock } from '../doc_links/doc_links_service.mock';
+import { contextServiceMock } from '../context/context_service.mock';
export let mockPluginInitializers: Map;
@@ -61,6 +62,9 @@ let mockStartContext: DeeplyMocked;
beforeEach(() => {
mockSetupDeps = {
application: applicationServiceMock.createSetupContract(),
+ context: contextServiceMock.createSetupContract(),
+ fatalErrors: fatalErrorsServiceMock.createSetupContract(),
+ http: httpServiceMock.createSetupContract(),
injectedMetadata: (function() {
const metadata = injectedMetadataServiceMock.createSetupContract();
metadata.getPlugins.mockReturnValue([
@@ -73,14 +77,16 @@ beforeEach(() => {
]);
return metadata;
})(),
- fatalErrors: fatalErrorsServiceMock.createSetupContract(),
- http: httpServiceMock.createSetupContract(),
notifications: notificationServiceMock.createSetupContract(),
uiSettings: uiSettingsServiceMock.createSetupContract(),
};
- mockSetupContext = omit(mockSetupDeps, 'application', 'injectedMetadata');
+ mockSetupContext = {
+ ...omit(mockSetupDeps, 'application', 'injectedMetadata'),
+ context: omit(mockSetupDeps.context, 'setCurrentPlugin'),
+ };
mockStartDeps = {
application: applicationServiceMock.createStartContract(),
+ context: contextServiceMock.createStartContract(),
docLinks: docLinksServiceMock.createStartContract(),
http: httpServiceMock.createStartContract(),
chrome: chromeServiceMock.createStartContract(),
@@ -91,7 +97,7 @@ beforeEach(() => {
uiSettings: uiSettingsServiceMock.createStartContract(),
};
mockStartContext = {
- ...omit(mockStartDeps, 'injectedMetadata'),
+ ...omit(mockStartDeps, 'context', 'injectedMetadata'),
application: {
capabilities: mockStartDeps.application.capabilities,
},
diff --git a/src/core/public/plugins/plugins_service.ts b/src/core/public/plugins/plugins_service.ts
index 03725a9d7f88394..94ba4ce9bc9e858 100644
--- a/src/core/public/plugins/plugins_service.ts
+++ b/src/core/public/plugins/plugins_service.ts
@@ -17,7 +17,7 @@
* under the License.
*/
-import { PluginName } from '../../server';
+import { DiscoveredPlugin, PluginName } from '../../server';
import { CoreService } from '../../types';
import { CoreContext } from '../core_system';
import { PluginWrapper } from './plugin';
@@ -35,11 +35,11 @@ export type PluginsServiceStartDeps = InternalCoreStart;
/** @internal */
export interface PluginsServiceSetup {
- contracts: Map;
+ contracts: ReadonlyMap;
}
/** @internal */
export interface PluginsServiceStart {
- contracts: Map;
+ contracts: ReadonlyMap;
}
/**
@@ -50,15 +50,28 @@ export interface PluginsServiceStart {
*/
export class PluginsService implements CoreService {
/** Plugin wrappers in topological order. */
- private readonly plugins: Map<
- PluginName,
- PluginWrapper>
- > = new Map();
+ private readonly plugins = new Map>>();
+ private readonly pluginDependencies = new Map();
+
private readonly satupPlugins: PluginName[] = [];
constructor(private readonly coreContext: CoreContext) {}
- public async setup(deps: PluginsServiceSetupDeps) {
+ public setPluginDependencies(plugins: Array<{ id: PluginName; plugin: DiscoveredPlugin }>) {
+ // Setup map of dependencies
+ const allPluginNames = new Set(plugins.map(p => p.id));
+ plugins.forEach(({ id, plugin }) =>
+ this.pluginDependencies.set(id, [
+ ...plugin.requiredPlugins,
+ ...plugin.optionalPlugins.filter(optPlugin => allPluginNames.has(optPlugin)),
+ ])
+ );
+
+ return this.pluginDependencies;
+ }
+
+ public async setup(deps: PluginsServiceSetupDeps): Promise {
+ this.setPluginDependencies(deps.injectedMetadata.getPlugins());
// Construct plugin wrappers, depending on the topological order set by the server.
deps.injectedMetadata
.getPlugins()
@@ -75,12 +88,10 @@ export class PluginsService implements CoreService();
for (const [pluginName, plugin] of this.plugins.entries()) {
- const pluginDeps = new Set([
- ...plugin.requiredPlugins,
- ...plugin.optionalPlugins.filter(optPlugin => this.plugins.get(optPlugin)),
- ]);
+ // Set global context variable for current plugin setting up
+ deps.context.setCurrentPlugin(pluginName);
- const pluginDepContracts = [...pluginDeps.keys()].reduce(
+ const pluginDepContracts = [...this.pluginDependencies.get(pluginName)!].reduce(
(depContracts, dependencyName) => {
// Only set if present. Could be absent if plugin does not have client-side code or is a
// missing optional plugin.
@@ -104,20 +115,21 @@ export class PluginsService implements CoreService {
// Setup each plugin with required and optional plugin contracts
const contracts = new Map();
for (const [pluginName, plugin] of this.plugins.entries()) {
- const pluginDeps = new Set([
- ...plugin.requiredPlugins,
- ...plugin.optionalPlugins.filter(optPlugin => this.plugins.get(optPlugin)),
- ]);
+ // Set global context variable for current plugin setting up
+ deps.context.setCurrentPlugin(pluginName);
- const pluginDepContracts = [...pluginDeps.keys()].reduce(
+ const pluginDepContracts = [...this.pluginDependencies.get(pluginName)!].reduce(
(depContracts, dependencyName) => {
// Only set if present. Could be absent if plugin does not have client-side code or is a
// missing optional plugin.
@@ -139,6 +151,9 @@ export class PluginsService implements CoreService {
+ // Warning: (ae-forgotten-export) The symbol "Promisify" needs to be exported by the entry point index.d.ts
+ createHandler(handler: Handler): (...rest: THandlerParameters) => Promisify;
+ registerContext(contextName: TContextName, provider: ContextProvider): this;
+}
+
+// @public
+export type ContextProvider = (context: Partial, ...rest: TProviderParameters) => Promise | TContext[TContextName];
+
+// @public
+export interface ContextSetup {
+ createContextContainer(): ContextContainer;
+ // @internal
+ setCurrentPlugin(plugin?: string): void;
+}
+
// @internal (undocumented)
export interface CoreContext {
}
// @public
export interface CoreSetup {
+ // (undocumented)
+ context: Pick;
// (undocumented)
fatalErrors: FatalErrorsSetup;
// (undocumented)
@@ -329,6 +348,9 @@ export interface FatalErrorsSetup {
get$: () => Rx.Observable;
}
+// @public
+export type Handler = (context: TContext, ...rest: THandlerParameters) => TReturn;
+
// @public (undocumented)
export interface HttpInterceptor {
// Warning: (ae-forgotten-export) The symbol "HttpInterceptController" needs to be exported by the entry point index.d.ts
@@ -404,6 +426,8 @@ export interface I18nStart {
export interface InternalCoreSetup extends CoreSetup {
// (undocumented)
application: ApplicationSetup;
+ // (undocumented)
+ context: ContextSetup;
// Warning: (ae-forgotten-export) The symbol "InjectedMetadataSetup" needs to be exported by the entry point index.d.ts
//
// (undocumented)
@@ -414,6 +438,10 @@ export interface InternalCoreSetup extends CoreSetup {
export interface InternalCoreStart extends CoreStart {
// (undocumented)
application: ApplicationStart;
+ // Warning: (ae-forgotten-export) The symbol "ContextStart" needs to be exported by the entry point index.d.ts
+ //
+ // (undocumented)
+ context: ContextStart;
// Warning: (ae-forgotten-export) The symbol "InjectedMetadataStart" needs to be exported by the entry point index.d.ts
//
// (undocumented)
diff --git a/src/core/utils/map_to_object.ts b/src/core/utils/map_to_object.ts
index bfbe5c8ab0beaf9..edb2fc2bcbfc72e 100644
--- a/src/core/utils/map_to_object.ts
+++ b/src/core/utils/map_to_object.ts
@@ -17,7 +17,7 @@
* under the License.
*/
-export function mapToObject(map: Map) {
+export function mapToObject(map: ReadonlyMap) {
const result: Record = Object.create(null);
for (const [key, value] of map) {
result[key] = value;