@openfin/cloud-interop-core-api 0.0.1-alpha.979385f → 0.0.1-alpha.97cea20

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/dist/index.cjs DELETED
@@ -1,473 +0,0 @@
1
- 'use strict';
2
-
3
- var mqtt = require('mqtt');
4
-
5
- class CloudInteropAPIError extends Error {
6
- code;
7
- constructor(message = 'An unexpected error has occurred', code = 'UNEXPECTED_ERROR', cause) {
8
- super(message, { cause: cause });
9
- this.name = this.constructor.name;
10
- this.code = code;
11
- }
12
- }
13
- class AuthorizationError extends CloudInteropAPIError {
14
- constructor(message = 'Not authorized', code = 'ERR_UNAUTHORIZED') {
15
- super(message, code, undefined);
16
- }
17
- }
18
-
19
- // Error codes as defined in https://docs.emqx.com/en/cloud/latest/connect_to_deployments/mqtt_client_error_codes.html
20
- const BadUserNamePasswordError = 134;
21
- /**
22
- * Represents a single connection to a Cloud Interop service
23
- *
24
- * @export
25
- * @class CloudInteropAPI
26
- * @implements {Client}
27
- */
28
- class CloudInteropAPI {
29
- #cloudInteropSettings;
30
- #sessionDetails;
31
- #mqttClient;
32
- #reconnectRetryLimit = 30;
33
- #keepAliveIntervalSeconds = 30;
34
- #logger = (level, message) => {
35
- console[level](message);
36
- };
37
- #reconnectRetries = 0;
38
- #connectionParams;
39
- #eventListeners = new Map();
40
- #attemptingToReconnect = false;
41
- #currentIntentDiscoveryId;
42
- constructor(cloudInteropSettings) {
43
- this.#cloudInteropSettings = cloudInteropSettings;
44
- }
45
- get sessionDetails() {
46
- return this.#sessionDetails;
47
- }
48
- get mqttClient() {
49
- return this.#mqttClient;
50
- }
51
- /**
52
- * Connects and creates a session on the Cloud Interop service
53
- *
54
- * @param {ConnectParameters} parameters - The parameters to use to connect
55
- * @return {*} {Promise<void>}
56
- * @memberof CloudInteropAPI
57
- * @throws {CloudInteropAPIError} - If an error occurs during connection
58
- * @throws {AuthorizationError} - If the connection is unauthorized
59
- */
60
- async connect(parameters) {
61
- this.#validateConnectParams(parameters);
62
- this.#connectionParams = parameters;
63
- this.#reconnectRetryLimit = parameters.reconnectRetryLimit || this.#reconnectRetryLimit;
64
- this.#keepAliveIntervalSeconds = parameters.keepAliveIntervalSeconds || this.#keepAliveIntervalSeconds;
65
- this.#logger = parameters.logger || this.#logger;
66
- const { sourceId, platformId } = this.#connectionParams;
67
- const createSessionResponse = await fetch(`${this.#cloudInteropSettings.url}/api/sessions`, {
68
- method: 'POST',
69
- headers: this.#getRequestHeaders(),
70
- body: JSON.stringify({ sourceId, platformId }),
71
- });
72
- if (!createSessionResponse.ok) {
73
- if (createSessionResponse.status === 401 || createSessionResponse.status === 403) {
74
- throw new AuthorizationError();
75
- }
76
- throw new CloudInteropAPIError();
77
- }
78
- if (createSessionResponse.status !== 201) {
79
- throw new CloudInteropAPIError(`Failed to connect to the Cloud Interop service: ${this.#cloudInteropSettings.url}`, 'ERR_CONNECT', new Error(createSessionResponse.statusText));
80
- }
81
- this.#sessionDetails = (await createSessionResponse.json());
82
- const sessionRootTopic = this.#sessionDetails.sessionRootTopic;
83
- const clientOptions = {
84
- keepalive: this.#keepAliveIntervalSeconds,
85
- clientId: this.#sessionDetails.sessionId,
86
- clean: true,
87
- protocolVersion: 5,
88
- // The "will" message will be published on an unexpected disconnection
89
- // The server can then tidy up. So it needs every for this client to do that, the session details is perfect
90
- will: {
91
- topic: 'interop/lastwill',
92
- payload: Buffer.from(JSON.stringify(this.#sessionDetails)),
93
- qos: 0,
94
- retain: false,
95
- },
96
- username: this.#sessionDetails.token,
97
- };
98
- this.#mqttClient = await mqtt.connectAsync(this.#sessionDetails.url, clientOptions);
99
- this.#logger('log', `Cloud Interop successfully connected to ${this.#cloudInteropSettings.url}`);
100
- this.#mqttClient.on('error', async (error) => {
101
- // We will receive errors for each failed reconnection attempt
102
- // We don't won't to disconnect on these else we will never reconnect
103
- if (!this.#attemptingToReconnect) {
104
- await this.#disconnect(false);
105
- }
106
- if (error instanceof mqtt.ErrorWithReasonCode) {
107
- switch (error.code) {
108
- case BadUserNamePasswordError: {
109
- await this.#disconnect(false);
110
- this.#logger('warn', `Session expired`);
111
- this.#emitEvent('session-expired');
112
- return;
113
- }
114
- default: {
115
- this.#logger('error', `Unknown Infrastructure Error Code ${error.code} : ${error.message}${this.#attemptingToReconnect ? ' during reconnection attempt' : ''}`);
116
- // As we are in the middle of a reconnect, lets not emit an error to cut down on the event noise
117
- if (!this.#attemptingToReconnect) {
118
- this.#emitEvent('error', new CloudInteropAPIError(`Unknown Infrastructure Error Code ${error.code} : ${error.message}`, 'ERR_INFRASTRUCTURE', error));
119
- break;
120
- }
121
- }
122
- }
123
- }
124
- else {
125
- this.#logger('error', `Unknown Error${this.#attemptingToReconnect ? ' during reconnection attempt' : ''}: ${error}`);
126
- // As we are in the middle of a reconnect, lets not emit an error to cut down on the event noise
127
- if (!this.#attemptingToReconnect) {
128
- this.#emitEvent('error', new CloudInteropAPIError(`Unknown Error`, 'ERR_UNKNOWN', error));
129
- }
130
- }
131
- });
132
- this.#mqttClient.on('reconnect', () => {
133
- this.#attemptingToReconnect = true;
134
- this.#reconnectRetries += 1;
135
- this.#logger('debug', `Cloud Interop attempting reconnection - ${this.#reconnectRetries}...`);
136
- if (this.#reconnectRetries === this.#reconnectRetryLimit) {
137
- this.#logger('warn', `Cloud Interop reached max reconnection attempts - ${this.#reconnectRetryLimit}...`);
138
- this.#disconnect(true);
139
- }
140
- this.#emitEvent('reconnecting', this.#reconnectRetries);
141
- });
142
- // Does not fire on initial connection, only successful reconnection attempts
143
- this.#mqttClient.on('connect', () => {
144
- this.#logger('debug', `Cloud Interop successfully reconnected after ${this.#reconnectRetries} attempts`);
145
- this.#reconnectRetries = 0;
146
- this.#attemptingToReconnect = false;
147
- this.#emitEvent('reconnected');
148
- });
149
- this.#mqttClient.on('message', (topic, message) => {
150
- if (!this.#sessionDetails) {
151
- this.#logger('warn', 'Received message when session not connected');
152
- return;
153
- }
154
- this.#handleMessage(topic, message, this.#sessionDetails);
155
- });
156
- // Subscribe to all context groups
157
- this.#mqttClient.subscribe(`${sessionRootTopic}/context-groups/#`);
158
- // Listen out for global commands
159
- this.#mqttClient.subscribe(`${sessionRootTopic}/commands`);
160
- }
161
- /**
162
- * Disconnects from the Cloud Interop service
163
- *
164
- * @return {*} {Promise<void>}
165
- * @memberof CloudInteropAPI
166
- * @throws {CloudInteropAPIError} - If an error occurs during disconnection
167
- */
168
- async disconnect() {
169
- await this.#disconnect(true);
170
- }
171
- /**
172
- * Publishes a new context for the given context group to the other connected sessions
173
- *
174
- * @param {string} contextGroup - The context group to publish to
175
- * @param {object} context - The context to publish
176
- * @return {*} {Promise<void>}
177
- * @memberof CloudInteropAPI
178
- */
179
- async setContext(contextGroup, context) {
180
- if (!this.#sessionDetails || !this.#connectionParams) {
181
- throw new Error('Session not connected');
182
- }
183
- const payload = {
184
- context,
185
- timestamp: Date.now(),
186
- };
187
- const postResponse = await fetch(`${this.#cloudInteropSettings.url}/api/context-groups/${this.#sessionDetails.sessionId}/${contextGroup}`, {
188
- method: 'POST',
189
- headers: this.#getRequestHeaders(),
190
- body: JSON.stringify(payload),
191
- });
192
- if (!postResponse.ok) {
193
- throw new CloudInteropAPIError(`Error setting context for ${contextGroup}`, 'ERR_SETTING_CONTEXT', new Error(postResponse.statusText));
194
- }
195
- }
196
- /**
197
- * Starts an intent discovery operation
198
- *
199
- * @return {*} {Promise<void>}
200
- * @memberof CloudInteropAPI
201
- * @throws {CloudInteropAPIError} - If an error occurs during intent discovery
202
- */
203
- async startIntentDiscovery() {
204
- if (!this.#sessionDetails || !this.#connectionParams) {
205
- throw new Error('Session not connected');
206
- }
207
- if (!this.#mqttClient) {
208
- throw new Error('MQTT client not connected');
209
- }
210
- if (this.#currentIntentDiscoveryId) {
211
- throw new Error('Intent discovery already in progress');
212
- }
213
- try {
214
- const startResponse = await fetch(`${this.#cloudInteropSettings.url}/api/intents/${this.#sessionDetails.sessionId}`, {
215
- method: 'POST',
216
- headers: this.#getRequestHeaders(),
217
- });
218
- if (!startResponse.ok) {
219
- throw new Error(startResponse.statusText);
220
- }
221
- const json = await startResponse.json();
222
- this.#currentIntentDiscoveryId = json.discoveryId;
223
- // Listen out for discovery results directly send to us
224
- await this.#mqttClient.subscribeAsync(`${this.#sessionDetails.sessionRootTopic}/commands/${this.#currentIntentDiscoveryId}`);
225
- }
226
- catch (error) {
227
- throw new CloudInteropAPIError('Error starting intent discovery', 'ERR_STARTING_INTENT_DISCOVERY', error);
228
- }
229
- }
230
- /**
231
- * Ends an intent discovery operation
232
- *
233
- * @return {*} {Promise<void>}
234
- * @memberof CloudInteropAPI
235
- * @throws {CloudInteropAPIError} - If an error occurs during stopping intent discovery
236
- */
237
- async endIntentDiscovery() {
238
- if (!this.#sessionDetails || !this.#connectionParams) {
239
- throw new Error('Session not connected');
240
- }
241
- if (!this.#currentIntentDiscoveryId) {
242
- throw new Error('Intent discovery not already in progress');
243
- }
244
- if (!this.#mqttClient) {
245
- throw new Error('MQTT client not connected');
246
- }
247
- try {
248
- await this.#mqttClient.unsubscribeAsync(`${this.#sessionDetails.sessionRootTopic}/commands/${this.#currentIntentDiscoveryId}`);
249
- const deleteResponse = await fetch(`${this.#cloudInteropSettings.url}/api/intents/${this.#sessionDetails.sessionId}/${this.#currentIntentDiscoveryId}`, {
250
- method: 'DELETE',
251
- headers: this.#getRequestHeaders(),
252
- });
253
- if (!deleteResponse.ok) {
254
- throw new Error(deleteResponse.statusText);
255
- }
256
- this.#currentIntentDiscoveryId = undefined;
257
- }
258
- catch (error) {
259
- throw new CloudInteropAPIError('Error ending intent discovery', 'ERR_ENDING_INTENT_DISCOVERY', error);
260
- }
261
- }
262
- async raiseIntent(intent, targetSessionId) {
263
- if (!this.#sessionDetails || !this.#connectionParams) {
264
- throw new Error('Session not connected');
265
- }
266
- const postResponse = await fetch(`${this.#cloudInteropSettings.url}/api/intents/${this.#sessionDetails.sessionId}/sessions/${targetSessionId}`, {
267
- method: 'POST',
268
- headers: this.#getRequestHeaders(),
269
- body: JSON.stringify({ intent }),
270
- });
271
- if (!postResponse.ok) {
272
- throw new CloudInteropAPIError(`Error raising intent: ${intent.intentMetadata.name}`, 'ERR_RAISING_INTENT', new Error(postResponse.statusText));
273
- }
274
- }
275
- async reportSupportedIntents(discoveryId, intents) {
276
- if (!this.#sessionDetails || !this.#connectionParams) {
277
- throw new Error('Session not connected');
278
- }
279
- const reportResponse = await fetch(`${this.#cloudInteropSettings.url}/api/intents/${this.#sessionDetails.sessionId}/${discoveryId}`, {
280
- method: 'POST',
281
- headers: this.#getRequestHeaders(),
282
- body: JSON.stringify({ intents }),
283
- });
284
- if (!reportResponse.ok) {
285
- throw new CloudInteropAPIError('Error starting intent discovery', 'ERR_REPORTING_INTENTS', new Error(reportResponse.statusText));
286
- }
287
- }
288
- async sendIntentResult(resultEvent) {
289
- if (!this.#sessionDetails || !this.#connectionParams) {
290
- throw new Error('Session not connected');
291
- }
292
- const { initiatingSessionId, result, sessionId } = resultEvent;
293
- const resultResponse = await fetch(`${this.#cloudInteropSettings.url}/api/intents/${initiatingSessionId}/result/${sessionId}`, {
294
- method: 'POST',
295
- headers: this.#getRequestHeaders(),
296
- body: JSON.stringify({ result }),
297
- });
298
- if (!resultResponse.ok) {
299
- throw new CloudInteropAPIError('Error sending intent result', 'ERR_SENDING_INTENT_RESULT', new Error(resultResponse.statusText));
300
- }
301
- }
302
- addEventListener(type, callback) {
303
- const listeners = this.#eventListeners.get(type) || [];
304
- listeners.push(callback);
305
- this.#eventListeners.set(type, listeners);
306
- }
307
- removeEventListener(type, callback) {
308
- const listeners = this.#eventListeners.get(type) || [];
309
- const index = listeners.indexOf(callback);
310
- if (index !== -1) {
311
- listeners.splice(index, 1);
312
- }
313
- this.#eventListeners.set(type, listeners);
314
- }
315
- async #disconnect(fireDisconnectedEvent) {
316
- if (!this.#sessionDetails) {
317
- return;
318
- }
319
- try {
320
- const disconnectResponse = await fetch(`${this.#cloudInteropSettings.url}/api/sessions/${this.#sessionDetails.sessionId}`, {
321
- method: 'DELETE',
322
- headers: this.#getRequestHeaders(),
323
- });
324
- if (disconnectResponse.status !== 200) {
325
- throw new CloudInteropAPIError('Error during session tear down - unexpected status', 'ERR_DISCONNECT', new Error(disconnectResponse.statusText));
326
- }
327
- }
328
- catch (error) {
329
- throw new CloudInteropAPIError('Error during disconnection', 'ERR_DISCONNECT', error);
330
- }
331
- finally {
332
- this.#mqttClient?.removeAllListeners();
333
- await this.#mqttClient?.endAsync(true);
334
- this.#sessionDetails = undefined;
335
- this.#mqttClient = undefined;
336
- this.#reconnectRetries = 0;
337
- this.#attemptingToReconnect = false;
338
- this.#currentIntentDiscoveryId = undefined;
339
- if (fireDisconnectedEvent) {
340
- this.#emitEvent('disconnected');
341
- }
342
- }
343
- }
344
- #handleMessage(topic, message, sessionDetails) {
345
- if (message.length === 0 || !sessionDetails) {
346
- // Ignore clean up messages
347
- return;
348
- }
349
- const messageEnvelope = JSON.parse(message.toString());
350
- if (topic.startsWith(`${sessionDetails.sessionRootTopic}/context-groups/`)) {
351
- if (messageEnvelope.source.sessionId === sessionDetails.sessionId) {
352
- return;
353
- }
354
- const { channelName: contextGroup, payload: context, source, history } = messageEnvelope;
355
- this.#emitEvent('context', { contextGroup, context, source, history: { ...history, clientReceived: Date.now() } });
356
- }
357
- else if (topic.startsWith(`${sessionDetails.sessionRootTopic}/commands`)) {
358
- this.#handleCommandMessage(messageEnvelope);
359
- }
360
- }
361
- #handleCommandMessage(message) {
362
- switch (message.command) {
363
- case 'report-intents': {
364
- if (message.initiatingSessionId === this.#sessionDetails?.sessionId) {
365
- // Ignore if this originated from us
366
- return;
367
- }
368
- this.#emitEvent('report-intents', {
369
- discoveryId: message.discoveryId,
370
- initiatingSessionId: message.initiatingSessionId,
371
- sessionId: message.sessionId,
372
- });
373
- break;
374
- }
375
- case 'intent-details': {
376
- if (message.discoveryId !== this.#currentIntentDiscoveryId) {
377
- // Ignore if its any other discovery id for some reason
378
- return;
379
- }
380
- this.#emitEvent('intent-details', {
381
- discoveryId: message.discoveryId,
382
- initiatingSessionId: message.initiatingSessionId,
383
- sessionId: message.sessionId,
384
- intents: message.intents,
385
- });
386
- break;
387
- }
388
- case 'end-report-intents': {
389
- if (message.initiatingSessionId === this.#sessionDetails?.sessionId) {
390
- // Ignore if this originated from us
391
- return;
392
- }
393
- this.#emitEvent('end-report-intents', {
394
- discoveryId: message.discoveryId,
395
- initiatingSessionId: message.initiatingSessionId,
396
- sessionId: message.sessionId,
397
- });
398
- break;
399
- }
400
- case 'invoke-intent': {
401
- if (message.sessionId === this.#sessionDetails?.sessionId) {
402
- this.#emitEvent('invoke-intent', {
403
- initiatingSessionId: message.initiatingSessionId,
404
- intent: message.intent,
405
- sessionId: message.sessionId,
406
- });
407
- }
408
- break;
409
- }
410
- case 'intent-result': {
411
- if (message.initiatingSessionId === this.#sessionDetails?.sessionId) {
412
- // Return result to originator and end discovery
413
- const { command: _, ...resultEvent } = message;
414
- this.#emitEvent('intent-result', resultEvent);
415
- this.endIntentDiscovery().catch(() => undefined);
416
- }
417
- break;
418
- }
419
- default: {
420
- this.#logger('warn', `Unknown command message received: ${message}`);
421
- break;
422
- }
423
- }
424
- }
425
- #emitEvent(type, ...args) {
426
- const listeners = this.#eventListeners.get(type) || [];
427
- listeners.forEach((listener) => listener(...args));
428
- }
429
- #validateConnectParams = (parameters) => {
430
- if (!parameters) {
431
- throw new Error('Connect parameters must be provided');
432
- }
433
- if (!parameters.sourceId) {
434
- throw new Error('sourceId must be provided');
435
- }
436
- if (!parameters.platformId) {
437
- throw new Error('platformId must be provided');
438
- }
439
- if (parameters.authenticationType === 'jwt' &&
440
- (!parameters.jwtAuthenticationParameters?.jwtRequestCallback || !parameters.jwtAuthenticationParameters?.authenticationId)) {
441
- throw new Error('jwtAuthenticationParameters must be provided when using jwt authentication');
442
- }
443
- if (parameters.authenticationType === 'basic' &&
444
- (!parameters.basicAuthenticationParameters?.username || !parameters.basicAuthenticationParameters?.password)) {
445
- throw new Error('basicAuthenticationParameters must be provided when using basic authentication');
446
- }
447
- };
448
- #getRequestHeaders = () => {
449
- if (!this.#connectionParams) {
450
- throw new Error('Connect parameters must be provided');
451
- }
452
- const headers = {};
453
- headers['Content-Type'] = 'application/json';
454
- if (this.#connectionParams.authenticationType === 'jwt' && this.#connectionParams.jwtAuthenticationParameters) {
455
- const tokenResult = this.#connectionParams.jwtAuthenticationParameters.jwtRequestCallback();
456
- if (!tokenResult) {
457
- throw new Error('jwtRequestCallback must return a token');
458
- }
459
- headers['x-of-auth-id'] = this.#connectionParams.jwtAuthenticationParameters.authenticationId;
460
- headers['Authorization'] =
461
- typeof tokenResult === 'string' ? `Bearer ${tokenResult}` : `Bearer ${Buffer.from(JSON.stringify(tokenResult)).toString('base64')}`;
462
- }
463
- if (this.#connectionParams.authenticationType === 'basic' && this.#connectionParams.basicAuthenticationParameters) {
464
- const { username, password } = this.#connectionParams.basicAuthenticationParameters;
465
- headers['Authorization'] = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
466
- }
467
- return headers;
468
- };
469
- }
470
-
471
- exports.AuthorizationError = AuthorizationError;
472
- exports.CloudInteropAPI = CloudInteropAPI;
473
- exports.CloudInteropAPIError = CloudInteropAPIError;