@morgan-stanley/composeui-messaging-message-router 0.1.0-alpha.10
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/LICENSE +201 -0
- package/README.md +110 -0
- package/dist/messageRouterMessaging-iife-bundle.js +860 -0
- package/package.json +44 -0
- package/rollup.config.js +16 -0
- package/src/AsyncDisposableWrapper.ts +20 -0
- package/src/MessageRouterMessaging.test.ts +173 -0
- package/src/MessageRouterMessaging.ts +105 -0
- package/src/index.ts +27 -0
- package/tsconfig.json +14 -0
- package/vite.config.ts +8 -0
|
@@ -0,0 +1,860 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
class AsyncDisposableWrapper {
|
|
5
|
+
constructor(messageRouterClient, serviceName) {
|
|
6
|
+
this.messageRouterClient = messageRouterClient;
|
|
7
|
+
this.serviceName = serviceName;
|
|
8
|
+
}
|
|
9
|
+
[Symbol.asyncDispose]() {
|
|
10
|
+
return this.messageRouterClient.unregisterService(this.serviceName);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/*
|
|
15
|
+
* Morgan Stanley makes this available to you under the Apache License,
|
|
16
|
+
* Version 2.0 (the "License"). You may obtain a copy of the License at
|
|
17
|
+
* http://www.apache.org/licenses/LICENSE-2.0.
|
|
18
|
+
* See the NOTICE file distributed with this work for additional information
|
|
19
|
+
* regarding copyright ownership. Unless required by applicable law or agreed
|
|
20
|
+
* to in writing, software distributed under the License is distributed on an
|
|
21
|
+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
|
22
|
+
* or implied. See the License for the specific language governing permissions
|
|
23
|
+
* and limitations under the License.
|
|
24
|
+
*
|
|
25
|
+
*/
|
|
26
|
+
/**
|
|
27
|
+
* Implementation of IMessaging interface using MessageRouter.
|
|
28
|
+
* Provides messaging capabilities through the MessageRouter client for ComposeUI applications.
|
|
29
|
+
*/
|
|
30
|
+
class MessageRouterMessaging {
|
|
31
|
+
/**
|
|
32
|
+
* Creates a new instance of MessageRouterMessaging.
|
|
33
|
+
* @param messageRouterClient The MessageRouter client instance to use for communication.
|
|
34
|
+
*/
|
|
35
|
+
constructor(messageRouterClient) {
|
|
36
|
+
this.messageRouterClient = messageRouterClient;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Subscribes to messages on a specific topic.
|
|
40
|
+
* @param topic The topic to subscribe to.
|
|
41
|
+
* @param subscriber Callback function that will be invoked with each received message.
|
|
42
|
+
* @param cancellationToken Optional signal to cancel the subscription setup.
|
|
43
|
+
* @returns A Promise that resolves to an Unsubscribable object for managing the subscription.
|
|
44
|
+
* @remarks If a message is received without a payload, a warning will be logged and the subscriber will not be called.
|
|
45
|
+
*/
|
|
46
|
+
subscribe(topic, subscriber, cancellationToken) {
|
|
47
|
+
return this.messageRouterClient.subscribe(topic, (message) => {
|
|
48
|
+
if (!message.payload) {
|
|
49
|
+
console.warn(`Received empty message on topic ${topic}`);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
subscriber(message.payload);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Publishes a message to a specific topic.
|
|
57
|
+
* @param topic The topic to publish to.
|
|
58
|
+
* @param message The message content to publish.
|
|
59
|
+
* @param cancellationToken Optional signal to cancel the publish operation.
|
|
60
|
+
* @returns A Promise that resolves when the message has been published.
|
|
61
|
+
*/
|
|
62
|
+
publish(topic, message, cancellationToken) {
|
|
63
|
+
return this.messageRouterClient.publish(topic, message);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Registers a service handler for a specific service name.
|
|
67
|
+
* @param serviceName The name of the service to register.
|
|
68
|
+
* @param serviceHandler The handler function that will process service requests.
|
|
69
|
+
* @param cancellationToken Optional signal to cancel the service registration.
|
|
70
|
+
* @returns A Promise that resolves to an AsyncDisposable for managing the service registration.
|
|
71
|
+
* @remarks The service handler will receive the payload from the request and should return a response.
|
|
72
|
+
* Both the payload and response can be null.
|
|
73
|
+
*/
|
|
74
|
+
async registerService(serviceName, serviceHandler, cancellationToken) {
|
|
75
|
+
await this.messageRouterClient.registerService(serviceName, async (endpoint, payload, context) => {
|
|
76
|
+
const result = await serviceHandler(payload);
|
|
77
|
+
return result;
|
|
78
|
+
});
|
|
79
|
+
const disposable = new AsyncDisposableWrapper(this.messageRouterClient, serviceName);
|
|
80
|
+
return disposable;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Invokes a registered service.
|
|
84
|
+
* @param serviceName The name of the service to invoke.
|
|
85
|
+
* @param payload Optional payload to send with the service request.
|
|
86
|
+
* @param cancellationToken Optional signal to cancel the service invocation.
|
|
87
|
+
* @returns A Promise that resolves to the service response or null if no response is received.
|
|
88
|
+
* @remarks If the payload is null, the service will be invoked without a payload.
|
|
89
|
+
* The response will be null if the service doesn't return a response or if an error occurs.
|
|
90
|
+
*/
|
|
91
|
+
async invokeService(serviceName, payload, cancellationToken) {
|
|
92
|
+
if (payload == null) {
|
|
93
|
+
const result = await this.messageRouterClient.invoke(serviceName);
|
|
94
|
+
if (!result) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
const response = await this.messageRouterClient.invoke(serviceName, payload);
|
|
100
|
+
if (!response) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
return response;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* @license
|
|
109
|
+
* author: Morgan Stanley
|
|
110
|
+
* composeui-messaging-client.js v0.1.0-alpha.10
|
|
111
|
+
* Released under the Apache-2.0 license.
|
|
112
|
+
*/
|
|
113
|
+
|
|
114
|
+
/*
|
|
115
|
+
* Morgan Stanley makes this available to you under the Apache License,
|
|
116
|
+
* Version 2.0 (the "License"). You may obtain a copy of the License at
|
|
117
|
+
* http://www.apache.org/licenses/LICENSE-2.0.
|
|
118
|
+
* See the NOTICE file distributed with this work for additional information
|
|
119
|
+
* regarding copyright ownership. Unless required by applicable law or agreed
|
|
120
|
+
* to in writing, software distributed under the License is distributed on an
|
|
121
|
+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
|
122
|
+
* or implied. See the License for the specific language governing permissions
|
|
123
|
+
* and limitations under the License.
|
|
124
|
+
*
|
|
125
|
+
*/
|
|
126
|
+
const ErrorNames = {
|
|
127
|
+
duplicateEndpoint: "DuplicateEndpoint",
|
|
128
|
+
duplicateRequestId: "DuplicateRequestId",
|
|
129
|
+
invalidEndpoint: "InvalidEndpoint",
|
|
130
|
+
invalidTopic: "InvalidTopic",
|
|
131
|
+
unknownEndpoint: "UnknownEndpoint",
|
|
132
|
+
connectionClosed: "ConnectionClosed",
|
|
133
|
+
connectionAborted: "ConnectionAborted",
|
|
134
|
+
connectionFailed: "ConnectionFailed",
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/*
|
|
138
|
+
* Morgan Stanley makes this available to you under the Apache License,
|
|
139
|
+
* Version 2.0 (the "License"). You may obtain a copy of the License at
|
|
140
|
+
* http://www.apache.org/licenses/LICENSE-2.0.
|
|
141
|
+
* See the NOTICE file distributed with this work for additional information
|
|
142
|
+
* regarding copyright ownership. Unless required by applicable law or agreed
|
|
143
|
+
* to in writing, software distributed under the License is distributed on an
|
|
144
|
+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
|
145
|
+
* or implied. See the License for the specific language governing permissions
|
|
146
|
+
* and limitations under the License.
|
|
147
|
+
*
|
|
148
|
+
*/
|
|
149
|
+
class WebSocketConnection {
|
|
150
|
+
options;
|
|
151
|
+
constructor(options) {
|
|
152
|
+
this.options = options;
|
|
153
|
+
}
|
|
154
|
+
connect() {
|
|
155
|
+
return new Promise((resolve, reject) => {
|
|
156
|
+
this.websocket = new WebSocket(this.options.url);
|
|
157
|
+
this.websocket.addEventListener('open', () => {
|
|
158
|
+
this.isConnected = true;
|
|
159
|
+
resolve();
|
|
160
|
+
});
|
|
161
|
+
this.websocket.addEventListener('message', ev => {
|
|
162
|
+
const message = WebSocketConnection.deserializeMessage(ev.data);
|
|
163
|
+
this._onMessage?.call(undefined, message);
|
|
164
|
+
});
|
|
165
|
+
this.websocket.addEventListener('error', ev => {
|
|
166
|
+
if (!this.isConnected) {
|
|
167
|
+
reject();
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
this.isConnected = false;
|
|
171
|
+
this._onError?.call(undefined, new Error());
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
this.websocket.addEventListener('close', () => {
|
|
175
|
+
this.isConnected = false;
|
|
176
|
+
delete this.websocket;
|
|
177
|
+
this._onClose?.call(undefined);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
send(message) {
|
|
182
|
+
if (!this.websocket) {
|
|
183
|
+
return Promise.reject();
|
|
184
|
+
}
|
|
185
|
+
this.websocket.send(JSON.stringify(message));
|
|
186
|
+
return Promise.resolve();
|
|
187
|
+
}
|
|
188
|
+
close() {
|
|
189
|
+
if (this.isConnected) {
|
|
190
|
+
this.websocket?.close(1000, "Closed by client");
|
|
191
|
+
this.isConnected = false;
|
|
192
|
+
delete this.websocket;
|
|
193
|
+
}
|
|
194
|
+
return Promise.resolve();
|
|
195
|
+
}
|
|
196
|
+
onMessage(callback) {
|
|
197
|
+
this._onMessage = callback;
|
|
198
|
+
}
|
|
199
|
+
onError(callback) {
|
|
200
|
+
this._onError = callback;
|
|
201
|
+
}
|
|
202
|
+
onClose(callback) {
|
|
203
|
+
this._onClose = callback;
|
|
204
|
+
}
|
|
205
|
+
static deserializeMessage(data) {
|
|
206
|
+
const msg = JSON.parse(data);
|
|
207
|
+
return msg;
|
|
208
|
+
}
|
|
209
|
+
websocket;
|
|
210
|
+
isConnected = false;
|
|
211
|
+
messageQueue = [];
|
|
212
|
+
_onMessage;
|
|
213
|
+
_onError;
|
|
214
|
+
_onClose;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/*
|
|
218
|
+
* Morgan Stanley makes this available to you under the Apache License,
|
|
219
|
+
* Version 2.0 (the "License"). You may obtain a copy of the License at
|
|
220
|
+
* http://www.apache.org/licenses/LICENSE-2.0.
|
|
221
|
+
* See the NOTICE file distributed with this work for additional information
|
|
222
|
+
* regarding copyright ownership. Unless required by applicable law or agreed
|
|
223
|
+
* to in writing, software distributed under the License is distributed on an
|
|
224
|
+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
|
225
|
+
* or implied. See the License for the specific language governing permissions
|
|
226
|
+
* and limitations under the License.
|
|
227
|
+
*
|
|
228
|
+
*/
|
|
229
|
+
class Deferred {
|
|
230
|
+
constructor() {
|
|
231
|
+
this.promise = new Promise((resolve, reject) => {
|
|
232
|
+
this.resolve =
|
|
233
|
+
(value) => {
|
|
234
|
+
this.settle();
|
|
235
|
+
resolve(value);
|
|
236
|
+
};
|
|
237
|
+
this.reject =
|
|
238
|
+
(reason) => {
|
|
239
|
+
this.settle();
|
|
240
|
+
reject(reason);
|
|
241
|
+
};
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
resolve = () => { };
|
|
245
|
+
reject = () => { };
|
|
246
|
+
promise;
|
|
247
|
+
settle() {
|
|
248
|
+
const noop = () => { };
|
|
249
|
+
this.resolve = noop;
|
|
250
|
+
this.reject = noop;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/*
|
|
255
|
+
* Morgan Stanley makes this available to you under the Apache License,
|
|
256
|
+
* Version 2.0 (the "License"). You may obtain a copy of the License at
|
|
257
|
+
* http://www.apache.org/licenses/LICENSE-2.0.
|
|
258
|
+
* See the NOTICE file distributed with this work for additional information
|
|
259
|
+
* regarding copyright ownership. Unless required by applicable law or agreed
|
|
260
|
+
* to in writing, software distributed under the License is distributed on an
|
|
261
|
+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
|
262
|
+
* or implied. See the License for the specific language governing permissions
|
|
263
|
+
* and limitations under the License.
|
|
264
|
+
*
|
|
265
|
+
*/
|
|
266
|
+
function isProtocolError(err) {
|
|
267
|
+
return (typeof err === "object") && ("name" in err);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/*
|
|
271
|
+
* Morgan Stanley makes this available to you under the Apache License,
|
|
272
|
+
* Version 2.0 (the "License"). You may obtain a copy of the License at
|
|
273
|
+
* http://www.apache.org/licenses/LICENSE-2.0.
|
|
274
|
+
* See the NOTICE file distributed with this work for additional information
|
|
275
|
+
* regarding copyright ownership. Unless required by applicable law or agreed
|
|
276
|
+
* to in writing, software distributed under the License is distributed on an
|
|
277
|
+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
|
278
|
+
* or implied. See the License for the specific language governing permissions
|
|
279
|
+
* and limitations under the License.
|
|
280
|
+
*
|
|
281
|
+
*/
|
|
282
|
+
class MessageRouterError extends Error {
|
|
283
|
+
constructor(err, message, stack) {
|
|
284
|
+
let [name, msg] = isProtocolError(err) ? [err.name, err.message] : [err, message];
|
|
285
|
+
super(msg);
|
|
286
|
+
this.name = name;
|
|
287
|
+
if (stack) {
|
|
288
|
+
this.stack = stack;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
function createProtocolError(err) {
|
|
293
|
+
if (typeof err === "string")
|
|
294
|
+
return {
|
|
295
|
+
name: "Error",
|
|
296
|
+
message: err
|
|
297
|
+
};
|
|
298
|
+
return {
|
|
299
|
+
name: err.name ?? "Error",
|
|
300
|
+
message: err.message ?? `Unknown error (${err})`
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/*
|
|
305
|
+
* Morgan Stanley makes this available to you under the Apache License,
|
|
306
|
+
* Version 2.0 (the "License"). You may obtain a copy of the License at
|
|
307
|
+
* http://www.apache.org/licenses/LICENSE-2.0.
|
|
308
|
+
* See the NOTICE file distributed with this work for additional information
|
|
309
|
+
* regarding copyright ownership. Unless required by applicable law or agreed
|
|
310
|
+
* to in writing, software distributed under the License is distributed on an
|
|
311
|
+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
|
312
|
+
* or implied. See the License for the specific language governing permissions
|
|
313
|
+
* and limitations under the License.
|
|
314
|
+
*
|
|
315
|
+
*/
|
|
316
|
+
class ThrowHelper {
|
|
317
|
+
static duplicateEndpoint(endpoint) {
|
|
318
|
+
return new MessageRouterError({ name: ErrorNames.duplicateEndpoint, message: `Duplicate endpoint registration: '${endpoint}'` });
|
|
319
|
+
}
|
|
320
|
+
static duplicateRequestId() {
|
|
321
|
+
return new MessageRouterError({ name: ErrorNames.duplicateRequestId, message: "Duplicate request ID" });
|
|
322
|
+
}
|
|
323
|
+
static invalidEndpoint(endpoint) {
|
|
324
|
+
return new MessageRouterError({ name: ErrorNames.invalidEndpoint, message: `Invalid endpoint: '${endpoint}'` });
|
|
325
|
+
}
|
|
326
|
+
static invalidTopic(topic) {
|
|
327
|
+
return new MessageRouterError({ name: ErrorNames.invalidTopic, message: `Invalid topic: '${topic}'` });
|
|
328
|
+
}
|
|
329
|
+
static unknownEndpoint(endpoint) {
|
|
330
|
+
return new MessageRouterError({ name: ErrorNames.unknownEndpoint, message: `Unknown endpoint: ${endpoint}` });
|
|
331
|
+
}
|
|
332
|
+
static connectionClosed() {
|
|
333
|
+
return new MessageRouterError({ name: ErrorNames.connectionClosed, message: "The connection has been closed" });
|
|
334
|
+
}
|
|
335
|
+
static connectionFailed(message) {
|
|
336
|
+
return new MessageRouterError({ name: ErrorNames.connectionFailed, message: `Connection failed with message ${message}` });
|
|
337
|
+
}
|
|
338
|
+
static connectionAborted() {
|
|
339
|
+
return new MessageRouterError({ name: ErrorNames.connectionAborted, message: "The connection dropped unexpectedly" });
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/*
|
|
344
|
+
* Morgan Stanley makes this available to you under the Apache License,
|
|
345
|
+
* Version 2.0 (the "License"). You may obtain a copy of the License at
|
|
346
|
+
* http://www.apache.org/licenses/LICENSE-2.0.
|
|
347
|
+
* See the NOTICE file distributed with this work for additional information
|
|
348
|
+
* regarding copyright ownership. Unless required by applicable law or agreed
|
|
349
|
+
* to in writing, software distributed under the License is distributed on an
|
|
350
|
+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
|
351
|
+
* or implied. See the License for the specific language governing permissions
|
|
352
|
+
* and limitations under the License.
|
|
353
|
+
*
|
|
354
|
+
*/
|
|
355
|
+
function isResponse(message) {
|
|
356
|
+
return (message.type === "InvokeResponse"
|
|
357
|
+
|| message.type === "RegisterServiceResponse"
|
|
358
|
+
|| message.type === "UnregisterServiceResponse"
|
|
359
|
+
|| message.type === "SubscribeResponse"
|
|
360
|
+
|| message.type === "UnsubscribeResponse"
|
|
361
|
+
|| message.type === "PublishResponse");
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/*
|
|
365
|
+
* Morgan Stanley makes this available to you under the Apache License,
|
|
366
|
+
* Version 2.0 (the "License"). You may obtain a copy of the License at
|
|
367
|
+
* http://www.apache.org/licenses/LICENSE-2.0.
|
|
368
|
+
* See the NOTICE file distributed with this work for additional information
|
|
369
|
+
* regarding copyright ownership. Unless required by applicable law or agreed
|
|
370
|
+
* to in writing, software distributed under the License is distributed on an
|
|
371
|
+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
|
372
|
+
* or implied. See the License for the specific language governing permissions
|
|
373
|
+
* and limitations under the License.
|
|
374
|
+
*
|
|
375
|
+
*/
|
|
376
|
+
function isConnectResponse(message) {
|
|
377
|
+
return message.type == "ConnectResponse";
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/*
|
|
381
|
+
* Morgan Stanley makes this available to you under the Apache License,
|
|
382
|
+
* Version 2.0 (the "License"). You may obtain a copy of the License at
|
|
383
|
+
* http://www.apache.org/licenses/LICENSE-2.0.
|
|
384
|
+
* See the NOTICE file distributed with this work for additional information
|
|
385
|
+
* regarding copyright ownership. Unless required by applicable law or agreed
|
|
386
|
+
* to in writing, software distributed under the License is distributed on an
|
|
387
|
+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
|
388
|
+
* or implied. See the License for the specific language governing permissions
|
|
389
|
+
* and limitations under the License.
|
|
390
|
+
*
|
|
391
|
+
*/
|
|
392
|
+
function isInvokeRequest(message) {
|
|
393
|
+
return message.type == "Invoke";
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/*
|
|
397
|
+
* Morgan Stanley makes this available to you under the Apache License,
|
|
398
|
+
* Version 2.0 (the "License"). You may obtain a copy of the License at
|
|
399
|
+
* http://www.apache.org/licenses/LICENSE-2.0.
|
|
400
|
+
* See the NOTICE file distributed with this work for additional information
|
|
401
|
+
* regarding copyright ownership. Unless required by applicable law or agreed
|
|
402
|
+
* to in writing, software distributed under the License is distributed on an
|
|
403
|
+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
|
404
|
+
* or implied. See the License for the specific language governing permissions
|
|
405
|
+
* and limitations under the License.
|
|
406
|
+
*
|
|
407
|
+
*/
|
|
408
|
+
function isTopicMessage(message) {
|
|
409
|
+
return message.type == "Topic";
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/*
|
|
413
|
+
* Morgan Stanley makes this available to you under the Apache License,
|
|
414
|
+
* Version 2.0 (the "License"). You may obtain a copy of the License at
|
|
415
|
+
* http://www.apache.org/licenses/LICENSE-2.0.
|
|
416
|
+
* See the NOTICE file distributed with this work for additional information
|
|
417
|
+
* regarding copyright ownership. Unless required by applicable law or agreed
|
|
418
|
+
* to in writing, software distributed under the License is distributed on an
|
|
419
|
+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
|
420
|
+
* or implied. See the License for the specific language governing permissions
|
|
421
|
+
* and limitations under the License.
|
|
422
|
+
*
|
|
423
|
+
*/
|
|
424
|
+
var ClientState;
|
|
425
|
+
(function (ClientState) {
|
|
426
|
+
ClientState[ClientState["Created"] = 0] = "Created";
|
|
427
|
+
ClientState[ClientState["Connecting"] = 1] = "Connecting";
|
|
428
|
+
ClientState[ClientState["Connected"] = 2] = "Connected";
|
|
429
|
+
ClientState[ClientState["Closing"] = 3] = "Closing";
|
|
430
|
+
ClientState[ClientState["Closed"] = 4] = "Closed";
|
|
431
|
+
})(ClientState || (ClientState = {}));
|
|
432
|
+
class MessageRouterClient {
|
|
433
|
+
connection;
|
|
434
|
+
options;
|
|
435
|
+
constructor(connection, options) {
|
|
436
|
+
this.connection = connection;
|
|
437
|
+
this.options = options;
|
|
438
|
+
this.options = options ?? {};
|
|
439
|
+
}
|
|
440
|
+
_clientId;
|
|
441
|
+
get clientId() {
|
|
442
|
+
return this._clientId;
|
|
443
|
+
}
|
|
444
|
+
connect() {
|
|
445
|
+
switch (this._state) {
|
|
446
|
+
case ClientState.Connected:
|
|
447
|
+
return Promise.resolve();
|
|
448
|
+
case ClientState.Created:
|
|
449
|
+
return this.connectCore();
|
|
450
|
+
case ClientState.Connecting:
|
|
451
|
+
return this.connected.promise;
|
|
452
|
+
}
|
|
453
|
+
return Promise.reject(ThrowHelper.connectionClosed());
|
|
454
|
+
}
|
|
455
|
+
close() {
|
|
456
|
+
return this.closeCore();
|
|
457
|
+
}
|
|
458
|
+
async subscribe(topicName, subscriber) {
|
|
459
|
+
this.checkState();
|
|
460
|
+
if (this.pendingUnsubscriptions[topicName]) {
|
|
461
|
+
await this.pendingUnsubscriptions[topicName];
|
|
462
|
+
}
|
|
463
|
+
let needsSubscription = false;
|
|
464
|
+
let topic = this.topics[topicName];
|
|
465
|
+
if (!topic) {
|
|
466
|
+
this.topics[topicName] = topic = new Topic(() => this.unsubscribe(topicName));
|
|
467
|
+
needsSubscription = true;
|
|
468
|
+
}
|
|
469
|
+
if (typeof subscriber === "function") {
|
|
470
|
+
subscriber = { next: subscriber };
|
|
471
|
+
}
|
|
472
|
+
const subscription = topic.subscribe(subscriber);
|
|
473
|
+
if (needsSubscription) {
|
|
474
|
+
try {
|
|
475
|
+
await this.sendRequest({
|
|
476
|
+
requestId: this.getRequestId(),
|
|
477
|
+
type: "Subscribe",
|
|
478
|
+
topic: topicName
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
catch (error) {
|
|
482
|
+
delete this.topics[topicName];
|
|
483
|
+
throw error;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return subscription;
|
|
487
|
+
}
|
|
488
|
+
async publish(topic, payload, options) {
|
|
489
|
+
this.checkState();
|
|
490
|
+
await this.sendRequest({
|
|
491
|
+
type: "Publish",
|
|
492
|
+
requestId: this.getRequestId(),
|
|
493
|
+
topic,
|
|
494
|
+
payload,
|
|
495
|
+
correlationId: options?.correlationId
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
async invoke(endpoint, payload, options) {
|
|
499
|
+
this.checkState();
|
|
500
|
+
const response = await this.sendRequest({
|
|
501
|
+
type: "Invoke",
|
|
502
|
+
requestId: this.getRequestId(),
|
|
503
|
+
endpoint,
|
|
504
|
+
payload,
|
|
505
|
+
correlationId: options?.correlationId
|
|
506
|
+
});
|
|
507
|
+
return response.payload;
|
|
508
|
+
}
|
|
509
|
+
async registerService(endpoint, handler, descriptor) {
|
|
510
|
+
this.checkState();
|
|
511
|
+
if (this.endpointHandlers[endpoint])
|
|
512
|
+
throw ThrowHelper.duplicateEndpoint(endpoint);
|
|
513
|
+
this.endpointHandlers[endpoint] = handler;
|
|
514
|
+
try {
|
|
515
|
+
await this.sendRequest({
|
|
516
|
+
type: "RegisterService",
|
|
517
|
+
requestId: this.getRequestId(),
|
|
518
|
+
endpoint,
|
|
519
|
+
descriptor
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
catch (error) {
|
|
523
|
+
delete this.endpointHandlers[endpoint];
|
|
524
|
+
throw error;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
async unregisterService(endpoint) {
|
|
528
|
+
this.checkState();
|
|
529
|
+
if (!this.endpointHandlers[endpoint])
|
|
530
|
+
return;
|
|
531
|
+
await this.sendRequest({
|
|
532
|
+
type: "UnregisterService",
|
|
533
|
+
requestId: this.getRequestId(),
|
|
534
|
+
endpoint
|
|
535
|
+
});
|
|
536
|
+
delete this.endpointHandlers[endpoint];
|
|
537
|
+
}
|
|
538
|
+
registerEndpoint(endpoint, handler, descriptor) {
|
|
539
|
+
this.checkState();
|
|
540
|
+
if (this.endpointHandlers[endpoint])
|
|
541
|
+
throw ThrowHelper.duplicateEndpoint(endpoint);
|
|
542
|
+
this.endpointHandlers[endpoint] = handler;
|
|
543
|
+
return Promise.resolve();
|
|
544
|
+
}
|
|
545
|
+
unregisterEndpoint(endpoint) {
|
|
546
|
+
this.checkState();
|
|
547
|
+
delete this.endpointHandlers[endpoint];
|
|
548
|
+
return Promise.resolve();
|
|
549
|
+
}
|
|
550
|
+
get state() {
|
|
551
|
+
return this._state;
|
|
552
|
+
}
|
|
553
|
+
_state = ClientState.Created;
|
|
554
|
+
lastRequestId = 0;
|
|
555
|
+
topics = {};
|
|
556
|
+
connected = new Deferred();
|
|
557
|
+
closed = new Deferred();
|
|
558
|
+
pendingRequests = {};
|
|
559
|
+
endpointHandlers = {};
|
|
560
|
+
pendingUnsubscriptions = {};
|
|
561
|
+
async connectCore() {
|
|
562
|
+
this._state = ClientState.Connecting;
|
|
563
|
+
try {
|
|
564
|
+
this.connection.onMessage(this.handleMessage.bind(this));
|
|
565
|
+
this.connection.onError(this.handleError.bind(this));
|
|
566
|
+
this.connection.onClose(this.handleClose.bind(this));
|
|
567
|
+
await this.connection.connect();
|
|
568
|
+
const req = {
|
|
569
|
+
type: "Connect",
|
|
570
|
+
accessToken: this.options.accessToken
|
|
571
|
+
};
|
|
572
|
+
await this.connection.send(req);
|
|
573
|
+
// This must be the last statement before catch so that awaiting `connected`
|
|
574
|
+
// has the same effect as awaiting `connect()`. `close()` also rejects this promise.
|
|
575
|
+
await this.connected.promise;
|
|
576
|
+
}
|
|
577
|
+
catch (error) {
|
|
578
|
+
if (error instanceof MessageRouterError) {
|
|
579
|
+
throw error;
|
|
580
|
+
}
|
|
581
|
+
else {
|
|
582
|
+
await this.closeCore(error);
|
|
583
|
+
throw ThrowHelper.connectionFailed(error.message || error);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
async closeCore(error) {
|
|
588
|
+
error ??= ThrowHelper.connectionClosed();
|
|
589
|
+
switch (this._state) {
|
|
590
|
+
case ClientState.Created:
|
|
591
|
+
{
|
|
592
|
+
this._state = ClientState.Closed;
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
case ClientState.Closed:
|
|
596
|
+
return;
|
|
597
|
+
case ClientState.Closing:
|
|
598
|
+
await this.closed.promise;
|
|
599
|
+
return;
|
|
600
|
+
case ClientState.Connecting:
|
|
601
|
+
{
|
|
602
|
+
this._state = ClientState.Closed;
|
|
603
|
+
this.connected.reject(ThrowHelper.connectionClosed());
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
this._state = ClientState.Closing;
|
|
608
|
+
this.failPendingRequests(error);
|
|
609
|
+
this.failSubscribers(error);
|
|
610
|
+
try {
|
|
611
|
+
await this.connection.close();
|
|
612
|
+
}
|
|
613
|
+
catch (e) {
|
|
614
|
+
console.error(e);
|
|
615
|
+
}
|
|
616
|
+
this._state = ClientState.Closed;
|
|
617
|
+
this.closed.resolve();
|
|
618
|
+
}
|
|
619
|
+
failPendingRequests(error) {
|
|
620
|
+
for (let requestId in this.pendingRequests) {
|
|
621
|
+
this.pendingRequests[requestId].reject(error);
|
|
622
|
+
delete this.pendingRequests[requestId];
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
async failSubscribers(error) {
|
|
626
|
+
for (let topicName in this.topics) {
|
|
627
|
+
const topic = this.topics[topicName];
|
|
628
|
+
topic.error(error);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
async sendMessage(message) {
|
|
632
|
+
await this.connect();
|
|
633
|
+
await this.connection.send(message);
|
|
634
|
+
}
|
|
635
|
+
async sendRequest(request) {
|
|
636
|
+
const deferred = this.pendingRequests[request.requestId] = new Deferred();
|
|
637
|
+
try {
|
|
638
|
+
await this.sendMessage(request);
|
|
639
|
+
}
|
|
640
|
+
catch (error) {
|
|
641
|
+
delete this.pendingRequests[request.requestId];
|
|
642
|
+
throw error;
|
|
643
|
+
}
|
|
644
|
+
return await deferred.promise;
|
|
645
|
+
}
|
|
646
|
+
handleMessage(message) {
|
|
647
|
+
if (isTopicMessage(message)) {
|
|
648
|
+
this.handleTopicMessage(message);
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
if (isResponse(message)) {
|
|
652
|
+
this.handleResponse(message);
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
if (isInvokeRequest(message)) {
|
|
656
|
+
this.handleInvokeRequest(message);
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
if (isConnectResponse(message)) {
|
|
660
|
+
this.handleConnectResponse(message);
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
handleTopicMessage(message) {
|
|
665
|
+
const topic = this.topics[message.topic];
|
|
666
|
+
if (!topic)
|
|
667
|
+
return;
|
|
668
|
+
topic.next({
|
|
669
|
+
topic: message.topic,
|
|
670
|
+
payload: message.payload,
|
|
671
|
+
context: {
|
|
672
|
+
sourceId: message.sourceId,
|
|
673
|
+
correlationId: message.correlationId
|
|
674
|
+
}
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
handleResponse(message) {
|
|
678
|
+
const request = this.pendingRequests[message.requestId];
|
|
679
|
+
if (!request)
|
|
680
|
+
return;
|
|
681
|
+
if (message.error) {
|
|
682
|
+
request.reject(new MessageRouterError(message.error));
|
|
683
|
+
}
|
|
684
|
+
else {
|
|
685
|
+
request.resolve(message);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
async handleInvokeRequest(message) {
|
|
689
|
+
try {
|
|
690
|
+
const handler = this.endpointHandlers[message.endpoint];
|
|
691
|
+
if (!handler)
|
|
692
|
+
throw ThrowHelper.unknownEndpoint(message.endpoint);
|
|
693
|
+
const result = await handler(message.endpoint, message.payload, {
|
|
694
|
+
sourceId: message.sourceId,
|
|
695
|
+
correlationId: message.correlationId
|
|
696
|
+
});
|
|
697
|
+
await this.sendMessage({
|
|
698
|
+
type: "InvokeResponse",
|
|
699
|
+
requestId: message.requestId,
|
|
700
|
+
payload: typeof result === "string" ? result : undefined
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
catch (error) {
|
|
704
|
+
await this.sendMessage({
|
|
705
|
+
type: "InvokeResponse",
|
|
706
|
+
requestId: message.requestId,
|
|
707
|
+
error: createProtocolError(error)
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
handleConnectResponse(message) {
|
|
712
|
+
if (message.error) {
|
|
713
|
+
this._state = ClientState.Closed;
|
|
714
|
+
this.connected.reject(new MessageRouterError(message.error));
|
|
715
|
+
}
|
|
716
|
+
else {
|
|
717
|
+
this._clientId = message.clientId;
|
|
718
|
+
this._state = ClientState.Connected;
|
|
719
|
+
this.connected.resolve();
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
checkState() {
|
|
723
|
+
if (this._state == ClientState.Closed || this._state == ClientState.Closing) {
|
|
724
|
+
throw ThrowHelper.connectionClosed();
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
handleError(error) {
|
|
728
|
+
switch (this._state) {
|
|
729
|
+
case ClientState.Closing:
|
|
730
|
+
case ClientState.Closed:
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
this.closeCore(error);
|
|
734
|
+
}
|
|
735
|
+
handleClose() {
|
|
736
|
+
this.handleError(ThrowHelper.connectionAborted());
|
|
737
|
+
}
|
|
738
|
+
async unsubscribe(topicName) {
|
|
739
|
+
let topic = this.topics[topicName];
|
|
740
|
+
if (!topic)
|
|
741
|
+
return;
|
|
742
|
+
if (this.pendingUnsubscriptions[topicName]) {
|
|
743
|
+
await this.pendingUnsubscriptions[topicName];
|
|
744
|
+
}
|
|
745
|
+
this.pendingUnsubscriptions[topicName] = this.sendRequest({
|
|
746
|
+
requestId: this.getRequestId(),
|
|
747
|
+
type: "Unsubscribe",
|
|
748
|
+
topic: topicName
|
|
749
|
+
})
|
|
750
|
+
.then(() => {
|
|
751
|
+
delete this.topics[topicName];
|
|
752
|
+
})
|
|
753
|
+
.catch(error => {
|
|
754
|
+
console.error("Exception thrown while unsubscribing.", error);
|
|
755
|
+
throw error;
|
|
756
|
+
})
|
|
757
|
+
.finally(() => {
|
|
758
|
+
delete this.pendingUnsubscriptions[topicName];
|
|
759
|
+
});
|
|
760
|
+
await this.pendingUnsubscriptions[topicName];
|
|
761
|
+
}
|
|
762
|
+
getRequestId() {
|
|
763
|
+
return '' + (++this.lastRequestId);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
class Topic {
|
|
767
|
+
constructor(onUnsubscribe) {
|
|
768
|
+
this.onUnsubscribe = onUnsubscribe;
|
|
769
|
+
}
|
|
770
|
+
subscribe(subscriber) {
|
|
771
|
+
if (this.isCompleted)
|
|
772
|
+
return {
|
|
773
|
+
unsubscribe: () => { }
|
|
774
|
+
};
|
|
775
|
+
this.subscribers.push(subscriber);
|
|
776
|
+
return {
|
|
777
|
+
unsubscribe: () => this.unsubscribe(subscriber)
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
unsubscribe(subscriber) {
|
|
781
|
+
if (this.isCompleted)
|
|
782
|
+
return;
|
|
783
|
+
const idx = this.subscribers.lastIndexOf(subscriber);
|
|
784
|
+
if (idx < 0)
|
|
785
|
+
return;
|
|
786
|
+
this.subscribers.splice(idx, 1);
|
|
787
|
+
if (this.subscribers.length == 0) {
|
|
788
|
+
this.onUnsubscribe();
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
next(message) {
|
|
792
|
+
if (this.isCompleted) {
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
for (let subscriber of this.subscribers) {
|
|
796
|
+
try {
|
|
797
|
+
subscriber.next?.call(subscriber, message);
|
|
798
|
+
}
|
|
799
|
+
catch (error) {
|
|
800
|
+
console.error(error);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
error(error) {
|
|
805
|
+
if (this.isCompleted)
|
|
806
|
+
return;
|
|
807
|
+
this.isCompleted = true;
|
|
808
|
+
for (let subscriber of this.subscribers) {
|
|
809
|
+
try {
|
|
810
|
+
subscriber.error?.call(subscriber, error);
|
|
811
|
+
}
|
|
812
|
+
catch (e) {
|
|
813
|
+
console.error(e);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
complete() {
|
|
818
|
+
if (this.isCompleted)
|
|
819
|
+
return;
|
|
820
|
+
for (let subscriber of this.subscribers) {
|
|
821
|
+
try {
|
|
822
|
+
subscriber.complete?.call(subscriber);
|
|
823
|
+
}
|
|
824
|
+
catch (e) {
|
|
825
|
+
console.error(e);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
isCompleted = false;
|
|
830
|
+
onUnsubscribe;
|
|
831
|
+
subscribers = [];
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
/*
|
|
835
|
+
* Morgan Stanley makes this available to you under the Apache License,
|
|
836
|
+
* Version 2.0 (the "License"). You may obtain a copy of the License at
|
|
837
|
+
* http://www.apache.org/licenses/LICENSE-2.0.
|
|
838
|
+
* See the NOTICE file distributed with this work for additional information
|
|
839
|
+
* regarding copyright ownership. Unless required by applicable law or agreed
|
|
840
|
+
* to in writing, software distributed under the License is distributed on an
|
|
841
|
+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
|
842
|
+
* or implied. See the License for the specific language governing permissions
|
|
843
|
+
* and limitations under the License.
|
|
844
|
+
*
|
|
845
|
+
*/
|
|
846
|
+
function createMessageRouter(config) {
|
|
847
|
+
config ??= window.composeui?.messageRouterConfig;
|
|
848
|
+
if (config?.webSocket) {
|
|
849
|
+
const connection = new WebSocketConnection(config.webSocket);
|
|
850
|
+
return new MessageRouterClient(connection, config);
|
|
851
|
+
}
|
|
852
|
+
throw ConfigNotFound();
|
|
853
|
+
function ConfigNotFound() {
|
|
854
|
+
return new Error("Unable to create the MessageRouter client, configuration is missing.");
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
window.composeui.messaging.communicator = new MessageRouterMessaging(createMessageRouter());
|
|
859
|
+
|
|
860
|
+
})();
|