@webex/plugin-meetings 3.11.0 → 3.12.0-mobius-socket.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/aiEnableRequest/index.js +184 -0
- package/dist/aiEnableRequest/index.js.map +1 -0
- package/dist/aiEnableRequest/utils.js +36 -0
- package/dist/aiEnableRequest/utils.js.map +1 -0
- package/dist/annotation/index.js +14 -5
- package/dist/annotation/index.js.map +1 -1
- package/dist/breakouts/breakout.js +1 -1
- package/dist/breakouts/index.js +1 -1
- package/dist/config.js +7 -2
- package/dist/config.js.map +1 -1
- package/dist/constants.js +28 -6
- package/dist/constants.js.map +1 -1
- package/dist/hashTree/constants.js +3 -1
- package/dist/hashTree/constants.js.map +1 -1
- package/dist/hashTree/hashTree.js +18 -0
- package/dist/hashTree/hashTree.js.map +1 -1
- package/dist/hashTree/hashTreeParser.js +868 -419
- package/dist/hashTree/hashTreeParser.js.map +1 -1
- package/dist/hashTree/types.js +4 -2
- package/dist/hashTree/types.js.map +1 -1
- package/dist/hashTree/utils.js +10 -0
- package/dist/hashTree/utils.js.map +1 -1
- package/dist/index.js +11 -2
- package/dist/index.js.map +1 -1
- package/dist/interceptors/constant.js +12 -0
- package/dist/interceptors/constant.js.map +1 -0
- package/dist/interceptors/dataChannelAuthToken.js +290 -0
- package/dist/interceptors/dataChannelAuthToken.js.map +1 -0
- package/dist/interceptors/index.js +7 -0
- package/dist/interceptors/index.js.map +1 -1
- package/dist/interceptors/utils.js +27 -0
- package/dist/interceptors/utils.js.map +1 -0
- package/dist/interpretation/index.js +2 -2
- package/dist/interpretation/index.js.map +1 -1
- package/dist/interpretation/siLanguage.js +1 -1
- package/dist/locus-info/controlsUtils.js +5 -3
- package/dist/locus-info/controlsUtils.js.map +1 -1
- package/dist/locus-info/index.js +522 -131
- package/dist/locus-info/index.js.map +1 -1
- package/dist/locus-info/selfUtils.js +1 -0
- package/dist/locus-info/selfUtils.js.map +1 -1
- package/dist/locus-info/types.js.map +1 -1
- package/dist/media/MediaConnectionAwaiter.js +57 -1
- package/dist/media/MediaConnectionAwaiter.js.map +1 -1
- package/dist/media/properties.js +4 -2
- package/dist/media/properties.js.map +1 -1
- package/dist/meeting/in-meeting-actions.js +7 -1
- package/dist/meeting/in-meeting-actions.js.map +1 -1
- package/dist/meeting/index.js +1304 -928
- package/dist/meeting/index.js.map +1 -1
- package/dist/meeting/request.js +50 -0
- package/dist/meeting/request.js.map +1 -1
- package/dist/meeting/request.type.js.map +1 -1
- package/dist/meeting/util.js +133 -3
- package/dist/meeting/util.js.map +1 -1
- package/dist/meetings/index.js +117 -48
- package/dist/meetings/index.js.map +1 -1
- package/dist/member/index.js +10 -0
- package/dist/member/index.js.map +1 -1
- package/dist/member/util.js +10 -0
- package/dist/member/util.js.map +1 -1
- package/dist/metrics/constants.js +6 -1
- package/dist/metrics/constants.js.map +1 -1
- package/dist/multistream/mediaRequestManager.js +9 -60
- package/dist/multistream/mediaRequestManager.js.map +1 -1
- package/dist/multistream/remoteMediaManager.js +11 -0
- package/dist/multistream/remoteMediaManager.js.map +1 -1
- package/dist/multistream/sendSlotManager.js +116 -2
- package/dist/multistream/sendSlotManager.js.map +1 -1
- package/dist/reachability/index.js +18 -10
- package/dist/reachability/index.js.map +1 -1
- package/dist/reactions/reactions.type.js.map +1 -1
- package/dist/reconnection-manager/index.js +0 -1
- package/dist/reconnection-manager/index.js.map +1 -1
- package/dist/types/aiEnableRequest/index.d.ts +5 -0
- package/dist/types/aiEnableRequest/utils.d.ts +2 -0
- package/dist/types/config.d.ts +4 -0
- package/dist/types/constants.d.ts +23 -1
- package/dist/types/hashTree/constants.d.ts +1 -0
- package/dist/types/hashTree/hashTree.d.ts +7 -0
- package/dist/types/hashTree/hashTreeParser.d.ts +122 -14
- package/dist/types/hashTree/types.d.ts +3 -0
- package/dist/types/hashTree/utils.d.ts +6 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/interceptors/constant.d.ts +5 -0
- package/dist/types/interceptors/dataChannelAuthToken.d.ts +43 -0
- package/dist/types/interceptors/index.d.ts +2 -1
- package/dist/types/interceptors/utils.d.ts +1 -0
- package/dist/types/locus-info/index.d.ts +60 -8
- package/dist/types/locus-info/types.d.ts +7 -0
- package/dist/types/media/MediaConnectionAwaiter.d.ts +10 -1
- package/dist/types/media/properties.d.ts +2 -1
- package/dist/types/meeting/in-meeting-actions.d.ts +6 -0
- package/dist/types/meeting/index.d.ts +72 -7
- package/dist/types/meeting/request.d.ts +16 -1
- package/dist/types/meeting/request.type.d.ts +5 -0
- package/dist/types/meeting/util.d.ts +31 -0
- package/dist/types/meetings/index.d.ts +4 -2
- package/dist/types/member/index.d.ts +1 -0
- package/dist/types/member/util.d.ts +5 -0
- package/dist/types/metrics/constants.d.ts +5 -0
- package/dist/types/multistream/mediaRequestManager.d.ts +0 -23
- package/dist/types/multistream/sendSlotManager.d.ts +23 -1
- package/dist/types/reactions/reactions.type.d.ts +1 -0
- package/dist/types/webinar/utils.d.ts +6 -0
- package/dist/webinar/index.js +438 -163
- package/dist/webinar/index.js.map +1 -1
- package/dist/webinar/utils.js +25 -0
- package/dist/webinar/utils.js.map +1 -0
- package/package.json +24 -23
- package/src/aiEnableRequest/README.md +84 -0
- package/src/aiEnableRequest/index.ts +170 -0
- package/src/aiEnableRequest/utils.ts +25 -0
- package/src/annotation/index.ts +27 -7
- package/src/config.ts +4 -0
- package/src/constants.ts +29 -1
- package/src/hashTree/constants.ts +1 -0
- package/src/hashTree/hashTree.ts +17 -0
- package/src/hashTree/hashTreeParser.ts +761 -260
- package/src/hashTree/types.ts +4 -0
- package/src/hashTree/utils.ts +9 -0
- package/src/index.ts +8 -1
- package/src/interceptors/constant.ts +6 -0
- package/src/interceptors/dataChannelAuthToken.ts +170 -0
- package/src/interceptors/index.ts +2 -1
- package/src/interceptors/utils.ts +16 -0
- package/src/interpretation/index.ts +2 -2
- package/src/locus-info/controlsUtils.ts +11 -0
- package/src/locus-info/index.ts +579 -113
- package/src/locus-info/selfUtils.ts +1 -0
- package/src/locus-info/types.ts +8 -0
- package/src/media/MediaConnectionAwaiter.ts +41 -1
- package/src/media/properties.ts +3 -1
- package/src/meeting/in-meeting-actions.ts +12 -0
- package/src/meeting/index.ts +389 -87
- package/src/meeting/request.ts +42 -0
- package/src/meeting/request.type.ts +6 -0
- package/src/meeting/util.ts +160 -2
- package/src/meetings/index.ts +157 -44
- package/src/member/index.ts +10 -0
- package/src/member/util.ts +12 -0
- package/src/metrics/constants.ts +6 -0
- package/src/multistream/mediaRequestManager.ts +4 -54
- package/src/multistream/remoteMediaManager.ts +13 -0
- package/src/multistream/sendSlotManager.ts +97 -3
- package/src/reachability/index.ts +9 -0
- package/src/reactions/reactions.type.ts +1 -0
- package/src/reconnection-manager/index.ts +0 -1
- package/src/webinar/index.ts +265 -6
- package/src/webinar/utils.ts +16 -0
- package/test/unit/spec/aiEnableRequest/index.ts +981 -0
- package/test/unit/spec/aiEnableRequest/utils.ts +130 -0
- package/test/unit/spec/annotation/index.ts +69 -7
- package/test/unit/spec/hashTree/hashTree.ts +66 -0
- package/test/unit/spec/hashTree/hashTreeParser.ts +2321 -175
- package/test/unit/spec/interceptors/dataChannelAuthToken.ts +210 -0
- package/test/unit/spec/interceptors/utils.ts +75 -0
- package/test/unit/spec/locus-info/controlsUtils.js +29 -0
- package/test/unit/spec/locus-info/index.js +1134 -55
- package/test/unit/spec/media/MediaConnectionAwaiter.ts +41 -1
- package/test/unit/spec/media/properties.ts +12 -3
- package/test/unit/spec/meeting/in-meeting-actions.ts +8 -2
- package/test/unit/spec/meeting/index.js +884 -152
- package/test/unit/spec/meeting/request.js +70 -0
- package/test/unit/spec/meeting/utils.js +438 -26
- package/test/unit/spec/meetings/index.js +653 -32
- package/test/unit/spec/member/index.js +28 -4
- package/test/unit/spec/member/util.js +65 -27
- package/test/unit/spec/multistream/mediaRequestManager.ts +2 -85
- package/test/unit/spec/multistream/remoteMediaManager.ts +30 -0
- package/test/unit/spec/multistream/sendSlotManager.ts +135 -36
- package/test/unit/spec/reachability/index.ts +23 -0
- package/test/unit/spec/reconnection-manager/index.js +4 -8
- package/test/unit/spec/webinar/index.ts +534 -37
- package/test/unit/spec/webinar/utils.ts +39 -0
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import HashTreeParser, {
|
|
2
2
|
LocusInfoUpdateType,
|
|
3
|
+
MeetingEndedError,
|
|
3
4
|
} from '@webex/plugin-meetings/src/hashTree/hashTreeParser';
|
|
4
5
|
import HashTree from '@webex/plugin-meetings/src/hashTree/hashTree';
|
|
5
6
|
import {expect} from '@webex/test-helper-chai';
|
|
6
7
|
import sinon from 'sinon';
|
|
7
8
|
import {assert} from '@webex/test-helper-chai';
|
|
9
|
+
import {EMPTY_HASH} from '@webex/plugin-meetings/src/hashTree/constants';
|
|
10
|
+
|
|
11
|
+
const visibleDataSetsUrl = 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/visibleDataSets';
|
|
8
12
|
|
|
9
13
|
const exampleInitialLocus = {
|
|
10
14
|
dataSets: [
|
|
@@ -46,6 +50,7 @@ const exampleInitialLocus = {
|
|
|
46
50
|
},
|
|
47
51
|
dataSetNames: ['main'],
|
|
48
52
|
},
|
|
53
|
+
links: {resources: {visibleDataSets: {url: visibleDataSetsUrl}}},
|
|
49
54
|
participants: [
|
|
50
55
|
{
|
|
51
56
|
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/11941033',
|
|
@@ -62,7 +67,6 @@ const exampleInitialLocus = {
|
|
|
62
67
|
],
|
|
63
68
|
self: {
|
|
64
69
|
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/11941033',
|
|
65
|
-
visibleDataSets: ['main', 'self', 'atd-unmuted'],
|
|
66
70
|
person: {},
|
|
67
71
|
htMeta: {
|
|
68
72
|
elementId: {
|
|
@@ -76,6 +80,28 @@ const exampleInitialLocus = {
|
|
|
76
80
|
},
|
|
77
81
|
};
|
|
78
82
|
|
|
83
|
+
const exampleMetadata = {
|
|
84
|
+
htMeta: {
|
|
85
|
+
elementId: {
|
|
86
|
+
type: 'metadata',
|
|
87
|
+
id: 5,
|
|
88
|
+
version: 50,
|
|
89
|
+
},
|
|
90
|
+
dataSetNames: ['self'],
|
|
91
|
+
},
|
|
92
|
+
visibleDataSets: [
|
|
93
|
+
{name: 'main', url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/main'},
|
|
94
|
+
{
|
|
95
|
+
name: 'self',
|
|
96
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/713e9f99/datasets/self',
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: 'atd-unmuted',
|
|
100
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/atd-unmuted',
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
};
|
|
104
|
+
|
|
79
105
|
function createDataSet(name: string, leafCount: number, version = 1) {
|
|
80
106
|
return {
|
|
81
107
|
url: `https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/${name}`,
|
|
@@ -119,7 +145,6 @@ function mockSyncRequest(webexRequest: sinon.SinonStub, datasetUrl: string, resp
|
|
|
119
145
|
}
|
|
120
146
|
|
|
121
147
|
describe('HashTreeParser', () => {
|
|
122
|
-
const visibleDataSetsUrl = 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/visibleDataSets';
|
|
123
148
|
const locusUrl = 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f';
|
|
124
149
|
|
|
125
150
|
let clock;
|
|
@@ -139,12 +164,18 @@ describe('HashTreeParser', () => {
|
|
|
139
164
|
});
|
|
140
165
|
|
|
141
166
|
// Helper to create a HashTreeParser instance with common defaults
|
|
142
|
-
function createHashTreeParser(
|
|
167
|
+
function createHashTreeParser(
|
|
168
|
+
initialLocus: any = exampleInitialLocus,
|
|
169
|
+
metadata: any = exampleMetadata,
|
|
170
|
+
excludedDataSets?: string[]
|
|
171
|
+
) {
|
|
143
172
|
return new HashTreeParser({
|
|
144
173
|
initialLocus,
|
|
174
|
+
metadata,
|
|
145
175
|
webexRequest,
|
|
146
176
|
locusInfoUpdateCallback: callback,
|
|
147
177
|
debugId: 'test',
|
|
178
|
+
excludedDataSets,
|
|
148
179
|
});
|
|
149
180
|
}
|
|
150
181
|
|
|
@@ -197,9 +228,50 @@ describe('HashTreeParser', () => {
|
|
|
197
228
|
body: response,
|
|
198
229
|
});
|
|
199
230
|
}
|
|
231
|
+
|
|
232
|
+
async function checkAsyncDatasetInitialization(
|
|
233
|
+
parser: HashTreeParser,
|
|
234
|
+
newDataSet: {name: string; leafCount: number; url: string}
|
|
235
|
+
) {
|
|
236
|
+
// immediately we don't have the dataset yet, so it should not be in visibleDataSets
|
|
237
|
+
// and no hash tree should exist yet
|
|
238
|
+
expect(parser.visibleDataSets.some((vds) => vds.name === newDataSet.name)).to.be.false;
|
|
239
|
+
assert.isUndefined(parser.dataSets[newDataSet.name]);
|
|
240
|
+
|
|
241
|
+
// Wait for the async initialization to complete (queued as microtask)
|
|
242
|
+
await clock.tickAsync(0);
|
|
243
|
+
|
|
244
|
+
// The visibleDataSets is updated from the metadata object data
|
|
245
|
+
expect(parser.visibleDataSets.some((vds) => vds.name === newDataSet.name)).to.be.true;
|
|
246
|
+
|
|
247
|
+
// Verify that a hash tree was created for newDataSet
|
|
248
|
+
assert.exists(parser.dataSets[newDataSet.name].hashTree);
|
|
249
|
+
assert.equal(parser.dataSets[newDataSet.name].hashTree.numLeaves, newDataSet.leafCount);
|
|
250
|
+
|
|
251
|
+
// Verify getAllDataSetsMetadata was called for async initialization
|
|
252
|
+
assert.calledWith(
|
|
253
|
+
webexRequest,
|
|
254
|
+
sinon.match({
|
|
255
|
+
method: 'GET',
|
|
256
|
+
uri: visibleDataSetsUrl,
|
|
257
|
+
})
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
// Verify sync request was sent for the new dataset
|
|
261
|
+
assert.calledWith(
|
|
262
|
+
webexRequest,
|
|
263
|
+
sinon.match({
|
|
264
|
+
method: 'POST',
|
|
265
|
+
uri: `${newDataSet.url}/sync`,
|
|
266
|
+
})
|
|
267
|
+
);
|
|
268
|
+
}
|
|
200
269
|
it('should correctly initialize trees from initialLocus data', () => {
|
|
201
270
|
const parser = createHashTreeParser();
|
|
202
271
|
|
|
272
|
+
// verify that visibleDataSetsUrl is read out from inside locus
|
|
273
|
+
expect(parser.visibleDataSetsUrl).to.equal(visibleDataSetsUrl);
|
|
274
|
+
|
|
203
275
|
// Check that the correct number of trees are created
|
|
204
276
|
expect(Object.keys(parser.dataSets).length).to.equal(3);
|
|
205
277
|
|
|
@@ -215,7 +287,11 @@ describe('HashTreeParser', () => {
|
|
|
215
287
|
const selfTree = parser.dataSets.self.hashTree;
|
|
216
288
|
expect(selfTree).to.be.instanceOf(HashTree);
|
|
217
289
|
const expectedSelfLeaves = new Array(1).fill(null).map(() => ({}));
|
|
218
|
-
|
|
290
|
+
// Both self (id=4) and metadata (id=5) map to the same leaf (4%1=0, 5%1=0)
|
|
291
|
+
expectedSelfLeaves[0] = {
|
|
292
|
+
self: {4: {type: 'self', id: 4, version: 100}},
|
|
293
|
+
metadata: {5: {type: 'metadata', id: 5, version: 50}},
|
|
294
|
+
};
|
|
219
295
|
expect(selfTree.leaves).to.deep.equal(expectedSelfLeaves);
|
|
220
296
|
expect(selfTree.numLeaves).to.equal(1);
|
|
221
297
|
|
|
@@ -247,7 +323,7 @@ describe('HashTreeParser', () => {
|
|
|
247
323
|
name: 'empty-set',
|
|
248
324
|
});
|
|
249
325
|
|
|
250
|
-
const parser = createHashTreeParser(modifiedLocus);
|
|
326
|
+
const parser = createHashTreeParser(modifiedLocus, exampleMetadata);
|
|
251
327
|
|
|
252
328
|
expect(Object.keys(parser.dataSets).length).to.equal(4); // main, self, atd-unmuted (now empty), empty-set
|
|
253
329
|
|
|
@@ -260,7 +336,10 @@ describe('HashTreeParser', () => {
|
|
|
260
336
|
|
|
261
337
|
const selfTree = parser.dataSets.self.hashTree;
|
|
262
338
|
const expectedSelfLeaves = new Array(1).fill(null).map(() => ({}));
|
|
263
|
-
expectedSelfLeaves[4 % 1] = {
|
|
339
|
+
expectedSelfLeaves[4 % 1] = {
|
|
340
|
+
self: {4: {type: 'self', id: 4, version: 100}},
|
|
341
|
+
metadata: {5: exampleMetadata.htMeta.elementId},
|
|
342
|
+
};
|
|
264
343
|
expect(selfTree.leaves).to.deep.equal(expectedSelfLeaves);
|
|
265
344
|
expect(selfTree.numLeaves).to.equal(1);
|
|
266
345
|
|
|
@@ -277,31 +356,100 @@ describe('HashTreeParser', () => {
|
|
|
277
356
|
expect(emptySet.hashTree).to.be.undefined;
|
|
278
357
|
});
|
|
279
358
|
|
|
359
|
+
it('should exclude datasets listed in excludedDataSets during initialization', () => {
|
|
360
|
+
const parser = createHashTreeParser(exampleInitialLocus, exampleMetadata, ['atd-unmuted']);
|
|
361
|
+
|
|
362
|
+
// 'atd-unmuted' should be excluded from visibleDataSets
|
|
363
|
+
expect(parser.visibleDataSets.some((vds) => vds.name === 'atd-unmuted')).to.be.false;
|
|
364
|
+
|
|
365
|
+
// 'main' and 'self' should still be visible
|
|
366
|
+
expect(parser.visibleDataSets.some((vds) => vds.name === 'main')).to.be.true;
|
|
367
|
+
expect(parser.visibleDataSets.some((vds) => vds.name === 'self')).to.be.true;
|
|
368
|
+
|
|
369
|
+
// 'atd-unmuted' dataset entry should exist but without a hash tree (because it's not visible)
|
|
370
|
+
expect(parser.dataSets['atd-unmuted']).to.exist;
|
|
371
|
+
expect(parser.dataSets['atd-unmuted'].hashTree).to.be.undefined;
|
|
372
|
+
|
|
373
|
+
// 'main' and 'self' should have hash trees
|
|
374
|
+
expect(parser.dataSets.main.hashTree).to.be.instanceOf(HashTree);
|
|
375
|
+
expect(parser.dataSets.self.hashTree).to.be.instanceOf(HashTree);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it('should exclude datasets listed in excludedDataSets when adding new visible datasets', async () => {
|
|
379
|
+
// Create parser without 'atd-unmuted' in initial metadata visibleDataSets
|
|
380
|
+
const metadataWithoutAtdUnmuted = {
|
|
381
|
+
...exampleMetadata,
|
|
382
|
+
visibleDataSets: exampleMetadata.visibleDataSets.filter((vds) => vds.name !== 'atd-unmuted'),
|
|
383
|
+
};
|
|
384
|
+
const parser = createHashTreeParser(exampleInitialLocus, metadataWithoutAtdUnmuted, [
|
|
385
|
+
'atd-unmuted',
|
|
386
|
+
]);
|
|
387
|
+
|
|
388
|
+
// 'atd-unmuted' should not be in visibleDataSets initially
|
|
389
|
+
expect(parser.visibleDataSets.some((vds) => vds.name === 'atd-unmuted')).to.be.false;
|
|
390
|
+
|
|
391
|
+
// Now simulate initializeDataSets which calls addToVisibleDataSetsList
|
|
392
|
+
const atdUnmutedDataSet = createDataSet('atd-unmuted', 16, 3000);
|
|
393
|
+
|
|
394
|
+
mockGetAllDataSetsMetadata(webexRequest, visibleDataSetsUrl, [
|
|
395
|
+
createDataSet('main', 16, 1000),
|
|
396
|
+
createDataSet('self', 1, 2000),
|
|
397
|
+
atdUnmutedDataSet,
|
|
398
|
+
]);
|
|
399
|
+
|
|
400
|
+
mockSyncRequest(
|
|
401
|
+
webexRequest,
|
|
402
|
+
'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/atd-unmuted'
|
|
403
|
+
);
|
|
404
|
+
|
|
405
|
+
const message = {
|
|
406
|
+
dataSets: [createDataSet('main', 16, 1000)],
|
|
407
|
+
visibleDataSetsUrl,
|
|
408
|
+
locusUrl,
|
|
409
|
+
};
|
|
410
|
+
await parser.initializeFromMessage(message);
|
|
411
|
+
|
|
412
|
+
// 'atd-unmuted' should still not be in visibleDataSets because it is excluded
|
|
413
|
+
expect(parser.visibleDataSets.some((vds) => vds.name === 'atd-unmuted')).to.be.false;
|
|
414
|
+
// but 'main' and 'self' should be there
|
|
415
|
+
expect(parser.visibleDataSets.some((vds) => vds.name === 'main')).to.be.true;
|
|
416
|
+
expect(parser.visibleDataSets.some((vds) => vds.name === 'self')).to.be.true;
|
|
417
|
+
});
|
|
418
|
+
|
|
280
419
|
// helper method, needed because both initializeFromMessage and initializeFromGetLociResponse
|
|
281
420
|
// do almost exactly the same thing
|
|
282
421
|
const testInitializationOfDatasetsAndHashTrees = async (testCallback) => {
|
|
283
422
|
// Create a parser with minimal initial data
|
|
284
423
|
const minimalInitialLocus = {
|
|
285
424
|
dataSets: [],
|
|
286
|
-
locus:
|
|
287
|
-
|
|
288
|
-
|
|
425
|
+
locus: null,
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
const minimalMetadata = {
|
|
429
|
+
htMeta: {
|
|
430
|
+
elementId: {
|
|
431
|
+
type: 'metadata',
|
|
432
|
+
id: 5,
|
|
433
|
+
version: 50,
|
|
289
434
|
},
|
|
435
|
+
dataSetNames: ['self'],
|
|
290
436
|
},
|
|
437
|
+
visibleDataSets: [
|
|
438
|
+
{name: 'main', url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/main'},
|
|
439
|
+
{
|
|
440
|
+
name: 'self',
|
|
441
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/713e9f99/datasets/self',
|
|
442
|
+
},
|
|
443
|
+
],
|
|
291
444
|
};
|
|
292
445
|
|
|
293
|
-
const hashTreeParser = createHashTreeParser(minimalInitialLocus);
|
|
446
|
+
const hashTreeParser = createHashTreeParser(minimalInitialLocus, minimalMetadata);
|
|
294
447
|
|
|
295
448
|
// Setup the datasets that will be returned from getAllDataSetsMetadata
|
|
296
449
|
const mainDataSet = createDataSet('main', 16, 1100);
|
|
297
450
|
const selfDataSet = createDataSet('self', 1, 2100);
|
|
298
|
-
const invisibleDataSet = createDataSet('invisible', 4, 4000);
|
|
299
451
|
|
|
300
|
-
mockGetAllDataSetsMetadata(webexRequest, visibleDataSetsUrl, [
|
|
301
|
-
mainDataSet,
|
|
302
|
-
selfDataSet,
|
|
303
|
-
invisibleDataSet,
|
|
304
|
-
]);
|
|
452
|
+
mockGetAllDataSetsMetadata(webexRequest, visibleDataSetsUrl, [mainDataSet, selfDataSet]);
|
|
305
453
|
|
|
306
454
|
// Mock sync requests for visible datasets with some updated objects
|
|
307
455
|
const mainSyncResponse = {
|
|
@@ -357,15 +505,16 @@ describe('HashTreeParser', () => {
|
|
|
357
505
|
})
|
|
358
506
|
);
|
|
359
507
|
|
|
360
|
-
//
|
|
508
|
+
// verify that visibleDataSetsUrl is set on the parser
|
|
509
|
+
expect(hashTreeParser.visibleDataSetsUrl).to.equal(visibleDataSetsUrl);
|
|
510
|
+
|
|
511
|
+
// Verify all datasets returned from visibleDataSetsUrl are added to dataSets
|
|
361
512
|
expect(hashTreeParser.dataSets.main).to.exist;
|
|
362
513
|
expect(hashTreeParser.dataSets.self).to.exist;
|
|
363
|
-
expect(hashTreeParser.dataSets.invisible).to.exist;
|
|
364
514
|
|
|
365
515
|
// Verify hash trees are created only for visible datasets
|
|
366
516
|
expect(hashTreeParser.dataSets.main.hashTree).to.be.instanceOf(HashTree);
|
|
367
517
|
expect(hashTreeParser.dataSets.self.hashTree).to.be.instanceOf(HashTree);
|
|
368
|
-
expect(hashTreeParser.dataSets.invisible.hashTree).to.be.undefined;
|
|
369
518
|
|
|
370
519
|
// Verify hash trees have correct leaf counts
|
|
371
520
|
expect(hashTreeParser.dataSets.main.hashTree.numLeaves).to.equal(16);
|
|
@@ -403,15 +552,6 @@ describe('HashTreeParser', () => {
|
|
|
403
552
|
})
|
|
404
553
|
);
|
|
405
554
|
|
|
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
555
|
// Verify callback was called with OBJECTS_UPDATED and correct updatedObjects list
|
|
416
556
|
assert.calledWith(callback, LocusInfoUpdateType.OBJECTS_UPDATED, {
|
|
417
557
|
updatedObjects: [
|
|
@@ -443,8 +583,6 @@ describe('HashTreeParser', () => {
|
|
|
443
583
|
// verify that sync timers are set for visible datasets
|
|
444
584
|
expect(hashTreeParser.dataSets.main.timer).to.not.be.undefined;
|
|
445
585
|
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
586
|
};
|
|
449
587
|
|
|
450
588
|
describe('#initializeFromMessage', () => {
|
|
@@ -457,11 +595,91 @@ describe('HashTreeParser', () => {
|
|
|
457
595
|
});
|
|
458
596
|
});
|
|
459
597
|
});
|
|
598
|
+
|
|
599
|
+
it('handles sync response that has locusStateElements undefined', async () => {
|
|
600
|
+
const minimalInitialLocus = {
|
|
601
|
+
dataSets: [],
|
|
602
|
+
locus: null,
|
|
603
|
+
};
|
|
604
|
+
|
|
605
|
+
const parser = createHashTreeParser(minimalInitialLocus, null);
|
|
606
|
+
|
|
607
|
+
const mainDataSet = createDataSet('main', 16, 1100);
|
|
608
|
+
|
|
609
|
+
// Mock getAllVisibleDataSetsFromLocus to return the main dataset
|
|
610
|
+
mockGetAllDataSetsMetadata(webexRequest, visibleDataSetsUrl, [mainDataSet]);
|
|
611
|
+
|
|
612
|
+
// Mock the sync response to have locusStateElements: undefined
|
|
613
|
+
// This is what sendInitializationSyncRequestToLocus will receive and pass to parseMessage
|
|
614
|
+
mockSyncRequest(webexRequest, mainDataSet.url, {
|
|
615
|
+
dataSets: [mainDataSet],
|
|
616
|
+
visibleDataSetsUrl,
|
|
617
|
+
locusUrl,
|
|
618
|
+
locusStateElements: undefined,
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
// Trigger sendInitializationSyncRequestToLocus via initializeFromMessage
|
|
622
|
+
await parser.initializeFromMessage({
|
|
623
|
+
dataSets: [],
|
|
624
|
+
visibleDataSetsUrl,
|
|
625
|
+
locusUrl,
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
// Verify the hash tree was created for main dataset
|
|
629
|
+
expect(parser.dataSets.main.hashTree).to.be.instanceOf(HashTree);
|
|
630
|
+
|
|
631
|
+
// updateItems should NOT have been called because locusStateElements is undefined
|
|
632
|
+
const mainUpdateItemsStub = sinon.spy(parser.dataSets.main.hashTree, 'updateItems');
|
|
633
|
+
assert.notCalled(mainUpdateItemsStub);
|
|
634
|
+
|
|
635
|
+
// callback should not be called, because there are no updates
|
|
636
|
+
assert.notCalled(callback);
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
[404, 409].forEach((errorCode) => {
|
|
640
|
+
it(`emits MeetingEndedError if getting visible datasets returns ${errorCode}`, async () => {
|
|
641
|
+
const minimalInitialLocus = {
|
|
642
|
+
dataSets: [],
|
|
643
|
+
locus: null,
|
|
644
|
+
};
|
|
645
|
+
|
|
646
|
+
const parser = createHashTreeParser(minimalInitialLocus, null);
|
|
647
|
+
|
|
648
|
+
// Mock getAllVisibleDataSetsFromLocus to reject with the error code
|
|
649
|
+
const error: any = new Error(`Request failed with status ${errorCode}`);
|
|
650
|
+
error.statusCode = errorCode;
|
|
651
|
+
if (errorCode === 409) {
|
|
652
|
+
error.body = {errorCode: 2403004};
|
|
653
|
+
}
|
|
654
|
+
webexRequest
|
|
655
|
+
.withArgs(
|
|
656
|
+
sinon.match({
|
|
657
|
+
method: 'GET',
|
|
658
|
+
uri: visibleDataSetsUrl,
|
|
659
|
+
})
|
|
660
|
+
)
|
|
661
|
+
.rejects(error);
|
|
662
|
+
|
|
663
|
+
// initializeFromMessage should throw MeetingEndedError
|
|
664
|
+
let thrownError;
|
|
665
|
+
try {
|
|
666
|
+
await parser.initializeFromMessage({
|
|
667
|
+
dataSets: [],
|
|
668
|
+
visibleDataSetsUrl,
|
|
669
|
+
locusUrl,
|
|
670
|
+
});
|
|
671
|
+
} catch (e) {
|
|
672
|
+
thrownError = e;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
expect(thrownError).to.be.instanceOf(MeetingEndedError);
|
|
676
|
+
});
|
|
677
|
+
});
|
|
460
678
|
});
|
|
461
679
|
|
|
462
680
|
describe('#initializeFromGetLociResponse', () => {
|
|
463
681
|
it('does nothing if url for visibleDataSets is missing from locus', async () => {
|
|
464
|
-
const parser = createHashTreeParser({dataSets: [], locus: {}});
|
|
682
|
+
const parser = createHashTreeParser({dataSets: [], locus: {}}, null);
|
|
465
683
|
|
|
466
684
|
await parser.initializeFromGetLociResponse({participants: []});
|
|
467
685
|
|
|
@@ -488,12 +706,9 @@ describe('HashTreeParser', () => {
|
|
|
488
706
|
it('updates hash trees based on provided new locus', () => {
|
|
489
707
|
const parser = createHashTreeParser();
|
|
490
708
|
|
|
491
|
-
const mainPutItemsSpy = sinon
|
|
492
|
-
|
|
493
|
-
const
|
|
494
|
-
.spy(parser.dataSets.self.hashTree, 'putItems');
|
|
495
|
-
const atdUnmutedPutItemsSpy = sinon
|
|
496
|
-
.spy(parser.dataSets['atd-unmuted'].hashTree, 'putItems');
|
|
709
|
+
const mainPutItemsSpy = sinon.spy(parser.dataSets.main.hashTree, 'putItems');
|
|
710
|
+
const selfPutItemsSpy = sinon.spy(parser.dataSets.self.hashTree, 'putItems');
|
|
711
|
+
const atdUnmutedPutItemsSpy = sinon.spy(parser.dataSets['atd-unmuted'].hashTree, 'putItems');
|
|
497
712
|
|
|
498
713
|
// Create a locus update with new htMeta information for some things
|
|
499
714
|
const locusUpdate = {
|
|
@@ -540,7 +755,6 @@ describe('HashTreeParser', () => {
|
|
|
540
755
|
],
|
|
541
756
|
self: {
|
|
542
757
|
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/11941033',
|
|
543
|
-
visibleDataSets: ['main', 'self', 'atd-unmuted'],
|
|
544
758
|
person: {},
|
|
545
759
|
htMeta: {
|
|
546
760
|
elementId: {
|
|
@@ -647,6 +861,116 @@ describe('HashTreeParser', () => {
|
|
|
647
861
|
});
|
|
648
862
|
});
|
|
649
863
|
|
|
864
|
+
it('handles updates to control entries correctly', () => {
|
|
865
|
+
const parser = createHashTreeParser();
|
|
866
|
+
|
|
867
|
+
const mainPutItemsSpy = sinon.spy(parser.dataSets.main.hashTree, 'putItems');
|
|
868
|
+
|
|
869
|
+
// Create a locus update with new htMeta information for some things
|
|
870
|
+
const locusUpdate = {
|
|
871
|
+
dataSets: [
|
|
872
|
+
createDataSet('main', 16, 1100),
|
|
873
|
+
],
|
|
874
|
+
locus: {
|
|
875
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f',
|
|
876
|
+
htMeta: {
|
|
877
|
+
elementId: {
|
|
878
|
+
type: 'locus',
|
|
879
|
+
id: 0,
|
|
880
|
+
version: 200, // same version
|
|
881
|
+
},
|
|
882
|
+
dataSetNames: ['main'],
|
|
883
|
+
},
|
|
884
|
+
participants: [],
|
|
885
|
+
controls: {
|
|
886
|
+
lock: {
|
|
887
|
+
locked: true,
|
|
888
|
+
htMeta: {
|
|
889
|
+
elementId: {
|
|
890
|
+
type: 'ControlEntry',
|
|
891
|
+
id: 10100,
|
|
892
|
+
version: 100,
|
|
893
|
+
},
|
|
894
|
+
dataSetNames: ['main'],
|
|
895
|
+
},
|
|
896
|
+
},
|
|
897
|
+
stream: {
|
|
898
|
+
streaming: true,
|
|
899
|
+
htMeta: {
|
|
900
|
+
elementId: {
|
|
901
|
+
type: 'ControlEntry',
|
|
902
|
+
id: 10101,
|
|
903
|
+
version: 100,
|
|
904
|
+
},
|
|
905
|
+
dataSetNames: ['main'],
|
|
906
|
+
},
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
},
|
|
910
|
+
};
|
|
911
|
+
|
|
912
|
+
// Call handleLocusUpdate
|
|
913
|
+
parser.handleLocusUpdate(locusUpdate);
|
|
914
|
+
|
|
915
|
+
// Verify putItems was called on main hash tree with correct data
|
|
916
|
+
assert.calledOnceWithExactly(mainPutItemsSpy, [
|
|
917
|
+
{type: 'locus', id: 0, version: 200},
|
|
918
|
+
{type: 'ControlEntry', id: 10100, version: 100},
|
|
919
|
+
{type: 'ControlEntry', id: 10101, version: 100}
|
|
920
|
+
]);
|
|
921
|
+
|
|
922
|
+
assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED, {
|
|
923
|
+
updatedObjects: [
|
|
924
|
+
{
|
|
925
|
+
htMeta: {
|
|
926
|
+
elementId: {
|
|
927
|
+
type: 'ControlEntry',
|
|
928
|
+
id: 10100,
|
|
929
|
+
version: 100,
|
|
930
|
+
},
|
|
931
|
+
dataSetNames: ['main'],
|
|
932
|
+
},
|
|
933
|
+
data: {
|
|
934
|
+
lock: {
|
|
935
|
+
locked: true,
|
|
936
|
+
htMeta: {
|
|
937
|
+
elementId: {
|
|
938
|
+
type: 'ControlEntry',
|
|
939
|
+
id: 10100,
|
|
940
|
+
version: 100,
|
|
941
|
+
},
|
|
942
|
+
dataSetNames: ['main'],
|
|
943
|
+
},
|
|
944
|
+
},
|
|
945
|
+
},
|
|
946
|
+
},
|
|
947
|
+
{
|
|
948
|
+
htMeta: {
|
|
949
|
+
elementId: {
|
|
950
|
+
type: 'ControlEntry',
|
|
951
|
+
id: 10101,
|
|
952
|
+
version: 100,
|
|
953
|
+
},
|
|
954
|
+
dataSetNames: ['main'],
|
|
955
|
+
},
|
|
956
|
+
data: {
|
|
957
|
+
stream: {
|
|
958
|
+
streaming: true,
|
|
959
|
+
htMeta: {
|
|
960
|
+
elementId: {
|
|
961
|
+
type: 'ControlEntry',
|
|
962
|
+
id: 10101,
|
|
963
|
+
version: 100,
|
|
964
|
+
},
|
|
965
|
+
dataSetNames: ['main'],
|
|
966
|
+
},
|
|
967
|
+
},
|
|
968
|
+
},
|
|
969
|
+
}
|
|
970
|
+
],
|
|
971
|
+
});
|
|
972
|
+
});
|
|
973
|
+
|
|
650
974
|
it('handles unknown datasets gracefully', () => {
|
|
651
975
|
const parser = createHashTreeParser();
|
|
652
976
|
|
|
@@ -711,47 +1035,313 @@ describe('HashTreeParser', () => {
|
|
|
711
1035
|
],
|
|
712
1036
|
});
|
|
713
1037
|
});
|
|
714
|
-
});
|
|
715
1038
|
|
|
716
|
-
|
|
717
|
-
it('handles root hash heartbeat message correctly', async () => {
|
|
1039
|
+
it('handles metadata updates with new version', async () => {
|
|
718
1040
|
const parser = createHashTreeParser();
|
|
719
1041
|
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
1042
|
+
const selfPutItemSpy = sinon.spy(parser.dataSets.self.hashTree, 'putItem');
|
|
1043
|
+
|
|
1044
|
+
// Create a locus update with updated metadata
|
|
1045
|
+
const locusUpdate = {
|
|
1046
|
+
dataSets: [createDataSet('self', 1, 2100), createDataSet('attendees', 8, 4000)],
|
|
1047
|
+
locus: {
|
|
1048
|
+
links: {resources: {visibleDataSets: {url: visibleDataSetsUrl}}},
|
|
1049
|
+
participants: [
|
|
1050
|
+
{
|
|
1051
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/15',
|
|
1052
|
+
person: {},
|
|
1053
|
+
htMeta: {
|
|
1054
|
+
elementId: {
|
|
1055
|
+
type: 'participant',
|
|
1056
|
+
id: 15, // new participant
|
|
1057
|
+
version: 999,
|
|
1058
|
+
},
|
|
1059
|
+
dataSetNames: ['attendees'],
|
|
1060
|
+
},
|
|
1061
|
+
},
|
|
1062
|
+
],
|
|
1063
|
+
},
|
|
1064
|
+
metadata: {
|
|
1065
|
+
htMeta: {
|
|
1066
|
+
elementId: {
|
|
1067
|
+
type: 'metadata',
|
|
1068
|
+
id: 5,
|
|
1069
|
+
version: 51, // incremented version
|
|
1070
|
+
},
|
|
1071
|
+
dataSetNames: ['self'],
|
|
726
1072
|
},
|
|
727
|
-
|
|
1073
|
+
// new visibleDataSets: atd-unmuted removed, "attendees" and "new-dataset" added
|
|
1074
|
+
visibleDataSets: [
|
|
1075
|
+
{
|
|
1076
|
+
name: 'main',
|
|
1077
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/main',
|
|
1078
|
+
},
|
|
1079
|
+
{
|
|
1080
|
+
name: 'self',
|
|
1081
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/713e9f99/datasets/self',
|
|
1082
|
+
},
|
|
1083
|
+
{
|
|
1084
|
+
name: 'new-dataset', // this one is not in dataSets, so will require async initialization
|
|
1085
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/new-dataset',
|
|
1086
|
+
},
|
|
1087
|
+
{
|
|
1088
|
+
name: 'attendees', // this one is in dataSets, so should be processed immediately
|
|
1089
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/attendees',
|
|
1090
|
+
},
|
|
1091
|
+
],
|
|
1092
|
+
},
|
|
1093
|
+
};
|
|
1094
|
+
|
|
1095
|
+
// Mock the async initialization of the new dataset
|
|
1096
|
+
const newDataSet = createDataSet('new-dataset', 4, 5000);
|
|
1097
|
+
mockGetAllDataSetsMetadata(webexRequest, visibleDataSetsUrl, [newDataSet]);
|
|
1098
|
+
mockSyncRequest(webexRequest, newDataSet.url, {
|
|
1099
|
+
dataSets: [newDataSet],
|
|
728
1100
|
visibleDataSetsUrl,
|
|
729
1101
|
locusUrl,
|
|
730
|
-
locusStateElements: [
|
|
1102
|
+
locusStateElements: [],
|
|
1103
|
+
});
|
|
1104
|
+
|
|
1105
|
+
// Call handleLocusUpdate
|
|
1106
|
+
parser.handleLocusUpdate(locusUpdate);
|
|
1107
|
+
|
|
1108
|
+
// Verify putItem was called on self hash tree with metadata
|
|
1109
|
+
assert.calledOnceWithExactly(selfPutItemSpy, {type: 'metadata', id: 5, version: 51});
|
|
1110
|
+
|
|
1111
|
+
// Verify callback was called with metadata object and removed dataset objects
|
|
1112
|
+
assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED, {
|
|
1113
|
+
updatedObjects: [
|
|
1114
|
+
// updated metadata object:
|
|
731
1115
|
{
|
|
732
1116
|
htMeta: {
|
|
733
1117
|
elementId: {
|
|
734
|
-
type: '
|
|
735
|
-
id:
|
|
736
|
-
version:
|
|
1118
|
+
type: 'metadata',
|
|
1119
|
+
id: 5,
|
|
1120
|
+
version: 51,
|
|
737
1121
|
},
|
|
738
|
-
dataSetNames: ['
|
|
1122
|
+
dataSetNames: ['self'],
|
|
1123
|
+
},
|
|
1124
|
+
data: {
|
|
1125
|
+
htMeta: {
|
|
1126
|
+
elementId: {
|
|
1127
|
+
type: 'metadata',
|
|
1128
|
+
id: 5,
|
|
1129
|
+
version: 51,
|
|
1130
|
+
},
|
|
1131
|
+
dataSetNames: ['self'],
|
|
1132
|
+
},
|
|
1133
|
+
visibleDataSets: [
|
|
1134
|
+
{
|
|
1135
|
+
name: 'main',
|
|
1136
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/main',
|
|
1137
|
+
},
|
|
1138
|
+
{
|
|
1139
|
+
name: 'self',
|
|
1140
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/713e9f99/datasets/self',
|
|
1141
|
+
},
|
|
1142
|
+
{
|
|
1143
|
+
name: 'new-dataset',
|
|
1144
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/new-dataset',
|
|
1145
|
+
},
|
|
1146
|
+
{
|
|
1147
|
+
name: 'attendees',
|
|
1148
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/attendees',
|
|
1149
|
+
},
|
|
1150
|
+
],
|
|
739
1151
|
},
|
|
740
|
-
data: {someData: 'value'},
|
|
741
1152
|
},
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
1153
|
+
// removed participant from a removed dataset 'atd-unmuted':
|
|
1154
|
+
{
|
|
1155
|
+
htMeta: {
|
|
1156
|
+
elementId: {
|
|
1157
|
+
type: 'participant',
|
|
1158
|
+
id: 14,
|
|
1159
|
+
version: 300,
|
|
1160
|
+
},
|
|
1161
|
+
dataSetNames: ['atd-unmuted'],
|
|
1162
|
+
},
|
|
1163
|
+
data: null,
|
|
1164
|
+
},
|
|
1165
|
+
// new participant from a new data set 'attendees':
|
|
1166
|
+
{
|
|
1167
|
+
htMeta: {
|
|
1168
|
+
elementId: {
|
|
1169
|
+
type: 'participant',
|
|
1170
|
+
id: 15,
|
|
1171
|
+
version: 999,
|
|
1172
|
+
},
|
|
1173
|
+
dataSetNames: ['attendees'],
|
|
1174
|
+
},
|
|
1175
|
+
data: {
|
|
1176
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/15',
|
|
1177
|
+
person: {},
|
|
1178
|
+
htMeta: {
|
|
1179
|
+
elementId: {
|
|
1180
|
+
type: 'participant',
|
|
1181
|
+
id: 15,
|
|
1182
|
+
version: 999,
|
|
1183
|
+
},
|
|
1184
|
+
dataSetNames: ['attendees'],
|
|
1185
|
+
},
|
|
1186
|
+
},
|
|
1187
|
+
},
|
|
1188
|
+
],
|
|
1189
|
+
});
|
|
1190
|
+
|
|
1191
|
+
// verify also that an async initialization was done for
|
|
1192
|
+
await checkAsyncDatasetInitialization(parser, newDataSet);
|
|
1193
|
+
});
|
|
1194
|
+
|
|
1195
|
+
it('handles metadata updates with same version (no callback)', () => {
|
|
1196
|
+
const parser = createHashTreeParser();
|
|
1197
|
+
|
|
1198
|
+
const selfPutItemSpy = sinon.spy(parser.dataSets.self.hashTree, 'putItem');
|
|
1199
|
+
|
|
1200
|
+
// Create a locus update with metadata that has the same version and same visibleDataSets
|
|
1201
|
+
const locusUpdate = {
|
|
1202
|
+
dataSets: [createDataSet('self', 1, 2100)],
|
|
1203
|
+
locus: {},
|
|
1204
|
+
metadata: {
|
|
1205
|
+
htMeta: {
|
|
1206
|
+
elementId: {
|
|
1207
|
+
type: 'metadata',
|
|
1208
|
+
id: 5,
|
|
1209
|
+
version: 50, // same version as initial
|
|
1210
|
+
},
|
|
1211
|
+
dataSetNames: ['self'],
|
|
1212
|
+
},
|
|
1213
|
+
visibleDataSets: [
|
|
1214
|
+
{
|
|
1215
|
+
name: 'main',
|
|
1216
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/main',
|
|
1217
|
+
},
|
|
1218
|
+
{
|
|
1219
|
+
name: 'self',
|
|
1220
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/713e9f99/datasets/self',
|
|
1221
|
+
},
|
|
1222
|
+
{
|
|
1223
|
+
name: 'atd-unmuted',
|
|
1224
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/atd-unmuted',
|
|
1225
|
+
},
|
|
1226
|
+
],
|
|
1227
|
+
},
|
|
1228
|
+
};
|
|
1229
|
+
|
|
1230
|
+
// Call handleLocusUpdate
|
|
1231
|
+
parser.handleLocusUpdate(locusUpdate);
|
|
1232
|
+
|
|
1233
|
+
// Verify putItem was called on self hash tree
|
|
1234
|
+
assert.calledOnceWithExactly(selfPutItemSpy, {type: 'metadata', id: 5, version: 50});
|
|
1235
|
+
|
|
1236
|
+
// Verify callback was NOT called because version didn't change
|
|
1237
|
+
assert.notCalled(callback);
|
|
1238
|
+
});
|
|
1239
|
+
|
|
1240
|
+
it('handles updates with no dataSets and metadata fields gracefully', () => {
|
|
1241
|
+
const parser = createHashTreeParser();
|
|
1242
|
+
|
|
1243
|
+
const mainPutItemsSpy = sinon.spy(parser.dataSets.main.hashTree, 'putItems');
|
|
1244
|
+
const selfPutItemsSpy = sinon.spy(parser.dataSets.self.hashTree, 'putItems');
|
|
1245
|
+
const atdUnmutedPutItemsSpy = sinon.spy(parser.dataSets['atd-unmuted'].hashTree, 'putItems');
|
|
1246
|
+
|
|
1247
|
+
// Create a locus update with no dataSets and no metadata
|
|
1248
|
+
const locusUpdate = {
|
|
1249
|
+
locus: {
|
|
1250
|
+
htMeta: {
|
|
1251
|
+
elementId: {
|
|
1252
|
+
type: 'locus',
|
|
1253
|
+
id: 0,
|
|
1254
|
+
version: 201,
|
|
1255
|
+
},
|
|
1256
|
+
dataSetNames: ['main'],
|
|
1257
|
+
},
|
|
1258
|
+
someData: 'value',
|
|
1259
|
+
},
|
|
1260
|
+
};
|
|
1261
|
+
|
|
1262
|
+
// Call handleLocusUpdate - should not throw
|
|
1263
|
+
parser.handleLocusUpdate(locusUpdate);
|
|
1264
|
+
|
|
1265
|
+
// Verify putItems was still called for the dataset referenced in locus
|
|
1266
|
+
assert.calledOnceWithExactly(mainPutItemsSpy, [{type: 'locus', id: 0, version: 201}]);
|
|
1267
|
+
|
|
1268
|
+
// Verify putItems was not called on other hash trees
|
|
1269
|
+
assert.notCalled(selfPutItemsSpy);
|
|
1270
|
+
assert.notCalled(atdUnmutedPutItemsSpy);
|
|
1271
|
+
|
|
1272
|
+
// Verify callback was called with the updated object
|
|
1273
|
+
assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED, {
|
|
1274
|
+
updatedObjects: [
|
|
1275
|
+
{
|
|
1276
|
+
htMeta: {
|
|
1277
|
+
elementId: {
|
|
1278
|
+
type: 'locus',
|
|
1279
|
+
id: 0,
|
|
1280
|
+
version: 201,
|
|
1281
|
+
},
|
|
1282
|
+
dataSetNames: ['main'],
|
|
1283
|
+
},
|
|
1284
|
+
data: {
|
|
1285
|
+
someData: 'value',
|
|
1286
|
+
htMeta: {
|
|
1287
|
+
elementId: {
|
|
1288
|
+
type: 'locus',
|
|
1289
|
+
id: 0,
|
|
1290
|
+
version: 201,
|
|
1291
|
+
},
|
|
1292
|
+
dataSetNames: ['main'],
|
|
1293
|
+
},
|
|
1294
|
+
},
|
|
1295
|
+
},
|
|
1296
|
+
],
|
|
1297
|
+
});
|
|
1298
|
+
|
|
1299
|
+
// Verify that dataset versions were NOT updated (no dataSets in the update)
|
|
1300
|
+
expect(parser.dataSets.main.version).to.equal(1000);
|
|
1301
|
+
expect(parser.dataSets.self.version).to.equal(2000);
|
|
1302
|
+
expect(parser.dataSets['atd-unmuted'].version).to.equal(3000);
|
|
1303
|
+
});
|
|
1304
|
+
});
|
|
1305
|
+
|
|
1306
|
+
describe('#handleMessage', () => {
|
|
1307
|
+
it('handles root hash heartbeat message correctly', async () => {
|
|
1308
|
+
const parser = createHashTreeParser();
|
|
1309
|
+
|
|
1310
|
+
// Step 1: Send a normal message with locusStateElements to start the sync timer
|
|
1311
|
+
const normalMessage = {
|
|
1312
|
+
dataSets: [
|
|
1313
|
+
{
|
|
1314
|
+
...createDataSet('main', 16, 1100),
|
|
1315
|
+
root: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1', // different from our hash
|
|
1316
|
+
},
|
|
1317
|
+
],
|
|
1318
|
+
visibleDataSetsUrl,
|
|
1319
|
+
locusUrl,
|
|
1320
|
+
locusStateElements: [
|
|
1321
|
+
{
|
|
1322
|
+
htMeta: {
|
|
1323
|
+
elementId: {
|
|
1324
|
+
type: 'locus' as const,
|
|
1325
|
+
id: 0,
|
|
1326
|
+
version: 201,
|
|
1327
|
+
},
|
|
1328
|
+
dataSetNames: ['main'],
|
|
1329
|
+
},
|
|
1330
|
+
data: {someData: 'value'},
|
|
1331
|
+
},
|
|
1332
|
+
],
|
|
1333
|
+
};
|
|
1334
|
+
|
|
1335
|
+
parser.handleMessage(normalMessage, 'initial message');
|
|
1336
|
+
|
|
1337
|
+
// Verify the timer was set (the sync algorithm should have started)
|
|
1338
|
+
expect(parser.dataSets.main.timer).to.not.be.undefined;
|
|
1339
|
+
const firstTimerDelay = parser.dataSets.main.idleMs; // 1000ms base + random backoff
|
|
1340
|
+
|
|
1341
|
+
// Step 2: Simulate half of the time passing
|
|
1342
|
+
clock.tick(500);
|
|
1343
|
+
|
|
1344
|
+
// Verify no webex requests have been made yet
|
|
755
1345
|
assert.notCalled(webexRequest);
|
|
756
1346
|
|
|
757
1347
|
// Step 3: Send a heartbeat message (no locusStateElements) with mismatched root hash
|
|
@@ -762,7 +1352,7 @@ describe('HashTreeParser', () => {
|
|
|
762
1352
|
'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' // still different from our hash
|
|
763
1353
|
);
|
|
764
1354
|
|
|
765
|
-
|
|
1355
|
+
parser.handleMessage(heartbeatMessage, 'heartbeat message');
|
|
766
1356
|
|
|
767
1357
|
// Verify the timer was restarted (should still exist)
|
|
768
1358
|
expect(parser.dataSets.main.timer).to.not.be.undefined;
|
|
@@ -889,7 +1479,7 @@ describe('HashTreeParser', () => {
|
|
|
889
1479
|
],
|
|
890
1480
|
};
|
|
891
1481
|
|
|
892
|
-
|
|
1482
|
+
parser.handleMessage(message, 'normal update');
|
|
893
1483
|
|
|
894
1484
|
// Verify updateItems was called on main hash tree
|
|
895
1485
|
assert.calledOnceWithExactly(mainUpdateItemsStub, [
|
|
@@ -910,13 +1500,6 @@ describe('HashTreeParser', () => {
|
|
|
910
1500
|
// Verify callback was called with OBJECTS_UPDATED and all updated objects
|
|
911
1501
|
assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED, {
|
|
912
1502
|
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
1503
|
{
|
|
921
1504
|
htMeta: {
|
|
922
1505
|
elementId: {type: 'locus', id: 0, version: 201},
|
|
@@ -924,10 +1507,6 @@ describe('HashTreeParser', () => {
|
|
|
924
1507
|
},
|
|
925
1508
|
data: {info: {id: 'updated-locus-info'}},
|
|
926
1509
|
},
|
|
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
1510
|
{
|
|
932
1511
|
htMeta: {
|
|
933
1512
|
elementId: {type: 'self', id: 4, version: 101},
|
|
@@ -953,43 +1532,71 @@ describe('HashTreeParser', () => {
|
|
|
953
1532
|
});
|
|
954
1533
|
});
|
|
955
1534
|
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
1535
|
+
describe('handles sentinel messages correctly', () => {
|
|
1536
|
+
['main', 'self', 'unjoined'].forEach((dataSetName) => {
|
|
1537
|
+
it('emits MEETING_ENDED for sentinel message with dataset ' + dataSetName, async () => {
|
|
1538
|
+
const parser = createHashTreeParser();
|
|
1539
|
+
|
|
1540
|
+
// Create a sentinel message: leafCount=1, root=EMPTY_HASH, version higher than current
|
|
1541
|
+
const sentinelMessage = createHeartbeatMessage(
|
|
1542
|
+
dataSetName,
|
|
1543
|
+
1,
|
|
1544
|
+
parser.dataSets[dataSetName]?.version
|
|
1545
|
+
? parser.dataSets[dataSetName].version + 1
|
|
1546
|
+
: 10000,
|
|
1547
|
+
EMPTY_HASH
|
|
1548
|
+
);
|
|
1549
|
+
|
|
1550
|
+
// If the dataset doesn't exist yet (e.g. 'unjoined'), create it
|
|
1551
|
+
if (!parser.dataSets[dataSetName]) {
|
|
1552
|
+
parser.dataSets[dataSetName] = {
|
|
1553
|
+
url: `https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/${dataSetName}`,
|
|
1554
|
+
name: dataSetName,
|
|
1555
|
+
version: 1,
|
|
1556
|
+
leafCount: 16,
|
|
1557
|
+
root: '0'.repeat(32),
|
|
1558
|
+
idleMs: 1000,
|
|
1559
|
+
backoff: {maxMs: 1000, exponent: 2},
|
|
1560
|
+
} as any;
|
|
1561
|
+
}
|
|
961
1562
|
|
|
962
|
-
|
|
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
|
-
};
|
|
1563
|
+
parser.handleMessage(sentinelMessage, 'sentinel message');
|
|
981
1564
|
|
|
982
|
-
|
|
1565
|
+
// Verify callback was called with MEETING_ENDED
|
|
1566
|
+
assert.calledOnceWithExactly(callback, LocusInfoUpdateType.MEETING_ENDED, {
|
|
1567
|
+
updatedObjects: undefined,
|
|
1568
|
+
});
|
|
983
1569
|
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
1570
|
+
// Verify that all timers were stopped
|
|
1571
|
+
Object.values(parser.dataSets).forEach((ds: any) => {
|
|
1572
|
+
assert.isUndefined(ds.timer);
|
|
1573
|
+
assert.isUndefined(ds.heartbeatWatchdogTimer);
|
|
1574
|
+
});
|
|
1575
|
+
});
|
|
987
1576
|
});
|
|
988
1577
|
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
1578
|
+
it('emits MEETING_ENDED for sentinel message with unknown dataset', async () => {
|
|
1579
|
+
const parser = createHashTreeParser();
|
|
1580
|
+
|
|
1581
|
+
// 'unjoined' is a valid sentinel dataset name but is not tracked by the parser
|
|
1582
|
+
assert.isUndefined(parser.dataSets['unjoined']);
|
|
1583
|
+
|
|
1584
|
+
// Create a sentinel message for 'unjoined' dataset which the parser has never seen
|
|
1585
|
+
const sentinelMessage = createHeartbeatMessage('unjoined', 1, 10000, EMPTY_HASH);
|
|
1586
|
+
|
|
1587
|
+
parser.handleMessage(sentinelMessage, 'sentinel message');
|
|
1588
|
+
|
|
1589
|
+
// Verify callback was called with MEETING_ENDED
|
|
1590
|
+
assert.calledOnceWithExactly(callback, LocusInfoUpdateType.MEETING_ENDED, {
|
|
1591
|
+
updatedObjects: undefined,
|
|
1592
|
+
});
|
|
1593
|
+
|
|
1594
|
+
// Verify that all timers were stopped
|
|
1595
|
+
Object.values(parser.dataSets).forEach((ds: any) => {
|
|
1596
|
+
assert.isUndefined(ds.timer);
|
|
1597
|
+
assert.isUndefined(ds.heartbeatWatchdogTimer);
|
|
1598
|
+
});
|
|
1599
|
+
});
|
|
993
1600
|
});
|
|
994
1601
|
|
|
995
1602
|
describe('sync algorithm', () => {
|
|
@@ -1020,7 +1627,7 @@ describe('HashTreeParser', () => {
|
|
|
1020
1627
|
],
|
|
1021
1628
|
};
|
|
1022
1629
|
|
|
1023
|
-
|
|
1630
|
+
parser.handleMessage(message, 'initial message');
|
|
1024
1631
|
|
|
1025
1632
|
// Verify callback was called with initial updates
|
|
1026
1633
|
assert.calledOnce(callback);
|
|
@@ -1090,6 +1697,134 @@ describe('HashTreeParser', () => {
|
|
|
1090
1697
|
],
|
|
1091
1698
|
});
|
|
1092
1699
|
});
|
|
1700
|
+
|
|
1701
|
+
describe('emits MEETING_ENDED', () => {
|
|
1702
|
+
[404, 409].forEach((statusCode) => {
|
|
1703
|
+
it(`when /hashtree returns ${statusCode}`, async () => {
|
|
1704
|
+
const parser = createHashTreeParser();
|
|
1705
|
+
|
|
1706
|
+
// Send a message to trigger sync algorithm
|
|
1707
|
+
const message = {
|
|
1708
|
+
dataSets: [createDataSet('main', 16, 1100)],
|
|
1709
|
+
visibleDataSetsUrl,
|
|
1710
|
+
locusUrl,
|
|
1711
|
+
locusStateElements: [
|
|
1712
|
+
{
|
|
1713
|
+
htMeta: {
|
|
1714
|
+
elementId: {
|
|
1715
|
+
type: 'locus' as const,
|
|
1716
|
+
id: 0,
|
|
1717
|
+
version: 201,
|
|
1718
|
+
},
|
|
1719
|
+
dataSetNames: ['main'],
|
|
1720
|
+
},
|
|
1721
|
+
data: {info: {id: 'initial-update'}},
|
|
1722
|
+
},
|
|
1723
|
+
],
|
|
1724
|
+
};
|
|
1725
|
+
|
|
1726
|
+
parser.handleMessage(message, 'initial message');
|
|
1727
|
+
callback.resetHistory();
|
|
1728
|
+
|
|
1729
|
+
const mainDataSetUrl = parser.dataSets.main.url;
|
|
1730
|
+
|
|
1731
|
+
// Mock getHashesFromLocus to reject with the sentinel error
|
|
1732
|
+
const error: any = new Error(`Request failed with status ${statusCode}`);
|
|
1733
|
+
error.statusCode = statusCode;
|
|
1734
|
+
if (statusCode === 409) {
|
|
1735
|
+
error.body = {errorCode: 2403004};
|
|
1736
|
+
}
|
|
1737
|
+
webexRequest
|
|
1738
|
+
.withArgs(
|
|
1739
|
+
sinon.match({
|
|
1740
|
+
method: 'GET',
|
|
1741
|
+
uri: `${mainDataSetUrl}/hashtree`,
|
|
1742
|
+
})
|
|
1743
|
+
)
|
|
1744
|
+
.rejects(error);
|
|
1745
|
+
|
|
1746
|
+
// Trigger sync by advancing time
|
|
1747
|
+
await clock.tickAsync(1000);
|
|
1748
|
+
|
|
1749
|
+
// Verify callback was called with MEETING_ENDED
|
|
1750
|
+
assert.calledOnceWithExactly(callback, LocusInfoUpdateType.MEETING_ENDED, {
|
|
1751
|
+
updatedObjects: undefined,
|
|
1752
|
+
});
|
|
1753
|
+
|
|
1754
|
+
// Verify all timers are stopped
|
|
1755
|
+
Object.values(parser.dataSets).forEach((ds: any) => {
|
|
1756
|
+
assert.isUndefined(ds.timer);
|
|
1757
|
+
assert.isUndefined(ds.heartbeatWatchdogTimer);
|
|
1758
|
+
});
|
|
1759
|
+
});
|
|
1760
|
+
|
|
1761
|
+
it(`when /sync returns ${statusCode}`, async () => {
|
|
1762
|
+
const parser = createHashTreeParser();
|
|
1763
|
+
|
|
1764
|
+
// Send a message to trigger sync algorithm
|
|
1765
|
+
const message = {
|
|
1766
|
+
dataSets: [createDataSet('main', 16, 1100)],
|
|
1767
|
+
visibleDataSetsUrl,
|
|
1768
|
+
locusUrl,
|
|
1769
|
+
locusStateElements: [
|
|
1770
|
+
{
|
|
1771
|
+
htMeta: {
|
|
1772
|
+
elementId: {
|
|
1773
|
+
type: 'locus' as const,
|
|
1774
|
+
id: 0,
|
|
1775
|
+
version: 201,
|
|
1776
|
+
},
|
|
1777
|
+
dataSetNames: ['main'],
|
|
1778
|
+
},
|
|
1779
|
+
data: {info: {id: 'initial-update'}},
|
|
1780
|
+
},
|
|
1781
|
+
],
|
|
1782
|
+
};
|
|
1783
|
+
|
|
1784
|
+
parser.handleMessage(message, 'initial message');
|
|
1785
|
+
callback.resetHistory();
|
|
1786
|
+
|
|
1787
|
+
const mainDataSetUrl = parser.dataSets.main.url;
|
|
1788
|
+
|
|
1789
|
+
// Mock getHashesFromLocus to succeed
|
|
1790
|
+
mockGetHashesFromLocusResponse(
|
|
1791
|
+
mainDataSetUrl,
|
|
1792
|
+
new Array(16).fill('00000000000000000000000000000000'),
|
|
1793
|
+
createDataSet('main', 16, 1101)
|
|
1794
|
+
);
|
|
1795
|
+
|
|
1796
|
+
// Mock sendSyncRequestToLocus to reject with the sentinel error
|
|
1797
|
+
const error: any = new Error(`Request failed with status ${statusCode}`);
|
|
1798
|
+
error.statusCode = statusCode;
|
|
1799
|
+
if (statusCode === 409) {
|
|
1800
|
+
error.body = {errorCode: 2403004};
|
|
1801
|
+
}
|
|
1802
|
+
webexRequest
|
|
1803
|
+
.withArgs(
|
|
1804
|
+
sinon.match({
|
|
1805
|
+
method: 'POST',
|
|
1806
|
+
uri: `${mainDataSetUrl}/sync`,
|
|
1807
|
+
})
|
|
1808
|
+
)
|
|
1809
|
+
.rejects(error);
|
|
1810
|
+
|
|
1811
|
+
// Trigger sync by advancing time
|
|
1812
|
+
await clock.tickAsync(1000);
|
|
1813
|
+
|
|
1814
|
+
// Verify callback was called with MEETING_ENDED
|
|
1815
|
+
assert.calledOnceWithExactly(callback, LocusInfoUpdateType.MEETING_ENDED, {
|
|
1816
|
+
updatedObjects: undefined,
|
|
1817
|
+
});
|
|
1818
|
+
|
|
1819
|
+
// Verify all timers are stopped
|
|
1820
|
+
Object.values(parser.dataSets).forEach((ds: any) => {
|
|
1821
|
+
assert.isUndefined(ds.timer);
|
|
1822
|
+
assert.isUndefined(ds.heartbeatWatchdogTimer);
|
|
1823
|
+
});
|
|
1824
|
+
});
|
|
1825
|
+
});
|
|
1826
|
+
});
|
|
1827
|
+
|
|
1093
1828
|
it('requests only mismatched hashes during sync', async () => {
|
|
1094
1829
|
const parser = createHashTreeParser();
|
|
1095
1830
|
|
|
@@ -1135,7 +1870,7 @@ describe('HashTreeParser', () => {
|
|
|
1135
1870
|
],
|
|
1136
1871
|
};
|
|
1137
1872
|
|
|
1138
|
-
|
|
1873
|
+
parser.handleMessage(message, 'initial message');
|
|
1139
1874
|
|
|
1140
1875
|
callback.resetHistory();
|
|
1141
1876
|
|
|
@@ -1179,6 +1914,9 @@ describe('HashTreeParser', () => {
|
|
|
1179
1914
|
sinon.match({
|
|
1180
1915
|
method: 'GET',
|
|
1181
1916
|
uri: `${mainDataSetUrl}/hashtree`,
|
|
1917
|
+
qs: {
|
|
1918
|
+
rootHash: hashTree.getRootHash(),
|
|
1919
|
+
},
|
|
1182
1920
|
})
|
|
1183
1921
|
);
|
|
1184
1922
|
|
|
@@ -1186,6 +1924,7 @@ describe('HashTreeParser', () => {
|
|
|
1186
1924
|
assert.calledWith(webexRequest, {
|
|
1187
1925
|
method: 'POST',
|
|
1188
1926
|
uri: `${mainDataSetUrl}/sync`,
|
|
1927
|
+
qs: {rootHash: hashTree.getRootHash()},
|
|
1189
1928
|
body: {
|
|
1190
1929
|
leafCount: 16,
|
|
1191
1930
|
leafDataEntries: [
|
|
@@ -1219,7 +1958,7 @@ describe('HashTreeParser', () => {
|
|
|
1219
1958
|
],
|
|
1220
1959
|
};
|
|
1221
1960
|
|
|
1222
|
-
|
|
1961
|
+
parser.handleMessage(message, 'message with self update');
|
|
1223
1962
|
|
|
1224
1963
|
callback.resetHistory();
|
|
1225
1964
|
|
|
@@ -1239,10 +1978,17 @@ describe('HashTreeParser', () => {
|
|
|
1239
1978
|
assert.calledWith(webexRequest, {
|
|
1240
1979
|
method: 'POST',
|
|
1241
1980
|
uri: `${parser.dataSets.self.url}/sync`,
|
|
1981
|
+
qs: {rootHash: parser.dataSets.self.hashTree.getRootHash()},
|
|
1242
1982
|
body: {
|
|
1243
1983
|
leafCount: 1,
|
|
1244
1984
|
leafDataEntries: [
|
|
1245
|
-
{
|
|
1985
|
+
{
|
|
1986
|
+
leafIndex: 0,
|
|
1987
|
+
elementIds: [
|
|
1988
|
+
{type: 'self', id: 4, version: 102},
|
|
1989
|
+
{type: 'metadata', id: 5, version: 50},
|
|
1990
|
+
],
|
|
1991
|
+
},
|
|
1246
1992
|
],
|
|
1247
1993
|
},
|
|
1248
1994
|
});
|
|
@@ -1257,7 +2003,7 @@ describe('HashTreeParser', () => {
|
|
|
1257
2003
|
// Stub updateItems on self hash tree to return true
|
|
1258
2004
|
sinon.stub(parser.dataSets.self.hashTree, 'updateItems').returns([true]);
|
|
1259
2005
|
|
|
1260
|
-
// Send a message with
|
|
2006
|
+
// Send a message with Metadata object that has a new visibleDataSets list
|
|
1261
2007
|
const message = {
|
|
1262
2008
|
dataSets: [createDataSet('self', 1, 2100), createDataSet('attendees', 8, 4000)],
|
|
1263
2009
|
visibleDataSetsUrl,
|
|
@@ -1266,47 +2012,98 @@ describe('HashTreeParser', () => {
|
|
|
1266
2012
|
{
|
|
1267
2013
|
htMeta: {
|
|
1268
2014
|
elementId: {
|
|
1269
|
-
type: '
|
|
1270
|
-
id:
|
|
1271
|
-
version:
|
|
2015
|
+
type: 'metadata' as const,
|
|
2016
|
+
id: 5,
|
|
2017
|
+
version: 51,
|
|
1272
2018
|
},
|
|
1273
2019
|
dataSetNames: ['self'],
|
|
1274
2020
|
},
|
|
1275
2021
|
data: {
|
|
1276
|
-
visibleDataSets: [
|
|
2022
|
+
visibleDataSets: [
|
|
2023
|
+
{
|
|
2024
|
+
name: 'main',
|
|
2025
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/main',
|
|
2026
|
+
},
|
|
2027
|
+
{
|
|
2028
|
+
name: 'self',
|
|
2029
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/713e9f99/datasets/self',
|
|
2030
|
+
},
|
|
2031
|
+
{
|
|
2032
|
+
name: 'atd-unmuted',
|
|
2033
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/atd-unmuted',
|
|
2034
|
+
},
|
|
2035
|
+
{
|
|
2036
|
+
name: 'attendees',
|
|
2037
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/attendees',
|
|
2038
|
+
},
|
|
2039
|
+
], // added 'attendees'
|
|
1277
2040
|
},
|
|
1278
2041
|
},
|
|
1279
2042
|
],
|
|
1280
2043
|
};
|
|
1281
2044
|
|
|
1282
|
-
|
|
2045
|
+
parser.handleMessage(message, 'add visible dataset');
|
|
1283
2046
|
|
|
1284
2047
|
// Verify that 'attendees' was added to visibleDataSets
|
|
1285
|
-
|
|
2048
|
+
expect(parser.visibleDataSets.some((vds) => vds.name === 'attendees')).to.be.true;
|
|
1286
2049
|
|
|
1287
2050
|
// Verify that a hash tree was created for 'attendees'
|
|
1288
2051
|
assert.exists(parser.dataSets.attendees.hashTree);
|
|
1289
2052
|
assert.equal(parser.dataSets.attendees.hashTree.numLeaves, 8);
|
|
1290
2053
|
|
|
1291
|
-
// Verify callback was called with the
|
|
2054
|
+
// Verify callback was called with the metadata update (appears twice - processed once for visible dataset changes, once in main loop)
|
|
1292
2055
|
assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED, {
|
|
1293
2056
|
updatedObjects: [
|
|
1294
2057
|
{
|
|
1295
2058
|
htMeta: {
|
|
1296
|
-
elementId: {type: '
|
|
2059
|
+
elementId: {type: 'metadata', id: 5, version: 51},
|
|
1297
2060
|
dataSetNames: ['self'],
|
|
1298
2061
|
},
|
|
1299
2062
|
data: {
|
|
1300
|
-
visibleDataSets: [
|
|
2063
|
+
visibleDataSets: [
|
|
2064
|
+
{
|
|
2065
|
+
name: 'main',
|
|
2066
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/main',
|
|
2067
|
+
},
|
|
2068
|
+
{
|
|
2069
|
+
name: 'self',
|
|
2070
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/713e9f99/datasets/self',
|
|
2071
|
+
},
|
|
2072
|
+
{
|
|
2073
|
+
name: 'atd-unmuted',
|
|
2074
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/atd-unmuted',
|
|
2075
|
+
},
|
|
2076
|
+
{
|
|
2077
|
+
name: 'attendees',
|
|
2078
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/attendees',
|
|
2079
|
+
},
|
|
2080
|
+
],
|
|
1301
2081
|
},
|
|
1302
2082
|
},
|
|
1303
2083
|
{
|
|
1304
2084
|
htMeta: {
|
|
1305
|
-
elementId: {type: '
|
|
2085
|
+
elementId: {type: 'metadata', id: 5, version: 51},
|
|
1306
2086
|
dataSetNames: ['self'],
|
|
1307
2087
|
},
|
|
1308
2088
|
data: {
|
|
1309
|
-
visibleDataSets: [
|
|
2089
|
+
visibleDataSets: [
|
|
2090
|
+
{
|
|
2091
|
+
name: 'main',
|
|
2092
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/main',
|
|
2093
|
+
},
|
|
2094
|
+
{
|
|
2095
|
+
name: 'self',
|
|
2096
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/713e9f99/datasets/self',
|
|
2097
|
+
},
|
|
2098
|
+
{
|
|
2099
|
+
name: 'atd-unmuted',
|
|
2100
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/atd-unmuted',
|
|
2101
|
+
},
|
|
2102
|
+
{
|
|
2103
|
+
name: 'attendees',
|
|
2104
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/attendees',
|
|
2105
|
+
},
|
|
2106
|
+
],
|
|
1310
2107
|
},
|
|
1311
2108
|
},
|
|
1312
2109
|
],
|
|
@@ -1320,7 +2117,7 @@ describe('HashTreeParser', () => {
|
|
|
1320
2117
|
// Stub updateItems on self hash tree to return true
|
|
1321
2118
|
sinon.stub(parser.dataSets.self.hashTree, 'updateItems').returns([true]);
|
|
1322
2119
|
|
|
1323
|
-
// Send a message with
|
|
2120
|
+
// Send a message with Metadata object that has a new visibleDataSets list (adding 'new-dataset')
|
|
1324
2121
|
// but WITHOUT providing info about the new dataset in dataSets array
|
|
1325
2122
|
const message = {
|
|
1326
2123
|
dataSets: [createDataSet('self', 1, 2100)],
|
|
@@ -1330,14 +2127,31 @@ describe('HashTreeParser', () => {
|
|
|
1330
2127
|
{
|
|
1331
2128
|
htMeta: {
|
|
1332
2129
|
elementId: {
|
|
1333
|
-
type: '
|
|
1334
|
-
id:
|
|
1335
|
-
version:
|
|
2130
|
+
type: 'metadata' as const,
|
|
2131
|
+
id: 5,
|
|
2132
|
+
version: 51,
|
|
1336
2133
|
},
|
|
1337
2134
|
dataSetNames: ['self'],
|
|
1338
2135
|
},
|
|
1339
2136
|
data: {
|
|
1340
|
-
visibleDataSets: [
|
|
2137
|
+
visibleDataSets: [
|
|
2138
|
+
{
|
|
2139
|
+
name: 'main',
|
|
2140
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/main',
|
|
2141
|
+
},
|
|
2142
|
+
{
|
|
2143
|
+
name: 'self',
|
|
2144
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/713e9f99/datasets/self',
|
|
2145
|
+
},
|
|
2146
|
+
{
|
|
2147
|
+
name: 'atd-unmuted',
|
|
2148
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/atd-unmuted',
|
|
2149
|
+
},
|
|
2150
|
+
{
|
|
2151
|
+
name: 'new-dataset',
|
|
2152
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/new-dataset',
|
|
2153
|
+
},
|
|
2154
|
+
],
|
|
1341
2155
|
},
|
|
1342
2156
|
},
|
|
1343
2157
|
],
|
|
@@ -1353,40 +2167,80 @@ describe('HashTreeParser', () => {
|
|
|
1353
2167
|
locusStateElements: [],
|
|
1354
2168
|
});
|
|
1355
2169
|
|
|
1356
|
-
|
|
2170
|
+
parser.handleMessage(message, 'add new dataset requiring async init');
|
|
1357
2171
|
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
assert.isFalse(parser.visibleDataSets.includes('new-dataset'));
|
|
1361
|
-
assert.isUndefined(parser.dataSets['new-dataset']);
|
|
2172
|
+
await checkAsyncDatasetInitialization(parser, newDataSet);
|
|
2173
|
+
});
|
|
1362
2174
|
|
|
1363
|
-
|
|
1364
|
-
|
|
2175
|
+
it('emits MEETING_ENDED if async init of a new visible dataset fails with 404', async () => {
|
|
2176
|
+
const parser = createHashTreeParser();
|
|
1365
2177
|
|
|
1366
|
-
//
|
|
1367
|
-
|
|
2178
|
+
// Stub updateItems on self hash tree to return true
|
|
2179
|
+
sinon.stub(parser.dataSets.self.hashTree, 'updateItems').returns([true]);
|
|
1368
2180
|
|
|
1369
|
-
//
|
|
1370
|
-
|
|
1371
|
-
|
|
2181
|
+
// Send a message with Metadata object that adds a new visible dataset
|
|
2182
|
+
const message = {
|
|
2183
|
+
dataSets: [createDataSet('self', 1, 2100)],
|
|
2184
|
+
visibleDataSetsUrl,
|
|
2185
|
+
locusUrl,
|
|
2186
|
+
locusStateElements: [
|
|
2187
|
+
{
|
|
2188
|
+
htMeta: {
|
|
2189
|
+
elementId: {
|
|
2190
|
+
type: 'metadata' as const,
|
|
2191
|
+
id: 5,
|
|
2192
|
+
version: 51,
|
|
2193
|
+
},
|
|
2194
|
+
dataSetNames: ['self'],
|
|
2195
|
+
},
|
|
2196
|
+
data: {
|
|
2197
|
+
visibleDataSets: [
|
|
2198
|
+
{
|
|
2199
|
+
name: 'main',
|
|
2200
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/main',
|
|
2201
|
+
},
|
|
2202
|
+
{
|
|
2203
|
+
name: 'self',
|
|
2204
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/713e9f99/datasets/self',
|
|
2205
|
+
},
|
|
2206
|
+
{
|
|
2207
|
+
name: 'atd-unmuted',
|
|
2208
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/atd-unmuted',
|
|
2209
|
+
},
|
|
2210
|
+
{
|
|
2211
|
+
name: 'new-dataset',
|
|
2212
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/new-dataset',
|
|
2213
|
+
},
|
|
2214
|
+
],
|
|
2215
|
+
},
|
|
2216
|
+
},
|
|
2217
|
+
],
|
|
2218
|
+
};
|
|
1372
2219
|
|
|
1373
|
-
//
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
2220
|
+
// Mock getAllDataSetsMetadata to reject with 404
|
|
2221
|
+
const error: any = new Error('Request failed with status 404');
|
|
2222
|
+
error.statusCode = 404;
|
|
2223
|
+
webexRequest
|
|
2224
|
+
.withArgs(
|
|
2225
|
+
sinon.match({
|
|
2226
|
+
method: 'GET',
|
|
2227
|
+
uri: visibleDataSetsUrl,
|
|
2228
|
+
})
|
|
2229
|
+
)
|
|
2230
|
+
.rejects(error);
|
|
2231
|
+
|
|
2232
|
+
parser.handleMessage(message, 'add new dataset triggering 404');
|
|
2233
|
+
|
|
2234
|
+
// The first callback call is from parseMessage with the metadata update
|
|
2235
|
+
callback.resetHistory();
|
|
1381
2236
|
|
|
1382
|
-
//
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
);
|
|
2237
|
+
// Wait for the async initialization (queueMicrotask) to complete
|
|
2238
|
+
await clock.tickAsync(0);
|
|
2239
|
+
|
|
2240
|
+
// Verify callback was called with MEETING_ENDED
|
|
2241
|
+
assert.calledOnceWithExactly(callback, LocusInfoUpdateType.MEETING_ENDED, {
|
|
2242
|
+
updatedObjects: undefined,
|
|
2243
|
+
});
|
|
1390
2244
|
});
|
|
1391
2245
|
|
|
1392
2246
|
it('handles removal of visible data set', async () => {
|
|
@@ -1406,7 +2260,7 @@ describe('HashTreeParser', () => {
|
|
|
1406
2260
|
// Stub updateItems on self hash tree to return true
|
|
1407
2261
|
sinon.stub(parser.dataSets.self.hashTree, 'updateItems').returns([true]);
|
|
1408
2262
|
|
|
1409
|
-
// Send a message with
|
|
2263
|
+
// Send a message with Metadata object that has removed 'atd-unmuted' from visibleDataSets
|
|
1410
2264
|
const message = {
|
|
1411
2265
|
dataSets: [createDataSet('self', 1, 2100)],
|
|
1412
2266
|
visibleDataSetsUrl,
|
|
@@ -1415,23 +2269,32 @@ describe('HashTreeParser', () => {
|
|
|
1415
2269
|
{
|
|
1416
2270
|
htMeta: {
|
|
1417
2271
|
elementId: {
|
|
1418
|
-
type: '
|
|
1419
|
-
id:
|
|
1420
|
-
version:
|
|
2272
|
+
type: 'metadata' as const,
|
|
2273
|
+
id: 5,
|
|
2274
|
+
version: 51,
|
|
1421
2275
|
},
|
|
1422
2276
|
dataSetNames: ['self'],
|
|
1423
2277
|
},
|
|
1424
2278
|
data: {
|
|
1425
|
-
visibleDataSets: [
|
|
2279
|
+
visibleDataSets: [
|
|
2280
|
+
{
|
|
2281
|
+
name: 'main',
|
|
2282
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/main',
|
|
2283
|
+
},
|
|
2284
|
+
{
|
|
2285
|
+
name: 'self',
|
|
2286
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/713e9f99/datasets/self',
|
|
2287
|
+
},
|
|
2288
|
+
], // removed 'atd-unmuted'
|
|
1426
2289
|
},
|
|
1427
2290
|
},
|
|
1428
2291
|
],
|
|
1429
2292
|
};
|
|
1430
2293
|
|
|
1431
|
-
|
|
2294
|
+
parser.handleMessage(message, 'remove visible dataset');
|
|
1432
2295
|
|
|
1433
2296
|
// Verify that 'atd-unmuted' was removed from visibleDataSets
|
|
1434
|
-
|
|
2297
|
+
expect(parser.visibleDataSets.some((vds) => vds.name === 'atd-unmuted')).to.be.false;
|
|
1435
2298
|
|
|
1436
2299
|
// Verify that the hash tree for 'atd-unmuted' was deleted
|
|
1437
2300
|
assert.isUndefined(parser.dataSets['atd-unmuted'].hashTree);
|
|
@@ -1439,16 +2302,25 @@ describe('HashTreeParser', () => {
|
|
|
1439
2302
|
// Verify that the timer was cleared
|
|
1440
2303
|
assert.isUndefined(parser.dataSets['atd-unmuted'].timer);
|
|
1441
2304
|
|
|
1442
|
-
// Verify callback was called with
|
|
2305
|
+
// Verify callback was called with the metadata update and the removed objects (metadata appears twice - processed once for dataset changes, once in main loop)
|
|
1443
2306
|
assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED, {
|
|
1444
2307
|
updatedObjects: [
|
|
1445
2308
|
{
|
|
1446
2309
|
htMeta: {
|
|
1447
|
-
elementId: {type: '
|
|
2310
|
+
elementId: {type: 'metadata', id: 5, version: 51},
|
|
1448
2311
|
dataSetNames: ['self'],
|
|
1449
2312
|
},
|
|
1450
2313
|
data: {
|
|
1451
|
-
visibleDataSets: [
|
|
2314
|
+
visibleDataSets: [
|
|
2315
|
+
{
|
|
2316
|
+
name: 'main',
|
|
2317
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/main',
|
|
2318
|
+
},
|
|
2319
|
+
{
|
|
2320
|
+
name: 'self',
|
|
2321
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/713e9f99/datasets/self',
|
|
2322
|
+
},
|
|
2323
|
+
],
|
|
1452
2324
|
},
|
|
1453
2325
|
},
|
|
1454
2326
|
{
|
|
@@ -1460,11 +2332,20 @@ describe('HashTreeParser', () => {
|
|
|
1460
2332
|
},
|
|
1461
2333
|
{
|
|
1462
2334
|
htMeta: {
|
|
1463
|
-
elementId: {type: '
|
|
2335
|
+
elementId: {type: 'metadata', id: 5, version: 51},
|
|
1464
2336
|
dataSetNames: ['self'],
|
|
1465
2337
|
},
|
|
1466
2338
|
data: {
|
|
1467
|
-
visibleDataSets: [
|
|
2339
|
+
visibleDataSets: [
|
|
2340
|
+
{
|
|
2341
|
+
name: 'main',
|
|
2342
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/main',
|
|
2343
|
+
},
|
|
2344
|
+
{
|
|
2345
|
+
name: 'self',
|
|
2346
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/713e9f99/datasets/self',
|
|
2347
|
+
},
|
|
2348
|
+
],
|
|
1468
2349
|
},
|
|
1469
2350
|
},
|
|
1470
2351
|
],
|
|
@@ -1489,7 +2370,7 @@ describe('HashTreeParser', () => {
|
|
|
1489
2370
|
});
|
|
1490
2371
|
|
|
1491
2372
|
// Verify attendees is NOT in visibleDataSets
|
|
1492
|
-
|
|
2373
|
+
expect(parser.visibleDataSets.some((vds) => vds.name === 'attendees')).to.be.false;
|
|
1493
2374
|
|
|
1494
2375
|
// Send a message with attendees data
|
|
1495
2376
|
const message = {
|
|
@@ -1511,7 +2392,7 @@ describe('HashTreeParser', () => {
|
|
|
1511
2392
|
],
|
|
1512
2393
|
};
|
|
1513
2394
|
|
|
1514
|
-
|
|
2395
|
+
parser.handleMessage(message, 'message with non-visible dataset');
|
|
1515
2396
|
|
|
1516
2397
|
// Verify that no hash tree was created for attendees
|
|
1517
2398
|
assert.isUndefined(parser.dataSets.attendees.hashTree);
|
|
@@ -1520,5 +2401,1270 @@ describe('HashTreeParser', () => {
|
|
|
1520
2401
|
assert.notCalled(callback);
|
|
1521
2402
|
});
|
|
1522
2403
|
});
|
|
2404
|
+
|
|
2405
|
+
describe('heartbeat watchdog', () => {
|
|
2406
|
+
it('initiates sync immediately only for the specific data set whose heartbeat watchdog fires', async () => {
|
|
2407
|
+
const parser = createHashTreeParser();
|
|
2408
|
+
const heartbeatIntervalMs = 5000;
|
|
2409
|
+
|
|
2410
|
+
// Send initial heartbeat message for 'main' only
|
|
2411
|
+
const heartbeatMessage = {
|
|
2412
|
+
dataSets: [
|
|
2413
|
+
{
|
|
2414
|
+
...createDataSet('main', 16, 1100),
|
|
2415
|
+
root: parser.dataSets.main.hashTree.getRootHash(),
|
|
2416
|
+
},
|
|
2417
|
+
],
|
|
2418
|
+
visibleDataSetsUrl,
|
|
2419
|
+
locusUrl,
|
|
2420
|
+
heartbeatIntervalMs,
|
|
2421
|
+
};
|
|
2422
|
+
|
|
2423
|
+
parser.handleMessage(heartbeatMessage, 'initial heartbeat');
|
|
2424
|
+
|
|
2425
|
+
// Verify only 'main' watchdog timer is set
|
|
2426
|
+
expect(parser.dataSets.main.heartbeatWatchdogTimer).to.not.be.undefined;
|
|
2427
|
+
expect(parser.dataSets.self.heartbeatWatchdogTimer).to.be.undefined;
|
|
2428
|
+
expect(parser.dataSets['atd-unmuted'].heartbeatWatchdogTimer).to.be.undefined;
|
|
2429
|
+
|
|
2430
|
+
// Mock responses for performSync (GET hashtree then POST sync for leafCount > 1)
|
|
2431
|
+
const mainDataSetUrl = parser.dataSets.main.url;
|
|
2432
|
+
mockGetHashesFromLocusResponse(
|
|
2433
|
+
mainDataSetUrl,
|
|
2434
|
+
new Array(16).fill('00000000000000000000000000000000'),
|
|
2435
|
+
createDataSet('main', 16, 1101)
|
|
2436
|
+
);
|
|
2437
|
+
mockSendSyncRequestResponse(mainDataSetUrl, null);
|
|
2438
|
+
|
|
2439
|
+
// Advance time past heartbeatIntervalMs + backoff (Math.random returns 0, so backoff = 0)
|
|
2440
|
+
// performSync is called immediately when the watchdog fires - no additional delay
|
|
2441
|
+
await clock.tickAsync(heartbeatIntervalMs);
|
|
2442
|
+
|
|
2443
|
+
// Verify sync request was sent immediately for 'main' (GET hashtree + POST sync)
|
|
2444
|
+
assert.calledWith(
|
|
2445
|
+
webexRequest,
|
|
2446
|
+
sinon.match({
|
|
2447
|
+
method: 'GET',
|
|
2448
|
+
uri: `${mainDataSetUrl}/hashtree`,
|
|
2449
|
+
})
|
|
2450
|
+
);
|
|
2451
|
+
|
|
2452
|
+
// Verify no sync requests were sent for other datasets
|
|
2453
|
+
assert.neverCalledWith(
|
|
2454
|
+
webexRequest,
|
|
2455
|
+
sinon.match({
|
|
2456
|
+
method: 'POST',
|
|
2457
|
+
uri: `${parser.dataSets.self.url}/sync`,
|
|
2458
|
+
})
|
|
2459
|
+
);
|
|
2460
|
+
assert.neverCalledWith(
|
|
2461
|
+
webexRequest,
|
|
2462
|
+
sinon.match({
|
|
2463
|
+
method: 'GET',
|
|
2464
|
+
uri: `${parser.dataSets['atd-unmuted'].url}/hashtree`,
|
|
2465
|
+
})
|
|
2466
|
+
);
|
|
2467
|
+
});
|
|
2468
|
+
|
|
2469
|
+
it('calls POST sync directly for leafCount === 1 data sets', async () => {
|
|
2470
|
+
const parser = createHashTreeParser();
|
|
2471
|
+
const heartbeatIntervalMs = 5000;
|
|
2472
|
+
|
|
2473
|
+
// Send heartbeat for 'self' (leafCount === 1)
|
|
2474
|
+
const heartbeatMessage = {
|
|
2475
|
+
dataSets: [
|
|
2476
|
+
{
|
|
2477
|
+
...createDataSet('self', 1, 2100),
|
|
2478
|
+
url: parser.dataSets.self.url,
|
|
2479
|
+
root: parser.dataSets.self.hashTree.getRootHash(),
|
|
2480
|
+
},
|
|
2481
|
+
],
|
|
2482
|
+
visibleDataSetsUrl,
|
|
2483
|
+
locusUrl,
|
|
2484
|
+
heartbeatIntervalMs,
|
|
2485
|
+
};
|
|
2486
|
+
|
|
2487
|
+
parser.handleMessage(heartbeatMessage, 'self heartbeat');
|
|
2488
|
+
|
|
2489
|
+
// Mock sync response for self
|
|
2490
|
+
mockSendSyncRequestResponse(parser.dataSets.self.url, null);
|
|
2491
|
+
|
|
2492
|
+
// Advance time past watchdog delay
|
|
2493
|
+
await clock.tickAsync(heartbeatIntervalMs);
|
|
2494
|
+
|
|
2495
|
+
// For leafCount === 1, performSync skips GET hashtree and goes straight to POST sync
|
|
2496
|
+
assert.neverCalledWith(
|
|
2497
|
+
webexRequest,
|
|
2498
|
+
sinon.match({
|
|
2499
|
+
method: 'GET',
|
|
2500
|
+
uri: `${parser.dataSets.self.url}/hashtree`,
|
|
2501
|
+
})
|
|
2502
|
+
);
|
|
2503
|
+
assert.calledWith(
|
|
2504
|
+
webexRequest,
|
|
2505
|
+
sinon.match({
|
|
2506
|
+
method: 'POST',
|
|
2507
|
+
uri: `${parser.dataSets.self.url}/sync`,
|
|
2508
|
+
})
|
|
2509
|
+
);
|
|
2510
|
+
});
|
|
2511
|
+
|
|
2512
|
+
it('sets watchdog timers for each data set in the message', async () => {
|
|
2513
|
+
const parser = createHashTreeParser();
|
|
2514
|
+
const heartbeatIntervalMs = 5000;
|
|
2515
|
+
|
|
2516
|
+
// Send heartbeat with multiple datasets
|
|
2517
|
+
const heartbeatMessage = {
|
|
2518
|
+
dataSets: [
|
|
2519
|
+
{
|
|
2520
|
+
...createDataSet('main', 16, 1100),
|
|
2521
|
+
root: parser.dataSets.main.hashTree.getRootHash(),
|
|
2522
|
+
},
|
|
2523
|
+
{
|
|
2524
|
+
...createDataSet('self', 1, 2100),
|
|
2525
|
+
url: parser.dataSets.self.url,
|
|
2526
|
+
root: parser.dataSets.self.hashTree.getRootHash(),
|
|
2527
|
+
},
|
|
2528
|
+
],
|
|
2529
|
+
visibleDataSetsUrl,
|
|
2530
|
+
locusUrl,
|
|
2531
|
+
heartbeatIntervalMs,
|
|
2532
|
+
};
|
|
2533
|
+
|
|
2534
|
+
parser.handleMessage(heartbeatMessage, 'multi-dataset heartbeat');
|
|
2535
|
+
|
|
2536
|
+
// Watchdog timers should be set for both datasets in the message
|
|
2537
|
+
expect(parser.dataSets.main.heartbeatWatchdogTimer).to.not.be.undefined;
|
|
2538
|
+
expect(parser.dataSets.self.heartbeatWatchdogTimer).to.not.be.undefined;
|
|
2539
|
+
// But not for datasets not in the message
|
|
2540
|
+
expect(parser.dataSets['atd-unmuted'].heartbeatWatchdogTimer).to.be.undefined;
|
|
2541
|
+
});
|
|
2542
|
+
|
|
2543
|
+
it('resets the watchdog timer for a specific data set when a new heartbeat for it is received', async () => {
|
|
2544
|
+
const parser = createHashTreeParser();
|
|
2545
|
+
const heartbeatIntervalMs = 5000;
|
|
2546
|
+
|
|
2547
|
+
// Send first heartbeat for 'main'
|
|
2548
|
+
const heartbeat1 = {
|
|
2549
|
+
dataSets: [
|
|
2550
|
+
{
|
|
2551
|
+
...createDataSet('main', 16, 1100),
|
|
2552
|
+
root: parser.dataSets.main.hashTree.getRootHash(),
|
|
2553
|
+
},
|
|
2554
|
+
],
|
|
2555
|
+
visibleDataSetsUrl,
|
|
2556
|
+
locusUrl,
|
|
2557
|
+
heartbeatIntervalMs,
|
|
2558
|
+
};
|
|
2559
|
+
|
|
2560
|
+
parser.handleMessage(heartbeat1, 'first heartbeat');
|
|
2561
|
+
|
|
2562
|
+
const firstTimer = parser.dataSets.main.heartbeatWatchdogTimer;
|
|
2563
|
+
expect(firstTimer).to.not.be.undefined;
|
|
2564
|
+
|
|
2565
|
+
// Advance time to just before the watchdog would fire
|
|
2566
|
+
clock.tick(4000);
|
|
2567
|
+
|
|
2568
|
+
// Send second heartbeat for 'main' - this should reset the watchdog
|
|
2569
|
+
const heartbeat2 = {
|
|
2570
|
+
dataSets: [
|
|
2571
|
+
{
|
|
2572
|
+
...createDataSet('main', 16, 1101),
|
|
2573
|
+
root: parser.dataSets.main.hashTree.getRootHash(),
|
|
2574
|
+
},
|
|
2575
|
+
],
|
|
2576
|
+
visibleDataSetsUrl,
|
|
2577
|
+
locusUrl,
|
|
2578
|
+
heartbeatIntervalMs,
|
|
2579
|
+
};
|
|
2580
|
+
|
|
2581
|
+
parser.handleMessage(heartbeat2, 'second heartbeat');
|
|
2582
|
+
|
|
2583
|
+
const secondTimer = parser.dataSets.main.heartbeatWatchdogTimer;
|
|
2584
|
+
expect(secondTimer).to.not.be.undefined;
|
|
2585
|
+
expect(secondTimer).to.not.equal(firstTimer);
|
|
2586
|
+
|
|
2587
|
+
// Advance another 4000ms (total 8000ms from start, but only 4000ms since last heartbeat)
|
|
2588
|
+
// The watchdog should NOT fire yet
|
|
2589
|
+
await clock.tickAsync(4000);
|
|
2590
|
+
|
|
2591
|
+
// No sync requests should have been sent
|
|
2592
|
+
assert.notCalled(webexRequest);
|
|
2593
|
+
});
|
|
2594
|
+
|
|
2595
|
+
it('resets the watchdog timer when a normal message (with locusStateElements) is received', async () => {
|
|
2596
|
+
const parser = createHashTreeParser();
|
|
2597
|
+
const heartbeatIntervalMs = 5000;
|
|
2598
|
+
|
|
2599
|
+
// Send initial heartbeat to start the watchdog for 'main'
|
|
2600
|
+
const heartbeat = {
|
|
2601
|
+
dataSets: [
|
|
2602
|
+
{
|
|
2603
|
+
...createDataSet('main', 16, 1100),
|
|
2604
|
+
root: parser.dataSets.main.hashTree.getRootHash(),
|
|
2605
|
+
},
|
|
2606
|
+
],
|
|
2607
|
+
visibleDataSetsUrl,
|
|
2608
|
+
locusUrl,
|
|
2609
|
+
heartbeatIntervalMs,
|
|
2610
|
+
};
|
|
2611
|
+
|
|
2612
|
+
parser.handleMessage(heartbeat, 'initial heartbeat');
|
|
2613
|
+
|
|
2614
|
+
const firstTimer = parser.dataSets.main.heartbeatWatchdogTimer;
|
|
2615
|
+
expect(firstTimer).to.not.be.undefined;
|
|
2616
|
+
|
|
2617
|
+
// Advance time partially
|
|
2618
|
+
clock.tick(3000);
|
|
2619
|
+
|
|
2620
|
+
// Stub updateItems so the normal message is processed
|
|
2621
|
+
sinon.stub(parser.dataSets.main.hashTree, 'updateItems').returns([true]);
|
|
2622
|
+
|
|
2623
|
+
// Send a normal message (with locusStateElements) for 'main' - should also reset watchdog
|
|
2624
|
+
const normalMessage = {
|
|
2625
|
+
dataSets: [createDataSet('main', 16, 1101)],
|
|
2626
|
+
visibleDataSetsUrl,
|
|
2627
|
+
locusUrl,
|
|
2628
|
+
locusStateElements: [
|
|
2629
|
+
{
|
|
2630
|
+
htMeta: {
|
|
2631
|
+
elementId: {type: 'locus' as const, id: 0, version: 201},
|
|
2632
|
+
dataSetNames: ['main'],
|
|
2633
|
+
},
|
|
2634
|
+
data: {someData: 'value'},
|
|
2635
|
+
},
|
|
2636
|
+
],
|
|
2637
|
+
heartbeatIntervalMs,
|
|
2638
|
+
};
|
|
2639
|
+
|
|
2640
|
+
parser.handleMessage(normalMessage, 'normal message');
|
|
2641
|
+
|
|
2642
|
+
const secondTimer = parser.dataSets.main.heartbeatWatchdogTimer;
|
|
2643
|
+
expect(secondTimer).to.not.be.undefined;
|
|
2644
|
+
expect(secondTimer).to.not.equal(firstTimer);
|
|
2645
|
+
});
|
|
2646
|
+
|
|
2647
|
+
it('does not set the watchdog timer when heartbeatIntervalMs is not set', async () => {
|
|
2648
|
+
const parser = createHashTreeParser();
|
|
2649
|
+
|
|
2650
|
+
// Send a heartbeat message without heartbeatIntervalMs
|
|
2651
|
+
const heartbeatMessage = createHeartbeatMessage(
|
|
2652
|
+
'main',
|
|
2653
|
+
16,
|
|
2654
|
+
1100,
|
|
2655
|
+
parser.dataSets.main.hashTree.getRootHash()
|
|
2656
|
+
);
|
|
2657
|
+
|
|
2658
|
+
parser.handleMessage(heartbeatMessage, 'heartbeat without interval');
|
|
2659
|
+
|
|
2660
|
+
expect(parser.dataSets.main.heartbeatWatchdogTimer).to.be.undefined;
|
|
2661
|
+
});
|
|
2662
|
+
|
|
2663
|
+
it('stops all watchdog timers when meeting ends via sentinel message', async () => {
|
|
2664
|
+
const parser = createHashTreeParser();
|
|
2665
|
+
const heartbeatIntervalMs = 5000;
|
|
2666
|
+
|
|
2667
|
+
// Send heartbeat for multiple datasets
|
|
2668
|
+
const heartbeat = {
|
|
2669
|
+
dataSets: [
|
|
2670
|
+
{
|
|
2671
|
+
...createDataSet('main', 16, 1100),
|
|
2672
|
+
root: parser.dataSets.main.hashTree.getRootHash(),
|
|
2673
|
+
},
|
|
2674
|
+
{
|
|
2675
|
+
...createDataSet('self', 1, 2100),
|
|
2676
|
+
url: parser.dataSets.self.url,
|
|
2677
|
+
root: parser.dataSets.self.hashTree.getRootHash(),
|
|
2678
|
+
},
|
|
2679
|
+
],
|
|
2680
|
+
visibleDataSetsUrl,
|
|
2681
|
+
locusUrl,
|
|
2682
|
+
heartbeatIntervalMs,
|
|
2683
|
+
};
|
|
2684
|
+
|
|
2685
|
+
parser.handleMessage(heartbeat, 'initial heartbeat');
|
|
2686
|
+
|
|
2687
|
+
expect(parser.dataSets.main.heartbeatWatchdogTimer).to.not.be.undefined;
|
|
2688
|
+
expect(parser.dataSets.self.heartbeatWatchdogTimer).to.not.be.undefined;
|
|
2689
|
+
|
|
2690
|
+
// Send a sentinel END MEETING message
|
|
2691
|
+
const sentinelMessage = createHeartbeatMessage(
|
|
2692
|
+
'main',
|
|
2693
|
+
1,
|
|
2694
|
+
parser.dataSets.main.version + 1,
|
|
2695
|
+
EMPTY_HASH
|
|
2696
|
+
);
|
|
2697
|
+
|
|
2698
|
+
parser.handleMessage(sentinelMessage as any, 'sentinel message');
|
|
2699
|
+
|
|
2700
|
+
// All watchdog timers should have been stopped
|
|
2701
|
+
expect(parser.dataSets.main.heartbeatWatchdogTimer).to.be.undefined;
|
|
2702
|
+
expect(parser.dataSets.self.heartbeatWatchdogTimer).to.be.undefined;
|
|
2703
|
+
});
|
|
2704
|
+
|
|
2705
|
+
it("uses each data set's own backoff for its watchdog delay", async () => {
|
|
2706
|
+
// Create a parser where datasets have different backoff configs
|
|
2707
|
+
const initialLocus = {
|
|
2708
|
+
dataSets: [
|
|
2709
|
+
{
|
|
2710
|
+
...createDataSet('main', 16, 1000),
|
|
2711
|
+
backoff: {maxMs: 500, exponent: 2},
|
|
2712
|
+
},
|
|
2713
|
+
{
|
|
2714
|
+
...createDataSet('self', 1, 2000),
|
|
2715
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/713e9f99/datasets/self',
|
|
2716
|
+
backoff: {maxMs: 2000, exponent: 3},
|
|
2717
|
+
},
|
|
2718
|
+
],
|
|
2719
|
+
locus: {
|
|
2720
|
+
...exampleInitialLocus.locus,
|
|
2721
|
+
},
|
|
2722
|
+
};
|
|
2723
|
+
|
|
2724
|
+
const metadata = {
|
|
2725
|
+
...exampleMetadata,
|
|
2726
|
+
visibleDataSets: [
|
|
2727
|
+
{name: 'main', url: initialLocus.dataSets[0].url},
|
|
2728
|
+
{name: 'self', url: initialLocus.dataSets[1].url},
|
|
2729
|
+
],
|
|
2730
|
+
};
|
|
2731
|
+
|
|
2732
|
+
const parser = createHashTreeParser(initialLocus, metadata);
|
|
2733
|
+
const heartbeatIntervalMs = 5000;
|
|
2734
|
+
|
|
2735
|
+
// Set Math.random to return 1 so that backoff = 1^exponent * maxMs = maxMs
|
|
2736
|
+
mathRandomStub.returns(1);
|
|
2737
|
+
|
|
2738
|
+
// Send heartbeat for both datasets
|
|
2739
|
+
const heartbeat = {
|
|
2740
|
+
dataSets: [
|
|
2741
|
+
{
|
|
2742
|
+
...createDataSet('main', 16, 1100),
|
|
2743
|
+
backoff: {maxMs: 500, exponent: 2},
|
|
2744
|
+
root: parser.dataSets.main.hashTree.getRootHash(),
|
|
2745
|
+
},
|
|
2746
|
+
{
|
|
2747
|
+
...createDataSet('self', 1, 2100),
|
|
2748
|
+
url: parser.dataSets.self.url,
|
|
2749
|
+
backoff: {maxMs: 2000, exponent: 3},
|
|
2750
|
+
root: parser.dataSets.self.hashTree.getRootHash(),
|
|
2751
|
+
},
|
|
2752
|
+
],
|
|
2753
|
+
visibleDataSetsUrl,
|
|
2754
|
+
locusUrl,
|
|
2755
|
+
heartbeatIntervalMs,
|
|
2756
|
+
};
|
|
2757
|
+
|
|
2758
|
+
parser.handleMessage(heartbeat, 'heartbeat');
|
|
2759
|
+
|
|
2760
|
+
// 'main' watchdog delay = 5000 + 1^2 * 500 = 5500ms
|
|
2761
|
+
// 'self' watchdog delay = 5000 + 1^3 * 2000 = 7000ms
|
|
2762
|
+
|
|
2763
|
+
// Mock sync responses
|
|
2764
|
+
mockGetHashesFromLocusResponse(
|
|
2765
|
+
parser.dataSets.main.url,
|
|
2766
|
+
new Array(16).fill('00000000000000000000000000000000'),
|
|
2767
|
+
createDataSet('main', 16, 1101)
|
|
2768
|
+
);
|
|
2769
|
+
mockSendSyncRequestResponse(parser.dataSets.main.url, null);
|
|
2770
|
+
mockSendSyncRequestResponse(parser.dataSets.self.url, null);
|
|
2771
|
+
|
|
2772
|
+
// At 5499ms, neither watchdog should have fired
|
|
2773
|
+
await clock.tickAsync(5499);
|
|
2774
|
+
assert.notCalled(webexRequest);
|
|
2775
|
+
|
|
2776
|
+
// At 5500ms, 'main' watchdog fires and performSync runs immediately
|
|
2777
|
+
await clock.tickAsync(1);
|
|
2778
|
+
|
|
2779
|
+
// main sync should have triggered immediately (GET hashtree + POST sync)
|
|
2780
|
+
assert.calledWith(
|
|
2781
|
+
webexRequest,
|
|
2782
|
+
sinon.match({
|
|
2783
|
+
method: 'GET',
|
|
2784
|
+
uri: `${parser.dataSets.main.url}/hashtree`,
|
|
2785
|
+
})
|
|
2786
|
+
);
|
|
2787
|
+
|
|
2788
|
+
webexRequest.resetHistory();
|
|
2789
|
+
|
|
2790
|
+
// At 7000ms, 'self' watchdog fires and performSync runs immediately
|
|
2791
|
+
await clock.tickAsync(1500);
|
|
2792
|
+
|
|
2793
|
+
// self sync should have also triggered (POST sync only, leafCount === 1)
|
|
2794
|
+
assert.calledWith(
|
|
2795
|
+
webexRequest,
|
|
2796
|
+
sinon.match({
|
|
2797
|
+
method: 'POST',
|
|
2798
|
+
uri: `${parser.dataSets.self.url}/sync`,
|
|
2799
|
+
})
|
|
2800
|
+
);
|
|
2801
|
+
});
|
|
2802
|
+
|
|
2803
|
+
it('does not set watchdog for data sets without a hash tree', async () => {
|
|
2804
|
+
const parser = createHashTreeParser();
|
|
2805
|
+
const heartbeatIntervalMs = 5000;
|
|
2806
|
+
|
|
2807
|
+
// 'atd-active' is in the initial locus but is not visible (no hash tree)
|
|
2808
|
+
// Send heartbeat mentioning a non-visible dataset
|
|
2809
|
+
const heartbeatMessage = {
|
|
2810
|
+
dataSets: [
|
|
2811
|
+
{
|
|
2812
|
+
...createDataSet('main', 16, 1100),
|
|
2813
|
+
root: parser.dataSets.main.hashTree.getRootHash(),
|
|
2814
|
+
},
|
|
2815
|
+
createDataSet('atd-active', 16, 4000),
|
|
2816
|
+
],
|
|
2817
|
+
visibleDataSetsUrl,
|
|
2818
|
+
locusUrl,
|
|
2819
|
+
heartbeatIntervalMs,
|
|
2820
|
+
};
|
|
2821
|
+
|
|
2822
|
+
parser.handleMessage(heartbeatMessage, 'heartbeat with non-visible dataset');
|
|
2823
|
+
|
|
2824
|
+
// Watchdog set for main (visible) but not for atd-active (no hash tree)
|
|
2825
|
+
expect(parser.dataSets.main.heartbeatWatchdogTimer).to.not.be.undefined;
|
|
2826
|
+
expect(parser.dataSets['atd-active']?.heartbeatWatchdogTimer).to.be.undefined;
|
|
2827
|
+
});
|
|
2828
|
+
});
|
|
2829
|
+
});
|
|
2830
|
+
|
|
2831
|
+
describe('#callLocusInfoUpdateCallback filtering', () => {
|
|
2832
|
+
// Helper to setup parser with initial objects and reset callback history
|
|
2833
|
+
function setupParserWithObjects(locusStateElements: any[]) {
|
|
2834
|
+
const parser = createHashTreeParser();
|
|
2835
|
+
|
|
2836
|
+
if (locusStateElements.length > 0) {
|
|
2837
|
+
// Determine which datasets to include based on the objects' dataSetNames
|
|
2838
|
+
const dataSetNames = new Set<string>();
|
|
2839
|
+
locusStateElements.forEach((element) => {
|
|
2840
|
+
element.htMeta?.dataSetNames?.forEach((name) => dataSetNames.add(name));
|
|
2841
|
+
});
|
|
2842
|
+
|
|
2843
|
+
const dataSets = [];
|
|
2844
|
+
if (dataSetNames.has('main')) dataSets.push(createDataSet('main', 16, 1100));
|
|
2845
|
+
if (dataSetNames.has('self')) dataSets.push(createDataSet('self', 1, 2100));
|
|
2846
|
+
if (dataSetNames.has('atd-unmuted')) dataSets.push(createDataSet('atd-unmuted', 16, 3100));
|
|
2847
|
+
|
|
2848
|
+
const setupMessage = {
|
|
2849
|
+
dataSets,
|
|
2850
|
+
visibleDataSetsUrl,
|
|
2851
|
+
locusUrl,
|
|
2852
|
+
locusStateElements,
|
|
2853
|
+
};
|
|
2854
|
+
|
|
2855
|
+
parser.handleMessage(setupMessage, 'setup');
|
|
2856
|
+
}
|
|
2857
|
+
|
|
2858
|
+
callback.resetHistory();
|
|
2859
|
+
return parser;
|
|
2860
|
+
}
|
|
2861
|
+
|
|
2862
|
+
it('filters out updates when a dataset has a higher version', () => {
|
|
2863
|
+
const parser = setupParserWithObjects([
|
|
2864
|
+
{
|
|
2865
|
+
htMeta: {
|
|
2866
|
+
elementId: {type: 'locus' as const, id: 5, version: 100},
|
|
2867
|
+
dataSetNames: ['main'],
|
|
2868
|
+
},
|
|
2869
|
+
data: {existingField: 'existing'},
|
|
2870
|
+
},
|
|
2871
|
+
]);
|
|
2872
|
+
|
|
2873
|
+
// Try to update with an older version (90)
|
|
2874
|
+
const updateMessage = {
|
|
2875
|
+
dataSets: [createDataSet('main', 16, 1101)],
|
|
2876
|
+
visibleDataSetsUrl,
|
|
2877
|
+
locusUrl,
|
|
2878
|
+
locusStateElements: [
|
|
2879
|
+
{
|
|
2880
|
+
htMeta: {
|
|
2881
|
+
elementId: {type: 'locus' as const, id: 5, version: 90},
|
|
2882
|
+
dataSetNames: ['main'],
|
|
2883
|
+
},
|
|
2884
|
+
data: {someField: 'value'},
|
|
2885
|
+
},
|
|
2886
|
+
],
|
|
2887
|
+
};
|
|
2888
|
+
|
|
2889
|
+
parser.handleMessage(updateMessage, 'update with older version');
|
|
2890
|
+
|
|
2891
|
+
// Callback should not be called because the update was filtered out
|
|
2892
|
+
assert.notCalled(callback);
|
|
2893
|
+
});
|
|
2894
|
+
|
|
2895
|
+
it('allows updates when version is newer than existing', () => {
|
|
2896
|
+
const parser = setupParserWithObjects([
|
|
2897
|
+
{
|
|
2898
|
+
htMeta: {
|
|
2899
|
+
elementId: {type: 'locus' as const, id: 5, version: 100},
|
|
2900
|
+
dataSetNames: ['main'],
|
|
2901
|
+
},
|
|
2902
|
+
data: {existingField: 'existing'},
|
|
2903
|
+
},
|
|
2904
|
+
]);
|
|
2905
|
+
|
|
2906
|
+
// Try to update with a newer version (110)
|
|
2907
|
+
const updateMessage = {
|
|
2908
|
+
dataSets: [createDataSet('main', 16, 1101)],
|
|
2909
|
+
visibleDataSetsUrl,
|
|
2910
|
+
locusUrl,
|
|
2911
|
+
locusStateElements: [
|
|
2912
|
+
{
|
|
2913
|
+
htMeta: {
|
|
2914
|
+
elementId: {type: 'locus' as const, id: 5, version: 110},
|
|
2915
|
+
dataSetNames: ['main'],
|
|
2916
|
+
},
|
|
2917
|
+
data: {someField: 'new value'},
|
|
2918
|
+
},
|
|
2919
|
+
],
|
|
2920
|
+
};
|
|
2921
|
+
|
|
2922
|
+
parser.handleMessage(updateMessage, 'update with newer version');
|
|
2923
|
+
|
|
2924
|
+
// Callback should be called with the update
|
|
2925
|
+
assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED, {
|
|
2926
|
+
updatedObjects: [
|
|
2927
|
+
{
|
|
2928
|
+
htMeta: {
|
|
2929
|
+
elementId: {type: 'locus', id: 5, version: 110},
|
|
2930
|
+
dataSetNames: ['main'],
|
|
2931
|
+
},
|
|
2932
|
+
data: {someField: 'new value'},
|
|
2933
|
+
},
|
|
2934
|
+
],
|
|
2935
|
+
});
|
|
2936
|
+
});
|
|
2937
|
+
|
|
2938
|
+
it('filters out removal when object still exists in any dataset', () => {
|
|
2939
|
+
const parser = setupParserWithObjects([
|
|
2940
|
+
{
|
|
2941
|
+
htMeta: {
|
|
2942
|
+
elementId: {type: 'participant' as const, id: 10, version: 50},
|
|
2943
|
+
dataSetNames: ['main', 'atd-unmuted'],
|
|
2944
|
+
},
|
|
2945
|
+
data: {name: 'participant'},
|
|
2946
|
+
},
|
|
2947
|
+
]);
|
|
2948
|
+
|
|
2949
|
+
// Try to remove the object from main only (it still exists in atd-unmuted)
|
|
2950
|
+
const removalMessage = {
|
|
2951
|
+
dataSets: [createDataSet('main', 16, 1101)],
|
|
2952
|
+
visibleDataSetsUrl,
|
|
2953
|
+
locusUrl,
|
|
2954
|
+
locusStateElements: [
|
|
2955
|
+
{
|
|
2956
|
+
htMeta: {
|
|
2957
|
+
elementId: {type: 'participant' as const, id: 10, version: 50},
|
|
2958
|
+
dataSetNames: ['main'],
|
|
2959
|
+
},
|
|
2960
|
+
data: null, // removal
|
|
2961
|
+
},
|
|
2962
|
+
],
|
|
2963
|
+
};
|
|
2964
|
+
|
|
2965
|
+
parser.handleMessage(removalMessage, 'removal from one dataset');
|
|
2966
|
+
|
|
2967
|
+
// Callback should not be called because object still exists in atd-unmuted
|
|
2968
|
+
assert.notCalled(callback);
|
|
2969
|
+
});
|
|
2970
|
+
|
|
2971
|
+
it('allows removal when object does not exist in any dataset', () => {
|
|
2972
|
+
const parser = setupParserWithObjects([]);
|
|
2973
|
+
|
|
2974
|
+
// Stub updateItems to return true (simulating that the removal was "applied")
|
|
2975
|
+
sinon.stub(parser.dataSets.main.hashTree, 'updateItems').returns([true]);
|
|
2976
|
+
|
|
2977
|
+
// Try to remove an object that doesn't exist anywhere
|
|
2978
|
+
const removalMessage = {
|
|
2979
|
+
dataSets: [createDataSet('main', 16, 1100)],
|
|
2980
|
+
visibleDataSetsUrl,
|
|
2981
|
+
locusUrl,
|
|
2982
|
+
locusStateElements: [
|
|
2983
|
+
{
|
|
2984
|
+
htMeta: {
|
|
2985
|
+
elementId: {type: 'participant' as const, id: 99, version: 10},
|
|
2986
|
+
dataSetNames: ['main'],
|
|
2987
|
+
},
|
|
2988
|
+
data: null, // removal
|
|
2989
|
+
},
|
|
2990
|
+
],
|
|
2991
|
+
};
|
|
2992
|
+
|
|
2993
|
+
parser.handleMessage(removalMessage, 'removal of non-existent object');
|
|
2994
|
+
|
|
2995
|
+
// Callback should be called with the removal
|
|
2996
|
+
assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED, {
|
|
2997
|
+
updatedObjects: [
|
|
2998
|
+
{
|
|
2999
|
+
htMeta: {
|
|
3000
|
+
elementId: {type: 'participant', id: 99, version: 10},
|
|
3001
|
+
dataSetNames: ['main'],
|
|
3002
|
+
},
|
|
3003
|
+
data: null,
|
|
3004
|
+
},
|
|
3005
|
+
],
|
|
3006
|
+
});
|
|
3007
|
+
});
|
|
3008
|
+
|
|
3009
|
+
it('filters out removal when object exists in another dataset with newer version', () => {
|
|
3010
|
+
const parser = createHashTreeParser();
|
|
3011
|
+
|
|
3012
|
+
// Setup: Add object to main with version 40
|
|
3013
|
+
parser.handleMessage(
|
|
3014
|
+
{
|
|
3015
|
+
dataSets: [createDataSet('main', 16, 1100)],
|
|
3016
|
+
visibleDataSetsUrl,
|
|
3017
|
+
locusUrl,
|
|
3018
|
+
locusStateElements: [
|
|
3019
|
+
{
|
|
3020
|
+
htMeta: {
|
|
3021
|
+
elementId: {type: 'participant' as const, id: 10, version: 40},
|
|
3022
|
+
dataSetNames: ['main'],
|
|
3023
|
+
},
|
|
3024
|
+
data: {name: 'participant v40'},
|
|
3025
|
+
},
|
|
3026
|
+
],
|
|
3027
|
+
},
|
|
3028
|
+
'setup main'
|
|
3029
|
+
);
|
|
3030
|
+
|
|
3031
|
+
// Add object to atd-unmuted with version 50
|
|
3032
|
+
parser.handleMessage(
|
|
3033
|
+
{
|
|
3034
|
+
dataSets: [createDataSet('atd-unmuted', 16, 3100)],
|
|
3035
|
+
visibleDataSetsUrl,
|
|
3036
|
+
locusUrl,
|
|
3037
|
+
locusStateElements: [
|
|
3038
|
+
{
|
|
3039
|
+
htMeta: {
|
|
3040
|
+
elementId: {type: 'participant' as const, id: 10, version: 50},
|
|
3041
|
+
dataSetNames: ['atd-unmuted'],
|
|
3042
|
+
},
|
|
3043
|
+
data: {name: 'participant v50'},
|
|
3044
|
+
},
|
|
3045
|
+
],
|
|
3046
|
+
},
|
|
3047
|
+
'setup atd-unmuted'
|
|
3048
|
+
);
|
|
3049
|
+
callback.resetHistory();
|
|
3050
|
+
|
|
3051
|
+
// Try to remove with version 40 from main
|
|
3052
|
+
const removalMessage = {
|
|
3053
|
+
dataSets: [createDataSet('main', 16, 1101)],
|
|
3054
|
+
visibleDataSetsUrl,
|
|
3055
|
+
locusUrl,
|
|
3056
|
+
locusStateElements: [
|
|
3057
|
+
{
|
|
3058
|
+
htMeta: {
|
|
3059
|
+
elementId: {type: 'participant' as const, id: 10, version: 40},
|
|
3060
|
+
dataSetNames: ['main'],
|
|
3061
|
+
},
|
|
3062
|
+
data: null, // removal
|
|
3063
|
+
},
|
|
3064
|
+
],
|
|
3065
|
+
};
|
|
3066
|
+
|
|
3067
|
+
parser.handleMessage(removalMessage, 'removal with older version');
|
|
3068
|
+
|
|
3069
|
+
// Callback should not be called because object still exists with newer version
|
|
3070
|
+
assert.notCalled(callback);
|
|
3071
|
+
});
|
|
3072
|
+
|
|
3073
|
+
it('filters mixed updates correctly - some pass, some filtered', () => {
|
|
3074
|
+
const parser = setupParserWithObjects([
|
|
3075
|
+
{
|
|
3076
|
+
htMeta: {
|
|
3077
|
+
elementId: {type: 'participant' as const, id: 1, version: 100},
|
|
3078
|
+
dataSetNames: ['main'],
|
|
3079
|
+
},
|
|
3080
|
+
data: {name: 'participant 1'},
|
|
3081
|
+
},
|
|
3082
|
+
{
|
|
3083
|
+
htMeta: {
|
|
3084
|
+
elementId: {type: 'participant' as const, id: 2, version: 50},
|
|
3085
|
+
dataSetNames: ['atd-unmuted'],
|
|
3086
|
+
},
|
|
3087
|
+
data: {name: 'participant 2'},
|
|
3088
|
+
},
|
|
3089
|
+
]);
|
|
3090
|
+
|
|
3091
|
+
// Send mixed updates
|
|
3092
|
+
const mixedMessage = {
|
|
3093
|
+
dataSets: [createDataSet('main', 16, 1101)],
|
|
3094
|
+
visibleDataSetsUrl,
|
|
3095
|
+
locusUrl,
|
|
3096
|
+
locusStateElements: [
|
|
3097
|
+
{
|
|
3098
|
+
htMeta: {
|
|
3099
|
+
elementId: {type: 'participant' as const, id: 1, version: 110}, // newer version - should pass
|
|
3100
|
+
dataSetNames: ['main'],
|
|
3101
|
+
},
|
|
3102
|
+
data: {name: 'updated'},
|
|
3103
|
+
},
|
|
3104
|
+
{
|
|
3105
|
+
htMeta: {
|
|
3106
|
+
elementId: {type: 'participant' as const, id: 1, version: 90}, // older version - should be filtered
|
|
3107
|
+
dataSetNames: ['main'],
|
|
3108
|
+
},
|
|
3109
|
+
data: {name: 'old'},
|
|
3110
|
+
},
|
|
3111
|
+
{
|
|
3112
|
+
htMeta: {
|
|
3113
|
+
elementId: {type: 'participant' as const, id: 3, version: 10}, // new object - should pass
|
|
3114
|
+
dataSetNames: ['main'],
|
|
3115
|
+
},
|
|
3116
|
+
data: {name: 'new'},
|
|
3117
|
+
},
|
|
3118
|
+
{
|
|
3119
|
+
htMeta: {
|
|
3120
|
+
elementId: {type: 'participant' as const, id: 2, version: 50}, // removal but exists in atd-unmuted - should be filtered
|
|
3121
|
+
dataSetNames: ['main'],
|
|
3122
|
+
},
|
|
3123
|
+
data: null,
|
|
3124
|
+
},
|
|
3125
|
+
],
|
|
3126
|
+
};
|
|
3127
|
+
|
|
3128
|
+
parser.handleMessage(mixedMessage, 'mixed updates');
|
|
3129
|
+
|
|
3130
|
+
// Callback should be called with only the valid updates (participant 1 v110 and participant 3 v10)
|
|
3131
|
+
assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED, {
|
|
3132
|
+
updatedObjects: [
|
|
3133
|
+
{
|
|
3134
|
+
htMeta: {
|
|
3135
|
+
elementId: {type: 'participant', id: 1, version: 110},
|
|
3136
|
+
dataSetNames: ['main'],
|
|
3137
|
+
},
|
|
3138
|
+
data: {name: 'updated'},
|
|
3139
|
+
},
|
|
3140
|
+
{
|
|
3141
|
+
htMeta: {
|
|
3142
|
+
elementId: {type: 'participant', id: 3, version: 10},
|
|
3143
|
+
dataSetNames: ['main'],
|
|
3144
|
+
},
|
|
3145
|
+
data: {name: 'new'},
|
|
3146
|
+
},
|
|
3147
|
+
],
|
|
3148
|
+
});
|
|
3149
|
+
});
|
|
3150
|
+
|
|
3151
|
+
it('does not call callback when all updates are filtered out', () => {
|
|
3152
|
+
const parser = setupParserWithObjects([
|
|
3153
|
+
{
|
|
3154
|
+
htMeta: {
|
|
3155
|
+
elementId: {type: 'locus' as const, id: 5, version: 100},
|
|
3156
|
+
dataSetNames: ['main'],
|
|
3157
|
+
},
|
|
3158
|
+
data: {existingField: 'existing'},
|
|
3159
|
+
},
|
|
3160
|
+
]);
|
|
3161
|
+
|
|
3162
|
+
// Try to update with older versions (all should be filtered)
|
|
3163
|
+
const updateMessage = {
|
|
3164
|
+
dataSets: [createDataSet('main', 16, 1101)],
|
|
3165
|
+
visibleDataSetsUrl,
|
|
3166
|
+
locusUrl,
|
|
3167
|
+
locusStateElements: [
|
|
3168
|
+
{
|
|
3169
|
+
htMeta: {
|
|
3170
|
+
elementId: {type: 'locus' as const, id: 5, version: 80},
|
|
3171
|
+
dataSetNames: ['main'],
|
|
3172
|
+
},
|
|
3173
|
+
data: {someField: 'value'},
|
|
3174
|
+
},
|
|
3175
|
+
{
|
|
3176
|
+
htMeta: {
|
|
3177
|
+
elementId: {type: 'locus' as const, id: 5, version: 90},
|
|
3178
|
+
dataSetNames: ['main'],
|
|
3179
|
+
},
|
|
3180
|
+
data: {someField: 'another value'},
|
|
3181
|
+
},
|
|
3182
|
+
],
|
|
3183
|
+
};
|
|
3184
|
+
|
|
3185
|
+
parser.handleMessage(updateMessage, 'all filtered updates');
|
|
3186
|
+
|
|
3187
|
+
// Callback should not be called at all
|
|
3188
|
+
assert.notCalled(callback);
|
|
3189
|
+
});
|
|
3190
|
+
|
|
3191
|
+
it('checks all visible datasets when filtering', () => {
|
|
3192
|
+
const parser = createHashTreeParser();
|
|
3193
|
+
|
|
3194
|
+
// Setup: Add same object to multiple datasets with different versions
|
|
3195
|
+
parser.handleMessage(
|
|
3196
|
+
{
|
|
3197
|
+
dataSets: [createDataSet('main', 16, 1100)],
|
|
3198
|
+
visibleDataSetsUrl,
|
|
3199
|
+
locusUrl,
|
|
3200
|
+
locusStateElements: [
|
|
3201
|
+
{
|
|
3202
|
+
htMeta: {
|
|
3203
|
+
elementId: {type: 'participant' as const, id: 10, version: 100},
|
|
3204
|
+
dataSetNames: ['main'],
|
|
3205
|
+
},
|
|
3206
|
+
data: {name: 'v100'},
|
|
3207
|
+
},
|
|
3208
|
+
],
|
|
3209
|
+
},
|
|
3210
|
+
'setup main'
|
|
3211
|
+
);
|
|
3212
|
+
|
|
3213
|
+
parser.handleMessage(
|
|
3214
|
+
{
|
|
3215
|
+
dataSets: [createDataSet('self', 1, 2100)],
|
|
3216
|
+
visibleDataSetsUrl,
|
|
3217
|
+
locusUrl,
|
|
3218
|
+
locusStateElements: [
|
|
3219
|
+
{
|
|
3220
|
+
htMeta: {
|
|
3221
|
+
elementId: {type: 'participant' as const, id: 10, version: 120}, // highest
|
|
3222
|
+
dataSetNames: ['self'],
|
|
3223
|
+
},
|
|
3224
|
+
data: {name: 'v120'},
|
|
3225
|
+
},
|
|
3226
|
+
],
|
|
3227
|
+
},
|
|
3228
|
+
'setup self'
|
|
3229
|
+
);
|
|
3230
|
+
|
|
3231
|
+
parser.handleMessage(
|
|
3232
|
+
{
|
|
3233
|
+
dataSets: [createDataSet('atd-unmuted', 16, 3100)],
|
|
3234
|
+
visibleDataSetsUrl,
|
|
3235
|
+
locusUrl,
|
|
3236
|
+
locusStateElements: [
|
|
3237
|
+
{
|
|
3238
|
+
htMeta: {
|
|
3239
|
+
elementId: {type: 'participant' as const, id: 10, version: 110},
|
|
3240
|
+
dataSetNames: ['atd-unmuted'],
|
|
3241
|
+
},
|
|
3242
|
+
data: {name: 'v110'},
|
|
3243
|
+
},
|
|
3244
|
+
],
|
|
3245
|
+
},
|
|
3246
|
+
'setup atd-unmuted'
|
|
3247
|
+
);
|
|
3248
|
+
callback.resetHistory();
|
|
3249
|
+
|
|
3250
|
+
// Try to update with version 115 (newer than main and atd-unmuted, but older than self)
|
|
3251
|
+
const updateMessage = {
|
|
3252
|
+
dataSets: [createDataSet('main', 16, 1101)],
|
|
3253
|
+
visibleDataSetsUrl,
|
|
3254
|
+
locusUrl,
|
|
3255
|
+
locusStateElements: [
|
|
3256
|
+
{
|
|
3257
|
+
htMeta: {
|
|
3258
|
+
elementId: {type: 'participant' as const, id: 10, version: 115},
|
|
3259
|
+
dataSetNames: ['main'],
|
|
3260
|
+
},
|
|
3261
|
+
data: {name: 'update'},
|
|
3262
|
+
},
|
|
3263
|
+
],
|
|
3264
|
+
};
|
|
3265
|
+
|
|
3266
|
+
parser.handleMessage(updateMessage, 'update with v115');
|
|
3267
|
+
|
|
3268
|
+
// Should be filtered out because self dataset has version 120
|
|
3269
|
+
assert.notCalled(callback);
|
|
3270
|
+
});
|
|
3271
|
+
|
|
3272
|
+
it('does not call callback for empty locusStateElements', () => {
|
|
3273
|
+
const parser = setupParserWithObjects([]);
|
|
3274
|
+
|
|
3275
|
+
const emptyMessage = {
|
|
3276
|
+
dataSets: [createDataSet('main', 16, 1100)],
|
|
3277
|
+
visibleDataSetsUrl,
|
|
3278
|
+
locusUrl,
|
|
3279
|
+
locusStateElements: [],
|
|
3280
|
+
};
|
|
3281
|
+
|
|
3282
|
+
parser.handleMessage(emptyMessage, 'empty elements');
|
|
3283
|
+
|
|
3284
|
+
assert.notCalled(callback);
|
|
3285
|
+
});
|
|
3286
|
+
|
|
3287
|
+
it('always calls callback for MEETING_ENDED regardless of filtering', () => {
|
|
3288
|
+
const parser = setupParserWithObjects([
|
|
3289
|
+
{
|
|
3290
|
+
htMeta: {
|
|
3291
|
+
elementId: {type: 'locus' as const, id: 0, version: 100},
|
|
3292
|
+
dataSetNames: ['main'],
|
|
3293
|
+
},
|
|
3294
|
+
data: {info: 'data'},
|
|
3295
|
+
},
|
|
3296
|
+
]);
|
|
3297
|
+
|
|
3298
|
+
// Send a sentinel END MEETING message
|
|
3299
|
+
const sentinelMessage = createHeartbeatMessage(
|
|
3300
|
+
'main',
|
|
3301
|
+
1,
|
|
3302
|
+
parser.dataSets.main.version + 1,
|
|
3303
|
+
EMPTY_HASH
|
|
3304
|
+
);
|
|
3305
|
+
|
|
3306
|
+
parser.handleMessage(sentinelMessage as any, 'sentinel message');
|
|
3307
|
+
|
|
3308
|
+
// Callback should be called with MEETING_ENDED
|
|
3309
|
+
assert.calledOnceWithExactly(callback, LocusInfoUpdateType.MEETING_ENDED, {
|
|
3310
|
+
updatedObjects: undefined,
|
|
3311
|
+
});
|
|
3312
|
+
});
|
|
3313
|
+
});
|
|
3314
|
+
|
|
3315
|
+
describe('#state', () => {
|
|
3316
|
+
it('should be initialized to active', () => {
|
|
3317
|
+
const parser = createHashTreeParser();
|
|
3318
|
+
|
|
3319
|
+
expect(parser.state).to.equal('active');
|
|
3320
|
+
});
|
|
3321
|
+
});
|
|
3322
|
+
|
|
3323
|
+
describe('#stop', () => {
|
|
3324
|
+
it('should set state to stopped', () => {
|
|
3325
|
+
const parser = createHashTreeParser();
|
|
3326
|
+
|
|
3327
|
+
parser.stop();
|
|
3328
|
+
|
|
3329
|
+
expect(parser.state).to.equal('stopped');
|
|
3330
|
+
});
|
|
3331
|
+
|
|
3332
|
+
it('should clear all hash trees', () => {
|
|
3333
|
+
const parser = createHashTreeParser();
|
|
3334
|
+
|
|
3335
|
+
expect(parser.dataSets.main.hashTree).to.be.instanceOf(HashTree);
|
|
3336
|
+
expect(parser.dataSets.self.hashTree).to.be.instanceOf(HashTree);
|
|
3337
|
+
|
|
3338
|
+
parser.stop();
|
|
3339
|
+
|
|
3340
|
+
expect(parser.dataSets.main.hashTree).to.be.undefined;
|
|
3341
|
+
expect(parser.dataSets.self.hashTree).to.be.undefined;
|
|
3342
|
+
expect(parser.dataSets['atd-unmuted'].hashTree).to.be.undefined;
|
|
3343
|
+
});
|
|
3344
|
+
|
|
3345
|
+
it('should clear visibleDataSets', () => {
|
|
3346
|
+
const parser = createHashTreeParser();
|
|
3347
|
+
|
|
3348
|
+
expect(parser.visibleDataSets).to.have.length.greaterThan(0);
|
|
3349
|
+
|
|
3350
|
+
parser.stop();
|
|
3351
|
+
|
|
3352
|
+
expect(parser.visibleDataSets).to.deep.equal([]);
|
|
3353
|
+
});
|
|
3354
|
+
|
|
3355
|
+
it('should stop all timers', () => {
|
|
3356
|
+
const parser = createHashTreeParser();
|
|
3357
|
+
|
|
3358
|
+
// manually set timers on data sets
|
|
3359
|
+
parser.dataSets.main.timer = setTimeout(() => {}, 10000);
|
|
3360
|
+
parser.dataSets.main.heartbeatWatchdogTimer = setTimeout(() => {}, 10000);
|
|
3361
|
+
|
|
3362
|
+
parser.stop();
|
|
3363
|
+
|
|
3364
|
+
expect(parser.dataSets.main.timer).to.be.undefined;
|
|
3365
|
+
expect(parser.dataSets.main.heartbeatWatchdogTimer).to.be.undefined;
|
|
3366
|
+
});
|
|
3367
|
+
|
|
3368
|
+
it('should not call locusInfoUpdateCallback when async initialization of a new visible dataset completes after stop()', async () => {
|
|
3369
|
+
const parser = createHashTreeParser();
|
|
3370
|
+
|
|
3371
|
+
// Stub updateItems on self hash tree to return true so the metadata update is applied
|
|
3372
|
+
sinon.stub(parser.dataSets.self.hashTree, 'updateItems').returns([true]);
|
|
3373
|
+
|
|
3374
|
+
// Send a message with Metadata that adds a new visible dataset requiring async initialization
|
|
3375
|
+
// (the new dataset is NOT in parser.dataSets, so it will go through queueInitForNewVisibleDataSets)
|
|
3376
|
+
const message = {
|
|
3377
|
+
dataSets: [createDataSet('self', 1, 2100)],
|
|
3378
|
+
visibleDataSetsUrl,
|
|
3379
|
+
locusUrl,
|
|
3380
|
+
locusStateElements: [
|
|
3381
|
+
{
|
|
3382
|
+
htMeta: {
|
|
3383
|
+
elementId: {
|
|
3384
|
+
type: 'metadata' as const,
|
|
3385
|
+
id: 5,
|
|
3386
|
+
version: 51,
|
|
3387
|
+
},
|
|
3388
|
+
dataSetNames: ['self'],
|
|
3389
|
+
},
|
|
3390
|
+
data: {
|
|
3391
|
+
visibleDataSets: [
|
|
3392
|
+
{
|
|
3393
|
+
name: 'main',
|
|
3394
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/main',
|
|
3395
|
+
},
|
|
3396
|
+
{
|
|
3397
|
+
name: 'self',
|
|
3398
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/713e9f99/datasets/self',
|
|
3399
|
+
},
|
|
3400
|
+
{
|
|
3401
|
+
name: 'atd-unmuted',
|
|
3402
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/atd-unmuted',
|
|
3403
|
+
},
|
|
3404
|
+
{
|
|
3405
|
+
name: 'new-dataset',
|
|
3406
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/new-dataset',
|
|
3407
|
+
},
|
|
3408
|
+
],
|
|
3409
|
+
},
|
|
3410
|
+
},
|
|
3411
|
+
],
|
|
3412
|
+
};
|
|
3413
|
+
|
|
3414
|
+
// Mock the async initialization - getAllVisibleDataSetsFromLocus and sync request
|
|
3415
|
+
const newDataSet = createDataSet('new-dataset', 4, 5000);
|
|
3416
|
+
mockGetAllDataSetsMetadata(webexRequest, visibleDataSetsUrl, [newDataSet]);
|
|
3417
|
+
mockSyncRequest(webexRequest, newDataSet.url, {
|
|
3418
|
+
dataSets: [newDataSet],
|
|
3419
|
+
visibleDataSetsUrl,
|
|
3420
|
+
locusUrl,
|
|
3421
|
+
locusStateElements: [
|
|
3422
|
+
{
|
|
3423
|
+
htMeta: {
|
|
3424
|
+
elementId: {type: 'participant' as const, id: 20, version: 100},
|
|
3425
|
+
dataSetNames: ['new-dataset'],
|
|
3426
|
+
},
|
|
3427
|
+
data: {person: {name: 'some participant'}},
|
|
3428
|
+
},
|
|
3429
|
+
],
|
|
3430
|
+
});
|
|
3431
|
+
|
|
3432
|
+
// handleMessage triggers queueInitForNewVisibleDataSets (via queueMicrotask)
|
|
3433
|
+
parser.handleMessage(message, 'add new dataset then stop');
|
|
3434
|
+
|
|
3435
|
+
// callback is called once synchronously by handleMessage for the metadata update
|
|
3436
|
+
callback.resetHistory();
|
|
3437
|
+
|
|
3438
|
+
// Stop the parser before the async initialization completes
|
|
3439
|
+
parser.stop();
|
|
3440
|
+
|
|
3441
|
+
// Let the queued microtask and async initialization complete
|
|
3442
|
+
await clock.tickAsync(0);
|
|
3443
|
+
|
|
3444
|
+
// The callback should NOT have been called again after stop()
|
|
3445
|
+
assert.notCalled(callback);
|
|
3446
|
+
|
|
3447
|
+
// parseMessage should not have processed the sync response data,
|
|
3448
|
+
// so no hash tree should exist for new-dataset (stop() clears all hash trees)
|
|
3449
|
+
assert.isUndefined(parser.dataSets['new-dataset']?.hashTree);
|
|
3450
|
+
});
|
|
3451
|
+
|
|
3452
|
+
it('should not call locusInfoUpdateCallback when initializeFromMessage completes after stop()', async () => {
|
|
3453
|
+
const minimalInitialLocus = {
|
|
3454
|
+
dataSets: [],
|
|
3455
|
+
locus: null,
|
|
3456
|
+
};
|
|
3457
|
+
const parser = createHashTreeParser(minimalInitialLocus, null);
|
|
3458
|
+
|
|
3459
|
+
const mainDataSet = createDataSet('main', 16, 1100);
|
|
3460
|
+
|
|
3461
|
+
// Use a deferred promise so we can control when getAllVisibleDataSetsFromLocus resolves
|
|
3462
|
+
let resolveGetDataSets;
|
|
3463
|
+
webexRequest
|
|
3464
|
+
.withArgs(
|
|
3465
|
+
sinon.match({
|
|
3466
|
+
method: 'GET',
|
|
3467
|
+
uri: visibleDataSetsUrl,
|
|
3468
|
+
})
|
|
3469
|
+
)
|
|
3470
|
+
.returns(
|
|
3471
|
+
new Promise((resolve) => {
|
|
3472
|
+
resolveGetDataSets = resolve;
|
|
3473
|
+
})
|
|
3474
|
+
);
|
|
3475
|
+
|
|
3476
|
+
mockSyncRequest(webexRequest, mainDataSet.url, {
|
|
3477
|
+
dataSets: [mainDataSet],
|
|
3478
|
+
visibleDataSetsUrl,
|
|
3479
|
+
locusUrl,
|
|
3480
|
+
locusStateElements: [
|
|
3481
|
+
{
|
|
3482
|
+
htMeta: {
|
|
3483
|
+
elementId: {type: 'locus' as const, id: 1, version: 210},
|
|
3484
|
+
dataSetNames: ['main'],
|
|
3485
|
+
},
|
|
3486
|
+
data: {info: {id: 'some-locus-info'}},
|
|
3487
|
+
},
|
|
3488
|
+
],
|
|
3489
|
+
});
|
|
3490
|
+
|
|
3491
|
+
// Start initializeFromMessage but don't await it
|
|
3492
|
+
const initPromise = parser.initializeFromMessage({
|
|
3493
|
+
dataSets: [],
|
|
3494
|
+
visibleDataSetsUrl,
|
|
3495
|
+
locusUrl,
|
|
3496
|
+
});
|
|
3497
|
+
|
|
3498
|
+
// Stop the parser before the GET response arrives
|
|
3499
|
+
parser.stop();
|
|
3500
|
+
|
|
3501
|
+
// Now resolve the pending GET request
|
|
3502
|
+
resolveGetDataSets({body: {dataSets: [mainDataSet]}});
|
|
3503
|
+
|
|
3504
|
+
// Wait for the initializeFromMessage to finish
|
|
3505
|
+
await initPromise;
|
|
3506
|
+
|
|
3507
|
+
// The callback should NOT have been called because the parser was stopped
|
|
3508
|
+
assert.notCalled(callback);
|
|
3509
|
+
|
|
3510
|
+
// Even though initializeDataSets may create a hash tree entry, parseMessage
|
|
3511
|
+
// should have returned [] without processing the sync response objects.
|
|
3512
|
+
// After stop(), hash trees are cleared, so verify that main has no hash tree.
|
|
3513
|
+
assert.isUndefined(parser.dataSets.main?.hashTree);
|
|
3514
|
+
});
|
|
3515
|
+
});
|
|
3516
|
+
|
|
3517
|
+
describe('#resume', () => {
|
|
3518
|
+
const createResumeMessage = (visibleDataSets?, dataSets?) => ({
|
|
3519
|
+
locusUrl,
|
|
3520
|
+
visibleDataSetsUrl,
|
|
3521
|
+
dataSets: dataSets || [
|
|
3522
|
+
createDataSet('main', 16, 2000),
|
|
3523
|
+
createDataSet('self', 1, 3000),
|
|
3524
|
+
],
|
|
3525
|
+
locusStateElements: [
|
|
3526
|
+
{
|
|
3527
|
+
htMeta: {elementId: {type: 'metadata' as const, id: 5, version: 60}, dataSetNames: ['self']},
|
|
3528
|
+
data: {
|
|
3529
|
+
visibleDataSets: visibleDataSets || [
|
|
3530
|
+
{name: 'main', url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/main'},
|
|
3531
|
+
{name: 'self', url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/713e9f99/datasets/self'},
|
|
3532
|
+
],
|
|
3533
|
+
},
|
|
3534
|
+
},
|
|
3535
|
+
],
|
|
3536
|
+
});
|
|
3537
|
+
|
|
3538
|
+
it('should set state back to active', () => {
|
|
3539
|
+
const parser = createHashTreeParser();
|
|
3540
|
+
parser.stop();
|
|
3541
|
+
|
|
3542
|
+
expect(parser.state).to.equal('stopped');
|
|
3543
|
+
|
|
3544
|
+
parser.resume(createResumeMessage());
|
|
3545
|
+
|
|
3546
|
+
expect(parser.state).to.equal('active');
|
|
3547
|
+
});
|
|
3548
|
+
|
|
3549
|
+
it('should not resume if message is missing metadata with visibleDataSets', () => {
|
|
3550
|
+
const parser = createHashTreeParser();
|
|
3551
|
+
parser.stop();
|
|
3552
|
+
|
|
3553
|
+
parser.resume({
|
|
3554
|
+
locusUrl,
|
|
3555
|
+
visibleDataSetsUrl,
|
|
3556
|
+
dataSets: [createDataSet('main', 16, 2000)],
|
|
3557
|
+
locusStateElements: [],
|
|
3558
|
+
});
|
|
3559
|
+
|
|
3560
|
+
expect(parser.state).to.equal('stopped');
|
|
3561
|
+
});
|
|
3562
|
+
|
|
3563
|
+
it('should re-initialize dataSets from the message', () => {
|
|
3564
|
+
const parser = createHashTreeParser();
|
|
3565
|
+
parser.stop();
|
|
3566
|
+
|
|
3567
|
+
const newDataSets = [
|
|
3568
|
+
createDataSet('main', 8, 5000),
|
|
3569
|
+
createDataSet('self', 2, 6000),
|
|
3570
|
+
];
|
|
3571
|
+
|
|
3572
|
+
parser.resume(createResumeMessage(undefined, newDataSets));
|
|
3573
|
+
|
|
3574
|
+
expect(Object.keys(parser.dataSets)).to.have.lengthOf(2);
|
|
3575
|
+
expect(parser.dataSets.main.leafCount).to.equal(8);
|
|
3576
|
+
expect(parser.dataSets.main.version).to.equal(5000);
|
|
3577
|
+
expect(parser.dataSets.self.leafCount).to.equal(2);
|
|
3578
|
+
});
|
|
3579
|
+
|
|
3580
|
+
it('should create hash trees only for visible data sets', () => {
|
|
3581
|
+
const parser = createHashTreeParser();
|
|
3582
|
+
parser.stop();
|
|
3583
|
+
|
|
3584
|
+
const dataSets = [
|
|
3585
|
+
createDataSet('main', 16, 2000),
|
|
3586
|
+
createDataSet('self', 1, 3000),
|
|
3587
|
+
createDataSet('atd-unmuted', 16, 4000),
|
|
3588
|
+
];
|
|
3589
|
+
const visibleDataSets = [
|
|
3590
|
+
{name: 'main', url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/main'},
|
|
3591
|
+
{name: 'self', url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/713e9f99/datasets/self'},
|
|
3592
|
+
];
|
|
3593
|
+
|
|
3594
|
+
parser.resume(createResumeMessage(visibleDataSets, dataSets));
|
|
3595
|
+
|
|
3596
|
+
expect(parser.dataSets.main.hashTree).to.be.instanceOf(HashTree);
|
|
3597
|
+
expect(parser.dataSets.self.hashTree).to.be.instanceOf(HashTree);
|
|
3598
|
+
expect(parser.dataSets['atd-unmuted'].hashTree).to.be.undefined;
|
|
3599
|
+
});
|
|
3600
|
+
|
|
3601
|
+
it('should call handleMessage with the resume message', () => {
|
|
3602
|
+
const parser = createHashTreeParser();
|
|
3603
|
+
parser.stop();
|
|
3604
|
+
|
|
3605
|
+
const handleMessageStub = sinon.stub(parser, 'handleMessage');
|
|
3606
|
+
|
|
3607
|
+
const message = createResumeMessage();
|
|
3608
|
+
parser.resume(message);
|
|
3609
|
+
|
|
3610
|
+
assert.calledOnceWithExactly(handleMessageStub, message, 'on resume');
|
|
3611
|
+
});
|
|
3612
|
+
|
|
3613
|
+
it('should set visibleDataSets from message metadata filtered by excludedDataSets', () => {
|
|
3614
|
+
const parser = createHashTreeParser(exampleInitialLocus, exampleMetadata, ['atd-unmuted']);
|
|
3615
|
+
parser.stop();
|
|
3616
|
+
|
|
3617
|
+
const dataSets = [
|
|
3618
|
+
createDataSet('main', 16, 2000),
|
|
3619
|
+
createDataSet('self', 1, 3000),
|
|
3620
|
+
createDataSet('atd-unmuted', 16, 4000),
|
|
3621
|
+
];
|
|
3622
|
+
const visibleDataSets = [
|
|
3623
|
+
{name: 'main', url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/main'},
|
|
3624
|
+
{name: 'self', url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/713e9f99/datasets/self'},
|
|
3625
|
+
{name: 'atd-unmuted', url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/atd-unmuted'},
|
|
3626
|
+
];
|
|
3627
|
+
|
|
3628
|
+
parser.resume(createResumeMessage(visibleDataSets, dataSets));
|
|
3629
|
+
|
|
3630
|
+
expect(parser.visibleDataSets.some((vds) => vds.name === 'atd-unmuted')).to.be.false;
|
|
3631
|
+
expect(parser.visibleDataSets.some((vds) => vds.name === 'main')).to.be.true;
|
|
3632
|
+
expect(parser.visibleDataSets.some((vds) => vds.name === 'self')).to.be.true;
|
|
3633
|
+
});
|
|
3634
|
+
});
|
|
3635
|
+
|
|
3636
|
+
describe('#handleLocusUpdate when stopped', () => {
|
|
3637
|
+
it('should return early without processing when parser is stopped', () => {
|
|
3638
|
+
const parser = createHashTreeParser();
|
|
3639
|
+
parser.stop();
|
|
3640
|
+
|
|
3641
|
+
parser.handleLocusUpdate({
|
|
3642
|
+
dataSets: [createDataSet('main', 16, 2000)],
|
|
3643
|
+
locus: {participants: []},
|
|
3644
|
+
});
|
|
3645
|
+
|
|
3646
|
+
assert.notCalled(callback);
|
|
3647
|
+
});
|
|
3648
|
+
});
|
|
3649
|
+
|
|
3650
|
+
describe('#handleMessage when stopped', () => {
|
|
3651
|
+
it('should return early without processing when parser is stopped', () => {
|
|
3652
|
+
const parser = createHashTreeParser();
|
|
3653
|
+
parser.stop();
|
|
3654
|
+
|
|
3655
|
+
parser.handleMessage({
|
|
3656
|
+
dataSets: [createDataSet('main', 16, 2000)],
|
|
3657
|
+
visibleDataSetsUrl,
|
|
3658
|
+
locusUrl,
|
|
3659
|
+
locusStateElements: [
|
|
3660
|
+
{
|
|
3661
|
+
htMeta: {elementId: {type: 'self' as const, id: 4, version: 200}, dataSetNames: ['self']},
|
|
3662
|
+
data: {id: 'new-self'},
|
|
3663
|
+
},
|
|
3664
|
+
],
|
|
3665
|
+
});
|
|
3666
|
+
|
|
3667
|
+
assert.notCalled(callback);
|
|
3668
|
+
});
|
|
1523
3669
|
});
|
|
1524
3670
|
});
|