@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 CHANGED
@@ -11,11 +11,12 @@ type CreateSessionResponse = {
11
11
  sourceId: string;
12
12
  };
13
13
  type EventMap = {
14
- connected: () => void;
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
- _sessionDetails;
30
- _mqttClient;
31
- reconnectRetryLimit = 30;
32
- logger = (level, message) => {
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.cloudInteropSettings = cloudInteropSettings;
43
+ this.#cloudInteropSettings = cloudInteropSettings;
40
44
  }
41
45
  get sessionDetails() {
42
- return this._sessionDetails;
46
+ return this.#sessionDetails;
43
47
  }
44
48
  get mqttClient() {
45
- return this._mqttClient;
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.connectionParams = parameters;
59
- this.reconnectRetryLimit = parameters.reconnectRetryLimit || this.reconnectRetryLimit;
60
- this.logger = parameters.logger || this.logger;
61
- const { sourceId, platformId } = this.connectionParams;
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.cloudInteropSettings.url}/api/sessions`, {
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.cloudInteropSettings.url}`, 'ERR_CONNECT', createSessionResponse.status);
75
+ throw new CloudInteropAPIError(`Failed to connect to the Cloud Interop service: ${this.#cloudInteropSettings.url}`, 'ERR_CONNECT', createSessionResponse.status);
71
76
  }
72
- this._sessionDetails = createSessionResponse.data;
73
- const sessionRootTopic = this._sessionDetails.sessionRootTopic;
77
+ this.#sessionDetails = createSessionResponse.data;
78
+ const sessionRootTopic = this.#sessionDetails.sessionRootTopic;
74
79
  const clientOptions = {
75
- clientId: this._sessionDetails.sessionId,
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._sessionDetails)),
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._sessionDetails.token,
95
+ username: this.#sessionDetails.token,
87
96
  };
88
- this._mqttClient = await mqtt.connectAsync(this._sessionDetails.url, clientOptions);
89
- this.logger('log', `Cloud Interop successfully connected to ${this.cloudInteropSettings.url}`);
90
- this._mqttClient.on('error', (error) => {
91
- this.logger('error', `Cloud Interop Infrastructure Error: ${error}`);
92
- this.#emitEvent('error', error);
93
- this.disconnect();
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._mqttClient.on('reconnect', () => {
96
- this.logger('debug', `Cloud Interop attempting reconnection...`);
97
- // Default reconnectPeriod = 30 seconds
98
- // Attempt reconnection 30 times before ending session
99
- this.reconnectRetries += 1;
100
- if (this.reconnectRetries === this.reconnectRetryLimit) {
101
- this.logger('warn', `Cloud Interop reached max reconnection attempts...`);
102
- this.disconnect();
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.reconnectRetries);
139
+ this.#emitEvent('reconnecting', this.#reconnectRetries);
105
140
  });
106
141
  // Does not fire on initial connection, only successful reconnection attempts
107
- this._mqttClient.on('connect', () => {
108
- this.logger('debug', `Cloud Interop successfully reconnected`);
109
- this.reconnectRetries = 0;
110
- this.#emitEvent('connected');
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._mqttClient.on('message', (topic, message) => {
113
- if (!this._sessionDetails) {
114
- this.logger('warn', 'Received message when session not connected');
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._sessionDetails);
153
+ this.#handleCommand(topic, message, this.#sessionDetails);
118
154
  });
119
155
  // Subscribe to all context groups
120
- this._mqttClient.subscribe(`${sessionRootTopic}/context-groups/#`);
156
+ this.#mqttClient.subscribe(`${sessionRootTopic}/context-groups/#`);
121
157
  // Listen out for global commands
122
- this._mqttClient.subscribe(`${sessionRootTopic}/commands`);
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
- if (!this._sessionDetails) {
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._sessionDetails || !this.connectionParams) {
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.cloudInteropSettings.url}/api/context-groups/${this._sessionDetails.sessionId}/${contextGroup}`, payload, {
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.eventListeners.get(type) || [];
201
+ const listeners = this.#eventListeners.get(type) || [];
188
202
  listeners.push(callback);
189
- this.eventListeners.set(type, listeners);
203
+ this.#eventListeners.set(type, listeners);
190
204
  }
191
205
  removeEventListener(type, callback) {
192
- const listeners = this.eventListeners.get(type) || [];
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.eventListeners.set(type, listeners);
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.eventListeners.get(type) || [];
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.connectionParams) {
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.connectionParams.authenticationType === 'jwt' && this.connectionParams.jwtAuthenticationParameters) {
237
- const tokenResult = this.connectionParams.jwtAuthenticationParameters.jwtRequestCallback();
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.connectionParams.jwtAuthenticationParameters.authenticationId;
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.connectionParams.authenticationType === 'basic' && this.connectionParams.basicAuthenticationParameters) {
246
- const { username, password } = this.connectionParams.basicAuthenticationParameters;
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
- _sessionDetails;
3794
- _mqttClient;
3795
- reconnectRetryLimit = 30;
3796
- logger = (level, message) => {
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.cloudInteropSettings = cloudInteropSettings;
3807
+ this.#cloudInteropSettings = cloudInteropSettings;
3804
3808
  }
3805
3809
  get sessionDetails() {
3806
- return this._sessionDetails;
3810
+ return this.#sessionDetails;
3807
3811
  }
3808
3812
  get mqttClient() {
3809
- return this._mqttClient;
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.connectionParams = parameters;
3823
- this.reconnectRetryLimit = parameters.reconnectRetryLimit || this.reconnectRetryLimit;
3824
- this.logger = parameters.logger || this.logger;
3825
- const { sourceId, platformId } = this.connectionParams;
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.cloudInteropSettings.url}/api/sessions`, {
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.cloudInteropSettings.url}`, 'ERR_CONNECT', createSessionResponse.status);
3839
+ throw new CloudInteropAPIError(`Failed to connect to the Cloud Interop service: ${this.#cloudInteropSettings.url}`, 'ERR_CONNECT', createSessionResponse.status);
3835
3840
  }
3836
- this._sessionDetails = createSessionResponse.data;
3837
- const sessionRootTopic = this._sessionDetails.sessionRootTopic;
3841
+ this.#sessionDetails = createSessionResponse.data;
3842
+ const sessionRootTopic = this.#sessionDetails.sessionRootTopic;
3838
3843
  const clientOptions = {
3839
- clientId: this._sessionDetails.sessionId,
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._sessionDetails)),
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._sessionDetails.token,
3859
+ username: this.#sessionDetails.token,
3851
3860
  };
3852
- this._mqttClient = await mqtt.connectAsync(this._sessionDetails.url, clientOptions);
3853
- this.logger('log', `Cloud Interop successfully connected to ${this.cloudInteropSettings.url}`);
3854
- this._mqttClient.on('error', (error) => {
3855
- this.logger('error', `Cloud Interop Infrastructure Error: ${error}`);
3856
- this.#emitEvent('error', error);
3857
- this.disconnect();
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._mqttClient.on('reconnect', () => {
3860
- this.logger('debug', `Cloud Interop attempting reconnection...`);
3861
- // Default reconnectPeriod = 30 seconds
3862
- // Attempt reconnection 30 times before ending session
3863
- this.reconnectRetries += 1;
3864
- if (this.reconnectRetries === this.reconnectRetryLimit) {
3865
- this.logger('warn', `Cloud Interop reached max reconnection attempts...`);
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.reconnectRetries);
3903
+ this.#emitEvent('reconnecting', this.#reconnectRetries);
3869
3904
  });
3870
3905
  // Does not fire on initial connection, only successful reconnection attempts
3871
- this._mqttClient.on('connect', () => {
3872
- this.logger('debug', `Cloud Interop successfully reconnected`);
3873
- this.reconnectRetries = 0;
3874
- this.#emitEvent('connected');
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._mqttClient.on('message', (topic, message) => {
3877
- if (!this._sessionDetails) {
3878
- this.logger('warn', 'Received message when session not connected');
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._sessionDetails);
3917
+ this.#handleCommand(topic, message, this.#sessionDetails);
3882
3918
  });
3883
3919
  // Subscribe to all context groups
3884
- this._mqttClient.subscribe(`${sessionRootTopic}/context-groups/#`);
3920
+ this.#mqttClient.subscribe(`${sessionRootTopic}/context-groups/#`);
3885
3921
  // Listen out for global commands
3886
- this._mqttClient.subscribe(`${sessionRootTopic}/commands`);
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
- if (!this._sessionDetails) {
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._sessionDetails || !this.connectionParams) {
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.cloudInteropSettings.url}/api/context-groups/${this._sessionDetails.sessionId}/${contextGroup}`, payload, {
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.eventListeners.get(type) || [];
3965
+ const listeners = this.#eventListeners.get(type) || [];
3952
3966
  listeners.push(callback);
3953
- this.eventListeners.set(type, listeners);
3967
+ this.#eventListeners.set(type, listeners);
3954
3968
  }
3955
3969
  removeEventListener(type, callback) {
3956
- const listeners = this.eventListeners.get(type) || [];
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.eventListeners.set(type, listeners);
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.eventListeners.get(type) || [];
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.connectionParams) {
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.connectionParams.authenticationType === 'jwt' && this.connectionParams.jwtAuthenticationParameters) {
4001
- const tokenResult = this.connectionParams.jwtAuthenticationParameters.jwtRequestCallback();
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.connectionParams.jwtAuthenticationParameters.authenticationId;
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.connectionParams.authenticationType === 'basic' && this.connectionParams.basicAuthenticationParameters) {
4010
- const { username, password } = this.connectionParams.basicAuthenticationParameters;
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;
@@ -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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openfin/cloud-interop-core-api",
3
- "version": "0.0.1-alpha.e6793f0",
3
+ "version": "0.0.1-alpha.fba3468",
4
4
  "type": "module",
5
5
  "description": "",
6
6
  "files": [