@openfin/cloud-interop-core-api 0.0.1-alpha.e6793f0 → 0.0.1-alpha.ffc0fe6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/api.d.ts 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,100 @@ 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,
85
91
  },
86
- username: this._sessionDetails.token,
92
+ username: this.#sessionDetails.token,
87
93
  };
88
- this._mqttClient = await mqtt.connectAsync(this._sessionDetails.url, clientOptions);
89
- this.logger('log', `Cloud Interop successfully connected to ${this.cloudInteropSettings.url}`);
90
- this._mqttClient.on('error', (error) => {
91
- this.logger('error', `Cloud Interop Infrastructure Error: ${error}`);
92
- this.#emitEvent('error', error);
93
- this.disconnect();
94
+ 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._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();
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.reconnectRetries);
136
+ this.#emitEvent('reconnecting', this.#reconnectRetries);
105
137
  });
106
138
  // 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');
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._mqttClient.on('message', (topic, message) => {
113
- if (!this._sessionDetails) {
114
- this.logger('warn', 'Received message when session not connected');
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._sessionDetails);
150
+ this.#handleCommand(topic, message, this.#sessionDetails);
118
151
  });
119
152
  // Subscribe to all context groups
120
- this._mqttClient.subscribe(`${sessionRootTopic}/context-groups/#`);
153
+ this.#mqttClient.subscribe(`${sessionRootTopic}/context-groups/#`);
121
154
  // Listen out for global commands
122
- this._mqttClient.subscribe(`${sessionRootTopic}/commands`);
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
- 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
- }
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._sessionDetails || !this.connectionParams) {
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.cloudInteropSettings.url}/api/context-groups/${this._sessionDetails.sessionId}/${contextGroup}`, payload, {
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.eventListeners.get(type) || [];
198
+ const listeners = this.#eventListeners.get(type) || [];
188
199
  listeners.push(callback);
189
- this.eventListeners.set(type, listeners);
200
+ this.#eventListeners.set(type, listeners);
190
201
  }
191
202
  removeEventListener(type, callback) {
192
- const listeners = this.eventListeners.get(type) || [];
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.eventListeners.set(type, listeners);
208
+ this.#eventListeners.set(type, listeners);
209
+ }
210
+ async #disconnect(fireDisconnectedEvent) {
211
+ if (!this.#sessionDetails) {
212
+ return;
213
+ }
214
+ try {
215
+ const disconnectResponse = await axios.delete(`${this.#cloudInteropSettings.url}/api/sessions/${this.#sessionDetails.sessionId}`, {
216
+ headers: this.#getRequestHeaders(),
217
+ });
218
+ if (disconnectResponse.status !== 200) {
219
+ throw new CloudInteropAPIError('Error during session tear down - unexpected status', 'ERR_DISCONNECT', disconnectResponse.status);
220
+ }
221
+ }
222
+ catch {
223
+ throw new CloudInteropAPIError('Error during disconnection', 'ERR_DISCONNECT');
224
+ }
225
+ finally {
226
+ this.#mqttClient?.removeAllListeners();
227
+ await this.#mqttClient?.endAsync(true);
228
+ this.#sessionDetails = undefined;
229
+ this.#mqttClient = undefined;
230
+ this.#reconnectRetries = 0;
231
+ this.#attemptingToReconnect = false;
232
+ if (fireDisconnectedEvent) {
233
+ this.#emitEvent('disconnected');
234
+ }
235
+ }
198
236
  }
199
237
  #handleCommand(topic, message, sessionDetails) {
200
238
  if (message.length === 0 || !sessionDetails) {
@@ -206,12 +244,12 @@ class CloudInteropAPI {
206
244
  if (messageEnvelope.source.sessionId === sessionDetails.sessionId) {
207
245
  return;
208
246
  }
209
- const { channelName: contextGroup, payload: context, source } = messageEnvelope;
210
- this.#emitEvent('context', { contextGroup, context, source });
247
+ const { channelName: contextGroup, payload: context, source, history } = messageEnvelope;
248
+ this.#emitEvent('context', { contextGroup, context, source, history: { ...history, clientReceived: Date.now() } });
211
249
  }
212
250
  }
213
251
  #emitEvent(type, ...args) {
214
- const listeners = this.eventListeners.get(type) || [];
252
+ const listeners = this.#eventListeners.get(type) || [];
215
253
  listeners.forEach((listener) => listener(...args));
216
254
  }
217
255
  #validateConnectParams = (parameters) => {
@@ -228,22 +266,22 @@ class CloudInteropAPI {
228
266
  }
229
267
  };
230
268
  #getRequestHeaders = () => {
231
- if (!this.connectionParams) {
269
+ if (!this.#connectionParams) {
232
270
  throw new Error('Connect parameters must be provided');
233
271
  }
234
272
  const headers = new axios.AxiosHeaders();
235
273
  headers['Content-Type'] = 'application/json';
236
- if (this.connectionParams.authenticationType === 'jwt' && this.connectionParams.jwtAuthenticationParameters) {
237
- const tokenResult = this.connectionParams.jwtAuthenticationParameters.jwtRequestCallback();
274
+ if (this.#connectionParams.authenticationType === 'jwt' && this.#connectionParams.jwtAuthenticationParameters) {
275
+ const tokenResult = this.#connectionParams.jwtAuthenticationParameters.jwtRequestCallback();
238
276
  if (!tokenResult) {
239
277
  throw new Error('jwtRequestCallback must return a token');
240
278
  }
241
- headers['x-of-auth-id'] = this.connectionParams.jwtAuthenticationParameters.authenticationId;
279
+ headers['x-of-auth-id'] = this.#connectionParams.jwtAuthenticationParameters.authenticationId;
242
280
  headers['Authorization'] =
243
281
  typeof tokenResult === 'string' ? `Bearer ${tokenResult}` : `Bearer ${Buffer.from(JSON.stringify(tokenResult)).toString('base64')}`;
244
282
  }
245
- if (this.connectionParams.authenticationType === 'basic' && this.connectionParams.basicAuthenticationParameters) {
246
- const { username, password } = this.connectionParams.basicAuthenticationParameters;
283
+ if (this.#connectionParams.authenticationType === 'basic' && this.#connectionParams.basicAuthenticationParameters) {
284
+ const { username, password } = this.#connectionParams.basicAuthenticationParameters;
247
285
  headers['Authorization'] = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
248
286
  }
249
287
  return headers;
package/dist/index.mjs CHANGED
@@ -3781,6 +3781,8 @@ class AuthorizationError extends CloudInteropAPIError {
3781
3781
  }
3782
3782
  }
3783
3783
 
3784
+ // Error codes as defined in https://docs.emqx.com/en/cloud/latest/connect_to_deployments/mqtt_client_error_codes.html
3785
+ const BadUserNamePasswordError = 134;
3784
3786
  /**
3785
3787
  * Represents a single connection to a Cloud Interop service
3786
3788
  *
@@ -3789,24 +3791,26 @@ class AuthorizationError extends CloudInteropAPIError {
3789
3791
  * @implements {Client}
3790
3792
  */
3791
3793
  class CloudInteropAPI {
3792
- cloudInteropSettings;
3793
- _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,100 @@ 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,
3849
3855
  },
3850
- username: this._sessionDetails.token,
3856
+ username: this.#sessionDetails.token,
3851
3857
  };
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();
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._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();
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.reconnectRetries);
3900
+ this.#emitEvent('reconnecting', this.#reconnectRetries);
3869
3901
  });
3870
3902
  // 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');
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._mqttClient.on('message', (topic, message) => {
3877
- if (!this._sessionDetails) {
3878
- this.logger('warn', 'Received message when session not connected');
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._sessionDetails);
3914
+ this.#handleCommand(topic, message, this.#sessionDetails);
3882
3915
  });
3883
3916
  // Subscribe to all context groups
3884
- this._mqttClient.subscribe(`${sessionRootTopic}/context-groups/#`);
3917
+ this.#mqttClient.subscribe(`${sessionRootTopic}/context-groups/#`);
3885
3918
  // Listen out for global commands
3886
- this._mqttClient.subscribe(`${sessionRootTopic}/commands`);
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
- 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
- }
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._sessionDetails || !this.connectionParams) {
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.cloudInteropSettings.url}/api/context-groups/${this._sessionDetails.sessionId}/${contextGroup}`, payload, {
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.eventListeners.get(type) || [];
3962
+ const listeners = this.#eventListeners.get(type) || [];
3952
3963
  listeners.push(callback);
3953
- this.eventListeners.set(type, listeners);
3964
+ this.#eventListeners.set(type, listeners);
3954
3965
  }
3955
3966
  removeEventListener(type, callback) {
3956
- const listeners = this.eventListeners.get(type) || [];
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.eventListeners.set(type, listeners);
3972
+ this.#eventListeners.set(type, listeners);
3973
+ }
3974
+ async #disconnect(fireDisconnectedEvent) {
3975
+ if (!this.#sessionDetails) {
3976
+ return;
3977
+ }
3978
+ try {
3979
+ const disconnectResponse = await axios.delete(`${this.#cloudInteropSettings.url}/api/sessions/${this.#sessionDetails.sessionId}`, {
3980
+ headers: this.#getRequestHeaders(),
3981
+ });
3982
+ if (disconnectResponse.status !== 200) {
3983
+ throw new CloudInteropAPIError('Error during session tear down - unexpected status', 'ERR_DISCONNECT', disconnectResponse.status);
3984
+ }
3985
+ }
3986
+ catch {
3987
+ throw new CloudInteropAPIError('Error during disconnection', 'ERR_DISCONNECT');
3988
+ }
3989
+ finally {
3990
+ this.#mqttClient?.removeAllListeners();
3991
+ await this.#mqttClient?.endAsync(true);
3992
+ this.#sessionDetails = undefined;
3993
+ this.#mqttClient = undefined;
3994
+ this.#reconnectRetries = 0;
3995
+ this.#attemptingToReconnect = false;
3996
+ if (fireDisconnectedEvent) {
3997
+ this.#emitEvent('disconnected');
3998
+ }
3999
+ }
3962
4000
  }
3963
4001
  #handleCommand(topic, message, sessionDetails) {
3964
4002
  if (message.length === 0 || !sessionDetails) {
@@ -3970,12 +4008,12 @@ class CloudInteropAPI {
3970
4008
  if (messageEnvelope.source.sessionId === sessionDetails.sessionId) {
3971
4009
  return;
3972
4010
  }
3973
- const { channelName: contextGroup, payload: context, source } = messageEnvelope;
3974
- this.#emitEvent('context', { contextGroup, context, source });
4011
+ const { channelName: contextGroup, payload: context, source, history } = messageEnvelope;
4012
+ this.#emitEvent('context', { contextGroup, context, source, history: { ...history, clientReceived: Date.now() } });
3975
4013
  }
3976
4014
  }
3977
4015
  #emitEvent(type, ...args) {
3978
- const listeners = this.eventListeners.get(type) || [];
4016
+ const listeners = this.#eventListeners.get(type) || [];
3979
4017
  listeners.forEach((listener) => listener(...args));
3980
4018
  }
3981
4019
  #validateConnectParams = (parameters) => {
@@ -3992,22 +4030,22 @@ class CloudInteropAPI {
3992
4030
  }
3993
4031
  };
3994
4032
  #getRequestHeaders = () => {
3995
- if (!this.connectionParams) {
4033
+ if (!this.#connectionParams) {
3996
4034
  throw new Error('Connect parameters must be provided');
3997
4035
  }
3998
4036
  const headers = new AxiosHeaders();
3999
4037
  headers['Content-Type'] = 'application/json';
4000
- if (this.connectionParams.authenticationType === 'jwt' && this.connectionParams.jwtAuthenticationParameters) {
4001
- const tokenResult = this.connectionParams.jwtAuthenticationParameters.jwtRequestCallback();
4038
+ if (this.#connectionParams.authenticationType === 'jwt' && this.#connectionParams.jwtAuthenticationParameters) {
4039
+ const tokenResult = this.#connectionParams.jwtAuthenticationParameters.jwtRequestCallback();
4002
4040
  if (!tokenResult) {
4003
4041
  throw new Error('jwtRequestCallback must return a token');
4004
4042
  }
4005
- headers['x-of-auth-id'] = this.connectionParams.jwtAuthenticationParameters.authenticationId;
4043
+ headers['x-of-auth-id'] = this.#connectionParams.jwtAuthenticationParameters.authenticationId;
4006
4044
  headers['Authorization'] =
4007
4045
  typeof tokenResult === 'string' ? `Bearer ${tokenResult}` : `Bearer ${Buffer.from(JSON.stringify(tokenResult)).toString('base64')}`;
4008
4046
  }
4009
- if (this.connectionParams.authenticationType === 'basic' && this.connectionParams.basicAuthenticationParameters) {
4010
- const { username, password } = this.connectionParams.basicAuthenticationParameters;
4047
+ if (this.#connectionParams.authenticationType === 'basic' && this.#connectionParams.basicAuthenticationParameters) {
4048
+ const { username, password } = this.#connectionParams.basicAuthenticationParameters;
4011
4049
  headers['Authorization'] = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
4012
4050
  }
4013
4051
  return headers;
@@ -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.ffc0fe6",
4
4
  "type": "module",
5
5
  "description": "",
6
6
  "files": [