@openfin/cloud-interop-core-api 0.0.1-alpha.e6793f0 → 0.0.1-alpha.ffc0fe6
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 +2 -9
- package/dist/index.cjs +124 -86
- package/dist/index.mjs +124 -86
- package/dist/interfaces.d.ts +14 -0
- package/package.json +1 -1
package/dist/api.d.ts
CHANGED
|
@@ -11,11 +11,12 @@ type CreateSessionResponse = {
|
|
|
11
11
|
sourceId: string;
|
|
12
12
|
};
|
|
13
13
|
type EventMap = {
|
|
14
|
-
|
|
14
|
+
reconnected: () => void;
|
|
15
15
|
disconnected: () => void;
|
|
16
16
|
context: (event: ContextEvent) => void;
|
|
17
17
|
reconnecting: (attemptNo: number) => void;
|
|
18
18
|
error: (error: Error) => void;
|
|
19
|
+
'session-expired': () => void;
|
|
19
20
|
};
|
|
20
21
|
/**
|
|
21
22
|
* Represents a single connection to a Cloud Interop service
|
|
@@ -26,14 +27,6 @@ type EventMap = {
|
|
|
26
27
|
*/
|
|
27
28
|
export declare class CloudInteropAPI {
|
|
28
29
|
#private;
|
|
29
|
-
private cloudInteropSettings;
|
|
30
|
-
private _sessionDetails?;
|
|
31
|
-
private _mqttClient?;
|
|
32
|
-
private reconnectRetryLimit;
|
|
33
|
-
private logger;
|
|
34
|
-
private reconnectRetries;
|
|
35
|
-
private connectionParams?;
|
|
36
|
-
private eventListeners;
|
|
37
30
|
constructor(cloudInteropSettings: CloudInteropSettings);
|
|
38
31
|
get sessionDetails(): CreateSessionResponse | undefined;
|
|
39
32
|
get mqttClient(): mqtt.MqttClient | undefined;
|
package/dist/index.cjs
CHANGED
|
@@ -17,6 +17,8 @@ class AuthorizationError extends CloudInteropAPIError {
|
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
// Error codes as defined in https://docs.emqx.com/en/cloud/latest/connect_to_deployments/mqtt_client_error_codes.html
|
|
21
|
+
const BadUserNamePasswordError = 134;
|
|
20
22
|
/**
|
|
21
23
|
* Represents a single connection to a Cloud Interop service
|
|
22
24
|
*
|
|
@@ -25,24 +27,26 @@ class AuthorizationError extends CloudInteropAPIError {
|
|
|
25
27
|
* @implements {Client}
|
|
26
28
|
*/
|
|
27
29
|
class CloudInteropAPI {
|
|
28
|
-
cloudInteropSettings;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
reconnectRetryLimit = 30;
|
|
32
|
-
|
|
30
|
+
#cloudInteropSettings;
|
|
31
|
+
#sessionDetails;
|
|
32
|
+
#mqttClient;
|
|
33
|
+
#reconnectRetryLimit = 30;
|
|
34
|
+
#keepAliveIntervalSeconds = 30;
|
|
35
|
+
#logger = (level, message) => {
|
|
33
36
|
console[level](message);
|
|
34
37
|
};
|
|
35
|
-
reconnectRetries = 0;
|
|
36
|
-
connectionParams;
|
|
37
|
-
eventListeners = new Map();
|
|
38
|
+
#reconnectRetries = 0;
|
|
39
|
+
#connectionParams;
|
|
40
|
+
#eventListeners = new Map();
|
|
41
|
+
#attemptingToReconnect = false;
|
|
38
42
|
constructor(cloudInteropSettings) {
|
|
39
|
-
this
|
|
43
|
+
this.#cloudInteropSettings = cloudInteropSettings;
|
|
40
44
|
}
|
|
41
45
|
get sessionDetails() {
|
|
42
|
-
return this
|
|
46
|
+
return this.#sessionDetails;
|
|
43
47
|
}
|
|
44
48
|
get mqttClient() {
|
|
45
|
-
return this
|
|
49
|
+
return this.#mqttClient;
|
|
46
50
|
}
|
|
47
51
|
/**
|
|
48
52
|
* Connects and creates a session on the Cloud Interop service
|
|
@@ -55,71 +59,100 @@ class CloudInteropAPI {
|
|
|
55
59
|
*/
|
|
56
60
|
async connect(parameters) {
|
|
57
61
|
this.#validateConnectParams(parameters);
|
|
58
|
-
this
|
|
59
|
-
this
|
|
60
|
-
this
|
|
61
|
-
|
|
62
|
+
this.#connectionParams = parameters;
|
|
63
|
+
this.#reconnectRetryLimit = parameters.reconnectRetryLimit || this.#reconnectRetryLimit;
|
|
64
|
+
this.#keepAliveIntervalSeconds = parameters.keepAliveIntervalSeconds || this.#keepAliveIntervalSeconds;
|
|
65
|
+
this.#logger = parameters.logger || this.#logger;
|
|
66
|
+
const { sourceId, platformId } = this.#connectionParams;
|
|
62
67
|
try {
|
|
63
|
-
const createSessionResponse = await axios.post(`${this
|
|
68
|
+
const createSessionResponse = await axios.post(`${this.#cloudInteropSettings.url}/api/sessions`, {
|
|
64
69
|
sourceId,
|
|
65
70
|
platformId,
|
|
66
71
|
}, {
|
|
67
72
|
headers: this.#getRequestHeaders(),
|
|
68
73
|
});
|
|
69
74
|
if (createSessionResponse.status !== 201) {
|
|
70
|
-
throw new CloudInteropAPIError(`Failed to connect to the Cloud Interop service: ${this
|
|
75
|
+
throw new CloudInteropAPIError(`Failed to connect to the Cloud Interop service: ${this.#cloudInteropSettings.url}`, 'ERR_CONNECT', createSessionResponse.status);
|
|
71
76
|
}
|
|
72
|
-
this
|
|
73
|
-
const sessionRootTopic = this.
|
|
77
|
+
this.#sessionDetails = createSessionResponse.data;
|
|
78
|
+
const sessionRootTopic = this.#sessionDetails.sessionRootTopic;
|
|
74
79
|
const clientOptions = {
|
|
75
|
-
|
|
80
|
+
keepalive: this.#keepAliveIntervalSeconds,
|
|
81
|
+
clientId: this.#sessionDetails.sessionId,
|
|
76
82
|
clean: true,
|
|
77
83
|
protocolVersion: 5,
|
|
78
84
|
// The "will" message will be published on an unexpected disconnection
|
|
79
85
|
// The server can then tidy up. So it needs every for this client to do that, the session details is perfect
|
|
80
86
|
will: {
|
|
81
87
|
topic: 'interop/lastwill',
|
|
82
|
-
payload: Buffer.from(JSON.stringify(this
|
|
88
|
+
payload: Buffer.from(JSON.stringify(this.#sessionDetails)),
|
|
83
89
|
qos: 0,
|
|
84
90
|
retain: false,
|
|
85
91
|
},
|
|
86
|
-
username: this.
|
|
92
|
+
username: this.#sessionDetails.token,
|
|
87
93
|
};
|
|
88
|
-
this
|
|
89
|
-
this
|
|
90
|
-
this.
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
this
|
|
94
|
+
this.#mqttClient = await mqtt.connectAsync(this.#sessionDetails.url, clientOptions);
|
|
95
|
+
this.#logger('log', `Cloud Interop successfully connected to ${this.#cloudInteropSettings.url}`);
|
|
96
|
+
this.#mqttClient.on('error', async (error) => {
|
|
97
|
+
// We will receive errors for each failed reconnection attempt
|
|
98
|
+
// We don't won't to disconnect on these else we will never reconnect
|
|
99
|
+
if (!this.#attemptingToReconnect) {
|
|
100
|
+
await this.#disconnect(false);
|
|
101
|
+
}
|
|
102
|
+
if (error instanceof mqtt.ErrorWithReasonCode) {
|
|
103
|
+
switch (error.code) {
|
|
104
|
+
case BadUserNamePasswordError: {
|
|
105
|
+
await this.#disconnect(false);
|
|
106
|
+
this.#logger('warn', `Session expired`);
|
|
107
|
+
this.#emitEvent('session-expired');
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
default: {
|
|
111
|
+
this.#logger('error', `Unknown Infrastructure Error Code ${error.code} : ${error.message}${this.#attemptingToReconnect ? ' during reconnection attempt' : ''}`);
|
|
112
|
+
// As we are in the middle of a reconnect, lets not emit an error to cut down on the event noise
|
|
113
|
+
if (!this.#attemptingToReconnect) {
|
|
114
|
+
this.#emitEvent('error', new CloudInteropAPIError(`Unknown Infrastructure Error Code ${error.code} : ${error.message}`, 'ERR_INFRASTRUCTURE', error));
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
this.#logger('error', `Unknown Error${this.#attemptingToReconnect ? ' during reconnection attempt' : ''}: ${error}`);
|
|
122
|
+
// As we are in the middle of a reconnect, lets not emit an error to cut down on the event noise
|
|
123
|
+
if (!this.#attemptingToReconnect) {
|
|
124
|
+
this.#emitEvent('error', new CloudInteropAPIError(`Unknown Error`, 'ERR_UNKNOWN', error));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
94
127
|
});
|
|
95
|
-
this.
|
|
96
|
-
this
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
this
|
|
100
|
-
|
|
101
|
-
this
|
|
102
|
-
this.disconnect();
|
|
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);
|
|
103
135
|
}
|
|
104
|
-
this.#emitEvent('reconnecting', this
|
|
136
|
+
this.#emitEvent('reconnecting', this.#reconnectRetries);
|
|
105
137
|
});
|
|
106
138
|
// Does not fire on initial connection, only successful reconnection attempts
|
|
107
|
-
this.
|
|
108
|
-
this
|
|
109
|
-
this
|
|
110
|
-
this.#
|
|
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');
|
|
111
144
|
});
|
|
112
|
-
this.
|
|
113
|
-
if (!this
|
|
114
|
-
this
|
|
145
|
+
this.#mqttClient.on('message', (topic, message) => {
|
|
146
|
+
if (!this.#sessionDetails) {
|
|
147
|
+
this.#logger('warn', 'Received message when session not connected');
|
|
115
148
|
return;
|
|
116
149
|
}
|
|
117
|
-
this.#handleCommand(topic, message, this
|
|
150
|
+
this.#handleCommand(topic, message, this.#sessionDetails);
|
|
118
151
|
});
|
|
119
152
|
// Subscribe to all context groups
|
|
120
|
-
this.
|
|
153
|
+
this.#mqttClient.subscribe(`${sessionRootTopic}/context-groups/#`);
|
|
121
154
|
// Listen out for global commands
|
|
122
|
-
this.
|
|
155
|
+
this.#mqttClient.subscribe(`${sessionRootTopic}/commands`);
|
|
123
156
|
}
|
|
124
157
|
catch (error) {
|
|
125
158
|
if (axios.isAxiosError(error)) {
|
|
@@ -139,28 +172,7 @@ class CloudInteropAPI {
|
|
|
139
172
|
* @throws {CloudInteropAPIError} - If an error occurs during disconnection
|
|
140
173
|
*/
|
|
141
174
|
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
|
-
}
|
|
175
|
+
await this.#disconnect(true);
|
|
164
176
|
}
|
|
165
177
|
/**
|
|
166
178
|
* Publishes a new context for the given context group to the other connected sessions
|
|
@@ -171,30 +183,56 @@ class CloudInteropAPI {
|
|
|
171
183
|
* @memberof CloudInteropAPI
|
|
172
184
|
*/
|
|
173
185
|
async setContext(contextGroup, context) {
|
|
174
|
-
if (!this
|
|
186
|
+
if (!this.#sessionDetails || !this.#connectionParams) {
|
|
175
187
|
throw new Error('Session not connected');
|
|
176
188
|
}
|
|
177
|
-
const { sourceId } = this.connectionParams;
|
|
178
189
|
const payload = {
|
|
179
|
-
sourceId,
|
|
180
190
|
context,
|
|
191
|
+
timestamp: Date.now(),
|
|
181
192
|
};
|
|
182
|
-
await axios.post(`${this
|
|
193
|
+
await axios.post(`${this.#cloudInteropSettings.url}/api/context-groups/${this.#sessionDetails.sessionId}/${contextGroup}`, payload, {
|
|
183
194
|
headers: this.#getRequestHeaders(),
|
|
184
195
|
});
|
|
185
196
|
}
|
|
186
197
|
addEventListener(type, callback) {
|
|
187
|
-
const listeners = this
|
|
198
|
+
const listeners = this.#eventListeners.get(type) || [];
|
|
188
199
|
listeners.push(callback);
|
|
189
|
-
this
|
|
200
|
+
this.#eventListeners.set(type, listeners);
|
|
190
201
|
}
|
|
191
202
|
removeEventListener(type, callback) {
|
|
192
|
-
const listeners = this
|
|
203
|
+
const listeners = this.#eventListeners.get(type) || [];
|
|
193
204
|
const index = listeners.indexOf(callback);
|
|
194
205
|
if (index !== -1) {
|
|
195
206
|
listeners.splice(index, 1);
|
|
196
207
|
}
|
|
197
|
-
this
|
|
208
|
+
this.#eventListeners.set(type, listeners);
|
|
209
|
+
}
|
|
210
|
+
async #disconnect(fireDisconnectedEvent) {
|
|
211
|
+
if (!this.#sessionDetails) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
try {
|
|
215
|
+
const disconnectResponse = await axios.delete(`${this.#cloudInteropSettings.url}/api/sessions/${this.#sessionDetails.sessionId}`, {
|
|
216
|
+
headers: this.#getRequestHeaders(),
|
|
217
|
+
});
|
|
218
|
+
if (disconnectResponse.status !== 200) {
|
|
219
|
+
throw new CloudInteropAPIError('Error during session tear down - unexpected status', 'ERR_DISCONNECT', disconnectResponse.status);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
throw new CloudInteropAPIError('Error during disconnection', 'ERR_DISCONNECT');
|
|
224
|
+
}
|
|
225
|
+
finally {
|
|
226
|
+
this.#mqttClient?.removeAllListeners();
|
|
227
|
+
await this.#mqttClient?.endAsync(true);
|
|
228
|
+
this.#sessionDetails = undefined;
|
|
229
|
+
this.#mqttClient = undefined;
|
|
230
|
+
this.#reconnectRetries = 0;
|
|
231
|
+
this.#attemptingToReconnect = false;
|
|
232
|
+
if (fireDisconnectedEvent) {
|
|
233
|
+
this.#emitEvent('disconnected');
|
|
234
|
+
}
|
|
235
|
+
}
|
|
198
236
|
}
|
|
199
237
|
#handleCommand(topic, message, sessionDetails) {
|
|
200
238
|
if (message.length === 0 || !sessionDetails) {
|
|
@@ -206,12 +244,12 @@ class CloudInteropAPI {
|
|
|
206
244
|
if (messageEnvelope.source.sessionId === sessionDetails.sessionId) {
|
|
207
245
|
return;
|
|
208
246
|
}
|
|
209
|
-
const { channelName: contextGroup, payload: context, source } = messageEnvelope;
|
|
210
|
-
this.#emitEvent('context', { contextGroup, context, source });
|
|
247
|
+
const { channelName: contextGroup, payload: context, source, history } = messageEnvelope;
|
|
248
|
+
this.#emitEvent('context', { contextGroup, context, source, history: { ...history, clientReceived: Date.now() } });
|
|
211
249
|
}
|
|
212
250
|
}
|
|
213
251
|
#emitEvent(type, ...args) {
|
|
214
|
-
const listeners = this
|
|
252
|
+
const listeners = this.#eventListeners.get(type) || [];
|
|
215
253
|
listeners.forEach((listener) => listener(...args));
|
|
216
254
|
}
|
|
217
255
|
#validateConnectParams = (parameters) => {
|
|
@@ -228,22 +266,22 @@ class CloudInteropAPI {
|
|
|
228
266
|
}
|
|
229
267
|
};
|
|
230
268
|
#getRequestHeaders = () => {
|
|
231
|
-
if (!this
|
|
269
|
+
if (!this.#connectionParams) {
|
|
232
270
|
throw new Error('Connect parameters must be provided');
|
|
233
271
|
}
|
|
234
272
|
const headers = new axios.AxiosHeaders();
|
|
235
273
|
headers['Content-Type'] = 'application/json';
|
|
236
|
-
if (this
|
|
237
|
-
const tokenResult = this
|
|
274
|
+
if (this.#connectionParams.authenticationType === 'jwt' && this.#connectionParams.jwtAuthenticationParameters) {
|
|
275
|
+
const tokenResult = this.#connectionParams.jwtAuthenticationParameters.jwtRequestCallback();
|
|
238
276
|
if (!tokenResult) {
|
|
239
277
|
throw new Error('jwtRequestCallback must return a token');
|
|
240
278
|
}
|
|
241
|
-
headers['x-of-auth-id'] = this
|
|
279
|
+
headers['x-of-auth-id'] = this.#connectionParams.jwtAuthenticationParameters.authenticationId;
|
|
242
280
|
headers['Authorization'] =
|
|
243
281
|
typeof tokenResult === 'string' ? `Bearer ${tokenResult}` : `Bearer ${Buffer.from(JSON.stringify(tokenResult)).toString('base64')}`;
|
|
244
282
|
}
|
|
245
|
-
if (this
|
|
246
|
-
const { username, password } = this
|
|
283
|
+
if (this.#connectionParams.authenticationType === 'basic' && this.#connectionParams.basicAuthenticationParameters) {
|
|
284
|
+
const { username, password } = this.#connectionParams.basicAuthenticationParameters;
|
|
247
285
|
headers['Authorization'] = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
|
|
248
286
|
}
|
|
249
287
|
return headers;
|
package/dist/index.mjs
CHANGED
|
@@ -3781,6 +3781,8 @@ class AuthorizationError extends CloudInteropAPIError {
|
|
|
3781
3781
|
}
|
|
3782
3782
|
}
|
|
3783
3783
|
|
|
3784
|
+
// Error codes as defined in https://docs.emqx.com/en/cloud/latest/connect_to_deployments/mqtt_client_error_codes.html
|
|
3785
|
+
const BadUserNamePasswordError = 134;
|
|
3784
3786
|
/**
|
|
3785
3787
|
* Represents a single connection to a Cloud Interop service
|
|
3786
3788
|
*
|
|
@@ -3789,24 +3791,26 @@ class AuthorizationError extends CloudInteropAPIError {
|
|
|
3789
3791
|
* @implements {Client}
|
|
3790
3792
|
*/
|
|
3791
3793
|
class CloudInteropAPI {
|
|
3792
|
-
cloudInteropSettings;
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
reconnectRetryLimit = 30;
|
|
3796
|
-
|
|
3794
|
+
#cloudInteropSettings;
|
|
3795
|
+
#sessionDetails;
|
|
3796
|
+
#mqttClient;
|
|
3797
|
+
#reconnectRetryLimit = 30;
|
|
3798
|
+
#keepAliveIntervalSeconds = 30;
|
|
3799
|
+
#logger = (level, message) => {
|
|
3797
3800
|
console[level](message);
|
|
3798
3801
|
};
|
|
3799
|
-
reconnectRetries = 0;
|
|
3800
|
-
connectionParams;
|
|
3801
|
-
eventListeners = new Map();
|
|
3802
|
+
#reconnectRetries = 0;
|
|
3803
|
+
#connectionParams;
|
|
3804
|
+
#eventListeners = new Map();
|
|
3805
|
+
#attemptingToReconnect = false;
|
|
3802
3806
|
constructor(cloudInteropSettings) {
|
|
3803
|
-
this
|
|
3807
|
+
this.#cloudInteropSettings = cloudInteropSettings;
|
|
3804
3808
|
}
|
|
3805
3809
|
get sessionDetails() {
|
|
3806
|
-
return this
|
|
3810
|
+
return this.#sessionDetails;
|
|
3807
3811
|
}
|
|
3808
3812
|
get mqttClient() {
|
|
3809
|
-
return this
|
|
3813
|
+
return this.#mqttClient;
|
|
3810
3814
|
}
|
|
3811
3815
|
/**
|
|
3812
3816
|
* Connects and creates a session on the Cloud Interop service
|
|
@@ -3819,71 +3823,100 @@ class CloudInteropAPI {
|
|
|
3819
3823
|
*/
|
|
3820
3824
|
async connect(parameters) {
|
|
3821
3825
|
this.#validateConnectParams(parameters);
|
|
3822
|
-
this
|
|
3823
|
-
this
|
|
3824
|
-
this
|
|
3825
|
-
|
|
3826
|
+
this.#connectionParams = parameters;
|
|
3827
|
+
this.#reconnectRetryLimit = parameters.reconnectRetryLimit || this.#reconnectRetryLimit;
|
|
3828
|
+
this.#keepAliveIntervalSeconds = parameters.keepAliveIntervalSeconds || this.#keepAliveIntervalSeconds;
|
|
3829
|
+
this.#logger = parameters.logger || this.#logger;
|
|
3830
|
+
const { sourceId, platformId } = this.#connectionParams;
|
|
3826
3831
|
try {
|
|
3827
|
-
const createSessionResponse = await axios.post(`${this
|
|
3832
|
+
const createSessionResponse = await axios.post(`${this.#cloudInteropSettings.url}/api/sessions`, {
|
|
3828
3833
|
sourceId,
|
|
3829
3834
|
platformId,
|
|
3830
3835
|
}, {
|
|
3831
3836
|
headers: this.#getRequestHeaders(),
|
|
3832
3837
|
});
|
|
3833
3838
|
if (createSessionResponse.status !== 201) {
|
|
3834
|
-
throw new CloudInteropAPIError(`Failed to connect to the Cloud Interop service: ${this
|
|
3839
|
+
throw new CloudInteropAPIError(`Failed to connect to the Cloud Interop service: ${this.#cloudInteropSettings.url}`, 'ERR_CONNECT', createSessionResponse.status);
|
|
3835
3840
|
}
|
|
3836
|
-
this
|
|
3837
|
-
const sessionRootTopic = this.
|
|
3841
|
+
this.#sessionDetails = createSessionResponse.data;
|
|
3842
|
+
const sessionRootTopic = this.#sessionDetails.sessionRootTopic;
|
|
3838
3843
|
const clientOptions = {
|
|
3839
|
-
|
|
3844
|
+
keepalive: this.#keepAliveIntervalSeconds,
|
|
3845
|
+
clientId: this.#sessionDetails.sessionId,
|
|
3840
3846
|
clean: true,
|
|
3841
3847
|
protocolVersion: 5,
|
|
3842
3848
|
// The "will" message will be published on an unexpected disconnection
|
|
3843
3849
|
// The server can then tidy up. So it needs every for this client to do that, the session details is perfect
|
|
3844
3850
|
will: {
|
|
3845
3851
|
topic: 'interop/lastwill',
|
|
3846
|
-
payload: Buffer.from(JSON.stringify(this
|
|
3852
|
+
payload: Buffer.from(JSON.stringify(this.#sessionDetails)),
|
|
3847
3853
|
qos: 0,
|
|
3848
3854
|
retain: false,
|
|
3849
3855
|
},
|
|
3850
|
-
username: this.
|
|
3856
|
+
username: this.#sessionDetails.token,
|
|
3851
3857
|
};
|
|
3852
|
-
this
|
|
3853
|
-
this
|
|
3854
|
-
this.
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
this
|
|
3858
|
+
this.#mqttClient = await mqtt.connectAsync(this.#sessionDetails.url, clientOptions);
|
|
3859
|
+
this.#logger('log', `Cloud Interop successfully connected to ${this.#cloudInteropSettings.url}`);
|
|
3860
|
+
this.#mqttClient.on('error', async (error) => {
|
|
3861
|
+
// We will receive errors for each failed reconnection attempt
|
|
3862
|
+
// We don't won't to disconnect on these else we will never reconnect
|
|
3863
|
+
if (!this.#attemptingToReconnect) {
|
|
3864
|
+
await this.#disconnect(false);
|
|
3865
|
+
}
|
|
3866
|
+
if (error instanceof mqtt.ErrorWithReasonCode) {
|
|
3867
|
+
switch (error.code) {
|
|
3868
|
+
case BadUserNamePasswordError: {
|
|
3869
|
+
await this.#disconnect(false);
|
|
3870
|
+
this.#logger('warn', `Session expired`);
|
|
3871
|
+
this.#emitEvent('session-expired');
|
|
3872
|
+
return;
|
|
3873
|
+
}
|
|
3874
|
+
default: {
|
|
3875
|
+
this.#logger('error', `Unknown Infrastructure Error Code ${error.code} : ${error.message}${this.#attemptingToReconnect ? ' during reconnection attempt' : ''}`);
|
|
3876
|
+
// As we are in the middle of a reconnect, lets not emit an error to cut down on the event noise
|
|
3877
|
+
if (!this.#attemptingToReconnect) {
|
|
3878
|
+
this.#emitEvent('error', new CloudInteropAPIError(`Unknown Infrastructure Error Code ${error.code} : ${error.message}`, 'ERR_INFRASTRUCTURE', error));
|
|
3879
|
+
break;
|
|
3880
|
+
}
|
|
3881
|
+
}
|
|
3882
|
+
}
|
|
3883
|
+
}
|
|
3884
|
+
else {
|
|
3885
|
+
this.#logger('error', `Unknown Error${this.#attemptingToReconnect ? ' during reconnection attempt' : ''}: ${error}`);
|
|
3886
|
+
// As we are in the middle of a reconnect, lets not emit an error to cut down on the event noise
|
|
3887
|
+
if (!this.#attemptingToReconnect) {
|
|
3888
|
+
this.#emitEvent('error', new CloudInteropAPIError(`Unknown Error`, 'ERR_UNKNOWN', error));
|
|
3889
|
+
}
|
|
3890
|
+
}
|
|
3858
3891
|
});
|
|
3859
|
-
this.
|
|
3860
|
-
this
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
this
|
|
3864
|
-
|
|
3865
|
-
this
|
|
3866
|
-
this.disconnect();
|
|
3892
|
+
this.#mqttClient.on('reconnect', () => {
|
|
3893
|
+
this.#attemptingToReconnect = true;
|
|
3894
|
+
this.#reconnectRetries += 1;
|
|
3895
|
+
this.#logger('debug', `Cloud Interop attempting reconnection - ${this.#reconnectRetries}...`);
|
|
3896
|
+
if (this.#reconnectRetries === this.#reconnectRetryLimit) {
|
|
3897
|
+
this.#logger('warn', `Cloud Interop reached max reconnection attempts - ${this.#reconnectRetryLimit}...`);
|
|
3898
|
+
this.#disconnect(true);
|
|
3867
3899
|
}
|
|
3868
|
-
this.#emitEvent('reconnecting', this
|
|
3900
|
+
this.#emitEvent('reconnecting', this.#reconnectRetries);
|
|
3869
3901
|
});
|
|
3870
3902
|
// Does not fire on initial connection, only successful reconnection attempts
|
|
3871
|
-
this.
|
|
3872
|
-
this
|
|
3873
|
-
this
|
|
3874
|
-
this.#
|
|
3903
|
+
this.#mqttClient.on('connect', () => {
|
|
3904
|
+
this.#logger('debug', `Cloud Interop successfully reconnected after ${this.#reconnectRetries} attempts`);
|
|
3905
|
+
this.#reconnectRetries = 0;
|
|
3906
|
+
this.#attemptingToReconnect = false;
|
|
3907
|
+
this.#emitEvent('reconnected');
|
|
3875
3908
|
});
|
|
3876
|
-
this.
|
|
3877
|
-
if (!this
|
|
3878
|
-
this
|
|
3909
|
+
this.#mqttClient.on('message', (topic, message) => {
|
|
3910
|
+
if (!this.#sessionDetails) {
|
|
3911
|
+
this.#logger('warn', 'Received message when session not connected');
|
|
3879
3912
|
return;
|
|
3880
3913
|
}
|
|
3881
|
-
this.#handleCommand(topic, message, this
|
|
3914
|
+
this.#handleCommand(topic, message, this.#sessionDetails);
|
|
3882
3915
|
});
|
|
3883
3916
|
// Subscribe to all context groups
|
|
3884
|
-
this.
|
|
3917
|
+
this.#mqttClient.subscribe(`${sessionRootTopic}/context-groups/#`);
|
|
3885
3918
|
// Listen out for global commands
|
|
3886
|
-
this.
|
|
3919
|
+
this.#mqttClient.subscribe(`${sessionRootTopic}/commands`);
|
|
3887
3920
|
}
|
|
3888
3921
|
catch (error) {
|
|
3889
3922
|
if (axios.isAxiosError(error)) {
|
|
@@ -3903,28 +3936,7 @@ class CloudInteropAPI {
|
|
|
3903
3936
|
* @throws {CloudInteropAPIError} - If an error occurs during disconnection
|
|
3904
3937
|
*/
|
|
3905
3938
|
async disconnect() {
|
|
3906
|
-
|
|
3907
|
-
return;
|
|
3908
|
-
}
|
|
3909
|
-
try {
|
|
3910
|
-
const disconnectResponse = await axios.delete(`${this.cloudInteropSettings.url}/api/sessions/${this._sessionDetails.sessionId}`, {
|
|
3911
|
-
headers: this.#getRequestHeaders(),
|
|
3912
|
-
});
|
|
3913
|
-
if (disconnectResponse.status !== 200) {
|
|
3914
|
-
throw new CloudInteropAPIError('Error during disconnection', 'ERR_DISCONNECT', disconnectResponse.status);
|
|
3915
|
-
}
|
|
3916
|
-
}
|
|
3917
|
-
catch {
|
|
3918
|
-
throw new CloudInteropAPIError('Error during disconnection', 'ERR_DISCONNECT');
|
|
3919
|
-
}
|
|
3920
|
-
finally {
|
|
3921
|
-
this._mqttClient?.removeAllListeners();
|
|
3922
|
-
this._mqttClient?.end(true);
|
|
3923
|
-
this._sessionDetails = undefined;
|
|
3924
|
-
this._mqttClient = undefined;
|
|
3925
|
-
this.reconnectRetries = 0;
|
|
3926
|
-
this.#emitEvent('disconnected');
|
|
3927
|
-
}
|
|
3939
|
+
await this.#disconnect(true);
|
|
3928
3940
|
}
|
|
3929
3941
|
/**
|
|
3930
3942
|
* Publishes a new context for the given context group to the other connected sessions
|
|
@@ -3935,30 +3947,56 @@ class CloudInteropAPI {
|
|
|
3935
3947
|
* @memberof CloudInteropAPI
|
|
3936
3948
|
*/
|
|
3937
3949
|
async setContext(contextGroup, context) {
|
|
3938
|
-
if (!this
|
|
3950
|
+
if (!this.#sessionDetails || !this.#connectionParams) {
|
|
3939
3951
|
throw new Error('Session not connected');
|
|
3940
3952
|
}
|
|
3941
|
-
const { sourceId } = this.connectionParams;
|
|
3942
3953
|
const payload = {
|
|
3943
|
-
sourceId,
|
|
3944
3954
|
context,
|
|
3955
|
+
timestamp: Date.now(),
|
|
3945
3956
|
};
|
|
3946
|
-
await axios.post(`${this
|
|
3957
|
+
await axios.post(`${this.#cloudInteropSettings.url}/api/context-groups/${this.#sessionDetails.sessionId}/${contextGroup}`, payload, {
|
|
3947
3958
|
headers: this.#getRequestHeaders(),
|
|
3948
3959
|
});
|
|
3949
3960
|
}
|
|
3950
3961
|
addEventListener(type, callback) {
|
|
3951
|
-
const listeners = this
|
|
3962
|
+
const listeners = this.#eventListeners.get(type) || [];
|
|
3952
3963
|
listeners.push(callback);
|
|
3953
|
-
this
|
|
3964
|
+
this.#eventListeners.set(type, listeners);
|
|
3954
3965
|
}
|
|
3955
3966
|
removeEventListener(type, callback) {
|
|
3956
|
-
const listeners = this
|
|
3967
|
+
const listeners = this.#eventListeners.get(type) || [];
|
|
3957
3968
|
const index = listeners.indexOf(callback);
|
|
3958
3969
|
if (index !== -1) {
|
|
3959
3970
|
listeners.splice(index, 1);
|
|
3960
3971
|
}
|
|
3961
|
-
this
|
|
3972
|
+
this.#eventListeners.set(type, listeners);
|
|
3973
|
+
}
|
|
3974
|
+
async #disconnect(fireDisconnectedEvent) {
|
|
3975
|
+
if (!this.#sessionDetails) {
|
|
3976
|
+
return;
|
|
3977
|
+
}
|
|
3978
|
+
try {
|
|
3979
|
+
const disconnectResponse = await axios.delete(`${this.#cloudInteropSettings.url}/api/sessions/${this.#sessionDetails.sessionId}`, {
|
|
3980
|
+
headers: this.#getRequestHeaders(),
|
|
3981
|
+
});
|
|
3982
|
+
if (disconnectResponse.status !== 200) {
|
|
3983
|
+
throw new CloudInteropAPIError('Error during session tear down - unexpected status', 'ERR_DISCONNECT', disconnectResponse.status);
|
|
3984
|
+
}
|
|
3985
|
+
}
|
|
3986
|
+
catch {
|
|
3987
|
+
throw new CloudInteropAPIError('Error during disconnection', 'ERR_DISCONNECT');
|
|
3988
|
+
}
|
|
3989
|
+
finally {
|
|
3990
|
+
this.#mqttClient?.removeAllListeners();
|
|
3991
|
+
await this.#mqttClient?.endAsync(true);
|
|
3992
|
+
this.#sessionDetails = undefined;
|
|
3993
|
+
this.#mqttClient = undefined;
|
|
3994
|
+
this.#reconnectRetries = 0;
|
|
3995
|
+
this.#attemptingToReconnect = false;
|
|
3996
|
+
if (fireDisconnectedEvent) {
|
|
3997
|
+
this.#emitEvent('disconnected');
|
|
3998
|
+
}
|
|
3999
|
+
}
|
|
3962
4000
|
}
|
|
3963
4001
|
#handleCommand(topic, message, sessionDetails) {
|
|
3964
4002
|
if (message.length === 0 || !sessionDetails) {
|
|
@@ -3970,12 +4008,12 @@ class CloudInteropAPI {
|
|
|
3970
4008
|
if (messageEnvelope.source.sessionId === sessionDetails.sessionId) {
|
|
3971
4009
|
return;
|
|
3972
4010
|
}
|
|
3973
|
-
const { channelName: contextGroup, payload: context, source } = messageEnvelope;
|
|
3974
|
-
this.#emitEvent('context', { contextGroup, context, source });
|
|
4011
|
+
const { channelName: contextGroup, payload: context, source, history } = messageEnvelope;
|
|
4012
|
+
this.#emitEvent('context', { contextGroup, context, source, history: { ...history, clientReceived: Date.now() } });
|
|
3975
4013
|
}
|
|
3976
4014
|
}
|
|
3977
4015
|
#emitEvent(type, ...args) {
|
|
3978
|
-
const listeners = this
|
|
4016
|
+
const listeners = this.#eventListeners.get(type) || [];
|
|
3979
4017
|
listeners.forEach((listener) => listener(...args));
|
|
3980
4018
|
}
|
|
3981
4019
|
#validateConnectParams = (parameters) => {
|
|
@@ -3992,22 +4030,22 @@ class CloudInteropAPI {
|
|
|
3992
4030
|
}
|
|
3993
4031
|
};
|
|
3994
4032
|
#getRequestHeaders = () => {
|
|
3995
|
-
if (!this
|
|
4033
|
+
if (!this.#connectionParams) {
|
|
3996
4034
|
throw new Error('Connect parameters must be provided');
|
|
3997
4035
|
}
|
|
3998
4036
|
const headers = new AxiosHeaders();
|
|
3999
4037
|
headers['Content-Type'] = 'application/json';
|
|
4000
|
-
if (this
|
|
4001
|
-
const tokenResult = this
|
|
4038
|
+
if (this.#connectionParams.authenticationType === 'jwt' && this.#connectionParams.jwtAuthenticationParameters) {
|
|
4039
|
+
const tokenResult = this.#connectionParams.jwtAuthenticationParameters.jwtRequestCallback();
|
|
4002
4040
|
if (!tokenResult) {
|
|
4003
4041
|
throw new Error('jwtRequestCallback must return a token');
|
|
4004
4042
|
}
|
|
4005
|
-
headers['x-of-auth-id'] = this
|
|
4043
|
+
headers['x-of-auth-id'] = this.#connectionParams.jwtAuthenticationParameters.authenticationId;
|
|
4006
4044
|
headers['Authorization'] =
|
|
4007
4045
|
typeof tokenResult === 'string' ? `Bearer ${tokenResult}` : `Bearer ${Buffer.from(JSON.stringify(tokenResult)).toString('base64')}`;
|
|
4008
4046
|
}
|
|
4009
|
-
if (this
|
|
4010
|
-
const { username, password } = this
|
|
4047
|
+
if (this.#connectionParams.authenticationType === 'basic' && this.#connectionParams.basicAuthenticationParameters) {
|
|
4048
|
+
const { username, password } = this.#connectionParams.basicAuthenticationParameters;
|
|
4011
4049
|
headers['Authorization'] = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
|
|
4012
4050
|
}
|
|
4013
4051
|
return headers;
|
package/dist/interfaces.d.ts
CHANGED
|
@@ -30,6 +30,11 @@ export type ConnectParameters = {
|
|
|
30
30
|
* defaults to 30
|
|
31
31
|
*/
|
|
32
32
|
reconnectRetryLimit?: number;
|
|
33
|
+
/**
|
|
34
|
+
* Specifies how often keep alive messages should be sent to the cloud interop service in seconds
|
|
35
|
+
* defaults to 30
|
|
36
|
+
*/
|
|
37
|
+
keepAliveIntervalSeconds?: number;
|
|
33
38
|
/**
|
|
34
39
|
* Optional function to call with any logging messages to allow integration with the host application's logging
|
|
35
40
|
*
|
|
@@ -127,7 +132,16 @@ export type IntentDetail = {
|
|
|
127
132
|
* @property {Source} source - The source of the context
|
|
128
133
|
*/
|
|
129
134
|
export type ContextEvent = {
|
|
135
|
+
/**
|
|
136
|
+
* The context group
|
|
137
|
+
*/
|
|
130
138
|
contextGroup: string;
|
|
139
|
+
/**
|
|
140
|
+
* The context object
|
|
141
|
+
*/
|
|
131
142
|
context: object;
|
|
143
|
+
/**
|
|
144
|
+
* The source of the context
|
|
145
|
+
*/
|
|
132
146
|
source: Source;
|
|
133
147
|
};
|