@openfin/cloud-interop-core-api 0.0.1-alpha.e6793f0 → 0.0.1-alpha.ebcbdf6
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 +16 -27
- 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 +526 -145
- package/dist/index.d.ts +4 -1
- package/dist/index.mjs +524 -3903
- package/dist/{interfaces.d.ts → interfaces/connect.interface.d.ts} +15 -63
- 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 +4 -4
package/dist/index.cjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var axios = require('axios');
|
|
4
3
|
var mqtt = require('mqtt');
|
|
4
|
+
var sharedUtils = require('@openfin/shared-utils');
|
|
5
5
|
|
|
6
6
|
class CloudInteropAPIError extends Error {
|
|
7
7
|
code;
|
|
@@ -17,6 +17,297 @@ 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
|
+
// Error codes as defined in https://docs.emqx.com/en/cloud/latest/connect_to_deployments/mqtt_client_error_codes.html
|
|
310
|
+
const BadUserNamePasswordError = 134;
|
|
20
311
|
/**
|
|
21
312
|
* Represents a single connection to a Cloud Interop service
|
|
22
313
|
*
|
|
@@ -25,24 +316,27 @@ class AuthorizationError extends CloudInteropAPIError {
|
|
|
25
316
|
* @implements {Client}
|
|
26
317
|
*/
|
|
27
318
|
class CloudInteropAPI {
|
|
28
|
-
cloudInteropSettings;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
reconnectRetryLimit = 30;
|
|
32
|
-
|
|
319
|
+
#cloudInteropSettings;
|
|
320
|
+
#sessionDetails;
|
|
321
|
+
#mqttClient;
|
|
322
|
+
#reconnectRetryLimit = 30;
|
|
323
|
+
#keepAliveIntervalSeconds = 30;
|
|
324
|
+
#logger = (level, message) => {
|
|
33
325
|
console[level](message);
|
|
34
326
|
};
|
|
35
|
-
reconnectRetries = 0;
|
|
36
|
-
connectionParams;
|
|
37
|
-
|
|
327
|
+
#reconnectRetries = 0;
|
|
328
|
+
#connectionParams;
|
|
329
|
+
#attemptingToReconnect = false;
|
|
330
|
+
#events = new EventController();
|
|
331
|
+
#intents;
|
|
38
332
|
constructor(cloudInteropSettings) {
|
|
39
|
-
this
|
|
333
|
+
this.#cloudInteropSettings = cloudInteropSettings;
|
|
40
334
|
}
|
|
41
335
|
get sessionDetails() {
|
|
42
|
-
return this
|
|
336
|
+
return this.#sessionDetails;
|
|
43
337
|
}
|
|
44
338
|
get mqttClient() {
|
|
45
|
-
return this
|
|
339
|
+
return this.#mqttClient;
|
|
46
340
|
}
|
|
47
341
|
/**
|
|
48
342
|
* Connects and creates a session on the Cloud Interop service
|
|
@@ -55,81 +349,107 @@ class CloudInteropAPI {
|
|
|
55
349
|
*/
|
|
56
350
|
async connect(parameters) {
|
|
57
351
|
this.#validateConnectParams(parameters);
|
|
58
|
-
this
|
|
59
|
-
this
|
|
60
|
-
this
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
},
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
if (createSessionResponse.status
|
|
70
|
-
throw new
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const sessionRootTopic = this._sessionDetails.sessionRootTopic;
|
|
74
|
-
const clientOptions = {
|
|
75
|
-
clientId: this._sessionDetails.sessionId,
|
|
76
|
-
clean: true,
|
|
77
|
-
protocolVersion: 5,
|
|
78
|
-
// The "will" message will be published on an unexpected disconnection
|
|
79
|
-
// The server can then tidy up. So it needs every for this client to do that, the session details is perfect
|
|
80
|
-
will: {
|
|
81
|
-
topic: 'interop/lastwill',
|
|
82
|
-
payload: Buffer.from(JSON.stringify(this._sessionDetails)),
|
|
83
|
-
qos: 0,
|
|
84
|
-
retain: false,
|
|
85
|
-
},
|
|
86
|
-
username: this._sessionDetails.token,
|
|
87
|
-
};
|
|
88
|
-
this._mqttClient = await mqtt.connectAsync(this._sessionDetails.url, clientOptions);
|
|
89
|
-
this.logger('log', `Cloud Interop successfully connected to ${this.cloudInteropSettings.url}`);
|
|
90
|
-
this._mqttClient.on('error', (error) => {
|
|
91
|
-
this.logger('error', `Cloud Interop Infrastructure Error: ${error}`);
|
|
92
|
-
this.#emitEvent('error', error);
|
|
93
|
-
this.disconnect();
|
|
94
|
-
});
|
|
95
|
-
this._mqttClient.on('reconnect', () => {
|
|
96
|
-
this.logger('debug', `Cloud Interop attempting reconnection...`);
|
|
97
|
-
// Default reconnectPeriod = 30 seconds
|
|
98
|
-
// Attempt reconnection 30 times before ending session
|
|
99
|
-
this.reconnectRetries += 1;
|
|
100
|
-
if (this.reconnectRetries === this.reconnectRetryLimit) {
|
|
101
|
-
this.logger('warn', `Cloud Interop reached max reconnection attempts...`);
|
|
102
|
-
this.disconnect();
|
|
103
|
-
}
|
|
104
|
-
this.#emitEvent('reconnecting', this.reconnectRetries);
|
|
105
|
-
});
|
|
106
|
-
// Does not fire on initial connection, only successful reconnection attempts
|
|
107
|
-
this._mqttClient.on('connect', () => {
|
|
108
|
-
this.logger('debug', `Cloud Interop successfully reconnected`);
|
|
109
|
-
this.reconnectRetries = 0;
|
|
110
|
-
this.#emitEvent('connected');
|
|
111
|
-
});
|
|
112
|
-
this._mqttClient.on('message', (topic, message) => {
|
|
113
|
-
if (!this._sessionDetails) {
|
|
114
|
-
this.logger('warn', 'Received message when session not connected');
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
this.#handleCommand(topic, message, this._sessionDetails);
|
|
118
|
-
});
|
|
119
|
-
// Subscribe to all context groups
|
|
120
|
-
this._mqttClient.subscribe(`${sessionRootTopic}/context-groups/#`);
|
|
121
|
-
// Listen out for global commands
|
|
122
|
-
this._mqttClient.subscribe(`${sessionRootTopic}/commands`);
|
|
352
|
+
this.#connectionParams = parameters;
|
|
353
|
+
this.#reconnectRetryLimit = parameters.reconnectRetryLimit || this.#reconnectRetryLimit;
|
|
354
|
+
this.#keepAliveIntervalSeconds = parameters.keepAliveIntervalSeconds || this.#keepAliveIntervalSeconds;
|
|
355
|
+
this.#logger = parameters.logger || this.#logger;
|
|
356
|
+
const { sourceId, platformId } = this.#connectionParams;
|
|
357
|
+
const createSessionResponse = await fetch(`${this.#cloudInteropSettings.url}/api/sessions`, {
|
|
358
|
+
method: 'POST',
|
|
359
|
+
headers: getRequestHeaders(this.#connectionParams),
|
|
360
|
+
body: JSON.stringify({ sourceId: sourceId.trim(), platformId }),
|
|
361
|
+
});
|
|
362
|
+
if (!createSessionResponse.ok) {
|
|
363
|
+
if (createSessionResponse.status === 401 || createSessionResponse.status === 403) {
|
|
364
|
+
throw new AuthorizationError();
|
|
365
|
+
}
|
|
366
|
+
throw new CloudInteropAPIError();
|
|
123
367
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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);
|
|
397
|
+
}
|
|
398
|
+
if (error instanceof mqtt.ErrorWithReasonCode) {
|
|
399
|
+
switch (error.code) {
|
|
400
|
+
case BadUserNamePasswordError: {
|
|
401
|
+
await this.#disconnect(false);
|
|
402
|
+
this.#logger('warn', `Session expired`);
|
|
403
|
+
this.#events.emitEvent('session-expired');
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
default: {
|
|
407
|
+
this.#logger('error', `Unknown Infrastructure Error Code ${error.code} : ${error.message}${this.#attemptingToReconnect ? ' during reconnection attempt' : ''}`);
|
|
408
|
+
// As we are in the middle of a reconnect, lets not emit an error to cut down on the event noise
|
|
409
|
+
if (!this.#attemptingToReconnect) {
|
|
410
|
+
this.#events.emitEvent('error', new CloudInteropAPIError(`Unknown Infrastructure Error Code ${error.code} : ${error.message}`, 'ERR_INFRASTRUCTURE', error));
|
|
411
|
+
break;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
128
414
|
}
|
|
129
|
-
throw new CloudInteropAPIError();
|
|
130
415
|
}
|
|
131
|
-
|
|
132
|
-
|
|
416
|
+
else {
|
|
417
|
+
this.#logger('error', `Unknown Error${this.#attemptingToReconnect ? ' during reconnection attempt' : ''}: ${error}`);
|
|
418
|
+
// As we are in the middle of a reconnect, lets not emit an error to cut down on the event noise
|
|
419
|
+
if (!this.#attemptingToReconnect) {
|
|
420
|
+
this.#events.emitEvent('error', new CloudInteropAPIError(`Unknown Error`, 'ERR_UNKNOWN', error));
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
this.#mqttClient.on('reconnect', () => {
|
|
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);
|
|
133
453
|
}
|
|
134
454
|
/**
|
|
135
455
|
* Disconnects from the Cloud Interop service
|
|
@@ -139,28 +459,7 @@ class CloudInteropAPI {
|
|
|
139
459
|
* @throws {CloudInteropAPIError} - If an error occurs during disconnection
|
|
140
460
|
*/
|
|
141
461
|
async disconnect() {
|
|
142
|
-
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
try {
|
|
146
|
-
const disconnectResponse = await axios.delete(`${this.cloudInteropSettings.url}/api/sessions/${this._sessionDetails.sessionId}`, {
|
|
147
|
-
headers: this.#getRequestHeaders(),
|
|
148
|
-
});
|
|
149
|
-
if (disconnectResponse.status !== 200) {
|
|
150
|
-
throw new CloudInteropAPIError('Error during disconnection', 'ERR_DISCONNECT', disconnectResponse.status);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
catch {
|
|
154
|
-
throw new CloudInteropAPIError('Error during disconnection', 'ERR_DISCONNECT');
|
|
155
|
-
}
|
|
156
|
-
finally {
|
|
157
|
-
this._mqttClient?.removeAllListeners();
|
|
158
|
-
this._mqttClient?.end(true);
|
|
159
|
-
this._sessionDetails = undefined;
|
|
160
|
-
this._mqttClient = undefined;
|
|
161
|
-
this.reconnectRetries = 0;
|
|
162
|
-
this.#emitEvent('disconnected');
|
|
163
|
-
}
|
|
462
|
+
await this.#disconnect(true);
|
|
164
463
|
}
|
|
165
464
|
/**
|
|
166
465
|
* Publishes a new context for the given context group to the other connected sessions
|
|
@@ -171,53 +470,136 @@ class CloudInteropAPI {
|
|
|
171
470
|
* @memberof CloudInteropAPI
|
|
172
471
|
*/
|
|
173
472
|
async setContext(contextGroup, context) {
|
|
174
|
-
|
|
473
|
+
// TODO: make context of type OpenFin.Context
|
|
474
|
+
if (!this.#sessionDetails || !this.#connectionParams) {
|
|
175
475
|
throw new Error('Session not connected');
|
|
176
476
|
}
|
|
177
|
-
|
|
477
|
+
if (!this.#mqttClient) {
|
|
478
|
+
throw new Error('MQTT client not connected');
|
|
479
|
+
}
|
|
178
480
|
const payload = {
|
|
179
|
-
sourceId,
|
|
180
481
|
context,
|
|
482
|
+
timestamp: Date.now(),
|
|
181
483
|
};
|
|
182
|
-
await
|
|
183
|
-
|
|
484
|
+
const postResponse = await fetch(`${this.#cloudInteropSettings.url}/api/context-groups/${this.#sessionDetails.sessionId}/${contextGroup}`, {
|
|
485
|
+
method: 'POST',
|
|
486
|
+
headers: getRequestHeaders(this.#connectionParams),
|
|
487
|
+
body: JSON.stringify(payload),
|
|
184
488
|
});
|
|
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);
|
|
185
518
|
}
|
|
186
519
|
addEventListener(type, callback) {
|
|
187
|
-
|
|
188
|
-
listeners.push(callback);
|
|
189
|
-
this.eventListeners.set(type, listeners);
|
|
520
|
+
this.#events.addEventListener(type, callback);
|
|
190
521
|
}
|
|
191
522
|
removeEventListener(type, callback) {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
523
|
+
this.#events.removeEventListener(type, callback);
|
|
524
|
+
}
|
|
525
|
+
once(type, callback) {
|
|
526
|
+
this.#events.once(type, callback);
|
|
527
|
+
}
|
|
528
|
+
async #disconnect(fireDisconnectedEvent) {
|
|
529
|
+
if (!this.#sessionDetails || !this.#connectionParams) {
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
try {
|
|
533
|
+
const disconnectResponse = await fetch(`${this.#cloudInteropSettings.url}/api/sessions/${this.#sessionDetails.sessionId}`, {
|
|
534
|
+
method: 'DELETE',
|
|
535
|
+
headers: getRequestHeaders(this.#connectionParams),
|
|
536
|
+
});
|
|
537
|
+
if (disconnectResponse.status !== 200) {
|
|
538
|
+
throw new CloudInteropAPIError('Error during session tear down - unexpected status', 'ERR_DISCONNECT', new Error(disconnectResponse.statusText));
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
catch (error) {
|
|
542
|
+
throw new CloudInteropAPIError('Error during disconnection', 'ERR_DISCONNECT', error);
|
|
543
|
+
}
|
|
544
|
+
finally {
|
|
545
|
+
this.#destroyControllers();
|
|
546
|
+
this.#mqttClient?.removeAllListeners();
|
|
547
|
+
await this.#mqttClient?.endAsync(true);
|
|
548
|
+
this.#sessionDetails = undefined;
|
|
549
|
+
this.#mqttClient = undefined;
|
|
550
|
+
this.#reconnectRetries = 0;
|
|
551
|
+
this.#attemptingToReconnect = false;
|
|
552
|
+
if (fireDisconnectedEvent) {
|
|
553
|
+
this.#events.emitEvent('disconnected');
|
|
554
|
+
}
|
|
196
555
|
}
|
|
197
|
-
this.eventListeners.set(type, listeners);
|
|
198
556
|
}
|
|
199
|
-
#
|
|
557
|
+
#handleMessage(topic, message, sessionDetails) {
|
|
200
558
|
if (message.length === 0 || !sessionDetails) {
|
|
201
559
|
// Ignore clean up messages
|
|
202
560
|
return;
|
|
203
561
|
}
|
|
204
562
|
const messageEnvelope = JSON.parse(message.toString());
|
|
205
563
|
if (topic.startsWith(`${sessionDetails.sessionRootTopic}/context-groups/`)) {
|
|
206
|
-
|
|
564
|
+
const contextEvent = messageEnvelope;
|
|
565
|
+
if (contextEvent.source.sessionId === sessionDetails.sessionId) {
|
|
207
566
|
return;
|
|
208
567
|
}
|
|
209
|
-
const {
|
|
210
|
-
this.#emitEvent('context', { contextGroup, context, source });
|
|
568
|
+
const { contextGroup, context, source, history } = contextEvent;
|
|
569
|
+
this.#events.emitEvent('context', { contextGroup, context, source, history: { ...history, clientReceived: Date.now() } });
|
|
570
|
+
}
|
|
571
|
+
else if (topic.startsWith(`${sessionDetails.sessionRootTopic}/commands`)) {
|
|
572
|
+
this.#handleCommandMessage(messageEnvelope);
|
|
211
573
|
}
|
|
212
574
|
}
|
|
213
|
-
#
|
|
214
|
-
|
|
215
|
-
|
|
575
|
+
#handleCommandMessage(message) {
|
|
576
|
+
switch (message.command) {
|
|
577
|
+
case 'report-intents':
|
|
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
|
+
}
|
|
216
589
|
}
|
|
217
590
|
#validateConnectParams = (parameters) => {
|
|
218
591
|
if (!parameters) {
|
|
219
592
|
throw new Error('Connect parameters must be provided');
|
|
220
593
|
}
|
|
594
|
+
if (!parameters.sourceId) {
|
|
595
|
+
throw new Error('sourceId must be provided');
|
|
596
|
+
}
|
|
597
|
+
if (parameters.sourceId.includes(APP_ID_DELIM)) {
|
|
598
|
+
throw new Error(`sourceId cannot contain "${APP_ID_DELIM}"`);
|
|
599
|
+
}
|
|
600
|
+
if (!parameters.platformId) {
|
|
601
|
+
throw new Error('platformId must be provided');
|
|
602
|
+
}
|
|
221
603
|
if (parameters.authenticationType === 'jwt' &&
|
|
222
604
|
(!parameters.jwtAuthenticationParameters?.jwtRequestCallback || !parameters.jwtAuthenticationParameters?.authenticationId)) {
|
|
223
605
|
throw new Error('jwtAuthenticationParameters must be provided when using jwt authentication');
|
|
@@ -227,29 +609,28 @@ class CloudInteropAPI {
|
|
|
227
609
|
throw new Error('basicAuthenticationParameters must be provided when using basic authentication');
|
|
228
610
|
}
|
|
229
611
|
};
|
|
230
|
-
#
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
throw new Error('jwtRequestCallback must return a token');
|
|
240
|
-
}
|
|
241
|
-
headers['x-of-auth-id'] = this.connectionParams.jwtAuthenticationParameters.authenticationId;
|
|
242
|
-
headers['Authorization'] =
|
|
243
|
-
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');
|
|
244
621
|
}
|
|
245
|
-
if (this
|
|
246
|
-
|
|
247
|
-
headers['Authorization'] = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
|
|
622
|
+
if (!this.#mqttClient) {
|
|
623
|
+
throw new Error('MQTT client not connected');
|
|
248
624
|
}
|
|
249
|
-
|
|
250
|
-
};
|
|
625
|
+
}
|
|
251
626
|
}
|
|
252
627
|
|
|
253
628
|
exports.AuthorizationError = AuthorizationError;
|
|
254
629
|
exports.CloudInteropAPI = CloudInteropAPI;
|
|
255
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
|
+
});
|