@webex/plugin-meetings 3.12.0-next.2 → 3.12.0-next.21

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 (67) hide show
  1. package/dist/aiEnableRequest/index.js +1 -1
  2. package/dist/breakouts/breakout.js +1 -1
  3. package/dist/breakouts/index.js +1 -1
  4. package/dist/controls-options-manager/constants.js +11 -1
  5. package/dist/controls-options-manager/constants.js.map +1 -1
  6. package/dist/controls-options-manager/index.js +23 -21
  7. package/dist/controls-options-manager/index.js.map +1 -1
  8. package/dist/controls-options-manager/util.js +91 -0
  9. package/dist/controls-options-manager/util.js.map +1 -1
  10. package/dist/hashTree/constants.js +10 -1
  11. package/dist/hashTree/constants.js.map +1 -1
  12. package/dist/hashTree/hashTreeParser.js +56 -31
  13. package/dist/hashTree/hashTreeParser.js.map +1 -1
  14. package/dist/hashTree/utils.js +22 -0
  15. package/dist/hashTree/utils.js.map +1 -1
  16. package/dist/interpretation/index.js +1 -1
  17. package/dist/interpretation/siLanguage.js +1 -1
  18. package/dist/locus-info/index.js +51 -23
  19. package/dist/locus-info/index.js.map +1 -1
  20. package/dist/meeting/index.js +372 -292
  21. package/dist/meeting/index.js.map +1 -1
  22. package/dist/meeting/util.js +1 -0
  23. package/dist/meeting/util.js.map +1 -1
  24. package/dist/meetings/index.js +8 -9
  25. package/dist/meetings/index.js.map +1 -1
  26. package/dist/meetings/util.js +21 -2
  27. package/dist/meetings/util.js.map +1 -1
  28. package/dist/metrics/constants.js +5 -1
  29. package/dist/metrics/constants.js.map +1 -1
  30. package/dist/multistream/sendSlotManager.js +116 -2
  31. package/dist/multistream/sendSlotManager.js.map +1 -1
  32. package/dist/types/controls-options-manager/constants.d.ts +6 -1
  33. package/dist/types/hashTree/constants.d.ts +1 -0
  34. package/dist/types/hashTree/hashTreeParser.d.ts +12 -2
  35. package/dist/types/hashTree/utils.d.ts +11 -0
  36. package/dist/types/locus-info/index.d.ts +9 -5
  37. package/dist/types/meeting/index.d.ts +11 -0
  38. package/dist/types/metrics/constants.d.ts +4 -0
  39. package/dist/types/multistream/sendSlotManager.d.ts +23 -1
  40. package/dist/webinar/index.js +301 -226
  41. package/dist/webinar/index.js.map +1 -1
  42. package/package.json +16 -16
  43. package/src/controls-options-manager/constants.ts +14 -1
  44. package/src/controls-options-manager/index.ts +26 -19
  45. package/src/controls-options-manager/util.ts +81 -1
  46. package/src/hashTree/constants.ts +9 -0
  47. package/src/hashTree/hashTreeParser.ts +60 -36
  48. package/src/hashTree/utils.ts +17 -0
  49. package/src/locus-info/index.ts +56 -30
  50. package/src/meeting/index.ts +98 -11
  51. package/src/meeting/util.ts +1 -0
  52. package/src/meetings/index.ts +15 -16
  53. package/src/meetings/util.ts +26 -1
  54. package/src/metrics/constants.ts +5 -0
  55. package/src/multistream/sendSlotManager.ts +97 -3
  56. package/src/webinar/index.ts +75 -1
  57. package/test/unit/spec/controls-options-manager/index.js +114 -6
  58. package/test/unit/spec/controls-options-manager/util.js +165 -0
  59. package/test/unit/spec/hashTree/hashTreeParser.ts +441 -30
  60. package/test/unit/spec/hashTree/utils.ts +88 -1
  61. package/test/unit/spec/locus-info/index.js +75 -27
  62. package/test/unit/spec/meeting/index.js +54 -36
  63. package/test/unit/spec/meeting/utils.js +4 -0
  64. package/test/unit/spec/meetings/index.js +36 -3
  65. package/test/unit/spec/meetings/utils.js +108 -0
  66. package/test/unit/spec/multistream/sendSlotManager.ts +135 -36
  67. package/test/unit/spec/webinar/index.ts +60 -0
@@ -7,6 +7,7 @@ import {expect} from '@webex/test-helper-chai';
7
7
  import sinon from 'sinon';
8
8
  import {assert} from '@webex/test-helper-chai';
9
9
  import {EMPTY_HASH} from '@webex/plugin-meetings/src/hashTree/constants';
10
+ import { some } from 'lodash';
10
11
 
11
12
  const visibleDataSetsUrl = 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/visibleDataSets';
12
13
 
@@ -553,7 +554,7 @@ describe('HashTreeParser', () => {
553
554
  );
554
555
 
555
556
  // Verify callback was called with OBJECTS_UPDATED and correct updatedObjects list
556
- assert.calledWith(callback, LocusInfoUpdateType.OBJECTS_UPDATED, {
557
+ assert.calledWith(callback, {updateType: LocusInfoUpdateType.OBJECTS_UPDATED,
557
558
  updatedObjects: [
558
559
  {
559
560
  htMeta: {
@@ -596,6 +597,41 @@ describe('HashTreeParser', () => {
596
597
  });
597
598
  });
598
599
 
600
+ it('initializes "main" before "self" regardless of order from Locus', async () => {
601
+ const parser = createHashTreeParser({dataSets: [], locus: null}, null);
602
+
603
+ // Locus returns datasets in non-priority order: atd-active, main, self
604
+ const atdActiveDataSet = createDataSet('atd-active', 4, 500);
605
+ const mainDataSet = createDataSet('main', 16, 1100);
606
+ const selfDataSet = createDataSet('self', 1, 2100);
607
+
608
+ mockGetAllDataSetsMetadata(webexRequest, visibleDataSetsUrl, [
609
+ atdActiveDataSet,
610
+ mainDataSet,
611
+ selfDataSet,
612
+ ]);
613
+
614
+ mockSyncRequest(webexRequest, selfDataSet.url);
615
+ mockSyncRequest(webexRequest, mainDataSet.url);
616
+ mockSyncRequest(webexRequest, atdActiveDataSet.url);
617
+
618
+ await parser.initializeFromMessage({
619
+ dataSets: [],
620
+ visibleDataSetsUrl,
621
+ locusUrl,
622
+ });
623
+
624
+ // Verify sync requests were sent in priority order: main, self, then atd-active
625
+ const syncCalls = webexRequest
626
+ .getCalls()
627
+ .filter((call) => call.args[0]?.method === 'POST' && call.args[0]?.uri?.endsWith('/sync'));
628
+
629
+ expect(syncCalls).to.have.lengthOf(3);
630
+ expect(syncCalls[0].args[0].uri).to.equal(`${mainDataSet.url}/sync`);
631
+ expect(syncCalls[1].args[0].uri).to.equal(`${selfDataSet.url}/sync`);
632
+ expect(syncCalls[2].args[0].uri).to.equal(`${atdActiveDataSet.url}/sync`);
633
+ });
634
+
599
635
  it('handles sync response that has locusStateElements undefined', async () => {
600
636
  const minimalInitialLocus = {
601
637
  dataSets: [],
@@ -788,7 +824,7 @@ describe('HashTreeParser', () => {
788
824
  expect(parser.dataSets.self.version).to.equal(2100);
789
825
  expect(parser.dataSets['atd-unmuted'].version).to.equal(3100);
790
826
 
791
- assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED, {
827
+ assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.OBJECTS_UPDATED,
792
828
  updatedObjects: [
793
829
  {
794
830
  htMeta: {
@@ -861,6 +897,116 @@ describe('HashTreeParser', () => {
861
897
  });
862
898
  });
863
899
 
900
+ it('handles updates to control entries correctly', () => {
901
+ const parser = createHashTreeParser();
902
+
903
+ const mainPutItemsSpy = sinon.spy(parser.dataSets.main.hashTree, 'putItems');
904
+
905
+ // Create a locus update with new htMeta information for some things
906
+ const locusUpdate = {
907
+ dataSets: [
908
+ createDataSet('main', 16, 1100),
909
+ ],
910
+ locus: {
911
+ url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f',
912
+ htMeta: {
913
+ elementId: {
914
+ type: 'locus',
915
+ id: 0,
916
+ version: 200, // same version
917
+ },
918
+ dataSetNames: ['main'],
919
+ },
920
+ participants: [],
921
+ controls: {
922
+ lock: {
923
+ locked: true,
924
+ htMeta: {
925
+ elementId: {
926
+ type: 'ControlEntry',
927
+ id: 10100,
928
+ version: 100,
929
+ },
930
+ dataSetNames: ['main'],
931
+ },
932
+ },
933
+ stream: {
934
+ streaming: true,
935
+ htMeta: {
936
+ elementId: {
937
+ type: 'ControlEntry',
938
+ id: 10101,
939
+ version: 100,
940
+ },
941
+ dataSetNames: ['main'],
942
+ },
943
+ }
944
+ }
945
+ },
946
+ };
947
+
948
+ // Call handleLocusUpdate
949
+ parser.handleLocusUpdate(locusUpdate);
950
+
951
+ // Verify putItems was called on main hash tree with correct data
952
+ assert.calledOnceWithExactly(mainPutItemsSpy, [
953
+ {type: 'locus', id: 0, version: 200},
954
+ {type: 'ControlEntry', id: 10100, version: 100},
955
+ {type: 'ControlEntry', id: 10101, version: 100}
956
+ ]);
957
+
958
+ assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.OBJECTS_UPDATED,
959
+ updatedObjects: [
960
+ {
961
+ htMeta: {
962
+ elementId: {
963
+ type: 'ControlEntry',
964
+ id: 10100,
965
+ version: 100,
966
+ },
967
+ dataSetNames: ['main'],
968
+ },
969
+ data: {
970
+ lock: {
971
+ locked: true,
972
+ htMeta: {
973
+ elementId: {
974
+ type: 'ControlEntry',
975
+ id: 10100,
976
+ version: 100,
977
+ },
978
+ dataSetNames: ['main'],
979
+ },
980
+ },
981
+ },
982
+ },
983
+ {
984
+ htMeta: {
985
+ elementId: {
986
+ type: 'ControlEntry',
987
+ id: 10101,
988
+ version: 100,
989
+ },
990
+ dataSetNames: ['main'],
991
+ },
992
+ data: {
993
+ stream: {
994
+ streaming: true,
995
+ htMeta: {
996
+ elementId: {
997
+ type: 'ControlEntry',
998
+ id: 10101,
999
+ version: 100,
1000
+ },
1001
+ dataSetNames: ['main'],
1002
+ },
1003
+ },
1004
+ },
1005
+ }
1006
+ ],
1007
+ });
1008
+ });
1009
+
864
1010
  it('handles unknown datasets gracefully', () => {
865
1011
  const parser = createHashTreeParser();
866
1012
 
@@ -899,7 +1045,7 @@ describe('HashTreeParser', () => {
899
1045
  assert.calledOnceWithExactly(mainPutItemsSpy, [{type: 'locus', id: 0, version: 201}]);
900
1046
 
901
1047
  // Verify callback was called only for known dataset
902
- assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED, {
1048
+ assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.OBJECTS_UPDATED,
903
1049
  updatedObjects: [
904
1050
  {
905
1051
  htMeta: {
@@ -999,7 +1145,7 @@ describe('HashTreeParser', () => {
999
1145
  assert.calledOnceWithExactly(selfPutItemSpy, {type: 'metadata', id: 5, version: 51});
1000
1146
 
1001
1147
  // Verify callback was called with metadata object and removed dataset objects
1002
- assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED, {
1148
+ assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.OBJECTS_UPDATED,
1003
1149
  updatedObjects: [
1004
1150
  // updated metadata object:
1005
1151
  {
@@ -1160,7 +1306,7 @@ describe('HashTreeParser', () => {
1160
1306
  assert.notCalled(atdUnmutedPutItemsSpy);
1161
1307
 
1162
1308
  // Verify callback was called with the updated object
1163
- assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED, {
1309
+ assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.OBJECTS_UPDATED,
1164
1310
  updatedObjects: [
1165
1311
  {
1166
1312
  htMeta: {
@@ -1388,7 +1534,7 @@ describe('HashTreeParser', () => {
1388
1534
  ]);
1389
1535
 
1390
1536
  // Verify callback was called with OBJECTS_UPDATED and all updated objects
1391
- assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED, {
1537
+ assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.OBJECTS_UPDATED,
1392
1538
  updatedObjects: [
1393
1539
  {
1394
1540
  htMeta: {
@@ -1453,9 +1599,7 @@ describe('HashTreeParser', () => {
1453
1599
  parser.handleMessage(sentinelMessage, 'sentinel message');
1454
1600
 
1455
1601
  // Verify callback was called with MEETING_ENDED
1456
- assert.calledOnceWithExactly(callback, LocusInfoUpdateType.MEETING_ENDED, {
1457
- updatedObjects: undefined,
1458
- });
1602
+ assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.MEETING_ENDED});
1459
1603
 
1460
1604
  // Verify that all timers were stopped
1461
1605
  Object.values(parser.dataSets).forEach((ds: any) => {
@@ -1477,9 +1621,7 @@ describe('HashTreeParser', () => {
1477
1621
  parser.handleMessage(sentinelMessage, 'sentinel message');
1478
1622
 
1479
1623
  // Verify callback was called with MEETING_ENDED
1480
- assert.calledOnceWithExactly(callback, LocusInfoUpdateType.MEETING_ENDED, {
1481
- updatedObjects: undefined,
1482
- });
1624
+ assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.MEETING_ENDED});
1483
1625
 
1484
1626
  // Verify that all timers were stopped
1485
1627
  Object.values(parser.dataSets).forEach((ds: any) => {
@@ -1575,7 +1717,7 @@ describe('HashTreeParser', () => {
1575
1717
  );
1576
1718
 
1577
1719
  // Verify that callback was called with synced objects
1578
- assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED, {
1720
+ assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.OBJECTS_UPDATED,
1579
1721
  updatedObjects: [
1580
1722
  {
1581
1723
  htMeta: {
@@ -1637,9 +1779,7 @@ describe('HashTreeParser', () => {
1637
1779
  await clock.tickAsync(1000);
1638
1780
 
1639
1781
  // Verify callback was called with MEETING_ENDED
1640
- assert.calledOnceWithExactly(callback, LocusInfoUpdateType.MEETING_ENDED, {
1641
- updatedObjects: undefined,
1642
- });
1782
+ assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.MEETING_ENDED});
1643
1783
 
1644
1784
  // Verify all timers are stopped
1645
1785
  Object.values(parser.dataSets).forEach((ds: any) => {
@@ -1702,9 +1842,7 @@ describe('HashTreeParser', () => {
1702
1842
  await clock.tickAsync(1000);
1703
1843
 
1704
1844
  // Verify callback was called with MEETING_ENDED
1705
- assert.calledOnceWithExactly(callback, LocusInfoUpdateType.MEETING_ENDED, {
1706
- updatedObjects: undefined,
1707
- });
1845
+ assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.MEETING_ENDED});
1708
1846
 
1709
1847
  // Verify all timers are stopped
1710
1848
  Object.values(parser.dataSets).forEach((ds: any) => {
@@ -1942,7 +2080,7 @@ describe('HashTreeParser', () => {
1942
2080
  assert.equal(parser.dataSets.attendees.hashTree.numLeaves, 8);
1943
2081
 
1944
2082
  // Verify callback was called with the metadata update (appears twice - processed once for visible dataset changes, once in main loop)
1945
- assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED, {
2083
+ assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.OBJECTS_UPDATED,
1946
2084
  updatedObjects: [
1947
2085
  {
1948
2086
  htMeta: {
@@ -2062,6 +2200,98 @@ describe('HashTreeParser', () => {
2062
2200
  await checkAsyncDatasetInitialization(parser, newDataSet);
2063
2201
  });
2064
2202
 
2203
+ it('initializes new visible data sets in priority order', async () => {
2204
+ // Create a parser that only has "self" as visible (no "main")
2205
+ const initialLocusWithoutMain = {
2206
+ dataSets: [createDataSet('self', 1, 2000)],
2207
+ locus: {
2208
+ ...exampleInitialLocus.locus,
2209
+ },
2210
+ };
2211
+ const metadataWithoutMain = {
2212
+ ...exampleMetadata,
2213
+ visibleDataSets: [
2214
+ {
2215
+ name: 'self',
2216
+ url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/713e9f99/datasets/self',
2217
+ },
2218
+ ],
2219
+ };
2220
+ const parser = createHashTreeParser(initialLocusWithoutMain, metadataWithoutMain);
2221
+
2222
+ // Verify "main" is not visible initially
2223
+ expect(parser.visibleDataSets.some((vds) => vds.name === 'main')).to.be.false;
2224
+
2225
+ // Stub updateItems on self hash tree to return true
2226
+ sinon.stub(parser.dataSets.self.hashTree, 'updateItems').returns([true]);
2227
+
2228
+ // Send a message that adds "main" and "atd-active" as new visible datasets.
2229
+ // Neither has info in dataSets, so both require async initialization.
2230
+ const newMainDataSet = createDataSet('main', 16, 6000);
2231
+ const newAtdActiveDataSet = createDataSet('atd-active', 4, 7000);
2232
+
2233
+ const message = {
2234
+ dataSets: [createDataSet('self', 1, 2100)],
2235
+ visibleDataSetsUrl,
2236
+ locusUrl,
2237
+ locusStateElements: [
2238
+ {
2239
+ htMeta: {
2240
+ elementId: {
2241
+ type: 'metadata' as const,
2242
+ id: 5,
2243
+ version: 51,
2244
+ },
2245
+ dataSetNames: ['self'],
2246
+ },
2247
+ data: {
2248
+ visibleDataSets: [
2249
+ {
2250
+ name: 'self',
2251
+ url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/713e9f99/datasets/self',
2252
+ },
2253
+ // listed in non-priority order: atd-active before main
2254
+ {name: 'atd-active', url: newAtdActiveDataSet.url},
2255
+ {name: 'main', url: newMainDataSet.url},
2256
+ ],
2257
+ },
2258
+ },
2259
+ ],
2260
+ };
2261
+
2262
+ // Mock getAllVisibleDataSetsFromLocus to return both new datasets (in non-priority order)
2263
+ mockGetAllDataSetsMetadata(webexRequest, visibleDataSetsUrl, [
2264
+ newAtdActiveDataSet,
2265
+ newMainDataSet,
2266
+ ]);
2267
+ mockSyncRequest(webexRequest, newMainDataSet.url);
2268
+ mockSyncRequest(webexRequest, newAtdActiveDataSet.url);
2269
+
2270
+ parser.handleMessage(message, 'add main and atd-active datasets');
2271
+
2272
+ // Wait for the async initialization (queueMicrotask) to complete
2273
+ await clock.tickAsync(0);
2274
+
2275
+ // Verify both datasets are initialized
2276
+ expect(parser.dataSets.main?.hashTree).to.exist;
2277
+ expect(parser.dataSets['atd-active']?.hashTree).to.exist;
2278
+
2279
+ // Verify sync requests were sent in priority order: "main" before "atd-active",
2280
+ // even though atd-active was listed first in both the message and the Locus response
2281
+ const syncCalls = webexRequest
2282
+ .getCalls()
2283
+ .filter(
2284
+ (call) =>
2285
+ call.args[0]?.method === 'POST' &&
2286
+ call.args[0]?.uri?.endsWith('/sync') &&
2287
+ (call.args[0]?.uri?.includes('/main/') || call.args[0]?.uri?.includes('/atd-active/'))
2288
+ );
2289
+
2290
+ expect(syncCalls).to.have.lengthOf(2);
2291
+ expect(syncCalls[0].args[0].uri).to.equal(`${newMainDataSet.url}/sync`);
2292
+ expect(syncCalls[1].args[0].uri).to.equal(`${newAtdActiveDataSet.url}/sync`);
2293
+ });
2294
+
2065
2295
  it('emits MEETING_ENDED if async init of a new visible dataset fails with 404', async () => {
2066
2296
  const parser = createHashTreeParser();
2067
2297
 
@@ -2128,9 +2358,7 @@ describe('HashTreeParser', () => {
2128
2358
  await clock.tickAsync(0);
2129
2359
 
2130
2360
  // Verify callback was called with MEETING_ENDED
2131
- assert.calledOnceWithExactly(callback, LocusInfoUpdateType.MEETING_ENDED, {
2132
- updatedObjects: undefined,
2133
- });
2361
+ assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.MEETING_ENDED});
2134
2362
  });
2135
2363
 
2136
2364
  it('handles removal of visible data set', async () => {
@@ -2193,7 +2421,7 @@ describe('HashTreeParser', () => {
2193
2421
  assert.isUndefined(parser.dataSets['atd-unmuted'].timer);
2194
2422
 
2195
2423
  // Verify callback was called with the metadata update and the removed objects (metadata appears twice - processed once for dataset changes, once in main loop)
2196
- assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED, {
2424
+ assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.OBJECTS_UPDATED,
2197
2425
  updatedObjects: [
2198
2426
  {
2199
2427
  htMeta: {
@@ -2290,6 +2518,151 @@ describe('HashTreeParser', () => {
2290
2518
  // Verify callback was NOT called (no updates for non-visible datasets)
2291
2519
  assert.notCalled(callback);
2292
2520
  });
2521
+
2522
+ it('reports update for object that moves from removed visible dataset to new visible dataset even if version is unchanged', async () => {
2523
+ // The purpose of this test is to verify that when an object
2524
+ // moves from one visible dataset to another without version change,
2525
+ // the parser still reports it as an update.
2526
+ // Locus has some additional signalling for this - the "view" property in htMeta.elementId.
2527
+ // When a view changes, the contents of the object may change even if version doesn't.
2528
+ // HashTreeParser doesn't use the "view" property, because it doesn't need to -
2529
+ // the same functionality is achieved thanks to the fact that a new visible data set means
2530
+ // a new hash tree is created, so HashTreeParser still detects the change as new
2531
+ // object is added to the new hash tree.
2532
+
2533
+ // Setup: parser with visible datasets "self" and "unjoined"
2534
+ const unjoinedDataSet = createDataSet('unjoined', 4, 3000);
2535
+ const selfDataSet = createDataSet('self', 1, 2000);
2536
+
2537
+ // start with Locus that has "info" in both "unjoined" and "main" datasets,
2538
+ // but only "unjoined" is visible.
2539
+ const initialLocus = {
2540
+ dataSets: [selfDataSet, unjoinedDataSet],
2541
+ locus: {
2542
+ url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f',
2543
+ links: {resources: {visibleDataSets: {url: visibleDataSetsUrl}}},
2544
+ // info object in "unjoined" dataset with version 500
2545
+ info: {
2546
+ htMeta: {
2547
+ elementId: {
2548
+ type: 'info',
2549
+ id: 42,
2550
+ version: 500,
2551
+ view: ['unjoined'], // not used by our code, but here for completeness - that's what real Locus would send
2552
+ },
2553
+ dataSetNames: ['main', 'unjoined'],
2554
+ },
2555
+ someField: 'some-initial-value',
2556
+ },
2557
+ self: {
2558
+ htMeta: {
2559
+ elementId: {
2560
+ type: 'self',
2561
+ id: 4,
2562
+ version: 100,
2563
+ },
2564
+ dataSetNames: ['self'],
2565
+ },
2566
+ },
2567
+ },
2568
+ };
2569
+
2570
+ const metadata = {
2571
+ htMeta: {
2572
+ elementId: {
2573
+ type: 'metadata',
2574
+ id: 5,
2575
+ version: 50,
2576
+ },
2577
+ dataSetNames: ['self'],
2578
+ },
2579
+ visibleDataSets: [
2580
+ {name: 'self', url: selfDataSet.url},
2581
+ {name: 'unjoined', url: unjoinedDataSet.url},
2582
+ ],
2583
+ };
2584
+
2585
+ const parser = createHashTreeParser(initialLocus, metadata);
2586
+
2587
+ // Verify initial state: unjoined is visible and has the info object
2588
+ expect(parser.visibleDataSets.some((vds) => vds.name === 'unjoined')).to.be.true;
2589
+ assert.exists(parser.dataSets.unjoined.hashTree);
2590
+ assert.equal(parser.dataSets.unjoined.hashTree?.getItemVersion(42, 'info'), 500);
2591
+
2592
+ // Stub updateItems on self hash tree to return true for metadata update
2593
+ sinon.stub(parser.dataSets.self.hashTree, 'updateItems').returns([true]);
2594
+
2595
+ // Now send a message that:
2596
+ // 1. Changes visible datasets: removes "unjoined", adds "main"
2597
+ // 2. Contains the same info object (same id=42, same version=500) but we see the view from "main" dataset
2598
+ const mainDataSet = createDataSet('main', 16, 1000);
2599
+
2600
+ const message = {
2601
+ dataSets: [selfDataSet, mainDataSet],
2602
+ visibleDataSetsUrl,
2603
+ locusUrl,
2604
+ locusStateElements: [
2605
+ {
2606
+ htMeta: {
2607
+ elementId: {
2608
+ type: 'metadata' as const,
2609
+ id: 5,
2610
+ version: 51,
2611
+ },
2612
+ dataSetNames: ['self'],
2613
+ },
2614
+ data: {
2615
+ visibleDataSets: [
2616
+ {name: 'self', url: selfDataSet.url},
2617
+ {name: 'main', url: mainDataSet.url},
2618
+ // "unjoined" is no longer here
2619
+ ],
2620
+ },
2621
+ },
2622
+ {
2623
+ htMeta: {
2624
+ elementId: {
2625
+ type: 'info' as const,
2626
+ id: 42,
2627
+ version: 500, // same version as before
2628
+ view: ['main'], // now points to "main" instead of "unjoined"
2629
+ },
2630
+ dataSetNames: ['main', 'unjoined'], // still in both datasets, but only "main" is visible now
2631
+ },
2632
+ data: {someNewField: 'some-value'},
2633
+ },
2634
+ ],
2635
+ };
2636
+
2637
+ parser.handleMessage(message, 'visible dataset swap with same-version object');
2638
+
2639
+ // Verify "unjoined" is no longer visible and "main" is now visible
2640
+ expect(parser.visibleDataSets.some((vds) => vds.name === 'unjoined')).to.be.false;
2641
+ expect(parser.visibleDataSets.some((vds) => vds.name === 'main')).to.be.true;
2642
+
2643
+ // Verify the info object is now in the "main" hash tree
2644
+ assert.exists(parser.dataSets.main.hashTree);
2645
+ assert.equal(parser.dataSets.main.hashTree?.getItemVersion(42, 'info'), 500);
2646
+
2647
+ // The key assertion: callback should be called with the info object update even though
2648
+ // its version hasn't changed - because visible datasets changed (moved from unjoined to main)
2649
+ assert.calledOnce(callback);
2650
+ const callbackArgs = callback.firstCall.args[0];
2651
+ assert.equal(callbackArgs.updateType, LocusInfoUpdateType.OBJECTS_UPDATED);
2652
+
2653
+ // Should contain the info object update (with data)
2654
+ const infoUpdate = callbackArgs.updatedObjects.find(
2655
+ (obj) => obj.htMeta.elementId.type === 'info' && obj.htMeta.elementId.id === 42
2656
+ );
2657
+ assert.exists(infoUpdate);
2658
+ assert.deepEqual(infoUpdate.htMeta.elementId, {
2659
+ type: 'info',
2660
+ id: 42,
2661
+ version: 500,
2662
+ view: ['main'],
2663
+ });
2664
+ assert.deepEqual(infoUpdate.data, {someNewField: 'some-value'});
2665
+ });
2293
2666
  });
2294
2667
 
2295
2668
  describe('heartbeat watchdog', () => {
@@ -2812,7 +3185,7 @@ describe('HashTreeParser', () => {
2812
3185
  parser.handleMessage(updateMessage, 'update with newer version');
2813
3186
 
2814
3187
  // Callback should be called with the update
2815
- assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED, {
3188
+ assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.OBJECTS_UPDATED,
2816
3189
  updatedObjects: [
2817
3190
  {
2818
3191
  htMeta: {
@@ -2883,7 +3256,7 @@ describe('HashTreeParser', () => {
2883
3256
  parser.handleMessage(removalMessage, 'removal of non-existent object');
2884
3257
 
2885
3258
  // Callback should be called with the removal
2886
- assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED, {
3259
+ assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.OBJECTS_UPDATED,
2887
3260
  updatedObjects: [
2888
3261
  {
2889
3262
  htMeta: {
@@ -3018,7 +3391,7 @@ describe('HashTreeParser', () => {
3018
3391
  parser.handleMessage(mixedMessage, 'mixed updates');
3019
3392
 
3020
3393
  // Callback should be called with only the valid updates (participant 1 v110 and participant 3 v10)
3021
- assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED, {
3394
+ assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.OBJECTS_UPDATED,
3022
3395
  updatedObjects: [
3023
3396
  {
3024
3397
  htMeta: {
@@ -3196,9 +3569,7 @@ describe('HashTreeParser', () => {
3196
3569
  parser.handleMessage(sentinelMessage as any, 'sentinel message');
3197
3570
 
3198
3571
  // Callback should be called with MEETING_ENDED
3199
- assert.calledOnceWithExactly(callback, LocusInfoUpdateType.MEETING_ENDED, {
3200
- updatedObjects: undefined,
3201
- });
3572
+ assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.MEETING_ENDED});
3202
3573
  });
3203
3574
  });
3204
3575
 
@@ -3557,4 +3928,44 @@ describe('HashTreeParser', () => {
3557
3928
  assert.notCalled(callback);
3558
3929
  });
3559
3930
  });
3931
+
3932
+ describe('#cleanUp', () => {
3933
+ it('should stop the parser, clear all timers and clear all dataSets', () => {
3934
+ const parser = createHashTreeParser();
3935
+
3936
+ // Send a message to set up sync timers via runSyncAlgorithm
3937
+ const message = {
3938
+ dataSets: [
3939
+ {
3940
+ ...createDataSet('main', 16, 1100),
3941
+ root: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1',
3942
+ },
3943
+ ],
3944
+ visibleDataSetsUrl,
3945
+ locusUrl,
3946
+ heartbeatIntervalMs: 5000,
3947
+ locusStateElements: [
3948
+ {
3949
+ htMeta: {
3950
+ elementId: {type: 'locus' as const, id: 0, version: 201},
3951
+ dataSetNames: ['main'],
3952
+ },
3953
+ data: {someData: 'value'},
3954
+ },
3955
+ ],
3956
+ };
3957
+
3958
+ parser.handleMessage(message, 'setup timers');
3959
+
3960
+ // Verify timers were set by handleMessage
3961
+ expect(parser.dataSets.main.timer).to.not.be.undefined;
3962
+ expect(parser.dataSets.main.heartbeatWatchdogTimer).to.not.be.undefined;
3963
+
3964
+ parser.cleanUp();
3965
+
3966
+ expect(parser.state).to.equal('stopped');
3967
+ expect(parser.visibleDataSets).to.deep.equal([]);
3968
+ expect(parser.dataSets).to.deep.equal({});
3969
+ });
3970
+ });
3560
3971
  });