@wix/services-manager 0.1.4 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Readme.md +250 -0
- package/build/core-services/signals/definition.d.ts +3 -13
- package/build/core-services/signals/definition.js +1 -2
- package/build/core-services/signals/registry.d.ts +27 -0
- package/build/core-services/signals/registry.js +115 -0
- package/build/core-services/signals/service.d.ts +19 -8
- package/build/core-services/signals/service.js +4 -17
- package/build/core-services/signals/transfer-helpers.d.ts +2 -0
- package/build/core-services/signals/transfer-helpers.js +64 -0
- package/build/helpers.d.ts +1 -6
- package/build/helpers.js +1 -11
- package/build/remote-helpers.d.ts +53 -0
- package/build/remote-helpers.js +225 -0
- package/build/services-manager.d.ts +8 -8
- package/build/services-manager.js +32 -15
- package/build/transfer-handlers/init-handlers.d.ts +2 -0
- package/build/transfer-handlers/init-handlers.js +79 -0
- package/build/types.d.ts +10 -17
- package/cjs/build/core-services/signals/definition.d.ts +3 -13
- package/cjs/build/core-services/signals/definition.js +2 -2
- package/cjs/build/core-services/signals/registry.d.ts +27 -0
- package/cjs/build/core-services/signals/registry.js +121 -0
- package/cjs/build/core-services/signals/service.d.ts +19 -8
- package/cjs/build/core-services/signals/service.js +3 -16
- package/cjs/build/core-services/signals/transfer-helpers.d.ts +2 -0
- package/cjs/build/core-services/signals/transfer-helpers.js +101 -0
- package/cjs/build/helpers.d.ts +1 -6
- package/cjs/build/helpers.js +4 -13
- package/cjs/build/remote-helpers.d.ts +53 -0
- package/cjs/build/remote-helpers.js +265 -0
- package/cjs/build/services-manager.d.ts +8 -8
- package/cjs/build/services-manager.js +34 -15
- package/cjs/build/transfer-handlers/init-handlers.d.ts +2 -0
- package/cjs/build/transfer-handlers/init-handlers.js +115 -0
- package/cjs/build/types.d.ts +10 -17
- package/package.json +18 -5
- package/remote-helpers/package.json +3 -0
package/Readme.md
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# Services Manager
|
|
2
|
+
A services manager for managing services used in order to generate SDK packages for Viewer, app settings and dashboard pages.
|
|
3
|
+
|
|
4
|
+
## Usage Example
|
|
5
|
+
```bash
|
|
6
|
+
yarn dev
|
|
7
|
+
```
|
|
8
|
+
This will start a demo application that uses the services manager to manage services.
|
|
9
|
+
It includes an iframe and a worker which are running services managers which communicate using the a remote services manager (using [Comlink](https://github.com/GoogleChromeLabs/comlink)).
|
|
10
|
+
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.
|
|
11
|
+
The application logic is demonstrated in the following diagram:
|
|
12
|
+

|
|
13
|
+
|
|
14
|
+
All the code for the usage example is located in the [test-data](./src/__tests__/test-data) folder.
|
|
15
|
+
|
|
16
|
+
## Design Document
|
|
17
|
+
The design document for the services manager can be found [here](https://docs.google.com/document/d/1hPcPvQJjEVbQ2Xx3gBxwp6NpkKfCItu1__qFAfdcudc/edit?usp=sharing)
|
|
18
|
+
|
|
19
|
+
## API
|
|
20
|
+
### ServiceAPI and defineService
|
|
21
|
+
`ServiceAPI<T>` and `defineService<TAPI, TConfig>` are two utility types and functions that are used to define and extract the API of a service.
|
|
22
|
+
The `ServiceAPI<T>` type extracts the API interface from a given `ServiceDefinition`. It represents the actual methods and properties that a service exposes.
|
|
23
|
+
|
|
24
|
+
#### Example:
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
import { defineService, ReadableSignal } from '@wix/services-manager';
|
|
28
|
+
|
|
29
|
+
interface MyServiceAPI {
|
|
30
|
+
methodA: (input: string) => Promise<number>;
|
|
31
|
+
methodB: () => Promise<string>;
|
|
32
|
+
signals: {
|
|
33
|
+
mySignal: ReadableSignal<number>;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const myServiceDefinition = defineService<MyServiceAPI, {}>('myService');
|
|
38
|
+
```
|
|
39
|
+
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.
|
|
40
|
+
|
|
41
|
+
### ServiceFactory
|
|
42
|
+
The `ServiceFactory` is a function that creates a service instance. It is used to create a service instance when the service is requested.
|
|
43
|
+
```typescript
|
|
44
|
+
import { myServiceDefinition } from './my-service-definition';
|
|
45
|
+
import { SignalsServiceDefinition } from '@wix/services-definitions/core-services/signals';
|
|
46
|
+
|
|
47
|
+
const myServiceFactory: ServiceFactory<MyServiceAPI, {}> = async ({ config, getService }) => {
|
|
48
|
+
const signalsService = getService(SignalsServiceDefinition);
|
|
49
|
+
const mySignal = signalsService.signal(0);
|
|
50
|
+
return {
|
|
51
|
+
methodA: async (input: string) => {
|
|
52
|
+
return input.length;
|
|
53
|
+
},
|
|
54
|
+
methodB: async () => {
|
|
55
|
+
return 'hello';
|
|
56
|
+
},
|
|
57
|
+
signals: {
|
|
58
|
+
mySignal,
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
```
|
|
63
|
+
The factory function receives an object with the service's configuration and a `getService` function that can be used to get other services.
|
|
64
|
+
|
|
65
|
+
### ServiceManager
|
|
66
|
+
The `ServiceManager` class is the main class that is used to manage services. It is responsible for creating, registering, and managing services.
|
|
67
|
+
#### Example
|
|
68
|
+
```typescript
|
|
69
|
+
import { createServicesManager, createServicesMap } from '@wix/services-manager';
|
|
70
|
+
import { myServiceDefinition, myServiceFactory } from './my-service-definition';
|
|
71
|
+
|
|
72
|
+
const manager = createServicesManager(
|
|
73
|
+
createServicesMap().addService(myServiceDefinition, myServiceFactory, {}),
|
|
74
|
+
);
|
|
75
|
+
```
|
|
76
|
+
The `createServicesManager` function creates a new `ServiceManager` instance with the given services map.
|
|
77
|
+
It can also accept a second argument, `SignalsRegistry`, which is used to manage signals.
|
|
78
|
+
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.
|
|
79
|
+
#### Methods
|
|
80
|
+
- `getService<T>(serviceDefinition: ServiceDefinition<T>): T` - Returns the service instance for the given service definition, uses the `ServiceFactory` in order to create the service if it is not already created.
|
|
81
|
+
- `hasService(serviceDefinition: ServiceDefinition): boolean` - Returns true if the service is registered.
|
|
82
|
+
- `addService<T, TConfig>(serviceDefinition: ServiceDefinition<T>, factory: ServiceFactory<T, TConfig>, config: TConfig): void` - Registers a new service.
|
|
83
|
+
- `getSignalsRegistry(): SignalsRegistry` - Returns the signals registry instance.
|
|
84
|
+
|
|
85
|
+
#### Core Services
|
|
86
|
+
Core services are services that are built-in and are always available in the services manager.
|
|
87
|
+
This includes the `SignalsService` which is used to manage signals.
|
|
88
|
+
|
|
89
|
+
### Signals
|
|
90
|
+
Signals are a way to share state between services. They are used to notify other services about changes or events.
|
|
91
|
+
This is using [preact-signals](https://preactjs.com/guide/v10/signals/) under the hood.
|
|
92
|
+
This allows an easy pub/sub mechanism.
|
|
93
|
+
Signals are created using a dedicated core service - `SignalsService`.
|
|
94
|
+
#### Example
|
|
95
|
+
```typescript
|
|
96
|
+
import { SignalsServiceDefinition } from '@wix/services-definitions/core-services/signals';
|
|
97
|
+
|
|
98
|
+
const signalsService = manager.getService(SignalsServiceDefinition);
|
|
99
|
+
const mySignal = signalsService.signal(0);
|
|
100
|
+
const myComputedSignal = signalsService.computed(signalsService.computed(() => mySignal.get() + 3));
|
|
101
|
+
|
|
102
|
+
mySignal.subscribe((value) => {
|
|
103
|
+
console.log('mySignal value changed:', value);
|
|
104
|
+
});
|
|
105
|
+
myComputedSignal.subscribe((value) => {
|
|
106
|
+
console.log('myComputedSignal value changed:', value);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
mySignal.set(5);
|
|
110
|
+
// Output:
|
|
111
|
+
// mySignal value changed: 5
|
|
112
|
+
// myComputedSignal value changed: 8
|
|
113
|
+
```
|
|
114
|
+
The `signal` method creates a new read/write signal with the given initial value.
|
|
115
|
+
The `computed` method creates a new read-only signal that is computed from other signals.
|
|
116
|
+
|
|
117
|
+
### Remote Services Manager
|
|
118
|
+
The `RemoteServicesManager` class is used to communicate with services in a different context (iframe, worker, etc).
|
|
119
|
+
It is used to create a proxy for a remote services manager and to create a proxy for a remote service.
|
|
120
|
+
The remote manager is wrapping a `ServiceManager` instance and used in order to connect to a remote services manager.
|
|
121
|
+
#### Example
|
|
122
|
+
```typescript
|
|
123
|
+
import { connectRemoteWorker, createRemoteServicesManager } from '@wix/services-manager/remote-helpers';
|
|
124
|
+
import { createServicesManager, createServicesMap } from '@wix/services-manager';
|
|
125
|
+
import { myServiceDefinition, myServiceFactory } from './my-service-definition';
|
|
126
|
+
|
|
127
|
+
// in worker.js
|
|
128
|
+
const managerInWorker = createServicesManager(createServicesMap());
|
|
129
|
+
|
|
130
|
+
// create remote mamanger and wait for connection (trigger a ready message)
|
|
131
|
+
const remoteManager = createRemoteServicesManager({
|
|
132
|
+
servicesManager: managerInWorker,
|
|
133
|
+
messageFrame: self,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
remoteManager.awaitConnectionFromMain().then(() => {
|
|
137
|
+
console.log('Connected to main');
|
|
138
|
+
// the service was added as a proxied service during the connection
|
|
139
|
+
const myService = managerInWorker.getService(myServiceDefinition);
|
|
140
|
+
// all proxied methods are async
|
|
141
|
+
myService.methodA('hello').then((result) => {
|
|
142
|
+
console.log('methodA result:', result);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// in main.js
|
|
147
|
+
const manager = createServicesManager(
|
|
148
|
+
createServicesMap().addService(myServiceDefinition, myServiceFactory, {}),
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
const worker = new Worker('worker.js');
|
|
152
|
+
const remoteWorkerManager = await connectRemoteWorker(manager, worker, {
|
|
153
|
+
// wait until the worker sent a message that it is ready
|
|
154
|
+
awaitInitialConnection: true,
|
|
155
|
+
// define the services that are exposed to the worker and should be proxied
|
|
156
|
+
remoteServices: [
|
|
157
|
+
myServiceDefinition,
|
|
158
|
+
]
|
|
159
|
+
});
|
|
160
|
+
```
|
|
161
|
+
#### API
|
|
162
|
+
- `createRemoteServicesManager` - Creates a new `RemoteServicesManager` instance.
|
|
163
|
+
- `servicesManager`: The `ServiceManager` instance that would be wrapped by the remote manager.
|
|
164
|
+
- `messageFrame`: The frame that the remote manager is communicating with (`self` in most cases).
|
|
165
|
+
- `connectRemoteWorker/connectRemoteIframe` - Connects to a remote worker/iframe and creates a proxy for the remote services manager.
|
|
166
|
+
- `servicesManager`: The `ServiceManager` instance that would be wrapped by the remote manager.
|
|
167
|
+
- `worker` (for connectRemoteWorker) /`iframe` (for connectRemoteIframe): The worker instance/iframe that the remote manager is communicating with.
|
|
168
|
+
- `options` (optional): Options for the connection.
|
|
169
|
+
- `awaitInitialConnection`(optional): If true, the function will wait for the worker/iframe to send a ready message before resolving.
|
|
170
|
+
- `retryOptions` (optional): { timeout?: number; interval?: number } - Options for retrying the connection.
|
|
171
|
+
- `timeout` (optional): The timeout for the connection.
|
|
172
|
+
- `interval` (optional): The interval between retries - if the interval is longer than the timeout, the connection will only be attempted once (default).
|
|
173
|
+
- `remoteServices` (optional): An array of service definitions that should be proxied.
|
|
174
|
+
|
|
175
|
+
#### The Remote Services Manager handshake
|
|
176
|
+
When creating a remote manager instance in a worker or iframe, it immediately sends a message to the main thread to establish a connection.
|
|
177
|
+
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.
|
|
178
|
+
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.
|
|
179
|
+
The handshake sequence is demonstrated in the following diagram:
|
|
180
|
+

|
|
181
|
+
The signals' replication and syncing is demonstrated in the following diagram:
|
|
182
|
+

|
|
183
|
+
|
|
184
|
+
#### Security Considerations
|
|
185
|
+
The solution does provide isolation of the services running in the worker/iframe from the main thread, but it does not provide full isolation.
|
|
186
|
+
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.
|
|
187
|
+
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.
|
|
188
|
+
##### Signals
|
|
189
|
+
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.
|
|
190
|
+
Therefore, it is recommended to avoid using signals for sensitive data, and to use them only for state management and notifications.
|
|
191
|
+
|
|
192
|
+
##### Running Services in a Worker
|
|
193
|
+
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.
|
|
194
|
+
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.
|
|
195
|
+
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.
|
|
196
|
+
This is achieved using the `RemoteServicesManager` and `connectRemoteWorker` + `createServiceProxy` functions.
|
|
197
|
+
Usage examples:
|
|
198
|
+
```typescript
|
|
199
|
+
import { connectRemoteWorker, createRemoteServicesManager, createServiceProxy } from '@wix/services-manager/remote-helpers';
|
|
200
|
+
import { createServicesManager, createServicesMap } from '@wix/services-manager';
|
|
201
|
+
import { myServiceDefinition, myServiceFactory } from './my-service-definition';
|
|
202
|
+
|
|
203
|
+
// in worker.js
|
|
204
|
+
const managerInWorker = createServicesManager(
|
|
205
|
+
// the service factory is in the worker
|
|
206
|
+
createServicesMap().addService(myServiceDefinition, myServiceFactory, {}),
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
// create remote mamanger and wait for connection (trigger a ready message)
|
|
210
|
+
const remoteManager = createRemoteServicesManager({
|
|
211
|
+
servicesManager: managerInWorker,
|
|
212
|
+
messageFrame: self,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
remoteManager.awaitConnectionFromMain().then(() => {
|
|
216
|
+
console.log('Connected to main');
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// in main.js
|
|
220
|
+
const manager = createServicesManager(createServicesMap());
|
|
221
|
+
|
|
222
|
+
const worker = new Worker('worker.js');
|
|
223
|
+
const remoteWorkerManager = await connectRemoteWorker(manager, worker, {
|
|
224
|
+
// wait until the worker sent a message that it is ready
|
|
225
|
+
awaitInitialConnection: true,
|
|
226
|
+
// no services are proxied in worker
|
|
227
|
+
remoteServices: []
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
manager.addService(myServiceDefinition, () => createServiceProxy(
|
|
231
|
+
// The service definition is the same as in the worker
|
|
232
|
+
myServiceDefinition,
|
|
233
|
+
// The remote manager connected to the worker which is running the service
|
|
234
|
+
remoteWorkerManager,
|
|
235
|
+
// The `SignalsRegistry` used for the signals of the service to proxy
|
|
236
|
+
manager.getSignalsRegistry(),
|
|
237
|
+
));
|
|
238
|
+
|
|
239
|
+
const myService = manager.getService(myServiceDefinition);
|
|
240
|
+
// all proxied methods are async
|
|
241
|
+
myService.methodA('hello').then((result) => {
|
|
242
|
+
console.log('methodA result:', result);
|
|
243
|
+
});
|
|
244
|
+
```
|
|
245
|
+
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.
|
|
246
|
+
This also allows services in one worker to consume services from another worker (via the services manager running in the main thread).
|
|
247
|
+

|
|
248
|
+
In the diagram above, app 2 is running in a worker and it exposes a service which is proxied in the main thread.
|
|
249
|
+
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.
|
|
250
|
+
From its point of view, it is using the service directly, but the service is actually running in a different worker.
|
|
@@ -1,13 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export
|
|
3
|
-
|
|
4
|
-
};
|
|
5
|
-
export type Signal<T> = ReadableSignal<T> & {
|
|
6
|
-
set: (newValue: T) => void;
|
|
7
|
-
};
|
|
8
|
-
export declare const SignalsServiceDefinition: import("../../types.js").ServiceDefinition<{
|
|
9
|
-
signal: <T>(initialValue: T) => Signal<T>;
|
|
10
|
-
computed: <T>(fn: () => T) => ReadableSignal<T>;
|
|
11
|
-
effect: typeof peffect;
|
|
12
|
-
getSignalsRegistry: () => ReadableSignal<any>[];
|
|
13
|
-
}, {}>;
|
|
1
|
+
import { ReadOnlySignal, Signal, SignalValue } from '@wix/services-definitions';
|
|
2
|
+
export { SignalsServiceDefinition } from '@wix/services-definitions/core-services/signals';
|
|
3
|
+
export type { ReadOnlySignal, Signal, SignalValue };
|
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export const SignalsServiceDefinition = defineService('core:signals');
|
|
1
|
+
export { SignalsServiceDefinition } from '@wix/services-definitions/core-services/signals';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { ReadOnlySignal, Signal } from './definition.js';
|
|
2
|
+
import { Jsonifiable } from 'type-fest';
|
|
3
|
+
import { SignalsWrapper, SignalValue } from '@wix/services-definitions';
|
|
4
|
+
export declare function generateSignalId(): string;
|
|
5
|
+
export type SignalReplicator = (id: string, value: any, revision: string) => void;
|
|
6
|
+
export type InnerSignal<T extends SignalValue> = Signal<T> & {
|
|
7
|
+
syncSignal: (value: T, revision: string) => void;
|
|
8
|
+
getRevision(): string;
|
|
9
|
+
readonly: boolean;
|
|
10
|
+
};
|
|
11
|
+
export declare class SignalsRegistry {
|
|
12
|
+
private signalsStore;
|
|
13
|
+
private serviceSignalsRegistry;
|
|
14
|
+
private replicators;
|
|
15
|
+
private createInnerSignal;
|
|
16
|
+
getSignals(): ReadOnlySignal<any>[];
|
|
17
|
+
clearSignalsRegistry(): void;
|
|
18
|
+
private _createSignal;
|
|
19
|
+
setOrCreateSignal<T extends Jsonifiable>(id: string, initialValue: T, readonly?: boolean): ReadOnlySignal<T>;
|
|
20
|
+
createSignal<T extends Jsonifiable>(initialValue: T, id?: string): Signal<T>;
|
|
21
|
+
createComputedSignal<T extends Jsonifiable>(fn: () => T, id?: string): ReadOnlySignal<T>;
|
|
22
|
+
getSignalById(id: string): InnerSignal<any> | undefined;
|
|
23
|
+
addReplicator(replicator: SignalReplicator): void;
|
|
24
|
+
getServiceSignals(serviceId: string): SignalsWrapper | undefined;
|
|
25
|
+
setServiceSignals(serviceId: string, signals: SignalsWrapper): void;
|
|
26
|
+
}
|
|
27
|
+
export declare const createSignalsRegistry: () => SignalsRegistry;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { computed as pcomputed, signal as psignal } from '@preact/signals-core';
|
|
2
|
+
export function generateSignalId() {
|
|
3
|
+
return 'signal:xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
|
4
|
+
const r = (Math.random() * 16) | 0, v = c === 'x' ? r : (r & 0x3) | 0x8;
|
|
5
|
+
return v.toString(16);
|
|
6
|
+
});
|
|
7
|
+
}
|
|
8
|
+
function generateRevisionId() {
|
|
9
|
+
return Math.random().toString(36).slice(2);
|
|
10
|
+
}
|
|
11
|
+
function toPublicSignal(signal) {
|
|
12
|
+
return {
|
|
13
|
+
...(signal.readonly ? {} : { set: signal.set }),
|
|
14
|
+
get: signal.get,
|
|
15
|
+
peek: signal.peek,
|
|
16
|
+
subscribe: signal.subscribe,
|
|
17
|
+
id: signal.id,
|
|
18
|
+
isSignal: true,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export class SignalsRegistry {
|
|
22
|
+
signalsStore = new Map();
|
|
23
|
+
serviceSignalsRegistry = new Map();
|
|
24
|
+
replicators = [];
|
|
25
|
+
createInnerSignal(value, id, readonly = false) {
|
|
26
|
+
let revisionId = generateRevisionId();
|
|
27
|
+
const sig = psignal(value);
|
|
28
|
+
const signal = {
|
|
29
|
+
get: () => sig.value,
|
|
30
|
+
peek: () => sig.peek(),
|
|
31
|
+
syncSignal: (newValue, revision) => {
|
|
32
|
+
if (sig.peek() !== newValue && revision !== revisionId) {
|
|
33
|
+
revisionId = revision;
|
|
34
|
+
sig.value = newValue;
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
set: (newValue) => {
|
|
38
|
+
if (sig.peek() !== newValue) {
|
|
39
|
+
revisionId = generateRevisionId();
|
|
40
|
+
sig.value = newValue;
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
subscribe: (sfn) => sig.subscribe(sfn),
|
|
44
|
+
id,
|
|
45
|
+
isSignal: true,
|
|
46
|
+
getRevision: () => revisionId,
|
|
47
|
+
readonly,
|
|
48
|
+
};
|
|
49
|
+
return signal;
|
|
50
|
+
}
|
|
51
|
+
getSignals() {
|
|
52
|
+
return Array.from(this.signalsStore.values());
|
|
53
|
+
}
|
|
54
|
+
clearSignalsRegistry() {
|
|
55
|
+
this.signalsStore.clear();
|
|
56
|
+
this.serviceSignalsRegistry.clear();
|
|
57
|
+
this.replicators = [];
|
|
58
|
+
}
|
|
59
|
+
_createSignal(initialValue, id = generateSignalId(), readonly = false) {
|
|
60
|
+
const innerSignal = this.createInnerSignal(initialValue, id, readonly);
|
|
61
|
+
this.signalsStore.set(innerSignal.id, innerSignal);
|
|
62
|
+
innerSignal.subscribe((newValue) => {
|
|
63
|
+
this.replicators.forEach((replicator) => replicator(innerSignal.id, newValue, innerSignal.getRevision()));
|
|
64
|
+
});
|
|
65
|
+
return toPublicSignal(innerSignal);
|
|
66
|
+
}
|
|
67
|
+
setOrCreateSignal(id, initialValue, readonly = false) {
|
|
68
|
+
const existingSignal = this.signalsStore.get(id);
|
|
69
|
+
if (existingSignal) {
|
|
70
|
+
return toPublicSignal(existingSignal);
|
|
71
|
+
}
|
|
72
|
+
return this._createSignal(initialValue, id, readonly);
|
|
73
|
+
}
|
|
74
|
+
createSignal(initialValue, id) {
|
|
75
|
+
return this._createSignal(initialValue, id);
|
|
76
|
+
}
|
|
77
|
+
createComputedSignal(fn, id = generateSignalId()) {
|
|
78
|
+
const sig = pcomputed(fn);
|
|
79
|
+
const signal = {
|
|
80
|
+
get: () => sig.value,
|
|
81
|
+
peek: () => sig.peek(),
|
|
82
|
+
id,
|
|
83
|
+
subscribe: (sfn) => sig.subscribe(sfn),
|
|
84
|
+
isSignal: true,
|
|
85
|
+
};
|
|
86
|
+
// a replica of the signal that can be used to subscribe to the computed signal and set the value
|
|
87
|
+
const replicaSignal = this.createInnerSignal(sig.peek(), id, true);
|
|
88
|
+
this.signalsStore.set(signal.id, replicaSignal);
|
|
89
|
+
sig.subscribe((newValue) => {
|
|
90
|
+
replicaSignal.set(newValue);
|
|
91
|
+
this.replicators.forEach((replicator) => {
|
|
92
|
+
try {
|
|
93
|
+
replicator(signal.id, newValue, replicaSignal.getRevision());
|
|
94
|
+
}
|
|
95
|
+
catch (e) {
|
|
96
|
+
console.error(`Error replicating signal ${signal.id}`, e);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
return signal;
|
|
101
|
+
}
|
|
102
|
+
getSignalById(id) {
|
|
103
|
+
return this.signalsStore.get(id);
|
|
104
|
+
}
|
|
105
|
+
addReplicator(replicator) {
|
|
106
|
+
this.replicators.push(replicator);
|
|
107
|
+
}
|
|
108
|
+
getServiceSignals(serviceId) {
|
|
109
|
+
return this.serviceSignalsRegistry.get(serviceId);
|
|
110
|
+
}
|
|
111
|
+
setServiceSignals(serviceId, signals) {
|
|
112
|
+
this.serviceSignalsRegistry.set(serviceId, signals);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
export const createSignalsRegistry = () => new SignalsRegistry();
|
|
@@ -1,8 +1,19 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
export declare const SignalsServiceFactory: import("
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
}
|
|
1
|
+
import { ReadOnlySignal, Signal, SignalValue } from './definition.js';
|
|
2
|
+
import { SignalsRegistry } from './registry.js';
|
|
3
|
+
export declare const SignalsServiceFactory: import("@wix/services-definitions").ServiceFactory<string & {
|
|
4
|
+
__api: {
|
|
5
|
+
signal: <T extends SignalValue>(initialValue: T) => Signal<T>;
|
|
6
|
+
computed: <T extends SignalValue>(fn: () => T) => ReadOnlySignal<T>;
|
|
7
|
+
effect: (fn: () => void) => () => void;
|
|
8
|
+
};
|
|
9
|
+
__config: {
|
|
10
|
+
signalsRegistry: any;
|
|
11
|
+
};
|
|
12
|
+
isServiceDefinition?: boolean;
|
|
13
|
+
} & {
|
|
14
|
+
signal: <T extends SignalValue>(initialValue: T) => Signal<T>;
|
|
15
|
+
computed: <T extends SignalValue>(fn: () => T) => ReadOnlySignal<T>;
|
|
16
|
+
effect: (fn: () => void) => () => void;
|
|
17
|
+
}, {
|
|
18
|
+
signalsRegistry: SignalsRegistry;
|
|
19
|
+
}, import("@wix/services-definitions").ThreadMode.MAIN>;
|
|
@@ -1,27 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { effect as peffect } from '@preact/signals-core';
|
|
2
2
|
import { implementService } from '../../helpers.js';
|
|
3
3
|
import { SignalsServiceDefinition, } from './definition.js';
|
|
4
|
-
export const SignalsServiceFactory = implementService(SignalsServiceDefinition, () => {
|
|
5
|
-
const signalsRegistry = [];
|
|
4
|
+
export const SignalsServiceFactory = implementService(SignalsServiceDefinition, ({ config: { signalsRegistry }, }) => {
|
|
6
5
|
return {
|
|
7
6
|
signal: (initialValue) => {
|
|
8
|
-
|
|
9
|
-
const signal = {
|
|
10
|
-
get: () => sig.value,
|
|
11
|
-
set: (newValue) => (sig.value = newValue),
|
|
12
|
-
};
|
|
13
|
-
signalsRegistry.push(signal);
|
|
14
|
-
return signal;
|
|
7
|
+
return signalsRegistry.createSignal(initialValue);
|
|
15
8
|
},
|
|
16
9
|
computed: (fn) => {
|
|
17
|
-
|
|
18
|
-
const signal = {
|
|
19
|
-
get: () => sig.value,
|
|
20
|
-
};
|
|
21
|
-
signalsRegistry.push(signal);
|
|
22
|
-
return signal;
|
|
10
|
+
return signalsRegistry.createComputedSignal(fn);
|
|
23
11
|
},
|
|
24
12
|
effect: peffect,
|
|
25
|
-
getSignalsRegistry: () => signalsRegistry,
|
|
26
13
|
};
|
|
27
14
|
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import * as Comlink from 'comlink';
|
|
2
|
+
function hasSignals(obj) {
|
|
3
|
+
if ((obj && typeof obj !== 'object') || typeof obj === 'function') {
|
|
4
|
+
return false;
|
|
5
|
+
}
|
|
6
|
+
if (obj?.isSignal) {
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
for (const key in obj) {
|
|
10
|
+
if (hasSignals(obj[key])) {
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
function deserializeSignals(obj, signalsRegistry) {
|
|
17
|
+
if (obj?.isSignal) {
|
|
18
|
+
return signalsRegistry.setOrCreateSignal(obj.id, obj.value, obj.readonly);
|
|
19
|
+
}
|
|
20
|
+
if (Array.isArray(obj)) {
|
|
21
|
+
return obj.map((item) => deserializeSignals(item, signalsRegistry));
|
|
22
|
+
}
|
|
23
|
+
if (typeof obj === 'object') {
|
|
24
|
+
const res = {};
|
|
25
|
+
for (const key in obj) {
|
|
26
|
+
res[key] = deserializeSignals(obj[key], signalsRegistry);
|
|
27
|
+
}
|
|
28
|
+
return res;
|
|
29
|
+
}
|
|
30
|
+
return obj;
|
|
31
|
+
}
|
|
32
|
+
function serializeSignals(obj) {
|
|
33
|
+
if (obj?.isSignal) {
|
|
34
|
+
const signal = obj;
|
|
35
|
+
return {
|
|
36
|
+
id: signal.id,
|
|
37
|
+
value: signal.peek(),
|
|
38
|
+
isSignal: true,
|
|
39
|
+
readonly: !obj.set,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
if (Array.isArray(obj)) {
|
|
43
|
+
return obj.map((item) => serializeSignals(item));
|
|
44
|
+
}
|
|
45
|
+
if (typeof obj === 'object') {
|
|
46
|
+
const res = {};
|
|
47
|
+
for (const key in obj) {
|
|
48
|
+
res[key] = serializeSignals(obj[key]);
|
|
49
|
+
}
|
|
50
|
+
return res;
|
|
51
|
+
}
|
|
52
|
+
return obj;
|
|
53
|
+
}
|
|
54
|
+
export const createSignalsTransferHandler = (signalsRegistry) => {
|
|
55
|
+
Comlink.transferHandlers.set('Signals', {
|
|
56
|
+
// @ts-expect-error
|
|
57
|
+
canHandle: hasSignals,
|
|
58
|
+
serialize: (obj) => {
|
|
59
|
+
const serialized = serializeSignals(obj);
|
|
60
|
+
return [serialized, []];
|
|
61
|
+
},
|
|
62
|
+
deserialize: (obj) => deserializeSignals(obj, signalsRegistry),
|
|
63
|
+
});
|
|
64
|
+
};
|
package/build/helpers.d.ts
CHANGED
|
@@ -1,6 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export declare function defineService<TAPI, TConfig = {}>(id: string): ServiceDefinition<TAPI, TConfig>;
|
|
3
|
-
export declare function implementService<T extends ServiceDefinition<any, any>, ImplConfig>(_: T, factory: ServiceFactory<T, ImplConfig>): ServiceFactory<T, ImplConfig>;
|
|
4
|
-
export declare namespace implementService {
|
|
5
|
-
var withConfig: <TConfig>() => <T extends ServiceDefinition<any, any>>(_: T, factory: ServiceFactory<T, TConfig>) => ServiceFactory<T, TConfig>;
|
|
6
|
-
}
|
|
1
|
+
export { defineService, implementService } from '@wix/services-definitions';
|
package/build/helpers.js
CHANGED
|
@@ -1,11 +1 @@
|
|
|
1
|
-
export
|
|
2
|
-
return id;
|
|
3
|
-
}
|
|
4
|
-
export function implementService(_, factory) {
|
|
5
|
-
return factory;
|
|
6
|
-
}
|
|
7
|
-
implementService.withConfig = function () {
|
|
8
|
-
return function (_, factory) {
|
|
9
|
-
return factory;
|
|
10
|
-
};
|
|
11
|
-
};
|
|
1
|
+
export { defineService, implementService } from '@wix/services-definitions';
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { ServicesManager, PromisifiedServiceAPI, ServiceDefinition, ThreadMode } from './types.js';
|
|
2
|
+
import { GetService, NamedSignals } from '@wix/services-definitions';
|
|
3
|
+
import * as Comlink from 'comlink';
|
|
4
|
+
export type RemoteServicesManager = ServicesManager<ThreadMode.REMOTE> & {
|
|
5
|
+
getServiceProxy: GetService<ThreadMode.REMOTE>;
|
|
6
|
+
syncSignal: (signalId: string, value: any, revision: string) => void;
|
|
7
|
+
exchangeServicesAndSignals: (serviceSignalsArr: {
|
|
8
|
+
serviceId: string;
|
|
9
|
+
signals: NamedSignals;
|
|
10
|
+
}[]) => {
|
|
11
|
+
serviceId: string;
|
|
12
|
+
signals: any;
|
|
13
|
+
}[];
|
|
14
|
+
awaitConnectionFromMain: () => Promise<boolean>;
|
|
15
|
+
};
|
|
16
|
+
export type ProxyServiceManager = Comlink.Remote<RemoteServicesManager>;
|
|
17
|
+
export declare function createServiceProxy<T extends ServiceDefinition<any, any>>(definition: T, remoteServiceManager: ProxyServiceManager, localServicesManager: ServicesManager): PromisifiedServiceAPI<T>;
|
|
18
|
+
export declare enum SERVICE_MANAGER_MESSAGES {
|
|
19
|
+
INIT_PORT = "WIX_MESSAGE.SERVICE_MANAGER.INIT_PORT",
|
|
20
|
+
SIGNAL = "WIX_MESSAGE.SERVICE_MANAGER.SIGNAL",
|
|
21
|
+
REMOTE_READY = "WIX_MESSAGE.SERVICE_MANAGER.REMOTE_READY",
|
|
22
|
+
READY_FOR_CONNECTION = "WIX_MESSAGE.SERVICE_MANAGER.READY_FOR_CONNECTION"
|
|
23
|
+
}
|
|
24
|
+
export declare function connectRemoteIframe(manager: ServicesManager<any>, target: Window, options?: {
|
|
25
|
+
retryOptions?: ConnectRemoteParams['retryOptions'];
|
|
26
|
+
remoteServices?: ConnectRemoteParams['remoteServices'];
|
|
27
|
+
awaitInitialConnection?: ConnectRemoteParams['awaitInitialConnection'];
|
|
28
|
+
}): Promise<ProxyServiceManager>;
|
|
29
|
+
export declare function connectRemoteWorker(manager: ServicesManager<any>, target: Worker, options?: {
|
|
30
|
+
retryOptions?: ConnectRemoteParams['retryOptions'];
|
|
31
|
+
remoteServices?: ConnectRemoteParams['remoteServices'];
|
|
32
|
+
awaitInitialConnection?: ConnectRemoteParams['awaitInitialConnection'];
|
|
33
|
+
}): Promise<ProxyServiceManager>;
|
|
34
|
+
type ConnectRemoteParams = {
|
|
35
|
+
awaitInitialConnection?: boolean;
|
|
36
|
+
manager: ServicesManager<any>;
|
|
37
|
+
retryOptions?: {
|
|
38
|
+
timeout?: number;
|
|
39
|
+
interval?: number;
|
|
40
|
+
};
|
|
41
|
+
remoteServices?: ServiceDefinition<any, any>[];
|
|
42
|
+
} & ({
|
|
43
|
+
target: Window;
|
|
44
|
+
type: 'iframe';
|
|
45
|
+
} | {
|
|
46
|
+
target: Worker;
|
|
47
|
+
type: 'worker';
|
|
48
|
+
});
|
|
49
|
+
export declare function createRemoteServicesManager({ servicesManager, messageFrame, }: {
|
|
50
|
+
servicesManager: ServicesManager<any>;
|
|
51
|
+
messageFrame?: Window | Worker;
|
|
52
|
+
}): RemoteServicesManager;
|
|
53
|
+
export {};
|