@openfin/cloud-interop-core-api 0.0.1-alpha.e6793f0 → 0.0.1-alpha.f23e349
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 +130 -86
- package/dist/index.mjs +130 -86
- package/dist/interfaces.d.ts +16 -7
- 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,18 +244,24 @@ 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) => {
|
|
218
256
|
if (!parameters) {
|
|
219
257
|
throw new Error('Connect parameters must be provided');
|
|
220
258
|
}
|
|
259
|
+
if (!parameters.sourceId) {
|
|
260
|
+
throw new Error('sourceId must be provided');
|
|
261
|
+
}
|
|
262
|
+
if (!parameters.platformId) {
|
|
263
|
+
throw new Error('platformId must be provided');
|
|
264
|
+
}
|
|
221
265
|
if (parameters.authenticationType === 'jwt' &&
|
|
222
266
|
(!parameters.jwtAuthenticationParameters?.jwtRequestCallback || !parameters.jwtAuthenticationParameters?.authenticationId)) {
|
|
223
267
|
throw new Error('jwtAuthenticationParameters must be provided when using jwt authentication');
|
|
@@ -228,22 +272,22 @@ class CloudInteropAPI {
|
|
|
228
272
|
}
|
|
229
273
|
};
|
|
230
274
|
#getRequestHeaders = () => {
|
|
231
|
-
if (!this
|
|
275
|
+
if (!this.#connectionParams) {
|
|
232
276
|
throw new Error('Connect parameters must be provided');
|
|
233
277
|
}
|
|
234
278
|
const headers = new axios.AxiosHeaders();
|
|
235
279
|
headers['Content-Type'] = 'application/json';
|
|
236
|
-
if (this
|
|
237
|
-
const tokenResult = this
|
|
280
|
+
if (this.#connectionParams.authenticationType === 'jwt' && this.#connectionParams.jwtAuthenticationParameters) {
|
|
281
|
+
const tokenResult = this.#connectionParams.jwtAuthenticationParameters.jwtRequestCallback();
|
|
238
282
|
if (!tokenResult) {
|
|
239
283
|
throw new Error('jwtRequestCallback must return a token');
|
|
240
284
|
}
|
|
241
|
-
headers['x-of-auth-id'] = this
|
|
285
|
+
headers['x-of-auth-id'] = this.#connectionParams.jwtAuthenticationParameters.authenticationId;
|
|
242
286
|
headers['Authorization'] =
|
|
243
287
|
typeof tokenResult === 'string' ? `Bearer ${tokenResult}` : `Bearer ${Buffer.from(JSON.stringify(tokenResult)).toString('base64')}`;
|
|
244
288
|
}
|
|
245
|
-
if (this
|
|
246
|
-
const { username, password } = this
|
|
289
|
+
if (this.#connectionParams.authenticationType === 'basic' && this.#connectionParams.basicAuthenticationParameters) {
|
|
290
|
+
const { username, password } = this.#connectionParams.basicAuthenticationParameters;
|
|
247
291
|
headers['Authorization'] = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
|
|
248
292
|
}
|
|
249
293
|
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,18 +4008,24 @@ 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) => {
|
|
3982
4020
|
if (!parameters) {
|
|
3983
4021
|
throw new Error('Connect parameters must be provided');
|
|
3984
4022
|
}
|
|
4023
|
+
if (!parameters.sourceId) {
|
|
4024
|
+
throw new Error('sourceId must be provided');
|
|
4025
|
+
}
|
|
4026
|
+
if (!parameters.platformId) {
|
|
4027
|
+
throw new Error('platformId must be provided');
|
|
4028
|
+
}
|
|
3985
4029
|
if (parameters.authenticationType === 'jwt' &&
|
|
3986
4030
|
(!parameters.jwtAuthenticationParameters?.jwtRequestCallback || !parameters.jwtAuthenticationParameters?.authenticationId)) {
|
|
3987
4031
|
throw new Error('jwtAuthenticationParameters must be provided when using jwt authentication');
|
|
@@ -3992,22 +4036,22 @@ class CloudInteropAPI {
|
|
|
3992
4036
|
}
|
|
3993
4037
|
};
|
|
3994
4038
|
#getRequestHeaders = () => {
|
|
3995
|
-
if (!this
|
|
4039
|
+
if (!this.#connectionParams) {
|
|
3996
4040
|
throw new Error('Connect parameters must be provided');
|
|
3997
4041
|
}
|
|
3998
4042
|
const headers = new AxiosHeaders();
|
|
3999
4043
|
headers['Content-Type'] = 'application/json';
|
|
4000
|
-
if (this
|
|
4001
|
-
const tokenResult = this
|
|
4044
|
+
if (this.#connectionParams.authenticationType === 'jwt' && this.#connectionParams.jwtAuthenticationParameters) {
|
|
4045
|
+
const tokenResult = this.#connectionParams.jwtAuthenticationParameters.jwtRequestCallback();
|
|
4002
4046
|
if (!tokenResult) {
|
|
4003
4047
|
throw new Error('jwtRequestCallback must return a token');
|
|
4004
4048
|
}
|
|
4005
|
-
headers['x-of-auth-id'] = this
|
|
4049
|
+
headers['x-of-auth-id'] = this.#connectionParams.jwtAuthenticationParameters.authenticationId;
|
|
4006
4050
|
headers['Authorization'] =
|
|
4007
4051
|
typeof tokenResult === 'string' ? `Bearer ${tokenResult}` : `Bearer ${Buffer.from(JSON.stringify(tokenResult)).toString('base64')}`;
|
|
4008
4052
|
}
|
|
4009
|
-
if (this
|
|
4010
|
-
const { username, password } = this
|
|
4053
|
+
if (this.#connectionParams.authenticationType === 'basic' && this.#connectionParams.basicAuthenticationParameters) {
|
|
4054
|
+
const { username, password } = this.#connectionParams.basicAuthenticationParameters;
|
|
4011
4055
|
headers['Authorization'] = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
|
|
4012
4056
|
}
|
|
4013
4057
|
return headers;
|
package/dist/interfaces.d.ts
CHANGED
|
@@ -9,27 +9,27 @@ export type CloudInteropLogger = (level: LogLevel, message: string) => void;
|
|
|
9
9
|
export type ConnectParameters = {
|
|
10
10
|
/**
|
|
11
11
|
* ID for a group of shared applications.
|
|
12
|
+
* This acts as a namespace for the interop messages that allows separation of messages between different groups of applications for the same user
|
|
12
13
|
*/
|
|
13
14
|
platformId: string;
|
|
14
15
|
/**
|
|
15
16
|
* An identifier for the source environment e.g. a hostname, a browser name etc.
|
|
16
17
|
*/
|
|
17
|
-
sourceId
|
|
18
|
+
sourceId: string;
|
|
18
19
|
/**
|
|
19
20
|
* A display name for the source environment e.g. Andys Mobile
|
|
20
21
|
*/
|
|
21
22
|
sourceDisplayName?: string;
|
|
22
|
-
/**
|
|
23
|
-
* Specifies an optional extra divider to use that allows separation of interop messages for the
|
|
24
|
-
* same user
|
|
25
|
-
* defaults to "default"
|
|
26
|
-
*/
|
|
27
|
-
realm?: string;
|
|
28
23
|
/**
|
|
29
24
|
* The maximum number of times to retry connecting to the cloud interop service when the connection is dropped
|
|
30
25
|
* defaults to 30
|
|
31
26
|
*/
|
|
32
27
|
reconnectRetryLimit?: number;
|
|
28
|
+
/**
|
|
29
|
+
* Specifies how often keep alive messages should be sent to the cloud interop service in seconds
|
|
30
|
+
* defaults to 30
|
|
31
|
+
*/
|
|
32
|
+
keepAliveIntervalSeconds?: number;
|
|
33
33
|
/**
|
|
34
34
|
* Optional function to call with any logging messages to allow integration with the host application's logging
|
|
35
35
|
*
|
|
@@ -127,7 +127,16 @@ export type IntentDetail = {
|
|
|
127
127
|
* @property {Source} source - The source of the context
|
|
128
128
|
*/
|
|
129
129
|
export type ContextEvent = {
|
|
130
|
+
/**
|
|
131
|
+
* The context group
|
|
132
|
+
*/
|
|
130
133
|
contextGroup: string;
|
|
134
|
+
/**
|
|
135
|
+
* The context object
|
|
136
|
+
*/
|
|
131
137
|
context: object;
|
|
138
|
+
/**
|
|
139
|
+
* The source of the context
|
|
140
|
+
*/
|
|
132
141
|
source: Source;
|
|
133
142
|
};
|