@webex/plugin-meetings 3.10.0-next.9 → 3.10.0-webex-services-ready.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 +11 -3
- package/dist/constants.js.map +1 -1
- package/dist/hashTree/constants.js +20 -0
- package/dist/hashTree/constants.js.map +1 -0
- package/dist/hashTree/hashTree.js +515 -0
- package/dist/hashTree/hashTree.js.map +1 -0
- package/dist/hashTree/hashTreeParser.js +1266 -0
- package/dist/hashTree/hashTreeParser.js.map +1 -0
- package/dist/hashTree/types.js +21 -0
- package/dist/hashTree/types.js.map +1 -0
- package/dist/hashTree/utils.js +48 -0
- package/dist/hashTree/utils.js.map +1 -0
- package/dist/interpretation/index.js +1 -1
- package/dist/interpretation/siLanguage.js +1 -1
- package/dist/locus-info/index.js +511 -48
- package/dist/locus-info/index.js.map +1 -1
- package/dist/locus-info/types.js +7 -0
- package/dist/locus-info/types.js.map +1 -0
- package/dist/meeting/index.js +41 -15
- package/dist/meeting/index.js.map +1 -1
- package/dist/meeting/util.js +1 -0
- package/dist/meeting/util.js.map +1 -1
- package/dist/meetings/index.js +112 -70
- package/dist/meetings/index.js.map +1 -1
- package/dist/metrics/constants.js +3 -1
- package/dist/metrics/constants.js.map +1 -1
- package/dist/reachability/clusterReachability.js +44 -358
- package/dist/reachability/clusterReachability.js.map +1 -1
- package/dist/reachability/reachability.types.js +14 -1
- package/dist/reachability/reachability.types.js.map +1 -1
- package/dist/reachability/reachabilityPeerConnection.js +445 -0
- package/dist/reachability/reachabilityPeerConnection.js.map +1 -0
- package/dist/types/constants.d.ts +26 -21
- package/dist/types/hashTree/constants.d.ts +8 -0
- package/dist/types/hashTree/hashTree.d.ts +129 -0
- package/dist/types/hashTree/hashTreeParser.d.ts +260 -0
- package/dist/types/hashTree/types.d.ts +25 -0
- package/dist/types/hashTree/utils.d.ts +9 -0
- package/dist/types/locus-info/index.d.ts +91 -42
- package/dist/types/locus-info/types.d.ts +46 -0
- package/dist/types/meeting/index.d.ts +22 -9
- package/dist/types/meetings/index.d.ts +9 -2
- package/dist/types/metrics/constants.d.ts +2 -0
- package/dist/types/reachability/clusterReachability.d.ts +10 -88
- package/dist/types/reachability/reachability.types.d.ts +12 -1
- package/dist/types/reachability/reachabilityPeerConnection.d.ts +111 -0
- package/dist/webinar/index.js +1 -1
- package/package.json +22 -21
- package/src/constants.ts +13 -1
- package/src/hashTree/constants.ts +9 -0
- package/src/hashTree/hashTree.ts +463 -0
- package/src/hashTree/hashTreeParser.ts +1161 -0
- package/src/hashTree/types.ts +30 -0
- package/src/hashTree/utils.ts +42 -0
- package/src/locus-info/index.ts +556 -85
- package/src/locus-info/types.ts +48 -0
- package/src/meeting/index.ts +58 -26
- package/src/meeting/util.ts +1 -0
- package/src/meetings/index.ts +104 -51
- package/src/metrics/constants.ts +2 -0
- package/src/reachability/clusterReachability.ts +50 -347
- package/src/reachability/reachability.types.ts +15 -1
- package/src/reachability/reachabilityPeerConnection.ts +416 -0
- package/test/unit/spec/hashTree/hashTree.ts +655 -0
- package/test/unit/spec/hashTree/hashTreeParser.ts +1532 -0
- package/test/unit/spec/hashTree/utils.ts +103 -0
- package/test/unit/spec/locus-info/index.js +667 -1
- package/test/unit/spec/meeting/index.js +91 -20
- package/test/unit/spec/meeting/utils.js +77 -0
- package/test/unit/spec/meetings/index.js +71 -26
- package/test/unit/spec/reachability/clusterReachability.ts +281 -138
|
@@ -0,0 +1,1532 @@
|
|
|
1
|
+
import HashTreeParser, {
|
|
2
|
+
LocusInfoUpdateType,
|
|
3
|
+
} from '@webex/plugin-meetings/src/hashTree/hashTreeParser';
|
|
4
|
+
import HashTree from '@webex/plugin-meetings/src/hashTree/hashTree';
|
|
5
|
+
import {expect} from '@webex/test-helper-chai';
|
|
6
|
+
import sinon from 'sinon';
|
|
7
|
+
import {assert} from '@webex/test-helper-chai';
|
|
8
|
+
|
|
9
|
+
const exampleInitialLocus = {
|
|
10
|
+
dataSets: [
|
|
11
|
+
{
|
|
12
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/main',
|
|
13
|
+
root: '9bb9d5a911a74d53a915b4dfbec7329f',
|
|
14
|
+
version: 1000,
|
|
15
|
+
leafCount: 16,
|
|
16
|
+
name: 'main',
|
|
17
|
+
idleMs: 1000,
|
|
18
|
+
backoff: {maxMs: 1000, exponent: 2},
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/713e9f99/datasets/self',
|
|
22
|
+
root: '5b8cc7ffda1346d2bfb1c0b60b8ab601',
|
|
23
|
+
version: 2000,
|
|
24
|
+
leafCount: 1,
|
|
25
|
+
name: 'self',
|
|
26
|
+
idleMs: 1000,
|
|
27
|
+
backoff: {maxMs: 1000, exponent: 2},
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/atd-unmuted',
|
|
31
|
+
root: '9279d2e149da43a1b8e2cd7cbf77f9f0',
|
|
32
|
+
version: 3000,
|
|
33
|
+
leafCount: 16,
|
|
34
|
+
name: 'atd-unmuted',
|
|
35
|
+
idleMs: 1000,
|
|
36
|
+
backoff: {maxMs: 1000, exponent: 2},
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
locus: {
|
|
40
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f',
|
|
41
|
+
htMeta: {
|
|
42
|
+
elementId: {
|
|
43
|
+
type: 'locus',
|
|
44
|
+
id: 0,
|
|
45
|
+
version: 200,
|
|
46
|
+
},
|
|
47
|
+
dataSetNames: ['main'],
|
|
48
|
+
},
|
|
49
|
+
participants: [
|
|
50
|
+
{
|
|
51
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/11941033',
|
|
52
|
+
person: {},
|
|
53
|
+
htMeta: {
|
|
54
|
+
elementId: {
|
|
55
|
+
type: 'participant',
|
|
56
|
+
id: 14,
|
|
57
|
+
version: 300,
|
|
58
|
+
},
|
|
59
|
+
dataSetNames: ['atd-active', 'attendees', 'atd-unmuted'],
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
self: {
|
|
64
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/11941033',
|
|
65
|
+
visibleDataSets: ['main', 'self', 'atd-unmuted'],
|
|
66
|
+
person: {},
|
|
67
|
+
htMeta: {
|
|
68
|
+
elementId: {
|
|
69
|
+
type: 'self',
|
|
70
|
+
id: 4,
|
|
71
|
+
version: 100,
|
|
72
|
+
},
|
|
73
|
+
dataSetNames: ['self'],
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
function createDataSet(name: string, leafCount: number, version = 1) {
|
|
80
|
+
return {
|
|
81
|
+
url: `https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/${name}`,
|
|
82
|
+
root: '0'.repeat(32),
|
|
83
|
+
version,
|
|
84
|
+
leafCount,
|
|
85
|
+
name,
|
|
86
|
+
idleMs: 1000,
|
|
87
|
+
backoff: {maxMs: 1000, exponent: 2},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Helper function to setup a webexRequest mock for getAllDataSetsMetadata
|
|
92
|
+
function mockGetAllDataSetsMetadata(webexRequest: sinon.SinonStub, url: string, dataSets: any[]) {
|
|
93
|
+
webexRequest
|
|
94
|
+
.withArgs(
|
|
95
|
+
sinon.match({
|
|
96
|
+
method: 'GET',
|
|
97
|
+
uri: url,
|
|
98
|
+
})
|
|
99
|
+
)
|
|
100
|
+
.resolves({
|
|
101
|
+
body: {dataSets},
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Helper function to setup a webexRequest mock for sync requests
|
|
106
|
+
function mockSyncRequest(webexRequest: sinon.SinonStub, datasetUrl: string, response: any = null) {
|
|
107
|
+
const stub = webexRequest.withArgs(
|
|
108
|
+
sinon.match({
|
|
109
|
+
method: 'POST',
|
|
110
|
+
uri: `${datasetUrl}/sync`,
|
|
111
|
+
})
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
if (response === null) {
|
|
115
|
+
stub.resolves({body: {}});
|
|
116
|
+
} else {
|
|
117
|
+
stub.resolves({body: response});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
describe('HashTreeParser', () => {
|
|
122
|
+
const visibleDataSetsUrl = 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/visibleDataSets';
|
|
123
|
+
const locusUrl = 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f';
|
|
124
|
+
|
|
125
|
+
let clock;
|
|
126
|
+
let webexRequest: sinon.SinonStub;
|
|
127
|
+
let callback: sinon.SinonStub;
|
|
128
|
+
let mathRandomStub: sinon.SinonStub;
|
|
129
|
+
|
|
130
|
+
beforeEach(() => {
|
|
131
|
+
clock = sinon.useFakeTimers();
|
|
132
|
+
webexRequest = sinon.stub();
|
|
133
|
+
callback = sinon.stub();
|
|
134
|
+
mathRandomStub = sinon.stub(Math, 'random').returns(0);
|
|
135
|
+
});
|
|
136
|
+
afterEach(() => {
|
|
137
|
+
clock.restore();
|
|
138
|
+
mathRandomStub.restore();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Helper to create a HashTreeParser instance with common defaults
|
|
142
|
+
function createHashTreeParser(initialLocus: any = exampleInitialLocus) {
|
|
143
|
+
return new HashTreeParser({
|
|
144
|
+
initialLocus,
|
|
145
|
+
webexRequest,
|
|
146
|
+
locusInfoUpdateCallback: callback,
|
|
147
|
+
debugId: 'test',
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Helper to create a heartbeat message (without locusStateElements)
|
|
152
|
+
function createHeartbeatMessage(
|
|
153
|
+
dataSetName: string,
|
|
154
|
+
leafCount: number,
|
|
155
|
+
version: number,
|
|
156
|
+
rootHash: string
|
|
157
|
+
) {
|
|
158
|
+
return {
|
|
159
|
+
dataSets: [
|
|
160
|
+
{
|
|
161
|
+
...createDataSet(dataSetName, leafCount, version),
|
|
162
|
+
root: rootHash,
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
visibleDataSetsUrl,
|
|
166
|
+
locusUrl,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Helper to mock getHashesFromLocus response
|
|
171
|
+
function mockGetHashesFromLocusResponse(dataSetUrl: string, hashes: string[], dataSetInfo: any) {
|
|
172
|
+
webexRequest
|
|
173
|
+
.withArgs(
|
|
174
|
+
sinon.match({
|
|
175
|
+
method: 'GET',
|
|
176
|
+
uri: `${dataSetUrl}/hashtree`,
|
|
177
|
+
})
|
|
178
|
+
)
|
|
179
|
+
.resolves({
|
|
180
|
+
body: {
|
|
181
|
+
hashes,
|
|
182
|
+
dataSet: dataSetInfo,
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Helper to mock sendSyncRequestToLocus response
|
|
188
|
+
function mockSendSyncRequestResponse(dataSetUrl: string, response: any) {
|
|
189
|
+
webexRequest
|
|
190
|
+
.withArgs(
|
|
191
|
+
sinon.match({
|
|
192
|
+
method: 'POST',
|
|
193
|
+
uri: `${dataSetUrl}/sync`,
|
|
194
|
+
})
|
|
195
|
+
)
|
|
196
|
+
.resolves({
|
|
197
|
+
body: response,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
it('should correctly initialize trees from initialLocus data', () => {
|
|
201
|
+
const parser = createHashTreeParser();
|
|
202
|
+
|
|
203
|
+
// Check that the correct number of trees are created
|
|
204
|
+
expect(Object.keys(parser.dataSets).length).to.equal(3);
|
|
205
|
+
|
|
206
|
+
// Verify the 'main' tree
|
|
207
|
+
const mainTree = parser.dataSets.main.hashTree;
|
|
208
|
+
expect(mainTree).to.be.instanceOf(HashTree);
|
|
209
|
+
const expectedMainLeaves = new Array(16).fill(null).map(() => ({}));
|
|
210
|
+
expectedMainLeaves[0 % 16] = {locus: {0: {type: 'locus', id: 0, version: 200}}};
|
|
211
|
+
expect(mainTree.leaves).to.deep.equal(expectedMainLeaves);
|
|
212
|
+
expect(mainTree.numLeaves).to.equal(16);
|
|
213
|
+
|
|
214
|
+
// Verify the 'self' tree
|
|
215
|
+
const selfTree = parser.dataSets.self.hashTree;
|
|
216
|
+
expect(selfTree).to.be.instanceOf(HashTree);
|
|
217
|
+
const expectedSelfLeaves = new Array(1).fill(null).map(() => ({}));
|
|
218
|
+
expectedSelfLeaves[4 % 1] = {self: {4: {type: 'self', id: 4, version: 100}}};
|
|
219
|
+
expect(selfTree.leaves).to.deep.equal(expectedSelfLeaves);
|
|
220
|
+
expect(selfTree.numLeaves).to.equal(1);
|
|
221
|
+
|
|
222
|
+
// Verify the 'atd-unmuted' tree
|
|
223
|
+
const atdUnmutedTree = parser.dataSets['atd-unmuted'].hashTree;
|
|
224
|
+
expect(atdUnmutedTree).to.be.instanceOf(HashTree);
|
|
225
|
+
const expectedAtdUnmutedLeaves = new Array(16).fill(null).map(() => ({}));
|
|
226
|
+
expectedAtdUnmutedLeaves[14 % 16] = {
|
|
227
|
+
participant: {14: {type: 'participant', id: 14, version: 300}},
|
|
228
|
+
};
|
|
229
|
+
expect(atdUnmutedTree.leaves).to.deep.equal(expectedAtdUnmutedLeaves);
|
|
230
|
+
expect(atdUnmutedTree.numLeaves).to.equal(16);
|
|
231
|
+
|
|
232
|
+
// Ensure no other trees were created
|
|
233
|
+
expect(parser.dataSets['atd-active']).to.be.undefined;
|
|
234
|
+
expect(parser.dataSets.attendees).to.be.undefined;
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('should handle datasets with no corresponding metadata found', () => {
|
|
238
|
+
const modifiedLocus = JSON.parse(JSON.stringify(exampleInitialLocus));
|
|
239
|
+
// Remove a participant to simulate missing data for 'atd-unmuted'
|
|
240
|
+
modifiedLocus.locus.participants = [];
|
|
241
|
+
// Add a new dataset that won't have corresponding metadata
|
|
242
|
+
modifiedLocus.dataSets.push({
|
|
243
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/empty-set',
|
|
244
|
+
root: 'f00f00f00f00f00f00f00f00f00f00f0',
|
|
245
|
+
version: 5000,
|
|
246
|
+
leafCount: 4,
|
|
247
|
+
name: 'empty-set',
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const parser = createHashTreeParser(modifiedLocus);
|
|
251
|
+
|
|
252
|
+
expect(Object.keys(parser.dataSets).length).to.equal(4); // main, self, atd-unmuted (now empty), empty-set
|
|
253
|
+
|
|
254
|
+
// 'main' and 'self' should be populated as before
|
|
255
|
+
const mainTree = parser.dataSets.main.hashTree;
|
|
256
|
+
const expectedMainLeaves = new Array(16).fill(null).map(() => ({}));
|
|
257
|
+
expectedMainLeaves[0 % 16] = {locus: {0: {type: 'locus', id: 0, version: 200}}};
|
|
258
|
+
expect(mainTree.leaves).to.deep.equal(expectedMainLeaves);
|
|
259
|
+
expect(mainTree.numLeaves).to.equal(16);
|
|
260
|
+
|
|
261
|
+
const selfTree = parser.dataSets.self.hashTree;
|
|
262
|
+
const expectedSelfLeaves = new Array(1).fill(null).map(() => ({}));
|
|
263
|
+
expectedSelfLeaves[4 % 1] = {self: {4: {type: 'self', id: 4, version: 100}}};
|
|
264
|
+
expect(selfTree.leaves).to.deep.equal(expectedSelfLeaves);
|
|
265
|
+
expect(selfTree.numLeaves).to.equal(1);
|
|
266
|
+
|
|
267
|
+
// 'atd-unmuted' metadata was removed from locus, so leaves should be empty
|
|
268
|
+
const atdUnmutedTree = parser.dataSets['atd-unmuted'].hashTree;
|
|
269
|
+
expect(atdUnmutedTree).to.be.instanceOf(HashTree);
|
|
270
|
+
const expectedAtdUnmutedEmptyLeaves = new Array(16).fill(null).map(() => ({}));
|
|
271
|
+
expect(atdUnmutedTree.leaves).to.deep.equal(expectedAtdUnmutedEmptyLeaves);
|
|
272
|
+
expect(atdUnmutedTree.numLeaves).to.equal(16); // leafCount from dataSet definition
|
|
273
|
+
|
|
274
|
+
// 'empty-set' was added to dataSets but has no metadata in locus and is not among visibleDataSets
|
|
275
|
+
// so an entry for it should exist, but hashTree shouldn't be created
|
|
276
|
+
const emptySet = parser.dataSets['empty-set'];
|
|
277
|
+
expect(emptySet.hashTree).to.be.undefined;
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// helper method, needed because both initializeFromMessage and initializeFromGetLociResponse
|
|
281
|
+
// do almost exactly the same thing
|
|
282
|
+
const testInitializationOfDatasetsAndHashTrees = async (testCallback) => {
|
|
283
|
+
// Create a parser with minimal initial data
|
|
284
|
+
const minimalInitialLocus = {
|
|
285
|
+
dataSets: [],
|
|
286
|
+
locus: {
|
|
287
|
+
self: {
|
|
288
|
+
visibleDataSets: ['main', 'self'],
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const hashTreeParser = createHashTreeParser(minimalInitialLocus);
|
|
294
|
+
|
|
295
|
+
// Setup the datasets that will be returned from getAllDataSetsMetadata
|
|
296
|
+
const mainDataSet = createDataSet('main', 16, 1100);
|
|
297
|
+
const selfDataSet = createDataSet('self', 1, 2100);
|
|
298
|
+
const invisibleDataSet = createDataSet('invisible', 4, 4000);
|
|
299
|
+
|
|
300
|
+
mockGetAllDataSetsMetadata(webexRequest, visibleDataSetsUrl, [
|
|
301
|
+
mainDataSet,
|
|
302
|
+
selfDataSet,
|
|
303
|
+
invisibleDataSet,
|
|
304
|
+
]);
|
|
305
|
+
|
|
306
|
+
// Mock sync requests for visible datasets with some updated objects
|
|
307
|
+
const mainSyncResponse = {
|
|
308
|
+
dataSets: [mainDataSet],
|
|
309
|
+
visibleDataSetsUrl,
|
|
310
|
+
locusUrl,
|
|
311
|
+
locusStateElements: [
|
|
312
|
+
{
|
|
313
|
+
htMeta: {
|
|
314
|
+
elementId: {
|
|
315
|
+
type: 'locus',
|
|
316
|
+
id: 1,
|
|
317
|
+
version: 210,
|
|
318
|
+
},
|
|
319
|
+
dataSetNames: ['main'],
|
|
320
|
+
},
|
|
321
|
+
data: {info: {id: 'some-fake-locus-info'}},
|
|
322
|
+
},
|
|
323
|
+
],
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const selfSyncResponse = {
|
|
327
|
+
dataSets: [selfDataSet],
|
|
328
|
+
visibleDataSetsUrl,
|
|
329
|
+
locusUrl,
|
|
330
|
+
locusStateElements: [
|
|
331
|
+
{
|
|
332
|
+
htMeta: {
|
|
333
|
+
elementId: {
|
|
334
|
+
type: 'self',
|
|
335
|
+
id: 2,
|
|
336
|
+
version: 110,
|
|
337
|
+
},
|
|
338
|
+
dataSetNames: ['self'],
|
|
339
|
+
},
|
|
340
|
+
data: {person: {name: 'fake self name'}},
|
|
341
|
+
},
|
|
342
|
+
],
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
mockSyncRequest(webexRequest, mainDataSet.url, mainSyncResponse);
|
|
346
|
+
mockSyncRequest(webexRequest, selfDataSet.url, selfSyncResponse);
|
|
347
|
+
|
|
348
|
+
// call the callback that actually calls the function being tested
|
|
349
|
+
await testCallback(hashTreeParser);
|
|
350
|
+
|
|
351
|
+
// Verify getAllDataSetsMetadata was called with correct URL
|
|
352
|
+
assert.calledWith(
|
|
353
|
+
webexRequest,
|
|
354
|
+
sinon.match({
|
|
355
|
+
method: 'GET',
|
|
356
|
+
uri: visibleDataSetsUrl,
|
|
357
|
+
})
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
// Verify all datasets are added to dataSets
|
|
361
|
+
expect(hashTreeParser.dataSets.main).to.exist;
|
|
362
|
+
expect(hashTreeParser.dataSets.self).to.exist;
|
|
363
|
+
expect(hashTreeParser.dataSets.invisible).to.exist;
|
|
364
|
+
|
|
365
|
+
// Verify hash trees are created only for visible datasets
|
|
366
|
+
expect(hashTreeParser.dataSets.main.hashTree).to.be.instanceOf(HashTree);
|
|
367
|
+
expect(hashTreeParser.dataSets.self.hashTree).to.be.instanceOf(HashTree);
|
|
368
|
+
expect(hashTreeParser.dataSets.invisible.hashTree).to.be.undefined;
|
|
369
|
+
|
|
370
|
+
// Verify hash trees have correct leaf counts
|
|
371
|
+
expect(hashTreeParser.dataSets.main.hashTree.numLeaves).to.equal(16);
|
|
372
|
+
expect(hashTreeParser.dataSets.self.hashTree.numLeaves).to.equal(1);
|
|
373
|
+
|
|
374
|
+
// Verify sync requests were sent for visible datasets
|
|
375
|
+
assert.calledWith(
|
|
376
|
+
webexRequest,
|
|
377
|
+
sinon.match({
|
|
378
|
+
method: 'POST',
|
|
379
|
+
uri: `${mainDataSet.url}/sync`,
|
|
380
|
+
})
|
|
381
|
+
);
|
|
382
|
+
assert.calledWith(
|
|
383
|
+
webexRequest,
|
|
384
|
+
sinon.match({
|
|
385
|
+
method: 'POST',
|
|
386
|
+
uri: `${selfDataSet.url}/sync`,
|
|
387
|
+
})
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
// and no requests for hashes were sent
|
|
391
|
+
assert.neverCalledWith(
|
|
392
|
+
webexRequest,
|
|
393
|
+
sinon.match({
|
|
394
|
+
method: 'GET',
|
|
395
|
+
uri: `${mainDataSet.url}/hashtree`,
|
|
396
|
+
})
|
|
397
|
+
);
|
|
398
|
+
assert.neverCalledWith(
|
|
399
|
+
webexRequest,
|
|
400
|
+
sinon.match({
|
|
401
|
+
method: 'GET',
|
|
402
|
+
uri: `${selfDataSet.url}/hashtree`,
|
|
403
|
+
})
|
|
404
|
+
);
|
|
405
|
+
|
|
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
|
+
// Verify callback was called with OBJECTS_UPDATED and correct updatedObjects list
|
|
416
|
+
assert.calledWith(callback, LocusInfoUpdateType.OBJECTS_UPDATED, {
|
|
417
|
+
updatedObjects: [
|
|
418
|
+
{
|
|
419
|
+
htMeta: {
|
|
420
|
+
elementId: {
|
|
421
|
+
type: 'locus',
|
|
422
|
+
id: 1,
|
|
423
|
+
version: 210,
|
|
424
|
+
},
|
|
425
|
+
dataSetNames: ['main'],
|
|
426
|
+
},
|
|
427
|
+
data: {info: {id: 'some-fake-locus-info'}},
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
htMeta: {
|
|
431
|
+
elementId: {
|
|
432
|
+
type: 'self',
|
|
433
|
+
id: 2,
|
|
434
|
+
version: 110,
|
|
435
|
+
},
|
|
436
|
+
dataSetNames: ['self'],
|
|
437
|
+
},
|
|
438
|
+
data: {person: {name: 'fake self name'}},
|
|
439
|
+
},
|
|
440
|
+
],
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
// verify that sync timers are set for visible datasets
|
|
444
|
+
expect(hashTreeParser.dataSets.main.timer).to.not.be.undefined;
|
|
445
|
+
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
|
+
};
|
|
449
|
+
|
|
450
|
+
describe('#initializeFromMessage', () => {
|
|
451
|
+
it('fetches datasets metadata and initializes hash trees for visible data sets', async () => {
|
|
452
|
+
await testInitializationOfDatasetsAndHashTrees(async (hashTreeParser: HashTreeParser) => {
|
|
453
|
+
await hashTreeParser.initializeFromMessage({
|
|
454
|
+
dataSets: [],
|
|
455
|
+
visibleDataSetsUrl,
|
|
456
|
+
locusUrl,
|
|
457
|
+
});
|
|
458
|
+
});
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
describe('#initializeFromGetLociResponse', () => {
|
|
463
|
+
it('does nothing if url for visibleDataSets is missing from locus', async () => {
|
|
464
|
+
const parser = createHashTreeParser({dataSets: [], locus: {}});
|
|
465
|
+
|
|
466
|
+
await parser.initializeFromGetLociResponse({participants: []});
|
|
467
|
+
|
|
468
|
+
assert.notCalled(webexRequest);
|
|
469
|
+
assert.notCalled(callback);
|
|
470
|
+
});
|
|
471
|
+
it('fetches datasets metadata and initializes hash trees for visible data sets', async () => {
|
|
472
|
+
await testInitializationOfDatasetsAndHashTrees(async (hashTreeParser: HashTreeParser) => {
|
|
473
|
+
await hashTreeParser.initializeFromGetLociResponse({
|
|
474
|
+
links: {
|
|
475
|
+
resources: {
|
|
476
|
+
visibleDataSets: {
|
|
477
|
+
url: visibleDataSetsUrl,
|
|
478
|
+
},
|
|
479
|
+
},
|
|
480
|
+
},
|
|
481
|
+
participants: [],
|
|
482
|
+
});
|
|
483
|
+
});
|
|
484
|
+
});
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
describe('#handleLocusUpdate', () => {
|
|
488
|
+
it('updates hash trees based on provided new locus', () => {
|
|
489
|
+
const parser = createHashTreeParser();
|
|
490
|
+
|
|
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');
|
|
497
|
+
|
|
498
|
+
// Create a locus update with new htMeta information for some things
|
|
499
|
+
const locusUpdate = {
|
|
500
|
+
dataSets: [
|
|
501
|
+
createDataSet('main', 16, 1100),
|
|
502
|
+
createDataSet('self', 1, 2100),
|
|
503
|
+
createDataSet('atd-unmuted', 16, 3100),
|
|
504
|
+
],
|
|
505
|
+
locus: {
|
|
506
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f',
|
|
507
|
+
htMeta: {
|
|
508
|
+
elementId: {
|
|
509
|
+
type: 'locus',
|
|
510
|
+
id: 0,
|
|
511
|
+
version: 210, // incremented version
|
|
512
|
+
},
|
|
513
|
+
dataSetNames: ['main'],
|
|
514
|
+
},
|
|
515
|
+
participants: [
|
|
516
|
+
{
|
|
517
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/11941033',
|
|
518
|
+
person: {},
|
|
519
|
+
htMeta: {
|
|
520
|
+
elementId: {
|
|
521
|
+
type: 'participant',
|
|
522
|
+
id: 14,
|
|
523
|
+
version: 310, // incremented version
|
|
524
|
+
},
|
|
525
|
+
dataSetNames: ['atd-unmuted'],
|
|
526
|
+
},
|
|
527
|
+
},
|
|
528
|
+
{
|
|
529
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/22222222',
|
|
530
|
+
person: {},
|
|
531
|
+
htMeta: {
|
|
532
|
+
elementId: {
|
|
533
|
+
type: 'participant',
|
|
534
|
+
id: 15,
|
|
535
|
+
version: 311, // new participant
|
|
536
|
+
},
|
|
537
|
+
dataSetNames: ['atd-unmuted'],
|
|
538
|
+
},
|
|
539
|
+
},
|
|
540
|
+
],
|
|
541
|
+
self: {
|
|
542
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/11941033',
|
|
543
|
+
visibleDataSets: ['main', 'self', 'atd-unmuted'],
|
|
544
|
+
person: {},
|
|
545
|
+
htMeta: {
|
|
546
|
+
elementId: {
|
|
547
|
+
type: 'self',
|
|
548
|
+
id: 4,
|
|
549
|
+
version: 100, // same version
|
|
550
|
+
},
|
|
551
|
+
dataSetNames: ['self'],
|
|
552
|
+
},
|
|
553
|
+
},
|
|
554
|
+
},
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
// Call handleLocusUpdate
|
|
558
|
+
parser.handleLocusUpdate(locusUpdate);
|
|
559
|
+
|
|
560
|
+
// Verify putItems was called on main hash tree with correct data
|
|
561
|
+
assert.calledOnceWithExactly(mainPutItemsSpy, [{type: 'locus', id: 0, version: 210}]);
|
|
562
|
+
|
|
563
|
+
// Verify putItems was called on self hash tree with correct data
|
|
564
|
+
assert.calledOnceWithExactly(selfPutItemsSpy, [{type: 'self', id: 4, version: 100}]);
|
|
565
|
+
|
|
566
|
+
// Verify putItems was called on atd-unmuted hash tree with correct data (2 participants)
|
|
567
|
+
assert.calledOnceWithExactly(atdUnmutedPutItemsSpy, [
|
|
568
|
+
{type: 'participant', id: 14, version: 310},
|
|
569
|
+
{type: 'participant', id: 15, version: 311},
|
|
570
|
+
]);
|
|
571
|
+
|
|
572
|
+
// check that the datasets metadata has been updated
|
|
573
|
+
expect(parser.dataSets.main.version).to.equal(1100);
|
|
574
|
+
expect(parser.dataSets.self.version).to.equal(2100);
|
|
575
|
+
expect(parser.dataSets['atd-unmuted'].version).to.equal(3100);
|
|
576
|
+
|
|
577
|
+
assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED, {
|
|
578
|
+
updatedObjects: [
|
|
579
|
+
{
|
|
580
|
+
htMeta: {
|
|
581
|
+
elementId: {
|
|
582
|
+
type: 'locus',
|
|
583
|
+
id: 0,
|
|
584
|
+
version: 210,
|
|
585
|
+
},
|
|
586
|
+
dataSetNames: ['main'],
|
|
587
|
+
},
|
|
588
|
+
data: {
|
|
589
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f',
|
|
590
|
+
htMeta: {
|
|
591
|
+
elementId: {
|
|
592
|
+
type: 'locus',
|
|
593
|
+
id: 0,
|
|
594
|
+
version: 210,
|
|
595
|
+
},
|
|
596
|
+
dataSetNames: ['main'],
|
|
597
|
+
},
|
|
598
|
+
participants: [],
|
|
599
|
+
},
|
|
600
|
+
},
|
|
601
|
+
{
|
|
602
|
+
htMeta: {
|
|
603
|
+
elementId: {
|
|
604
|
+
type: 'participant',
|
|
605
|
+
id: 14,
|
|
606
|
+
version: 310,
|
|
607
|
+
},
|
|
608
|
+
dataSetNames: ['atd-unmuted'],
|
|
609
|
+
},
|
|
610
|
+
data: {
|
|
611
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/11941033',
|
|
612
|
+
person: {},
|
|
613
|
+
htMeta: {
|
|
614
|
+
elementId: {
|
|
615
|
+
type: 'participant',
|
|
616
|
+
id: 14,
|
|
617
|
+
version: 310,
|
|
618
|
+
},
|
|
619
|
+
dataSetNames: ['atd-unmuted'],
|
|
620
|
+
},
|
|
621
|
+
},
|
|
622
|
+
},
|
|
623
|
+
{
|
|
624
|
+
htMeta: {
|
|
625
|
+
elementId: {
|
|
626
|
+
type: 'participant',
|
|
627
|
+
id: 15,
|
|
628
|
+
version: 311,
|
|
629
|
+
},
|
|
630
|
+
dataSetNames: ['atd-unmuted'],
|
|
631
|
+
},
|
|
632
|
+
data: {
|
|
633
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/22222222',
|
|
634
|
+
person: {},
|
|
635
|
+
htMeta: {
|
|
636
|
+
elementId: {
|
|
637
|
+
type: 'participant',
|
|
638
|
+
id: 15,
|
|
639
|
+
version: 311,
|
|
640
|
+
},
|
|
641
|
+
dataSetNames: ['atd-unmuted'],
|
|
642
|
+
},
|
|
643
|
+
},
|
|
644
|
+
},
|
|
645
|
+
// self missing, because it had the same version, so no update
|
|
646
|
+
],
|
|
647
|
+
});
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
it('handles unknown datasets gracefully', () => {
|
|
651
|
+
const parser = createHashTreeParser();
|
|
652
|
+
|
|
653
|
+
const mainPutItemsSpy = sinon.spy(parser.dataSets.main.hashTree, 'putItems');
|
|
654
|
+
|
|
655
|
+
// Create a locus update with data for an unknown dataset
|
|
656
|
+
const locusUpdate = {
|
|
657
|
+
dataSets: [createDataSet('main', 16)],
|
|
658
|
+
locus: {
|
|
659
|
+
htMeta: {
|
|
660
|
+
elementId: {
|
|
661
|
+
type: 'locus',
|
|
662
|
+
id: 0,
|
|
663
|
+
version: 201,
|
|
664
|
+
},
|
|
665
|
+
dataSetNames: ['main'],
|
|
666
|
+
},
|
|
667
|
+
someNewData: 'value',
|
|
668
|
+
unknownData: {
|
|
669
|
+
htMeta: {
|
|
670
|
+
elementId: {
|
|
671
|
+
type: 'UNKNOWN',
|
|
672
|
+
id: 99,
|
|
673
|
+
version: 999,
|
|
674
|
+
},
|
|
675
|
+
dataSetNames: ['unknown-dataset'], // dataset that doesn't exist
|
|
676
|
+
},
|
|
677
|
+
},
|
|
678
|
+
},
|
|
679
|
+
};
|
|
680
|
+
|
|
681
|
+
// Call handleLocusUpdate - should not throw
|
|
682
|
+
parser.handleLocusUpdate(locusUpdate);
|
|
683
|
+
|
|
684
|
+
// Verify putItems was still called for known dataset
|
|
685
|
+
assert.calledOnceWithExactly(mainPutItemsSpy, [{type: 'locus', id: 0, version: 201}]);
|
|
686
|
+
|
|
687
|
+
// Verify callback was called only for known dataset
|
|
688
|
+
assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED, {
|
|
689
|
+
updatedObjects: [
|
|
690
|
+
{
|
|
691
|
+
htMeta: {
|
|
692
|
+
elementId: {
|
|
693
|
+
type: 'locus',
|
|
694
|
+
id: 0,
|
|
695
|
+
version: 201,
|
|
696
|
+
},
|
|
697
|
+
dataSetNames: ['main'],
|
|
698
|
+
},
|
|
699
|
+
data: {
|
|
700
|
+
someNewData: 'value',
|
|
701
|
+
htMeta: {
|
|
702
|
+
elementId: {
|
|
703
|
+
type: 'locus',
|
|
704
|
+
id: 0,
|
|
705
|
+
version: 201,
|
|
706
|
+
},
|
|
707
|
+
dataSetNames: ['main'],
|
|
708
|
+
},
|
|
709
|
+
},
|
|
710
|
+
},
|
|
711
|
+
],
|
|
712
|
+
});
|
|
713
|
+
});
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
describe('#handleMessage', () => {
|
|
717
|
+
it('handles root hash heartbeat message correctly', async () => {
|
|
718
|
+
const parser = createHashTreeParser();
|
|
719
|
+
|
|
720
|
+
// Step 1: Send a normal message with locusStateElements to start the sync timer
|
|
721
|
+
const normalMessage = {
|
|
722
|
+
dataSets: [
|
|
723
|
+
{
|
|
724
|
+
...createDataSet('main', 16, 1100),
|
|
725
|
+
root: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1', // different from our hash
|
|
726
|
+
},
|
|
727
|
+
],
|
|
728
|
+
visibleDataSetsUrl,
|
|
729
|
+
locusUrl,
|
|
730
|
+
locusStateElements: [
|
|
731
|
+
{
|
|
732
|
+
htMeta: {
|
|
733
|
+
elementId: {
|
|
734
|
+
type: 'locus' as const,
|
|
735
|
+
id: 0,
|
|
736
|
+
version: 201,
|
|
737
|
+
},
|
|
738
|
+
dataSetNames: ['main'],
|
|
739
|
+
},
|
|
740
|
+
data: {someData: 'value'},
|
|
741
|
+
},
|
|
742
|
+
],
|
|
743
|
+
};
|
|
744
|
+
|
|
745
|
+
await parser.handleMessage(normalMessage, 'initial message');
|
|
746
|
+
|
|
747
|
+
// Verify the timer was set (the sync algorithm should have started)
|
|
748
|
+
expect(parser.dataSets.main.timer).to.not.be.undefined;
|
|
749
|
+
const firstTimerDelay = parser.dataSets.main.idleMs; // 1000ms base + random backoff
|
|
750
|
+
|
|
751
|
+
// Step 2: Simulate half of the time passing
|
|
752
|
+
clock.tick(500);
|
|
753
|
+
|
|
754
|
+
// Verify no webex requests have been made yet
|
|
755
|
+
assert.notCalled(webexRequest);
|
|
756
|
+
|
|
757
|
+
// Step 3: Send a heartbeat message (no locusStateElements) with mismatched root hash
|
|
758
|
+
const heartbeatMessage = createHeartbeatMessage(
|
|
759
|
+
'main',
|
|
760
|
+
16,
|
|
761
|
+
1101,
|
|
762
|
+
'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' // still different from our hash
|
|
763
|
+
);
|
|
764
|
+
|
|
765
|
+
await parser.handleMessage(heartbeatMessage, 'heartbeat message');
|
|
766
|
+
|
|
767
|
+
// Verify the timer was restarted (should still exist)
|
|
768
|
+
expect(parser.dataSets.main.timer).to.not.be.undefined;
|
|
769
|
+
|
|
770
|
+
// Step 4: Simulate more time passing (another 500ms) - total 1000ms from start
|
|
771
|
+
// This should NOT trigger the sync yet because the timer was restarted
|
|
772
|
+
clock.tick(500);
|
|
773
|
+
|
|
774
|
+
// Verify still no hash requests or sync requests were sent
|
|
775
|
+
assert.notCalled(webexRequest);
|
|
776
|
+
|
|
777
|
+
// Step 5: Mock the responses for the sync algorithm
|
|
778
|
+
const mainDataSetUrl = 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/main';
|
|
779
|
+
|
|
780
|
+
// Mock getHashesFromLocus response
|
|
781
|
+
mockGetHashesFromLocusResponse(
|
|
782
|
+
mainDataSetUrl,
|
|
783
|
+
new Array(16).fill('00000000000000000000000000000000'),
|
|
784
|
+
createDataSet('main', 16, 1102)
|
|
785
|
+
);
|
|
786
|
+
|
|
787
|
+
// Mock sendSyncRequestToLocus response - use matching root hash so no new timer is started
|
|
788
|
+
const syncResponseDataSet = createDataSet('main', 16, 1103);
|
|
789
|
+
syncResponseDataSet.root = parser.dataSets.main.hashTree.getRootHash();
|
|
790
|
+
mockSendSyncRequestResponse(mainDataSetUrl, {
|
|
791
|
+
dataSets: [syncResponseDataSet],
|
|
792
|
+
visibleDataSetsUrl,
|
|
793
|
+
locusUrl,
|
|
794
|
+
locusStateElements: [],
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
// Step 6: Simulate the full delay passing (another 1000ms + 0ms backoff)
|
|
798
|
+
// We need to advance enough time for the restarted timer to expire
|
|
799
|
+
await clock.tickAsync(1000);
|
|
800
|
+
|
|
801
|
+
// Now verify that the sync algorithm ran:
|
|
802
|
+
// 1. First, getHashesFromLocus should have been called
|
|
803
|
+
assert.calledWith(
|
|
804
|
+
webexRequest,
|
|
805
|
+
sinon.match({
|
|
806
|
+
method: 'GET',
|
|
807
|
+
uri: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/main/hashtree',
|
|
808
|
+
})
|
|
809
|
+
);
|
|
810
|
+
|
|
811
|
+
// 2. Then, sendSyncRequestToLocus should have been called
|
|
812
|
+
assert.calledWith(
|
|
813
|
+
webexRequest,
|
|
814
|
+
sinon.match({
|
|
815
|
+
method: 'POST',
|
|
816
|
+
uri: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/main/sync',
|
|
817
|
+
})
|
|
818
|
+
);
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
it('handles normal updates to hash trees correctly - updates hash trees', async () => {
|
|
822
|
+
const parser = createHashTreeParser();
|
|
823
|
+
|
|
824
|
+
// Stub updateItems on hash trees
|
|
825
|
+
const mainUpdateItemsStub = sinon
|
|
826
|
+
.stub(parser.dataSets.main.hashTree, 'updateItems')
|
|
827
|
+
.returns([true]);
|
|
828
|
+
const selfUpdateItemsStub = sinon
|
|
829
|
+
.stub(parser.dataSets.self.hashTree, 'updateItems')
|
|
830
|
+
.returns([true]);
|
|
831
|
+
const atdUnmutedUpdateItemsStub = sinon
|
|
832
|
+
.stub(parser.dataSets['atd-unmuted'].hashTree, 'updateItems')
|
|
833
|
+
.returns([true, true]);
|
|
834
|
+
|
|
835
|
+
// Create a message with updates to multiple datasets
|
|
836
|
+
const message = {
|
|
837
|
+
dataSets: [
|
|
838
|
+
createDataSet('main', 16, 1100),
|
|
839
|
+
createDataSet('self', 1, 2100),
|
|
840
|
+
createDataSet('atd-unmuted', 16, 3100),
|
|
841
|
+
],
|
|
842
|
+
visibleDataSetsUrl,
|
|
843
|
+
locusUrl,
|
|
844
|
+
locusStateElements: [
|
|
845
|
+
{
|
|
846
|
+
htMeta: {
|
|
847
|
+
elementId: {
|
|
848
|
+
type: 'locus' as const,
|
|
849
|
+
id: 0,
|
|
850
|
+
version: 201,
|
|
851
|
+
},
|
|
852
|
+
dataSetNames: ['main'],
|
|
853
|
+
},
|
|
854
|
+
data: {info: {id: 'updated-locus-info'}},
|
|
855
|
+
},
|
|
856
|
+
{
|
|
857
|
+
htMeta: {
|
|
858
|
+
elementId: {
|
|
859
|
+
type: 'self' as const,
|
|
860
|
+
id: 4,
|
|
861
|
+
version: 101,
|
|
862
|
+
},
|
|
863
|
+
dataSetNames: ['self'],
|
|
864
|
+
},
|
|
865
|
+
data: {person: {name: 'updated self name'}},
|
|
866
|
+
},
|
|
867
|
+
{
|
|
868
|
+
htMeta: {
|
|
869
|
+
elementId: {
|
|
870
|
+
type: 'participant' as const,
|
|
871
|
+
id: 14,
|
|
872
|
+
version: 301,
|
|
873
|
+
},
|
|
874
|
+
dataSetNames: ['atd-unmuted'],
|
|
875
|
+
},
|
|
876
|
+
data: {person: {name: 'participant name'}},
|
|
877
|
+
},
|
|
878
|
+
{
|
|
879
|
+
htMeta: {
|
|
880
|
+
elementId: {
|
|
881
|
+
type: 'participant' as const,
|
|
882
|
+
id: 15,
|
|
883
|
+
version: 302,
|
|
884
|
+
},
|
|
885
|
+
dataSetNames: ['atd-unmuted'],
|
|
886
|
+
},
|
|
887
|
+
data: {person: {name: 'another participant'}},
|
|
888
|
+
},
|
|
889
|
+
],
|
|
890
|
+
};
|
|
891
|
+
|
|
892
|
+
await parser.handleMessage(message, 'normal update');
|
|
893
|
+
|
|
894
|
+
// Verify updateItems was called on main hash tree
|
|
895
|
+
assert.calledOnceWithExactly(mainUpdateItemsStub, [
|
|
896
|
+
{operation: 'update', item: {type: 'locus', id: 0, version: 201}},
|
|
897
|
+
]);
|
|
898
|
+
|
|
899
|
+
// Verify updateItems was called on self hash tree
|
|
900
|
+
assert.calledOnceWithExactly(selfUpdateItemsStub, [
|
|
901
|
+
{operation: 'update', item: {type: 'self', id: 4, version: 101}},
|
|
902
|
+
]);
|
|
903
|
+
|
|
904
|
+
// Verify updateItems was called on atd-unmuted hash tree with both participants
|
|
905
|
+
assert.calledOnceWithExactly(atdUnmutedUpdateItemsStub, [
|
|
906
|
+
{operation: 'update', item: {type: 'participant', id: 14, version: 301}},
|
|
907
|
+
{operation: 'update', item: {type: 'participant', id: 15, version: 302}},
|
|
908
|
+
]);
|
|
909
|
+
|
|
910
|
+
// Verify callback was called with OBJECTS_UPDATED and all updated objects
|
|
911
|
+
assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED, {
|
|
912
|
+
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
|
+
{
|
|
921
|
+
htMeta: {
|
|
922
|
+
elementId: {type: 'locus', id: 0, version: 201},
|
|
923
|
+
dataSetNames: ['main'],
|
|
924
|
+
},
|
|
925
|
+
data: {info: {id: 'updated-locus-info'}},
|
|
926
|
+
},
|
|
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
|
+
{
|
|
932
|
+
htMeta: {
|
|
933
|
+
elementId: {type: 'self', id: 4, version: 101},
|
|
934
|
+
dataSetNames: ['self'],
|
|
935
|
+
},
|
|
936
|
+
data: {person: {name: 'updated self name'}},
|
|
937
|
+
},
|
|
938
|
+
{
|
|
939
|
+
htMeta: {
|
|
940
|
+
elementId: {type: 'participant', id: 14, version: 301},
|
|
941
|
+
dataSetNames: ['atd-unmuted'],
|
|
942
|
+
},
|
|
943
|
+
data: {person: {name: 'participant name'}},
|
|
944
|
+
},
|
|
945
|
+
{
|
|
946
|
+
htMeta: {
|
|
947
|
+
elementId: {type: 'participant', id: 15, version: 302},
|
|
948
|
+
dataSetNames: ['atd-unmuted'],
|
|
949
|
+
},
|
|
950
|
+
data: {person: {name: 'another participant'}},
|
|
951
|
+
},
|
|
952
|
+
],
|
|
953
|
+
});
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
it('detects roster drop correctly', async () => {
|
|
957
|
+
const parser = createHashTreeParser();
|
|
958
|
+
|
|
959
|
+
// Stub updateItems to return true (indicating the change was applied)
|
|
960
|
+
sinon.stub(parser.dataSets.self.hashTree, 'updateItems').returns([true]);
|
|
961
|
+
|
|
962
|
+
// Send a roster drop message (SELF object with no data)
|
|
963
|
+
const rosterDropMessage = {
|
|
964
|
+
dataSets: [createDataSet('self', 1, 2101)],
|
|
965
|
+
visibleDataSetsUrl,
|
|
966
|
+
locusUrl,
|
|
967
|
+
locusStateElements: [
|
|
968
|
+
{
|
|
969
|
+
htMeta: {
|
|
970
|
+
elementId: {
|
|
971
|
+
type: 'self' as const,
|
|
972
|
+
id: 4,
|
|
973
|
+
version: 102,
|
|
974
|
+
},
|
|
975
|
+
dataSetNames: ['self'],
|
|
976
|
+
},
|
|
977
|
+
data: undefined, // No data - this indicates roster drop
|
|
978
|
+
},
|
|
979
|
+
],
|
|
980
|
+
};
|
|
981
|
+
|
|
982
|
+
await parser.handleMessage(rosterDropMessage, 'roster drop message');
|
|
983
|
+
|
|
984
|
+
// Verify callback was called with MEETING_ENDED
|
|
985
|
+
assert.calledOnceWithExactly(callback, LocusInfoUpdateType.MEETING_ENDED, {
|
|
986
|
+
updatedObjects: undefined,
|
|
987
|
+
});
|
|
988
|
+
|
|
989
|
+
// Verify that all timers were stopped (timer should be undefined after roster drop)
|
|
990
|
+
assert.equal(parser.dataSets.self.timer, undefined);
|
|
991
|
+
assert.equal(parser.dataSets.main.timer, undefined);
|
|
992
|
+
assert.equal(parser.dataSets['atd-unmuted'].timer, undefined);
|
|
993
|
+
});
|
|
994
|
+
|
|
995
|
+
describe('sync algorithm', () => {
|
|
996
|
+
it('runs correctly after a message is received', async () => {
|
|
997
|
+
const parser = createHashTreeParser();
|
|
998
|
+
|
|
999
|
+
// Create a message with updates and mismatched root hash
|
|
1000
|
+
const message = {
|
|
1001
|
+
dataSets: [
|
|
1002
|
+
{
|
|
1003
|
+
...createDataSet('main', 16, 1100),
|
|
1004
|
+
},
|
|
1005
|
+
],
|
|
1006
|
+
visibleDataSetsUrl,
|
|
1007
|
+
locusUrl,
|
|
1008
|
+
locusStateElements: [
|
|
1009
|
+
{
|
|
1010
|
+
htMeta: {
|
|
1011
|
+
elementId: {
|
|
1012
|
+
type: 'locus' as const,
|
|
1013
|
+
id: 0,
|
|
1014
|
+
version: 201,
|
|
1015
|
+
},
|
|
1016
|
+
dataSetNames: ['main'],
|
|
1017
|
+
},
|
|
1018
|
+
data: {info: {id: 'initial-update'}},
|
|
1019
|
+
},
|
|
1020
|
+
],
|
|
1021
|
+
};
|
|
1022
|
+
|
|
1023
|
+
await parser.handleMessage(message, 'initial message');
|
|
1024
|
+
|
|
1025
|
+
// Verify callback was called with initial updates
|
|
1026
|
+
assert.calledOnce(callback);
|
|
1027
|
+
callback.resetHistory();
|
|
1028
|
+
|
|
1029
|
+
// Setup mocks for sync algorithm
|
|
1030
|
+
const mainDataSetUrl = parser.dataSets.main.url;
|
|
1031
|
+
|
|
1032
|
+
// Mock getHashesFromLocus response
|
|
1033
|
+
mockGetHashesFromLocusResponse(
|
|
1034
|
+
mainDataSetUrl,
|
|
1035
|
+
new Array(16).fill('00000000000000000000000000000000'),
|
|
1036
|
+
createDataSet('main', 16, 1101)
|
|
1037
|
+
);
|
|
1038
|
+
|
|
1039
|
+
// Mock sendSyncRequestToLocus response with matching root hash
|
|
1040
|
+
const mainSyncDataSet = createDataSet('main', 16, 1101);
|
|
1041
|
+
mainSyncDataSet.root = parser.dataSets.main.hashTree.getRootHash();
|
|
1042
|
+
mockSendSyncRequestResponse(mainDataSetUrl, {
|
|
1043
|
+
dataSets: [mainSyncDataSet],
|
|
1044
|
+
visibleDataSetsUrl,
|
|
1045
|
+
locusUrl,
|
|
1046
|
+
locusStateElements: [
|
|
1047
|
+
{
|
|
1048
|
+
htMeta: {
|
|
1049
|
+
elementId: {
|
|
1050
|
+
type: 'locus' as const,
|
|
1051
|
+
id: 1,
|
|
1052
|
+
version: 202,
|
|
1053
|
+
},
|
|
1054
|
+
dataSetNames: ['main'],
|
|
1055
|
+
},
|
|
1056
|
+
data: {info: {id: 'synced-locus'}},
|
|
1057
|
+
},
|
|
1058
|
+
],
|
|
1059
|
+
});
|
|
1060
|
+
|
|
1061
|
+
// Simulate time passing to trigger sync algorithm (1000ms base + 0 backoff)
|
|
1062
|
+
await clock.tickAsync(1000);
|
|
1063
|
+
|
|
1064
|
+
// Verify that sync requests were sent for main dataset
|
|
1065
|
+
assert.calledWith(
|
|
1066
|
+
webexRequest,
|
|
1067
|
+
sinon.match({
|
|
1068
|
+
method: 'GET',
|
|
1069
|
+
uri: `${mainDataSetUrl}/hashtree`,
|
|
1070
|
+
})
|
|
1071
|
+
);
|
|
1072
|
+
assert.calledWith(
|
|
1073
|
+
webexRequest,
|
|
1074
|
+
sinon.match({
|
|
1075
|
+
method: 'POST',
|
|
1076
|
+
uri: `${mainDataSetUrl}/sync`,
|
|
1077
|
+
})
|
|
1078
|
+
);
|
|
1079
|
+
|
|
1080
|
+
// Verify that callback was called with synced objects
|
|
1081
|
+
assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED, {
|
|
1082
|
+
updatedObjects: [
|
|
1083
|
+
{
|
|
1084
|
+
htMeta: {
|
|
1085
|
+
elementId: {type: 'locus', id: 1, version: 202},
|
|
1086
|
+
dataSetNames: ['main'],
|
|
1087
|
+
},
|
|
1088
|
+
data: {info: {id: 'synced-locus'}},
|
|
1089
|
+
},
|
|
1090
|
+
],
|
|
1091
|
+
});
|
|
1092
|
+
});
|
|
1093
|
+
it('requests only mismatched hashes during sync', async () => {
|
|
1094
|
+
const parser = createHashTreeParser();
|
|
1095
|
+
|
|
1096
|
+
// Create a message with updates to trigger sync algorithm
|
|
1097
|
+
const message = {
|
|
1098
|
+
dataSets: [createDataSet('main', 16, 1100)],
|
|
1099
|
+
visibleDataSetsUrl,
|
|
1100
|
+
locusUrl,
|
|
1101
|
+
locusStateElements: [
|
|
1102
|
+
{
|
|
1103
|
+
htMeta: {
|
|
1104
|
+
elementId: {
|
|
1105
|
+
type: 'locus' as const,
|
|
1106
|
+
id: 0,
|
|
1107
|
+
version: 201,
|
|
1108
|
+
},
|
|
1109
|
+
dataSetNames: ['main'],
|
|
1110
|
+
},
|
|
1111
|
+
data: {info: {id: 'initial-update'}},
|
|
1112
|
+
},
|
|
1113
|
+
{
|
|
1114
|
+
htMeta: {
|
|
1115
|
+
elementId: {
|
|
1116
|
+
type: 'participant' as const,
|
|
1117
|
+
id: 3,
|
|
1118
|
+
version: 301,
|
|
1119
|
+
},
|
|
1120
|
+
dataSetNames: ['main'],
|
|
1121
|
+
},
|
|
1122
|
+
data: {id: 'participant with id=3'},
|
|
1123
|
+
},
|
|
1124
|
+
{
|
|
1125
|
+
htMeta: {
|
|
1126
|
+
elementId: {
|
|
1127
|
+
type: 'participant' as const,
|
|
1128
|
+
id: 4,
|
|
1129
|
+
version: 301,
|
|
1130
|
+
},
|
|
1131
|
+
dataSetNames: ['main'],
|
|
1132
|
+
},
|
|
1133
|
+
data: {id: 'participant with id=4'},
|
|
1134
|
+
},
|
|
1135
|
+
],
|
|
1136
|
+
};
|
|
1137
|
+
|
|
1138
|
+
await parser.handleMessage(message, 'initial message');
|
|
1139
|
+
|
|
1140
|
+
callback.resetHistory();
|
|
1141
|
+
|
|
1142
|
+
// Setup the hash tree to have specific hashes for each leaf
|
|
1143
|
+
// We'll make leaf 0 and leaf 4 have mismatched hashes
|
|
1144
|
+
const hashTree = parser.dataSets.main.hashTree;
|
|
1145
|
+
|
|
1146
|
+
// Get the actual hashes for all leaves after the items were added
|
|
1147
|
+
const actualHashes = new Array(16);
|
|
1148
|
+
for (let i = 0; i < 16; i++) {
|
|
1149
|
+
actualHashes[i] = hashTree.leafHashes[i];
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
// Mock getHashesFromLocus to return hashes where most match but 0 and 4 don't
|
|
1153
|
+
actualHashes[0] = 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb';
|
|
1154
|
+
actualHashes[4] = 'cccccccccccccccccccccccccccccccc';
|
|
1155
|
+
|
|
1156
|
+
const mainDataSetUrl = parser.dataSets.main.url;
|
|
1157
|
+
mockGetHashesFromLocusResponse(
|
|
1158
|
+
mainDataSetUrl,
|
|
1159
|
+
actualHashes,
|
|
1160
|
+
createDataSet('main', 16, 1101)
|
|
1161
|
+
);
|
|
1162
|
+
|
|
1163
|
+
// Mock sendSyncRequestToLocus response with matching root hash
|
|
1164
|
+
const mainSyncDataSet = createDataSet('main', 16, 1101);
|
|
1165
|
+
mainSyncDataSet.root = hashTree.getRootHash();
|
|
1166
|
+
mockSendSyncRequestResponse(mainDataSetUrl, {
|
|
1167
|
+
dataSets: [mainSyncDataSet],
|
|
1168
|
+
visibleDataSetsUrl,
|
|
1169
|
+
locusUrl,
|
|
1170
|
+
locusStateElements: [],
|
|
1171
|
+
});
|
|
1172
|
+
|
|
1173
|
+
// Trigger the sync algorithm by advancing time
|
|
1174
|
+
await clock.tickAsync(1000);
|
|
1175
|
+
|
|
1176
|
+
// Verify getHashesFromLocus was called
|
|
1177
|
+
assert.calledWith(
|
|
1178
|
+
webexRequest,
|
|
1179
|
+
sinon.match({
|
|
1180
|
+
method: 'GET',
|
|
1181
|
+
uri: `${mainDataSetUrl}/hashtree`,
|
|
1182
|
+
})
|
|
1183
|
+
);
|
|
1184
|
+
|
|
1185
|
+
// Verify sendSyncRequestToLocus was called with only the mismatched leaf indices 0 and 4
|
|
1186
|
+
assert.calledWith(webexRequest, {
|
|
1187
|
+
method: 'POST',
|
|
1188
|
+
uri: `${mainDataSetUrl}/sync`,
|
|
1189
|
+
body: {
|
|
1190
|
+
dataSet: {
|
|
1191
|
+
name: 'main',
|
|
1192
|
+
leafCount: 16,
|
|
1193
|
+
root: '472801612a448c4e0ab74975ed9d7a2e'
|
|
1194
|
+
},
|
|
1195
|
+
leafDataEntries: [
|
|
1196
|
+
{leafIndex: 0, elementIds: [{type: 'locus', id: 0, version: 201}]},
|
|
1197
|
+
{leafIndex: 4, elementIds: [{type: 'participant', id: 4, version: 301}]},
|
|
1198
|
+
],
|
|
1199
|
+
},
|
|
1200
|
+
});
|
|
1201
|
+
});
|
|
1202
|
+
|
|
1203
|
+
it('does not get the hashes if leafCount === 1', async () => {
|
|
1204
|
+
const parser = createHashTreeParser();
|
|
1205
|
+
|
|
1206
|
+
// Create a message with updates to self dataset
|
|
1207
|
+
const message = {
|
|
1208
|
+
dataSets: [createDataSet('self', 1, 2001)],
|
|
1209
|
+
visibleDataSetsUrl,
|
|
1210
|
+
locusUrl,
|
|
1211
|
+
locusStateElements: [
|
|
1212
|
+
{
|
|
1213
|
+
htMeta: {
|
|
1214
|
+
elementId: {
|
|
1215
|
+
type: 'self' as const,
|
|
1216
|
+
id: 4,
|
|
1217
|
+
version: 102,
|
|
1218
|
+
},
|
|
1219
|
+
dataSetNames: ['self'],
|
|
1220
|
+
},
|
|
1221
|
+
data: {id: 'updated self'},
|
|
1222
|
+
},
|
|
1223
|
+
],
|
|
1224
|
+
};
|
|
1225
|
+
|
|
1226
|
+
await parser.handleMessage(message, 'message with self update');
|
|
1227
|
+
|
|
1228
|
+
callback.resetHistory();
|
|
1229
|
+
|
|
1230
|
+
// Trigger the sync algorithm by advancing time
|
|
1231
|
+
await clock.tickAsync(1000);
|
|
1232
|
+
|
|
1233
|
+
// self data set has only 1 leaf, so sync should skip the step of getting hashes
|
|
1234
|
+
assert.neverCalledWith(
|
|
1235
|
+
webexRequest,
|
|
1236
|
+
sinon.match({
|
|
1237
|
+
method: 'GET',
|
|
1238
|
+
uri: `${parser.dataSets.self.url}/hashtree`,
|
|
1239
|
+
})
|
|
1240
|
+
);
|
|
1241
|
+
|
|
1242
|
+
// Verify sendSyncRequestToLocus was called with the single leaf
|
|
1243
|
+
assert.calledWith(webexRequest, {
|
|
1244
|
+
method: 'POST',
|
|
1245
|
+
uri: `${parser.dataSets.self.url}/sync`,
|
|
1246
|
+
body: {
|
|
1247
|
+
dataSet: {
|
|
1248
|
+
name: 'self',
|
|
1249
|
+
leafCount: 1,
|
|
1250
|
+
root: '483ba32a5db954720b4c43ed528d8075'
|
|
1251
|
+
},
|
|
1252
|
+
leafDataEntries: [
|
|
1253
|
+
{leafIndex: 0, elementIds: [{type: 'self', id: 4, version: 102}]},
|
|
1254
|
+
],
|
|
1255
|
+
},
|
|
1256
|
+
});
|
|
1257
|
+
});
|
|
1258
|
+
});
|
|
1259
|
+
|
|
1260
|
+
describe('handles visible data sets changes correctly', () => {
|
|
1261
|
+
it('handles addition of visible data set (one that does not require async initialization)', async () => {
|
|
1262
|
+
// Create a parser with visible datasets
|
|
1263
|
+
const parser = createHashTreeParser();
|
|
1264
|
+
|
|
1265
|
+
// Stub updateItems on self hash tree to return true
|
|
1266
|
+
sinon.stub(parser.dataSets.self.hashTree, 'updateItems').returns([true]);
|
|
1267
|
+
|
|
1268
|
+
// Send a message with SELF object that has a new visibleDataSets list
|
|
1269
|
+
const message = {
|
|
1270
|
+
dataSets: [createDataSet('self', 1, 2100), createDataSet('attendees', 8, 4000)],
|
|
1271
|
+
visibleDataSetsUrl,
|
|
1272
|
+
locusUrl,
|
|
1273
|
+
locusStateElements: [
|
|
1274
|
+
{
|
|
1275
|
+
htMeta: {
|
|
1276
|
+
elementId: {
|
|
1277
|
+
type: 'self' as const,
|
|
1278
|
+
id: 4,
|
|
1279
|
+
version: 101,
|
|
1280
|
+
},
|
|
1281
|
+
dataSetNames: ['self'],
|
|
1282
|
+
},
|
|
1283
|
+
data: {
|
|
1284
|
+
visibleDataSets: ['main', 'self', 'atd-unmuted', 'attendees'], // added 'attendees'
|
|
1285
|
+
},
|
|
1286
|
+
},
|
|
1287
|
+
],
|
|
1288
|
+
};
|
|
1289
|
+
|
|
1290
|
+
await parser.handleMessage(message, 'add visible dataset');
|
|
1291
|
+
|
|
1292
|
+
// Verify that 'attendees' was added to visibleDataSets
|
|
1293
|
+
assert.include(parser.visibleDataSets, 'attendees');
|
|
1294
|
+
|
|
1295
|
+
// Verify that a hash tree was created for 'attendees'
|
|
1296
|
+
assert.exists(parser.dataSets.attendees.hashTree);
|
|
1297
|
+
assert.equal(parser.dataSets.attendees.hashTree.numLeaves, 8);
|
|
1298
|
+
|
|
1299
|
+
// Verify callback was called with the self update (appears twice due to SPARK-744859)
|
|
1300
|
+
assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED, {
|
|
1301
|
+
updatedObjects: [
|
|
1302
|
+
{
|
|
1303
|
+
htMeta: {
|
|
1304
|
+
elementId: {type: 'self', id: 4, version: 101},
|
|
1305
|
+
dataSetNames: ['self'],
|
|
1306
|
+
},
|
|
1307
|
+
data: {
|
|
1308
|
+
visibleDataSets: ['main', 'self', 'atd-unmuted', 'attendees'],
|
|
1309
|
+
},
|
|
1310
|
+
},
|
|
1311
|
+
{
|
|
1312
|
+
htMeta: {
|
|
1313
|
+
elementId: {type: 'self', id: 4, version: 101},
|
|
1314
|
+
dataSetNames: ['self'],
|
|
1315
|
+
},
|
|
1316
|
+
data: {
|
|
1317
|
+
visibleDataSets: ['main', 'self', 'atd-unmuted', 'attendees'],
|
|
1318
|
+
},
|
|
1319
|
+
},
|
|
1320
|
+
],
|
|
1321
|
+
});
|
|
1322
|
+
});
|
|
1323
|
+
|
|
1324
|
+
it('handles addition of visible data set (one that requires async initialization)', async () => {
|
|
1325
|
+
// Create a parser with visible datasets
|
|
1326
|
+
const parser = createHashTreeParser();
|
|
1327
|
+
|
|
1328
|
+
// Stub updateItems on self hash tree to return true
|
|
1329
|
+
sinon.stub(parser.dataSets.self.hashTree, 'updateItems').returns([true]);
|
|
1330
|
+
|
|
1331
|
+
// Send a message with SELF object that has a new visibleDataSets list (adding 'new-dataset')
|
|
1332
|
+
// but WITHOUT providing info about the new dataset in dataSets array
|
|
1333
|
+
const message = {
|
|
1334
|
+
dataSets: [createDataSet('self', 1, 2100)],
|
|
1335
|
+
visibleDataSetsUrl,
|
|
1336
|
+
locusUrl,
|
|
1337
|
+
locusStateElements: [
|
|
1338
|
+
{
|
|
1339
|
+
htMeta: {
|
|
1340
|
+
elementId: {
|
|
1341
|
+
type: 'self' as const,
|
|
1342
|
+
id: 4,
|
|
1343
|
+
version: 101,
|
|
1344
|
+
},
|
|
1345
|
+
dataSetNames: ['self'],
|
|
1346
|
+
},
|
|
1347
|
+
data: {
|
|
1348
|
+
visibleDataSets: ['main', 'self', 'atd-unmuted', 'new-dataset'],
|
|
1349
|
+
},
|
|
1350
|
+
},
|
|
1351
|
+
],
|
|
1352
|
+
};
|
|
1353
|
+
|
|
1354
|
+
// Mock the async initialization of the new dataset
|
|
1355
|
+
const newDataSet = createDataSet('new-dataset', 4, 5000);
|
|
1356
|
+
mockGetAllDataSetsMetadata(webexRequest, visibleDataSetsUrl, [newDataSet]);
|
|
1357
|
+
mockSyncRequest(webexRequest, newDataSet.url, {
|
|
1358
|
+
dataSets: [newDataSet],
|
|
1359
|
+
visibleDataSetsUrl,
|
|
1360
|
+
locusUrl,
|
|
1361
|
+
locusStateElements: [],
|
|
1362
|
+
});
|
|
1363
|
+
|
|
1364
|
+
await parser.handleMessage(message, 'add new dataset requiring async init');
|
|
1365
|
+
|
|
1366
|
+
// immediately we don't have the dataset yet, so it should not be in visibleDataSets
|
|
1367
|
+
// and no hash tree should exist yet
|
|
1368
|
+
assert.isFalse(parser.visibleDataSets.includes('new-dataset'));
|
|
1369
|
+
assert.isUndefined(parser.dataSets['new-dataset']);
|
|
1370
|
+
|
|
1371
|
+
// Wait for the async initialization to complete (queued as microtask)
|
|
1372
|
+
await clock.tickAsync(0);
|
|
1373
|
+
|
|
1374
|
+
// The visibleDataSets is updated from the self object data
|
|
1375
|
+
assert.include(parser.visibleDataSets, 'new-dataset');
|
|
1376
|
+
|
|
1377
|
+
// Verify that a hash tree was created for 'new-dataset'
|
|
1378
|
+
assert.exists(parser.dataSets['new-dataset'].hashTree);
|
|
1379
|
+
assert.equal(parser.dataSets['new-dataset'].hashTree.numLeaves, 4);
|
|
1380
|
+
|
|
1381
|
+
// Verify getAllDataSetsMetadata was called for async initialization
|
|
1382
|
+
assert.calledWith(
|
|
1383
|
+
webexRequest,
|
|
1384
|
+
sinon.match({
|
|
1385
|
+
method: 'GET',
|
|
1386
|
+
uri: visibleDataSetsUrl,
|
|
1387
|
+
})
|
|
1388
|
+
);
|
|
1389
|
+
|
|
1390
|
+
// Verify sync request was sent for the new dataset
|
|
1391
|
+
assert.calledWith(
|
|
1392
|
+
webexRequest,
|
|
1393
|
+
sinon.match({
|
|
1394
|
+
method: 'POST',
|
|
1395
|
+
uri: `${newDataSet.url}/sync`,
|
|
1396
|
+
})
|
|
1397
|
+
);
|
|
1398
|
+
});
|
|
1399
|
+
|
|
1400
|
+
it('handles removal of visible data set', async () => {
|
|
1401
|
+
// Create a parser with visible datasets
|
|
1402
|
+
const parser = createHashTreeParser();
|
|
1403
|
+
|
|
1404
|
+
// Store the initial hash tree for atd-unmuted to verify it gets deleted
|
|
1405
|
+
const atdUnmutedHashTree = parser.dataSets['atd-unmuted'].hashTree;
|
|
1406
|
+
assert.exists(atdUnmutedHashTree);
|
|
1407
|
+
|
|
1408
|
+
// Stub getLeafData to return some items that will be marked as removed
|
|
1409
|
+
// It's called for each leaf (16 leaves), so return an array for leaf 14 and empty for others
|
|
1410
|
+
const getLeafDataStub = sinon.stub(atdUnmutedHashTree, 'getLeafData');
|
|
1411
|
+
getLeafDataStub.withArgs(14).returns([{type: 'participant', id: 14, version: 301}]);
|
|
1412
|
+
getLeafDataStub.returns([]);
|
|
1413
|
+
|
|
1414
|
+
// Stub updateItems on self hash tree to return true
|
|
1415
|
+
sinon.stub(parser.dataSets.self.hashTree, 'updateItems').returns([true]);
|
|
1416
|
+
|
|
1417
|
+
// Send a message with SELF object that has removed 'atd-unmuted' from visibleDataSets
|
|
1418
|
+
const message = {
|
|
1419
|
+
dataSets: [createDataSet('self', 1, 2100)],
|
|
1420
|
+
visibleDataSetsUrl,
|
|
1421
|
+
locusUrl,
|
|
1422
|
+
locusStateElements: [
|
|
1423
|
+
{
|
|
1424
|
+
htMeta: {
|
|
1425
|
+
elementId: {
|
|
1426
|
+
type: 'self' as const,
|
|
1427
|
+
id: 4,
|
|
1428
|
+
version: 101,
|
|
1429
|
+
},
|
|
1430
|
+
dataSetNames: ['self'],
|
|
1431
|
+
},
|
|
1432
|
+
data: {
|
|
1433
|
+
visibleDataSets: ['main', 'self'], // removed 'atd-unmuted'
|
|
1434
|
+
},
|
|
1435
|
+
},
|
|
1436
|
+
],
|
|
1437
|
+
};
|
|
1438
|
+
|
|
1439
|
+
await parser.handleMessage(message, 'remove visible dataset');
|
|
1440
|
+
|
|
1441
|
+
// Verify that 'atd-unmuted' was removed from visibleDataSets
|
|
1442
|
+
assert.notInclude(parser.visibleDataSets, 'atd-unmuted');
|
|
1443
|
+
|
|
1444
|
+
// Verify that the hash tree for 'atd-unmuted' was deleted
|
|
1445
|
+
assert.isUndefined(parser.dataSets['atd-unmuted'].hashTree);
|
|
1446
|
+
|
|
1447
|
+
// Verify that the timer was cleared
|
|
1448
|
+
assert.isUndefined(parser.dataSets['atd-unmuted'].timer);
|
|
1449
|
+
|
|
1450
|
+
// Verify callback was called with both the self update and the removed objects
|
|
1451
|
+
assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED, {
|
|
1452
|
+
updatedObjects: [
|
|
1453
|
+
{
|
|
1454
|
+
htMeta: {
|
|
1455
|
+
elementId: {type: 'self', id: 4, version: 101},
|
|
1456
|
+
dataSetNames: ['self'],
|
|
1457
|
+
},
|
|
1458
|
+
data: {
|
|
1459
|
+
visibleDataSets: ['main', 'self'],
|
|
1460
|
+
},
|
|
1461
|
+
},
|
|
1462
|
+
{
|
|
1463
|
+
htMeta: {
|
|
1464
|
+
elementId: {type: 'participant', id: 14, version: 301},
|
|
1465
|
+
dataSetNames: ['atd-unmuted'],
|
|
1466
|
+
},
|
|
1467
|
+
data: null,
|
|
1468
|
+
},
|
|
1469
|
+
{
|
|
1470
|
+
htMeta: {
|
|
1471
|
+
elementId: {type: 'self', id: 4, version: 101}, // 2nd self because of SPARK-744859
|
|
1472
|
+
dataSetNames: ['self'],
|
|
1473
|
+
},
|
|
1474
|
+
data: {
|
|
1475
|
+
visibleDataSets: ['main', 'self'],
|
|
1476
|
+
},
|
|
1477
|
+
},
|
|
1478
|
+
],
|
|
1479
|
+
});
|
|
1480
|
+
});
|
|
1481
|
+
it('ignores data if it is not in a visible data set', async () => {
|
|
1482
|
+
// Create a parser with attendees in datasets but not in visibleDataSets
|
|
1483
|
+
const parser = createHashTreeParser({
|
|
1484
|
+
dataSets: [
|
|
1485
|
+
...exampleInitialLocus.dataSets,
|
|
1486
|
+
{
|
|
1487
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/attendees',
|
|
1488
|
+
root: '0'.repeat(32),
|
|
1489
|
+
version: 4000,
|
|
1490
|
+
leafCount: 8,
|
|
1491
|
+
name: 'attendees',
|
|
1492
|
+
idleMs: 1000,
|
|
1493
|
+
backoff: {maxMs: 1000, exponent: 2},
|
|
1494
|
+
},
|
|
1495
|
+
],
|
|
1496
|
+
locus: {...exampleInitialLocus.locus},
|
|
1497
|
+
});
|
|
1498
|
+
|
|
1499
|
+
// Verify attendees is NOT in visibleDataSets
|
|
1500
|
+
assert.notInclude(parser.visibleDataSets, 'attendees');
|
|
1501
|
+
|
|
1502
|
+
// Send a message with attendees data
|
|
1503
|
+
const message = {
|
|
1504
|
+
dataSets: [createDataSet('attendees', 8, 4001)],
|
|
1505
|
+
visibleDataSetsUrl,
|
|
1506
|
+
locusUrl,
|
|
1507
|
+
locusStateElements: [
|
|
1508
|
+
{
|
|
1509
|
+
htMeta: {
|
|
1510
|
+
elementId: {
|
|
1511
|
+
type: 'participant' as const,
|
|
1512
|
+
id: 20,
|
|
1513
|
+
version: 303,
|
|
1514
|
+
},
|
|
1515
|
+
dataSetNames: ['attendees'],
|
|
1516
|
+
},
|
|
1517
|
+
data: {person: {name: 'participant in attendees'}},
|
|
1518
|
+
},
|
|
1519
|
+
],
|
|
1520
|
+
};
|
|
1521
|
+
|
|
1522
|
+
await parser.handleMessage(message, 'message with non-visible dataset');
|
|
1523
|
+
|
|
1524
|
+
// Verify that no hash tree was created for attendees
|
|
1525
|
+
assert.isUndefined(parser.dataSets.attendees.hashTree);
|
|
1526
|
+
|
|
1527
|
+
// Verify callback was NOT called (no updates for non-visible datasets)
|
|
1528
|
+
assert.notCalled(callback);
|
|
1529
|
+
});
|
|
1530
|
+
});
|
|
1531
|
+
});
|
|
1532
|
+
});
|