A services manager for managing services used in order to generate SDK packages for Viewer, app settings and dashboard pages.
npm install @wix/services-managerbash
yarn dev
`
This 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:
!config-diagram.pngAll 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 hereAPI
$3
ServiceAPI and defineService are two utility types and functions that are used to define and extract the API of a service.
The ServiceAPI type extracts the API interface from a given ServiceDefinition. It represents the actual methods and properties that a service exposes.#### Example:
`typescript
import { defineService, ReadableSignal } from '@wix/services-manager';interface MyServiceAPI {
methodA: (input: string) => Promise;
methodB: () => Promise;
signals: {
mySignal: ReadableSignal;
};
}
const myServiceDefinition = defineService('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.$3
The ServiceFactory is a function that creates a service instance. It is used to create a service instance when the service is requested.
`typescript
import { myServiceDefinition } from './my-service-definition';
import { SignalsServiceDefinition } from '@wix/services-definitions/core-services/signals';const myServiceFactory: ServiceFactory = 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.$3
The ServiceManager class is the main class that is used to manage services. It is responsible for creating, registering, and managing services.
#### Example
`typescript
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 - Returns the service instance for the given service definition, uses the ServiceFactory in order to create the service if it is not already created.
- hasService(serviceDefinition: ServiceDefinition): boolean - Returns true if the service is registered.
- addService - Registers a new service.
- addServices(servicesRegistrar: ServicesRegistrar): void - Registers and initializes multiple services at once from a ServicesRegistrar (as created by createServicesMap).
- 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.$3
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
`typescript
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: 8
`
The 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.$3
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
`typescript
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 new RemoteServicesManager instance.
- servicesManager: The ServiceManager instance that would be wrapped by the remote manager.
- messageFrame: The frame that the remote manager is communicating with (self in most cases).
- connectRemoteWorker/connectRemoteIframe - Connects to a remote worker/iframe and creates a proxy for the remote services manager.
- servicesManager: The ServiceManager instance 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:
!handshake.png
The signals' replication and syncing is demonstrated in the following diagram:
!signals-replication.png#### 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:
`typescript
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);
});
``