@webex/plugin-meetings 3.11.0-next.2 → 3.11.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 (64) hide show
  1. package/dist/breakouts/breakout.js +1 -1
  2. package/dist/breakouts/index.js +1 -1
  3. package/dist/hashTree/hashTree.js +18 -0
  4. package/dist/hashTree/hashTree.js.map +1 -1
  5. package/dist/hashTree/hashTreeParser.js +307 -139
  6. package/dist/hashTree/hashTreeParser.js.map +1 -1
  7. package/dist/hashTree/types.js +2 -1
  8. package/dist/hashTree/types.js.map +1 -1
  9. package/dist/hashTree/utils.js +10 -0
  10. package/dist/hashTree/utils.js.map +1 -1
  11. package/dist/interpretation/index.js +1 -1
  12. package/dist/interpretation/siLanguage.js +1 -1
  13. package/dist/locus-info/index.js +55 -42
  14. package/dist/locus-info/index.js.map +1 -1
  15. package/dist/media/MediaConnectionAwaiter.js +57 -1
  16. package/dist/media/MediaConnectionAwaiter.js.map +1 -1
  17. package/dist/media/properties.js +4 -2
  18. package/dist/media/properties.js.map +1 -1
  19. package/dist/meeting/index.js +33 -22
  20. package/dist/meeting/index.js.map +1 -1
  21. package/dist/meeting/util.js +108 -2
  22. package/dist/meeting/util.js.map +1 -1
  23. package/dist/meetings/index.js +76 -26
  24. package/dist/meetings/index.js.map +1 -1
  25. package/dist/metrics/constants.js +2 -1
  26. package/dist/metrics/constants.js.map +1 -1
  27. package/dist/multistream/mediaRequestManager.js +1 -1
  28. package/dist/multistream/mediaRequestManager.js.map +1 -1
  29. package/dist/reactions/reactions.type.js.map +1 -1
  30. package/dist/types/hashTree/hashTree.d.ts +7 -0
  31. package/dist/types/hashTree/hashTreeParser.d.ts +47 -12
  32. package/dist/types/hashTree/types.d.ts +1 -0
  33. package/dist/types/hashTree/utils.d.ts +6 -0
  34. package/dist/types/locus-info/index.d.ts +9 -2
  35. package/dist/types/media/MediaConnectionAwaiter.d.ts +10 -1
  36. package/dist/types/media/properties.d.ts +2 -1
  37. package/dist/types/meeting/index.d.ts +8 -5
  38. package/dist/types/meeting/util.d.ts +28 -0
  39. package/dist/types/meetings/index.d.ts +3 -1
  40. package/dist/types/metrics/constants.d.ts +1 -0
  41. package/dist/types/reactions/reactions.type.d.ts +1 -0
  42. package/dist/webinar/index.js +1 -1
  43. package/package.json +22 -22
  44. package/src/hashTree/hashTree.ts +17 -0
  45. package/src/hashTree/hashTreeParser.ts +294 -96
  46. package/src/hashTree/types.ts +1 -0
  47. package/src/hashTree/utils.ts +9 -0
  48. package/src/locus-info/index.ts +83 -35
  49. package/src/media/MediaConnectionAwaiter.ts +41 -1
  50. package/src/media/properties.ts +3 -1
  51. package/src/meeting/index.ts +24 -11
  52. package/src/meeting/util.ts +132 -1
  53. package/src/meetings/index.ts +93 -8
  54. package/src/metrics/constants.ts +1 -0
  55. package/src/multistream/mediaRequestManager.ts +1 -1
  56. package/src/reactions/reactions.type.ts +1 -0
  57. package/test/unit/spec/hashTree/hashTree.ts +66 -0
  58. package/test/unit/spec/hashTree/hashTreeParser.ts +942 -110
  59. package/test/unit/spec/locus-info/index.js +88 -17
  60. package/test/unit/spec/media/MediaConnectionAwaiter.ts +41 -1
  61. package/test/unit/spec/media/properties.ts +12 -3
  62. package/test/unit/spec/meeting/index.js +160 -2
  63. package/test/unit/spec/meeting/utils.js +294 -22
  64. package/test/unit/spec/meetings/index.js +594 -17
@@ -5,6 +5,9 @@ import HashTree from '@webex/plugin-meetings/src/hashTree/hashTree';
5
5
  import {expect} from '@webex/test-helper-chai';
6
6
  import sinon from 'sinon';
7
7
  import {assert} from '@webex/test-helper-chai';
8
+ import {EMPTY_HASH} from '@webex/plugin-meetings/src/hashTree/constants';
9
+
10
+ const visibleDataSetsUrl = 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/visibleDataSets';
8
11
 
9
12
  const exampleInitialLocus = {
10
13
  dataSets: [
@@ -46,6 +49,7 @@ const exampleInitialLocus = {
46
49
  },
47
50
  dataSetNames: ['main'],
48
51
  },
52
+ links: {resources: {visibleDataSets: {url: visibleDataSetsUrl}}},
49
53
  participants: [
50
54
  {
51
55
  url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/11941033',
@@ -62,7 +66,6 @@ const exampleInitialLocus = {
62
66
  ],
63
67
  self: {
64
68
  url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/11941033',
65
- visibleDataSets: ['main', 'self', 'atd-unmuted'],
66
69
  person: {},
67
70
  htMeta: {
68
71
  elementId: {
@@ -76,6 +79,28 @@ const exampleInitialLocus = {
76
79
  },
77
80
  };
78
81
 
82
+ const exampleMetadata = {
83
+ htMeta: {
84
+ elementId: {
85
+ type: 'metadata',
86
+ id: 5,
87
+ version: 50,
88
+ },
89
+ dataSetNames: ['self'],
90
+ },
91
+ visibleDataSets: [
92
+ {name: 'main', url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/main'},
93
+ {
94
+ name: 'self',
95
+ url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/713e9f99/datasets/self',
96
+ },
97
+ {
98
+ name: 'atd-unmuted',
99
+ url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/atd-unmuted',
100
+ },
101
+ ],
102
+ };
103
+
79
104
  function createDataSet(name: string, leafCount: number, version = 1) {
80
105
  return {
81
106
  url: `https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/${name}`,
@@ -119,7 +144,6 @@ function mockSyncRequest(webexRequest: sinon.SinonStub, datasetUrl: string, resp
119
144
  }
120
145
 
121
146
  describe('HashTreeParser', () => {
122
- const visibleDataSetsUrl = 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/visibleDataSets';
123
147
  const locusUrl = 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f';
124
148
 
125
149
  let clock;
@@ -139,9 +163,13 @@ describe('HashTreeParser', () => {
139
163
  });
140
164
 
141
165
  // Helper to create a HashTreeParser instance with common defaults
142
- function createHashTreeParser(initialLocus: any = exampleInitialLocus) {
166
+ function createHashTreeParser(
167
+ initialLocus: any = exampleInitialLocus,
168
+ metadata: any = exampleMetadata
169
+ ) {
143
170
  return new HashTreeParser({
144
171
  initialLocus,
172
+ metadata,
145
173
  webexRequest,
146
174
  locusInfoUpdateCallback: callback,
147
175
  debugId: 'test',
@@ -197,9 +225,50 @@ describe('HashTreeParser', () => {
197
225
  body: response,
198
226
  });
199
227
  }
228
+
229
+ async function checkAsyncDatasetInitialization(
230
+ parser: HashTreeParser,
231
+ newDataSet: {name: string; leafCount: number; url: string}
232
+ ) {
233
+ // immediately we don't have the dataset yet, so it should not be in visibleDataSets
234
+ // and no hash tree should exist yet
235
+ expect(parser.visibleDataSets.some((vds) => vds.name === newDataSet.name)).to.be.false;
236
+ assert.isUndefined(parser.dataSets[newDataSet.name]);
237
+
238
+ // Wait for the async initialization to complete (queued as microtask)
239
+ await clock.tickAsync(0);
240
+
241
+ // The visibleDataSets is updated from the metadata object data
242
+ expect(parser.visibleDataSets.some((vds) => vds.name === newDataSet.name)).to.be.true;
243
+
244
+ // Verify that a hash tree was created for newDataSet
245
+ assert.exists(parser.dataSets[newDataSet.name].hashTree);
246
+ assert.equal(parser.dataSets[newDataSet.name].hashTree.numLeaves, newDataSet.leafCount);
247
+
248
+ // Verify getAllDataSetsMetadata was called for async initialization
249
+ assert.calledWith(
250
+ webexRequest,
251
+ sinon.match({
252
+ method: 'GET',
253
+ uri: visibleDataSetsUrl,
254
+ })
255
+ );
256
+
257
+ // Verify sync request was sent for the new dataset
258
+ assert.calledWith(
259
+ webexRequest,
260
+ sinon.match({
261
+ method: 'POST',
262
+ uri: `${newDataSet.url}/sync`,
263
+ })
264
+ );
265
+ }
200
266
  it('should correctly initialize trees from initialLocus data', () => {
201
267
  const parser = createHashTreeParser();
202
268
 
269
+ // verify that visibleDataSetsUrl is read out from inside locus
270
+ expect(parser.visibleDataSetsUrl).to.equal(visibleDataSetsUrl);
271
+
203
272
  // Check that the correct number of trees are created
204
273
  expect(Object.keys(parser.dataSets).length).to.equal(3);
205
274
 
@@ -215,7 +284,11 @@ describe('HashTreeParser', () => {
215
284
  const selfTree = parser.dataSets.self.hashTree;
216
285
  expect(selfTree).to.be.instanceOf(HashTree);
217
286
  const expectedSelfLeaves = new Array(1).fill(null).map(() => ({}));
218
- expectedSelfLeaves[4 % 1] = {self: {4: {type: 'self', id: 4, version: 100}}};
287
+ // Both self (id=4) and metadata (id=5) map to the same leaf (4%1=0, 5%1=0)
288
+ expectedSelfLeaves[0] = {
289
+ self: {4: {type: 'self', id: 4, version: 100}},
290
+ metadata: {5: {type: 'metadata', id: 5, version: 50}},
291
+ };
219
292
  expect(selfTree.leaves).to.deep.equal(expectedSelfLeaves);
220
293
  expect(selfTree.numLeaves).to.equal(1);
221
294
 
@@ -247,7 +320,7 @@ describe('HashTreeParser', () => {
247
320
  name: 'empty-set',
248
321
  });
249
322
 
250
- const parser = createHashTreeParser(modifiedLocus);
323
+ const parser = createHashTreeParser(modifiedLocus, exampleMetadata);
251
324
 
252
325
  expect(Object.keys(parser.dataSets).length).to.equal(4); // main, self, atd-unmuted (now empty), empty-set
253
326
 
@@ -260,7 +333,10 @@ describe('HashTreeParser', () => {
260
333
 
261
334
  const selfTree = parser.dataSets.self.hashTree;
262
335
  const expectedSelfLeaves = new Array(1).fill(null).map(() => ({}));
263
- expectedSelfLeaves[4 % 1] = {self: {4: {type: 'self', id: 4, version: 100}}};
336
+ expectedSelfLeaves[4 % 1] = {
337
+ self: {4: {type: 'self', id: 4, version: 100}},
338
+ metadata: {5: exampleMetadata.htMeta.elementId},
339
+ };
264
340
  expect(selfTree.leaves).to.deep.equal(expectedSelfLeaves);
265
341
  expect(selfTree.numLeaves).to.equal(1);
266
342
 
@@ -283,25 +359,34 @@ describe('HashTreeParser', () => {
283
359
  // Create a parser with minimal initial data
284
360
  const minimalInitialLocus = {
285
361
  dataSets: [],
286
- locus: {
287
- self: {
288
- visibleDataSets: ['main', 'self'],
362
+ locus: null,
363
+ };
364
+
365
+ const minimalMetadata = {
366
+ htMeta: {
367
+ elementId: {
368
+ type: 'metadata',
369
+ id: 5,
370
+ version: 50,
289
371
  },
372
+ dataSetNames: ['self'],
290
373
  },
374
+ visibleDataSets: [
375
+ {name: 'main', url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/main'},
376
+ {
377
+ name: 'self',
378
+ url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/713e9f99/datasets/self',
379
+ },
380
+ ],
291
381
  };
292
382
 
293
- const hashTreeParser = createHashTreeParser(minimalInitialLocus);
383
+ const hashTreeParser = createHashTreeParser(minimalInitialLocus, minimalMetadata);
294
384
 
295
385
  // Setup the datasets that will be returned from getAllDataSetsMetadata
296
386
  const mainDataSet = createDataSet('main', 16, 1100);
297
387
  const selfDataSet = createDataSet('self', 1, 2100);
298
- const invisibleDataSet = createDataSet('invisible', 4, 4000);
299
388
 
300
- mockGetAllDataSetsMetadata(webexRequest, visibleDataSetsUrl, [
301
- mainDataSet,
302
- selfDataSet,
303
- invisibleDataSet,
304
- ]);
389
+ mockGetAllDataSetsMetadata(webexRequest, visibleDataSetsUrl, [mainDataSet, selfDataSet]);
305
390
 
306
391
  // Mock sync requests for visible datasets with some updated objects
307
392
  const mainSyncResponse = {
@@ -357,15 +442,16 @@ describe('HashTreeParser', () => {
357
442
  })
358
443
  );
359
444
 
360
- // Verify all datasets are added to dataSets
445
+ // verify that visibleDataSetsUrl is set on the parser
446
+ expect(hashTreeParser.visibleDataSetsUrl).to.equal(visibleDataSetsUrl);
447
+
448
+ // Verify all datasets returned from visibleDataSetsUrl are added to dataSets
361
449
  expect(hashTreeParser.dataSets.main).to.exist;
362
450
  expect(hashTreeParser.dataSets.self).to.exist;
363
- expect(hashTreeParser.dataSets.invisible).to.exist;
364
451
 
365
452
  // Verify hash trees are created only for visible datasets
366
453
  expect(hashTreeParser.dataSets.main.hashTree).to.be.instanceOf(HashTree);
367
454
  expect(hashTreeParser.dataSets.self.hashTree).to.be.instanceOf(HashTree);
368
- expect(hashTreeParser.dataSets.invisible.hashTree).to.be.undefined;
369
455
 
370
456
  // Verify hash trees have correct leaf counts
371
457
  expect(hashTreeParser.dataSets.main.hashTree.numLeaves).to.equal(16);
@@ -403,15 +489,6 @@ describe('HashTreeParser', () => {
403
489
  })
404
490
  );
405
491
 
406
- // Verify sync request was NOT sent for invisible dataset
407
- assert.neverCalledWith(
408
- webexRequest,
409
- sinon.match({
410
- method: 'POST',
411
- uri: `${invisibleDataSet.url}/sync`,
412
- })
413
- );
414
-
415
492
  // Verify callback was called with OBJECTS_UPDATED and correct updatedObjects list
416
493
  assert.calledWith(callback, LocusInfoUpdateType.OBJECTS_UPDATED, {
417
494
  updatedObjects: [
@@ -443,8 +520,6 @@ describe('HashTreeParser', () => {
443
520
  // verify that sync timers are set for visible datasets
444
521
  expect(hashTreeParser.dataSets.main.timer).to.not.be.undefined;
445
522
  expect(hashTreeParser.dataSets.self.timer).to.not.be.undefined;
446
- // and not for invisible dataset
447
- expect(hashTreeParser.dataSets.invisible.timer).to.be.undefined;
448
523
  };
449
524
 
450
525
  describe('#initializeFromMessage', () => {
@@ -461,7 +536,7 @@ describe('HashTreeParser', () => {
461
536
 
462
537
  describe('#initializeFromGetLociResponse', () => {
463
538
  it('does nothing if url for visibleDataSets is missing from locus', async () => {
464
- const parser = createHashTreeParser({dataSets: [], locus: {}});
539
+ const parser = createHashTreeParser({dataSets: [], locus: {}}, null);
465
540
 
466
541
  await parser.initializeFromGetLociResponse({participants: []});
467
542
 
@@ -488,12 +563,9 @@ describe('HashTreeParser', () => {
488
563
  it('updates hash trees based on provided new locus', () => {
489
564
  const parser = createHashTreeParser();
490
565
 
491
- const mainPutItemsSpy = sinon
492
- .spy(parser.dataSets.main.hashTree, 'putItems');
493
- const selfPutItemsSpy = sinon
494
- .spy(parser.dataSets.self.hashTree, 'putItems');
495
- const atdUnmutedPutItemsSpy = sinon
496
- .spy(parser.dataSets['atd-unmuted'].hashTree, 'putItems');
566
+ const mainPutItemsSpy = sinon.spy(parser.dataSets.main.hashTree, 'putItems');
567
+ const selfPutItemsSpy = sinon.spy(parser.dataSets.self.hashTree, 'putItems');
568
+ const atdUnmutedPutItemsSpy = sinon.spy(parser.dataSets['atd-unmuted'].hashTree, 'putItems');
497
569
 
498
570
  // Create a locus update with new htMeta information for some things
499
571
  const locusUpdate = {
@@ -540,7 +612,6 @@ describe('HashTreeParser', () => {
540
612
  ],
541
613
  self: {
542
614
  url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/11941033',
543
- visibleDataSets: ['main', 'self', 'atd-unmuted'],
544
615
  person: {},
545
616
  htMeta: {
546
617
  elementId: {
@@ -711,6 +782,211 @@ describe('HashTreeParser', () => {
711
782
  ],
712
783
  });
713
784
  });
785
+
786
+ it('handles metadata updates with new version', async () => {
787
+ const parser = createHashTreeParser();
788
+
789
+ const selfPutItemSpy = sinon.spy(parser.dataSets.self.hashTree, 'putItem');
790
+
791
+ // Create a locus update with updated metadata
792
+ const locusUpdate = {
793
+ dataSets: [createDataSet('self', 1, 2100), createDataSet('attendees', 8, 4000)],
794
+ locus: {
795
+ links: {resources: {visibleDataSets: {url: visibleDataSetsUrl}}},
796
+ participants: [
797
+ {
798
+ url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/15',
799
+ person: {},
800
+ htMeta: {
801
+ elementId: {
802
+ type: 'participant',
803
+ id: 15, // new participant
804
+ version: 999,
805
+ },
806
+ dataSetNames: ['attendees'],
807
+ },
808
+ },
809
+ ],
810
+ },
811
+ metadata: {
812
+ htMeta: {
813
+ elementId: {
814
+ type: 'metadata',
815
+ id: 5,
816
+ version: 51, // incremented version
817
+ },
818
+ dataSetNames: ['self'],
819
+ },
820
+ // new visibleDataSets: atd-unmuted removed, "attendees" and "new-dataset" added
821
+ visibleDataSets: [
822
+ {
823
+ name: 'main',
824
+ url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/main',
825
+ },
826
+ {
827
+ name: 'self',
828
+ url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/713e9f99/datasets/self',
829
+ },
830
+ {
831
+ name: 'new-dataset', // this one is not in dataSets, so will require async initialization
832
+ url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/new-dataset',
833
+ },
834
+ {
835
+ name: 'attendees', // this one is in dataSets, so should be processed immediately
836
+ url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/attendees',
837
+ },
838
+ ],
839
+ },
840
+ };
841
+
842
+ // Mock the async initialization of the new dataset
843
+ const newDataSet = createDataSet('new-dataset', 4, 5000);
844
+ mockGetAllDataSetsMetadata(webexRequest, visibleDataSetsUrl, [newDataSet]);
845
+ mockSyncRequest(webexRequest, newDataSet.url, {
846
+ dataSets: [newDataSet],
847
+ visibleDataSetsUrl,
848
+ locusUrl,
849
+ locusStateElements: [],
850
+ });
851
+
852
+ // Call handleLocusUpdate
853
+ parser.handleLocusUpdate(locusUpdate);
854
+
855
+ // Verify putItem was called on self hash tree with metadata
856
+ assert.calledOnceWithExactly(selfPutItemSpy, {type: 'metadata', id: 5, version: 51});
857
+
858
+ console.log(
859
+ 'callback calls',
860
+ callback.getCalls().map((call) => JSON.stringify(call.args, null, 2))
861
+ );
862
+ // Verify callback was called with metadata object and removed dataset objects
863
+ assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED, {
864
+ updatedObjects: [
865
+ // updated metadata object:
866
+ {
867
+ htMeta: {
868
+ elementId: {
869
+ type: 'metadata',
870
+ id: 5,
871
+ version: 51,
872
+ },
873
+ dataSetNames: ['self'],
874
+ },
875
+ data: {
876
+ htMeta: {
877
+ elementId: {
878
+ type: 'metadata',
879
+ id: 5,
880
+ version: 51,
881
+ },
882
+ dataSetNames: ['self'],
883
+ },
884
+ visibleDataSets: [
885
+ {
886
+ name: 'main',
887
+ url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/main',
888
+ },
889
+ {
890
+ name: 'self',
891
+ url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/713e9f99/datasets/self',
892
+ },
893
+ {
894
+ name: 'new-dataset',
895
+ url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/new-dataset',
896
+ },
897
+ {
898
+ name: 'attendees',
899
+ url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/attendees',
900
+ },
901
+ ],
902
+ },
903
+ },
904
+ // removed participant from a removed dataset 'atd-unmuted':
905
+ {
906
+ htMeta: {
907
+ elementId: {
908
+ type: 'participant',
909
+ id: 14,
910
+ version: 300,
911
+ },
912
+ dataSetNames: ['atd-unmuted'],
913
+ },
914
+ data: null,
915
+ },
916
+ // new participant from a new data set 'attendees':
917
+ {
918
+ htMeta: {
919
+ elementId: {
920
+ type: 'participant',
921
+ id: 15,
922
+ version: 999,
923
+ },
924
+ dataSetNames: ['attendees'],
925
+ },
926
+ data: {
927
+ url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/15',
928
+ person: {},
929
+ htMeta: {
930
+ elementId: {
931
+ type: 'participant',
932
+ id: 15,
933
+ version: 999,
934
+ },
935
+ dataSetNames: ['attendees'],
936
+ },
937
+ },
938
+ },
939
+ ],
940
+ });
941
+
942
+ // verify also that an async initialization was done for
943
+ await checkAsyncDatasetInitialization(parser, newDataSet);
944
+ });
945
+
946
+ it('handles metadata updates with same version (no callback)', () => {
947
+ const parser = createHashTreeParser();
948
+
949
+ const selfPutItemSpy = sinon.spy(parser.dataSets.self.hashTree, 'putItem');
950
+
951
+ // Create a locus update with metadata that has the same version and same visibleDataSets
952
+ const locusUpdate = {
953
+ dataSets: [createDataSet('self', 1, 2100)],
954
+ locus: {},
955
+ metadata: {
956
+ htMeta: {
957
+ elementId: {
958
+ type: 'metadata',
959
+ id: 5,
960
+ version: 50, // same version as initial
961
+ },
962
+ dataSetNames: ['self'],
963
+ },
964
+ visibleDataSets: [
965
+ {
966
+ name: 'main',
967
+ url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/main',
968
+ },
969
+ {
970
+ name: 'self',
971
+ url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/713e9f99/datasets/self',
972
+ },
973
+ {
974
+ name: 'atd-unmuted',
975
+ url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/atd-unmuted',
976
+ },
977
+ ],
978
+ },
979
+ };
980
+
981
+ // Call handleLocusUpdate
982
+ parser.handleLocusUpdate(locusUpdate);
983
+
984
+ // Verify putItem was called on self hash tree
985
+ assert.calledOnceWithExactly(selfPutItemSpy, {type: 'metadata', id: 5, version: 50});
986
+
987
+ // Verify callback was NOT called because version didn't change
988
+ assert.notCalled(callback);
989
+ });
714
990
  });
715
991
 
716
992
  describe('#handleMessage', () => {
@@ -910,13 +1186,6 @@ describe('HashTreeParser', () => {
910
1186
  // Verify callback was called with OBJECTS_UPDATED and all updated objects
911
1187
  assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED, {
912
1188
  updatedObjects: [
913
- {
914
- htMeta: {
915
- elementId: {type: 'self', id: 4, version: 101},
916
- dataSetNames: ['self'],
917
- },
918
- data: {person: {name: 'updated self name'}},
919
- },
920
1189
  {
921
1190
  htMeta: {
922
1191
  elementId: {type: 'locus', id: 0, version: 201},
@@ -924,10 +1193,6 @@ describe('HashTreeParser', () => {
924
1193
  },
925
1194
  data: {info: {id: 'updated-locus-info'}},
926
1195
  },
927
- // self updates appear twice, because they are processed twice in HashTreeParser.parseMessage()
928
- // (first for checking for visibleDataSets changes and again with the rest of updates in the main part of parseMessage())
929
- // this is only temporary until SPARK-744859 is done and having them twice here is not harmful
930
- // so keeping it like this for now
931
1196
  {
932
1197
  htMeta: {
933
1198
  elementId: {type: 'self', id: 4, version: 101},
@@ -1179,6 +1444,9 @@ describe('HashTreeParser', () => {
1179
1444
  sinon.match({
1180
1445
  method: 'GET',
1181
1446
  uri: `${mainDataSetUrl}/hashtree`,
1447
+ qs: {
1448
+ rootHash: hashTree.getRootHash(),
1449
+ },
1182
1450
  })
1183
1451
  );
1184
1452
 
@@ -1186,6 +1454,7 @@ describe('HashTreeParser', () => {
1186
1454
  assert.calledWith(webexRequest, {
1187
1455
  method: 'POST',
1188
1456
  uri: `${mainDataSetUrl}/sync`,
1457
+ qs: {rootHash: hashTree.getRootHash()},
1189
1458
  body: {
1190
1459
  leafCount: 16,
1191
1460
  leafDataEntries: [
@@ -1239,10 +1508,17 @@ describe('HashTreeParser', () => {
1239
1508
  assert.calledWith(webexRequest, {
1240
1509
  method: 'POST',
1241
1510
  uri: `${parser.dataSets.self.url}/sync`,
1511
+ qs: {rootHash: parser.dataSets.self.hashTree.getRootHash()},
1242
1512
  body: {
1243
1513
  leafCount: 1,
1244
1514
  leafDataEntries: [
1245
- {leafIndex: 0, elementIds: [{type: 'self', id: 4, version: 102}]},
1515
+ {
1516
+ leafIndex: 0,
1517
+ elementIds: [
1518
+ {type: 'self', id: 4, version: 102},
1519
+ {type: 'metadata', id: 5, version: 50},
1520
+ ],
1521
+ },
1246
1522
  ],
1247
1523
  },
1248
1524
  });
@@ -1257,7 +1533,7 @@ describe('HashTreeParser', () => {
1257
1533
  // Stub updateItems on self hash tree to return true
1258
1534
  sinon.stub(parser.dataSets.self.hashTree, 'updateItems').returns([true]);
1259
1535
 
1260
- // Send a message with SELF object that has a new visibleDataSets list
1536
+ // Send a message with Metadata object that has a new visibleDataSets list
1261
1537
  const message = {
1262
1538
  dataSets: [createDataSet('self', 1, 2100), createDataSet('attendees', 8, 4000)],
1263
1539
  visibleDataSetsUrl,
@@ -1266,14 +1542,31 @@ describe('HashTreeParser', () => {
1266
1542
  {
1267
1543
  htMeta: {
1268
1544
  elementId: {
1269
- type: 'self' as const,
1270
- id: 4,
1271
- version: 101,
1545
+ type: 'metadata' as const,
1546
+ id: 5,
1547
+ version: 51,
1272
1548
  },
1273
1549
  dataSetNames: ['self'],
1274
1550
  },
1275
1551
  data: {
1276
- visibleDataSets: ['main', 'self', 'atd-unmuted', 'attendees'], // added 'attendees'
1552
+ visibleDataSets: [
1553
+ {
1554
+ name: 'main',
1555
+ url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/main',
1556
+ },
1557
+ {
1558
+ name: 'self',
1559
+ url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/713e9f99/datasets/self',
1560
+ },
1561
+ {
1562
+ name: 'atd-unmuted',
1563
+ url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/atd-unmuted',
1564
+ },
1565
+ {
1566
+ name: 'attendees',
1567
+ url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/attendees',
1568
+ },
1569
+ ], // added 'attendees'
1277
1570
  },
1278
1571
  },
1279
1572
  ],
@@ -1282,31 +1575,65 @@ describe('HashTreeParser', () => {
1282
1575
  await parser.handleMessage(message, 'add visible dataset');
1283
1576
 
1284
1577
  // Verify that 'attendees' was added to visibleDataSets
1285
- assert.include(parser.visibleDataSets, 'attendees');
1578
+ expect(parser.visibleDataSets.some((vds) => vds.name === 'attendees')).to.be.true;
1286
1579
 
1287
1580
  // Verify that a hash tree was created for 'attendees'
1288
1581
  assert.exists(parser.dataSets.attendees.hashTree);
1289
1582
  assert.equal(parser.dataSets.attendees.hashTree.numLeaves, 8);
1290
1583
 
1291
- // Verify callback was called with the self update (appears twice due to SPARK-744859)
1584
+ // Verify callback was called with the metadata update (appears twice - processed once for visible dataset changes, once in main loop)
1292
1585
  assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED, {
1293
1586
  updatedObjects: [
1294
1587
  {
1295
1588
  htMeta: {
1296
- elementId: {type: 'self', id: 4, version: 101},
1589
+ elementId: {type: 'metadata', id: 5, version: 51},
1297
1590
  dataSetNames: ['self'],
1298
1591
  },
1299
1592
  data: {
1300
- visibleDataSets: ['main', 'self', 'atd-unmuted', 'attendees'],
1593
+ visibleDataSets: [
1594
+ {
1595
+ name: 'main',
1596
+ url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/main',
1597
+ },
1598
+ {
1599
+ name: 'self',
1600
+ url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/713e9f99/datasets/self',
1601
+ },
1602
+ {
1603
+ name: 'atd-unmuted',
1604
+ url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/atd-unmuted',
1605
+ },
1606
+ {
1607
+ name: 'attendees',
1608
+ url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/attendees',
1609
+ },
1610
+ ],
1301
1611
  },
1302
1612
  },
1303
1613
  {
1304
1614
  htMeta: {
1305
- elementId: {type: 'self', id: 4, version: 101},
1615
+ elementId: {type: 'metadata', id: 5, version: 51},
1306
1616
  dataSetNames: ['self'],
1307
1617
  },
1308
1618
  data: {
1309
- visibleDataSets: ['main', 'self', 'atd-unmuted', 'attendees'],
1619
+ visibleDataSets: [
1620
+ {
1621
+ name: 'main',
1622
+ url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/main',
1623
+ },
1624
+ {
1625
+ name: 'self',
1626
+ url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/713e9f99/datasets/self',
1627
+ },
1628
+ {
1629
+ name: 'atd-unmuted',
1630
+ url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/atd-unmuted',
1631
+ },
1632
+ {
1633
+ name: 'attendees',
1634
+ url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/attendees',
1635
+ },
1636
+ ],
1310
1637
  },
1311
1638
  },
1312
1639
  ],
@@ -1320,7 +1647,7 @@ describe('HashTreeParser', () => {
1320
1647
  // Stub updateItems on self hash tree to return true
1321
1648
  sinon.stub(parser.dataSets.self.hashTree, 'updateItems').returns([true]);
1322
1649
 
1323
- // Send a message with SELF object that has a new visibleDataSets list (adding 'new-dataset')
1650
+ // Send a message with Metadata object that has a new visibleDataSets list (adding 'new-dataset')
1324
1651
  // but WITHOUT providing info about the new dataset in dataSets array
1325
1652
  const message = {
1326
1653
  dataSets: [createDataSet('self', 1, 2100)],
@@ -1330,14 +1657,31 @@ describe('HashTreeParser', () => {
1330
1657
  {
1331
1658
  htMeta: {
1332
1659
  elementId: {
1333
- type: 'self' as const,
1334
- id: 4,
1335
- version: 101,
1660
+ type: 'metadata' as const,
1661
+ id: 5,
1662
+ version: 51,
1336
1663
  },
1337
1664
  dataSetNames: ['self'],
1338
1665
  },
1339
1666
  data: {
1340
- visibleDataSets: ['main', 'self', 'atd-unmuted', 'new-dataset'],
1667
+ visibleDataSets: [
1668
+ {
1669
+ name: 'main',
1670
+ url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/main',
1671
+ },
1672
+ {
1673
+ name: 'self',
1674
+ url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/713e9f99/datasets/self',
1675
+ },
1676
+ {
1677
+ name: 'atd-unmuted',
1678
+ url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/atd-unmuted',
1679
+ },
1680
+ {
1681
+ name: 'new-dataset',
1682
+ url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/new-dataset',
1683
+ },
1684
+ ],
1341
1685
  },
1342
1686
  },
1343
1687
  ],
@@ -1355,38 +1699,7 @@ describe('HashTreeParser', () => {
1355
1699
 
1356
1700
  await parser.handleMessage(message, 'add new dataset requiring async init');
1357
1701
 
1358
- // immediately we don't have the dataset yet, so it should not be in visibleDataSets
1359
- // and no hash tree should exist yet
1360
- assert.isFalse(parser.visibleDataSets.includes('new-dataset'));
1361
- assert.isUndefined(parser.dataSets['new-dataset']);
1362
-
1363
- // Wait for the async initialization to complete (queued as microtask)
1364
- await clock.tickAsync(0);
1365
-
1366
- // The visibleDataSets is updated from the self object data
1367
- assert.include(parser.visibleDataSets, 'new-dataset');
1368
-
1369
- // Verify that a hash tree was created for 'new-dataset'
1370
- assert.exists(parser.dataSets['new-dataset'].hashTree);
1371
- assert.equal(parser.dataSets['new-dataset'].hashTree.numLeaves, 4);
1372
-
1373
- // Verify getAllDataSetsMetadata was called for async initialization
1374
- assert.calledWith(
1375
- webexRequest,
1376
- sinon.match({
1377
- method: 'GET',
1378
- uri: visibleDataSetsUrl,
1379
- })
1380
- );
1381
-
1382
- // Verify sync request was sent for the new dataset
1383
- assert.calledWith(
1384
- webexRequest,
1385
- sinon.match({
1386
- method: 'POST',
1387
- uri: `${newDataSet.url}/sync`,
1388
- })
1389
- );
1702
+ await checkAsyncDatasetInitialization(parser, newDataSet);
1390
1703
  });
1391
1704
 
1392
1705
  it('handles removal of visible data set', async () => {
@@ -1406,7 +1719,7 @@ describe('HashTreeParser', () => {
1406
1719
  // Stub updateItems on self hash tree to return true
1407
1720
  sinon.stub(parser.dataSets.self.hashTree, 'updateItems').returns([true]);
1408
1721
 
1409
- // Send a message with SELF object that has removed 'atd-unmuted' from visibleDataSets
1722
+ // Send a message with Metadata object that has removed 'atd-unmuted' from visibleDataSets
1410
1723
  const message = {
1411
1724
  dataSets: [createDataSet('self', 1, 2100)],
1412
1725
  visibleDataSetsUrl,
@@ -1415,14 +1728,23 @@ describe('HashTreeParser', () => {
1415
1728
  {
1416
1729
  htMeta: {
1417
1730
  elementId: {
1418
- type: 'self' as const,
1419
- id: 4,
1420
- version: 101,
1731
+ type: 'metadata' as const,
1732
+ id: 5,
1733
+ version: 51,
1421
1734
  },
1422
1735
  dataSetNames: ['self'],
1423
1736
  },
1424
1737
  data: {
1425
- visibleDataSets: ['main', 'self'], // removed 'atd-unmuted'
1738
+ visibleDataSets: [
1739
+ {
1740
+ name: 'main',
1741
+ url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/main',
1742
+ },
1743
+ {
1744
+ name: 'self',
1745
+ url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/713e9f99/datasets/self',
1746
+ },
1747
+ ], // removed 'atd-unmuted'
1426
1748
  },
1427
1749
  },
1428
1750
  ],
@@ -1431,7 +1753,7 @@ describe('HashTreeParser', () => {
1431
1753
  await parser.handleMessage(message, 'remove visible dataset');
1432
1754
 
1433
1755
  // Verify that 'atd-unmuted' was removed from visibleDataSets
1434
- assert.notInclude(parser.visibleDataSets, 'atd-unmuted');
1756
+ expect(parser.visibleDataSets.some((vds) => vds.name === 'atd-unmuted')).to.be.false;
1435
1757
 
1436
1758
  // Verify that the hash tree for 'atd-unmuted' was deleted
1437
1759
  assert.isUndefined(parser.dataSets['atd-unmuted'].hashTree);
@@ -1439,16 +1761,25 @@ describe('HashTreeParser', () => {
1439
1761
  // Verify that the timer was cleared
1440
1762
  assert.isUndefined(parser.dataSets['atd-unmuted'].timer);
1441
1763
 
1442
- // Verify callback was called with both the self update and the removed objects
1764
+ // Verify callback was called with the metadata update and the removed objects (metadata appears twice - processed once for dataset changes, once in main loop)
1443
1765
  assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED, {
1444
1766
  updatedObjects: [
1445
1767
  {
1446
1768
  htMeta: {
1447
- elementId: {type: 'self', id: 4, version: 101},
1769
+ elementId: {type: 'metadata', id: 5, version: 51},
1448
1770
  dataSetNames: ['self'],
1449
1771
  },
1450
1772
  data: {
1451
- visibleDataSets: ['main', 'self'],
1773
+ visibleDataSets: [
1774
+ {
1775
+ name: 'main',
1776
+ url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/main',
1777
+ },
1778
+ {
1779
+ name: 'self',
1780
+ url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/713e9f99/datasets/self',
1781
+ },
1782
+ ],
1452
1783
  },
1453
1784
  },
1454
1785
  {
@@ -1460,11 +1791,20 @@ describe('HashTreeParser', () => {
1460
1791
  },
1461
1792
  {
1462
1793
  htMeta: {
1463
- elementId: {type: 'self', id: 4, version: 101}, // 2nd self because of SPARK-744859
1794
+ elementId: {type: 'metadata', id: 5, version: 51},
1464
1795
  dataSetNames: ['self'],
1465
1796
  },
1466
1797
  data: {
1467
- visibleDataSets: ['main', 'self'],
1798
+ visibleDataSets: [
1799
+ {
1800
+ name: 'main',
1801
+ url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/main',
1802
+ },
1803
+ {
1804
+ name: 'self',
1805
+ url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/713e9f99/datasets/self',
1806
+ },
1807
+ ],
1468
1808
  },
1469
1809
  },
1470
1810
  ],
@@ -1489,7 +1829,7 @@ describe('HashTreeParser', () => {
1489
1829
  });
1490
1830
 
1491
1831
  // Verify attendees is NOT in visibleDataSets
1492
- assert.notInclude(parser.visibleDataSets, 'attendees');
1832
+ expect(parser.visibleDataSets.some((vds) => vds.name === 'attendees')).to.be.false;
1493
1833
 
1494
1834
  // Send a message with attendees data
1495
1835
  const message = {
@@ -1521,4 +1861,496 @@ describe('HashTreeParser', () => {
1521
1861
  });
1522
1862
  });
1523
1863
  });
1864
+
1865
+ describe('#callLocusInfoUpdateCallback filtering', () => {
1866
+ // Helper to setup parser with initial objects and reset callback history
1867
+ async function setupParserWithObjects(locusStateElements: any[]) {
1868
+ const parser = createHashTreeParser();
1869
+
1870
+ if (locusStateElements.length > 0) {
1871
+ // Determine which datasets to include based on the objects' dataSetNames
1872
+ const dataSetNames = new Set<string>();
1873
+ locusStateElements.forEach((element) => {
1874
+ element.htMeta?.dataSetNames?.forEach((name) => dataSetNames.add(name));
1875
+ });
1876
+
1877
+ const dataSets = [];
1878
+ if (dataSetNames.has('main')) dataSets.push(createDataSet('main', 16, 1100));
1879
+ if (dataSetNames.has('self')) dataSets.push(createDataSet('self', 1, 2100));
1880
+ if (dataSetNames.has('atd-unmuted')) dataSets.push(createDataSet('atd-unmuted', 16, 3100));
1881
+
1882
+ const setupMessage = {
1883
+ dataSets,
1884
+ visibleDataSetsUrl,
1885
+ locusUrl,
1886
+ locusStateElements,
1887
+ };
1888
+
1889
+ await parser.handleMessage(setupMessage, 'setup');
1890
+ }
1891
+
1892
+ callback.resetHistory();
1893
+ return parser;
1894
+ }
1895
+
1896
+ it('filters out updates when a dataset has a higher version', async () => {
1897
+ const parser = await setupParserWithObjects([
1898
+ {
1899
+ htMeta: {
1900
+ elementId: {type: 'locus' as const, id: 5, version: 100},
1901
+ dataSetNames: ['main'],
1902
+ },
1903
+ data: {existingField: 'existing'},
1904
+ },
1905
+ ]);
1906
+
1907
+ // Try to update with an older version (90)
1908
+ const updateMessage = {
1909
+ dataSets: [createDataSet('main', 16, 1101)],
1910
+ visibleDataSetsUrl,
1911
+ locusUrl,
1912
+ locusStateElements: [
1913
+ {
1914
+ htMeta: {
1915
+ elementId: {type: 'locus' as const, id: 5, version: 90},
1916
+ dataSetNames: ['main'],
1917
+ },
1918
+ data: {someField: 'value'},
1919
+ },
1920
+ ],
1921
+ };
1922
+
1923
+ await parser.handleMessage(updateMessage, 'update with older version');
1924
+
1925
+ // Callback should not be called because the update was filtered out
1926
+ assert.notCalled(callback);
1927
+ });
1928
+
1929
+ it('allows updates when version is newer than existing', async () => {
1930
+ const parser = await setupParserWithObjects([
1931
+ {
1932
+ htMeta: {
1933
+ elementId: {type: 'locus' as const, id: 5, version: 100},
1934
+ dataSetNames: ['main'],
1935
+ },
1936
+ data: {existingField: 'existing'},
1937
+ },
1938
+ ]);
1939
+
1940
+ // Try to update with a newer version (110)
1941
+ const updateMessage = {
1942
+ dataSets: [createDataSet('main', 16, 1101)],
1943
+ visibleDataSetsUrl,
1944
+ locusUrl,
1945
+ locusStateElements: [
1946
+ {
1947
+ htMeta: {
1948
+ elementId: {type: 'locus' as const, id: 5, version: 110},
1949
+ dataSetNames: ['main'],
1950
+ },
1951
+ data: {someField: 'new value'},
1952
+ },
1953
+ ],
1954
+ };
1955
+
1956
+ await parser.handleMessage(updateMessage, 'update with newer version');
1957
+
1958
+ // Callback should be called with the update
1959
+ assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED, {
1960
+ updatedObjects: [
1961
+ {
1962
+ htMeta: {
1963
+ elementId: {type: 'locus', id: 5, version: 110},
1964
+ dataSetNames: ['main'],
1965
+ },
1966
+ data: {someField: 'new value'},
1967
+ },
1968
+ ],
1969
+ });
1970
+ });
1971
+
1972
+ it('filters out removal when object still exists in any dataset', async () => {
1973
+ const parser = await setupParserWithObjects([
1974
+ {
1975
+ htMeta: {
1976
+ elementId: {type: 'participant' as const, id: 10, version: 50},
1977
+ dataSetNames: ['main', 'atd-unmuted'],
1978
+ },
1979
+ data: {name: 'participant'},
1980
+ },
1981
+ ]);
1982
+
1983
+ // Try to remove the object from main only (it still exists in atd-unmuted)
1984
+ const removalMessage = {
1985
+ dataSets: [createDataSet('main', 16, 1101)],
1986
+ visibleDataSetsUrl,
1987
+ locusUrl,
1988
+ locusStateElements: [
1989
+ {
1990
+ htMeta: {
1991
+ elementId: {type: 'participant' as const, id: 10, version: 50},
1992
+ dataSetNames: ['main'],
1993
+ },
1994
+ data: null, // removal
1995
+ },
1996
+ ],
1997
+ };
1998
+
1999
+ await parser.handleMessage(removalMessage, 'removal from one dataset');
2000
+
2001
+ // Callback should not be called because object still exists in atd-unmuted
2002
+ assert.notCalled(callback);
2003
+ });
2004
+
2005
+ it('allows removal when object does not exist in any dataset', async () => {
2006
+ const parser = await setupParserWithObjects([]);
2007
+
2008
+ // Stub updateItems to return true (simulating that the removal was "applied")
2009
+ sinon.stub(parser.dataSets.main.hashTree, 'updateItems').returns([true]);
2010
+
2011
+ // Try to remove an object that doesn't exist anywhere
2012
+ const removalMessage = {
2013
+ dataSets: [createDataSet('main', 16, 1100)],
2014
+ visibleDataSetsUrl,
2015
+ locusUrl,
2016
+ locusStateElements: [
2017
+ {
2018
+ htMeta: {
2019
+ elementId: {type: 'participant' as const, id: 99, version: 10},
2020
+ dataSetNames: ['main'],
2021
+ },
2022
+ data: null, // removal
2023
+ },
2024
+ ],
2025
+ };
2026
+
2027
+ await parser.handleMessage(removalMessage, 'removal of non-existent object');
2028
+
2029
+ // Callback should be called with the removal
2030
+ assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED, {
2031
+ updatedObjects: [
2032
+ {
2033
+ htMeta: {
2034
+ elementId: {type: 'participant', id: 99, version: 10},
2035
+ dataSetNames: ['main'],
2036
+ },
2037
+ data: null,
2038
+ },
2039
+ ],
2040
+ });
2041
+ });
2042
+
2043
+ it('filters out removal when object exists in another dataset with newer version', async () => {
2044
+ const parser = createHashTreeParser();
2045
+
2046
+ // Setup: Add object to main with version 40
2047
+ await parser.handleMessage(
2048
+ {
2049
+ dataSets: [createDataSet('main', 16, 1100)],
2050
+ visibleDataSetsUrl,
2051
+ locusUrl,
2052
+ locusStateElements: [
2053
+ {
2054
+ htMeta: {
2055
+ elementId: {type: 'participant' as const, id: 10, version: 40},
2056
+ dataSetNames: ['main'],
2057
+ },
2058
+ data: {name: 'participant v40'},
2059
+ },
2060
+ ],
2061
+ },
2062
+ 'setup main'
2063
+ );
2064
+
2065
+ // Add object to atd-unmuted with version 50
2066
+ await parser.handleMessage(
2067
+ {
2068
+ dataSets: [createDataSet('atd-unmuted', 16, 3100)],
2069
+ visibleDataSetsUrl,
2070
+ locusUrl,
2071
+ locusStateElements: [
2072
+ {
2073
+ htMeta: {
2074
+ elementId: {type: 'participant' as const, id: 10, version: 50},
2075
+ dataSetNames: ['atd-unmuted'],
2076
+ },
2077
+ data: {name: 'participant v50'},
2078
+ },
2079
+ ],
2080
+ },
2081
+ 'setup atd-unmuted'
2082
+ );
2083
+ callback.resetHistory();
2084
+
2085
+ // Try to remove with version 40 from main
2086
+ const removalMessage = {
2087
+ dataSets: [createDataSet('main', 16, 1101)],
2088
+ visibleDataSetsUrl,
2089
+ locusUrl,
2090
+ locusStateElements: [
2091
+ {
2092
+ htMeta: {
2093
+ elementId: {type: 'participant' as const, id: 10, version: 40},
2094
+ dataSetNames: ['main'],
2095
+ },
2096
+ data: null, // removal
2097
+ },
2098
+ ],
2099
+ };
2100
+
2101
+ await parser.handleMessage(removalMessage, 'removal with older version');
2102
+
2103
+ // Callback should not be called because object still exists with newer version
2104
+ assert.notCalled(callback);
2105
+ });
2106
+
2107
+ it('filters mixed updates correctly - some pass, some filtered', async () => {
2108
+ const parser = await setupParserWithObjects([
2109
+ {
2110
+ htMeta: {
2111
+ elementId: {type: 'participant' as const, id: 1, version: 100},
2112
+ dataSetNames: ['main'],
2113
+ },
2114
+ data: {name: 'participant 1'},
2115
+ },
2116
+ {
2117
+ htMeta: {
2118
+ elementId: {type: 'participant' as const, id: 2, version: 50},
2119
+ dataSetNames: ['atd-unmuted'],
2120
+ },
2121
+ data: {name: 'participant 2'},
2122
+ },
2123
+ ]);
2124
+
2125
+ // Send mixed updates
2126
+ const mixedMessage = {
2127
+ dataSets: [createDataSet('main', 16, 1101)],
2128
+ visibleDataSetsUrl,
2129
+ locusUrl,
2130
+ locusStateElements: [
2131
+ {
2132
+ htMeta: {
2133
+ elementId: {type: 'participant' as const, id: 1, version: 110}, // newer version - should pass
2134
+ dataSetNames: ['main'],
2135
+ },
2136
+ data: {name: 'updated'},
2137
+ },
2138
+ {
2139
+ htMeta: {
2140
+ elementId: {type: 'participant' as const, id: 1, version: 90}, // older version - should be filtered
2141
+ dataSetNames: ['main'],
2142
+ },
2143
+ data: {name: 'old'},
2144
+ },
2145
+ {
2146
+ htMeta: {
2147
+ elementId: {type: 'participant' as const, id: 3, version: 10}, // new object - should pass
2148
+ dataSetNames: ['main'],
2149
+ },
2150
+ data: {name: 'new'},
2151
+ },
2152
+ {
2153
+ htMeta: {
2154
+ elementId: {type: 'participant' as const, id: 2, version: 50}, // removal but exists in atd-unmuted - should be filtered
2155
+ dataSetNames: ['main'],
2156
+ },
2157
+ data: null,
2158
+ },
2159
+ ],
2160
+ };
2161
+
2162
+ await parser.handleMessage(mixedMessage, 'mixed updates');
2163
+
2164
+ // Callback should be called with only the valid updates (participant 1 v110 and participant 3 v10)
2165
+ assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED, {
2166
+ updatedObjects: [
2167
+ {
2168
+ htMeta: {
2169
+ elementId: {type: 'participant', id: 1, version: 110},
2170
+ dataSetNames: ['main'],
2171
+ },
2172
+ data: {name: 'updated'},
2173
+ },
2174
+ {
2175
+ htMeta: {
2176
+ elementId: {type: 'participant', id: 3, version: 10},
2177
+ dataSetNames: ['main'],
2178
+ },
2179
+ data: {name: 'new'},
2180
+ },
2181
+ ],
2182
+ });
2183
+ });
2184
+
2185
+ it('does not call callback when all updates are filtered out', async () => {
2186
+ const parser = await setupParserWithObjects([
2187
+ {
2188
+ htMeta: {
2189
+ elementId: {type: 'locus' as const, id: 5, version: 100},
2190
+ dataSetNames: ['main'],
2191
+ },
2192
+ data: {existingField: 'existing'},
2193
+ },
2194
+ ]);
2195
+
2196
+ // Try to update with older versions (all should be filtered)
2197
+ const updateMessage = {
2198
+ dataSets: [createDataSet('main', 16, 1101)],
2199
+ visibleDataSetsUrl,
2200
+ locusUrl,
2201
+ locusStateElements: [
2202
+ {
2203
+ htMeta: {
2204
+ elementId: {type: 'locus' as const, id: 5, version: 80},
2205
+ dataSetNames: ['main'],
2206
+ },
2207
+ data: {someField: 'value'},
2208
+ },
2209
+ {
2210
+ htMeta: {
2211
+ elementId: {type: 'locus' as const, id: 5, version: 90},
2212
+ dataSetNames: ['main'],
2213
+ },
2214
+ data: {someField: 'another value'},
2215
+ },
2216
+ ],
2217
+ };
2218
+
2219
+ await parser.handleMessage(updateMessage, 'all filtered updates');
2220
+
2221
+ // Callback should not be called at all
2222
+ assert.notCalled(callback);
2223
+ });
2224
+
2225
+ it('checks all visible datasets when filtering', async () => {
2226
+ const parser = createHashTreeParser();
2227
+
2228
+ // Setup: Add same object to multiple datasets with different versions
2229
+ await parser.handleMessage(
2230
+ {
2231
+ dataSets: [createDataSet('main', 16, 1100)],
2232
+ visibleDataSetsUrl,
2233
+ locusUrl,
2234
+ locusStateElements: [
2235
+ {
2236
+ htMeta: {
2237
+ elementId: {type: 'participant' as const, id: 10, version: 100},
2238
+ dataSetNames: ['main'],
2239
+ },
2240
+ data: {name: 'v100'},
2241
+ },
2242
+ ],
2243
+ },
2244
+ 'setup main'
2245
+ );
2246
+
2247
+ await parser.handleMessage(
2248
+ {
2249
+ dataSets: [createDataSet('self', 1, 2100)],
2250
+ visibleDataSetsUrl,
2251
+ locusUrl,
2252
+ locusStateElements: [
2253
+ {
2254
+ htMeta: {
2255
+ elementId: {type: 'participant' as const, id: 10, version: 120}, // highest
2256
+ dataSetNames: ['self'],
2257
+ },
2258
+ data: {name: 'v120'},
2259
+ },
2260
+ ],
2261
+ },
2262
+ 'setup self'
2263
+ );
2264
+
2265
+ await parser.handleMessage(
2266
+ {
2267
+ dataSets: [createDataSet('atd-unmuted', 16, 3100)],
2268
+ visibleDataSetsUrl,
2269
+ locusUrl,
2270
+ locusStateElements: [
2271
+ {
2272
+ htMeta: {
2273
+ elementId: {type: 'participant' as const, id: 10, version: 110},
2274
+ dataSetNames: ['atd-unmuted'],
2275
+ },
2276
+ data: {name: 'v110'},
2277
+ },
2278
+ ],
2279
+ },
2280
+ 'setup atd-unmuted'
2281
+ );
2282
+ callback.resetHistory();
2283
+
2284
+ // Try to update with version 115 (newer than main and atd-unmuted, but older than self)
2285
+ const updateMessage = {
2286
+ dataSets: [createDataSet('main', 16, 1101)],
2287
+ visibleDataSetsUrl,
2288
+ locusUrl,
2289
+ locusStateElements: [
2290
+ {
2291
+ htMeta: {
2292
+ elementId: {type: 'participant' as const, id: 10, version: 115},
2293
+ dataSetNames: ['main'],
2294
+ },
2295
+ data: {name: 'update'},
2296
+ },
2297
+ ],
2298
+ };
2299
+
2300
+ await parser.handleMessage(updateMessage, 'update with v115');
2301
+
2302
+ // Should be filtered out because self dataset has version 120
2303
+ assert.notCalled(callback);
2304
+ });
2305
+
2306
+ it('does not call callback for empty locusStateElements', async () => {
2307
+ const parser = await setupParserWithObjects([]);
2308
+
2309
+ const emptyMessage = {
2310
+ dataSets: [createDataSet('main', 16, 1100)],
2311
+ visibleDataSetsUrl,
2312
+ locusUrl,
2313
+ locusStateElements: [],
2314
+ };
2315
+
2316
+ await parser.handleMessage(emptyMessage, 'empty elements');
2317
+
2318
+ assert.notCalled(callback);
2319
+ });
2320
+
2321
+ it('always calls callback for MEETING_ENDED regardless of filtering', async () => {
2322
+ const parser = await setupParserWithObjects([
2323
+ {
2324
+ htMeta: {
2325
+ elementId: {type: 'locus' as const, id: 0, version: 100},
2326
+ dataSetNames: ['main'],
2327
+ },
2328
+ data: {info: 'data'},
2329
+ },
2330
+ ]);
2331
+
2332
+ // Send roster drop message (SELF object with no data) to trigger MEETING_ENDED
2333
+ const rosterDropMessage = {
2334
+ dataSets: [createDataSet('self', 1, 2101)],
2335
+ visibleDataSetsUrl,
2336
+ locusUrl,
2337
+ locusStateElements: [
2338
+ {
2339
+ htMeta: {
2340
+ elementId: {type: 'self' as const, id: 4, version: 102},
2341
+ dataSetNames: ['self'],
2342
+ },
2343
+ data: undefined, // roster drop triggers MEETING_ENDED
2344
+ },
2345
+ ],
2346
+ };
2347
+
2348
+ await parser.handleMessage(rosterDropMessage, 'roster drop message');
2349
+
2350
+ // Callback should be called with MEETING_ENDED
2351
+ assert.calledOnceWithExactly(callback, LocusInfoUpdateType.MEETING_ENDED, {
2352
+ updatedObjects: undefined,
2353
+ });
2354
+ });
2355
+ });
1524
2356
  });