@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
@@ -7,10 +7,20 @@ import {
7
7
  StreamState,
8
8
  } from '@webex/internal-media-core';
9
9
 
10
+ /**
11
+ * This class is used to manage the sendSlots for the given media types.
12
+ */
10
13
  export default class SendSlotManager {
11
14
  private readonly slots: Map<MediaType, SendSlot> = new Map();
12
15
  private readonly LoggerProxy: any;
16
+ private readonly sourceStateOverrides: Map<MediaType, StreamState> = new Map();
13
17
 
18
+ /**
19
+ * Constructor for SendSlotManager
20
+ *
21
+ * @param {any} LoggerProxy is used to log the messages
22
+ * @constructor
23
+ */
14
24
  constructor(LoggerProxy: any) {
15
25
  this.LoggerProxy = LoggerProxy;
16
26
  }
@@ -93,7 +103,7 @@ export default class SendSlotManager {
93
103
  public setSourceStateOverride(mediaType: MediaType, state: StreamState | null) {
94
104
  if (mediaType !== MediaType.VideoMain) {
95
105
  throw new Error(
96
- `sendSlotManager cannot set source state override which media type is ${mediaType}`
106
+ `Invalid media type '${mediaType}'. Source state overrides are only applicable to ${MediaType.VideoMain}.`
97
107
  );
98
108
  }
99
109
 
@@ -103,17 +113,39 @@ export default class SendSlotManager {
103
113
  throw new Error(`Slot for ${mediaType} does not exist`);
104
114
  }
105
115
 
116
+ const currentStateOverride = this.getSourceStateOverride(mediaType);
117
+ if (currentStateOverride === state) {
118
+ return;
119
+ }
120
+
106
121
  if (state) {
107
122
  slot.setSourceStateOverride(state);
123
+ this.sourceStateOverrides.set(mediaType, state);
108
124
  } else {
109
125
  slot.clearSourceStateOverride();
126
+ this.sourceStateOverrides.delete(mediaType);
110
127
  }
111
128
 
112
129
  this.LoggerProxy.logger.info(
113
- `SendSlotsManager->setSourceStateOverride#set source state override for ${mediaType} to ${state}`
130
+ `SendSlotManager->setSourceStateOverride#set source state override for ${mediaType} to ${state}`
114
131
  );
115
132
  }
116
133
 
134
+ /**
135
+ * Gets the source state override for the given media type.
136
+ * @param {MediaType} mediaType - The type of media to get the source state override for.
137
+ * @returns {StreamState | null} - The current source state override or null if not set.
138
+ */
139
+ private getSourceStateOverride(mediaType: MediaType): StreamState | null {
140
+ if (mediaType !== MediaType.VideoMain) {
141
+ throw new Error(
142
+ `Invalid media type '${mediaType}'. Source state overrides are only applicable to ${MediaType.VideoMain}.`
143
+ );
144
+ }
145
+
146
+ return this.sourceStateOverrides.get(mediaType) || null;
147
+ }
148
+
117
149
  /**
118
150
  * This method publishes the given stream to the sendSlot for the given mediaType
119
151
  * @param {MediaType} mediaType MediaType of the sendSlot to which a stream needs to be published (AUDIO_MAIN/VIDEO_MAIN/AUDIO_SLIDES/VIDEO_SLIDES)
@@ -0,0 +1,394 @@
1
+ import HashTree from '@webex/plugin-meetings/src/hashTree/hashTree';
2
+ import {EMPTY_HASH} from '@webex/plugin-meetings/src/hashTree/constants';
3
+
4
+ import { expect } from "@webex/test-helper-chai";
5
+
6
+ // Define a type for the leaf data items used in tests
7
+ type LeafDataItem = {
8
+ type: string;
9
+ id: number;
10
+ version: number;
11
+ };
12
+
13
+ describe('HashTree', () => {
14
+ it('should initialize with empty leaves and hashes', () => {
15
+ const leafData: LeafDataItem[] = [];
16
+ const numLeaves = 4;
17
+ const hashTree = new HashTree(leafData, numLeaves);
18
+
19
+ expect(hashTree.leaves).to.deep.equal(new Array(numLeaves).fill(null).map(() => ({})));
20
+ expect(hashTree.leafHashes).to.deep.equal(new Array(numLeaves).fill(EMPTY_HASH));
21
+ expect(hashTree.getLeafCount()).to.equal(numLeaves);
22
+ expect(hashTree.getTotalItemCount()).to.equal(0);
23
+ });
24
+
25
+ it('constructor should allow 0 leaves', () => {
26
+ const leafData: LeafDataItem[] = [];
27
+ const numLeaves = 0;
28
+ const hashTree = new HashTree(leafData, numLeaves);
29
+ expect(hashTree.getLeafCount()).to.equal(0);
30
+ expect(hashTree.getRootHash()).to.equal(EMPTY_HASH);
31
+ expect(hashTree.getHashes()).to.deep.equal([EMPTY_HASH]);
32
+ });
33
+
34
+ it('number of leaves must be 0 or a power of 2', () => {
35
+ const leafData: LeafDataItem[] = [];
36
+ const numLeaves = 3; // Not a power of 2
37
+ expect(() => new HashTree(leafData, numLeaves)).to.throw('Number of leaves must be a power of 2, saw 3');
38
+ const numLeavesNegative = -1;
39
+ expect(() => new HashTree(leafData, numLeavesNegative)).to.throw('Number of leaves must be a power of 2, saw -1');
40
+ });
41
+
42
+ it('should have the correct hashes after putting ObjectIds using constructor', () => {
43
+ const oids: LeafDataItem[] = [
44
+ {type: 'participant', id: 1, version: 3} // Hashes to bucket 1 % 4 = 1
45
+ ];
46
+ // numLeaves is 4. Item id 1 % 4 = 1. So, leafHashes[1] will be updated.
47
+ // leafHashes[0], leafHashes[2], leafHashes[3] remain EMPTY_HASH.
48
+ const tree = new HashTree(oids, 4);
49
+
50
+ // These are the expected hash values from the Java reference for a similar structure.
51
+ // The actual values depend on the specific XXHash128 implementation and input serialization.
52
+ // For this test, we'll use the previously provided values, assuming they are correct for the TS implementation.
53
+ expect(tree.getHashes()).to.deep.equal([
54
+ "24a75d115a0a90ddb376a02b435c780f", // Root hash
55
+ "457eeb22808eadfcff92ee47d67acbbf", // Internal node (children: leaf 0, leaf 1)
56
+ "b113a76304e3a7121afecfe1606ee1c1", // Internal node (children: leaf 2, leaf 3)
57
+ EMPTY_HASH, // Leaf 0 hash (empty)
58
+ "42df811f5a902c5b6bfcf50c7004e275", // Leaf 1 hash (for item {type: 'participant', id: 1, version: 3})
59
+ EMPTY_HASH, // Leaf 2 hash (empty)
60
+ EMPTY_HASH // Leaf 3 hash (empty)
61
+ ]);
62
+ expect(tree.getRootHash()).to.equal("24a75d115a0a90ddb376a02b435c780f");
63
+ });
64
+
65
+ it('should have the correct hashes after putting multiple ObjectIds using constructor', () => {
66
+ const oids: LeafDataItem[] = [
67
+ {type: "typeA", id: 1, version: 3}, // Leaf 1 (1 % 4 = 1)
68
+ {type: "typeA", id: 6, version: 2}, // Leaf 2 (6 % 4 = 2)
69
+ {type: "typeA", id: 7, version: 1}, // Leaf 3 (7 % 4 = 3)
70
+ {type: "typeB", id: 11, version: 4},// Leaf 3 (11 % 4 = 3)
71
+ ];
72
+ const tree = new HashTree(oids, 4);
73
+
74
+ // Corrected expected hashes based on the test failure output
75
+ expect(tree.getHashes()).to.deep.equal([
76
+ "c8415198d4abca6f885fe974e9b3729d", // Root
77
+ "457eeb22808eadfcff92ee47d67acbbf", // Internal node (L0, L1)
78
+ "5c9ba182a069c16a77a1928fce52dad8", // Internal node (L2, L3)
79
+ EMPTY_HASH, // Leaf 0 (empty)
80
+ "42df811f5a902c5b6bfcf50c7004e275", // Leaf 1 (item id 1)
81
+ "feb384d8ac6374ffdbee92a9f48f2b40", // Leaf 2 (item id 6)
82
+ "ebfa4f7e104e1e30fbb6b8857ccb685d" // Leaf 3 (items id 7, 11)
83
+ ]);
84
+ expect(tree.getRootHash()).to.equal("c8415198d4abca6f885fe974e9b3729d");
85
+ });
86
+
87
+ it('should putItems and compute hashes correctly', () => {
88
+ const initialLeafData: LeafDataItem[] = [];
89
+ const numLeaves = 4;
90
+ const hashTree = new HashTree(initialLeafData, numLeaves);
91
+
92
+ const itemsToPut: LeafDataItem[] = [
93
+ { type: 'participant', id: 1, version: 1 }, // bucket 1
94
+ { type: 'participant', id: 2, version: 1 }, // bucket 2
95
+ ];
96
+ const results = hashTree.putItems(itemsToPut);
97
+
98
+ expect(results).to.deep.equal([true, true]);
99
+ expect(hashTree.leaves[1]['participant'][1]).to.deep.equal({ type: 'participant', id: 1, version: 1 });
100
+ expect(hashTree.leaves[2]['participant'][2]).to.deep.equal({ type: 'participant', id: 2, version: 1 });
101
+ expect(hashTree.leafHashes[0]).to.equal(EMPTY_HASH);
102
+ expect(hashTree.leafHashes[1]).to.not.equal(EMPTY_HASH);
103
+ expect(hashTree.leafHashes[2]).to.not.equal(EMPTY_HASH);
104
+ expect(hashTree.leafHashes[3]).to.equal(EMPTY_HASH);
105
+ expect(hashTree.getTotalItemCount()).to.equal(2);
106
+ });
107
+
108
+ it('putItem should add a single item and update hash', () => {
109
+ const hashTree = new HashTree([], 2);
110
+ const item: LeafDataItem = {type: 'data', id: 3, version: 1}; // bucket 1
111
+
112
+ const result = hashTree.putItem(item);
113
+ expect(result).to.be.true;
114
+ expect(hashTree.leaves[1]['data'][3]).to.deep.equal(item);
115
+ expect(hashTree.leafHashes[1]).to.not.equal(EMPTY_HASH);
116
+ expect(hashTree.getTotalItemCount()).to.equal(1);
117
+
118
+ const itemSameVersion = {type: 'data', id: 3, version: 1};
119
+ const resultSame = hashTree.putItem(itemSameVersion);
120
+ expect(resultSame).to.be.false; // Not updated as version is not newer
121
+
122
+ const itemNewerVersion = {type: 'data', id: 3, version: 2};
123
+ const resultNewer = hashTree.putItem(itemNewerVersion);
124
+ expect(resultNewer).to.be.true;
125
+ expect(hashTree.leaves[1]['data'][3].version).to.equal(2);
126
+ });
127
+
128
+ it('putItem should return false for tree with 0 leaves', () => {
129
+ const hashTree = new HashTree([], 0);
130
+ const item: LeafDataItem = {type: 'data', id: 1, version: 1};
131
+ expect(hashTree.putItem(item)).to.be.false;
132
+ });
133
+
134
+ it('putItems should return array of false for tree with 0 leaves if items are provided', () => {
135
+ const hashTree = new HashTree([], 0);
136
+ const items: LeafDataItem[] = [{type: 'data', id: 1, version: 1}];
137
+ expect(hashTree.putItems(items)).to.deep.equal([false]);
138
+ });
139
+
140
+
141
+ it('should have correct root hash after putting one item', () => {
142
+ const leafData: LeafDataItem[] = [{type: 'participant', id: 1, version: 10}]; // bucket 1 (1 % 2 = 1)
143
+ const numLeaves = 2;
144
+ const hashTree = new HashTree(leafData, numLeaves);
145
+
146
+ expect(hashTree.leaves[1]['participant'][1]).to.deep.equal({
147
+ type: 'participant',
148
+ id: 1,
149
+ version: 10,
150
+ });
151
+ // This hash is from the original test.
152
+ expect(hashTree.getRootHash()).to.equal('e1cb70c75b488d87cbc8f74934a4290b');
153
+ });
154
+
155
+ it('removeItem should remove an item and update hash', () => {
156
+ const items: LeafDataItem[] = [{type: 'p', id: 1, version: 1}];
157
+ const hashTree = new HashTree(items, 2); // item in bucket 1
158
+ expect(hashTree.getTotalItemCount()).to.equal(1);
159
+ const oldRootHash = hashTree.getRootHash();
160
+
161
+ const result = hashTree.removeItem({type: 'p', id: 1, version: 1});
162
+ expect(result).to.be.true;
163
+ expect(hashTree.getTotalItemCount()).to.equal(0);
164
+ expect(hashTree.leaves[1]['p']).to.be.undefined;
165
+ expect(hashTree.leafHashes[1]).to.equal(EMPTY_HASH);
166
+ expect(hashTree.getRootHash()).to.not.equal(oldRootHash);
167
+ // After removing the only item, it should be like an empty tree with 2 leaves
168
+ const emptyTree = new HashTree([], 2);
169
+ expect(hashTree.getRootHash()).to.equal(emptyTree.getRootHash());
170
+
171
+ const resultNotFound = hashTree.removeItem({type: 'p', id: 1, version: 1});
172
+ expect(resultNotFound).to.be.false;
173
+ });
174
+
175
+ it('removeItem should only remove if version is <= existing (as per new logic)', () => {
176
+ const itemV1 = {type: 'test', id: 5, version: 1}; // bucket 1 (5%2=1)
177
+ const hashTree = new HashTree([itemV1], 2);
178
+
179
+ // Try to remove with older version - should fail if strict "version must be >=" is used for removal item
180
+ // The current removeItem logic: existingItem.version <= item.version for removal
181
+ let removed = hashTree.removeItem({type: 'test', id: 5, version: 0});
182
+ expect(removed).to.be.false;
183
+
184
+ removed = hashTree.removeItem({type: 'test', id: 5, version: 1}); // same version
185
+ expect(removed).to.be.true;
186
+ expect(hashTree.getTotalItemCount()).to.equal(0);
187
+
188
+ hashTree.putItem(itemV1); // re-add
189
+ expect(hashTree.getTotalItemCount()).to.equal(1);
190
+ removed = hashTree.removeItem({type: 'test', id: 5, version: 2}); // newer version in request
191
+ expect(removed).to.be.true;
192
+ expect(hashTree.getTotalItemCount()).to.equal(0);
193
+ });
194
+
195
+ it('removeItem should return false for tree with 0 leaves', () => {
196
+ const hashTree = new HashTree([], 0);
197
+ const item: LeafDataItem = {type: 'data', id: 1, version: 1};
198
+ expect(hashTree.removeItem(item)).to.be.false;
199
+ });
200
+
201
+ it('removeItems should process multiple items', () => {
202
+ const items: LeafDataItem[] = [
203
+ {type: 'a', id: 1, version: 2}, // bucket 1
204
+ {type: 'b', id: 2, version: 2} // bucket 0
205
+ ];
206
+ const hashTree = new HashTree(items, 2);
207
+ expect(hashTree.getTotalItemCount()).to.equal(2);
208
+
209
+ const itemsToRemove: LeafDataItem[] = [
210
+ {type: 'a', id: 1, version: 3}, // remove with newer version (original logic)
211
+ {type: 'b', id: 2, version: 1}, // attempt remove with older version (should fail by original logic)
212
+ {type: 'c', id: 3, version: 1} // item not present
213
+ ];
214
+ const results = hashTree.removeItems(itemsToRemove);
215
+ expect(results).to.deep.equal([true, false, false]);
216
+ expect(hashTree.getTotalItemCount()).to.equal(1); // item 'b' should remain
217
+ expect(hashTree.leaves[1]['a']).to.be.undefined;
218
+ expect(hashTree.leaves[0]['b'][2]).to.deep.equal({type: 'b', id: 2, version: 2});
219
+ });
220
+
221
+ it('removeItems should return array of false for tree with 0 leaves if items are provided', () => {
222
+ const hashTree = new HashTree([], 0);
223
+ const items: LeafDataItem[] = [{type: 'data', id: 1, version: 1}];
224
+ expect(hashTree.removeItems(items)).to.deep.equal([false]);
225
+ });
226
+
227
+ it('returns the correct root hash for an empty tree (0 leaves)', () => {
228
+ const hashTree = new HashTree([], 0);
229
+ expect(hashTree.getRootHash()).to.equal(EMPTY_HASH);
230
+ });
231
+
232
+ it('returns the correct root hash for an empty tree with 2 leaves', () => {
233
+ const hashTree = new HashTree([], 2);
234
+ // This hash is from the original test.
235
+ expect(hashTree.getRootHash()).to.equal('b113a76304e3a7121afecfe1606ee1c1');
236
+ });
237
+
238
+ it('returns the correct root hash for an empty tree with 4 leaves', () => {
239
+ const hashTree = new HashTree([], 4);
240
+ // This hash is from the original test.
241
+ expect(hashTree.getRootHash()).to.equal('b5df9b92242752424d87053a14e6222d');
242
+ });
243
+
244
+ describe('getLeafData', () => {
245
+ it('should return items from a specific leaf', () => {
246
+ const items: LeafDataItem[] = [
247
+ {type: 't1', id: 0, version: 1}, // leaf 0
248
+ {type: 't2', id: 1, version: 1}, // leaf 1
249
+ {type: 't1', id: 2, version: 1}, // leaf 0
250
+ ];
251
+ const tree = new HashTree(items, 2);
252
+ const leaf0Data = tree.getLeafData(0);
253
+ expect(leaf0Data).to.have.deep.members([
254
+ {type: 't1', id: 0, version: 1},
255
+ {type: 't1', id: 2, version: 1}
256
+ ]);
257
+ expect(leaf0Data.length).to.equal(2);
258
+
259
+ const leaf1Data = tree.getLeafData(1);
260
+ expect(leaf1Data).to.have.deep.members([{type: 't2', id: 1, version: 1}]);
261
+ expect(leaf1Data.length).to.equal(1);
262
+ });
263
+
264
+ it('should return empty array for invalid leaf index or empty leaf', () => {
265
+ const tree = new HashTree([{type: 't', id: 0, version: 1}], 2); // item in leaf 0
266
+ expect(tree.getLeafData(1)).to.deep.equal([]); // leaf 1 is empty
267
+ expect(tree.getLeafData(2)).to.deep.equal([]); // invalid index
268
+ expect(tree.getLeafData(-1)).to.deep.equal([]); // invalid index
269
+ });
270
+
271
+ it('should return empty array for tree with 0 leaves', () => {
272
+ const tree = new HashTree([], 0);
273
+ expect(tree.getLeafData(0)).to.deep.equal([]);
274
+ });
275
+ });
276
+
277
+ describe('resize', () => {
278
+ it('should resize the tree and redistribute items', () => {
279
+ const items: LeafDataItem[] = [
280
+ {type: 'a', id: 0, version: 1}, // old leaf 0 (0%2=0)
281
+ {type: 'b', id: 1, version: 1}, // old leaf 1 (1%2=1)
282
+ {type: 'c', id: 2, version: 1}, // old leaf 0 (2%2=0)
283
+ {type: 'd', id: 3, version: 1}, // old leaf 1 (3%2=1)
284
+ ];
285
+ const tree = new HashTree(items, 2);
286
+ expect(tree.getLeafCount()).to.equal(2);
287
+ expect(tree.getTotalItemCount()).to.equal(4);
288
+ const originalRootHash = tree.getRootHash();
289
+
290
+ const resized = tree.resize(4);
291
+ expect(resized).to.be.true;
292
+ expect(tree.getLeafCount()).to.equal(4);
293
+ expect(tree.getTotalItemCount()).to.equal(4); // count should remain same
294
+
295
+ // Check redistribution
296
+ // id:0 -> 0%4 = 0
297
+ // id:1 -> 1%4 = 1
298
+ // id:2 -> 2%4 = 2
299
+ // id:3 -> 3%4 = 3
300
+ expect(tree.getLeafData(0)).to.deep.include({type: 'a', id: 0, version: 1});
301
+ expect(tree.getLeafData(1)).to.deep.include({type: 'b', id: 1, version: 1});
302
+ expect(tree.getLeafData(2)).to.deep.include({type: 'c', id: 2, version: 1});
303
+ expect(tree.getLeafData(3)).to.deep.include({type: 'd', id: 3, version: 1});
304
+ expect(tree.getRootHash()).to.not.equal(originalRootHash); // Hash should change
305
+ });
306
+
307
+ it('should return false if size does not change', () => {
308
+ const tree = new HashTree([], 4);
309
+ expect(tree.resize(4)).to.be.false;
310
+ });
311
+
312
+ it('should throw error for invalid new number of leaves', () => {
313
+ const tree = new HashTree([], 4);
314
+ expect(() => tree.resize(3)).to.throw('New number of leaves must be 0 or a power of 2');
315
+ });
316
+
317
+ it('should handle resize to 0 leaves', () => {
318
+ const items: LeafDataItem[] = [{type: 'a', id: 0, version: 1}];
319
+ const tree = new HashTree(items, 2);
320
+ expect(tree.getTotalItemCount()).to.equal(1);
321
+ tree.resize(0);
322
+ expect(tree.getLeafCount()).to.equal(0);
323
+ expect(tree.getTotalItemCount()).to.equal(0);
324
+ expect(tree.getRootHash()).to.equal(EMPTY_HASH);
325
+ expect(tree.leaves.length).to.equal(0);
326
+ expect(tree.leafHashes.length).to.equal(0);
327
+ });
328
+
329
+ it('should handle resize from 0 leaves', () => {
330
+ const tree = new HashTree([], 0);
331
+ tree.resize(2);
332
+ expect(tree.getLeafCount()).to.equal(2);
333
+ expect(tree.getTotalItemCount()).to.equal(0);
334
+ const emptyTree = new HashTree([], 2);
335
+ expect(tree.getRootHash()).to.equal(emptyTree.getRootHash());
336
+ });
337
+ });
338
+
339
+ describe('diffHashes', () => {
340
+ it('should return empty array if hashes are identical', () => {
341
+ const items: LeafDataItem[] = [{type: 'x', id: 1, version: 1}];
342
+ const tree1 = new HashTree(items, 2);
343
+ const tree2 = new HashTree(items, 2);
344
+ expect(tree1.diffHashes(tree2.getHashes())).to.deep.equal([]);
345
+ });
346
+
347
+ it('should return differing leaf indices', () => {
348
+ const tree1 = new HashTree([{type: 'x', id: 0, version: 1}], 4); // item in leaf 0
349
+ const tree2 = new HashTree([{type: 'y', id: 1, version: 1}], 4); // item in leaf 1
350
+ // tree1: leaf 0 has item, leaves 1,2,3 empty
351
+ // tree2: leaf 1 has item, leaves 0,2,3 empty
352
+ // Expected diffs: leaf 0 (present in 1, not in 2), leaf 1 (present in 2, not in 1)
353
+ const diff = tree1.diffHashes(tree2.getHashes());
354
+ expect(diff).to.include.members([0, 1]);
355
+ // If one leaf's hash is EMPTY_HASH and the other's is a computed hash, they are different.
356
+ });
357
+
358
+ it('should return all leaf indices if externalHashes is for a different structure (e.g. too short)', () => {
359
+ const tree = new HashTree([{type: 'x', id: 0, version: 1}], 4);
360
+ const externalHashesShort = [EMPTY_HASH, EMPTY_HASH]; // Too short for 4 leaves + internal nodes
361
+ expect(tree.diffHashes(externalHashesShort)).to.deep.equal([0,1,2,3]);
362
+ });
363
+
364
+ it('should handle diff for 0-leaf trees', () => {
365
+ const tree0 = new HashTree([], 0);
366
+ expect(tree0.diffHashes([EMPTY_HASH])).to.deep.equal([]);
367
+ expect(tree0.diffHashes(["some_other_hash"])).to.deep.equal([]); // No leaves to differ
368
+ const tree2 = new HashTree([],2);
369
+ // Comparing a 0-leaf tree with a 2-leaf tree's hashes
370
+ expect(tree0.diffHashes(tree2.getHashes())).to.deep.equal([]);
371
+ });
372
+
373
+ it('should correctly identify differences when one leaf changes', () => {
374
+ const initialItems: LeafDataItem[] = [
375
+ { type: 'a', id: 0, version: 1 }, // leaf 0
376
+ { type: 'b', id: 1, version: 1 } // leaf 1
377
+ ];
378
+ const tree1 = new HashTree(initialItems, 2);
379
+ const tree1Hashes = tree1.getHashes();
380
+
381
+ const modifiedItems: LeafDataItem[] = [
382
+ { type: 'a', id: 0, version: 1 }, // leaf 0 (same)
383
+ { type: 'b', id: 1, version: 2 } // leaf 1 (changed version)
384
+ ];
385
+ const tree2 = new HashTree(modifiedItems, 2);
386
+
387
+ const diff1_2 = tree1.diffHashes(tree2.getHashes());
388
+ expect(diff1_2).to.deep.equal([1]); // Leaf 1 should differ
389
+
390
+ const diff2_1 = tree2.diffHashes(tree1Hashes);
391
+ expect(diff2_1).to.deep.equal([1]);
392
+ });
393
+ });
394
+ });
@@ -0,0 +1,156 @@
1
+ import HashTreeParser from '@webex/plugin-meetings/src/hashTree/hashTreeParser';
2
+ import HashTree from '@webex/plugin-meetings/src/hashTree/hashTree';
3
+ import { expect } from "@webex/test-helper-chai";
4
+
5
+ const exampleInitialLocus = {
6
+ dataSets: [
7
+ {
8
+ url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/main',
9
+ root: '9bb9d5a911a74d53a915b4dfbec7329f',
10
+ version: 51118,
11
+ leafCount: 16,
12
+ name: 'main',
13
+ idleMs: 1000,
14
+ backoff: {maxMs: 1000, exponent: 2}
15
+ },
16
+ {
17
+ url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/713e9f99/datasets/self',
18
+ root: '5b8cc7ffda1346d2bfb1c0b60b8ab601',
19
+ version: 89891,
20
+ leafCount: 1,
21
+ name: 'self',
22
+ idleMs: 1000,
23
+ backoff: {maxMs: 1000, exponent: 2}
24
+ },
25
+ {
26
+ url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/atd-unmuted',
27
+ root: '9279d2e149da43a1b8e2cd7cbf77f9f0',
28
+ version: 91277,
29
+ leafCount: 16,
30
+ name: 'atd-unmuted',
31
+ idleMs: 1000,
32
+ backoff: {maxMs: 1000, exponent: 2}
33
+ },
34
+ ],
35
+ locus: {
36
+ url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f',
37
+ htMeta: {
38
+ elementId: {
39
+ type: 'LOCUS',
40
+ id: 0,
41
+ version: 5678,
42
+ },
43
+ dataSetNames: ['main'],
44
+ },
45
+ participants: [
46
+ {
47
+ url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/11941033',
48
+ person: {},
49
+ htMeta: {
50
+ elementId: {
51
+ type: 'PARTICIPANT',
52
+ id: 14,
53
+ version: 5678,
54
+ },
55
+ dataSetNames: ['atd-active', 'attendees', 'atd-unmuted'],
56
+ },
57
+ },
58
+ ],
59
+ self: {
60
+ url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/11941033',
61
+ visibleDataSets: ['main', 'self', 'atd-unmuted'],
62
+ person: {},
63
+ htMeta: {
64
+ elementId: {
65
+ type: 'SELF',
66
+ id: 4,
67
+ version: 5678,
68
+ },
69
+ dataSetNames: ['self'],
70
+ },
71
+ },
72
+ },
73
+ };
74
+
75
+ describe('HashTreeParser', () => {
76
+ it('should correctly initialize trees from initialLocus data', () => {
77
+ const parser = new HashTreeParser({initialLocus: exampleInitialLocus, webexRequest: () => Promise.resolve(), locusInfoUpdateCallback : () => {}});
78
+
79
+ // Check that the correct number of trees are created
80
+ expect(Object.keys(parser.dataSets).length).to.equal(3);
81
+
82
+ // Verify the 'main' tree
83
+ const mainTree = parser.dataSets.main.hashTree;
84
+ expect(mainTree).to.be.instanceOf(HashTree);
85
+ const expectedMainLeaves = new Array(16).fill(null).map(() => ({}));
86
+ expectedMainLeaves[0 % 16] = { LOCUS: { 0: { type: 'LOCUS', id: 0, version: 5678 } } };
87
+ expect(mainTree.leaves).to.deep.equal(expectedMainLeaves);
88
+ expect(mainTree.numLeaves).to.equal(16);
89
+
90
+ // Verify the 'self' tree
91
+ const selfTree = parser.dataSets.self.hashTree;
92
+ expect(selfTree).to.be.instanceOf(HashTree);
93
+ const expectedSelfLeaves = new Array(1).fill(null).map(() => ({}));
94
+ expectedSelfLeaves[4 % 1] = { SELF: { 4: { type: 'SELF', id: 4, version: 5678 } } };
95
+ expect(selfTree.leaves).to.deep.equal(expectedSelfLeaves);
96
+ expect(selfTree.numLeaves).to.equal(1);
97
+
98
+ // Verify the 'atd-unmuted' tree
99
+ const atdUnmutedTree = parser.dataSets['atd-unmuted'].hashTree;
100
+ expect(atdUnmutedTree).to.be.instanceOf(HashTree);
101
+ const expectedAtdUnmutedLeaves = new Array(16).fill(null).map(() => ({}));
102
+ expectedAtdUnmutedLeaves[14 % 16] = { PARTICIPANT: { 14: { type: 'PARTICIPANT', id: 14, version: 5678 } } };
103
+ expect(atdUnmutedTree.leaves).to.deep.equal(expectedAtdUnmutedLeaves);
104
+ expect(atdUnmutedTree.numLeaves).to.equal(16);
105
+
106
+ // Ensure no other trees were created
107
+ expect(parser.dataSets['atd-active']).to.be.undefined;
108
+ expect(parser.dataSets.attendees).to.be.undefined;
109
+ });
110
+
111
+ it('should handle datasets with no corresponding metadata found', () => {
112
+ const modifiedLocus = JSON.parse(JSON.stringify(exampleInitialLocus));
113
+ // Remove a participant meta to simulate missing data for 'atd-unmuted'
114
+ modifiedLocus.locus.participants = [];
115
+ // Add a new dataset that won't have corresponding metadata
116
+ modifiedLocus.dataSets.push({
117
+ url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/empty-set',
118
+ root: 'f00f00f00f00f00f00f00f00f00f00f0',
119
+ version: 1,
120
+ leafCount: 4,
121
+ name: 'empty-set',
122
+ });
123
+
124
+
125
+ const parser = new HashTreeParser({initialLocus: modifiedLocus, webexRequest: () => Promise.resolve(), locusInfoUpdateCallback : () => {}});
126
+
127
+ expect(Object.keys(parser.dataSets).length).to.equal(4); // main, self, atd-unmuted (now empty), empty-set
128
+
129
+ // 'main' and 'self' should be populated as before
130
+ const mainTree = parser.dataSets.main.hashTree;
131
+ const expectedMainLeaves = new Array(16).fill(null).map(() => ({}));
132
+ expectedMainLeaves[0 % 16] = { LOCUS: { 0: { type: 'LOCUS', id: 0, version: 5678 } } };
133
+ expect(mainTree.leaves).to.deep.equal(expectedMainLeaves);
134
+ expect(mainTree.numLeaves).to.equal(16);
135
+
136
+ const selfTree = parser.dataSets.self.hashTree;
137
+ const expectedSelfLeaves = new Array(1).fill(null).map(() => ({}));
138
+ expectedSelfLeaves[4 % 1] = { SELF: { 4: { type: 'SELF', id: 4, version: 5678 } } };
139
+ expect(selfTree.leaves).to.deep.equal(expectedSelfLeaves);
140
+ expect(selfTree.numLeaves).to.equal(1);
141
+
142
+ // 'atd-unmuted' metadata was removed from locus, so leaves should be empty
143
+ const atdUnmutedTree = parser.dataSets['atd-unmuted'].hashTree;
144
+ expect(atdUnmutedTree).to.be.instanceOf(HashTree);
145
+ const expectedAtdUnmutedEmptyLeaves = new Array(16).fill(null).map(() => ({}));
146
+ expect(atdUnmutedTree.leaves).to.deep.equal(expectedAtdUnmutedEmptyLeaves);
147
+ expect(atdUnmutedTree.numLeaves).to.equal(16); // leafCount from dataSet definition
148
+
149
+ // 'empty-set' was added to dataSets but has no metadata in locus
150
+ const emptySetTree = parser.dataSets['empty-set'].hashTree;
151
+ expect(emptySetTree).to.be.instanceOf(HashTree);
152
+ const expectedEmptySetLeaves = new Array(4).fill(null).map(() => ({})); // leafCount is 4
153
+ expect(emptySetTree.leaves).to.deep.equal(expectedEmptySetLeaves);
154
+ expect(emptySetTree.numLeaves).to.equal(4);
155
+ });
156
+ });