@webex/plugin-meetings 3.8.1-web-workers-keepalive.1 → 3.9.0-webinar5k.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.
Files changed (87) hide show
  1. package/dist/breakouts/breakout.js +1 -1
  2. package/dist/breakouts/index.js +1 -1
  3. package/dist/constants.js +8 -2
  4. package/dist/constants.js.map +1 -1
  5. package/dist/hashTree/constants.js +23 -0
  6. package/dist/hashTree/constants.js.map +1 -0
  7. package/dist/hashTree/hashTree.js +516 -0
  8. package/dist/hashTree/hashTree.js.map +1 -0
  9. package/dist/hashTree/hashTreeParser.js +521 -0
  10. package/dist/hashTree/hashTreeParser.js.map +1 -0
  11. package/dist/interpretation/index.js +1 -1
  12. package/dist/interpretation/siLanguage.js +1 -1
  13. package/dist/locus-info/index.js +301 -59
  14. package/dist/locus-info/index.js.map +1 -1
  15. package/dist/meeting/brbState.js +14 -12
  16. package/dist/meeting/brbState.js.map +1 -1
  17. package/dist/meeting/index.js +110 -12
  18. package/dist/meeting/index.js.map +1 -1
  19. package/dist/meeting/muteState.js +2 -5
  20. package/dist/meeting/muteState.js.map +1 -1
  21. package/dist/meeting/request.js +19 -0
  22. package/dist/meeting/request.js.map +1 -1
  23. package/dist/meeting/request.type.js.map +1 -1
  24. package/dist/meeting/util.js +8 -11
  25. package/dist/meeting/util.js.map +1 -1
  26. package/dist/meetings/index.js +6 -2
  27. package/dist/meetings/index.js.map +1 -1
  28. package/dist/member/index.js.map +1 -1
  29. package/dist/member/types.js.map +1 -1
  30. package/dist/members/collection.js +13 -0
  31. package/dist/members/collection.js.map +1 -1
  32. package/dist/members/index.js +44 -23
  33. package/dist/members/index.js.map +1 -1
  34. package/dist/members/request.js +3 -3
  35. package/dist/members/request.js.map +1 -1
  36. package/dist/members/util.js +18 -6
  37. package/dist/members/util.js.map +1 -1
  38. package/dist/multistream/sendSlotManager.js +32 -2
  39. package/dist/multistream/sendSlotManager.js.map +1 -1
  40. package/dist/types/constants.d.ts +6 -0
  41. package/dist/types/hashTree/constants.d.ts +8 -0
  42. package/dist/types/hashTree/hashTree.d.ts +128 -0
  43. package/dist/types/hashTree/hashTreeParser.d.ts +152 -0
  44. package/dist/types/locus-info/index.d.ts +93 -3
  45. package/dist/types/meeting/brbState.d.ts +0 -1
  46. package/dist/types/meeting/index.d.ts +29 -3
  47. package/dist/types/meeting/request.d.ts +9 -1
  48. package/dist/types/meeting/request.type.d.ts +74 -0
  49. package/dist/types/meeting/util.d.ts +3 -3
  50. package/dist/types/member/types.d.ts +1 -0
  51. package/dist/types/members/collection.d.ts +6 -0
  52. package/dist/types/members/index.d.ts +15 -3
  53. package/dist/types/members/request.d.ts +1 -1
  54. package/dist/types/members/util.d.ts +5 -2
  55. package/dist/types/multistream/sendSlotManager.d.ts +16 -0
  56. package/dist/webinar/index.js +1 -1
  57. package/package.json +24 -23
  58. package/src/constants.ts +7 -0
  59. package/src/hashTree/constants.ts +12 -0
  60. package/src/hashTree/hashTree.ts +460 -0
  61. package/src/hashTree/hashTreeParser.ts +556 -0
  62. package/src/locus-info/index.ts +393 -58
  63. package/src/meeting/brbState.ts +9 -7
  64. package/src/meeting/index.ts +104 -6
  65. package/src/meeting/muteState.ts +2 -6
  66. package/src/meeting/request.ts +16 -0
  67. package/src/meeting/request.type.ts +64 -0
  68. package/src/meeting/util.ts +17 -20
  69. package/src/meetings/index.ts +17 -3
  70. package/src/member/index.ts +1 -0
  71. package/src/member/types.ts +1 -0
  72. package/src/members/collection.ts +11 -0
  73. package/src/members/index.ts +33 -7
  74. package/src/members/request.ts +2 -2
  75. package/src/members/util.ts +14 -3
  76. package/src/multistream/sendSlotManager.ts +34 -2
  77. package/test/unit/spec/hashTree/hashTree.ts +394 -0
  78. package/test/unit/spec/hashTree/hashTreeParser.ts +156 -0
  79. package/test/unit/spec/locus-info/index.js +506 -55
  80. package/test/unit/spec/meeting/brbState.ts +9 -9
  81. package/test/unit/spec/meeting/index.js +475 -42
  82. package/test/unit/spec/meeting/request.js +71 -0
  83. package/test/unit/spec/members/index.js +33 -10
  84. package/test/unit/spec/members/request.js +2 -2
  85. package/test/unit/spec/members/utils.js +27 -7
  86. package/test/unit/spec/multistream/sendSlotManager.ts +59 -0
  87. package/test/unit/spec/reachability/index.ts +3 -1
@@ -305,7 +305,7 @@ describe('plugin-meetings', () => {
305
305
  {state: newControls.rdcControl}
306
306
  );
307
307
  });
308
-
308
+
309
309
  it('should trigger the CONTROLS_POLLING_QA_CHANGED event when necessary', () => {
310
310
  locusInfo.controls = {};
311
311
  locusInfo.emitScoped = sinon.stub();
@@ -772,7 +772,7 @@ describe('plugin-meetings', () => {
772
772
  },
773
773
  };
774
774
  locusInfo.emitScoped = sinon.stub();
775
- locusInfo.updateParticipants({});
775
+ locusInfo.updateParticipants({}, []);
776
776
 
777
777
  // if this assertion fails, double-check the attributes used in
778
778
  // the updateParticipants function in locus-info/index.js
@@ -790,6 +790,7 @@ describe('plugin-meetings', () => {
790
790
  selfId: '2',
791
791
  hostId: '3',
792
792
  isReplace: undefined,
793
+ removedParticipantIds: [],
793
794
  }
794
795
  );
795
796
  // note: in a real use case, recordingId, selfId, and hostId would all be the same
@@ -814,7 +815,7 @@ describe('plugin-meetings', () => {
814
815
  };
815
816
 
816
817
  locusInfo.emitScoped = sinon.stub();
817
- locusInfo.updateParticipants({}, true);
818
+ locusInfo.updateParticipants({}, [], true);
818
819
 
819
820
  assert.calledWith(
820
821
  locusInfo.emitScoped,
@@ -830,6 +831,7 @@ describe('plugin-meetings', () => {
830
831
  selfId: '2',
831
832
  hostId: '3',
832
833
  isReplace: true,
834
+ removedParticipantIds: [],
833
835
  }
834
836
  );
835
837
  });
@@ -880,7 +882,7 @@ describe('plugin-meetings', () => {
880
882
  ];
881
883
 
882
884
  locusInfo.emitScoped = sinon.stub();
883
- locusInfo.updateParticipants(failureParticipant);
885
+ locusInfo.updateParticipants(failureParticipant, []);
884
886
  assert.calledWith(
885
887
  locusInfo.emitScoped,
886
888
  {
@@ -2061,7 +2063,7 @@ describe('plugin-meetings', () => {
2061
2063
 
2062
2064
  fakeLocus = {
2063
2065
  meeting: true,
2064
- participants: true,
2066
+ participants: [],
2065
2067
  url: 'newLocusUrl',
2066
2068
  syncUrl: 'newSyncUrl',
2067
2069
  };
@@ -2108,6 +2110,38 @@ describe('plugin-meetings', () => {
2108
2110
  assert.isFunction(locusParser.onDeltaAction);
2109
2111
  });
2110
2112
 
2113
+ it("#updateLocusInfo invokes updateLocusUrl before updateMeetingInfo", () => {
2114
+ const callOrder = [];
2115
+ sinon.stub(locusInfo, "updateControls");
2116
+ sinon.stub(locusInfo, "updateConversationUrl");
2117
+ sinon.stub(locusInfo, "updateCreated");
2118
+ sinon.stub(locusInfo, "updateFullState");
2119
+ sinon.stub(locusInfo, "updateHostInfo");
2120
+ sinon.stub(locusInfo, "updateMeetingInfo").callsFake(() => {
2121
+ callOrder.push("updateMeetingInfo");
2122
+ });
2123
+ sinon.stub(locusInfo, "updateMediaShares");
2124
+ sinon.stub(locusInfo, "updateParticipantsUrl");
2125
+ sinon.stub(locusInfo, "updateReplace");
2126
+ sinon.stub(locusInfo, "updateSelf");
2127
+ sinon.stub(locusInfo, "updateLocusUrl").callsFake(() => {
2128
+ callOrder.push("updateLocusUrl");
2129
+ });
2130
+ sinon.stub(locusInfo, "updateAclUrl");
2131
+ sinon.stub(locusInfo, "updateBasequence");
2132
+ sinon.stub(locusInfo, "updateSequence");
2133
+ sinon.stub(locusInfo, "updateMemberShip");
2134
+ sinon.stub(locusInfo, "updateIdentifiers");
2135
+ sinon.stub(locusInfo, "updateEmbeddedApps");
2136
+ sinon.stub(locusInfo, "updateResources");
2137
+ sinon.stub(locusInfo, "compareAndUpdate");
2138
+
2139
+ locusInfo.updateLocusInfo(locus);
2140
+
2141
+ // Ensure updateLocusUrl is called before updateMeetingInfo if both are called
2142
+ assert.deepEqual(callOrder, ['updateLocusUrl', 'updateMeetingInfo']);
2143
+ });
2144
+
2111
2145
  it('#updateLocusInfo ignores breakout LEFT message', () => {
2112
2146
  const newLocus = {
2113
2147
  self: {
@@ -2159,6 +2193,8 @@ describe('plugin-meetings', () => {
2159
2193
  assert.notCalled(locusInfo.compareAndUpdate);
2160
2194
  });
2161
2195
 
2196
+
2197
+
2162
2198
  it('onFullLocus() updates the working-copy of locus parser', () => {
2163
2199
  const eventType = 'fakeEvent';
2164
2200
 
@@ -2257,7 +2293,7 @@ describe('plugin-meetings', () => {
2257
2293
 
2258
2294
  it('applyLocusDeltaData gets delta locus on DESYNC action if we have a syncUrl', () => {
2259
2295
  const {DESYNC} = LocusDeltaParser.loci;
2260
- const fakeDeltaLocus = {id: 'fake delta locus'};
2296
+ const fakeDeltaLocus = {baseSequence: {}, id: 'fake delta locus'};
2261
2297
  const meeting = {
2262
2298
  meetingRequest: {
2263
2299
  getLocusDTO: sandbox.stub().resolves({body: fakeDeltaLocus}),
@@ -2392,25 +2428,22 @@ describe('plugin-meetings', () => {
2392
2428
  };
2393
2429
  });
2394
2430
 
2395
- it('applyLocusDeltaData gets full locus on DESYNC action if we do not have a syncUrl and destroys the meeting if that fails', () => {
2431
+ it('applyLocusDeltaData gets full locus on DESYNC action if we do not have a syncUrl and destroys the meeting if that fails', async () => {
2396
2432
  meeting.meetingRequest.getLocusDTO.rejects(new Error('fake error'));
2397
2433
 
2398
2434
  locusInfo.locusParser.workingCopy = {}; // no syncUrl
2399
2435
 
2400
- // Since we have a promise inside a function we want to test that's not returned,
2401
- // we will wait and stub it's last function to resolve this waiting promise.
2402
- return new Promise((resolve) => {
2403
- webex.meetings.destroy.callsFake(() => resolve());
2404
- locusInfo.applyLocusDeltaData(DESYNC, fakeLocus, meeting);
2405
- }).then(() => {
2406
- assert.calledOnceWithExactly(meeting.meetingRequest.getLocusDTO, {url: 'fullSyncUrl'});
2436
+ locusInfo.applyLocusDeltaData(DESYNC, fakeLocus, meeting);
2407
2437
 
2408
- assert.notCalled(meeting.locusInfo.handleLocusDelta);
2409
- assert.notCalled(meeting.locusInfo.onFullLocus);
2410
- assert.notCalled(locusInfo.locusParser.resume);
2438
+ await testUtils.flushPromises();
2411
2439
 
2412
- assert.calledOnceWithExactly(webex.meetings.destroy, meeting, 'LOCUS_DTO_SYNC_FAILED');
2413
- });
2440
+ assert.calledOnceWithExactly(meeting.meetingRequest.getLocusDTO, {url: 'fullSyncUrl'});
2441
+
2442
+ assert.notCalled(meeting.locusInfo.handleLocusDelta);
2443
+ assert.notCalled(meeting.locusInfo.onFullLocus);
2444
+ assert.notCalled(locusInfo.locusParser.resume);
2445
+
2446
+ assert.calledOnceWithExactly(webex.meetings.destroy, meeting, 'LOCUS_DTO_SYNC_FAILED');
2414
2447
  });
2415
2448
 
2416
2449
  it('applyLocusDeltaData first tries a delta sync on DESYNC action and if that fails, does a full locus sync', () => {
@@ -2447,44 +2480,67 @@ describe('plugin-meetings', () => {
2447
2480
  });
2448
2481
  });
2449
2482
 
2450
- it('applyLocusDeltaData destroys the meeting if both delta sync and full sync fail', () => {
2483
+ it('applyLocusDeltaData first tries a delta sync on DESYNC action and if that fails with 403, it does not do a full locus sync', async () => {
2484
+ const fake403Error = new Error('fake error');
2485
+ fake403Error.statusCode = 403;
2486
+
2487
+ meeting.meetingRequest.getLocusDTO.onCall(0).rejects(fake403Error);
2488
+
2489
+ locusInfo.applyLocusDeltaData(DESYNC, fakeLocus, meeting);
2490
+
2491
+ await testUtils.flushPromises();
2492
+
2493
+ assert.calledOnceWithExactly(meeting.meetingRequest.getLocusDTO, {url: 'deltaSyncUrl'});
2494
+
2495
+ assert.calledWith(sendBehavioralMetricStub, 'js_sdk_locus_delta_sync_failed', {
2496
+ correlationId: meeting.correlationId,
2497
+ url: 'deltaSyncUrl',
2498
+ reason: 'fake error',
2499
+ errorName: 'Error',
2500
+ stack: sinon.match.any,
2501
+ code: sinon.match.any,
2502
+ });
2503
+
2504
+ assert.notCalled(meeting.locusInfo.handleLocusDelta);
2505
+ assert.notCalled(meeting.locusInfo.onFullLocus);
2506
+ assert.notCalled(locusInfo.locusParser.resume);
2507
+ });
2508
+
2509
+ it('applyLocusDeltaData destroys the meeting if both delta sync and full sync fail', async () => {
2451
2510
  meeting.meetingRequest.getLocusDTO.rejects(new Error('fake error'));
2452
2511
 
2453
- // Since we have a promise inside a function we want to test that's not returned,
2454
- // we will wait and stub it's last function to resolve this waiting promise.
2455
- return new Promise((resolve) => {
2456
- webex.meetings.destroy.callsFake(() => resolve());
2457
- locusInfo.applyLocusDeltaData(DESYNC, fakeLocus, meeting);
2458
- }).then(() => {
2459
- assert.calledTwice(meeting.meetingRequest.getLocusDTO);
2512
+ locusInfo.applyLocusDeltaData(DESYNC, fakeLocus, meeting);
2460
2513
 
2461
- assert.deepEqual(meeting.meetingRequest.getLocusDTO.getCalls()[0].args, [
2462
- {url: 'deltaSyncUrl'},
2463
- ]);
2464
- assert.deepEqual(meeting.meetingRequest.getLocusDTO.getCalls()[1].args, [
2465
- {url: 'fullSyncUrl'},
2466
- ]);
2514
+ await testUtils.flushPromises();
2467
2515
 
2468
- assert.calledWith(sendBehavioralMetricStub, 'js_sdk_locus_delta_sync_failed', {
2469
- correlationId: meeting.correlationId,
2470
- url: 'deltaSyncUrl',
2471
- reason: 'fake error',
2472
- errorName: 'Error',
2473
- stack: sinon.match.any,
2474
- code: sinon.match.any,
2475
- });
2516
+ assert.calledTwice(meeting.meetingRequest.getLocusDTO);
2476
2517
 
2477
- assert.notCalled(meeting.locusInfo.handleLocusDelta);
2478
- assert.notCalled(meeting.locusInfo.onFullLocus);
2479
- assert.notCalled(locusInfo.locusParser.resume);
2518
+ assert.deepEqual(meeting.meetingRequest.getLocusDTO.getCalls()[0].args, [
2519
+ {url: 'deltaSyncUrl'},
2520
+ ]);
2521
+ assert.deepEqual(meeting.meetingRequest.getLocusDTO.getCalls()[1].args, [
2522
+ {url: 'fullSyncUrl'},
2523
+ ]);
2480
2524
 
2481
- assert.calledOnceWithExactly(webex.meetings.destroy, meeting, 'LOCUS_DTO_SYNC_FAILED');
2525
+ assert.calledWith(sendBehavioralMetricStub, 'js_sdk_locus_delta_sync_failed', {
2526
+ correlationId: meeting.correlationId,
2527
+ url: 'deltaSyncUrl',
2528
+ reason: 'fake error',
2529
+ errorName: 'Error',
2530
+ stack: sinon.match.any,
2531
+ code: sinon.match.any,
2482
2532
  });
2533
+
2534
+ assert.notCalled(meeting.locusInfo.handleLocusDelta);
2535
+ assert.notCalled(meeting.locusInfo.onFullLocus);
2536
+ assert.notCalled(locusInfo.locusParser.resume);
2537
+
2538
+ assert.calledOnceWithExactly(webex.meetings.destroy, meeting, 'LOCUS_DTO_SYNC_FAILED');
2483
2539
  });
2484
2540
  });
2485
2541
 
2486
2542
  it('onDeltaLocus handle delta data', () => {
2487
- fakeLocus.participants = {};
2543
+ fakeLocus.participants = [];
2488
2544
  const fakeBreakout = {
2489
2545
  sessionId: 'sessionId',
2490
2546
  groupId: 'groupId',
@@ -2501,17 +2557,15 @@ describe('plugin-meetings', () => {
2501
2557
  };
2502
2558
  locusInfo.updateParticipants = sinon.stub();
2503
2559
  locusInfo.onDeltaLocus(fakeLocus);
2504
- assert.calledWith(locusInfo.updateParticipants, {}, false);
2560
+ assert.calledWith(locusInfo.updateParticipants, [], undefined, false);
2505
2561
 
2506
2562
  fakeLocus.controls.breakout.sessionId = 'sessionId2';
2507
2563
  locusInfo.onDeltaLocus(fakeLocus);
2508
- assert.calledWith(locusInfo.updateParticipants, {}, true);
2564
+ assert.calledWith(locusInfo.updateParticipants, [], undefined, true);
2509
2565
  });
2510
2566
 
2511
2567
  it('onDeltaLocus merges delta participants with existing participants', () => {
2512
- const FAKE_DELTA_PARTICIPANTS = [
2513
- {id: '1111'}, {id: '2222'}
2514
- ]
2568
+ const FAKE_DELTA_PARTICIPANTS = [{id: '1111'}, {id: '2222'}];
2515
2569
  fakeLocus.participants = FAKE_DELTA_PARTICIPANTS;
2516
2570
 
2517
2571
  sinon.spy(locusInfo, 'mergeParticipants');
@@ -2519,8 +2573,86 @@ describe('plugin-meetings', () => {
2519
2573
  const existingParticipants = locusInfo.participants;
2520
2574
 
2521
2575
  locusInfo.onDeltaLocus(fakeLocus);
2522
- assert.calledOnceWithExactly(locusInfo.mergeParticipants, existingParticipants, FAKE_DELTA_PARTICIPANTS);
2523
- assert.calledWith(locusInfo.updateParticipants, FAKE_DELTA_PARTICIPANTS, false);
2576
+ assert.calledOnceWithExactly(
2577
+ locusInfo.mergeParticipants,
2578
+ existingParticipants,
2579
+ FAKE_DELTA_PARTICIPANTS
2580
+ );
2581
+ assert.calledWith(locusInfo.updateParticipants, FAKE_DELTA_PARTICIPANTS, undefined, false);
2582
+ });
2583
+
2584
+ [true, false].forEach((isDelta) =>
2585
+ it(`applyLocusDeltaData - handles empty ${
2586
+ isDelta ? 'delta' : 'full'
2587
+ } DTO in response`, async () => {
2588
+ const {DESYNC} = LocusDeltaParser.loci;
2589
+ const fakeFullLocusDto = {};
2590
+ const meeting = {
2591
+ meetingRequest: {
2592
+ getLocusDTO: sandbox.stub().resolves({body: fakeFullLocusDto}),
2593
+ },
2594
+ locusInfo: {
2595
+ onFullLocus: sandbox.stub(),
2596
+ handleLocusDelta: sandbox.stub(),
2597
+ },
2598
+ locusUrl: 'fake locus FULL url',
2599
+ };
2600
+
2601
+ sinon.stub(locusInfo.locusParser, 'resume').resolves();
2602
+
2603
+ if (isDelta) {
2604
+ locusInfo.locusParser.workingCopy = {syncUrl: 'fake locus DELTA url'};
2605
+ } else {
2606
+ locusInfo.locusParser.workingCopy = {}; // no syncUrl (to trigger FULL DTO request)
2607
+ }
2608
+
2609
+ await locusInfo.applyLocusDeltaData(DESYNC, fakeLocus, meeting);
2610
+
2611
+ await testUtils.flushPromises();
2612
+
2613
+ if (isDelta) {
2614
+ assert.calledOnceWithExactly(meeting.meetingRequest.getLocusDTO, {
2615
+ url: 'fake locus DELTA url',
2616
+ });
2617
+ } else {
2618
+ assert.calledOnceWithExactly(meeting.meetingRequest.getLocusDTO, {
2619
+ url: 'fake locus FULL url',
2620
+ });
2621
+ }
2622
+ assert.notCalled(meeting.locusInfo.handleLocusDelta);
2623
+ assert.notCalled(meeting.locusInfo.onFullLocus);
2624
+ assert.calledOnce(locusInfo.locusParser.resume);
2625
+ })
2626
+ );
2627
+
2628
+ it(`applyLocusDeltaData - handles the case when we get FULL DTO when we asked for DELTA DTO`, async () => {
2629
+ const {DESYNC} = LocusDeltaParser.loci;
2630
+ const fakeFullLocusDto = {someStuff: 'data'}; // non-empty DTO, without baseSequence
2631
+ const meeting = {
2632
+ meetingRequest: {
2633
+ getLocusDTO: sandbox.stub().resolves({body: fakeFullLocusDto}),
2634
+ },
2635
+ locusInfo: {
2636
+ onFullLocus: sandbox.stub(),
2637
+ handleLocusDelta: sandbox.stub(),
2638
+ },
2639
+ locusUrl: 'fake locus FULL url',
2640
+ };
2641
+
2642
+ sinon.stub(locusInfo.locusParser, 'resume').resolves();
2643
+
2644
+ locusInfo.locusParser.workingCopy = {syncUrl: 'fake locus DELTA url'};
2645
+
2646
+ await locusInfo.applyLocusDeltaData(DESYNC, fakeLocus, meeting);
2647
+
2648
+ await testUtils.flushPromises();
2649
+
2650
+ assert.calledOnceWithExactly(meeting.meetingRequest.getLocusDTO, {
2651
+ url: 'fake locus DELTA url',
2652
+ });
2653
+ assert.notCalled(meeting.locusInfo.handleLocusDelta);
2654
+ assert.calledOnceWithExactly(meeting.locusInfo.onFullLocus, fakeFullLocusDto);
2655
+ assert.calledOnce(locusInfo.locusParser.resume);
2524
2656
  });
2525
2657
  });
2526
2658
 
@@ -2936,8 +3068,8 @@ describe('plugin-meetings', () => {
2936
3068
 
2937
3069
  sinon.stub(locusInfo, 'updateParticipantDeltas');
2938
3070
  sinon.stub(locusInfo, 'updateParticipants');
2939
- sinon.stub(locusInfo, 'isMeetingActive'),
2940
- sinon.stub(locusInfo, 'handleOneOnOneEvent'),
3071
+ sinon.stub(locusInfo, 'isMeetingActive');
3072
+ sinon.stub(locusInfo, 'handleOneOnOneEvent');
2941
3073
  (updateLocusInfoStub = sinon.stub(locusInfo, 'updateLocusInfo'));
2942
3074
  syncRequestStub = sinon.stub().resolves({body: {}});
2943
3075
 
@@ -2958,6 +3090,7 @@ describe('plugin-meetings', () => {
2958
3090
  id: 'test person id',
2959
3091
  },
2960
3092
  },
3093
+ participants: [],
2961
3094
  });
2962
3095
 
2963
3096
  updateLocusInfoStub.resetHistory();
@@ -3106,6 +3239,7 @@ describe('plugin-meetings', () => {
3106
3239
  await testUtils.flushPromises();
3107
3240
 
3108
3241
  assert.calledOnceWithExactly(syncRequestStub, {url: mockMeeting.locusUrl});
3242
+ assert.calledOnce(updateLocusInfoStub);
3109
3243
  assert.calledOnceWithExactly(updateLocusInfoStub, fullLocus);
3110
3244
  });
3111
3245
 
@@ -3181,5 +3315,322 @@ describe('plugin-meetings', () => {
3181
3315
  assert.calledWith(updateLocusInfoStub.getCall(2), deltaEvents[7]);
3182
3316
  });
3183
3317
  });
3318
+
3319
+ describe('Hash trees - webinar 5k', () => {
3320
+ let mockFullLocus;
3321
+ let clock;
3322
+
3323
+ beforeEach(() => {
3324
+
3325
+ sinon.stub(Math, 'random').returns(0.5); // to make sure the backoff timer is predictable
3326
+
3327
+ mockFullLocus = {
3328
+ dataSets: [
3329
+ {
3330
+ url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/session/a73e9f2c/datasets/main',
3331
+ root: '9bb9d5a911a74d53a915b4dfbec7329f',
3332
+ version: 51118,
3333
+ leafCount: 16,
3334
+ name: 'main',
3335
+ idleMs : 5000,
3336
+ backoff : {
3337
+ maxMs : 500,
3338
+ exponent : 0.5
3339
+ },
3340
+ },
3341
+ {
3342
+ url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/session/a73e9f2c/participant/713e9f99/datasets/self',
3343
+ root: '5b8cc7ffda1346d2bfb1c0b60b8ab601',
3344
+ version: 89891,
3345
+ leafCount: 1,
3346
+ name: 'self',
3347
+ idleMs : 10000,
3348
+ backoff : {
3349
+ maxMs : 500,
3350
+ exponent : 0.5
3351
+ },
3352
+ },
3353
+ {
3354
+ url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/session/a73e9f2c/datasets/atd-unmuted',
3355
+ root: '9279d2e149da43a1b8e2cd7cbf77f9f0',
3356
+ version: 91277,
3357
+ leafCount: 16,
3358
+ name: 'atd-unmuted',
3359
+ idleMs : 15000,
3360
+ backoff : {
3361
+ maxMs : 500,
3362
+ exponent : 0.5
3363
+ },
3364
+ },
3365
+ ],
3366
+ locus: {
3367
+ url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f',
3368
+ htMeta: {
3369
+ elementId: {
3370
+ type: 'LOCUS',
3371
+ id: 0,
3372
+ version: 5678,
3373
+ },
3374
+ dataSetNames: ['main'],
3375
+ },
3376
+ participants: [
3377
+ {
3378
+ url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/11941033',
3379
+ person: {
3380
+ id: '111',
3381
+ name: '1st participant',
3382
+ },
3383
+ htMeta: {
3384
+ elementId: {
3385
+ type: 'PARTICIPANT',
3386
+ id: 2,
3387
+ version: 5678,
3388
+ },
3389
+ dataSetNames: ['atd-active', 'attendees', 'atd-unmuted'],
3390
+ },
3391
+ },
3392
+ {
3393
+ url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/11941034',
3394
+ person: {
3395
+ id: '222',
3396
+ name: '2nd participant',
3397
+ },
3398
+ htMeta: {
3399
+ elementId: {
3400
+ type: 'PARTICIPANT',
3401
+ id: 3,
3402
+ version: 5678,
3403
+ },
3404
+ dataSetNames: ['attendees'],
3405
+ },
3406
+ },
3407
+ ],
3408
+ self: {
3409
+ url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/11941033',
3410
+ visibleDataSets: ['main', 'self', 'atd-unmuted'],
3411
+ person: {
3412
+ id: '333',
3413
+ name: 'myself',
3414
+ },
3415
+ htMeta: {
3416
+ elementId: {
3417
+ type: 'SELF',
3418
+ id: 4,
3419
+ version: 5678,
3420
+ },
3421
+ dataSetNames: ['self'],
3422
+ },
3423
+ },
3424
+ },
3425
+ };
3426
+
3427
+ clock = sinon.useFakeTimers();
3428
+
3429
+ });
3430
+
3431
+ afterEach(() => {
3432
+ clock.restore();
3433
+ });
3434
+
3435
+ const getMaxBackoffTime = (dataSets) => {
3436
+ return Object.values(dataSets).reduce((max, dataSet) => {
3437
+ const maxBackOff = dataSet.idleMs + dataSet.backoff.maxMs;
3438
+ return Math.max(max, maxBackOff);
3439
+ }, 0);
3440
+ }
3441
+
3442
+ const waitForMaxPossibleBackoffTime = async (dataSets) => {
3443
+ const maxBackoffTime = getMaxBackoffTime(dataSets);
3444
+ clock.tick(maxBackoffTime);
3445
+ await testUtils.flushPromises();
3446
+ }
3447
+
3448
+ it('initializes hash trees correctly from initial full locus', () => {
3449
+ locusInfo.initialSetup(mockFullLocus.locus, mockFullLocus.dataSets);
3450
+
3451
+ // check that the hash tree parser is initialized correctly
3452
+ assert.isDefined(locusInfo.hashTreeParser);
3453
+ assert.deepEqual(Object.keys(locusInfo.hashTreeParser.dataSets), ['main', 'self', 'atd-unmuted']);
3454
+ assert.deepEqual(locusInfo.hashTreeParser.dataSets['main'].hashTree.getLeafData(0), [ { type: 'LOCUS', id: 0, version: 5678 } ]);
3455
+ assert.deepEqual(locusInfo.hashTreeParser.dataSets['self'].hashTree.getLeafData(0), [ { type: 'SELF', id: 4, version: 5678 } ]);
3456
+ assert.deepEqual(locusInfo.hashTreeParser.dataSets['atd-unmuted'].hashTree.getLeafData(2), [ { type: 'PARTICIPANT', id: 2, version: 5678 } ]);
3457
+
3458
+ // participant with id=3 is not part of any of our datasets, so should be undefined
3459
+ assert.deepEqual(locusInfo.hashTreeParser.dataSets['atd-unmuted'].hashTree.getLeafData(3), []);
3460
+ });
3461
+
3462
+ it('handles hash tree messages correctly', async () => {
3463
+ locusInfo.initialSetup(mockFullLocus.locus, mockFullLocus.dataSets);
3464
+
3465
+ // simulate a hash tree message for a participant
3466
+ locusInfo.parse(mockMeeting, {
3467
+ "locusUrl" : "https://locus.wbx2.com/locus/api/v1/loci/dbe6eeb9-49c7-4727-bdb1-1c59fa6e56d2",
3468
+ "locusSessionId" : "fe3d019c-08ca-0f21-919f-f2cc646030ae",
3469
+ "dataSets" : [ {
3470
+ "url" : "https://locus.wbx2.com/locus/api/v1/loci/dbe6eeb9-49c7-4727-bdb1-1c59fa6e56d2/session/fe3d019c-08ca-0f21-919f-f2cc646030ae/datasets/attendees",
3471
+ "name" : "attendees",
3472
+ "root" : "7cfc14bdae7909e6fe7acd37bebf66db",
3473
+ "version" : 4739733700975,
3474
+ "leafCount" : 16,
3475
+ "idleMs" : 5000,
3476
+ "backoff" : {
3477
+ "maxMs" : 500,
3478
+ "exponent" : 0.5
3479
+ }
3480
+ }, {
3481
+ "url" : "https://locus.wbx2.com/locus/api/v1/loci/dbe6eeb9-49c7-4727-bdb1-1c59fa6e56d2/session/fe3d019c-08ca-0f21-919f-f2cc646030ae/datasets/atd-active",
3482
+ "name" : "atd-unmuted",
3483
+ "root" : "178bff6e3344f551a811712c57a9eac3",
3484
+ "version" : 5738696122316,
3485
+ "leafCount" : 4,
3486
+ "idleMs" : 5000,
3487
+ "backoff" : {
3488
+ "maxMs" : 500,
3489
+ "exponent" : 0.5
3490
+ }
3491
+ } ],
3492
+ "locusStateElements" : [ {
3493
+ "htMeta" : {
3494
+ "elementId" : {
3495
+ "type" : "PARTICIPANT",
3496
+ "id" : 2,
3497
+ "version" : 5679
3498
+ },
3499
+ "dataSetNames" : [ "attendees", "atd-unmuted" ]
3500
+ },
3501
+ "data" : "{\"isCreator\":false,\"url\":\"https://locus.wbx2.com/locus/api/v1/loci/dbe6eeb9-49c7-4727-bdb1-1c59fa6e56d2/participant/18ef210c-234a-49ac-83d6-1803bc401bc9\",\"id\":\"18ef210c-234a-49ac-83d6-1803bc401bc9\",\"guest\":false,\"resourceGuest\":false,\"moderator\":false,\"panelist\":false}"
3502
+ }, {
3503
+ "htMeta" : {
3504
+ "elementId" : {
3505
+ "type" : "PARTICIPANT",
3506
+ "id" : 3,
3507
+ "version" : 999999
3508
+ },
3509
+ "dataSetNames" : [ "attendees" ]
3510
+ },
3511
+ "data" : "{\"isCreator\":false,\"url\":\"https://locus.wbx2.com/locus/api/v1/loci/dbe6eeb9-49c7-4727-bdb1-1c59fa6e56d2/participant/29c0751a-7ada-40a1-94a4-eb5f5c80c863\",\"id\":\"29c0751a-7ada-40a1-94a4-eb5f5c80c863\",\"guest\":false,\"resourceGuest\":false,\"moderator\":false,\"panelist\":false}"
3512
+ } ]
3513
+ });
3514
+
3515
+ // main and self should be unchanged
3516
+ assert.deepEqual(Object.keys(locusInfo.hashTreeParser.dataSets), ['main', 'self', 'atd-unmuted']);
3517
+ assert.deepEqual(locusInfo.hashTreeParser.dataSets['main'].hashTree.getLeafData(0), [ { type: 'LOCUS', id: 0, version: 5678 } ]);
3518
+ assert.deepEqual(locusInfo.hashTreeParser.dataSets['self'].hashTree.getLeafData(0), [ { type: 'SELF', id: 4, version: 5678 } ]);
3519
+
3520
+ // participant should be updated
3521
+ assert.deepEqual(locusInfo.hashTreeParser.dataSets['atd-unmuted'].hashTree.getLeafData(2), [ { type: 'PARTICIPANT', id: 2, version: 5679 } ]);
3522
+
3523
+ // there should be no requests to Locus sent
3524
+ await waitForMaxPossibleBackoffTime(locusInfo.hashTreeParser.dataSets);
3525
+ assert.notCalled(webex.request);
3526
+ });
3527
+
3528
+ it('does a sync if hashes don\'t match after a timer fires', async () => {
3529
+ const atdUnmutedDataSetUrl = mockFullLocus.dataSets[2].url;
3530
+
3531
+ locusInfo.initialSetup(mockFullLocus.locus, mockFullLocus.dataSets);
3532
+
3533
+ // simulate a hash tree message for a participant
3534
+ locusInfo.parse(mockMeeting, {
3535
+ "locusUrl" : "https://locus.wbx2.com/locus/api/v1/loci/dbe6eeb9-49c7-4727-bdb1-1c59fa6e56d2",
3536
+ "locusSessionId" : "fe3d019c-08ca-0f21-919f-f2cc646030ae",
3537
+ "dataSets" : [ {
3538
+ "url" : atdUnmutedDataSetUrl,
3539
+ "name" : "atd-unmuted",
3540
+ "root" : "deadbeef", // wrong to trigger a sync
3541
+ "version" : 5738696122316,
3542
+ "leafCount" : 4,
3543
+ "idleMs" : 25000,
3544
+ "backoff" : {
3545
+ "maxMs" : 500,
3546
+ "exponent" : 0.5
3547
+ }
3548
+ } ],
3549
+ "locusStateElements" : [ {
3550
+ "htMeta" : {
3551
+ "elementId" : {
3552
+ "type" : "PARTICIPANT",
3553
+ "id" : 2,
3554
+ "version" : 5679
3555
+ },
3556
+ "dataSetNames" : [ "attendees", "atd-unmuted" ]
3557
+ },
3558
+ "data" : "{\"isCreator\":false,\"url\":\"https://locus.wbx2.com/locus/api/v1/loci/dbe6eeb9-49c7-4727-bdb1-1c59fa6e56d2/participant/18ef210c-234a-49ac-83d6-1803bc401bc9\",\"id\":\"18ef210c-234a-49ac-83d6-1803bc401bc9\",\"guest\":false,\"resourceGuest\":false,\"moderator\":false,\"panelist\":false}"
3559
+ }]
3560
+ });
3561
+
3562
+ // main and self should be unchanged
3563
+ assert.deepEqual(Object.keys(locusInfo.hashTreeParser.dataSets), ['main', 'self', 'atd-unmuted']);
3564
+ assert.deepEqual(locusInfo.hashTreeParser.dataSets['main'].hashTree.getLeafData(0), [ { type: 'LOCUS', id: 0, version: 5678 } ]);
3565
+ assert.deepEqual(locusInfo.hashTreeParser.dataSets['self'].hashTree.getLeafData(0), [ { type: 'SELF', id: 4, version: 5678 } ]);
3566
+
3567
+ // participant should be updated
3568
+ assert.deepEqual(locusInfo.hashTreeParser.dataSets['atd-unmuted'].hashTree.getLeafData(2), [ { type: 'PARTICIPANT', id: 2, version: 5679 } ]);
3569
+
3570
+ await testUtils.flushPromises();
3571
+
3572
+ // the root hash doesn't match, but we shouldn't send a request to Locus for hashes just yet
3573
+ assert.notCalled(webex.request);
3574
+
3575
+ webex.request.callsFake(async (options) => {
3576
+ if (options?.method === 'GET' && options?.uri?.endsWith('/hashtree')) {
3577
+ return {
3578
+ body: {
3579
+ hashes: [
3580
+ '178bff6e3344f551a811712c57a9eac3',
3581
+ 'b113a76304e3a7121afecfe1606ee1c1',
3582
+ 'ae70773ebb3be3653209648071b9bdad',
3583
+ '99aa06d3014798d86001c324468d497f',
3584
+ '99aa06d3014798d86001c324468d497f',
3585
+ 'deadbeef', // wrong hash that should cause the participant with id=2 to be deemed out of sync
3586
+ '99aa06d3014798d86001c324468d497f'
3587
+ ]
3588
+ }
3589
+ };
3590
+ } else if (options?.method === 'POST' && options?.uri?.endsWith('/sync')) {
3591
+ return {
3592
+ body: {},
3593
+ statusCode: 202,
3594
+ };
3595
+ }
3596
+ return {};
3597
+ });
3598
+
3599
+ // only after timeout there should be requests to get hashes and sync sent to Locus
3600
+ await waitForMaxPossibleBackoffTime(locusInfo.hashTreeParser.dataSets);
3601
+ assert.calledTwice(webex.request);
3602
+
3603
+ const firstCallArgs = webex.request.getCall(0).args[0];
3604
+ const secondCallArgs = webex.request.getCall(1).args[0];
3605
+
3606
+ assert.deepEqual(firstCallArgs, {method: 'GET', uri: `${atdUnmutedDataSetUrl}/hashtree`});
3607
+
3608
+ assert.deepEqual(secondCallArgs, {method: 'POST', uri: `${atdUnmutedDataSetUrl}/sync`,
3609
+ body: {
3610
+ dataSet: {
3611
+ name: 'atd-unmuted',
3612
+ leafCount: 16,
3613
+ root: '178bff6e3344f551a811712c57a9eac3',
3614
+ },
3615
+ leafDataEntries: [{
3616
+ leafIndex: 2,
3617
+ elementIds: [
3618
+ {
3619
+ id: 2,
3620
+ type: "PARTICIPANT",
3621
+ version: 5679
3622
+ }
3623
+ ],
3624
+ }]
3625
+ }
3626
+ });
3627
+ });
3628
+
3629
+ it('handles end meeting message correctly', async () => {
3630
+ });
3631
+ it('handles heartbeat messages correctly', async () => {
3632
+ });
3633
+
3634
+ });
3184
3635
  });
3185
3636
  });