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

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