@openfin/cloud-interop-core-api 0.0.1-alpha.39beb33 → 0.0.1-alpha.3cbe14e
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/api.d.ts +20 -16
- package/dist/index.cjs +139 -476
- package/dist/index.d.ts +1 -4
- package/dist/index.mjs +3898 -475
- package/dist/{interfaces/connect.interface.d.ts → interfaces.d.ts} +65 -8
- package/package.json +4 -4
- package/dist/controllers/event.controller.d.ts +0 -8
- package/dist/controllers/index.d.ts +0 -2
- package/dist/controllers/intent.controller.d.ts +0 -13
- package/dist/interfaces/event.interface.d.ts +0 -15
- package/dist/interfaces/index.d.ts +0 -3
- package/dist/interfaces/intents.interface.d.ts +0 -22
- package/dist/utils.d.ts +0 -25
package/dist/index.cjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
var axios = require('axios');
|
|
3
4
|
var mqtt = require('mqtt');
|
|
4
|
-
var sharedUtils = require('@openfin/shared-utils');
|
|
5
5
|
|
|
6
6
|
class CloudInteropAPIError extends Error {
|
|
7
7
|
code;
|
|
@@ -17,295 +17,6 @@ class AuthorizationError extends CloudInteropAPIError {
|
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
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
|
-
|
|
309
20
|
// Error codes as defined in https://docs.emqx.com/en/cloud/latest/connect_to_deployments/mqtt_client_error_codes.html
|
|
310
21
|
const BadUserNamePasswordError = 134;
|
|
311
22
|
/**
|
|
@@ -326,9 +37,8 @@ class CloudInteropAPI {
|
|
|
326
37
|
};
|
|
327
38
|
#reconnectRetries = 0;
|
|
328
39
|
#connectionParams;
|
|
40
|
+
#eventListeners = new Map();
|
|
329
41
|
#attemptingToReconnect = false;
|
|
330
|
-
#events = new EventController();
|
|
331
|
-
#intents;
|
|
332
42
|
constructor(cloudInteropSettings) {
|
|
333
43
|
this.#cloudInteropSettings = cloudInteropSettings;
|
|
334
44
|
}
|
|
@@ -354,102 +64,105 @@ class CloudInteropAPI {
|
|
|
354
64
|
this.#keepAliveIntervalSeconds = parameters.keepAliveIntervalSeconds || this.#keepAliveIntervalSeconds;
|
|
355
65
|
this.#logger = parameters.logger || this.#logger;
|
|
356
66
|
const { sourceId, platformId } = this.#connectionParams;
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
throw new CloudInteropAPIError();
|
|
367
|
-
}
|
|
368
|
-
if (createSessionResponse.status !== 201) {
|
|
369
|
-
throw new CloudInteropAPIError(`Failed to connect to the Cloud Interop service: ${this.#cloudInteropSettings.url}`, 'ERR_CONNECT', new Error(createSessionResponse.statusText));
|
|
370
|
-
}
|
|
371
|
-
this.#sessionDetails = (await createSessionResponse.json());
|
|
372
|
-
const sessionRootTopic = this.#sessionDetails.sessionRootTopic;
|
|
373
|
-
const clientOptions = {
|
|
374
|
-
keepalive: this.#keepAliveIntervalSeconds,
|
|
375
|
-
clientId: this.#sessionDetails.sessionId,
|
|
376
|
-
clean: true,
|
|
377
|
-
protocolVersion: 5,
|
|
378
|
-
// The "will" message will be published on an unexpected disconnection
|
|
379
|
-
// The server can then tidy up. So it needs every for this client to do that, the session details is perfect
|
|
380
|
-
will: {
|
|
381
|
-
topic: 'interop/lastwill',
|
|
382
|
-
payload: Buffer.from(JSON.stringify(this.#sessionDetails)),
|
|
383
|
-
qos: 0,
|
|
384
|
-
retain: false,
|
|
385
|
-
},
|
|
386
|
-
username: this.#sessionDetails.token,
|
|
387
|
-
};
|
|
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
|
|
391
|
-
this.#logger('log', `Cloud Interop successfully connected to ${this.#cloudInteropSettings.url}`);
|
|
392
|
-
this.#mqttClient.on('error', async (error) => {
|
|
393
|
-
// We will receive errors for each failed reconnection attempt
|
|
394
|
-
// We don't want to disconnect on these else we will never reconnect
|
|
395
|
-
if (!this.#attemptingToReconnect) {
|
|
396
|
-
await this.#disconnect(false);
|
|
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);
|
|
397
76
|
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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
|
+
}
|
|
412
117
|
}
|
|
413
118
|
}
|
|
414
119
|
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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));
|
|
125
|
+
}
|
|
421
126
|
}
|
|
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();
|
|
161
|
+
}
|
|
162
|
+
throw new CloudInteropAPIError();
|
|
422
163
|
}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
this.#attemptingToReconnect = true;
|
|
426
|
-
this.#reconnectRetries += 1;
|
|
427
|
-
this.#logger('debug', `Cloud Interop attempting reconnection - ${this.#reconnectRetries}...`);
|
|
428
|
-
if (this.#reconnectRetries === this.#reconnectRetryLimit) {
|
|
429
|
-
this.#logger('warn', `Cloud Interop reached max reconnection attempts - ${this.#reconnectRetryLimit}...`);
|
|
430
|
-
this.#disconnect(true);
|
|
431
|
-
}
|
|
432
|
-
this.#events.emitEvent('reconnecting', this.#reconnectRetries);
|
|
433
|
-
});
|
|
434
|
-
// Does not fire on initial connection, only successful reconnection attempts
|
|
435
|
-
this.#mqttClient.on('connect', () => {
|
|
436
|
-
this.#logger('debug', `Cloud Interop successfully reconnected after ${this.#reconnectRetries} attempts`);
|
|
437
|
-
this.#reconnectRetries = 0;
|
|
438
|
-
this.#attemptingToReconnect = false;
|
|
439
|
-
this.#events.emitEvent('reconnected');
|
|
440
|
-
});
|
|
441
|
-
this.#mqttClient.on('message', (topic, message) => {
|
|
442
|
-
if (!this.#sessionDetails) {
|
|
443
|
-
this.#logger('warn', 'Received message when session not connected');
|
|
444
|
-
return;
|
|
445
|
-
}
|
|
446
|
-
this.#handleMessage(topic, message, this.#sessionDetails);
|
|
447
|
-
});
|
|
448
|
-
// Subscribe to all context groups
|
|
449
|
-
this.#mqttClient.subscribe(`${sessionRootTopic}/context-groups/#`);
|
|
450
|
-
// Listen out for global commands
|
|
451
|
-
this.#mqttClient.subscribe(`${sessionRootTopic}/commands`);
|
|
452
|
-
this.#initControllers(this.#mqttClient, this.#sessionDetails, this.#connectionParams);
|
|
164
|
+
throw error;
|
|
165
|
+
}
|
|
453
166
|
}
|
|
454
167
|
/**
|
|
455
168
|
* Disconnects from the Cloud Interop service
|
|
@@ -470,79 +183,46 @@ class CloudInteropAPI {
|
|
|
470
183
|
* @memberof CloudInteropAPI
|
|
471
184
|
*/
|
|
472
185
|
async setContext(contextGroup, context) {
|
|
473
|
-
// TODO: make context of type OpenFin.Context
|
|
474
186
|
if (!this.#sessionDetails || !this.#connectionParams) {
|
|
475
187
|
throw new Error('Session not connected');
|
|
476
188
|
}
|
|
477
|
-
if (!this.#mqttClient) {
|
|
478
|
-
throw new Error('MQTT client not connected');
|
|
479
|
-
}
|
|
480
189
|
const payload = {
|
|
481
190
|
context,
|
|
482
191
|
timestamp: Date.now(),
|
|
483
192
|
};
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
headers: getRequestHeaders(this.#connectionParams),
|
|
487
|
-
body: JSON.stringify(payload),
|
|
193
|
+
await axios.post(`${this.#cloudInteropSettings.url}/api/context-groups/${this.#sessionDetails.sessionId}/${contextGroup}`, payload, {
|
|
194
|
+
headers: this.#getRequestHeaders(),
|
|
488
195
|
});
|
|
489
|
-
if (!postResponse.ok) {
|
|
490
|
-
throw new CloudInteropAPIError(`Error setting context for ${contextGroup}`, 'ERR_SETTING_CONTEXT', new Error(postResponse.statusText));
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
/**
|
|
494
|
-
* Starts an intent discovery operation
|
|
495
|
-
*
|
|
496
|
-
* @return {*} {Promise<void>}
|
|
497
|
-
* @memberof CloudInteropAPI
|
|
498
|
-
* @throws {CloudInteropAPIError} - If an error occurs during intent discovery
|
|
499
|
-
*/
|
|
500
|
-
async startIntentDiscovery(options) {
|
|
501
|
-
this.#throwIfNotConnected();
|
|
502
|
-
return this.#intents?.startIntentDiscovery(options);
|
|
503
|
-
}
|
|
504
|
-
async raiseIntent(options) {
|
|
505
|
-
this.#throwIfNotConnected();
|
|
506
|
-
return this.#intents?.raiseIntent(options);
|
|
507
|
-
}
|
|
508
|
-
async reportAppIntents(discoveryId, intents) {
|
|
509
|
-
this.#throwIfNotConnected();
|
|
510
|
-
return this.#intents?.reportAppIntents(discoveryId, intents) ?? false;
|
|
511
|
-
}
|
|
512
|
-
async sendIntentResult(initiatingSessionId, result) {
|
|
513
|
-
this.#throwIfNotConnected();
|
|
514
|
-
return this.#intents?.sendIntentResult(initiatingSessionId, result);
|
|
515
|
-
}
|
|
516
|
-
parseSessionId(appId) {
|
|
517
|
-
return parseSessionId(appId);
|
|
518
196
|
}
|
|
519
197
|
addEventListener(type, callback) {
|
|
520
|
-
this.#
|
|
198
|
+
const listeners = this.#eventListeners.get(type) || [];
|
|
199
|
+
listeners.push(callback);
|
|
200
|
+
this.#eventListeners.set(type, listeners);
|
|
521
201
|
}
|
|
522
202
|
removeEventListener(type, callback) {
|
|
523
|
-
this.#
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
203
|
+
const listeners = this.#eventListeners.get(type) || [];
|
|
204
|
+
const index = listeners.indexOf(callback);
|
|
205
|
+
if (index !== -1) {
|
|
206
|
+
listeners.splice(index, 1);
|
|
207
|
+
}
|
|
208
|
+
this.#eventListeners.set(type, listeners);
|
|
527
209
|
}
|
|
528
210
|
async #disconnect(fireDisconnectedEvent) {
|
|
529
|
-
if (!this.#sessionDetails
|
|
211
|
+
if (!this.#sessionDetails) {
|
|
530
212
|
return;
|
|
531
213
|
}
|
|
532
214
|
try {
|
|
533
|
-
const disconnectResponse = await
|
|
534
|
-
|
|
535
|
-
headers: getRequestHeaders(this.#connectionParams),
|
|
215
|
+
const disconnectResponse = await axios.delete(`${this.#cloudInteropSettings.url}/api/sessions/${this.#sessionDetails.sessionId}`, {
|
|
216
|
+
headers: this.#getRequestHeaders(),
|
|
536
217
|
});
|
|
537
218
|
if (disconnectResponse.status !== 200) {
|
|
538
|
-
throw new CloudInteropAPIError('Error during session tear down - unexpected status', 'ERR_DISCONNECT',
|
|
219
|
+
throw new CloudInteropAPIError('Error during session tear down - unexpected status', 'ERR_DISCONNECT', disconnectResponse.status);
|
|
539
220
|
}
|
|
540
221
|
}
|
|
541
|
-
catch
|
|
542
|
-
throw new CloudInteropAPIError('Error during disconnection', 'ERR_DISCONNECT'
|
|
222
|
+
catch {
|
|
223
|
+
throw new CloudInteropAPIError('Error during disconnection', 'ERR_DISCONNECT');
|
|
543
224
|
}
|
|
544
225
|
finally {
|
|
545
|
-
this.#destroyControllers();
|
|
546
226
|
this.#mqttClient?.removeAllListeners();
|
|
547
227
|
await this.#mqttClient?.endAsync(true);
|
|
548
228
|
this.#sessionDetails = undefined;
|
|
@@ -550,42 +230,27 @@ class CloudInteropAPI {
|
|
|
550
230
|
this.#reconnectRetries = 0;
|
|
551
231
|
this.#attemptingToReconnect = false;
|
|
552
232
|
if (fireDisconnectedEvent) {
|
|
553
|
-
this.#
|
|
233
|
+
this.#emitEvent('disconnected');
|
|
554
234
|
}
|
|
555
235
|
}
|
|
556
236
|
}
|
|
557
|
-
#
|
|
237
|
+
#handleCommand(topic, message, sessionDetails) {
|
|
558
238
|
if (message.length === 0 || !sessionDetails) {
|
|
559
239
|
// Ignore clean up messages
|
|
560
240
|
return;
|
|
561
241
|
}
|
|
562
242
|
const messageEnvelope = JSON.parse(message.toString());
|
|
563
243
|
if (topic.startsWith(`${sessionDetails.sessionRootTopic}/context-groups/`)) {
|
|
564
|
-
|
|
565
|
-
if (contextEvent.source.sessionId === sessionDetails.sessionId) {
|
|
244
|
+
if (messageEnvelope.source.sessionId === sessionDetails.sessionId) {
|
|
566
245
|
return;
|
|
567
246
|
}
|
|
568
|
-
const { contextGroup, context, source, history } =
|
|
569
|
-
this.#
|
|
570
|
-
}
|
|
571
|
-
else if (topic.startsWith(`${sessionDetails.sessionRootTopic}/commands`)) {
|
|
572
|
-
this.#handleCommandMessage(messageEnvelope);
|
|
247
|
+
const { channelName: contextGroup, payload: context, source, history } = messageEnvelope;
|
|
248
|
+
this.#emitEvent('context', { contextGroup, context, source, history: { ...history, clientReceived: Date.now() } });
|
|
573
249
|
}
|
|
574
250
|
}
|
|
575
|
-
#
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
case 'intent-details':
|
|
579
|
-
case 'raise-intent':
|
|
580
|
-
case 'intent-result': {
|
|
581
|
-
this.#intents?.handleCommandMessage(message);
|
|
582
|
-
break;
|
|
583
|
-
}
|
|
584
|
-
default: {
|
|
585
|
-
this.#logger('warn', `Unknown command message received:\n${JSON.stringify(message, null, 2)}`);
|
|
586
|
-
break;
|
|
587
|
-
}
|
|
588
|
-
}
|
|
251
|
+
#emitEvent(type, ...args) {
|
|
252
|
+
const listeners = this.#eventListeners.get(type) || [];
|
|
253
|
+
listeners.forEach((listener) => listener(...args));
|
|
589
254
|
}
|
|
590
255
|
#validateConnectParams = (parameters) => {
|
|
591
256
|
if (!parameters) {
|
|
@@ -594,9 +259,6 @@ class CloudInteropAPI {
|
|
|
594
259
|
if (!parameters.sourceId) {
|
|
595
260
|
throw new Error('sourceId must be provided');
|
|
596
261
|
}
|
|
597
|
-
if (parameters.sourceId.includes(APP_ID_DELIM)) {
|
|
598
|
-
throw new Error(`sourceId cannot contain "${APP_ID_DELIM}"`);
|
|
599
|
-
}
|
|
600
262
|
if (!parameters.platformId) {
|
|
601
263
|
throw new Error('platformId must be provided');
|
|
602
264
|
}
|
|
@@ -609,28 +271,29 @@ class CloudInteropAPI {
|
|
|
609
271
|
throw new Error('basicAuthenticationParameters must be provided when using basic authentication');
|
|
610
272
|
}
|
|
611
273
|
};
|
|
612
|
-
#
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
#destroyControllers() {
|
|
616
|
-
this.#intents = undefined;
|
|
617
|
-
}
|
|
618
|
-
#throwIfNotConnected() {
|
|
619
|
-
if (!this.#sessionDetails || !this.#connectionParams) {
|
|
620
|
-
throw new Error('Session not connected');
|
|
274
|
+
#getRequestHeaders = () => {
|
|
275
|
+
if (!this.#connectionParams) {
|
|
276
|
+
throw new Error('Connect parameters must be provided');
|
|
621
277
|
}
|
|
622
|
-
|
|
623
|
-
|
|
278
|
+
const headers = new axios.AxiosHeaders();
|
|
279
|
+
headers['Content-Type'] = 'application/json';
|
|
280
|
+
if (this.#connectionParams.authenticationType === 'jwt' && this.#connectionParams.jwtAuthenticationParameters) {
|
|
281
|
+
const tokenResult = this.#connectionParams.jwtAuthenticationParameters.jwtRequestCallback();
|
|
282
|
+
if (!tokenResult) {
|
|
283
|
+
throw new Error('jwtRequestCallback must return a token');
|
|
284
|
+
}
|
|
285
|
+
headers['x-of-auth-id'] = this.#connectionParams.jwtAuthenticationParameters.authenticationId;
|
|
286
|
+
headers['Authorization'] =
|
|
287
|
+
typeof tokenResult === 'string' ? `Bearer ${tokenResult}` : `Bearer ${Buffer.from(JSON.stringify(tokenResult)).toString('base64')}`;
|
|
624
288
|
}
|
|
625
|
-
|
|
289
|
+
if (this.#connectionParams.authenticationType === 'basic' && this.#connectionParams.basicAuthenticationParameters) {
|
|
290
|
+
const { username, password } = this.#connectionParams.basicAuthenticationParameters;
|
|
291
|
+
headers['Authorization'] = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
|
|
292
|
+
}
|
|
293
|
+
return headers;
|
|
294
|
+
};
|
|
626
295
|
}
|
|
627
296
|
|
|
628
297
|
exports.AuthorizationError = AuthorizationError;
|
|
629
298
|
exports.CloudInteropAPI = CloudInteropAPI;
|
|
630
299
|
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
|
-
});
|