genesys-cloud-streaming-client 15.1.0 → 15.1.2

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.
@@ -22,6 +22,7 @@ export declare class Client extends EventEmitter {
22
22
  private autoReconnect;
23
23
  private extensions;
24
24
  private connectionManager;
25
+ private channelReuses;
25
26
  http: HttpClient;
26
27
  notifications: NotificationsAPI;
27
28
  _notifications: Notifications;
@@ -22,6 +22,7 @@ let extensions = {
22
22
  };
23
23
  const STANZA_DISCONNECTED = 'stanzaDisconnected';
24
24
  const NO_LONGER_SUBSCRIBED = 'notify:no_longer_subscribed';
25
+ const MAX_CHANNEL_REUSES = 10;
25
26
  class Client extends events_1.default {
26
27
  constructor(options) {
27
28
  super();
@@ -32,6 +33,7 @@ class Client extends events_1.default {
32
33
  this.backgroundAssistantMode = false;
33
34
  this.autoReconnect = true;
34
35
  this.extensions = [];
36
+ this.channelReuses = 0;
35
37
  this.http = new http_client_1.HttpClient();
36
38
  this.reconnectOnNoLongerSubscribed = options.reconnectOnNoLongerSubscribed !== false;
37
39
  this.config = {
@@ -179,20 +181,27 @@ class Client extends events_1.default {
179
181
  this.activeStanzaInstance.disconnect();
180
182
  }, 5000, 'disconnecting streaming service');
181
183
  }
182
- async connect(connectOpts = { keepTryingOnFailure: false }) {
184
+ async connect(connectOpts) {
183
185
  var _a;
184
186
  if (this.connecting) {
185
187
  const error = new Error('Already trying to connect streaming client');
186
188
  return this.logger.warn(error);
187
189
  }
188
190
  this.connecting = true;
191
+ const maxDelay = (connectOpts === null || connectOpts === void 0 ? void 0 : connectOpts.maxDelayBetweenConnectionAttempts) || 90000;
192
+ let maxAttempts = (connectOpts === null || connectOpts === void 0 ? void 0 : connectOpts.maxConnectionAttempts) || 1;
193
+ // tslint:disable-next-line
194
+ if (connectOpts === null || connectOpts === void 0 ? void 0 : connectOpts.keepTryingOnFailure) {
195
+ // this maintains the previous functionality
196
+ maxAttempts = Infinity;
197
+ }
189
198
  try {
190
199
  await exponential_backoff_1.backOff(() => this.makeConnectionAttempt(), {
191
200
  jitter: 'full',
192
- maxDelay: 10000,
193
- numOfAttempts: connectOpts.keepTryingOnFailure ? Infinity : 1,
201
+ maxDelay,
202
+ numOfAttempts: maxAttempts,
194
203
  startingDelay: 2000,
195
- retry: this.backoffConnectRetryHandler.bind(this, connectOpts)
204
+ retry: this.backoffConnectRetryHandler.bind(this, { maxConnectionAttempts: maxAttempts })
196
205
  });
197
206
  }
198
207
  catch (err) {
@@ -222,11 +231,11 @@ class Client extends events_1.default {
222
231
  throw err;
223
232
  }
224
233
  }
225
- backoffConnectRetryHandler(connectOpts, err, connectionAttempt) {
226
- var _a, _b;
234
+ async backoffConnectRetryHandler(connectOpts, err, connectionAttempt) {
235
+ var _a, _b, _c, _d, _e;
227
236
  // if we exceed the `numOfAttempts` in the backoff config it still calls this retry fn and just ignores the result
228
237
  // if that's the case, we just want to bail out and ignore all the extra logging here.
229
- if (!connectOpts.keepTryingOnFailure) {
238
+ if (connectionAttempt >= connectOpts.maxConnectionAttempts) {
230
239
  return false;
231
240
  }
232
241
  const additionalErrorDetails = { connectionAttempt, error: err };
@@ -267,6 +276,22 @@ class Client extends events_1.default {
267
276
  additionalErrorDetails.details = details;
268
277
  }
269
278
  }
279
+ if (err === null || err === void 0 ? void 0 : err.response) {
280
+ // This *should* be an axios error according to typings, but it appears this could be an AxiosError *or* and XmlHttpRequest
281
+ // we'll check both to be safe
282
+ const retryAfter = ((_c = err.response.headers) === null || _c === void 0 ? void 0 : _c['retry-after']) || ((_e = (_d = err.response).getResponseHeader) === null || _e === void 0 ? void 0 : _e.call(_d, 'retry-after'));
283
+ if (retryAfter) {
284
+ // retry after comes in seconds, we need to return milliseconds
285
+ let retryDelay = parseInt(retryAfter, 10) * 1000;
286
+ additionalErrorDetails.retryDelay = retryDelay;
287
+ this.logger.error('Failed streaming client connection attempt, respecting retry-after header and will retry afterwards.', additionalErrorDetails, { skipServer: err instanceof offline_error_1.default });
288
+ await new Promise((resolve) => {
289
+ setTimeout(resolve, retryDelay);
290
+ });
291
+ this.logger.debug('finished waiting for retry-after');
292
+ return true;
293
+ }
294
+ }
270
295
  this.logger.error('Failed streaming client connection attempt, retrying', additionalErrorDetails, { skipServer: err instanceof offline_error_1.default });
271
296
  return true;
272
297
  }
@@ -274,28 +299,54 @@ class Client extends events_1.default {
274
299
  if (!navigator.onLine) {
275
300
  throw new offline_error_1.default('Browser is offline, skipping connection attempt');
276
301
  }
277
- await this.prepareForConnect();
278
- const stanzaInstance = await this.connectionManager.getNewStanzaConnection();
279
- this.connected = true;
280
- this.connecting = false;
281
- this.addInateEventHandlers(stanzaInstance);
282
- this.proxyStanzaEvents(stanzaInstance);
283
- stanzaInstance.pinger = new ping_1.Ping(this, stanzaInstance);
284
- // handle any extension configuration
285
- for (const extension of this.extensions) {
286
- if (extension.configureNewStanzaInstance) {
287
- await extension.configureNewStanzaInstance(stanzaInstance);
302
+ let stanzaInstance;
303
+ let previousConnectingState = this.connecting;
304
+ try {
305
+ await this.prepareForConnect();
306
+ stanzaInstance = await this.connectionManager.getNewStanzaConnection();
307
+ this.connected = true;
308
+ this.connecting = false;
309
+ this.addInateEventHandlers(stanzaInstance);
310
+ this.proxyStanzaEvents(stanzaInstance);
311
+ stanzaInstance.pinger = new ping_1.Ping(this, stanzaInstance);
312
+ // handle any extension configuration
313
+ for (const extension of this.extensions) {
314
+ if (extension.configureNewStanzaInstance) {
315
+ await extension.configureNewStanzaInstance(stanzaInstance);
316
+ }
317
+ }
318
+ for (const extension of this.extensions) {
319
+ extension.handleStanzaInstanceChange(stanzaInstance);
288
320
  }
321
+ this.activeStanzaInstance = stanzaInstance;
322
+ this.emit('connected');
323
+ }
324
+ catch (err) {
325
+ if (stanzaInstance) {
326
+ this.logger.error('Error occurred in connection attempt, but after websocket connected. Cleaning up connection so backoff is respected', { stanzaInstanceId: stanzaInstance.id, channelId: stanzaInstance.channelId });
327
+ // unproxy stanza events so we don't try and reconnect
328
+ stanzaInstance.emit = stanzaInstance.originalEmitter;
329
+ stanzaInstance.pinger.stop();
330
+ stanzaInstance.disconnect();
331
+ this.connected = false;
332
+ this.connecting = previousConnectingState;
333
+ }
334
+ throw err;
289
335
  }
290
- this.extensions.forEach(extension => extension.handleStanzaInstanceChange(stanzaInstance));
291
- this.activeStanzaInstance = stanzaInstance;
292
- this.emit('connected');
293
336
  }
294
337
  async prepareForConnect() {
295
338
  if (this.config.jwt) {
296
339
  this.hardReconnectRequired = false;
297
340
  return this.connectionManager.setConfig(this.config);
298
341
  }
342
+ if (!this.hardReconnectRequired) {
343
+ this.channelReuses++;
344
+ if (this.channelReuses > MAX_CHANNEL_REUSES) {
345
+ this.logger.warn('Forcing a hard reconnect due to max channel reuses', { channelId: this.config.channelId, channelReuses: this.channelReuses });
346
+ this.channelReuses = 0;
347
+ this.hardReconnectRequired = true;
348
+ }
349
+ }
299
350
  if (this.hardReconnectRequired) {
300
351
  let jidPromise;
301
352
  if (this.config.jid) {
@@ -308,7 +359,7 @@ class Client extends events_1.default {
308
359
  authToken: this.config.authToken,
309
360
  logger: this.logger
310
361
  };
311
- jidPromise = this.http.requestApiWithRetry('users/me', jidRequestOpts).promise
362
+ jidPromise = this.http.requestApi('users/me', jidRequestOpts)
312
363
  .then(res => res.data.chat.jabberId);
313
364
  }
314
365
  const channelRequestOpts = {
@@ -317,7 +368,7 @@ class Client extends events_1.default {
317
368
  authToken: this.config.authToken,
318
369
  logger: this.logger
319
370
  };
320
- const channelPromise = this.http.requestApiWithRetry('notifications/channels?connectionType=streaming', channelRequestOpts).promise
371
+ const channelPromise = this.http.requestApi('notifications/channels?connectionType=streaming', channelRequestOpts)
321
372
  .then(res => res.data.id);
322
373
  const [jid, channelId] = await Promise.all([jidPromise, channelPromise]);
323
374
  this.config.jid = jid;
@@ -351,7 +402,7 @@ class Client extends events_1.default {
351
402
  return Client.version;
352
403
  }
353
404
  static get version() {
354
- return '15.1.0';
405
+ return '15.1.2';
355
406
  }
356
407
  }
357
408
  exports.Client = Client;
@@ -11,12 +11,15 @@ class HttpClient {
11
11
  }
12
12
  requestApiWithRetry(path, opts, retryInterval) {
13
13
  const retry = utils_1.retryPromise(this.requestApi.bind(this, path, opts), (error) => {
14
- var _a;
14
+ var _a, _b, _c;
15
15
  let retryValue = false;
16
16
  if (error === null || error === void 0 ? void 0 : error.response) {
17
17
  retryValue = HttpClient.retryStatusCodes.has(error.response.status || 0);
18
- const retryAfter = (_a = error.response.headers) === null || _a === void 0 ? void 0 : _a['retry-after'];
18
+ // This *should* be an axios error according to typings, but it appears this could be an AxiosError *or* and XmlHttpRequest
19
+ // we'll check both to be safe
20
+ const retryAfter = ((_a = error.response.headers) === null || _a === void 0 ? void 0 : _a['retry-after']) || ((_c = (_b = error.response).getResponseHeader) === null || _c === void 0 ? void 0 : _c.call(_b, 'retry-after'));
19
21
  if (retryAfter) {
22
+ (opts.logger || console).debug('retry-after header found on response. setting retry delay', { retryAfter });
20
23
  // retry after comes in seconds, we need to return milliseconds
21
24
  retryValue = parseInt(retryAfter, 10) * 1000;
22
25
  }
@@ -51,6 +54,7 @@ class HttpClient {
51
54
  .then(boundHandler, boundHandler);
52
55
  }
53
56
  handleResponse(logger, start, params, res) {
57
+ var _a;
54
58
  let now = new Date().getTime();
55
59
  let elapsed = (now - start) + 'ms';
56
60
  if (res instanceof axios_1.AxiosError) {
@@ -70,7 +74,7 @@ class HttpClient {
70
74
  let body = response.data;
71
75
  let error = {
72
76
  ...res,
73
- text: response.request.response
77
+ text: (_a = response.request) === null || _a === void 0 ? void 0 : _a.response
74
78
  };
75
79
  logger.debug(`request error: ${params.url}`, {
76
80
  message: res.message,
package/dist/cjs/ping.js CHANGED
@@ -33,7 +33,8 @@ class Ping {
33
33
  catch (err) {
34
34
  const info = {
35
35
  channelId: this.client.config.channelId,
36
- jid: this.stanzaInstance.jid
36
+ jid: this.stanzaInstance.jid,
37
+ stanzaInstanceId: this.stanzaInstance.id
37
38
  };
38
39
  this.client.logger.warn('Missed a ping.', Object.assign({ error: err }, info));
39
40
  /* if we have reached max number of missed pings, disconnect */
@@ -138,7 +138,12 @@ export interface StreamingClientExtension {
138
138
  expose: any;
139
139
  }
140
140
  export interface StreamingClientConnectOptions {
141
- keepTryingOnFailure: boolean;
141
+ /**
142
+ * @deprecated since version 15.1.1. Please use maxConnectionAttempts instead
143
+ */
144
+ keepTryingOnFailure?: boolean;
145
+ maxConnectionAttempts?: number;
146
+ maxDelayBetweenConnectionAttempts?: number;
142
147
  }
143
148
  export interface GenesysWebrtcJsonRpcMessage extends JsonRpcMessage {
144
149
  id?: string;
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "developercenter-cdn/streaming-client",
3
- "version": "15.1.0",
3
+ "version": "15.1.2",
4
4
  "ecosystem": "pc",
5
5
  "team": "Genesys Client Media (WebRTC)",
6
6
  "indexFiles": [
7
7
  {
8
- "file": "/v15.1.0/streaming-client.browser.ie.js"
8
+ "file": "/v15.1.2/streaming-client.browser.ie.js"
9
9
  },
10
10
  {
11
- "file": "/v15.1.0/streaming-client.browser.js"
11
+ "file": "/v15.1.2/streaming-client.browser.js"
12
12
  },
13
13
  {
14
14
  "file": "/v15/streaming-client.browser.ie.js"
@@ -17,6 +17,6 @@
17
17
  "file": "/v15/streaming-client.browser.js"
18
18
  }
19
19
  ],
20
- "build": "54",
21
- "buildDate": "2023-02-22T18:01:39.328019Z"
20
+ "build": "57",
21
+ "buildDate": "2023-03-02T21:43:00.982202Z"
22
22
  }
@@ -22,6 +22,7 @@ export declare class Client extends EventEmitter {
22
22
  private autoReconnect;
23
23
  private extensions;
24
24
  private connectionManager;
25
+ private channelReuses;
25
26
  http: HttpClient;
26
27
  notifications: NotificationsAPI;
27
28
  _notifications: Notifications;
package/dist/es/client.js CHANGED
@@ -20,6 +20,7 @@ let extensions = {
20
20
  };
21
21
  const STANZA_DISCONNECTED = 'stanzaDisconnected';
22
22
  const NO_LONGER_SUBSCRIBED = 'notify:no_longer_subscribed';
23
+ const MAX_CHANNEL_REUSES = 10;
23
24
  export class Client extends EventEmitter {
24
25
  constructor(options) {
25
26
  super();
@@ -30,6 +31,7 @@ export class Client extends EventEmitter {
30
31
  this.backgroundAssistantMode = false;
31
32
  this.autoReconnect = true;
32
33
  this.extensions = [];
34
+ this.channelReuses = 0;
33
35
  this.http = new HttpClient();
34
36
  this.reconnectOnNoLongerSubscribed = options.reconnectOnNoLongerSubscribed !== false;
35
37
  this.config = {
@@ -181,7 +183,7 @@ export class Client extends EventEmitter {
181
183
  }, 5000, 'disconnecting streaming service');
182
184
  });
183
185
  }
184
- connect(connectOpts = { keepTryingOnFailure: false }) {
186
+ connect(connectOpts) {
185
187
  var _a;
186
188
  return __awaiter(this, void 0, void 0, function* () {
187
189
  if (this.connecting) {
@@ -189,13 +191,20 @@ export class Client extends EventEmitter {
189
191
  return this.logger.warn(error);
190
192
  }
191
193
  this.connecting = true;
194
+ const maxDelay = (connectOpts === null || connectOpts === void 0 ? void 0 : connectOpts.maxDelayBetweenConnectionAttempts) || 90000;
195
+ let maxAttempts = (connectOpts === null || connectOpts === void 0 ? void 0 : connectOpts.maxConnectionAttempts) || 1;
196
+ // tslint:disable-next-line
197
+ if (connectOpts === null || connectOpts === void 0 ? void 0 : connectOpts.keepTryingOnFailure) {
198
+ // this maintains the previous functionality
199
+ maxAttempts = Infinity;
200
+ }
192
201
  try {
193
202
  yield backOff(() => this.makeConnectionAttempt(), {
194
203
  jitter: 'full',
195
- maxDelay: 10000,
196
- numOfAttempts: connectOpts.keepTryingOnFailure ? Infinity : 1,
204
+ maxDelay,
205
+ numOfAttempts: maxAttempts,
197
206
  startingDelay: 2000,
198
- retry: this.backoffConnectRetryHandler.bind(this, connectOpts)
207
+ retry: this.backoffConnectRetryHandler.bind(this, { maxConnectionAttempts: maxAttempts })
199
208
  });
200
209
  }
201
210
  catch (err) {
@@ -227,74 +236,110 @@ export class Client extends EventEmitter {
227
236
  });
228
237
  }
229
238
  backoffConnectRetryHandler(connectOpts, err, connectionAttempt) {
230
- var _a, _b;
231
- // if we exceed the `numOfAttempts` in the backoff config it still calls this retry fn and just ignores the result
232
- // if that's the case, we just want to bail out and ignore all the extra logging here.
233
- if (!connectOpts.keepTryingOnFailure) {
234
- return false;
235
- }
236
- const additionalErrorDetails = { connectionAttempt, error: err };
237
- if (!err) {
238
- additionalErrorDetails.error = new Error('streaming client backoff handler received undefined error');
239
- }
240
- else if (err.name === 'AxiosError') {
241
- const axiosError = err;
242
- const config = axiosError.config;
243
- let sanitizedError = {
244
- config: {
245
- url: config.url,
246
- method: config.method
247
- },
248
- status: (_a = axiosError.response) === null || _a === void 0 ? void 0 : _a.status,
249
- code: axiosError.code,
250
- name: axiosError.name,
251
- message: axiosError.message
252
- };
253
- additionalErrorDetails.error = sanitizedError;
254
- if ([401, 403].includes(((_b = err.response) === null || _b === void 0 ? void 0 : _b.status) || 0)) {
255
- this.logger.error('Streaming client received an error that it can\'t recover from and will not attempt to reconnect', additionalErrorDetails);
239
+ var _a, _b, _c, _d, _e;
240
+ return __awaiter(this, void 0, void 0, function* () {
241
+ // if we exceed the `numOfAttempts` in the backoff config it still calls this retry fn and just ignores the result
242
+ // if that's the case, we just want to bail out and ignore all the extra logging here.
243
+ if (connectionAttempt >= connectOpts.maxConnectionAttempts) {
256
244
  return false;
257
245
  }
258
- }
259
- // if we get a sasl error, that means we made it all the way to the point of trying to open a websocket and
260
- // it was rejected for some reason. At this point we should do a hard reconnect then try again.
261
- if (err instanceof SaslError) {
262
- this.logger.info('hardReconnectRequired set to true due to sasl error');
263
- this.hardReconnectRequired = true;
264
- Object.assign(additionalErrorDetails, { channelId: err.channelId, stanzaInstanceId: err.stanzaInstanceId });
265
- }
266
- // we don't need to log the stack for a timeout message
267
- if (err instanceof TimeoutError) {
268
- additionalErrorDetails.error = err.message;
269
- const details = err.details;
270
- if (details) {
271
- additionalErrorDetails.details = details;
246
+ const additionalErrorDetails = { connectionAttempt, error: err };
247
+ if (!err) {
248
+ additionalErrorDetails.error = new Error('streaming client backoff handler received undefined error');
272
249
  }
273
- }
274
- this.logger.error('Failed streaming client connection attempt, retrying', additionalErrorDetails, { skipServer: err instanceof OfflineError });
275
- return true;
250
+ else if (err.name === 'AxiosError') {
251
+ const axiosError = err;
252
+ const config = axiosError.config;
253
+ let sanitizedError = {
254
+ config: {
255
+ url: config.url,
256
+ method: config.method
257
+ },
258
+ status: (_a = axiosError.response) === null || _a === void 0 ? void 0 : _a.status,
259
+ code: axiosError.code,
260
+ name: axiosError.name,
261
+ message: axiosError.message
262
+ };
263
+ additionalErrorDetails.error = sanitizedError;
264
+ if ([401, 403].includes(((_b = err.response) === null || _b === void 0 ? void 0 : _b.status) || 0)) {
265
+ this.logger.error('Streaming client received an error that it can\'t recover from and will not attempt to reconnect', additionalErrorDetails);
266
+ return false;
267
+ }
268
+ }
269
+ // if we get a sasl error, that means we made it all the way to the point of trying to open a websocket and
270
+ // it was rejected for some reason. At this point we should do a hard reconnect then try again.
271
+ if (err instanceof SaslError) {
272
+ this.logger.info('hardReconnectRequired set to true due to sasl error');
273
+ this.hardReconnectRequired = true;
274
+ Object.assign(additionalErrorDetails, { channelId: err.channelId, stanzaInstanceId: err.stanzaInstanceId });
275
+ }
276
+ // we don't need to log the stack for a timeout message
277
+ if (err instanceof TimeoutError) {
278
+ additionalErrorDetails.error = err.message;
279
+ const details = err.details;
280
+ if (details) {
281
+ additionalErrorDetails.details = details;
282
+ }
283
+ }
284
+ if (err === null || err === void 0 ? void 0 : err.response) {
285
+ // This *should* be an axios error according to typings, but it appears this could be an AxiosError *or* and XmlHttpRequest
286
+ // we'll check both to be safe
287
+ const retryAfter = ((_c = err.response.headers) === null || _c === void 0 ? void 0 : _c['retry-after']) || ((_e = (_d = err.response).getResponseHeader) === null || _e === void 0 ? void 0 : _e.call(_d, 'retry-after'));
288
+ if (retryAfter) {
289
+ // retry after comes in seconds, we need to return milliseconds
290
+ let retryDelay = parseInt(retryAfter, 10) * 1000;
291
+ additionalErrorDetails.retryDelay = retryDelay;
292
+ this.logger.error('Failed streaming client connection attempt, respecting retry-after header and will retry afterwards.', additionalErrorDetails, { skipServer: err instanceof OfflineError });
293
+ yield new Promise((resolve) => {
294
+ setTimeout(resolve, retryDelay);
295
+ });
296
+ this.logger.debug('finished waiting for retry-after');
297
+ return true;
298
+ }
299
+ }
300
+ this.logger.error('Failed streaming client connection attempt, retrying', additionalErrorDetails, { skipServer: err instanceof OfflineError });
301
+ return true;
302
+ });
276
303
  }
277
304
  makeConnectionAttempt() {
278
305
  return __awaiter(this, void 0, void 0, function* () {
279
306
  if (!navigator.onLine) {
280
307
  throw new OfflineError('Browser is offline, skipping connection attempt');
281
308
  }
282
- yield this.prepareForConnect();
283
- const stanzaInstance = yield this.connectionManager.getNewStanzaConnection();
284
- this.connected = true;
285
- this.connecting = false;
286
- this.addInateEventHandlers(stanzaInstance);
287
- this.proxyStanzaEvents(stanzaInstance);
288
- stanzaInstance.pinger = new Ping(this, stanzaInstance);
289
- // handle any extension configuration
290
- for (const extension of this.extensions) {
291
- if (extension.configureNewStanzaInstance) {
292
- yield extension.configureNewStanzaInstance(stanzaInstance);
309
+ let stanzaInstance;
310
+ let previousConnectingState = this.connecting;
311
+ try {
312
+ yield this.prepareForConnect();
313
+ stanzaInstance = yield this.connectionManager.getNewStanzaConnection();
314
+ this.connected = true;
315
+ this.connecting = false;
316
+ this.addInateEventHandlers(stanzaInstance);
317
+ this.proxyStanzaEvents(stanzaInstance);
318
+ stanzaInstance.pinger = new Ping(this, stanzaInstance);
319
+ // handle any extension configuration
320
+ for (const extension of this.extensions) {
321
+ if (extension.configureNewStanzaInstance) {
322
+ yield extension.configureNewStanzaInstance(stanzaInstance);
323
+ }
324
+ }
325
+ for (const extension of this.extensions) {
326
+ extension.handleStanzaInstanceChange(stanzaInstance);
293
327
  }
328
+ this.activeStanzaInstance = stanzaInstance;
329
+ this.emit('connected');
330
+ }
331
+ catch (err) {
332
+ if (stanzaInstance) {
333
+ this.logger.error('Error occurred in connection attempt, but after websocket connected. Cleaning up connection so backoff is respected', { stanzaInstanceId: stanzaInstance.id, channelId: stanzaInstance.channelId });
334
+ // unproxy stanza events so we don't try and reconnect
335
+ stanzaInstance.emit = stanzaInstance.originalEmitter;
336
+ stanzaInstance.pinger.stop();
337
+ stanzaInstance.disconnect();
338
+ this.connected = false;
339
+ this.connecting = previousConnectingState;
340
+ }
341
+ throw err;
294
342
  }
295
- this.extensions.forEach(extension => extension.handleStanzaInstanceChange(stanzaInstance));
296
- this.activeStanzaInstance = stanzaInstance;
297
- this.emit('connected');
298
343
  });
299
344
  }
300
345
  prepareForConnect() {
@@ -303,6 +348,14 @@ export class Client extends EventEmitter {
303
348
  this.hardReconnectRequired = false;
304
349
  return this.connectionManager.setConfig(this.config);
305
350
  }
351
+ if (!this.hardReconnectRequired) {
352
+ this.channelReuses++;
353
+ if (this.channelReuses > MAX_CHANNEL_REUSES) {
354
+ this.logger.warn('Forcing a hard reconnect due to max channel reuses', { channelId: this.config.channelId, channelReuses: this.channelReuses });
355
+ this.channelReuses = 0;
356
+ this.hardReconnectRequired = true;
357
+ }
358
+ }
306
359
  if (this.hardReconnectRequired) {
307
360
  let jidPromise;
308
361
  if (this.config.jid) {
@@ -315,7 +368,7 @@ export class Client extends EventEmitter {
315
368
  authToken: this.config.authToken,
316
369
  logger: this.logger
317
370
  };
318
- jidPromise = this.http.requestApiWithRetry('users/me', jidRequestOpts).promise
371
+ jidPromise = this.http.requestApi('users/me', jidRequestOpts)
319
372
  .then(res => res.data.chat.jabberId);
320
373
  }
321
374
  const channelRequestOpts = {
@@ -324,7 +377,7 @@ export class Client extends EventEmitter {
324
377
  authToken: this.config.authToken,
325
378
  logger: this.logger
326
379
  };
327
- const channelPromise = this.http.requestApiWithRetry('notifications/channels?connectionType=streaming', channelRequestOpts).promise
380
+ const channelPromise = this.http.requestApi('notifications/channels?connectionType=streaming', channelRequestOpts)
328
381
  .then(res => res.data.id);
329
382
  const [jid, channelId] = yield Promise.all([jidPromise, channelPromise]);
330
383
  this.config.jid = jid;
@@ -359,6 +412,6 @@ export class Client extends EventEmitter {
359
412
  return Client.version;
360
413
  }
361
414
  static get version() {
362
- return '15.1.0';
415
+ return '15.1.2';
363
416
  }
364
417
  }
@@ -7,12 +7,15 @@ export class HttpClient {
7
7
  }
8
8
  requestApiWithRetry(path, opts, retryInterval) {
9
9
  const retry = retryPromise(this.requestApi.bind(this, path, opts), (error) => {
10
- var _a;
10
+ var _a, _b, _c;
11
11
  let retryValue = false;
12
12
  if (error === null || error === void 0 ? void 0 : error.response) {
13
13
  retryValue = HttpClient.retryStatusCodes.has(error.response.status || 0);
14
- const retryAfter = (_a = error.response.headers) === null || _a === void 0 ? void 0 : _a['retry-after'];
14
+ // This *should* be an axios error according to typings, but it appears this could be an AxiosError *or* and XmlHttpRequest
15
+ // we'll check both to be safe
16
+ const retryAfter = ((_a = error.response.headers) === null || _a === void 0 ? void 0 : _a['retry-after']) || ((_c = (_b = error.response).getResponseHeader) === null || _c === void 0 ? void 0 : _c.call(_b, 'retry-after'));
15
17
  if (retryAfter) {
18
+ (opts.logger || console).debug('retry-after header found on response. setting retry delay', { retryAfter });
16
19
  // retry after comes in seconds, we need to return milliseconds
17
20
  retryValue = parseInt(retryAfter, 10) * 1000;
18
21
  }
@@ -47,6 +50,7 @@ export class HttpClient {
47
50
  .then(boundHandler, boundHandler);
48
51
  }
49
52
  handleResponse(logger, start, params, res) {
53
+ var _a;
50
54
  let now = new Date().getTime();
51
55
  let elapsed = (now - start) + 'ms';
52
56
  if (res instanceof AxiosError) {
@@ -64,7 +68,7 @@ export class HttpClient {
64
68
  let status = response.status;
65
69
  let correlationId = response.headers && response.headers[correlationIdHeaderName];
66
70
  let body = response.data;
67
- let error = Object.assign(Object.assign({}, res), { text: response.request.response });
71
+ let error = Object.assign(Object.assign({}, res), { text: (_a = response.request) === null || _a === void 0 ? void 0 : _a.response });
68
72
  logger.debug(`request error: ${params.url}`, {
69
73
  message: res.message,
70
74
  now,