close

Filter

loading table of contents...

Studio Developer Manual / Version 2404

Table Of Contents

9.36.5.1 Service Agent API

In this section the basics of the Service Agent API are described. Built-in services of CoreMedia Studio Apps and other pre-defined service types are covered later on.

What is a Service?

The requirements for a service are quite simple and stem from the fact that a service can only offer asynchronous methods due to the message communication layer that is used underneath. A service must comply to the type constraint Service<T> (from the @coremedia/service-agent module) where T is the service's interface. This means in particular:

  • A service method can return an rxjsObservable. In that case, the method name must start with the observe_ prefix. Covering the library rxjs for reactive streams is out of scope for this documentation. For more details, see the various online materials, e.g. RxJS Overview or Learn RxJS.
  • If a service method does not return an Observable then is must return a Promise.
  • No other methods are allowed.
  • The service may have additional non-function properties.

How the Promises and Observables are handled across the BroadcastChannel bweteen the different apps is hidden from the developer and taken over by the framework.

There is one additional requirement for services that is not covered by the type constraint Service<T>. As all service handling is carried out via the browser's BroadcastChannel, the arguments and return values of all service methods must be serializable for the BroadcastChannel. Data sent to the channel is serialized using the structured clone algorithm. That means you can send a broad variety of data objects safely without having to serialize them yourself.

Registering Services

As shown in Figure Figure 9.27, “Different Studio Apps Connected Via Service Layer”, each app uses its own instance of the Service Agent. When an app registers a service with its Service Agent, this agent takes care of broadcasting the service to the Service Agents of other apps. The registration of a service is done via the ServiceAgent#registerService() method. The following code fragment shows the example where the Content App registers the ContentFormService whch allows to open content items in a form tab.

getServiceAgent().registerService(
  new ContentFormServiceImpl(),
  createContentFormServiceDescriptor({
    providingApp: studioApps._.getMyAppDescriptor().key
  }),
);

A service is always registered together with a so-called service descriptor. The descriptor describes the service in terms of specific properties where the name property is the only madatory one. Upon querying for services, another service descriptor is given and the Service Agent matches it against the descriptors of registered services. More details follow below.

As stated above, the registered service needs to comply to the type constraint Service<T>. However, this only applies to the service interface. In the case above for example, ContentFormServiceImpl has methods that do not comply to the Service<T> constraint. But the class implements the interface ContentFormService which does comply to the constraint. There are two possibilities to register ContentFormServiceImpl with the Service Agent. Either it needs to be cast / assigned to its interface ContentFormService or the service descriptor is used like in the example. The service descriptor for the ContentFormService is defined like this:

import { serviceDescriptorFactory, ServiceDescriptorWithProps }
  from "@coremedia/service-agent";
import { WithProvidingApp } from "@coremedia/studio-client.app-context-models";

export interface ContentFormServiceProps extends WithProvidingApp {}

export function createContentFormServiceDescriptor(
  props: ContentFormServiceProps = {},
): ServiceDescriptorWithProps<ContentFormService, ContentFormServiceProps> {
  return serviceDescriptorFactory<ContentFormService, ContentFormServiceProps>({
    ...props,
    name: "contentFormService",
  });
}

To define service descriptors, it is recommended to use the serviceDescriptorFactory function from the @coremedia/service-agent module. It returns a ServiceDescriptorWithProps which is typed for specific services and descriptor properties. In this case, the descriptor is for services of type ContentFormService and their additional properties of type ContentFormServiceProps (which currently just extends WithProvidingApp). So using this service descriptor for ServiceAgent#registerService() fulfils two purposes: (1) It defines the describing properties of the registered service and (2) it enforces that the registered service is of type ContentFormService which complies to the Service<T>.

Registering Service Runners

Besides registering service it is also possible to register a service runner. A service runner is a function that is called when a service is requested but no such service is running at the moment. So it can be used to only launch a service once it is requested. It is done via the ServiceAgent#registerServiceRunner() method. It also takes a service descriptor as its first argument and the runner function as its second argument.

An important example was already mentioned in Section Section 9.36.4, “App Manifest and Apps Menu Entries”. The services of an app manifest's cmServices are required to be set up upon app initialization. The Studio Apps framework automatically sets up a service runner for each of these services. In this case, the runner simply starts the app if it is not yet running. For example, if the Content App is not yet running in a browser window, there is no running service ContentFormService in place. But because the ContentFormService (more specifically, its descriptor) is part of the Content App's cmServices manifest entry, a runner for this service is already in place.

Un-Registering Services and Service Runners

To un-register a service or a service runner, the methods ServiceAgent#unregisterServices() and ServiceAgent#unregisterServiceRunners() are used. Both methods take a ServiceDescriptorWithProps as their argument. All services whose descriptor matches the given descriptor are un-registered. A service descriptor match is carried out based on lodash's isMatch() method, see lodash documentation. The descriptor argument of the unregister method is taken as the second argument of isMatch() and the descriptor of the registered services / runners are taken as the first argument. Consequently, the call ServiceAgent#unregisterServices({name: "someService", prop: "myValue"}) would unregister all services with the "name" value "someService" and the "prop" value "myValue", no matter what other properties their descriptors have.

Querying / Requesting Services

The previous paragraphs cover the topic of registering services and service runners and using service descriptors for this purpose. Now the topic of querying / requesting services is covered.

There are two ways to request services.

  • ServiceAgent#getServices(): Returns all services that are currently known.
  • ServiceAgent#observeServices(): Returns an rxjsObservable of all known services. Once the known services change, the Observable emits a new value. The Observable also always emits the current value on subscribe.

In both cases, a singular version exists (ServiceAgent#getService() and ServiceAgent#observeService())) where the first matching service is returned.

The first argument that both methods take is a service descriptor. The descriptor is used to match against the descriptors of the registered services. The matching is done based on lodash's isMatch() method. The descriptor argument of the request method is taken as the second argument of isMatch() and the descriptors of the registered services are taken as the first argument. Consequently, the call ServiceAgent#getServices({name: "someService", prop: "myValue"}) would return all services with the "name" value "someService" and the "prop" value "myValue", no matter what other properties their descriptors have. Just as for registering services, it is also recommended for requesting services to use ServiceDescriptorsWithProps as arguments. For example, the code for registering the ContentFormService in the Content App was shown above. The My-Edited-Contents App uses this service to open a content item from the edited contents list when clicked. The code looks as follows (the fetch() of the code fragment is explained below).

<IconButton color={"primary"} onClick={async () => {
  const contentFormService = await getServiceAgent()
    .getService(createContentFormServiceDescriptor())?
    .fetch();
  contentFormService && await contentFormService.openContentForm(params.row.id);
}}>

Because the ServiceDescriptorWithProps<ContentFormService, ContentFormServiceProps> returned from createContentFormServiceDescriptor() is used, it is possible to continue in a type-safe way: Typescript knows that the returned service is of type ContentFormService.

In addition to the service descriptor, both service request methods take an optional second argument of type ServiceRetrievalOptions. Currently, there is only one option, namely ServiceRetrievalOptions#state. It allows to specify whether only running services shall be retrieved or also runnable services. The latter are services that are not yet running but a service runner exists, see above. By default, all services are retrieved, running and runnable ones.

Using Services

The fact that both running and runnable services can be retrieved is also the reason that no services are directly returned by the two request methods. Instead, a ServiceHandler is returned for each service in the result. A handler provides the descriptor for its service via ServiceHandler#descriptor and the state of its service ("running" or "runnable") via ServiceHandler#state. An important property of a ServiceHandler#descriptor is provider which holds the ServiceAgent#getOrigin() of the service agent where the service was initially registered.

Above all, a service handler offers the ServiceHandler#fetch() method. This method returns a Promise for the service that only resolves once the service is running. For a running service, this is straightforward. But for a runnable service, the service runner needs to be called first and then it is waited until the service is actually running.

One catch with the ServiceHandler#fetch() method is that it always tries to immediately provide a service. Either it is already running or a service runner is attempted. If one simply wants to wait for a service to become available, this can be achieved in multiple ways. For example, in the My-Edited-Contents App, the "Opened" column from Figure Figure 9.28, “The My-Edited-Contents Demo App” shows which content items are opened in the Content App. To determine this, once again the ContentFormService is used. But in this case, it is just waited for the service to become available. If it is not running, the column is simply hidden. The code looks as follows:

import {filter, firstValueFrom } from "rxjs";

const serviceHandler = await firstValueFrom(
  getServiceAgent()
    .observeService(createContentFormServiceDescriptor(), {
      state: "running"
    })
    .pipe(filter(Boolean))
);
const contentFormService = await serviceHandler.fetch();
contentFormService.observe_openedContents().subscribe({
  ...
});

The combination of ServiceAgent#observeService(), state "running" and firstValueFrom lets the code wait for a running service and only then continue. For ReactJs apps like the My-Edited-Contents App, it is helpful to use a generic custom hook. The following useService() hook is part of the demo app code and provides the requested service or null if no such service is running.

          
import { switchMap } from "rxjs";
import {getServiceAgent, ServiceDescriptorWithProps, Service}
  from "@coremedia/service-agent";

export function useService<T extends Service<T>>(
  serviceDesc: ServiceDescriptorWithProps<T>
): T | null {
  const [service, setService] = useState<T | null>(null);

  useEffect(() => {
    const subscription = getServiceAgent()
      .observeService(serviceDesc, {state: "running"}).pipe(
        switchMap((services) => services.length > 0
          ? services[0].fetch()
          : Promise.resolve(null))
      )
      .subscribe({
        next: (service) =>
          setService((currentService: T | null) => {
            if (currentService && !service) {
              return null;
            }
            if (!currentService && service) {
              return service;
            }
            return currentService;
          }),
        error: () => setService(null),
        complete: () => setService(null),
      });
    return () => subscription.unsubscribe();
  }, [serviceDesc]);

  return service;
}

        

A further possibility to just wait for a service is using ServiceAgent#executeServiceMethod(), see below.

As an advanced option, the ServiceHandler#fetch() method takes an optional argument of type ServiceRunningOptions. The option ServiceRunningOptions#reconnect allows to specify how to proceed if the service becomes unavailable while still in use. This is especially interesting for Observable service methods. If the value is set to "off", the service is simply no longer usable. Subscriptions to Observables from service methods are automatically terminated with an error. If the value is set to "wait" (which is the default), it is waited for a service to become available which has a service descriptor matching the descriptor of the initial service. If the value is set to "launch", it is attempted to launch a service (via a service runner) with a descriptor matching the descriptor of the initial service if none is already available.

Shorthand Utility executeServiceMethod()

Instead of first retrieving ServiceHandlers, then calling fetch() on them and finally executing a service method, the ServiceAgent provides a shorthand utility. The ServiceAgent#executeServiceMethod() allows to execute a service method without the need to retrieve a ServiceHandler beforehand. The service method is identified by a given ServiceMethodDescriptorWithProps (which includes a ServiceDescriptorWithProps and additional options to identify the specific method). ServiceAgent#executeServiceMethod() is only successful if a service matching the descriptor is running or can be run. The option ServiceMethodDescriptorWithProps#serviceRunningOpts#reconnect does not only decide how to proceed in case of a connection loss but also how to initially proceed if a matching service is not available. In case of value "launch", a service is launched if missing (via a service runner). In case of value "wait", the method waits for a service to become available. In case of value "off", an error is thrown. The default value is "launch". As an example, the following code once again shows how to wait for the ContentFormService to become available and then observe content items opened in form tabs.

getServiceAgent().executeServiceMethod({
  serviceDescriptor: createContentFormServiceDescriptor(),
  method: "observe_openedContents",
  serviceRunningOpts: {reconnect: "wait"}
}).subscribe(...);

Search Results

Table Of Contents
warning

Your Internet Explorer is no longer supported.

Please use Mozilla Firefox, Google Chrome, or Microsoft Edge.