Skip to content

Replace client-side context with getStartServices API #49691

@joshdover

Description

@joshdover

This issue evolved from the discussion in #46483

After seeing this play out in practice in a few different areas, I think we're seeing context on the client-side being used very differently than on the server-side.

Server-side context

On the server-side, we're using context providers to:

  1. Simplify consumption of an API. For instance, provide the most recent emitted value of an Observable rather than the Observable itself to a HTTP route.
  2. Bind a service to the current request. This allows us to avoid having to pass the request object all over the place just to get user-scoped access to Elasticsearch.

This makes the context providers on the server a bit more complex (and useful) than just the raw APIs they're built on top of. It also means they're more specific to their usages. For instance, a context provider for HTTP routes would be different than a context provider for a background task.

Client-side context

On the client-side, things are a bit different:

  1. We're really just injecting services into functions, unchanged from their raw form
  2. Exposing "fixed" versions of a service don't quite make sense for many of the rendering contexts because it's quite likely that we'd want the UI to adapt to Observables. This is different than HTTP routes on the backend because rendering contexts are long-lived.
  3. It's hard to imagine fundamentally different types of usage patterns like we might see on the server. Almost all client-side code is ultimately concerned with showing something to the user. Even the "search" context mentioned by Stacey above should probably adapt to Observables changing so that UI that depends on these search APIs adapt to an setting change.

Ultimately, many of the motivations in the original RFC don't apply to the use-cases we see on the client-side.

Since we're using context providers in such a simple way on the client, it does feel like a lot of unnecessary complexity just to get access to some core APIs.

Alternative: Service binder

This leads me to believe that maybe what we really need on the client is a simple way to bind Core and Plugin APIs to a function:

interface CoreSetup {
  bindServices<T extends (...args: any[] => any)>(
    callback: (core: CoreStart, plugins: object) => T
  ): T;
}

class Plugin {
  setup(core) {
    core.application.register({
      id: 'myApp',
      mount: core.bindServices(
        (core: CoreStart, plugins: MyPlugins) => ({ element }) => {
          // normal app mounting code here
        }
      ),
    });
  }
}

This bindServices method would still solve one of the core problems that context currently solves on the client: getting access to APIs available only during the start lifecycle for functions registered during setup.

Advantages of this approach:

  1. Neither registries nor calling plugins would necessarily need to know about bindServices at all.
  2. This eliminates the potential bug highlighted in the context service documentation where a plugin could use the context service incorrectly, resulting in handlers getting the wrong dependencies.
  3. Can be used deeply inside a UI or service tree to get access to Core APIs without having to pass all of them down.

Basically this bindServices function would be a very simple dependency injector that would respect the calling plugin's dependencies and does not introduce any magic to get any dependency in the system.

Disadvantages:

  1. We lose the ability to expose "fixed" services such as resolving Observables. However, as noted in the difference (3) listed above, this is not something we generally want to do on the client.
  2. We cannot tailor APIs to specific contexts. However, I have yet to see a use-case for this on the client.
  3. This may limit what we could remove from CoreStart as proposed by a pending RFC though that RFC is primarily focused on the server-side.

Metadata

Metadata

Assignees

Labels

Feature:New PlatformTeam:CorePlatform Core services: plugins, logging, config, saved objects, http, ES client, i18n, etc t//blocker

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions