@sogni-ai/sogni-client 4.0.0-alpha.19 → 4.0.0-alpha.20
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/CHANGELOG.md +7 -0
- package/dist/ApiClient/WebSocketClient/BrowserWebSocketClient/ChannelCoordinator.d.ts +66 -0
- package/dist/ApiClient/WebSocketClient/BrowserWebSocketClient/ChannelCoordinator.js +332 -0
- package/dist/ApiClient/WebSocketClient/BrowserWebSocketClient/ChannelCoordinator.js.map +1 -0
- package/dist/ApiClient/WebSocketClient/BrowserWebSocketClient/index.d.ts +3 -9
- package/dist/ApiClient/WebSocketClient/BrowserWebSocketClient/index.js +104 -100
- package/dist/ApiClient/WebSocketClient/BrowserWebSocketClient/index.js.map +1 -1
- package/dist/ApiClient/WebSocketClient/index.js +2 -2
- package/dist/ApiClient/WebSocketClient/index.js.map +1 -1
- package/dist/ApiClient/index.js +1 -1
- package/dist/ApiClient/index.js.map +1 -1
- package/package.json +1 -1
- package/src/ApiClient/WebSocketClient/BrowserWebSocketClient/ChannelCoordinator.ts +426 -0
- package/src/ApiClient/WebSocketClient/BrowserWebSocketClient/index.ts +127 -98
- package/src/ApiClient/WebSocketClient/index.ts +2 -2
- package/src/ApiClient/index.ts +1 -1
- package/dist/ApiClient/WebSocketClient/BrowserWebSocketClient/WSCoordinator.d.ts +0 -102
- package/dist/ApiClient/WebSocketClient/BrowserWebSocketClient/WSCoordinator.js +0 -378
- package/dist/ApiClient/WebSocketClient/BrowserWebSocketClient/WSCoordinator.js.map +0 -1
- package/dist/ApiClient/WebSocketClient/BrowserWebSocketClient/types.d.ts +0 -102
- package/dist/ApiClient/WebSocketClient/BrowserWebSocketClient/types.js +0 -3
- package/dist/ApiClient/WebSocketClient/BrowserWebSocketClient/types.js.map +0 -1
- package/src/ApiClient/WebSocketClient/BrowserWebSocketClient/WSCoordinator.ts +0 -443
- package/src/ApiClient/WebSocketClient/BrowserWebSocketClient/types.ts +0 -107
|
@@ -3,10 +3,41 @@ import { AuthManager, TokenAuthManager } from '../../../lib/AuthManager';
|
|
|
3
3
|
import { Logger } from '../../../lib/DefaultLogger';
|
|
4
4
|
import WebSocketClient from '../index';
|
|
5
5
|
import RestClient from '../../../lib/RestClient';
|
|
6
|
-
import {
|
|
7
|
-
import WSCoordinator from './WSCoordinator';
|
|
6
|
+
import { SocketEventMap } from '../events';
|
|
8
7
|
import { MessageType, SocketMessageMap } from '../messages';
|
|
9
|
-
import
|
|
8
|
+
import ChannelCoordinator from './ChannelCoordinator';
|
|
9
|
+
|
|
10
|
+
interface SocketSend<T extends MessageType = MessageType> {
|
|
11
|
+
type: 'socket-send';
|
|
12
|
+
payload: { type: T; data: SocketMessageMap[T] };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface SocketConnect {
|
|
16
|
+
type: 'connect';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface SocketDisconnect {
|
|
20
|
+
type: 'disconnect';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface SwitchNetwork {
|
|
24
|
+
type: 'switchNetwork';
|
|
25
|
+
payload: SupernetType;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type Message = SocketConnect | SocketDisconnect | SocketSend | SwitchNetwork;
|
|
29
|
+
|
|
30
|
+
interface EventNotification<T extends keyof SocketEventMap = keyof SocketEventMap> {
|
|
31
|
+
type: 'socket-event';
|
|
32
|
+
payload: { type: T; data: SocketEventMap[T] };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface AuthStateChanged {
|
|
36
|
+
type: 'auth-state-changed';
|
|
37
|
+
payload: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type Notification = EventNotification | AuthStateChanged;
|
|
10
41
|
|
|
11
42
|
type EventInterceptor<T extends keyof SocketEventMap = keyof SocketEventMap> = (
|
|
12
43
|
eventType: T,
|
|
@@ -30,8 +61,7 @@ class BrowserWebSocketClient extends RestClient<SocketEventMap> implements IWebS
|
|
|
30
61
|
appId: string;
|
|
31
62
|
baseUrl: string;
|
|
32
63
|
private socketClient: WrappedClient;
|
|
33
|
-
private coordinator:
|
|
34
|
-
private isPrimary = false;
|
|
64
|
+
private coordinator: ChannelCoordinator<Message, Notification>;
|
|
35
65
|
private _isConnected = false;
|
|
36
66
|
private _supernetType: SupernetType;
|
|
37
67
|
|
|
@@ -48,67 +78,125 @@ class BrowserWebSocketClient extends RestClient<SocketEventMap> implements IWebS
|
|
|
48
78
|
this.appId = appId;
|
|
49
79
|
this.baseUrl = socketClient.baseUrl;
|
|
50
80
|
this._supernetType = supernetType;
|
|
51
|
-
this.coordinator = new
|
|
52
|
-
{
|
|
53
|
-
onAuthChanged: this.handleAuthChanged.bind(this),
|
|
81
|
+
this.coordinator = new ChannelCoordinator({
|
|
82
|
+
callbacks: {
|
|
54
83
|
onRoleChange: this.handleRoleChange.bind(this),
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
onSendRequest: this.handleSendRequest.bind(this)
|
|
84
|
+
onMessage: this.handleMessage.bind(this),
|
|
85
|
+
onNotification: this.handleNotification.bind(this)
|
|
58
86
|
},
|
|
59
87
|
logger
|
|
60
|
-
);
|
|
88
|
+
});
|
|
61
89
|
this.auth.on('updated', this.handleAuthUpdated.bind(this));
|
|
62
90
|
this.socketClient.intercept(this.handleSocketEvent.bind(this));
|
|
91
|
+
//@ts-expect-error window is defined in browser
|
|
92
|
+
window.DISCONNECT = () => {
|
|
93
|
+
this.disconnect();
|
|
94
|
+
};
|
|
63
95
|
}
|
|
64
96
|
|
|
65
97
|
get isConnected() {
|
|
66
|
-
return this.isPrimary ? this.socketClient.isConnected : this._isConnected;
|
|
98
|
+
return this.coordinator.isPrimary ? this.socketClient.isConnected : this._isConnected;
|
|
67
99
|
}
|
|
68
100
|
|
|
69
101
|
get supernetType() {
|
|
70
|
-
return this.isPrimary ? this.socketClient.supernetType : this._supernetType;
|
|
102
|
+
return this.coordinator.isPrimary ? this.socketClient.supernetType : this._supernetType;
|
|
71
103
|
}
|
|
72
104
|
|
|
73
105
|
async connect(): Promise<void> {
|
|
74
|
-
|
|
75
|
-
this.isPrimary
|
|
76
|
-
if (isPrimary) {
|
|
106
|
+
await this.coordinator.isReady();
|
|
107
|
+
if (this.coordinator.isPrimary) {
|
|
77
108
|
await this.socketClient.connect();
|
|
78
109
|
} else {
|
|
79
|
-
this.coordinator.
|
|
110
|
+
return this.coordinator.sendMessage({
|
|
111
|
+
type: 'connect'
|
|
112
|
+
});
|
|
80
113
|
}
|
|
81
114
|
}
|
|
82
115
|
|
|
83
|
-
disconnect() {
|
|
84
|
-
|
|
116
|
+
async disconnect() {
|
|
117
|
+
await this.coordinator.isReady();
|
|
118
|
+
if (this.coordinator.isPrimary) {
|
|
85
119
|
this.socketClient.disconnect();
|
|
86
120
|
} else {
|
|
87
|
-
this.coordinator.
|
|
121
|
+
this.coordinator.sendMessage({
|
|
122
|
+
type: 'disconnect'
|
|
123
|
+
});
|
|
88
124
|
}
|
|
89
125
|
}
|
|
90
126
|
|
|
91
127
|
async switchNetwork(supernetType: SupernetType): Promise<SupernetType> {
|
|
92
|
-
|
|
128
|
+
await this.coordinator.isReady();
|
|
129
|
+
if (this.coordinator.isPrimary) {
|
|
93
130
|
return this.socketClient.switchNetwork(supernetType);
|
|
94
131
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
resolve(network);
|
|
99
|
-
});
|
|
100
|
-
await this.send('changeNetwork', supernetType);
|
|
132
|
+
await this.coordinator.sendMessage({
|
|
133
|
+
type: 'switchNetwork',
|
|
134
|
+
payload: supernetType
|
|
101
135
|
});
|
|
136
|
+
this._supernetType = supernetType;
|
|
137
|
+
return supernetType;
|
|
102
138
|
}
|
|
103
139
|
|
|
104
140
|
async send<T extends MessageType>(messageType: T, data: SocketMessageMap[T]): Promise<void> {
|
|
105
|
-
|
|
141
|
+
await this.coordinator.isReady();
|
|
142
|
+
if (this.coordinator.isPrimary) {
|
|
106
143
|
if (!this.socketClient.isConnected) {
|
|
107
144
|
await this.socketClient.connect();
|
|
108
145
|
}
|
|
109
146
|
return this.socketClient.send(messageType, data);
|
|
110
147
|
}
|
|
111
|
-
return this.coordinator.
|
|
148
|
+
return this.coordinator.sendMessage({
|
|
149
|
+
type: 'socket-send',
|
|
150
|
+
payload: { type: messageType, data }
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private async handleMessage(message: Message) {
|
|
155
|
+
this._logger.debug('Received control message', message);
|
|
156
|
+
switch (message.type) {
|
|
157
|
+
case 'socket-send': {
|
|
158
|
+
if (!this.socketClient.isConnected) {
|
|
159
|
+
await this.socketClient.connect();
|
|
160
|
+
}
|
|
161
|
+
return this.socketClient.send(message.payload.type, message.payload.data);
|
|
162
|
+
}
|
|
163
|
+
case 'connect': {
|
|
164
|
+
if (!this.socketClient.isConnected) {
|
|
165
|
+
await this.socketClient.connect();
|
|
166
|
+
}
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
case 'disconnect': {
|
|
170
|
+
if (this.socketClient.isConnected) {
|
|
171
|
+
this.socketClient.disconnect();
|
|
172
|
+
}
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
case 'switchNetwork': {
|
|
176
|
+
await this.switchNetwork(message.payload);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
default: {
|
|
180
|
+
this._logger.error('Received unknown message type:', message);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private async handleNotification(notification: Notification) {
|
|
186
|
+
this._logger.debug('Received notification', notification.type, notification.payload);
|
|
187
|
+
switch (notification.type) {
|
|
188
|
+
case 'socket-event': {
|
|
189
|
+
this.emit(notification.payload.type, notification.payload.data);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
case 'auth-state-changed': {
|
|
193
|
+
this.handleAuthChanged(notification.payload);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
default: {
|
|
197
|
+
this._logger.error('Received unknown notification type:', notification);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
112
200
|
}
|
|
113
201
|
|
|
114
202
|
private handleAuthChanged(isAuthenticated: boolean) {
|
|
@@ -125,88 +213,29 @@ class BrowserWebSocketClient extends RestClient<SocketEventMap> implements IWebS
|
|
|
125
213
|
}
|
|
126
214
|
|
|
127
215
|
private handleSocketEvent(eventType: keyof SocketEventMap, payload: any) {
|
|
128
|
-
if (this.isPrimary) {
|
|
129
|
-
this.coordinator.
|
|
216
|
+
if (this.coordinator.isPrimary) {
|
|
217
|
+
this.coordinator.notify({
|
|
218
|
+
type: 'socket-event',
|
|
219
|
+
payload: { type: eventType, data: payload }
|
|
220
|
+
});
|
|
130
221
|
this.emit(eventType, payload);
|
|
131
222
|
}
|
|
132
223
|
}
|
|
133
224
|
|
|
134
225
|
private handleAuthUpdated(isAuthenticated: boolean) {
|
|
135
|
-
this.coordinator.
|
|
226
|
+
this.coordinator.notify({
|
|
227
|
+
type: 'auth-state-changed',
|
|
228
|
+
payload: isAuthenticated
|
|
229
|
+
});
|
|
136
230
|
}
|
|
137
231
|
|
|
138
232
|
private handleRoleChange(isPrimary: boolean) {
|
|
139
|
-
this.isPrimary = isPrimary;
|
|
140
233
|
if (isPrimary && !this.socketClient.isConnected && this.isConnected) {
|
|
141
234
|
this.socketClient.connect();
|
|
142
235
|
} else if (!isPrimary && this.socketClient.isConnected) {
|
|
143
236
|
this.socketClient.disconnect();
|
|
144
237
|
}
|
|
145
238
|
}
|
|
146
|
-
|
|
147
|
-
private handleConnectionToggle(isConnected: boolean) {
|
|
148
|
-
if (this.isPrimary) {
|
|
149
|
-
if (isConnected && !this.socketClient.isConnected) {
|
|
150
|
-
this.socketClient.connect();
|
|
151
|
-
} else if (!isConnected && this.socketClient.isConnected) {
|
|
152
|
-
this.socketClient.disconnect();
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Emit events from socket to listeners
|
|
159
|
-
* @param message
|
|
160
|
-
*/
|
|
161
|
-
private handleMessageFromPrimary(message: SocketEventReceived | Heartbeat) {
|
|
162
|
-
if (this.isPrimary) {
|
|
163
|
-
throw new Error('Received message from primary socket, but it is primary');
|
|
164
|
-
}
|
|
165
|
-
this._logger.debug('Received message from primary client:', message.type, message.payload);
|
|
166
|
-
if (message.type === 'primary-present') {
|
|
167
|
-
const shouldUpdateStatus = message.payload.connected !== this._isConnected;
|
|
168
|
-
if (shouldUpdateStatus) {
|
|
169
|
-
this._isConnected = message.payload.connected;
|
|
170
|
-
if (message.payload.connected) {
|
|
171
|
-
this._logger.debug('Primary socket is active emitting connected event');
|
|
172
|
-
this.emit('connected', { network: this._supernetType });
|
|
173
|
-
} else {
|
|
174
|
-
this._logger.debug('Primary socket is inactive emitting disconnected event');
|
|
175
|
-
this.emit('disconnected', { code: 5000, reason: 'Primary socket disconnected' });
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
const event = message.payload;
|
|
181
|
-
switch (event.eventType) {
|
|
182
|
-
case 'connected': {
|
|
183
|
-
if (!this._isConnected) {
|
|
184
|
-
this._isConnected = true;
|
|
185
|
-
this.emit('connected', { network: this._supernetType });
|
|
186
|
-
}
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
case 'disconnected': {
|
|
190
|
-
this._isConnected = false;
|
|
191
|
-
this.emit('disconnected', event.payload as ServerDisconnectData);
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
default: {
|
|
195
|
-
this.emit(event.eventType, event.payload as any);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
private async handleSendRequest(message: SendSocketMessage) {
|
|
201
|
-
if (!this.isPrimary) {
|
|
202
|
-
// Should never happen, but just in case
|
|
203
|
-
return Promise.resolve();
|
|
204
|
-
}
|
|
205
|
-
if (!this.socketClient.isConnected) {
|
|
206
|
-
await this.socketClient.connect();
|
|
207
|
-
}
|
|
208
|
-
return this.socketClient.send(message.payload.messageType, message.payload.data);
|
|
209
|
-
}
|
|
210
239
|
}
|
|
211
240
|
|
|
212
241
|
export default BrowserWebSocketClient;
|
|
@@ -86,7 +86,7 @@ class WebSocketClient extends RestClient<SocketEventMap> implements IWebSocketCl
|
|
|
86
86
|
socket.onmessage = null;
|
|
87
87
|
socket.onopen = null;
|
|
88
88
|
this.stopPing();
|
|
89
|
-
socket.close();
|
|
89
|
+
socket.close(1000, 'Client disconnected');
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
private startPing(socket: WebSocket) {
|
|
@@ -148,7 +148,7 @@ class WebSocketClient extends RestClient<SocketEventMap> implements IWebSocketCl
|
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
private handleClose(e: CloseEvent) {
|
|
151
|
-
if (e.target === this.socket) {
|
|
151
|
+
if (e.target === this.socket || !this.socket) {
|
|
152
152
|
this._logger.info('WebSocket disconnected, cleanup', e);
|
|
153
153
|
this.disconnect();
|
|
154
154
|
this.emit('disconnected', {
|
package/src/ApiClient/index.ts
CHANGED
|
@@ -112,7 +112,7 @@ class ApiClient extends TypedEventEmitter<ApiClientEvents> {
|
|
|
112
112
|
|
|
113
113
|
handleSocketDisconnect(data: ServerDisconnectData) {
|
|
114
114
|
// If user is not authenticated, we don't need to reconnect
|
|
115
|
-
if (!this.auth.isAuthenticated) {
|
|
115
|
+
if (!this.auth.isAuthenticated || data.code === 1000) {
|
|
116
116
|
this.emit('disconnected', data);
|
|
117
117
|
return;
|
|
118
118
|
}
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
import { Logger } from '../../../lib/DefaultLogger';
|
|
2
|
-
import { SocketEventMap, SocketEventName } from '../events';
|
|
3
|
-
import { MessageType, SocketMessageMap } from '../messages';
|
|
4
|
-
import { Heartbeat, SocketEventReceived, SendSocketMessage } from './types';
|
|
5
|
-
interface WSCoordinatorCallbacks {
|
|
6
|
-
/**
|
|
7
|
-
* Invoked when authentication state changes (authenticated/unauthenticated).
|
|
8
|
-
* @param isAuthenticated - true if client is authenticated, false if not.
|
|
9
|
-
*/
|
|
10
|
-
onAuthChanged: (isAuthenticated: boolean) => void;
|
|
11
|
-
/**
|
|
12
|
-
* Invoked when role changes (primary/secondary).
|
|
13
|
-
* @param isPrimary - true if client is primary, false if secondary.
|
|
14
|
-
*/
|
|
15
|
-
onRoleChange: (isPrimary: boolean) => void;
|
|
16
|
-
/**
|
|
17
|
-
* Invoked when connection state must change (connected/disconnected).
|
|
18
|
-
* @param isConnected
|
|
19
|
-
*/
|
|
20
|
-
onConnectionToggle: (isConnected: boolean) => void;
|
|
21
|
-
/**
|
|
22
|
-
* Invoked when secondary client receives a socket event from primary.
|
|
23
|
-
* @param message
|
|
24
|
-
*/
|
|
25
|
-
onMessageFromPrimary: (message: SocketEventReceived | Heartbeat) => void;
|
|
26
|
-
/**
|
|
27
|
-
* Invoked when primary client receives a socket send request from secondary.
|
|
28
|
-
* @param message
|
|
29
|
-
*/
|
|
30
|
-
onSendRequest: (message: SendSocketMessage) => Promise<void>;
|
|
31
|
-
}
|
|
32
|
-
declare class WSCoordinator {
|
|
33
|
-
private static readonly HEARTBEAT_INTERVAL;
|
|
34
|
-
private static readonly PRIMARY_TIMEOUT;
|
|
35
|
-
private static readonly CHANNEL_NAME;
|
|
36
|
-
private static readonly ACK_TIMEOUT;
|
|
37
|
-
private id;
|
|
38
|
-
private callbacks;
|
|
39
|
-
private channel;
|
|
40
|
-
private _isPrimary;
|
|
41
|
-
private _isConnected;
|
|
42
|
-
private lastPrimaryHeartbeat;
|
|
43
|
-
private logger;
|
|
44
|
-
private heartbeatInterval;
|
|
45
|
-
private primaryCheckInterval;
|
|
46
|
-
private startingEvents;
|
|
47
|
-
private ackCallbacks;
|
|
48
|
-
private initialized;
|
|
49
|
-
constructor(callbacks: WSCoordinatorCallbacks, logger: Logger);
|
|
50
|
-
/**
|
|
51
|
-
* Initialize tab coordination and determine role
|
|
52
|
-
*/
|
|
53
|
-
initialize(): Promise<boolean>;
|
|
54
|
-
connect(): void;
|
|
55
|
-
disconnect(): void;
|
|
56
|
-
changeAuthState(isAuthenticated: boolean): Promise<void>;
|
|
57
|
-
/**
|
|
58
|
-
* Wait briefly to see if a primary tab responds
|
|
59
|
-
*/
|
|
60
|
-
private waitForPrimaryResponse;
|
|
61
|
-
/**
|
|
62
|
-
* Become the primary tab
|
|
63
|
-
*/
|
|
64
|
-
private becomePrimary;
|
|
65
|
-
/**
|
|
66
|
-
* Release primary role (when closing)
|
|
67
|
-
*/
|
|
68
|
-
private releasePrimary;
|
|
69
|
-
/**
|
|
70
|
-
* Start sending heartbeat messages as primary
|
|
71
|
-
*/
|
|
72
|
-
private startHeartbeat;
|
|
73
|
-
/**
|
|
74
|
-
* Stop sending heartbeat messages
|
|
75
|
-
*/
|
|
76
|
-
private stopHeartbeat;
|
|
77
|
-
/**
|
|
78
|
-
* Start checking for primary heartbeat (as secondary)
|
|
79
|
-
*/
|
|
80
|
-
private startPrimaryCheck;
|
|
81
|
-
/**
|
|
82
|
-
* Stop checking for primary heartbeat
|
|
83
|
-
*/
|
|
84
|
-
private stopPrimaryCheck;
|
|
85
|
-
private handleMessage;
|
|
86
|
-
private broadcast;
|
|
87
|
-
/**
|
|
88
|
-
* Send a message to be transmitted over the socket (from secondary to primary)
|
|
89
|
-
*/
|
|
90
|
-
sendToSocket<T extends MessageType = MessageType>(messageType: T, data: SocketMessageMap[T]): Promise<void>;
|
|
91
|
-
/**
|
|
92
|
-
* Broadcast a socket event from primary to all secondaries
|
|
93
|
-
*/
|
|
94
|
-
broadcastSocketEvent<E extends SocketEventName = SocketEventName>(eventType: E, payload: SocketEventMap[E]): void;
|
|
95
|
-
private updateStartingState;
|
|
96
|
-
isPrimary(): boolean;
|
|
97
|
-
/**
|
|
98
|
-
* Handle tab closing event
|
|
99
|
-
*/
|
|
100
|
-
private handleBeforeUnload;
|
|
101
|
-
}
|
|
102
|
-
export default WSCoordinator;
|