@openfin/cloud-interop-core-api 0.0.1-alpha.384d88e → 0.0.1-alpha.47835ce
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/{src/api.d.ts → api.d.ts} +9 -38
- package/dist/controllers/event.controller.d.ts +8 -0
- package/dist/controllers/index.d.ts +2 -0
- package/dist/controllers/intent.controller.d.ts +13 -0
- package/dist/index.cjs +361 -198
- package/dist/index.d.ts +6 -0
- package/dist/index.mjs +361 -203
- package/dist/{src/interfaces.d.ts → interfaces/connect.interface.d.ts} +8 -48
- package/dist/interfaces/event.interface.d.ts +15 -0
- package/dist/interfaces/index.d.ts +3 -0
- package/dist/interfaces/intents.interface.d.ts +22 -0
- package/dist/utils.d.ts +25 -0
- package/package.json +3 -3
- package/dist/src/index.d.ts +0 -3
- package/dist/tests/connect.test.d.ts +0 -1
- package/dist/tests/context.test.d.ts +0 -1
- package/dist/tests/mocks/fetch.mock.d.ts +0 -128
- /package/dist/{src/errors → errors}/api.error.d.ts +0 -0
package/dist/index.cjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var mqtt = require('mqtt');
|
|
4
|
+
var sharedUtils = require('@openfin/shared-utils');
|
|
4
5
|
|
|
5
6
|
class CloudInteropAPIError extends Error {
|
|
6
7
|
code;
|
|
@@ -16,6 +17,295 @@ class AuthorizationError extends CloudInteropAPIError {
|
|
|
16
17
|
}
|
|
17
18
|
}
|
|
18
19
|
|
|
20
|
+
class EventController {
|
|
21
|
+
#eventListeners = new Map();
|
|
22
|
+
addEventListener(type, callback) {
|
|
23
|
+
const listeners = this.#eventListeners.get(type) || [];
|
|
24
|
+
listeners.push(callback);
|
|
25
|
+
this.#eventListeners.set(type, listeners);
|
|
26
|
+
}
|
|
27
|
+
removeEventListener(type, callback) {
|
|
28
|
+
const listeners = this.#eventListeners.get(type) || [];
|
|
29
|
+
const index = listeners.indexOf(callback);
|
|
30
|
+
if (index !== -1) {
|
|
31
|
+
listeners.splice(index, 1);
|
|
32
|
+
}
|
|
33
|
+
this.#eventListeners.set(type, listeners);
|
|
34
|
+
}
|
|
35
|
+
once(type, callback) {
|
|
36
|
+
const listener = (...args) => {
|
|
37
|
+
this.removeEventListener(type, listener);
|
|
38
|
+
// @ts-expect-error - TS doesn't like the spread operator here
|
|
39
|
+
callback(...args);
|
|
40
|
+
};
|
|
41
|
+
this.addEventListener(type, listener);
|
|
42
|
+
}
|
|
43
|
+
emitEvent(type, ...args) {
|
|
44
|
+
const listeners = this.#eventListeners.get(type) || [];
|
|
45
|
+
listeners.forEach((listener) => listener(...args));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const APP_ID_DELIM = '::';
|
|
50
|
+
const getRequestHeaders = (connectionParameters) => {
|
|
51
|
+
const headers = {};
|
|
52
|
+
headers['Content-Type'] = 'application/json';
|
|
53
|
+
if (connectionParameters.authenticationType === 'jwt' && connectionParameters.jwtAuthenticationParameters) {
|
|
54
|
+
const tokenResult = connectionParameters.jwtAuthenticationParameters.jwtRequestCallback();
|
|
55
|
+
if (!tokenResult) {
|
|
56
|
+
throw new Error('jwtRequestCallback must return a token');
|
|
57
|
+
}
|
|
58
|
+
headers['x-of-auth-id'] = connectionParameters.jwtAuthenticationParameters.authenticationId;
|
|
59
|
+
headers['Authorization'] =
|
|
60
|
+
typeof tokenResult === 'string' ? `Bearer ${tokenResult}` : `Bearer ${Buffer.from(JSON.stringify(tokenResult)).toString('base64')}`;
|
|
61
|
+
}
|
|
62
|
+
if (connectionParameters.authenticationType === 'basic' && connectionParameters.basicAuthenticationParameters) {
|
|
63
|
+
const { username, password } = connectionParameters.basicAuthenticationParameters;
|
|
64
|
+
headers['Authorization'] = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
|
|
65
|
+
}
|
|
66
|
+
return headers;
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Encodes all app intents in the format: `appId::sourceId::sessionId`,
|
|
70
|
+
* where sourceId and sessionId are URI encoded
|
|
71
|
+
* @param appIntents
|
|
72
|
+
* @param source
|
|
73
|
+
* @returns
|
|
74
|
+
*/
|
|
75
|
+
const encodeAppIntents = (appIntents, { sessionId, sourceId }) => appIntents.map((intent) => ({
|
|
76
|
+
...intent,
|
|
77
|
+
apps: intent.apps.map((app) => {
|
|
78
|
+
const id = encodeURIComponent(app.appId);
|
|
79
|
+
const sId = encodeURIComponent(sourceId);
|
|
80
|
+
return { ...app, appId: `${id}${APP_ID_DELIM}${sId}${APP_ID_DELIM}${sessionId}` };
|
|
81
|
+
}),
|
|
82
|
+
}));
|
|
83
|
+
/**
|
|
84
|
+
* Decodes all app intents by URI decoding the parts previously encoded by `encodeAppIntents`
|
|
85
|
+
* @param appIntents
|
|
86
|
+
* @returns
|
|
87
|
+
*/
|
|
88
|
+
const decodeAppIntents = (appIntents) => appIntents.map((intent) => ({
|
|
89
|
+
...intent,
|
|
90
|
+
apps: intent.apps.map((app) => {
|
|
91
|
+
const [encodedAppId, encodedSourceId, sessionId] = app.appId.split(APP_ID_DELIM);
|
|
92
|
+
const id = decodeURIComponent(encodedAppId);
|
|
93
|
+
const sourceId = decodeURIComponent(encodedSourceId);
|
|
94
|
+
return { ...app, appId: `${id}${APP_ID_DELIM}${sourceId}${APP_ID_DELIM}${sessionId}` };
|
|
95
|
+
}),
|
|
96
|
+
}));
|
|
97
|
+
/**
|
|
98
|
+
* Decodes the app id to extract the sessionId, returns '' if not able to parse
|
|
99
|
+
* @param app
|
|
100
|
+
* @returns
|
|
101
|
+
*/
|
|
102
|
+
const parseSessionId = (appId) => (typeof appId === 'string' ? appId : appId.appId).split(APP_ID_DELIM)?.[2]?.trim() ?? '';
|
|
103
|
+
const getSourceFromSession = (sessionDetails) => ({
|
|
104
|
+
sessionId: sessionDetails.sessionId,
|
|
105
|
+
sourceId: sessionDetails.sourceId,
|
|
106
|
+
userId: sessionDetails.sub,
|
|
107
|
+
orgId: sessionDetails.orgId,
|
|
108
|
+
platformId: sessionDetails.platformId,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const MIN_TIMEOUT = 500;
|
|
112
|
+
const DEFAULT_TIMEOUT = 3000;
|
|
113
|
+
const newDiscovery = () => ({
|
|
114
|
+
id: undefined,
|
|
115
|
+
pendingIntentDetailsEvents: [],
|
|
116
|
+
sessionCount: 0,
|
|
117
|
+
responseCount: 0,
|
|
118
|
+
state: 'not-started',
|
|
119
|
+
});
|
|
120
|
+
class IntentController {
|
|
121
|
+
#url;
|
|
122
|
+
#mqttClient;
|
|
123
|
+
#sessionDetails;
|
|
124
|
+
#connectionParams;
|
|
125
|
+
#events;
|
|
126
|
+
#logger;
|
|
127
|
+
#discovery = newDiscovery();
|
|
128
|
+
#discoveryTimeout;
|
|
129
|
+
constructor(url, mqttClient, sessionDetails, connectionParameters, events, logger) {
|
|
130
|
+
this.#url = url;
|
|
131
|
+
this.#mqttClient = mqttClient;
|
|
132
|
+
this.#sessionDetails = sessionDetails;
|
|
133
|
+
this.#connectionParams = connectionParameters;
|
|
134
|
+
this.#events = events;
|
|
135
|
+
this.#logger = logger;
|
|
136
|
+
}
|
|
137
|
+
async startIntentDiscovery(options) {
|
|
138
|
+
if (this.#discovery.state === 'in-progress') {
|
|
139
|
+
throw new Error('Intent discovery already in progress');
|
|
140
|
+
}
|
|
141
|
+
const { timeout = DEFAULT_TIMEOUT, findOptions } = options;
|
|
142
|
+
// clamp min value to 500ms
|
|
143
|
+
const clampedTimeout = Math.max(timeout, MIN_TIMEOUT);
|
|
144
|
+
try {
|
|
145
|
+
const startResponse = await fetch(`${this.#url}/api/intents/${this.#sessionDetails.sessionId}`, {
|
|
146
|
+
method: 'POST',
|
|
147
|
+
headers: getRequestHeaders(this.#connectionParams),
|
|
148
|
+
body: JSON.stringify({ findOptions }),
|
|
149
|
+
});
|
|
150
|
+
if (!startResponse.ok) {
|
|
151
|
+
throw new Error(startResponse.statusText);
|
|
152
|
+
}
|
|
153
|
+
// TODO: type this response?
|
|
154
|
+
const json = await startResponse.json();
|
|
155
|
+
this.#discovery.id = json.discoveryId;
|
|
156
|
+
this.#discovery.sessionCount = json.sessionCount;
|
|
157
|
+
this.#discovery.state = 'in-progress';
|
|
158
|
+
// Listen out for discovery results directly sent to us
|
|
159
|
+
await this.#mqttClient.subscribeAsync(`${this.#sessionDetails.sessionRootTopic}/commands/${this.#discovery.id}`);
|
|
160
|
+
this.#discoveryTimeout = setTimeout(() => this.#endIntentDiscovery(), clampedTimeout);
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
// Clean up any ongoing discoveries
|
|
164
|
+
this.#endIntentDiscovery();
|
|
165
|
+
throw new CloudInteropAPIError('Error starting intent discovery', 'ERR_STARTING_INTENT_DISCOVERY', error);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
async #endIntentDiscovery() {
|
|
169
|
+
if (this.#discovery.state !== 'in-progress') {
|
|
170
|
+
// TODO: remove debug logs
|
|
171
|
+
this.#logger('debug', 'Intent discovery not in progress');
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (this.#discoveryTimeout) {
|
|
175
|
+
clearTimeout(this.#discoveryTimeout);
|
|
176
|
+
this.#discoveryTimeout = undefined;
|
|
177
|
+
}
|
|
178
|
+
this.#discovery.state = 'ended';
|
|
179
|
+
// emit our aggregated events
|
|
180
|
+
this.#events.emitEvent('aggregate-intent-details', { responses: this.#discovery.pendingIntentDetailsEvents });
|
|
181
|
+
// gracefully end discovery
|
|
182
|
+
await this.#mqttClient.unsubscribeAsync(`${this.#sessionDetails.sessionRootTopic}/commands/${this.#discovery.id}`).catch(() => {
|
|
183
|
+
this.#logger('warn', `Error ending intent discovery: could not unsubscribe from discovery id ${this.#discovery.id}`);
|
|
184
|
+
});
|
|
185
|
+
await fetch(`${this.#url}/api/intents/${this.#sessionDetails.sessionId}/${this.#discovery.id}`, {
|
|
186
|
+
method: 'DELETE',
|
|
187
|
+
headers: getRequestHeaders(this.#connectionParams),
|
|
188
|
+
})
|
|
189
|
+
.then((deleteResponse) => {
|
|
190
|
+
if (!deleteResponse.ok) {
|
|
191
|
+
throw new Error(`Error ending intent discovery: ${deleteResponse.statusText}`);
|
|
192
|
+
}
|
|
193
|
+
this.#logger('debug', 'Intent discovery ended');
|
|
194
|
+
})
|
|
195
|
+
.catch((error) => {
|
|
196
|
+
this.#logger('warn', `Error ending intent discovery: ${error}`);
|
|
197
|
+
});
|
|
198
|
+
// clean up
|
|
199
|
+
this.#discovery = newDiscovery();
|
|
200
|
+
}
|
|
201
|
+
async raiseIntent({ raiseOptions, appId }) {
|
|
202
|
+
const targetSessionId = parseSessionId(appId);
|
|
203
|
+
if (!targetSessionId) {
|
|
204
|
+
// TODO: should we add more info here about the format?
|
|
205
|
+
throw new CloudInteropAPIError(`Invalid AppId specified, must be encoded as a cloud-session app id`, 'ERR_INVALID_TARGET_SESSION_ID');
|
|
206
|
+
}
|
|
207
|
+
const postResponse = await fetch(`${this.#url}/api/intents/${this.#sessionDetails.sessionId}/sessions/${targetSessionId}`, {
|
|
208
|
+
method: 'POST',
|
|
209
|
+
headers: getRequestHeaders(this.#connectionParams),
|
|
210
|
+
body: JSON.stringify({ raiseOptions }),
|
|
211
|
+
});
|
|
212
|
+
if (!postResponse.ok) {
|
|
213
|
+
// TODO: maybe add a debug flag to print these when dev'ing?
|
|
214
|
+
// console.log(`Error raising intent: ${await postResponse.text()}`);
|
|
215
|
+
throw new CloudInteropAPIError(`Error raising intent`, 'ERR_RAISING_INTENT', new Error(postResponse.statusText));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
async reportAppIntents(discoveryId, intents) {
|
|
219
|
+
intents = encodeAppIntents(intents, getSourceFromSession(this.#sessionDetails));
|
|
220
|
+
try {
|
|
221
|
+
const reportResponse = await fetch(`${this.#url}/api/intents/${this.#sessionDetails.sessionId}/${discoveryId}`, {
|
|
222
|
+
method: 'POST',
|
|
223
|
+
headers: getRequestHeaders(this.#connectionParams),
|
|
224
|
+
body: JSON.stringify({ intents }),
|
|
225
|
+
});
|
|
226
|
+
if (reportResponse.ok) {
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
throw new CloudInteropAPIError('Error reporting intents', 'ERR_REPORTING_INTENTS', new Error(reportResponse.statusText));
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
this.#logger('warn', `Error reporting intents for discovery ID ${discoveryId}: ${error}`);
|
|
233
|
+
}
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
async sendIntentResult(initiatingSessionId, result) {
|
|
237
|
+
const { sessionId } = getSourceFromSession(this.#sessionDetails);
|
|
238
|
+
const resultResponse = await fetch(`${this.#url}/api/intents/${initiatingSessionId}/result/${sessionId}`, {
|
|
239
|
+
method: 'POST',
|
|
240
|
+
headers: getRequestHeaders(this.#connectionParams),
|
|
241
|
+
body: JSON.stringify({ result }),
|
|
242
|
+
});
|
|
243
|
+
if (!resultResponse.ok) {
|
|
244
|
+
throw new CloudInteropAPIError('Error sending intent result', 'ERR_SENDING_INTENT_RESULT', new Error(resultResponse.statusText));
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
handleCommandMessage(message) {
|
|
248
|
+
switch (message.command) {
|
|
249
|
+
case 'report-intents': {
|
|
250
|
+
if (message.initiatingSessionId === this.#sessionDetails?.sessionId) {
|
|
251
|
+
// Ignore if this originated from us
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const { command: _, ...event } = message;
|
|
255
|
+
this.#events.emitEvent('report-intents', event);
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
case 'intent-details': {
|
|
259
|
+
/**
|
|
260
|
+
* We aggregate intent details commands from all connected sessions on the server side
|
|
261
|
+
* to ensure upstream clients don't have to do this. Also given @openfin/cloud-interop
|
|
262
|
+
* exposes FDC3 compliant APIs, there is no concept of streaming available, hence we
|
|
263
|
+
* aggregate the responses here and send them in a synchronous manner.
|
|
264
|
+
*/
|
|
265
|
+
const { command: _, ...event } = message;
|
|
266
|
+
// Decode intents before emitting to client
|
|
267
|
+
event.intents = decodeAppIntents(event.intents);
|
|
268
|
+
// always emit individual intent-details events in addition to aggregate-intent-details
|
|
269
|
+
// for flexibility after intent discovery has ended, this can be useful for late-joining
|
|
270
|
+
// clients nearing the timeout
|
|
271
|
+
this.#events.emitEvent('intent-details', event);
|
|
272
|
+
if (message.discoveryId !== this.#discovery.id || this.#discovery.state !== 'in-progress') {
|
|
273
|
+
// Ignore if its any other discovery id for some reason, or
|
|
274
|
+
// if we're not in the middle of a discovery
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
this.#discovery.responseCount += 1;
|
|
278
|
+
this.#logger('debug', `Received intent details from ${message.source.sessionId}, received: ${this.#discovery.responseCount}, out of connected sessions: ${this.#discovery.sessionCount - 1}`);
|
|
279
|
+
this.#discovery.pendingIntentDetailsEvents.push(event);
|
|
280
|
+
const allResponded = this.#discovery.responseCount === this.#discovery.sessionCount - 1;
|
|
281
|
+
if (allResponded) {
|
|
282
|
+
this.#endIntentDiscovery();
|
|
283
|
+
}
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
case 'raise-intent': {
|
|
287
|
+
if (message.targetSessionId === this.#sessionDetails?.sessionId) {
|
|
288
|
+
const { command: _, ...event } = message;
|
|
289
|
+
this.#events.emitEvent('raise-intent', event);
|
|
290
|
+
}
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
293
|
+
case 'intent-result': {
|
|
294
|
+
if (message.initiatingSessionId === this.#sessionDetails?.sessionId) {
|
|
295
|
+
// Return result to originator and end discovery
|
|
296
|
+
const { command: _, ...resultEvent } = message;
|
|
297
|
+
this.#events.emitEvent('intent-result', resultEvent);
|
|
298
|
+
}
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
default: {
|
|
302
|
+
this.#logger('warn', `Unknown command message received:\n${JSON.stringify(message, null, 2)}`);
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
19
309
|
// Error codes as defined in https://docs.emqx.com/en/cloud/latest/connect_to_deployments/mqtt_client_error_codes.html
|
|
20
310
|
const BadUserNamePasswordError = 134;
|
|
21
311
|
/**
|
|
@@ -36,9 +326,9 @@ class CloudInteropAPI {
|
|
|
36
326
|
};
|
|
37
327
|
#reconnectRetries = 0;
|
|
38
328
|
#connectionParams;
|
|
39
|
-
#eventListeners = new Map();
|
|
40
329
|
#attemptingToReconnect = false;
|
|
41
|
-
#
|
|
330
|
+
#events = new EventController();
|
|
331
|
+
#intents;
|
|
42
332
|
constructor(cloudInteropSettings) {
|
|
43
333
|
this.#cloudInteropSettings = cloudInteropSettings;
|
|
44
334
|
}
|
|
@@ -66,8 +356,8 @@ class CloudInteropAPI {
|
|
|
66
356
|
const { sourceId, platformId } = this.#connectionParams;
|
|
67
357
|
const createSessionResponse = await fetch(`${this.#cloudInteropSettings.url}/api/sessions`, {
|
|
68
358
|
method: 'POST',
|
|
69
|
-
headers: this.#
|
|
70
|
-
body: JSON.stringify({ sourceId, platformId }),
|
|
359
|
+
headers: getRequestHeaders(this.#connectionParams),
|
|
360
|
+
body: JSON.stringify({ sourceId: sourceId.trim(), platformId }),
|
|
71
361
|
});
|
|
72
362
|
if (!createSessionResponse.ok) {
|
|
73
363
|
if (createSessionResponse.status === 401 || createSessionResponse.status === 403) {
|
|
@@ -96,10 +386,12 @@ class CloudInteropAPI {
|
|
|
96
386
|
username: this.#sessionDetails.token,
|
|
97
387
|
};
|
|
98
388
|
this.#mqttClient = await mqtt.connectAsync(this.#sessionDetails.url, clientOptions);
|
|
389
|
+
// TODO: Dynamic intent discovery
|
|
390
|
+
// search for any ongoing discoveries in DB and fire report-intents on self
|
|
99
391
|
this.#logger('log', `Cloud Interop successfully connected to ${this.#cloudInteropSettings.url}`);
|
|
100
392
|
this.#mqttClient.on('error', async (error) => {
|
|
101
393
|
// We will receive errors for each failed reconnection attempt
|
|
102
|
-
// We don't
|
|
394
|
+
// We don't want to disconnect on these else we will never reconnect
|
|
103
395
|
if (!this.#attemptingToReconnect) {
|
|
104
396
|
await this.#disconnect(false);
|
|
105
397
|
}
|
|
@@ -108,14 +400,14 @@ class CloudInteropAPI {
|
|
|
108
400
|
case BadUserNamePasswordError: {
|
|
109
401
|
await this.#disconnect(false);
|
|
110
402
|
this.#logger('warn', `Session expired`);
|
|
111
|
-
this.#emitEvent('session-expired');
|
|
403
|
+
this.#events.emitEvent('session-expired');
|
|
112
404
|
return;
|
|
113
405
|
}
|
|
114
406
|
default: {
|
|
115
407
|
this.#logger('error', `Unknown Infrastructure Error Code ${error.code} : ${error.message}${this.#attemptingToReconnect ? ' during reconnection attempt' : ''}`);
|
|
116
408
|
// As we are in the middle of a reconnect, lets not emit an error to cut down on the event noise
|
|
117
409
|
if (!this.#attemptingToReconnect) {
|
|
118
|
-
this.#emitEvent('error', new CloudInteropAPIError(`Unknown Infrastructure Error Code ${error.code} : ${error.message}`, 'ERR_INFRASTRUCTURE', error));
|
|
410
|
+
this.#events.emitEvent('error', new CloudInteropAPIError(`Unknown Infrastructure Error Code ${error.code} : ${error.message}`, 'ERR_INFRASTRUCTURE', error));
|
|
119
411
|
break;
|
|
120
412
|
}
|
|
121
413
|
}
|
|
@@ -125,7 +417,7 @@ class CloudInteropAPI {
|
|
|
125
417
|
this.#logger('error', `Unknown Error${this.#attemptingToReconnect ? ' during reconnection attempt' : ''}: ${error}`);
|
|
126
418
|
// As we are in the middle of a reconnect, lets not emit an error to cut down on the event noise
|
|
127
419
|
if (!this.#attemptingToReconnect) {
|
|
128
|
-
this.#emitEvent('error', new CloudInteropAPIError(`Unknown Error`, 'ERR_UNKNOWN', error));
|
|
420
|
+
this.#events.emitEvent('error', new CloudInteropAPIError(`Unknown Error`, 'ERR_UNKNOWN', error));
|
|
129
421
|
}
|
|
130
422
|
}
|
|
131
423
|
});
|
|
@@ -137,14 +429,14 @@ class CloudInteropAPI {
|
|
|
137
429
|
this.#logger('warn', `Cloud Interop reached max reconnection attempts - ${this.#reconnectRetryLimit}...`);
|
|
138
430
|
this.#disconnect(true);
|
|
139
431
|
}
|
|
140
|
-
this.#emitEvent('reconnecting', this.#reconnectRetries);
|
|
432
|
+
this.#events.emitEvent('reconnecting', this.#reconnectRetries);
|
|
141
433
|
});
|
|
142
434
|
// Does not fire on initial connection, only successful reconnection attempts
|
|
143
435
|
this.#mqttClient.on('connect', () => {
|
|
144
436
|
this.#logger('debug', `Cloud Interop successfully reconnected after ${this.#reconnectRetries} attempts`);
|
|
145
437
|
this.#reconnectRetries = 0;
|
|
146
438
|
this.#attemptingToReconnect = false;
|
|
147
|
-
this.#emitEvent('reconnected');
|
|
439
|
+
this.#events.emitEvent('reconnected');
|
|
148
440
|
});
|
|
149
441
|
this.#mqttClient.on('message', (topic, message) => {
|
|
150
442
|
if (!this.#sessionDetails) {
|
|
@@ -157,6 +449,7 @@ class CloudInteropAPI {
|
|
|
157
449
|
this.#mqttClient.subscribe(`${sessionRootTopic}/context-groups/#`);
|
|
158
450
|
// Listen out for global commands
|
|
159
451
|
this.#mqttClient.subscribe(`${sessionRootTopic}/commands`);
|
|
452
|
+
this.#initControllers(this.#mqttClient, this.#sessionDetails, this.#connectionParams);
|
|
160
453
|
}
|
|
161
454
|
/**
|
|
162
455
|
* Disconnects from the Cloud Interop service
|
|
@@ -177,16 +470,20 @@ class CloudInteropAPI {
|
|
|
177
470
|
* @memberof CloudInteropAPI
|
|
178
471
|
*/
|
|
179
472
|
async setContext(contextGroup, context) {
|
|
473
|
+
// TODO: make context of type OpenFin.Context
|
|
180
474
|
if (!this.#sessionDetails || !this.#connectionParams) {
|
|
181
475
|
throw new Error('Session not connected');
|
|
182
476
|
}
|
|
477
|
+
if (!this.#mqttClient) {
|
|
478
|
+
throw new Error('MQTT client not connected');
|
|
479
|
+
}
|
|
183
480
|
const payload = {
|
|
184
481
|
context,
|
|
185
482
|
timestamp: Date.now(),
|
|
186
483
|
};
|
|
187
484
|
const postResponse = await fetch(`${this.#cloudInteropSettings.url}/api/context-groups/${this.#sessionDetails.sessionId}/${contextGroup}`, {
|
|
188
485
|
method: 'POST',
|
|
189
|
-
headers: this.#
|
|
486
|
+
headers: getRequestHeaders(this.#connectionParams),
|
|
190
487
|
body: JSON.stringify(payload),
|
|
191
488
|
});
|
|
192
489
|
if (!postResponse.ok) {
|
|
@@ -200,126 +497,42 @@ class CloudInteropAPI {
|
|
|
200
497
|
* @memberof CloudInteropAPI
|
|
201
498
|
* @throws {CloudInteropAPIError} - If an error occurs during intent discovery
|
|
202
499
|
*/
|
|
203
|
-
async startIntentDiscovery() {
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
}
|
|
500
|
+
async startIntentDiscovery(options) {
|
|
501
|
+
this.#throwIfNotConnected();
|
|
502
|
+
return this.#intents?.startIntentDiscovery(options);
|
|
229
503
|
}
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
}
|
|
504
|
+
async raiseIntent(options) {
|
|
505
|
+
this.#throwIfNotConnected();
|
|
506
|
+
return this.#intents?.raiseIntent(options);
|
|
261
507
|
}
|
|
262
|
-
async
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
}
|
|
508
|
+
async reportAppIntents(discoveryId, intents) {
|
|
509
|
+
this.#throwIfNotConnected();
|
|
510
|
+
return this.#intents?.reportAppIntents(discoveryId, intents) ?? false;
|
|
274
511
|
}
|
|
275
|
-
async
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
}
|
|
512
|
+
async sendIntentResult(initiatingSessionId, result) {
|
|
513
|
+
this.#throwIfNotConnected();
|
|
514
|
+
return this.#intents?.sendIntentResult(initiatingSessionId, result);
|
|
287
515
|
}
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
}
|
|
516
|
+
parseSessionId(appId) {
|
|
517
|
+
return parseSessionId(appId);
|
|
301
518
|
}
|
|
302
519
|
addEventListener(type, callback) {
|
|
303
|
-
|
|
304
|
-
listeners.push(callback);
|
|
305
|
-
this.#eventListeners.set(type, listeners);
|
|
520
|
+
this.#events.addEventListener(type, callback);
|
|
306
521
|
}
|
|
307
522
|
removeEventListener(type, callback) {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
}
|
|
313
|
-
this.#eventListeners.set(type, listeners);
|
|
523
|
+
this.#events.removeEventListener(type, callback);
|
|
524
|
+
}
|
|
525
|
+
once(type, callback) {
|
|
526
|
+
this.#events.once(type, callback);
|
|
314
527
|
}
|
|
315
528
|
async #disconnect(fireDisconnectedEvent) {
|
|
316
|
-
if (!this.#sessionDetails) {
|
|
529
|
+
if (!this.#sessionDetails || !this.#connectionParams) {
|
|
317
530
|
return;
|
|
318
531
|
}
|
|
319
532
|
try {
|
|
320
533
|
const disconnectResponse = await fetch(`${this.#cloudInteropSettings.url}/api/sessions/${this.#sessionDetails.sessionId}`, {
|
|
321
534
|
method: 'DELETE',
|
|
322
|
-
headers: this.#
|
|
535
|
+
headers: getRequestHeaders(this.#connectionParams),
|
|
323
536
|
});
|
|
324
537
|
if (disconnectResponse.status !== 200) {
|
|
325
538
|
throw new CloudInteropAPIError('Error during session tear down - unexpected status', 'ERR_DISCONNECT', new Error(disconnectResponse.statusText));
|
|
@@ -329,15 +542,15 @@ class CloudInteropAPI {
|
|
|
329
542
|
throw new CloudInteropAPIError('Error during disconnection', 'ERR_DISCONNECT', error);
|
|
330
543
|
}
|
|
331
544
|
finally {
|
|
545
|
+
this.#destroyControllers();
|
|
332
546
|
this.#mqttClient?.removeAllListeners();
|
|
333
547
|
await this.#mqttClient?.endAsync(true);
|
|
334
548
|
this.#sessionDetails = undefined;
|
|
335
549
|
this.#mqttClient = undefined;
|
|
336
550
|
this.#reconnectRetries = 0;
|
|
337
551
|
this.#attemptingToReconnect = false;
|
|
338
|
-
this.#currentIntentDiscoveryId = undefined;
|
|
339
552
|
if (fireDisconnectedEvent) {
|
|
340
|
-
this.#emitEvent('disconnected');
|
|
553
|
+
this.#events.emitEvent('disconnected');
|
|
341
554
|
}
|
|
342
555
|
}
|
|
343
556
|
}
|
|
@@ -348,11 +561,12 @@ class CloudInteropAPI {
|
|
|
348
561
|
}
|
|
349
562
|
const messageEnvelope = JSON.parse(message.toString());
|
|
350
563
|
if (topic.startsWith(`${sessionDetails.sessionRootTopic}/context-groups/`)) {
|
|
351
|
-
|
|
564
|
+
const contextEvent = messageEnvelope;
|
|
565
|
+
if (contextEvent.source.sessionId === sessionDetails.sessionId) {
|
|
352
566
|
return;
|
|
353
567
|
}
|
|
354
|
-
const {
|
|
355
|
-
this.#emitEvent('context', { contextGroup, context, source, history: { ...history, clientReceived: Date.now() } });
|
|
568
|
+
const { contextGroup, context, source, history } = contextEvent;
|
|
569
|
+
this.#events.emitEvent('context', { contextGroup, context, source, history: { ...history, clientReceived: Date.now() } });
|
|
356
570
|
}
|
|
357
571
|
else if (topic.startsWith(`${sessionDetails.sessionRootTopic}/commands`)) {
|
|
358
572
|
this.#handleCommandMessage(messageEnvelope);
|
|
@@ -360,72 +574,19 @@ class CloudInteropAPI {
|
|
|
360
574
|
}
|
|
361
575
|
#handleCommandMessage(message) {
|
|
362
576
|
switch (message.command) {
|
|
363
|
-
case 'report-intents':
|
|
364
|
-
|
|
365
|
-
|
|
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
|
-
}
|
|
577
|
+
case 'report-intents':
|
|
578
|
+
case 'intent-details':
|
|
579
|
+
case 'raise-intent':
|
|
410
580
|
case 'intent-result': {
|
|
411
|
-
|
|
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
|
-
}
|
|
581
|
+
this.#intents?.handleCommandMessage(message);
|
|
417
582
|
break;
|
|
418
583
|
}
|
|
419
584
|
default: {
|
|
420
|
-
this.#logger('warn', `Unknown command message received
|
|
585
|
+
this.#logger('warn', `Unknown command message received:\n${JSON.stringify(message, null, 2)}`);
|
|
421
586
|
break;
|
|
422
587
|
}
|
|
423
588
|
}
|
|
424
589
|
}
|
|
425
|
-
#emitEvent(type, ...args) {
|
|
426
|
-
const listeners = this.#eventListeners.get(type) || [];
|
|
427
|
-
listeners.forEach((listener) => listener(...args));
|
|
428
|
-
}
|
|
429
590
|
#validateConnectParams = (parameters) => {
|
|
430
591
|
if (!parameters) {
|
|
431
592
|
throw new Error('Connect parameters must be provided');
|
|
@@ -433,6 +594,9 @@ class CloudInteropAPI {
|
|
|
433
594
|
if (!parameters.sourceId) {
|
|
434
595
|
throw new Error('sourceId must be provided');
|
|
435
596
|
}
|
|
597
|
+
if (parameters.sourceId.includes(APP_ID_DELIM)) {
|
|
598
|
+
throw new Error(`sourceId cannot contain "${APP_ID_DELIM}"`);
|
|
599
|
+
}
|
|
436
600
|
if (!parameters.platformId) {
|
|
437
601
|
throw new Error('platformId must be provided');
|
|
438
602
|
}
|
|
@@ -445,29 +609,28 @@ class CloudInteropAPI {
|
|
|
445
609
|
throw new Error('basicAuthenticationParameters must be provided when using basic authentication');
|
|
446
610
|
}
|
|
447
611
|
};
|
|
448
|
-
#
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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')}`;
|
|
612
|
+
#initControllers(mqttClient, sessionDetails, connectionParameters) {
|
|
613
|
+
this.#intents = new IntentController(this.#cloudInteropSettings.url, mqttClient, sessionDetails, connectionParameters, this.#events, this.#logger);
|
|
614
|
+
}
|
|
615
|
+
#destroyControllers() {
|
|
616
|
+
this.#intents = undefined;
|
|
617
|
+
}
|
|
618
|
+
#throwIfNotConnected() {
|
|
619
|
+
if (!this.#sessionDetails || !this.#connectionParams) {
|
|
620
|
+
throw new Error('Session not connected');
|
|
462
621
|
}
|
|
463
|
-
if (this.#
|
|
464
|
-
|
|
465
|
-
headers['Authorization'] = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
|
|
622
|
+
if (!this.#mqttClient) {
|
|
623
|
+
throw new Error('MQTT client not connected');
|
|
466
624
|
}
|
|
467
|
-
|
|
468
|
-
};
|
|
625
|
+
}
|
|
469
626
|
}
|
|
470
627
|
|
|
471
628
|
exports.AuthorizationError = AuthorizationError;
|
|
472
629
|
exports.CloudInteropAPI = CloudInteropAPI;
|
|
473
630
|
exports.CloudInteropAPIError = CloudInteropAPIError;
|
|
631
|
+
Object.keys(sharedUtils).forEach(function (k) {
|
|
632
|
+
if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
|
|
633
|
+
enumerable: true,
|
|
634
|
+
get: function () { return sharedUtils[k]; }
|
|
635
|
+
});
|
|
636
|
+
});
|