Skip to content

Work in progress: this website is still under construction. Content may be incomplete or change at any time.

Services

Services are application components that implement one or more interfaces. Service instances are created automatically by the framework when required, and their dependencies will automatically be resolved prior to their construction.

A service is declared as a simple JavaScript class:

some-package/HelloWorldService.ts
export class HelloWorldService {
sayHello() {
console.info("hello!");
}
}

Services are not discovered automatically; they must be declared in the package’s build.config.mjs:

some-package/build.config.mjs
import { defineBuildConfig } from "@open-pioneer/build-support";
export default defineBuildConfig({
services: {
// (1)
HelloWorldService: {
provides: "hello.HelloWorldService"
}
}
});

A matching export using the same name (see (1)) must be provided from services.ts or (.js). The framework will try to import the class from that file:

some-package/services.ts
// The export name here must match the service name in the build.config.js.
// The actual class name in the source code does not matter.
// The class could also be defined in this module instead of importing it from a different file.
export { HelloWorldService } from "./HelloWorldService";

Instead of exporting a plain class, you can use a ServiceFactory to control how a service is created. This is useful when you need custom initialization logic or want full control over the service lifecycle.

A factory is declared as a class that implements ServiceFactory and is wrapped with defineServiceFactory:

some-package/HelloWorldServiceFactory.ts
import {
defineServiceFactory,
DeclaredService,
ServiceFactory,
ServiceOptions
} from "@open-pioneer/runtime";
interface HelloWorldService extends DeclaredService<"hello.HelloWorldService"> {
sayHello(): void;
}
class HelloWorldServiceFactoryImpl implements ServiceFactory<HelloWorldService> {
constructor(options: ServiceOptions) {
// ...
}
createService() {
const myServ = new MyHelloWorldService({/* ... */});
return myServ;
}
// optional
destroyService(srv: HelloWorldService) {
// cleanup
// NOTE: the service's own `destroy()` method will NOT be called automatically when using a factory.
}
}
export const HelloWorldServiceFactory = defineServiceFactory(HelloWorldServiceFactoryImpl);

The rules for the build.config.mjs and services.ts are exactly the same as with normal services. It is recommended to export the factory using the service name (not the factory name) in services.ts:

some-package/services.ts
export { HelloWorldServiceFactory as HelloWorldService } from "./HelloWorldServiceFactory";

This keeps the configuration identical to a normal service, so that the use of a factory remains a local implementation detail.

Services accept configuration options such as the interfaces they provide and the references they require. These options are provided by editing the build.config.mjs of the containing package, see Package Reference.

Services can depend on other services by referencing an interface name.

sample-app/build.config.mjs
import { defineBuildConfig } from "@open-pioneer/build-support";
export default defineBuildConfig({
services: {
HelloWorldConsumer: {
references: {
// (1)
helloWorldService: "hello.HelloWorldService"
}
// provides: ...
}
}
});

If the reference can be satisfied, the service implementing that interface name will automatically be injected by the framework into the other service’s constructor (in serviceOptions.references). The reference name in the other service’s constructor matches the name of the reference in (1):

sample-app/services.js
export class HelloWorldConsumer {
constructor(serviceOptions) {
const references = serviceOptions.references;
const service = references.helloWorldService;
service.sayHello();
}
}

References are either injected as an object (direct reference, the service that provides the interface) or as an array (when multiple services are requested). See documentation and examples in Package Reference for more details.

If a reference cannot be satisfied, the application will refuse to start with a detailed error message.

Services can also be used directly from React components. Just like services, the UI must declare its dependencies before it can reference any service:

sample-app/build.config.mjs
export default defineBuildConfig({
ui: {
references: ["hello.HelloWorldService"]
}
});

The snippet above tells the system that React components from the package sample-app may reference the service implementing "hello.HelloWorldService". The framework will ensure that that service is started, or that an error is thrown if no service provides that interface.

From within the React component, the useService hook can be used to reference the service:

sample-app/ExampleComponent.jsx
import { useService } from "open-pioneer:react-hooks";
import { useEffect } from "react";
function ExampleComponent() {
const service = useService("hello.HelloWorldService");
useEffect(() => {
service.sayHello();
}, []);
return <div>This components calls service.sayHello after mounting</div>;
}

Reference all services that provide a certain interface

Section titled ā€œReference all services that provide a certain interfaceā€

It is possible to gather all services that provide a certain interface name by specifying all: true in the build.config.mjs. The services are gathered and available as an array in the referencing service or UI.

An example is shown in How To Create a Service Tutorial.

See documentation and examples in Package Reference for more details.

All services receive a serviceOptions parameter in their constructor. This parameter is an object generated by the @open-pioneer/runtime package that provides access to the rest of the system.

The serviceOptions parameter conforms to the interface ServiceOptions exported by @open-pioneer/runtime (see TypeScript integration).

MyService.js
export class MyService {
constructor(serviceOptions) {
console.log(serviceOptions);
}
}

The following values are available as properties of serviceOptions:

  • references: An object containing references to other services (as defined in the build.config.mjs).
  • referencesMeta: Metadata about references, uses the same names as references.
  • properties: An object containing the current package’s properties (default values from build.config.mjs, possibly modified/overwritten by the application).
  • intl: The current package’s intl object to support translations and formatting in the user’s locale (see I18N Format).

Detailed documentation of serviceOptions is available in the API documentation of @open-pioneer/runtime (type ServiceOptions).

Services are started when the application launches, i.e. when the application’s DOM element has been mounted. All services used by an app are started before the UI is rendered for the first time.

A service is considered ā€œusedā€ if it is needed by the UI (see ui.references in build.config.mjs), if it defines an API on the application (provides integration.ApiExtension) or if it provides runtime.AutoStart. The framework will take care to start all those services (and their dependencies) in the correct order.

The service start algorithm for a service S that depends on (references) two other services D1 and D2 is as follows:

  1. Recursively create D1 and D2.
  2. Invoke the constructor of S (passing references to D1 and D2).

Creating the dependencies before S ensures that S always sees a fully initialized version of its dependencies.

Service destruction happens when the application is destroyed, i.e. when the DOM element gets unmounted. It reverses the construction algorithm:

  1. Destroy S if it is no longer being referenced by invoking the destroy() method.
  2. Recursively destroy D1 and D2.

Dependencies are destroyed after their dependents to ensure that their instances are still valid in the destroy() method of S.

NOTE: If a service is created by a ServiceFactory, the service’s own destroy() method is not called automatically. Instead, the optional destroyService() method on the factory is invoked. The factory takes full responsibility for the service lifecycle.

NOTE: Reference cycles between services are forbidden: the app will refuse to launch. This can often be worked around by fixing the design: common functionality needs to move to a shared service. In the future, we could consider implementing lazy references (see Internal Documentation).

Applications can override services from other packages, see Reference.

It is recommended, but not required, to author all services in TypeScript. The runtime provides a few facilities to aid with the implementation.

The Service interface provides types for the service’s lifecycle methods. At the time of this writing this is just destroy(), but more methods may be added in the future. It also accepts an optional type parameter representing the service’s public API. The properties and methods from the type are merged with the service’s lifecycle methods:

ExampleService.ts
import { Service } from "@open-pioneer/runtime";
// The ./api file contains the public interface
import { MyService } from "./api";
// The Impl class implements Service<MyService>, i.e. the optional
// lifecycle methods and the methods and properties defined by `MyService`.
export class MyServiceImpl implements Service<MyService> {
// ... methods
}

The ServiceOptions type is helpful when typing a service’s constructor parameters. It accepts an optional References type parameter with the types of the expected references, for example:

ExampleService.ts
import { Service, ServiceOptions } from "@open-pioneer/runtime";
import { MyService } from "./api";
import { SomeRefType } from "otherPackage/api";
interface References {
// someRef is actually declared in the build.config.mjs,
// but we tell TypeScript which type to expect.
someRef: SomeRefType;
}
export class MyServiceImpl implements Service<MyService> {
constructor(options: ServiceOptions<References>) {
const someRef = options.references.someRef; // of type SomeRefType, no compiler error
}
// ... methods
}

A Service’s TypeScript interface can be associated with an interface name (a string constant), which greatly improves the developer experience when interacting with services.

To link your TypeScript type with its associated interface name, make sure that your service’s interface extends from DeclaredService. The DeclaredService type does not add any required properties or methods to your service; it simply transports the associated interface name at compile time.

somePackage/api.ts
import { DeclaredService } from "@open-pioneer/runtime";
/** All implementations of `"hello.HelloWorldService"` must conform to this interface. */
export interface HelloWorldService extends DeclaredService<"hello.HelloWorldService"> {
/** Says hello. */
sayHello(): void;
}

The declaration above tells the compiler that one must use the interface name "hello.HelloWorldService" whenever one requires the type HelloWorldService.

That information can be used in React components using the interface via useService:

ExampleComponent.tsx
function ExampleComponent() {
// Would report an error via TypeScript if we accidentally mistyped the string argument.
const service = useService<HelloWorldService>("hello.HelloWorldService");
}

NOTE: The mapping from type to interface name may be used in other places in the future, too. For now, you have to make sure to use the correct interface in your service classes on your own.

Package properties are essentially untyped JSON data. However, you can provide helper types and functions in your package to make configuration of your package in an app less error prone.

In your package:

some-logger-package/api.ts
/** Properties accepted by this package */
export interface LoggingProperties {
/** Log level for the shared logger. */
logLevel?: "DEBUG" | "INFO" | "ERROR";
}
/**
* Helper function to construct type safe properties.
*
* Note: this function does nothing and returns the original `properties` object (see usage below).
*/
export function createLoggingProperties(properties: LoggingProperties): LoggingProperties {
return properties;
}

In an app:

your-app/app.ts
import { createCustomElement } from "@open-pioneer/runtime";
import { AppUI } from "./AppUI";
import * as appMetadata from "open-pioneer:app";
import { createLoggingProperties } from "some-logger-package";
const element = createCustomElement({
component: AppUI,
appMetadata,
config: {
properties: {
// Contents of the object in `createLoggingProperties` have auto completion and will be type checked.
"some-logger-package": createLoggingProperties({
logLevel: "INFO"
})
}
}
});
customElements.define("your-app", element);