@openfin/cloud-interop-core-api 0.0.1-alpha.e6793f0 → 0.0.1-alpha.fba3468
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 +127 -86
- package/dist/index.mjs +127 -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,103 @@ 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,
|
|
91
|
+
properties: {
|
|
92
|
+
willDelayInterval: 10,
|
|
93
|
+
},
|
|
85
94
|
},
|
|
86
|
-
username: this.
|
|
95
|
+
username: this.#sessionDetails.token,
|
|
87
96
|
};
|
|
88
|
-
this
|
|
89
|
-
this
|
|
90
|
-
this.
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
this
|
|
97
|
+
this.#mqttClient = await mqtt.connectAsync(this.#sessionDetails.url, clientOptions);
|
|
98
|
+
this.#logger('log', `Cloud Interop successfully connected to ${this.#cloudInteropSettings.url}`);
|
|
99
|
+
this.#mqttClient.on('error', async (error) => {
|
|
100
|
+
// We will receive errors for each failed reconnection attempt
|
|
101
|
+
// We don't won't to disconnect on these else we will never reconnect
|
|
102
|
+
if (!this.#attemptingToReconnect) {
|
|
103
|
+
await this.#disconnect(false);
|
|
104
|
+
}
|
|
105
|
+
if (error instanceof mqtt.ErrorWithReasonCode) {
|
|
106
|
+
switch (error.code) {
|
|
107
|
+
case BadUserNamePasswordError: {
|
|
108
|
+
await this.#disconnect(false);
|
|
109
|
+
this.#logger('warn', `Session expired`);
|
|
110
|
+
this.#emitEvent('session-expired');
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
default: {
|
|
114
|
+
this.#logger('error', `Unknown Infrastructure Error Code ${error.code} : ${error.message}${this.#attemptingToReconnect ? ' during reconnection attempt' : ''}`);
|
|
115
|
+
// As we are in the middle of a reconnect, lets not emit an error to cut down on the event noise
|
|
116
|
+
if (!this.#attemptingToReconnect) {
|
|
117
|
+
this.#emitEvent('error', new CloudInteropAPIError(`Unknown Infrastructure Error Code ${error.code} : ${error.message}`, 'ERR_INFRASTRUCTURE', error));
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
this.#logger('error', `Unknown Error${this.#attemptingToReconnect ? ' during reconnection attempt' : ''}: ${error}`);
|
|
125
|
+
// As we are in the middle of a reconnect, lets not emit an error to cut down on the event noise
|
|
126
|
+
if (!this.#attemptingToReconnect) {
|
|
127
|
+
this.#emitEvent('error', new CloudInteropAPIError(`Unknown Error`, 'ERR_UNKNOWN', error));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
94
130
|
});
|
|
95
|
-
this.
|
|
96
|
-
this
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
this
|
|
100
|
-
|
|
101
|
-
this
|
|
102
|
-
this.disconnect();
|
|
131
|
+
this.#mqttClient.on('reconnect', () => {
|
|
132
|
+
this.#attemptingToReconnect = true;
|
|
133
|
+
this.#reconnectRetries += 1;
|
|
134
|
+
this.#logger('debug', `Cloud Interop attempting reconnection - ${this.#reconnectRetries}...`);
|
|
135
|
+
if (this.#reconnectRetries === this.#reconnectRetryLimit) {
|
|
136
|
+
this.#logger('warn', `Cloud Interop reached max reconnection attempts - ${this.#reconnectRetryLimit}...`);
|
|
137
|
+
this.#disconnect(true);
|
|
103
138
|
}
|
|
104
|
-
this.#emitEvent('reconnecting', this
|
|
139
|
+
this.#emitEvent('reconnecting', this.#reconnectRetries);
|
|
105
140
|
});
|
|
106
141
|
// Does not fire on initial connection, only successful reconnection attempts
|
|
107
|
-
this.
|
|
108
|
-
this
|
|
109
|
-
this
|
|
110
|
-
this.#
|
|
142
|
+
this.#mqttClient.on('connect', () => {
|
|
143
|
+
this.#logger('debug', `Cloud Interop successfully reconnected after ${this.#reconnectRetries} attempts`);
|
|
144
|
+
this.#reconnectRetries = 0;
|
|
145
|
+
this.#attemptingToReconnect = false;
|
|
146
|
+
this.#emitEvent('reconnected');
|
|
111
147
|
});
|
|
112
|
-
this.
|
|
113
|
-
if (!this
|
|
114
|
-
this
|
|
148
|
+
this.#mqttClient.on('message', (topic, message) => {
|
|
149
|
+
if (!this.#sessionDetails) {
|
|
150
|
+
this.#logger('warn', 'Received message when session not connected');
|
|
115
151
|
return;
|
|
116
152
|
}
|
|
117
|
-
this.#handleCommand(topic, message, this
|
|
153
|
+
this.#handleCommand(topic, message, this.#sessionDetails);
|
|
118
154
|
});
|
|
119
155
|
// Subscribe to all context groups
|
|
120
|
-
this.
|
|
156
|
+
this.#mqttClient.subscribe(`${sessionRootTopic}/context-groups/#`);
|
|
121
157
|
// Listen out for global commands
|
|
122
|
-
this.
|
|
158
|
+
this.#mqttClient.subscribe(`${sessionRootTopic}/commands`);
|
|
123
159
|
}
|
|
124
160
|
catch (error) {
|
|
125
161
|
if (axios.isAxiosError(error)) {
|
|
@@ -139,28 +175,7 @@ class CloudInteropAPI {
|
|
|
139
175
|
* @throws {CloudInteropAPIError} - If an error occurs during disconnection
|
|
140
176
|
*/
|
|
141
177
|
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
|
-
}
|
|
178
|
+
await this.#disconnect(true);
|
|
164
179
|
}
|
|
165
180
|
/**
|
|
166
181
|
* Publishes a new context for the given context group to the other connected sessions
|
|
@@ -171,30 +186,56 @@ class CloudInteropAPI {
|
|
|
171
186
|
* @memberof CloudInteropAPI
|
|
172
187
|
*/
|
|
173
188
|
async setContext(contextGroup, context) {
|
|
174
|
-
if (!this
|
|
189
|
+
if (!this.#sessionDetails || !this.#connectionParams) {
|
|
175
190
|
throw new Error('Session not connected');
|
|
176
191
|
}
|
|
177
|
-
const { sourceId } = this.connectionParams;
|
|
178
192
|
const payload = {
|
|
179
|
-
sourceId,
|
|
180
193
|
context,
|
|
194
|
+
timestamp: Date.now(),
|
|
181
195
|
};
|
|
182
|
-
await axios.post(`${this
|
|
196
|
+
await axios.post(`${this.#cloudInteropSettings.url}/api/context-groups/${this.#sessionDetails.sessionId}/${contextGroup}`, payload, {
|
|
183
197
|
headers: this.#getRequestHeaders(),
|
|
184
198
|
});
|
|
185
199
|
}
|
|
186
200
|
addEventListener(type, callback) {
|
|
187
|
-
const listeners = this
|
|
201
|
+
const listeners = this.#eventListeners.get(type) || [];
|
|
188
202
|
listeners.push(callback);
|
|
189
|
-
this
|
|
203
|
+
this.#eventListeners.set(type, listeners);
|
|
190
204
|
}
|
|
191
205
|
removeEventListener(type, callback) {
|
|
192
|
-
const listeners = this
|
|
206
|
+
const listeners = this.#eventListeners.get(type) || [];
|
|
193
207
|
const index = listeners.indexOf(callback);
|
|
194
208
|
if (index !== -1) {
|
|
195
209
|
listeners.splice(index, 1);
|
|
196
210
|
}
|
|
197
|
-
this
|
|
211
|
+
this.#eventListeners.set(type, listeners);
|
|
212
|
+
}
|
|
213
|
+
async #disconnect(fireDisconnectedEvent) {
|
|
214
|
+
if (!this.#sessionDetails) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
try {
|
|
218
|
+
const disconnectResponse = await axios.delete(`${this.#cloudInteropSettings.url}/api/sessions/${this.#sessionDetails.sessionId}`, {
|
|
219
|
+
headers: this.#getRequestHeaders(),
|
|
220
|
+
});
|
|
221
|
+
if (disconnectResponse.status !== 200) {
|
|
222
|
+
throw new CloudInteropAPIError('Error during session tear down - unexpected status', 'ERR_DISCONNECT', disconnectResponse.status);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
throw new CloudInteropAPIError('Error during disconnection', 'ERR_DISCONNECT');
|
|
227
|
+
}
|
|
228
|
+
finally {
|
|
229
|
+
this.#mqttClient?.removeAllListeners();
|
|
230
|
+
await this.#mqttClient?.endAsync(true);
|
|
231
|
+
this.#sessionDetails = undefined;
|
|
232
|
+
this.#mqttClient = undefined;
|
|
233
|
+
this.#reconnectRetries = 0;
|
|
234
|
+
this.#attemptingToReconnect = false;
|
|
235
|
+
if (fireDisconnectedEvent) {
|
|
236
|
+
this.#emitEvent('disconnected');
|
|
237
|
+
}
|
|
238
|
+
}
|
|
198
239
|
}
|
|
199
240
|
#handleCommand(topic, message, sessionDetails) {
|
|
200
241
|
if (message.length === 0 || !sessionDetails) {
|
|
@@ -206,12 +247,12 @@ class CloudInteropAPI {
|
|
|
206
247
|
if (messageEnvelope.source.sessionId === sessionDetails.sessionId) {
|
|
207
248
|
return;
|
|
208
249
|
}
|
|
209
|
-
const { channelName: contextGroup, payload: context, source } = messageEnvelope;
|
|
210
|
-
this.#emitEvent('context', { contextGroup, context, source });
|
|
250
|
+
const { channelName: contextGroup, payload: context, source, history } = messageEnvelope;
|
|
251
|
+
this.#emitEvent('context', { contextGroup, context, source, history: { ...history, clientReceived: Date.now() } });
|
|
211
252
|
}
|
|
212
253
|
}
|
|
213
254
|
#emitEvent(type, ...args) {
|
|
214
|
-
const listeners = this
|
|
255
|
+
const listeners = this.#eventListeners.get(type) || [];
|
|
215
256
|
listeners.forEach((listener) => listener(...args));
|
|
216
257
|
}
|
|
217
258
|
#validateConnectParams = (parameters) => {
|
|
@@ -228,22 +269,22 @@ class CloudInteropAPI {
|
|
|
228
269
|
}
|
|
229
270
|
};
|
|
230
271
|
#getRequestHeaders = () => {
|
|
231
|
-
if (!this
|
|
272
|
+
if (!this.#connectionParams) {
|
|
232
273
|
throw new Error('Connect parameters must be provided');
|
|
233
274
|
}
|
|
234
275
|
const headers = new axios.AxiosHeaders();
|
|
235
276
|
headers['Content-Type'] = 'application/json';
|
|
236
|
-
if (this
|
|
237
|
-
const tokenResult = this
|
|
277
|
+
if (this.#connectionParams.authenticationType === 'jwt' && this.#connectionParams.jwtAuthenticationParameters) {
|
|
278
|
+
const tokenResult = this.#connectionParams.jwtAuthenticationParameters.jwtRequestCallback();
|
|
238
279
|
if (!tokenResult) {
|
|
239
280
|
throw new Error('jwtRequestCallback must return a token');
|
|
240
281
|
}
|
|
241
|
-
headers['x-of-auth-id'] = this
|
|
282
|
+
headers['x-of-auth-id'] = this.#connectionParams.jwtAuthenticationParameters.authenticationId;
|
|
242
283
|
headers['Authorization'] =
|
|
243
284
|
typeof tokenResult === 'string' ? `Bearer ${tokenResult}` : `Bearer ${Buffer.from(JSON.stringify(tokenResult)).toString('base64')}`;
|
|
244
285
|
}
|
|
245
|
-
if (this
|
|
246
|
-
const { username, password } = this
|
|
286
|
+
if (this.#connectionParams.authenticationType === 'basic' && this.#connectionParams.basicAuthenticationParameters) {
|
|
287
|
+
const { username, password } = this.#connectionParams.basicAuthenticationParameters;
|
|
247
288
|
headers['Authorization'] = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
|
|
248
289
|
}
|
|
249
290
|
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,103 @@ 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,
|
|
3855
|
+
properties: {
|
|
3856
|
+
willDelayInterval: 10,
|
|
3857
|
+
},
|
|
3849
3858
|
},
|
|
3850
|
-
username: this.
|
|
3859
|
+
username: this.#sessionDetails.token,
|
|
3851
3860
|
};
|
|
3852
|
-
this
|
|
3853
|
-
this
|
|
3854
|
-
this.
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
this
|
|
3861
|
+
this.#mqttClient = await mqtt.connectAsync(this.#sessionDetails.url, clientOptions);
|
|
3862
|
+
this.#logger('log', `Cloud Interop successfully connected to ${this.#cloudInteropSettings.url}`);
|
|
3863
|
+
this.#mqttClient.on('error', async (error) => {
|
|
3864
|
+
// We will receive errors for each failed reconnection attempt
|
|
3865
|
+
// We don't won't to disconnect on these else we will never reconnect
|
|
3866
|
+
if (!this.#attemptingToReconnect) {
|
|
3867
|
+
await this.#disconnect(false);
|
|
3868
|
+
}
|
|
3869
|
+
if (error instanceof mqtt.ErrorWithReasonCode) {
|
|
3870
|
+
switch (error.code) {
|
|
3871
|
+
case BadUserNamePasswordError: {
|
|
3872
|
+
await this.#disconnect(false);
|
|
3873
|
+
this.#logger('warn', `Session expired`);
|
|
3874
|
+
this.#emitEvent('session-expired');
|
|
3875
|
+
return;
|
|
3876
|
+
}
|
|
3877
|
+
default: {
|
|
3878
|
+
this.#logger('error', `Unknown Infrastructure Error Code ${error.code} : ${error.message}${this.#attemptingToReconnect ? ' during reconnection attempt' : ''}`);
|
|
3879
|
+
// As we are in the middle of a reconnect, lets not emit an error to cut down on the event noise
|
|
3880
|
+
if (!this.#attemptingToReconnect) {
|
|
3881
|
+
this.#emitEvent('error', new CloudInteropAPIError(`Unknown Infrastructure Error Code ${error.code} : ${error.message}`, 'ERR_INFRASTRUCTURE', error));
|
|
3882
|
+
break;
|
|
3883
|
+
}
|
|
3884
|
+
}
|
|
3885
|
+
}
|
|
3886
|
+
}
|
|
3887
|
+
else {
|
|
3888
|
+
this.#logger('error', `Unknown Error${this.#attemptingToReconnect ? ' during reconnection attempt' : ''}: ${error}`);
|
|
3889
|
+
// As we are in the middle of a reconnect, lets not emit an error to cut down on the event noise
|
|
3890
|
+
if (!this.#attemptingToReconnect) {
|
|
3891
|
+
this.#emitEvent('error', new CloudInteropAPIError(`Unknown Error`, 'ERR_UNKNOWN', error));
|
|
3892
|
+
}
|
|
3893
|
+
}
|
|
3858
3894
|
});
|
|
3859
|
-
this.
|
|
3860
|
-
this
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
this
|
|
3864
|
-
|
|
3865
|
-
this
|
|
3866
|
-
this.disconnect();
|
|
3895
|
+
this.#mqttClient.on('reconnect', () => {
|
|
3896
|
+
this.#attemptingToReconnect = true;
|
|
3897
|
+
this.#reconnectRetries += 1;
|
|
3898
|
+
this.#logger('debug', `Cloud Interop attempting reconnection - ${this.#reconnectRetries}...`);
|
|
3899
|
+
if (this.#reconnectRetries === this.#reconnectRetryLimit) {
|
|
3900
|
+
this.#logger('warn', `Cloud Interop reached max reconnection attempts - ${this.#reconnectRetryLimit}...`);
|
|
3901
|
+
this.#disconnect(true);
|
|
3867
3902
|
}
|
|
3868
|
-
this.#emitEvent('reconnecting', this
|
|
3903
|
+
this.#emitEvent('reconnecting', this.#reconnectRetries);
|
|
3869
3904
|
});
|
|
3870
3905
|
// Does not fire on initial connection, only successful reconnection attempts
|
|
3871
|
-
this.
|
|
3872
|
-
this
|
|
3873
|
-
this
|
|
3874
|
-
this.#
|
|
3906
|
+
this.#mqttClient.on('connect', () => {
|
|
3907
|
+
this.#logger('debug', `Cloud Interop successfully reconnected after ${this.#reconnectRetries} attempts`);
|
|
3908
|
+
this.#reconnectRetries = 0;
|
|
3909
|
+
this.#attemptingToReconnect = false;
|
|
3910
|
+
this.#emitEvent('reconnected');
|
|
3875
3911
|
});
|
|
3876
|
-
this.
|
|
3877
|
-
if (!this
|
|
3878
|
-
this
|
|
3912
|
+
this.#mqttClient.on('message', (topic, message) => {
|
|
3913
|
+
if (!this.#sessionDetails) {
|
|
3914
|
+
this.#logger('warn', 'Received message when session not connected');
|
|
3879
3915
|
return;
|
|
3880
3916
|
}
|
|
3881
|
-
this.#handleCommand(topic, message, this
|
|
3917
|
+
this.#handleCommand(topic, message, this.#sessionDetails);
|
|
3882
3918
|
});
|
|
3883
3919
|
// Subscribe to all context groups
|
|
3884
|
-
this.
|
|
3920
|
+
this.#mqttClient.subscribe(`${sessionRootTopic}/context-groups/#`);
|
|
3885
3921
|
// Listen out for global commands
|
|
3886
|
-
this.
|
|
3922
|
+
this.#mqttClient.subscribe(`${sessionRootTopic}/commands`);
|
|
3887
3923
|
}
|
|
3888
3924
|
catch (error) {
|
|
3889
3925
|
if (axios.isAxiosError(error)) {
|
|
@@ -3903,28 +3939,7 @@ class CloudInteropAPI {
|
|
|
3903
3939
|
* @throws {CloudInteropAPIError} - If an error occurs during disconnection
|
|
3904
3940
|
*/
|
|
3905
3941
|
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
|
-
}
|
|
3942
|
+
await this.#disconnect(true);
|
|
3928
3943
|
}
|
|
3929
3944
|
/**
|
|
3930
3945
|
* Publishes a new context for the given context group to the other connected sessions
|
|
@@ -3935,30 +3950,56 @@ class CloudInteropAPI {
|
|
|
3935
3950
|
* @memberof CloudInteropAPI
|
|
3936
3951
|
*/
|
|
3937
3952
|
async setContext(contextGroup, context) {
|
|
3938
|
-
if (!this
|
|
3953
|
+
if (!this.#sessionDetails || !this.#connectionParams) {
|
|
3939
3954
|
throw new Error('Session not connected');
|
|
3940
3955
|
}
|
|
3941
|
-
const { sourceId } = this.connectionParams;
|
|
3942
3956
|
const payload = {
|
|
3943
|
-
sourceId,
|
|
3944
3957
|
context,
|
|
3958
|
+
timestamp: Date.now(),
|
|
3945
3959
|
};
|
|
3946
|
-
await axios.post(`${this
|
|
3960
|
+
await axios.post(`${this.#cloudInteropSettings.url}/api/context-groups/${this.#sessionDetails.sessionId}/${contextGroup}`, payload, {
|
|
3947
3961
|
headers: this.#getRequestHeaders(),
|
|
3948
3962
|
});
|
|
3949
3963
|
}
|
|
3950
3964
|
addEventListener(type, callback) {
|
|
3951
|
-
const listeners = this
|
|
3965
|
+
const listeners = this.#eventListeners.get(type) || [];
|
|
3952
3966
|
listeners.push(callback);
|
|
3953
|
-
this
|
|
3967
|
+
this.#eventListeners.set(type, listeners);
|
|
3954
3968
|
}
|
|
3955
3969
|
removeEventListener(type, callback) {
|
|
3956
|
-
const listeners = this
|
|
3970
|
+
const listeners = this.#eventListeners.get(type) || [];
|
|
3957
3971
|
const index = listeners.indexOf(callback);
|
|
3958
3972
|
if (index !== -1) {
|
|
3959
3973
|
listeners.splice(index, 1);
|
|
3960
3974
|
}
|
|
3961
|
-
this
|
|
3975
|
+
this.#eventListeners.set(type, listeners);
|
|
3976
|
+
}
|
|
3977
|
+
async #disconnect(fireDisconnectedEvent) {
|
|
3978
|
+
if (!this.#sessionDetails) {
|
|
3979
|
+
return;
|
|
3980
|
+
}
|
|
3981
|
+
try {
|
|
3982
|
+
const disconnectResponse = await axios.delete(`${this.#cloudInteropSettings.url}/api/sessions/${this.#sessionDetails.sessionId}`, {
|
|
3983
|
+
headers: this.#getRequestHeaders(),
|
|
3984
|
+
});
|
|
3985
|
+
if (disconnectResponse.status !== 200) {
|
|
3986
|
+
throw new CloudInteropAPIError('Error during session tear down - unexpected status', 'ERR_DISCONNECT', disconnectResponse.status);
|
|
3987
|
+
}
|
|
3988
|
+
}
|
|
3989
|
+
catch {
|
|
3990
|
+
throw new CloudInteropAPIError('Error during disconnection', 'ERR_DISCONNECT');
|
|
3991
|
+
}
|
|
3992
|
+
finally {
|
|
3993
|
+
this.#mqttClient?.removeAllListeners();
|
|
3994
|
+
await this.#mqttClient?.endAsync(true);
|
|
3995
|
+
this.#sessionDetails = undefined;
|
|
3996
|
+
this.#mqttClient = undefined;
|
|
3997
|
+
this.#reconnectRetries = 0;
|
|
3998
|
+
this.#attemptingToReconnect = false;
|
|
3999
|
+
if (fireDisconnectedEvent) {
|
|
4000
|
+
this.#emitEvent('disconnected');
|
|
4001
|
+
}
|
|
4002
|
+
}
|
|
3962
4003
|
}
|
|
3963
4004
|
#handleCommand(topic, message, sessionDetails) {
|
|
3964
4005
|
if (message.length === 0 || !sessionDetails) {
|
|
@@ -3970,12 +4011,12 @@ class CloudInteropAPI {
|
|
|
3970
4011
|
if (messageEnvelope.source.sessionId === sessionDetails.sessionId) {
|
|
3971
4012
|
return;
|
|
3972
4013
|
}
|
|
3973
|
-
const { channelName: contextGroup, payload: context, source } = messageEnvelope;
|
|
3974
|
-
this.#emitEvent('context', { contextGroup, context, source });
|
|
4014
|
+
const { channelName: contextGroup, payload: context, source, history } = messageEnvelope;
|
|
4015
|
+
this.#emitEvent('context', { contextGroup, context, source, history: { ...history, clientReceived: Date.now() } });
|
|
3975
4016
|
}
|
|
3976
4017
|
}
|
|
3977
4018
|
#emitEvent(type, ...args) {
|
|
3978
|
-
const listeners = this
|
|
4019
|
+
const listeners = this.#eventListeners.get(type) || [];
|
|
3979
4020
|
listeners.forEach((listener) => listener(...args));
|
|
3980
4021
|
}
|
|
3981
4022
|
#validateConnectParams = (parameters) => {
|
|
@@ -3992,22 +4033,22 @@ class CloudInteropAPI {
|
|
|
3992
4033
|
}
|
|
3993
4034
|
};
|
|
3994
4035
|
#getRequestHeaders = () => {
|
|
3995
|
-
if (!this
|
|
4036
|
+
if (!this.#connectionParams) {
|
|
3996
4037
|
throw new Error('Connect parameters must be provided');
|
|
3997
4038
|
}
|
|
3998
4039
|
const headers = new AxiosHeaders();
|
|
3999
4040
|
headers['Content-Type'] = 'application/json';
|
|
4000
|
-
if (this
|
|
4001
|
-
const tokenResult = this
|
|
4041
|
+
if (this.#connectionParams.authenticationType === 'jwt' && this.#connectionParams.jwtAuthenticationParameters) {
|
|
4042
|
+
const tokenResult = this.#connectionParams.jwtAuthenticationParameters.jwtRequestCallback();
|
|
4002
4043
|
if (!tokenResult) {
|
|
4003
4044
|
throw new Error('jwtRequestCallback must return a token');
|
|
4004
4045
|
}
|
|
4005
|
-
headers['x-of-auth-id'] = this
|
|
4046
|
+
headers['x-of-auth-id'] = this.#connectionParams.jwtAuthenticationParameters.authenticationId;
|
|
4006
4047
|
headers['Authorization'] =
|
|
4007
4048
|
typeof tokenResult === 'string' ? `Bearer ${tokenResult}` : `Bearer ${Buffer.from(JSON.stringify(tokenResult)).toString('base64')}`;
|
|
4008
4049
|
}
|
|
4009
|
-
if (this
|
|
4010
|
-
const { username, password } = this
|
|
4050
|
+
if (this.#connectionParams.authenticationType === 'basic' && this.#connectionParams.basicAuthenticationParameters) {
|
|
4051
|
+
const { username, password } = this.#connectionParams.basicAuthenticationParameters;
|
|
4011
4052
|
headers['Authorization'] = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
|
|
4012
4053
|
}
|
|
4013
4054
|
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
|
};
|