@openfin/cloud-interop-core-api 0.0.1-alpha.94bf3ec → 0.0.1-alpha.9655dc9

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 CHANGED
@@ -1,6 +1,5 @@
1
1
  'use strict';
2
2
 
3
- var axios = require('axios');
4
3
  var mqtt = require('mqtt');
5
4
 
6
5
  class CloudInteropAPIError extends Error {
@@ -39,6 +38,7 @@ class CloudInteropAPI {
39
38
  #connectionParams;
40
39
  #eventListeners = new Map();
41
40
  #attemptingToReconnect = false;
41
+ #currentIntentDiscoveryId;
42
42
  constructor(cloudInteropSettings) {
43
43
  this.#cloudInteropSettings = cloudInteropSettings;
44
44
  }
@@ -64,105 +64,99 @@ class CloudInteropAPI {
64
64
  this.#keepAliveIntervalSeconds = parameters.keepAliveIntervalSeconds || this.#keepAliveIntervalSeconds;
65
65
  this.#logger = parameters.logger || this.#logger;
66
66
  const { sourceId, platformId } = this.#connectionParams;
67
- try {
68
- const createSessionResponse = await axios.post(`${this.#cloudInteropSettings.url}/api/sessions`, {
69
- sourceId,
70
- platformId,
71
- }, {
72
- headers: this.#getRequestHeaders(),
73
- });
74
- if (createSessionResponse.status !== 201) {
75
- throw new CloudInteropAPIError(`Failed to connect to the Cloud Interop service: ${this.#cloudInteropSettings.url}`, 'ERR_CONNECT', createSessionResponse.status);
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();
76
75
  }
77
- this.#sessionDetails = createSessionResponse.data;
78
- const sessionRootTopic = this.#sessionDetails.sessionRootTopic;
79
- const clientOptions = {
80
- keepalive: this.#keepAliveIntervalSeconds,
81
- clientId: this.#sessionDetails.sessionId,
82
- clean: true,
83
- protocolVersion: 5,
84
- // The "will" message will be published on an unexpected disconnection
85
- // The server can then tidy up. So it needs every for this client to do that, the session details is perfect
86
- will: {
87
- topic: 'interop/lastwill',
88
- payload: Buffer.from(JSON.stringify(this.#sessionDetails)),
89
- qos: 0,
90
- retain: false,
91
- },
92
- username: this.#sessionDetails.token,
93
- };
94
- this.#mqttClient = await mqtt.connectAsync(this.#sessionDetails.url, clientOptions);
95
- this.#logger('log', `Cloud Interop successfully connected to ${this.#cloudInteropSettings.url}`);
96
- this.#mqttClient.on('error', async (error) => {
97
- // We will receive errors for each failed reconnection attempt
98
- // We don't won't to disconnect on these else we will never reconnect
99
- if (!this.#attemptingToReconnect) {
100
- await this.#disconnect(false);
101
- }
102
- if (error instanceof mqtt.ErrorWithReasonCode) {
103
- switch (error.code) {
104
- case BadUserNamePasswordError: {
105
- await this.#disconnect(false);
106
- this.#logger('warn', `Session expired`);
107
- this.#emitEvent('session-expired');
108
- return;
109
- }
110
- default: {
111
- this.#logger('error', `Unknown Infrastructure Error Code ${error.code} : ${error.message}${this.#attemptingToReconnect ? ' during reconnection attempt' : ''}`);
112
- // As we are in the middle of a reconnect, lets not emit an error to cut down on the event noise
113
- if (!this.#attemptingToReconnect) {
114
- this.#emitEvent('error', new CloudInteropAPIError(`Unknown Infrastructure Error Code ${error.code} : ${error.message}`, 'ERR_INFRASTRUCTURE', error));
115
- break;
116
- }
117
- }
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;
118
113
  }
119
- }
120
- else {
121
- this.#logger('error', `Unknown Error${this.#attemptingToReconnect ? ' during reconnection attempt' : ''}: ${error}`);
122
- // As we are in the middle of a reconnect, lets not emit an error to cut down on the event noise
123
- if (!this.#attemptingToReconnect) {
124
- this.#emitEvent('error', new CloudInteropAPIError(`Unknown Error`, 'ERR_UNKNOWN', error));
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
+ }
125
121
  }
126
122
  }
127
- });
128
- this.#mqttClient.on('reconnect', () => {
129
- this.#attemptingToReconnect = true;
130
- this.#reconnectRetries += 1;
131
- this.#logger('debug', `Cloud Interop attempting reconnection - ${this.#reconnectRetries}...`);
132
- if (this.#reconnectRetries === this.#reconnectRetryLimit) {
133
- this.#logger('warn', `Cloud Interop reached max reconnection attempts - ${this.#reconnectRetryLimit}...`);
134
- this.#disconnect(true);
135
- }
136
- this.#emitEvent('reconnecting', this.#reconnectRetries);
137
- });
138
- // Does not fire on initial connection, only successful reconnection attempts
139
- this.#mqttClient.on('connect', () => {
140
- this.#logger('debug', `Cloud Interop successfully reconnected after ${this.#reconnectRetries} attempts`);
141
- this.#reconnectRetries = 0;
142
- this.#attemptingToReconnect = false;
143
- this.#emitEvent('reconnected');
144
- });
145
- this.#mqttClient.on('message', (topic, message) => {
146
- if (!this.#sessionDetails) {
147
- this.#logger('warn', 'Received message when session not connected');
148
- return;
149
- }
150
- this.#handleCommand(topic, message, this.#sessionDetails);
151
- });
152
- // Subscribe to all context groups
153
- this.#mqttClient.subscribe(`${sessionRootTopic}/context-groups/#`);
154
- // Listen out for global commands
155
- this.#mqttClient.subscribe(`${sessionRootTopic}/commands`);
156
- }
157
- catch (error) {
158
- if (axios.isAxiosError(error)) {
159
- if (error.response?.status === 401 || error.response?.status === 403) {
160
- throw new AuthorizationError();
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));
161
129
  }
162
- throw new CloudInteropAPIError();
163
130
  }
164
- throw error;
165
- }
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`);
166
160
  }
167
161
  /**
168
162
  * Disconnects from the Cloud Interop service
@@ -190,9 +184,120 @@ class CloudInteropAPI {
190
184
  context,
191
185
  timestamp: Date.now(),
192
186
  };
193
- await axios.post(`${this.#cloudInteropSettings.url}/api/context-groups/${this.#sessionDetails.sessionId}/${contextGroup}`, payload, {
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',
194
295
  headers: this.#getRequestHeaders(),
296
+ body: JSON.stringify({ result }),
195
297
  });
298
+ if (!resultResponse.ok) {
299
+ throw new CloudInteropAPIError('Error sending intent result', 'ERR_SENDING_INTENT_RESULT', new Error(resultResponse.statusText));
300
+ }
196
301
  }
197
302
  addEventListener(type, callback) {
198
303
  const listeners = this.#eventListeners.get(type) || [];
@@ -212,15 +317,16 @@ class CloudInteropAPI {
212
317
  return;
213
318
  }
214
319
  try {
215
- const disconnectResponse = await axios.delete(`${this.#cloudInteropSettings.url}/api/sessions/${this.#sessionDetails.sessionId}`, {
320
+ const disconnectResponse = await fetch(`${this.#cloudInteropSettings.url}/api/sessions/${this.#sessionDetails.sessionId}`, {
321
+ method: 'DELETE',
216
322
  headers: this.#getRequestHeaders(),
217
323
  });
218
324
  if (disconnectResponse.status !== 200) {
219
- throw new CloudInteropAPIError('Error during session tear down - unexpected status', 'ERR_DISCONNECT', disconnectResponse.status);
325
+ throw new CloudInteropAPIError('Error during session tear down - unexpected status', 'ERR_DISCONNECT', new Error(disconnectResponse.statusText));
220
326
  }
221
327
  }
222
- catch {
223
- throw new CloudInteropAPIError('Error during disconnection', 'ERR_DISCONNECT');
328
+ catch (error) {
329
+ throw new CloudInteropAPIError('Error during disconnection', 'ERR_DISCONNECT', error);
224
330
  }
225
331
  finally {
226
332
  this.#mqttClient?.removeAllListeners();
@@ -229,12 +335,13 @@ class CloudInteropAPI {
229
335
  this.#mqttClient = undefined;
230
336
  this.#reconnectRetries = 0;
231
337
  this.#attemptingToReconnect = false;
338
+ this.#currentIntentDiscoveryId = undefined;
232
339
  if (fireDisconnectedEvent) {
233
340
  this.#emitEvent('disconnected');
234
341
  }
235
342
  }
236
343
  }
237
- #handleCommand(topic, message, sessionDetails) {
344
+ #handleMessage(topic, message, sessionDetails) {
238
345
  if (message.length === 0 || !sessionDetails) {
239
346
  // Ignore clean up messages
240
347
  return;
@@ -247,6 +354,73 @@ class CloudInteropAPI {
247
354
  const { channelName: contextGroup, payload: context, source, history } = messageEnvelope;
248
355
  this.#emitEvent('context', { contextGroup, context, source, history: { ...history, clientReceived: Date.now() } });
249
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
+ }
250
424
  }
251
425
  #emitEvent(type, ...args) {
252
426
  const listeners = this.#eventListeners.get(type) || [];
@@ -275,7 +449,7 @@ class CloudInteropAPI {
275
449
  if (!this.#connectionParams) {
276
450
  throw new Error('Connect parameters must be provided');
277
451
  }
278
- const headers = new axios.AxiosHeaders();
452
+ const headers = {};
279
453
  headers['Content-Type'] = 'application/json';
280
454
  if (this.#connectionParams.authenticationType === 'jwt' && this.#connectionParams.jwtAuthenticationParameters) {
281
455
  const tokenResult = this.#connectionParams.jwtAuthenticationParameters.jwtRequestCallback();