@webex/internal-plugin-device 3.11.0 → 3.12.0-next.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.
package/src/device.js CHANGED
@@ -6,7 +6,13 @@ import {orderBy} from 'lodash';
6
6
  import uuid from 'uuid';
7
7
 
8
8
  import METRICS from './metrics';
9
- import {FEATURE_COLLECTION_NAMES, DEVICE_EVENT_REGISTRATION_SUCCESS} from './constants';
9
+ import {
10
+ FEATURE_COLLECTION_NAMES,
11
+ DEVICE_EVENT_REGISTRATION_SUCCESS,
12
+ MIN_DEVICES_FOR_CLEANUP,
13
+ MAX_DELETION_CONFIRMATION_ATTEMPTS,
14
+ DELETION_CONFIRMATION_DELAY_MS,
15
+ } from './constants';
10
16
  import FeaturesModel from './features/features-model';
11
17
  import IpNetworkDetector from './ipNetworkDetector';
12
18
  import {CatalogDetails} from './types';
@@ -454,46 +460,117 @@ const Device = WebexPlugin.extend({
454
460
  });
455
461
  },
456
462
  /**
457
- * Fetches the web devices and deletes the third of them which are not recent devices in use
458
- * @returns {Promise<void, Error>}
463
+ * Fetches devices matching the current device type.
464
+ * @returns {Promise<Array>} filtered device list
459
465
  */
460
- deleteDevices() {
461
- // Fetch devices with a GET request
466
+ _getDevicesOfCurrentType() {
467
+ const {deviceType} = this._getBody();
468
+
462
469
  return this.request({
463
470
  method: 'GET',
464
471
  service: 'wdm',
465
472
  resource: 'devices',
466
- })
467
- .then((response) => {
468
- const {devices} = response.body;
473
+ }).then((response) => response.body.devices.filter((item) => item.deviceType === deviceType));
474
+ },
475
+
476
+ /**
477
+ * Waits until the server-side device count drops to or below targetCount,
478
+ * polling up to maxAttempts times with a delay between each check.
479
+ * @param {number} targetCount - resolve when device count drops to this value or below
480
+ * @param {number} [attempt=0]
481
+ * @returns {Promise<void>}
482
+ */
483
+ _waitForDeviceCountBelowLimit(targetCount, attempt = 0) {
484
+ if (attempt >= MAX_DELETION_CONFIRMATION_ATTEMPTS) {
485
+ this.logger.warn('device: max confirmation attempts reached, proceeding anyway');
486
+
487
+ return Promise.resolve();
488
+ }
489
+
490
+ return new Promise((resolve) => setTimeout(resolve, DELETION_CONFIRMATION_DELAY_MS))
491
+ .then(() => this._getDevicesOfCurrentType())
492
+ .then((devices) => {
493
+ this.logger.info(
494
+ `device: confirmation check ${attempt + 1}/${MAX_DELETION_CONFIRMATION_ATTEMPTS}, ` +
495
+ `${devices.length} devices remaining (target: ≤ ${targetCount})`
496
+ );
497
+
498
+ if (devices.length <= targetCount) {
499
+ this.logger.info('device: device count is now safely below limit');
469
500
 
470
- const {deviceType} = this._getBody();
501
+ return Promise.resolve();
502
+ }
503
+
504
+ return this._waitForDeviceCountBelowLimit(targetCount, attempt + 1);
505
+ })
506
+ .catch((error) => {
507
+ this.logger.warn(
508
+ `device: confirmation check ${attempt + 1} failed, proceeding anyway:`,
509
+ error
510
+ );
511
+
512
+ return Promise.resolve();
513
+ });
514
+ },
471
515
 
472
- // Filter devices of type deviceType
473
- const webDevices = devices.filter((item) => item.deviceType === deviceType);
516
+ /**
517
+ * Fetches the web devices and deletes the oldest third, then waits
518
+ * for the server to confirm the count is below the limit.
519
+ * @returns {Promise<void>}
520
+ */
521
+ deleteDevices() {
522
+ let targetCount;
474
523
 
524
+ return this._getDevicesOfCurrentType()
525
+ .then((webDevices) => {
475
526
  const sortedDevices = orderBy(webDevices, [(item) => new Date(item.modificationTime)]);
476
527
 
477
- // If there are more than two devices, delete the last third
478
- if (sortedDevices.length > 2) {
479
- const totalItems = sortedDevices.length;
480
- const countToDelete = Math.ceil(totalItems / 3);
481
- const urlsToDelete = sortedDevices.slice(0, countToDelete).map((item) => item.url);
482
-
483
- return Promise.race(
484
- urlsToDelete.map((url) => {
485
- return this.request({
486
- uri: url,
487
- method: 'DELETE',
488
- });
489
- })
528
+ if (sortedDevices.length <= MIN_DEVICES_FOR_CLEANUP) {
529
+ this.logger.info(
530
+ `device: only ${sortedDevices.length} devices found (minimum ${MIN_DEVICES_FOR_CLEANUP}), skipping cleanup`
490
531
  );
532
+
533
+ return Promise.resolve();
491
534
  }
492
535
 
493
- return Promise.resolve();
536
+ const devicesToDelete = sortedDevices.slice(0, Math.ceil(sortedDevices.length / 3));
537
+ targetCount = sortedDevices.length - Math.min(5, devicesToDelete.length);
538
+
539
+ this.logger.info(
540
+ `device: deleting ${devicesToDelete.length} of ${webDevices.length} devices`
541
+ );
542
+
543
+ return Promise.all(
544
+ devicesToDelete.map((device) =>
545
+ this.request({uri: device.url, method: 'DELETE'})
546
+ .then(() => ({status: 'fulfilled'}))
547
+ .catch((reason) => ({status: 'rejected', reason}))
548
+ )
549
+ ).then((results) => {
550
+ const failed = results.filter((r) => r.status === 'rejected');
551
+
552
+ if (failed.length > 0) {
553
+ this.logger.warn(
554
+ `device: ${failed.length} of ${devicesToDelete.length} deletions failed (best-effort, continuing)`
555
+ );
556
+ }
557
+ this.logger.info(
558
+ `device: deleted ${devicesToDelete.length - failed.length} of ${
559
+ devicesToDelete.length
560
+ } devices`
561
+ );
562
+ });
563
+ })
564
+ .then(() =>
565
+ targetCount !== undefined
566
+ ? this._waitForDeviceCountBelowLimit(targetCount, 0)
567
+ : Promise.resolve()
568
+ )
569
+ .then(() => {
570
+ this.logger.info('device: device count confirmed below limit, cleanup successful');
494
571
  })
495
572
  .catch((error) => {
496
- this.logger.error('Failed to retrieve devices:', error);
573
+ this.logger.error('device: failed to delete devices:', error);
497
574
 
498
575
  return Promise.reject(error);
499
576
  });
@@ -519,7 +596,11 @@ const Device = WebexPlugin.extend({
519
596
 
520
597
  return this._registerInternal(deviceRegistrationOptions).catch((error) => {
521
598
  if (error?.body?.message === 'User has excessive device registrations') {
599
+ this.logger.info('device: excessive device registrations detected, initiating cleanup');
600
+
522
601
  return this.deleteDevices().then(() => {
602
+ this.logger.info('device: device cleanup complete, retrying registration');
603
+
523
604
  return this._registerInternal(deviceRegistrationOptions);
524
605
  });
525
606
  }
@@ -786,6 +867,34 @@ const Device = WebexPlugin.extend({
786
867
  return Promise.reject(new Error('device: failed to get the current websocket url'));
787
868
  },
788
869
 
870
+ /**
871
+ * Get sanitized processed debug features from session storage
872
+ * these should be JSON encoded and in the form {feature1: true, feature2: false}
873
+ *
874
+ * @returns {Array<Object>} - Array of sanitized debug feature toggles
875
+ */
876
+ getDebugFeatures() {
877
+ const sanitizedDebugFeatures = [];
878
+ if (this.config.debugFeatureTogglesKey) {
879
+ const debugFeaturesString = this.webex
880
+ .getWindow()
881
+ .sessionStorage.getItem(this.config.debugFeatureTogglesKey);
882
+ if (debugFeaturesString) {
883
+ const debugFeatures = JSON.parse(debugFeaturesString);
884
+ Object.entries(debugFeatures).forEach(([key, value]) => {
885
+ sanitizedDebugFeatures.push({
886
+ key,
887
+ val: value ? 'true' : 'false',
888
+ mutable: true,
889
+ lastModified: new Date().toISOString(),
890
+ });
891
+ });
892
+ }
893
+ }
894
+
895
+ return sanitizedDebugFeatures;
896
+ },
897
+
789
898
  /**
790
899
  * Process a successful device registration.
791
900
  *
@@ -814,6 +923,14 @@ const Device = WebexPlugin.extend({
814
923
  // When using the etag feature cache, user and entitlement features are still returned
815
924
  this.features.user.reset(features.user);
816
925
  this.features.entitlement.reset(features.entitlement);
926
+ } else if (this.config.debugFeatureTogglesKey && body?.features?.developer) {
927
+ // Add the debug feature toggles from session storage if available
928
+ try {
929
+ const debugFeatures = this.getDebugFeatures();
930
+ body.features.developer.push(...debugFeatures);
931
+ } catch (error) {
932
+ this.logger.error('Failed to parse debug feature toggles from session storage:', error);
933
+ }
817
934
  }
818
935
 
819
936
  // Assign the recieved DTO from **WDM** to this device.
@@ -946,6 +1063,13 @@ const Device = WebexPlugin.extend({
946
1063
  // Prototype the extended class in order to preserve the parent member.
947
1064
  Reflect.apply(WebexPlugin.prototype.initialize, this, args);
948
1065
 
1066
+ this.listenToOnce(this.webex, 'change:config', () => {
1067
+ // If debug feature toggles exist, clear the etag to ensure developer feature toggles are fetched
1068
+ if (this.getDebugFeatures(this.config.debugFeatureTogglesKey).length > 0) {
1069
+ this.set('etag', undefined);
1070
+ }
1071
+ });
1072
+
949
1073
  // Initialize feature events and listeners.
950
1074
  FEATURE_COLLECTION_NAMES.forEach((collectionName) => {
951
1075
  this.features.on(`change:${collectionName}`, (model, value, options) => {
@@ -188,7 +188,7 @@ const IpNetworkDetector = WebexPlugin.extend({
188
188
 
189
189
  results = await this.gatherLocalCandidates(pc);
190
190
  } finally {
191
- pc.close();
191
+ pc?.close();
192
192
  this.state = STATE.IDLE;
193
193
  }
194
194