genesys-cloud-streaming-client 20.0.0 → 20.0.1-release.1

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/README.md CHANGED
@@ -2,22 +2,46 @@
2
2
  [![Test Matrix](https://github.com/purecloudlabs/genesys-cloud-streaming-client/actions/workflows/matrix.yml/badge.svg)](https://github.com/purecloudlabs/genesys-cloud-streaming-client/actions/workflows/matrix.yml)
3
3
 
4
4
  ### Overview
5
- Client library for streaming-service
5
+ Client library for Genesys Cloud streaming services (websocket/XMPP interface).
6
6
 
7
7
  ### Installation
8
- Run `npm install` in order to install all the dependencies
8
+ ```bash
9
+ npm install genesys-cloud-streaming-client
10
+ ```
9
11
 
10
- ### Testing
11
- Run the tests using `npm test` in the command line
12
+ ### Quick Start
13
+ ```ts
14
+ import StreamingClient from 'genesys-cloud-streaming-client';
15
+
16
+ const client = new StreamingClient({
17
+ host: 'wss://streaming.mypurecloud.com',
18
+ authToken: 'your-access-token',
19
+ });
20
+
21
+ await client.connect();
22
+ ```
23
+
24
+ > If you're using this in a browser with a bundler like Vite or Webpack 5, you'll
25
+ > need polyfills for `global`, `events`, and possibly `process`. See the
26
+ > [full documentation](doc/documentation.md#browser-usage--polyfills) for details.
12
27
 
13
- **In order to see code coverage run `npm run test:coverage`**
28
+ ### Documentation
29
+ See [doc/documentation.md](doc/documentation.md) for the full API reference,
30
+ browser polyfill setup, and known issues.
31
+
32
+ ### Development
33
+
34
+ Run `npm install` to install dependencies.
35
+
36
+ ### Testing
37
+ Run the tests using `npm test` in the command line.
14
38
 
15
39
  ### Build for Local Use
16
- Run the script `npm run build`
40
+ Run `npm run build`
17
41
 
18
42
  ### Linting and Style
19
- semistandard has been added and you can run linting through the command line via `npm run lint` script
43
+ ESLint with the semistandard config has been added and you can run linting through the command line via `npm run lint`
20
44
 
21
45
  To fix minor styling errors, run `npm run lint:fix`
22
46
 
23
- **If you can configure you editor to run linting while typing or on save this is preferrable**
47
+ **If you can configure your editor to run linting while typing or on save, this is preferable.**
@@ -7,13 +7,14 @@ export declare class AlertingLeaderExtension extends EventEmitter implements Str
7
7
  private client;
8
8
  private connectionId?;
9
9
  private alertableInteractionTypes;
10
- private abortController?;
10
+ private getLeaderAbortController?;
11
11
  private leaderStatus;
12
12
  constructor(client: Client, options: IClientOptions);
13
13
  handleStanzaInstanceChange(stanzaInstance: NamedAgent): void;
14
14
  private setupAlertingLeader;
15
15
  private subscribeToAlertingLeader;
16
16
  private markAsAlertable;
17
+ private getAlertingLeaderEarly;
17
18
  private getAlertingLeader;
18
19
  private claimAlertingLeader;
19
20
  get expose(): AlertingLeaderApi;
@@ -22,12 +22,13 @@ class AlertingLeaderExtension extends events_1.EventEmitter {
22
22
  async setupAlertingLeader() {
23
23
  if (this.alertableInteractionTypes.length !== 0) {
24
24
  try {
25
+ this.getAlertingLeaderEarly();
25
26
  await this.subscribeToAlertingLeader();
26
27
  await this.markAsAlertable();
27
28
  await this.getAlertingLeader();
28
29
  }
29
30
  catch (err) {
30
- this.client.logger.warn('Failed to setup alerting leader; falling back to the default of acting as the alerting leader');
31
+ this.client.logger.warn('Failed to setup alerting leader; falling back to acting as the leader');
31
32
  // Fail 'open' so users don't miss calls
32
33
  this.leaderStatus = { voice: { alerting: true, configured: false } };
33
34
  this.emit('alertingLeaderChanged', this.leaderStatus);
@@ -38,9 +39,9 @@ class AlertingLeaderExtension extends events_1.EventEmitter {
38
39
  const topic = `v2.users.${this.client.config.userId}.alertingleader`;
39
40
  this.client.on(`notify:${topic}`, (event) => {
40
41
  var _a, _b;
41
- (_a = this.abortController) === null || _a === void 0 ? void 0 : _a.abort();
42
+ (_a = this.getLeaderAbortController) === null || _a === void 0 ? void 0 : _a.abort();
42
43
  if ((_b = event.eventBody) === null || _b === void 0 ? void 0 : _b.connectionId) {
43
- // We should alert if our connection is the alerting leader connection
44
+ // The consuming client should be the one alerting if our connection is the current leader
44
45
  const alerting = event.eventBody.connectionId === this.connectionId;
45
46
  const clientType = event.eventBody.clientType;
46
47
  let voice = { alerting, configured: true };
@@ -79,21 +80,36 @@ class AlertingLeaderExtension extends events_1.EventEmitter {
79
80
  }, 500, this.client.logger);
80
81
  return retry.promise
81
82
  .catch(() => {
82
- this.client.logger.warn('Could not mark this connection as alertable; this client may not alert for incoming interactions');
83
+ this.client.logger.warn('Could not mark this connection as alertable');
83
84
  });
84
85
  }
86
+ async getAlertingLeaderEarly() {
87
+ try {
88
+ await this.getAlertingLeader();
89
+ }
90
+ catch (error) {
91
+ if (axios_1.default.isAxiosError(error) && error.status === 400) {
92
+ this.client.logger.info('The org has not configured alerting leader functionality or there are not yet any active alertable connections; falling back to acting as the leader');
93
+ this.leaderStatus = { voice: { alerting: true, configured: false } };
94
+ this.emit('alertingLeaderChanged', this.leaderStatus);
95
+ }
96
+ }
97
+ }
85
98
  async getAlertingLeader() {
86
- this.abortController = new AbortController();
99
+ var _a;
100
+ // If an early request is still in-flight, cancel it and get more recent data
101
+ (_a = this.getLeaderAbortController) === null || _a === void 0 ? void 0 : _a.abort();
102
+ this.getLeaderAbortController = new AbortController();
87
103
  const leaderRequestOptions = {
88
104
  method: 'get',
89
105
  host: this.client.config.apiHost,
90
106
  authToken: this.client.config.authToken,
91
107
  logger: this.client.logger,
92
- signal: this.abortController.signal
108
+ signal: this.getLeaderAbortController.signal
93
109
  };
94
110
  try {
95
- const currentLeader = await this.client.http.requestApi('users/alertingleader', leaderRequestOptions);
96
- // We should alert if our connection is the alerting leader connection
111
+ const currentLeader = await this.client.http.requestApiWithRetry('users/alertingleader', leaderRequestOptions, 1000).promise;
112
+ // The consuming client should be the one alerting if our connection is the current leader
97
113
  const alerting = currentLeader.data.connectionId === this.connectionId;
98
114
  const clientType = currentLeader.data.clientType;
99
115
  let voice = { alerting, configured: true };
@@ -670,7 +670,7 @@ class Client extends events_1.default {
670
670
  return Client.version;
671
671
  }
672
672
  static get version() {
673
- return '20.0.0';
673
+ return '20.0.1';
674
674
  }
675
675
  }
676
676
  exports.Client = Client;
@@ -12,6 +12,8 @@ export declare class Notifications implements StreamingClientExtension {
12
12
  debouncedResubscribe: () => Promise<BulkSubscribeResult>;
13
13
  enablePartialBulkResubscribe: boolean;
14
14
  private internalSubscriptions;
15
+ private pendingBulkSubscribeRetry?;
16
+ private pendingConnectedWait?;
15
17
  constructor(client: any, options?: IClientOptions);
16
18
  get pubsubHost(): string;
17
19
  handleStanzaInstanceChange(stanza: NamedAgent): void;
@@ -178,20 +178,57 @@ class Notifications {
178
178
  }
179
179
  return keptTopics;
180
180
  }
181
- makeBulkSubscribeRequest(topics, options) {
181
+ async makeBulkSubscribeRequest(topics, options) {
182
+ // Cancel any in-flight retry or connected-wait from a previous bulk subscribe.
183
+ // If the topic list has changed (subscribe/unsubscribe called between retries),
184
+ // the stale request would send an outdated topic list. Since bulk subscribes with
185
+ // replace=true use PUT (full replacement), only the most recent request matters.
186
+ if (this.pendingConnectedWait) {
187
+ this.pendingConnectedWait.cancel(new Error('Superseded by newer bulk subscribe request'));
188
+ this.pendingConnectedWait = undefined;
189
+ }
190
+ if (this.pendingBulkSubscribeRetry && !this.pendingBulkSubscribeRetry.hasCompleted()) {
191
+ this.client.logger.info('Cancelling previous bulk subscribe retry — new request supersedes it');
192
+ this.pendingBulkSubscribeRetry.cancel(new Error('Superseded by newer bulk subscribe request'));
193
+ }
194
+ // If the client is not connected, wait for reconnection before attempting the request.
195
+ // This prevents firing requests against a dead/stale channel ID which would result in
196
+ // 400 errors (notification.unable.to.get.channel.id). After reconnection, the stanza
197
+ // instance will have the fresh channel ID from the new connection.
198
+ if (!this.client.connected) {
199
+ this.client.logger.info('Client not connected, waiting for reconnection before bulk subscribe', {
200
+ topicCount: topics.length
201
+ });
202
+ await new Promise((resolve, reject) => {
203
+ const onConnected = () => {
204
+ this.pendingConnectedWait = undefined;
205
+ resolve();
206
+ };
207
+ this.client.once('connected', onConnected);
208
+ this.pendingConnectedWait = {
209
+ cancel: (reason) => {
210
+ this.client.off('connected', onConnected);
211
+ reject(reason);
212
+ }
213
+ };
214
+ });
215
+ }
182
216
  const requestOptions = {
183
217
  method: options.replace ? 'put' : 'post',
184
218
  host: this.client.config.apiHost,
185
219
  authToken: this.client.config.authToken,
186
220
  data: JSON.stringify(this.mapCombineTopics(topics)),
187
- logger: this.client.logger
221
+ logger: this.client.logger,
222
+ maxAttempts: 3
188
223
  };
189
224
  const channelId = this.stanzaInstance.channelId;
190
225
  let path = `notifications/channels/${channelId}/subscriptions`;
191
226
  if (this.enablePartialBulkResubscribe) {
192
227
  path += '?ignoreErrors=true';
193
228
  }
194
- return this.client.http.requestApi(path, requestOptions);
229
+ const retry = this.client.http.requestApiWithRetry(path, requestOptions, 2000);
230
+ this.pendingBulkSubscribeRetry = retry;
231
+ return retry.promise;
195
232
  }
196
233
  createSubscription(topic, handler) {
197
234
  const topics = (0, utils_1.splitIntoIndividualTopics)(topic);
@@ -366,7 +403,7 @@ class Notifications {
366
403
  return this.xmppUnsubscribe(topic);
367
404
  }
368
405
  async bulkSubscribe(topics, options = { replace: false, force: false }, priorities = {}) {
369
- var _a;
406
+ var _a, _b;
370
407
  this.setTopicPriorities(priorities);
371
408
  let toSubscribe = mergeAndDedup(topics, this.internalSubscriptions);
372
409
  if (options.replace && !options.force) {
@@ -377,7 +414,24 @@ class Notifications {
377
414
  // if it's a forcible bulk subscribe, wipe out individual subscriptions
378
415
  this.subscriptions = {};
379
416
  }
380
- const response = await this.makeBulkSubscribeRequest(toSubscribe, options);
417
+ let response;
418
+ try {
419
+ response = await this.makeBulkSubscribeRequest(toSubscribe, options);
420
+ }
421
+ catch (err) {
422
+ // If this request was cancelled because a newer bulk subscribe superseded it,
423
+ // return an 'Unknown' state for all topics. The superseding request will carry
424
+ // the current desired topic list and handle the actual subscription.
425
+ if ((_a = err === null || err === void 0 ? void 0 : err.message) === null || _a === void 0 ? void 0 : _a.includes('Superseded by newer bulk subscribe request')) {
426
+ this.client.logger.debug('Bulk subscribe was superseded, deferring to newer request');
427
+ const result = {};
428
+ for (const topic of toSubscribe) {
429
+ result[topic] = { topic, state: 'Unknown' };
430
+ }
431
+ return result;
432
+ }
433
+ throw err;
434
+ }
381
435
  let topicResponseEntities = [];
382
436
  if (response && response.data && 'entities' in response.data && Array.isArray(response.data.entities)) {
383
437
  topicResponseEntities = response.data.entities;
@@ -415,7 +469,7 @@ class Notifications {
415
469
  // With partial bulk resubscribe enabled missing result means "Unknown" state but when not
416
470
  // enabled the fallback is "Permitted" for backward compatibility (success response means OK).
417
471
  for (const topic of toSubscribe) {
418
- (_a = result[topic]) !== null && _a !== void 0 ? _a : (result[topic] = { topic, state: this.enablePartialBulkResubscribe ? 'Unknown' : 'Permitted' });
472
+ (_b = result[topic]) !== null && _b !== void 0 ? _b : (result[topic] = { topic, state: this.enablePartialBulkResubscribe ? 'Unknown' : 'Permitted' });
419
473
  }
420
474
  return result;
421
475
  }
@@ -1,13 +1,13 @@
1
1
  {
2
- "version": "20.0.0",
3
- "build": "12",
4
- "buildDate": "2026-06-05T11:16:47.414Z",
2
+ "version": "release/v20.0.1",
3
+ "build": "1",
4
+ "buildDate": "2026-06-23T16:09:25.734Z",
5
5
  "indexFiles": [
6
6
  {
7
- "file": "v20.0.0/streaming-client.browser.js"
7
+ "file": "v20.0.1/streaming-client.browser.js"
8
8
  },
9
9
  {
10
- "file": "v20.0.0/streaming-client.browser.js.LICENSE.txt"
10
+ "file": "v20.0.1/streaming-client.browser.js.LICENSE.txt"
11
11
  },
12
12
  {
13
13
  "file": "v20/streaming-client.browser.js"
@@ -7,13 +7,14 @@ export declare class AlertingLeaderExtension extends EventEmitter implements Str
7
7
  private client;
8
8
  private connectionId?;
9
9
  private alertableInteractionTypes;
10
- private abortController?;
10
+ private getLeaderAbortController?;
11
11
  private leaderStatus;
12
12
  constructor(client: Client, options: IClientOptions);
13
13
  handleStanzaInstanceChange(stanzaInstance: NamedAgent): void;
14
14
  private setupAlertingLeader;
15
15
  private subscribeToAlertingLeader;
16
16
  private markAsAlertable;
17
+ private getAlertingLeaderEarly;
17
18
  private getAlertingLeader;
18
19
  private claimAlertingLeader;
19
20
  get expose(): AlertingLeaderApi;
@@ -20,12 +20,13 @@ export class AlertingLeaderExtension extends EventEmitter {
20
20
  return __awaiter(this, void 0, void 0, function* () {
21
21
  if (this.alertableInteractionTypes.length !== 0) {
22
22
  try {
23
+ this.getAlertingLeaderEarly();
23
24
  yield this.subscribeToAlertingLeader();
24
25
  yield this.markAsAlertable();
25
26
  yield this.getAlertingLeader();
26
27
  }
27
28
  catch (err) {
28
- this.client.logger.warn('Failed to setup alerting leader; falling back to the default of acting as the alerting leader');
29
+ this.client.logger.warn('Failed to setup alerting leader; falling back to acting as the leader');
29
30
  // Fail 'open' so users don't miss calls
30
31
  this.leaderStatus = { voice: { alerting: true, configured: false } };
31
32
  this.emit('alertingLeaderChanged', this.leaderStatus);
@@ -38,9 +39,9 @@ export class AlertingLeaderExtension extends EventEmitter {
38
39
  const topic = `v2.users.${this.client.config.userId}.alertingleader`;
39
40
  this.client.on(`notify:${topic}`, (event) => {
40
41
  var _a, _b;
41
- (_a = this.abortController) === null || _a === void 0 ? void 0 : _a.abort();
42
+ (_a = this.getLeaderAbortController) === null || _a === void 0 ? void 0 : _a.abort();
42
43
  if ((_b = event.eventBody) === null || _b === void 0 ? void 0 : _b.connectionId) {
43
- // We should alert if our connection is the alerting leader connection
44
+ // The consuming client should be the one alerting if our connection is the current leader
44
45
  const alerting = event.eventBody.connectionId === this.connectionId;
45
46
  const clientType = event.eventBody.clientType;
46
47
  let voice = { alerting, configured: true };
@@ -81,23 +82,40 @@ export class AlertingLeaderExtension extends EventEmitter {
81
82
  }, 500, this.client.logger);
82
83
  return retry.promise
83
84
  .catch(() => {
84
- this.client.logger.warn('Could not mark this connection as alertable; this client may not alert for incoming interactions');
85
+ this.client.logger.warn('Could not mark this connection as alertable');
85
86
  });
86
87
  });
87
88
  }
89
+ getAlertingLeaderEarly() {
90
+ return __awaiter(this, void 0, void 0, function* () {
91
+ try {
92
+ yield this.getAlertingLeader();
93
+ }
94
+ catch (error) {
95
+ if (axios.isAxiosError(error) && error.status === 400) {
96
+ this.client.logger.info('The org has not configured alerting leader functionality or there are not yet any active alertable connections; falling back to acting as the leader');
97
+ this.leaderStatus = { voice: { alerting: true, configured: false } };
98
+ this.emit('alertingLeaderChanged', this.leaderStatus);
99
+ }
100
+ }
101
+ });
102
+ }
88
103
  getAlertingLeader() {
104
+ var _a;
89
105
  return __awaiter(this, void 0, void 0, function* () {
90
- this.abortController = new AbortController();
106
+ // If an early request is still in-flight, cancel it and get more recent data
107
+ (_a = this.getLeaderAbortController) === null || _a === void 0 ? void 0 : _a.abort();
108
+ this.getLeaderAbortController = new AbortController();
91
109
  const leaderRequestOptions = {
92
110
  method: 'get',
93
111
  host: this.client.config.apiHost,
94
112
  authToken: this.client.config.authToken,
95
113
  logger: this.client.logger,
96
- signal: this.abortController.signal
114
+ signal: this.getLeaderAbortController.signal
97
115
  };
98
116
  try {
99
- const currentLeader = yield this.client.http.requestApi('users/alertingleader', leaderRequestOptions);
100
- // We should alert if our connection is the alerting leader connection
117
+ const currentLeader = yield this.client.http.requestApiWithRetry('users/alertingleader', leaderRequestOptions, 1000).promise;
118
+ // The consuming client should be the one alerting if our connection is the current leader
101
119
  const alerting = currentLeader.data.connectionId === this.connectionId;
102
120
  const clientType = currentLeader.data.clientType;
103
121
  let voice = { alerting, configured: true };
package/dist/es/client.js CHANGED
@@ -684,6 +684,6 @@ export class Client extends EventEmitter {
684
684
  return Client.version;
685
685
  }
686
686
  static get version() {
687
- return '20.0.0';
687
+ return '20.0.1';
688
688
  }
689
689
  }