@webex/plugin-meetings 3.0.0-beta.185 → 3.0.0-beta.187

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.
@@ -1,8 +1,26 @@
1
1
  import {difference} from 'lodash';
2
2
 
3
- import SimpleQueue from '../common/queue';
3
+ import SortedQueue from '../common/queue';
4
4
  import LoggerProxy from '../common/logs/logger-proxy';
5
5
 
6
+ const MAX_OOO_DELTA_COUNT = 5; // when we receive an out-of-order delta and the queue builds up to MAX_OOO_DELTA_COUNT, we do a sync with Locus
7
+ const OOO_DELTA_WAIT_TIME = 10000; // [ms] minimum wait time before we do a sync if we get out-of-order deltas
8
+ const OOO_DELTA_WAIT_TIME_RANDOM_DELAY = 5000; // [ms] max random delay added to OOO_DELTA_WAIT_TIME
9
+
10
+ type LocusDeltaDto = {
11
+ baseSequence: {
12
+ rangeStart: number;
13
+ rangeEnd: number;
14
+ entries: number[];
15
+ };
16
+ sequence: {
17
+ rangeStart: number;
18
+ rangeEnd: number;
19
+ entries: number[];
20
+ };
21
+ syncUrl: string;
22
+ };
23
+
6
24
  /**
7
25
  * Locus Delta Parser
8
26
  * @private
@@ -10,11 +28,11 @@ import LoggerProxy from '../common/logs/logger-proxy';
10
28
  */
11
29
  export default class Parser {
12
30
  // processing status
13
- static status = {
14
- IDLE: 'IDLE',
15
- PAUSED: 'PAUSED',
16
- WORKING: 'WORKING',
17
- };
31
+ status:
32
+ | 'IDLE' // not doing anything
33
+ | 'PAUSED' // paused, because we are doing a sync
34
+ | 'WORKING' // processing a delta event
35
+ | 'BLOCKED'; // received an out-of-order delta, so waiting for the missing one
18
36
 
19
37
  // loci comparison states
20
38
  static loci = {
@@ -24,21 +42,59 @@ export default class Parser {
24
42
  DESYNC: 'DESYNC',
25
43
  USE_INCOMING: 'USE_INCOMING',
26
44
  USE_CURRENT: 'USE_CURRENT',
45
+ WAIT: 'WAIT',
27
46
  ERROR: 'ERROR',
28
47
  };
29
48
 
30
- queue: any;
49
+ queue: SortedQueue<LocusDeltaDto>;
31
50
  workingCopy: any;
51
+ syncTimer: null | number | NodeJS.Timeout;
32
52
 
33
53
  /**
34
54
  * @constructs Parser
35
55
  */
36
56
  constructor() {
37
- this.queue = new SimpleQueue();
38
- // @ts-ignore - This is declared as static class member and again being initialized here from same
39
- this.status = Parser.status.IDLE;
57
+ const deltaCompareFunc = (left: LocusDeltaDto, right: LocusDeltaDto) => {
58
+ const {LT, GT} = Parser.loci;
59
+ const {extractComparisonState: extract} = Parser;
60
+
61
+ if (Parser.isSequenceEmpty(left)) {
62
+ return -1;
63
+ }
64
+ if (Parser.isSequenceEmpty(right)) {
65
+ return 1;
66
+ }
67
+ const result = extract(Parser.compareSequence(left.baseSequence, right.baseSequence));
68
+
69
+ if (result === LT) {
70
+ return -1;
71
+ }
72
+ if (result === GT) {
73
+ return 1;
74
+ }
75
+
76
+ return 0;
77
+ };
78
+
79
+ this.queue = new SortedQueue<LocusDeltaDto>(deltaCompareFunc);
80
+ this.status = 'IDLE';
40
81
  this.onDeltaAction = null;
41
82
  this.workingCopy = null;
83
+ this.syncTimer = null;
84
+ }
85
+
86
+ /**
87
+ * Returns a debug string representing a locus delta - useful for logging
88
+ *
89
+ * @param {LocusDeltaDto} locus Locus delta
90
+ * @returns {string}
91
+ */
92
+ static locus2string(locus: LocusDeltaDto) {
93
+ if (!locus.sequence?.entries) {
94
+ return 'invalid';
95
+ }
96
+
97
+ return locus.sequence.entries.length ? `seq=${locus.sequence.entries.at(-1)}` : 'empty';
42
98
  }
43
99
 
44
100
  /**
@@ -208,7 +264,7 @@ export default class Parser {
208
264
  * @returns {string} loci comparison state
209
265
  */
210
266
  private static compareDelta(current, incoming) {
211
- const {LT, GT, EQ, DESYNC, USE_INCOMING} = Parser.loci;
267
+ const {LT, GT, EQ, DESYNC, USE_INCOMING, WAIT} = Parser.loci;
212
268
 
213
269
  const {extractComparisonState: extract} = Parser;
214
270
  const {packComparisonResult: pack} = Parser;
@@ -228,6 +284,17 @@ export default class Parser {
228
284
  comparison = USE_INCOMING;
229
285
  break;
230
286
 
287
+ case LT:
288
+ if (extract(Parser.compareSequence(incoming.baseSequence, incoming.sequence)) === EQ) {
289
+ // special case where Locus sends a delta with baseSequence === sequence to trigger a sync,
290
+ // because the delta event is too large to be sent over mercury connection
291
+ comparison = DESYNC;
292
+ } else {
293
+ // the incoming locus has baseSequence from the future, so it is out-of-order,
294
+ // we are missing 1 or more locus that should be in front of it, we need to wait for it
295
+ comparison = WAIT;
296
+ }
297
+ break;
231
298
  default:
232
299
  comparison = DESYNC;
233
300
  }
@@ -436,17 +503,10 @@ export default class Parser {
436
503
  */
437
504
  isValidLocus(newLoci) {
438
505
  let isValid = false;
439
- const {IDLE} = Parser.status;
440
506
  const {isLoci} = Parser;
441
- // @ts-ignore
442
- const setStatus = (status) => {
443
- // @ts-ignore
444
- this.status = status;
445
- };
446
507
 
447
508
  // one or both objects are not locus delta events
448
509
  if (!isLoci(this.workingCopy) || !isLoci(newLoci)) {
449
- setStatus(IDLE);
450
510
  LoggerProxy.logger.info(
451
511
  'Locus-info:parser#processDeltaEvent --> Ignoring non-locus object. workingCopy:',
452
512
  this.workingCopy,
@@ -498,19 +558,25 @@ export default class Parser {
498
558
  * @returns {undefined}
499
559
  */
500
560
  nextEvent() {
501
- // @ts-ignore
502
- if (this.status === Parser.status.PAUSED) {
561
+ if (this.status === 'PAUSED') {
503
562
  LoggerProxy.logger.info('Locus-info:parser#nextEvent --> Locus parser paused.');
504
563
 
505
564
  return;
506
565
  }
507
566
 
567
+ if (this.status === 'BLOCKED') {
568
+ LoggerProxy.logger.info(
569
+ 'Locus-info:parser#nextEvent --> Locus parser blocked by out-of-order delta.'
570
+ );
571
+
572
+ return;
573
+ }
574
+
508
575
  // continue processing until queue is empty
509
576
  if (this.queue.size() > 0) {
510
577
  this.processDeltaEvent();
511
578
  } else {
512
- // @ts-ignore
513
- this.status = Parser.status.IDLE;
579
+ this.status = 'IDLE';
514
580
  }
515
581
  }
516
582
 
@@ -532,15 +598,20 @@ export default class Parser {
532
598
  onDeltaEvent(loci) {
533
599
  // enqueue the new loci
534
600
  this.queue.enqueue(loci);
535
- // start processing events in the queue if idle
536
- // and a function handler is defined
537
- // @ts-ignore
538
- if (this.status === Parser.status.IDLE && this.onDeltaAction) {
539
- // Update status, ensure we only process one event at a time.
540
- // @ts-ignore
541
- this.status = Parser.status.WORKING;
542
601
 
543
- this.processDeltaEvent();
602
+ if (this.onDeltaAction) {
603
+ if (this.status === 'BLOCKED') {
604
+ if (this.queue.size() > MAX_OOO_DELTA_COUNT) {
605
+ this.triggerSync('queue too big, blocked on out-of-order delta');
606
+ } else {
607
+ this.processDeltaEvent();
608
+ }
609
+ } else if (this.status === 'IDLE') {
610
+ // Update status, ensure we only process one event at a time.
611
+ this.status = 'WORKING';
612
+
613
+ this.processDeltaEvent();
614
+ }
544
615
  }
545
616
  }
546
617
 
@@ -559,11 +630,55 @@ export default class Parser {
559
630
  * @returns {undefined}
560
631
  */
561
632
  pause() {
562
- // @ts-ignore
563
- this.status = Parser.status.PAUSED;
633
+ this.status = 'PAUSED';
564
634
  LoggerProxy.logger.info('Locus-info:parser#pause --> Locus parser paused.');
565
635
  }
566
636
 
637
+ /**
638
+ * Triggers a sync with Locus
639
+ *
640
+ * @param {string} reason used just for logging
641
+ * @returns {undefined}
642
+ */
643
+ private triggerSync(reason: string) {
644
+ LoggerProxy.logger.info(`Locus-info:parser#triggerSync --> doing sync, reason: ${reason}`);
645
+ this.stopSyncTimer();
646
+ this.pause();
647
+ this.onDeltaAction(Parser.loci.DESYNC, this.workingCopy);
648
+ }
649
+
650
+ /**
651
+ * Starts a timer with a random delay. When that timer expires we will do a sync.
652
+ *
653
+ * The main purpose of this timer is to handle a case when we get some out-of-order deltas,
654
+ * so we start waiting to receive the missing delta. If that delta never arrives, this timer
655
+ * will trigger a sync with Locus.
656
+ *
657
+ * @returns {undefined}
658
+ */
659
+ private startSyncTimer() {
660
+ if (this.syncTimer === null) {
661
+ const timeout = OOO_DELTA_WAIT_TIME + Math.random() * OOO_DELTA_WAIT_TIME_RANDOM_DELAY;
662
+
663
+ this.syncTimer = setTimeout(() => {
664
+ this.syncTimer = null;
665
+ this.triggerSync('timer expired, blocked on out-of-order delta');
666
+ }, timeout);
667
+ }
668
+ }
669
+
670
+ /**
671
+ * Stops the timer for triggering a sync
672
+ *
673
+ * @returns {undefined}
674
+ */
675
+ private stopSyncTimer() {
676
+ if (this.syncTimer !== null) {
677
+ clearTimeout(this.syncTimer);
678
+ this.syncTimer = null;
679
+ }
680
+ }
681
+
567
682
  /**
568
683
  * Processes next locus delta in the queue,
569
684
  * continues until the queue is empty
@@ -571,11 +686,13 @@ export default class Parser {
571
686
  * @returns {undefined}
572
687
  */
573
688
  processDeltaEvent() {
574
- const {DESYNC, USE_INCOMING} = Parser.loci;
689
+ const {DESYNC, USE_INCOMING, WAIT} = Parser.loci;
575
690
  const {extractComparisonState: extract} = Parser;
576
691
  const newLoci = this.queue.dequeue();
577
692
 
578
693
  if (!this.isValidLocus(newLoci)) {
694
+ this.nextEvent();
695
+
579
696
  return;
580
697
  }
581
698
 
@@ -586,6 +703,8 @@ export default class Parser {
586
703
  // for full debugging.
587
704
  LoggerProxy.logger.debug(`Locus-info:parser#processDeltaEvent --> Locus Debug: ${result}`);
588
705
 
706
+ let needToWait = false;
707
+
589
708
  if (lociComparison === DESYNC) {
590
709
  // wait for desync response
591
710
  this.pause();
@@ -594,15 +713,39 @@ export default class Parser {
594
713
  // Note: The working copy of parser gets updated in .onFullLocus()
595
714
  // and here when USE_INCOMING locus.
596
715
  this.workingCopy = newLoci;
716
+ } else if (lociComparison === WAIT) {
717
+ // we've taken newLoci from the front of the queue, so put it back there as we have to wait
718
+ // for the one that should be in front of it, before we can process it
719
+ this.queue.enqueue(newLoci);
720
+ needToWait = true;
721
+ }
722
+
723
+ if (needToWait) {
724
+ this.status = 'BLOCKED';
725
+ this.startSyncTimer();
726
+ } else {
727
+ this.stopSyncTimer();
728
+
729
+ if (this.status === 'BLOCKED') {
730
+ // we are not blocked anymore
731
+ this.status = 'WORKING';
732
+
733
+ LoggerProxy.logger.info(
734
+ `Locus-info:parser#processDeltaEvent --> received delta that we were waiting for ${Parser.locus2string(
735
+ newLoci
736
+ )}, not blocked anymore`
737
+ );
738
+ }
597
739
  }
598
740
 
599
741
  if (this.onDeltaAction) {
600
742
  LoggerProxy.logger.info(
601
- `Locus-info:parser#processDeltaEvent --> Locus Delta Action: ${lociComparison}`
743
+ `Locus-info:parser#processDeltaEvent --> Locus Delta ${Parser.locus2string(
744
+ newLoci
745
+ )}, Action: ${lociComparison}`
602
746
  );
603
747
 
604
- // eslint-disable-next-line no-useless-call
605
- this.onDeltaAction.call(this, lociComparison, newLoci);
748
+ this.onDeltaAction(lociComparison, newLoci);
606
749
  }
607
750
 
608
751
  this.nextEvent();
@@ -614,8 +757,7 @@ export default class Parser {
614
757
  */
615
758
  resume() {
616
759
  LoggerProxy.logger.info('Locus-info:parser#resume --> Locus parser resumed.');
617
- // @ts-ignore
618
- this.status = Parser.status.WORKING;
760
+ this.status = 'WORKING';
619
761
  this.nextEvent();
620
762
  }
621
763
 
@@ -278,7 +278,7 @@ export class MuteState {
278
278
  this.state.server.localMute = this.type === AUDIO ? audioMuted : videoMuted;
279
279
 
280
280
  if (locus) {
281
- meeting.locusInfo.onDeltaLocus(locus);
281
+ meeting.locusInfo.handleLocusDelta(locus, meeting);
282
282
  }
283
283
 
284
284
  return locus;
@@ -18,7 +18,6 @@ import {
18
18
  HTTP_VERBS,
19
19
  LEAVE,
20
20
  LOCI,
21
- LOCUS,
22
21
  PARTICIPANT,
23
22
  PROVISIONAL_TYPE_DIAL_IN,
24
23
  PROVISIONAL_TYPE_DIAL_OUT,
@@ -383,62 +382,22 @@ export default class MeetingRequest extends StatelessWebexPlugin {
383
382
  }
384
383
 
385
384
  /**
386
- * Syns the missed delta event
385
+ * Sends a requests to get the latest locus DTO, it might be a full Locus or a delta, depending on the url provided
387
386
  * @param {Object} options
388
- * @param {boolean} options.desync flag to get partial or whole locus object
389
- * @param {String} options.syncUrl sync url to get ht elatest locus delta
390
- * @returns {Promise}
391
- */
392
- syncMeeting(options: {desync: boolean; syncUrl: string}) {
393
- /* eslint-disable no-else-return */
394
- const {desync} = options;
395
- let {syncUrl} = options;
396
-
397
- /* istanbul ignore else */
398
- if (desync) {
399
- // check for existing URL parameters
400
- syncUrl = syncUrl
401
- .concat(syncUrl.split('?')[1] ? '&' : '?')
402
- .concat(`${LOCUS.SYNCDEBUG}=${desync}`);
403
- }
404
-
405
- // @ts-ignore
406
- return this.request({
407
- method: HTTP_VERBS.GET,
408
- uri: syncUrl,
409
- }) // TODO: Handle if delta sync failed . Get the full locus object
410
- .catch((err) => {
411
- LoggerProxy.logger.error(
412
- `Meeting:request#syncMeeting --> Error syncing meeting, error ${err}`
413
- );
414
-
415
- return err;
416
- });
417
- }
418
-
419
- /**
420
- * Request to get the complete locus object
421
- * @param {Object} options
422
- * @param {boolean} options.desync flag to get partial or whole locus object
423
387
  * @param {String} options.locusUrl sync url to get ht elatest locus delta
424
388
  * @returns {Promise}
425
389
  */
426
- getFullLocus(options: {desync: boolean; locusUrl: string}) {
427
- let {locusUrl} = options;
428
- const {desync} = options;
429
-
430
- if (locusUrl) {
431
- if (desync) {
432
- locusUrl += `?${LOCUS.SYNCDEBUG}=${desync}`;
433
- }
390
+ getLocusDTO(options: {url: string}) {
391
+ const {url} = options;
434
392
 
393
+ if (url) {
435
394
  // @ts-ignore
436
395
  return this.request({
437
396
  method: HTTP_VERBS.GET,
438
- uri: locusUrl,
397
+ uri: url,
439
398
  }).catch((err) => {
440
399
  LoggerProxy.logger.error(
441
- `Meeting:request#getFullLocus --> Error getting full locus, error ${err}`
400
+ `Meeting:request#getLocusDTO --> Error getting latest locus, error ${err}`
442
401
  );
443
402
 
444
403
  return err;
@@ -529,7 +529,7 @@ const MeetingUtil = {
529
529
  const locus = response?.body?.locus;
530
530
 
531
531
  if (locus) {
532
- meeting.locusInfo.onDeltaLocus(locus);
532
+ meeting.locusInfo.handleLocusDelta(locus, meeting);
533
533
  }
534
534
 
535
535
  return response;
@@ -1,5 +1,5 @@
1
1
  import {assert} from '@webex/test-helper-chai';
2
- import SimpleQueue from '@webex/plugin-meetings/src/common/queue';
2
+ import SortedQueue from '@webex/plugin-meetings/src/common/queue';
3
3
 
4
4
  describe('common/queue', () => {
5
5
  let fifo = null;
@@ -11,7 +11,15 @@ describe('common/queue', () => {
11
11
  };
12
12
 
13
13
  beforeEach(() => {
14
- fifo = new SimpleQueue();
14
+ fifo = new SortedQueue((left, right) => {
15
+ if (left.text > right.text) {
16
+ return 1;
17
+ }
18
+ if (left.text < right.text) {
19
+ return -1;
20
+ }
21
+ return 0;
22
+ });
15
23
  });
16
24
 
17
25
  afterEach(() => {
@@ -66,4 +74,25 @@ describe('common/queue', () => {
66
74
  it('Returns null if the queue is empty.', () => {
67
75
  assert.equal(fifo.dequeue(), null);
68
76
  });
77
+
78
+ it('Implement lifo', () => {
79
+ const lifo = new SortedQueue((left, right) => {
80
+ if (left.text < right.text) {
81
+ return 1;
82
+ }
83
+ if (left.text > right.text) {
84
+ return -1;
85
+ }
86
+ return 0;
87
+ });
88
+
89
+ const item1 = {text: 'fake item1'};
90
+ const item2 = {text: 'fake item2'};
91
+
92
+ lifo.enqueue(item1);
93
+ lifo.enqueue(item2);
94
+
95
+ assert.equal(lifo.dequeue(), item2);
96
+ assert.equal(lifo.dequeue(), item1);
97
+ });
69
98
  });