@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.
- package/dist/breakouts/breakout.js +1 -1
- package/dist/breakouts/index.js +1 -1
- package/dist/constants.js +8 -2
- package/dist/constants.js.map +1 -1
- package/dist/hashTree/constants.js +23 -0
- package/dist/hashTree/constants.js.map +1 -0
- package/dist/hashTree/hashTree.js +516 -0
- package/dist/hashTree/hashTree.js.map +1 -0
- package/dist/hashTree/hashTreeParser.js +521 -0
- package/dist/hashTree/hashTreeParser.js.map +1 -0
- package/dist/interpretation/index.js +1 -1
- package/dist/interpretation/siLanguage.js +1 -1
- package/dist/locus-info/index.js +301 -59
- package/dist/locus-info/index.js.map +1 -1
- package/dist/meeting/brbState.js +14 -12
- package/dist/meeting/brbState.js.map +1 -1
- package/dist/meeting/index.js +110 -12
- package/dist/meeting/index.js.map +1 -1
- package/dist/meeting/muteState.js +2 -5
- package/dist/meeting/muteState.js.map +1 -1
- package/dist/meeting/request.js +19 -0
- package/dist/meeting/request.js.map +1 -1
- package/dist/meeting/request.type.js.map +1 -1
- package/dist/meeting/util.js +8 -11
- package/dist/meeting/util.js.map +1 -1
- package/dist/meetings/index.js +6 -2
- package/dist/meetings/index.js.map +1 -1
- package/dist/member/index.js.map +1 -1
- package/dist/member/types.js.map +1 -1
- package/dist/members/collection.js +13 -0
- package/dist/members/collection.js.map +1 -1
- package/dist/members/index.js +44 -23
- package/dist/members/index.js.map +1 -1
- package/dist/members/request.js +3 -3
- package/dist/members/request.js.map +1 -1
- package/dist/members/util.js +18 -6
- package/dist/members/util.js.map +1 -1
- package/dist/multistream/sendSlotManager.js +32 -2
- package/dist/multistream/sendSlotManager.js.map +1 -1
- package/dist/types/constants.d.ts +6 -0
- package/dist/types/hashTree/constants.d.ts +8 -0
- package/dist/types/hashTree/hashTree.d.ts +128 -0
- package/dist/types/hashTree/hashTreeParser.d.ts +152 -0
- package/dist/types/locus-info/index.d.ts +93 -3
- package/dist/types/meeting/brbState.d.ts +0 -1
- package/dist/types/meeting/index.d.ts +29 -3
- package/dist/types/meeting/request.d.ts +9 -1
- package/dist/types/meeting/request.type.d.ts +74 -0
- package/dist/types/meeting/util.d.ts +3 -3
- package/dist/types/member/types.d.ts +1 -0
- package/dist/types/members/collection.d.ts +6 -0
- package/dist/types/members/index.d.ts +15 -3
- package/dist/types/members/request.d.ts +1 -1
- package/dist/types/members/util.d.ts +5 -2
- package/dist/types/multistream/sendSlotManager.d.ts +16 -0
- package/dist/webinar/index.js +1 -1
- package/package.json +24 -23
- package/src/constants.ts +7 -0
- package/src/hashTree/constants.ts +12 -0
- package/src/hashTree/hashTree.ts +460 -0
- package/src/hashTree/hashTreeParser.ts +556 -0
- package/src/locus-info/index.ts +393 -58
- package/src/meeting/brbState.ts +9 -7
- package/src/meeting/index.ts +104 -6
- package/src/meeting/muteState.ts +2 -6
- package/src/meeting/request.ts +16 -0
- package/src/meeting/request.type.ts +64 -0
- package/src/meeting/util.ts +17 -20
- package/src/meetings/index.ts +17 -3
- package/src/member/index.ts +1 -0
- package/src/member/types.ts +1 -0
- package/src/members/collection.ts +11 -0
- package/src/members/index.ts +33 -7
- package/src/members/request.ts +2 -2
- package/src/members/util.ts +14 -3
- package/src/multistream/sendSlotManager.ts +34 -2
- package/test/unit/spec/hashTree/hashTree.ts +394 -0
- package/test/unit/spec/hashTree/hashTreeParser.ts +156 -0
- package/test/unit/spec/locus-info/index.js +506 -55
- package/test/unit/spec/meeting/brbState.ts +9 -9
- package/test/unit/spec/meeting/index.js +475 -42
- package/test/unit/spec/meeting/request.js +71 -0
- package/test/unit/spec/members/index.js +33 -10
- package/test/unit/spec/members/request.js +2 -2
- package/test/unit/spec/members/utils.js +27 -7
- package/test/unit/spec/multistream/sendSlotManager.ts +59 -0
- 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
|
-
`
|
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
|
-
`
|
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
|
+
});
|