@wix/services-manager
v1.0.0
Published
A services manager for managing services used in order to generate SDK packages for Viewer, app settings and dashboard pages.
Maintainers
Keywords
Readme
Services Manager
A services manager for managing services used in order to generate SDK packages for Viewer, app settings and dashboard pages.
Usage Example
yarn devThis will start a demo application that uses the services manager to manage services.
It includes an iframe and a worker which are running services managers which communicate using the a remote services manager (using Comlink).
In the demo application, you can see how the services manager can be used to manage services and how services can be shared between different contexts (iframe, worker, etc), and how the communication is done using the remote services manager.
The application logic is demonstrated in the following diagram:

All the code for the usage example is located in the test-data folder.
Design Document
The design document for the services manager can be found here
API
ServiceAPI and defineService
ServiceAPI<T> and defineService<TAPI, TConfig> are two utility types and functions that are used to define and extract the API of a service.
The ServiceAPI<T> type extracts the API interface from a given ServiceDefinition. It represents the actual methods and properties that a service exposes.
Example:
import { defineService, ReadableSignal } from '@wix/services-manager';
interface MyServiceAPI {
methodA: (input: string) => Promise<number>;
methodB: () => Promise<string>;
signals: {
mySignal: ReadableSignal<number>;
};
}
const myServiceDefinition = defineService<MyServiceAPI, {}>('myService');The defineService function creates a ServiceDefinition. A ServiceDefinition is a unique identifier (string) that also carries type information about the service's API and configuration.
ServiceFactory
The ServiceFactory is a function that creates a service instance. It is used to create a service instance when the service is requested.
import { myServiceDefinition } from './my-service-definition';
import { SignalsServiceDefinition } from '@wix/services-definitions/core-services/signals';
const myServiceFactory: ServiceFactory<MyServiceAPI, {}> = async ({ config, getService }) => {
const signalsService = getService(SignalsServiceDefinition);
const mySignal = signalsService.signal(0);
return {
methodA: async (input: string) => {
return input.length;
},
methodB: async () => {
return 'hello';
},
signals: {
mySignal,
},
};
};The factory function receives an object with the service's configuration and a getService function that can be used to get other services.
ServiceManager
The ServiceManager class is the main class that is used to manage services. It is responsible for creating, registering, and managing services.
Example
import { createServicesManager, createServicesMap } from '@wix/services-manager';
import { myServiceDefinition, myServiceFactory } from './my-service-definition';
const manager = createServicesManager(
createServicesMap().addService(myServiceDefinition, myServiceFactory, {}),
);The createServicesManager function creates a new ServiceManager instance with the given services map.
It can also accept a second argument, SignalsRegistry, which is used to manage signals.
In most cases, you can use the default SignalsRegistry implementation (create new), but in case for some reason you have more than one services manager in the same frame, you can pass the same SignalsRegistry instance to both of them.
Methods
getService<T>(serviceDefinition: ServiceDefinition<T>): T- Returns the service instance for the given service definition, uses theServiceFactoryin order to create the service if it is not already created.hasService(serviceDefinition: ServiceDefinition): boolean- Returns true if the service is registered.addService<T, TConfig>(serviceDefinition: ServiceDefinition<T>, factory: ServiceFactory<T, TConfig>, config: TConfig): void- Registers a new service.addServices(servicesRegistrar: ServicesRegistrar): void- Registers and initializes multiple services at once from aServicesRegistrar(as created bycreateServicesMap).getSignalsRegistry(): SignalsRegistry- Returns the signals registry instance.
Core Services
Core services are services that are built-in and are always available in the services manager.
This includes the SignalsService which is used to manage signals.
Signals
Signals are a way to share state between services. They are used to notify other services about changes or events.
This is using preact-signals under the hood.
This allows an easy pub/sub mechanism.
Signals are created using a dedicated core service - SignalsService.
Example
import { SignalsServiceDefinition } from '@wix/services-definitions/core-services/signals';
const signalsService = manager.getService(SignalsServiceDefinition);
const mySignal = signalsService.signal(0);
const myComputedSignal = signalsService.computed(signalsService.computed(() => mySignal.get() + 3));
mySignal.subscribe((value) => {
console.log('mySignal value changed:', value);
});
myComputedSignal.subscribe((value) => {
console.log('myComputedSignal value changed:', value);
});
mySignal.set(5);
// Output:
// mySignal value changed: 5
// myComputedSignal value changed: 8The signal method creates a new read/write signal with the given initial value.
The computed method creates a new read-only signal that is computed from other signals.
Remote Services Manager
The RemoteServicesManager class is used to communicate with services in a different context (iframe, worker, etc).
It is used to create a proxy for a remote services manager and to create a proxy for a remote service.
The remote manager is wrapping a ServiceManager instance and used in order to connect to a remote services manager.
Example
import { connectRemoteWorker, createRemoteServicesManager } from '@wix/services-manager/remote-helpers';
import { createServicesManager, createServicesMap } from '@wix/services-manager';
import { myServiceDefinition, myServiceFactory } from './my-service-definition';
// in worker.js
const managerInWorker = createServicesManager(createServicesMap());
// create remote mamanger and wait for connection (trigger a ready message)
const remoteManager = createRemoteServicesManager({
servicesManager: managerInWorker,
messageFrame: self,
});
remoteManager.awaitConnectionFromMain().then(() => {
console.log('Connected to main');
// the service was added as a proxied service during the connection
const myService = managerInWorker.getService(myServiceDefinition);
// all proxied methods are async
myService.methodA('hello').then((result) => {
console.log('methodA result:', result);
});
});
// in main.js
const manager = createServicesManager(
createServicesMap().addService(myServiceDefinition, myServiceFactory, {}),
);
const worker = new Worker('worker.js');
const remoteWorkerManager = await connectRemoteWorker(manager, worker, {
// wait until the worker sent a message that it is ready
awaitInitialConnection: true,
// define the services that are exposed to the worker and should be proxied
remoteServices: [
myServiceDefinition,
]
});API
createRemoteServicesManager- Creates a newRemoteServicesManagerinstance.servicesManager: TheServiceManagerinstance that would be wrapped by the remote manager.messageFrame: The frame that the remote manager is communicating with (selfin most cases).
connectRemoteWorker/connectRemoteIframe- Connects to a remote worker/iframe and creates a proxy for the remote services manager.servicesManager: TheServiceManagerinstance that would be wrapped by the remote manager.worker(for connectRemoteWorker) /iframe(for connectRemoteIframe): The worker instance/iframe that the remote manager is communicating with.options(optional): Options for the connection.awaitInitialConnection(optional): If true, the function will wait for the worker/iframe to send a ready message before resolving.retryOptions(optional): { timeout?: number; interval?: number } - Options for retrying the connection.timeout(optional): The timeout for the connection.interval(optional): The interval between retries - if the interval is longer than the timeout, the connection will only be attempted once (default).
remoteServices(optional): An array of service definitions that should be proxied.
The Remote Services Manager handshake
When creating a remote manager instance in a worker or iframe, it immediately sends a message to the main thread to establish a connection.
The main thread uses connectRemoteWorker or connectRemoteIframe to connect to the remote manager, it can either attempt connecting immediately (eagerly with retries) or wait for the worker/iframe to send a ready message.
As soon as the connection is established, the main services manager sends a message to the remote manager with the services that should be proxied, and which signals should be replicated and synced.
The handshake sequence is demonstrated in the following diagram:
The signals' replication and syncing is demonstrated in the following diagram:

Security Considerations
The solution does provide isolation of the services running in the worker/iframe from the main thread, but it does not provide full isolation. The services manager can determine if an app can access a service, but it cannot prevent app code from trying to access global objects and manipulate code running in the same context. When using the remote services manager, it is possible to control what is being synchronized and proxied, so if a real isolation is needed, it is recommended to only proxy the services that are needed and to avoid exposing global objects.
Signals
Signals are replicated and synced between the main thread and the worker/iframe, ReadOnlySignals are only aimed to define a signal which is computed from other signals and not to define access control, so a service cannot prevent another service running in the same context from updating its signals. Therefore, it is recommended to avoid using signals for sensitive data, and to use them only for state management and notifications.
Running Services in a Worker
In order to create a proper sandboxed environment, it is recommended to run untrusted code in an iFrame or a worker. The services manager allows such approach.
The services manager can be used to run services in a worker. This is done by creating a services manager in the worker which runs actual service instances and connecting to it from the main thread.
This is useful for running heavy services in a worker in order to avoid blocking the main thread, or when the services are provided by a 3rd party and you want to run them in a sandboxed environment.
This is achieved using the RemoteServicesManager and connectRemoteWorker + createServiceProxy functions.
Usage examples:
import { connectRemoteWorker, createRemoteServicesManager, createServiceProxy } from '@wix/services-manager/remote-helpers';
import { createServicesManager, createServicesMap } from '@wix/services-manager';
import { myServiceDefinition, myServiceFactory } from './my-service-definition';
// in worker.js
const managerInWorker = createServicesManager(
// the service factory is in the worker
createServicesMap().addService(myServiceDefinition, myServiceFactory, {}),
);
// create remote mamanger and wait for connection (trigger a ready message)
const remoteManager = createRemoteServicesManager({
servicesManager: managerInWorker,
messageFrame: self,
});
remoteManager.awaitConnectionFromMain().then(() => {
console.log('Connected to main');
});
// in main.js
const manager = createServicesManager(createServicesMap());
const worker = new Worker('worker.js');
const remoteWorkerManager = await connectRemoteWorker(manager, worker, {
// wait until the worker sent a message that it is ready
awaitInitialConnection: true,
// no services are proxied in worker
remoteServices: []
});
manager.addService(myServiceDefinition, () => createServiceProxy(
// The service definition is the same as in the worker
myServiceDefinition,
// The remote manager connected to the worker which is running the service
remoteWorkerManager,
// The `SignalsRegistry` used for the signals of the service to proxy
manager.getSignalsRegistry(),
));
const myService = manager.getService(myServiceDefinition);
// all proxied methods are async
myService.methodA('hello').then((result) => {
console.log('methodA result:', result);
});Since a service can be used in multiple contexts (iframe, worker, etc), the service manager can be used to manage services in different contexts and to communicate between them using the remote services manager.
This also allows services in one worker to consume services from another worker (via the services manager running in the main thread).
In the diagram above, app 2 is running in a worker and it exposes a service which is proxied in the main thread.
App 1 is running in a worker and uses its parent (the services manager in the main thread) to get the service from app 2.
From its point of view, it is using the service directly, but the service is actually running in a different worker.
