@webex/plugin-meetings 3.12.0-next.5 → 3.12.0-next.50
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/AGENTS.md +9 -0
- package/dist/aiEnableRequest/index.js +15 -2
- package/dist/aiEnableRequest/index.js.map +1 -1
- package/dist/breakouts/breakout.js +6 -2
- package/dist/breakouts/breakout.js.map +1 -1
- package/dist/breakouts/index.js +1 -1
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/constants.js +6 -3
- package/dist/constants.js.map +1 -1
- package/dist/controls-options-manager/constants.js +11 -1
- package/dist/controls-options-manager/constants.js.map +1 -1
- package/dist/controls-options-manager/index.js +38 -24
- package/dist/controls-options-manager/index.js.map +1 -1
- package/dist/controls-options-manager/util.js +91 -0
- package/dist/controls-options-manager/util.js.map +1 -1
- package/dist/hashTree/constants.js +10 -1
- package/dist/hashTree/constants.js.map +1 -1
- package/dist/hashTree/hashTreeParser.js +593 -358
- package/dist/hashTree/hashTreeParser.js.map +1 -1
- package/dist/hashTree/utils.js +22 -0
- package/dist/hashTree/utils.js.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/interceptors/locusRetry.js +23 -8
- package/dist/interceptors/locusRetry.js.map +1 -1
- package/dist/interpretation/index.js +10 -1
- package/dist/interpretation/index.js.map +1 -1
- package/dist/interpretation/siLanguage.js +1 -1
- package/dist/locus-info/controlsUtils.js +4 -1
- package/dist/locus-info/controlsUtils.js.map +1 -1
- package/dist/locus-info/index.js +277 -86
- package/dist/locus-info/index.js.map +1 -1
- package/dist/locus-info/types.js +16 -0
- package/dist/locus-info/types.js.map +1 -1
- package/dist/media/properties.js +1 -0
- package/dist/media/properties.js.map +1 -1
- package/dist/meeting/in-meeting-actions.js +3 -1
- package/dist/meeting/in-meeting-actions.js.map +1 -1
- package/dist/meeting/index.js +842 -521
- package/dist/meeting/index.js.map +1 -1
- package/dist/meeting/util.js +19 -2
- package/dist/meeting/util.js.map +1 -1
- package/dist/meetings/index.js +199 -77
- package/dist/meetings/index.js.map +1 -1
- package/dist/meetings/meetings.types.js +6 -1
- package/dist/meetings/meetings.types.js.map +1 -1
- package/dist/meetings/request.js +39 -0
- package/dist/meetings/request.js.map +1 -1
- package/dist/meetings/util.js +67 -5
- package/dist/meetings/util.js.map +1 -1
- package/dist/member/index.js +10 -0
- package/dist/member/index.js.map +1 -1
- package/dist/member/types.js.map +1 -1
- package/dist/member/util.js +3 -0
- package/dist/member/util.js.map +1 -1
- package/dist/metrics/constants.js +2 -1
- package/dist/metrics/constants.js.map +1 -1
- package/dist/recording-controller/index.js +1 -3
- package/dist/recording-controller/index.js.map +1 -1
- package/dist/types/config.d.ts +1 -0
- package/dist/types/constants.d.ts +2 -0
- package/dist/types/controls-options-manager/constants.d.ts +6 -1
- package/dist/types/controls-options-manager/index.d.ts +10 -0
- package/dist/types/hashTree/constants.d.ts +1 -0
- package/dist/types/hashTree/hashTreeParser.d.ts +61 -15
- package/dist/types/hashTree/utils.d.ts +11 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/interceptors/locusRetry.d.ts +4 -4
- package/dist/types/locus-info/index.d.ts +46 -6
- package/dist/types/locus-info/types.d.ts +17 -1
- package/dist/types/media/properties.d.ts +1 -0
- package/dist/types/meeting/in-meeting-actions.d.ts +2 -0
- package/dist/types/meeting/index.d.ts +70 -1
- package/dist/types/meeting/util.d.ts +8 -0
- package/dist/types/meetings/index.d.ts +18 -1
- package/dist/types/meetings/meetings.types.d.ts +15 -0
- package/dist/types/meetings/request.d.ts +14 -0
- package/dist/types/member/index.d.ts +1 -0
- package/dist/types/member/types.d.ts +1 -0
- package/dist/types/member/util.d.ts +1 -0
- package/dist/types/metrics/constants.d.ts +1 -0
- package/dist/webinar/index.js +361 -235
- package/dist/webinar/index.js.map +1 -1
- package/package.json +22 -22
- package/src/aiEnableRequest/index.ts +16 -0
- package/src/breakouts/breakout.ts +2 -1
- package/src/config.ts +1 -0
- package/src/constants.ts +5 -1
- package/src/controls-options-manager/constants.ts +14 -1
- package/src/controls-options-manager/index.ts +47 -24
- package/src/controls-options-manager/util.ts +81 -1
- package/src/hashTree/constants.ts +9 -0
- package/src/hashTree/hashTreeParser.ts +306 -160
- package/src/hashTree/utils.ts +17 -0
- package/src/index.ts +5 -0
- package/src/interceptors/locusRetry.ts +25 -4
- package/src/interpretation/index.ts +25 -8
- package/src/locus-info/controlsUtils.ts +3 -1
- package/src/locus-info/index.ts +276 -93
- package/src/locus-info/types.ts +19 -1
- package/src/media/properties.ts +1 -0
- package/src/meeting/in-meeting-actions.ts +4 -0
- package/src/meeting/index.ts +315 -26
- package/src/meeting/util.ts +20 -2
- package/src/meetings/index.ts +104 -43
- package/src/meetings/meetings.types.ts +19 -0
- package/src/meetings/request.ts +43 -0
- package/src/meetings/util.ts +80 -1
- package/src/member/index.ts +10 -0
- package/src/member/types.ts +1 -0
- package/src/member/util.ts +3 -0
- package/src/metrics/constants.ts +1 -0
- package/src/recording-controller/index.ts +1 -2
- package/src/webinar/index.ts +162 -21
- package/test/unit/spec/aiEnableRequest/index.ts +86 -0
- package/test/unit/spec/breakouts/breakout.ts +7 -3
- package/test/unit/spec/controls-options-manager/index.js +140 -29
- package/test/unit/spec/controls-options-manager/util.js +165 -0
- package/test/unit/spec/hashTree/hashTreeParser.ts +1294 -191
- package/test/unit/spec/hashTree/utils.ts +88 -1
- package/test/unit/spec/interceptors/locusRetry.ts +205 -4
- package/test/unit/spec/interpretation/index.ts +26 -4
- package/test/unit/spec/locus-info/controlsUtils.js +172 -57
- package/test/unit/spec/locus-info/index.js +443 -81
- package/test/unit/spec/meeting/in-meeting-actions.ts +2 -0
- package/test/unit/spec/meeting/index.js +836 -41
- package/test/unit/spec/meeting/muteState.js +3 -0
- package/test/unit/spec/meeting/utils.js +33 -0
- package/test/unit/spec/meetings/index.js +275 -10
- package/test/unit/spec/meetings/request.js +141 -0
- package/test/unit/spec/meetings/utils.js +161 -0
- package/test/unit/spec/member/index.js +7 -0
- package/test/unit/spec/member/util.js +24 -0
- package/test/unit/spec/recording-controller/index.js +9 -8
- package/test/unit/spec/webinar/index.ts +141 -16
|
@@ -7,6 +7,9 @@ import {expect} from '@webex/test-helper-chai';
|
|
|
7
7
|
import sinon from 'sinon';
|
|
8
8
|
import {assert} from '@webex/test-helper-chai';
|
|
9
9
|
import {EMPTY_HASH} from '@webex/plugin-meetings/src/hashTree/constants';
|
|
10
|
+
import { some } from 'lodash';
|
|
11
|
+
import Metrics from '@webex/plugin-meetings/src/metrics';
|
|
12
|
+
import BEHAVIORAL_METRICS from '@webex/plugin-meetings/src/metrics/constants';
|
|
10
13
|
|
|
11
14
|
const visibleDataSetsUrl = 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/visibleDataSets';
|
|
12
15
|
|
|
@@ -151,16 +154,19 @@ describe('HashTreeParser', () => {
|
|
|
151
154
|
let webexRequest: sinon.SinonStub;
|
|
152
155
|
let callback: sinon.SinonStub;
|
|
153
156
|
let mathRandomStub: sinon.SinonStub;
|
|
157
|
+
let metricsStub: sinon.SinonStub;
|
|
154
158
|
|
|
155
159
|
beforeEach(() => {
|
|
156
160
|
clock = sinon.useFakeTimers();
|
|
157
161
|
webexRequest = sinon.stub();
|
|
158
162
|
callback = sinon.stub();
|
|
159
163
|
mathRandomStub = sinon.stub(Math, 'random').returns(0);
|
|
164
|
+
metricsStub = sinon.stub(Metrics, 'sendBehavioralMetric');
|
|
160
165
|
});
|
|
161
166
|
afterEach(() => {
|
|
162
167
|
clock.restore();
|
|
163
168
|
mathRandomStub.restore();
|
|
169
|
+
metricsStub.restore();
|
|
164
170
|
});
|
|
165
171
|
|
|
166
172
|
// Helper to create a HashTreeParser instance with common defaults
|
|
@@ -553,7 +559,7 @@ describe('HashTreeParser', () => {
|
|
|
553
559
|
);
|
|
554
560
|
|
|
555
561
|
// Verify callback was called with OBJECTS_UPDATED and correct updatedObjects list
|
|
556
|
-
assert.calledWith(callback, LocusInfoUpdateType.OBJECTS_UPDATED,
|
|
562
|
+
assert.calledWith(callback, {updateType: LocusInfoUpdateType.OBJECTS_UPDATED,
|
|
557
563
|
updatedObjects: [
|
|
558
564
|
{
|
|
559
565
|
htMeta: {
|
|
@@ -566,6 +572,11 @@ describe('HashTreeParser', () => {
|
|
|
566
572
|
},
|
|
567
573
|
data: {info: {id: 'some-fake-locus-info'}},
|
|
568
574
|
},
|
|
575
|
+
],
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
assert.calledWith(callback, {updateType: LocusInfoUpdateType.OBJECTS_UPDATED,
|
|
579
|
+
updatedObjects: [
|
|
569
580
|
{
|
|
570
581
|
htMeta: {
|
|
571
582
|
elementId: {
|
|
@@ -596,6 +607,67 @@ describe('HashTreeParser', () => {
|
|
|
596
607
|
});
|
|
597
608
|
});
|
|
598
609
|
|
|
610
|
+
it('initializes "main" before "self" regardless of order from Locus', async () => {
|
|
611
|
+
const parser = createHashTreeParser({dataSets: [], locus: null}, null);
|
|
612
|
+
|
|
613
|
+
// Locus returns datasets in non-priority order: atd-active, main, self
|
|
614
|
+
const atdActiveDataSet = createDataSet('atd-active', 4, 500);
|
|
615
|
+
const mainDataSet = createDataSet('main', 16, 1100);
|
|
616
|
+
const selfDataSet = createDataSet('self', 1, 2100);
|
|
617
|
+
|
|
618
|
+
mockGetAllDataSetsMetadata(webexRequest, visibleDataSetsUrl, [
|
|
619
|
+
atdActiveDataSet,
|
|
620
|
+
mainDataSet,
|
|
621
|
+
selfDataSet,
|
|
622
|
+
]);
|
|
623
|
+
|
|
624
|
+
mockSyncRequest(webexRequest, selfDataSet.url);
|
|
625
|
+
mockSyncRequest(webexRequest, mainDataSet.url);
|
|
626
|
+
mockSyncRequest(webexRequest, atdActiveDataSet.url);
|
|
627
|
+
|
|
628
|
+
await parser.initializeFromMessage({
|
|
629
|
+
dataSets: [],
|
|
630
|
+
visibleDataSetsUrl,
|
|
631
|
+
locusUrl,
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
// Verify sync requests were sent in priority order: main, self, then atd-active
|
|
635
|
+
const syncCalls = webexRequest
|
|
636
|
+
.getCalls()
|
|
637
|
+
.filter((call) => call.args[0]?.method === 'POST' && call.args[0]?.uri?.endsWith('/sync'));
|
|
638
|
+
|
|
639
|
+
expect(syncCalls).to.have.lengthOf(3);
|
|
640
|
+
expect(syncCalls[0].args[0].uri).to.equal(`${mainDataSet.url}/sync`);
|
|
641
|
+
expect(syncCalls[1].args[0].uri).to.equal(`${selfDataSet.url}/sync`);
|
|
642
|
+
expect(syncCalls[2].args[0].uri).to.equal(`${atdActiveDataSet.url}/sync`);
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
it('sends leafCount=1 with a single empty leaf for initialization sync, regardless of actual dataset leafCount', async () => {
|
|
646
|
+
const parser = createHashTreeParser({dataSets: [], locus: null}, null);
|
|
647
|
+
|
|
648
|
+
// Use a dataset with leafCount=16 to verify the initialization sync always uses leafCount=1
|
|
649
|
+
const mainDataSet = createDataSet('main', 16, 1100);
|
|
650
|
+
|
|
651
|
+
mockGetAllDataSetsMetadata(webexRequest, visibleDataSetsUrl, [mainDataSet]);
|
|
652
|
+
mockSyncRequest(webexRequest, mainDataSet.url);
|
|
653
|
+
|
|
654
|
+
await parser.initializeFromMessage({
|
|
655
|
+
dataSets: [],
|
|
656
|
+
visibleDataSetsUrl,
|
|
657
|
+
locusUrl,
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
assert.calledWith(webexRequest, {
|
|
661
|
+
method: 'POST',
|
|
662
|
+
uri: `${mainDataSet.url}/sync`,
|
|
663
|
+
qs: {rootHash: sinon.match.string},
|
|
664
|
+
body: {
|
|
665
|
+
leafCount: 1,
|
|
666
|
+
leafDataEntries: [{leafIndex: 0, elementIds: []}],
|
|
667
|
+
},
|
|
668
|
+
});
|
|
669
|
+
});
|
|
670
|
+
|
|
599
671
|
it('handles sync response that has locusStateElements undefined', async () => {
|
|
600
672
|
const minimalInitialLocus = {
|
|
601
673
|
dataSets: [],
|
|
@@ -788,7 +860,7 @@ describe('HashTreeParser', () => {
|
|
|
788
860
|
expect(parser.dataSets.self.version).to.equal(2100);
|
|
789
861
|
expect(parser.dataSets['atd-unmuted'].version).to.equal(3100);
|
|
790
862
|
|
|
791
|
-
assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED,
|
|
863
|
+
assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.OBJECTS_UPDATED,
|
|
792
864
|
updatedObjects: [
|
|
793
865
|
{
|
|
794
866
|
htMeta: {
|
|
@@ -861,6 +933,116 @@ describe('HashTreeParser', () => {
|
|
|
861
933
|
});
|
|
862
934
|
});
|
|
863
935
|
|
|
936
|
+
it('handles updates to control entries correctly', () => {
|
|
937
|
+
const parser = createHashTreeParser();
|
|
938
|
+
|
|
939
|
+
const mainPutItemsSpy = sinon.spy(parser.dataSets.main.hashTree, 'putItems');
|
|
940
|
+
|
|
941
|
+
// Create a locus update with new htMeta information for some things
|
|
942
|
+
const locusUpdate = {
|
|
943
|
+
dataSets: [
|
|
944
|
+
createDataSet('main', 16, 1100),
|
|
945
|
+
],
|
|
946
|
+
locus: {
|
|
947
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f',
|
|
948
|
+
htMeta: {
|
|
949
|
+
elementId: {
|
|
950
|
+
type: 'locus',
|
|
951
|
+
id: 0,
|
|
952
|
+
version: 200, // same version
|
|
953
|
+
},
|
|
954
|
+
dataSetNames: ['main'],
|
|
955
|
+
},
|
|
956
|
+
participants: [],
|
|
957
|
+
controls: {
|
|
958
|
+
lock: {
|
|
959
|
+
locked: true,
|
|
960
|
+
htMeta: {
|
|
961
|
+
elementId: {
|
|
962
|
+
type: 'ControlEntry',
|
|
963
|
+
id: 10100,
|
|
964
|
+
version: 100,
|
|
965
|
+
},
|
|
966
|
+
dataSetNames: ['main'],
|
|
967
|
+
},
|
|
968
|
+
},
|
|
969
|
+
stream: {
|
|
970
|
+
streaming: true,
|
|
971
|
+
htMeta: {
|
|
972
|
+
elementId: {
|
|
973
|
+
type: 'ControlEntry',
|
|
974
|
+
id: 10101,
|
|
975
|
+
version: 100,
|
|
976
|
+
},
|
|
977
|
+
dataSetNames: ['main'],
|
|
978
|
+
},
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
},
|
|
982
|
+
};
|
|
983
|
+
|
|
984
|
+
// Call handleLocusUpdate
|
|
985
|
+
parser.handleLocusUpdate(locusUpdate);
|
|
986
|
+
|
|
987
|
+
// Verify putItems was called on main hash tree with correct data
|
|
988
|
+
assert.calledOnceWithExactly(mainPutItemsSpy, [
|
|
989
|
+
{type: 'locus', id: 0, version: 200},
|
|
990
|
+
{type: 'ControlEntry', id: 10100, version: 100},
|
|
991
|
+
{type: 'ControlEntry', id: 10101, version: 100}
|
|
992
|
+
]);
|
|
993
|
+
|
|
994
|
+
assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.OBJECTS_UPDATED,
|
|
995
|
+
updatedObjects: [
|
|
996
|
+
{
|
|
997
|
+
htMeta: {
|
|
998
|
+
elementId: {
|
|
999
|
+
type: 'ControlEntry',
|
|
1000
|
+
id: 10100,
|
|
1001
|
+
version: 100,
|
|
1002
|
+
},
|
|
1003
|
+
dataSetNames: ['main'],
|
|
1004
|
+
},
|
|
1005
|
+
data: {
|
|
1006
|
+
lock: {
|
|
1007
|
+
locked: true,
|
|
1008
|
+
htMeta: {
|
|
1009
|
+
elementId: {
|
|
1010
|
+
type: 'ControlEntry',
|
|
1011
|
+
id: 10100,
|
|
1012
|
+
version: 100,
|
|
1013
|
+
},
|
|
1014
|
+
dataSetNames: ['main'],
|
|
1015
|
+
},
|
|
1016
|
+
},
|
|
1017
|
+
},
|
|
1018
|
+
},
|
|
1019
|
+
{
|
|
1020
|
+
htMeta: {
|
|
1021
|
+
elementId: {
|
|
1022
|
+
type: 'ControlEntry',
|
|
1023
|
+
id: 10101,
|
|
1024
|
+
version: 100,
|
|
1025
|
+
},
|
|
1026
|
+
dataSetNames: ['main'],
|
|
1027
|
+
},
|
|
1028
|
+
data: {
|
|
1029
|
+
stream: {
|
|
1030
|
+
streaming: true,
|
|
1031
|
+
htMeta: {
|
|
1032
|
+
elementId: {
|
|
1033
|
+
type: 'ControlEntry',
|
|
1034
|
+
id: 10101,
|
|
1035
|
+
version: 100,
|
|
1036
|
+
},
|
|
1037
|
+
dataSetNames: ['main'],
|
|
1038
|
+
},
|
|
1039
|
+
},
|
|
1040
|
+
},
|
|
1041
|
+
}
|
|
1042
|
+
],
|
|
1043
|
+
});
|
|
1044
|
+
});
|
|
1045
|
+
|
|
864
1046
|
it('handles unknown datasets gracefully', () => {
|
|
865
1047
|
const parser = createHashTreeParser();
|
|
866
1048
|
|
|
@@ -899,7 +1081,7 @@ describe('HashTreeParser', () => {
|
|
|
899
1081
|
assert.calledOnceWithExactly(mainPutItemsSpy, [{type: 'locus', id: 0, version: 201}]);
|
|
900
1082
|
|
|
901
1083
|
// Verify callback was called only for known dataset
|
|
902
|
-
assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED,
|
|
1084
|
+
assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.OBJECTS_UPDATED,
|
|
903
1085
|
updatedObjects: [
|
|
904
1086
|
{
|
|
905
1087
|
htMeta: {
|
|
@@ -999,7 +1181,7 @@ describe('HashTreeParser', () => {
|
|
|
999
1181
|
assert.calledOnceWithExactly(selfPutItemSpy, {type: 'metadata', id: 5, version: 51});
|
|
1000
1182
|
|
|
1001
1183
|
// Verify callback was called with metadata object and removed dataset objects
|
|
1002
|
-
assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED,
|
|
1184
|
+
assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.OBJECTS_UPDATED,
|
|
1003
1185
|
updatedObjects: [
|
|
1004
1186
|
// updated metadata object:
|
|
1005
1187
|
{
|
|
@@ -1160,7 +1342,7 @@ describe('HashTreeParser', () => {
|
|
|
1160
1342
|
assert.notCalled(atdUnmutedPutItemsSpy);
|
|
1161
1343
|
|
|
1162
1344
|
// Verify callback was called with the updated object
|
|
1163
|
-
assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED,
|
|
1345
|
+
assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.OBJECTS_UPDATED,
|
|
1164
1346
|
updatedObjects: [
|
|
1165
1347
|
{
|
|
1166
1348
|
htMeta: {
|
|
@@ -1388,7 +1570,7 @@ describe('HashTreeParser', () => {
|
|
|
1388
1570
|
]);
|
|
1389
1571
|
|
|
1390
1572
|
// Verify callback was called with OBJECTS_UPDATED and all updated objects
|
|
1391
|
-
assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED,
|
|
1573
|
+
assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.OBJECTS_UPDATED,
|
|
1392
1574
|
updatedObjects: [
|
|
1393
1575
|
{
|
|
1394
1576
|
htMeta: {
|
|
@@ -1453,9 +1635,7 @@ describe('HashTreeParser', () => {
|
|
|
1453
1635
|
parser.handleMessage(sentinelMessage, 'sentinel message');
|
|
1454
1636
|
|
|
1455
1637
|
// Verify callback was called with MEETING_ENDED
|
|
1456
|
-
assert.calledOnceWithExactly(callback, LocusInfoUpdateType.MEETING_ENDED
|
|
1457
|
-
updatedObjects: undefined,
|
|
1458
|
-
});
|
|
1638
|
+
assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.MEETING_ENDED});
|
|
1459
1639
|
|
|
1460
1640
|
// Verify that all timers were stopped
|
|
1461
1641
|
Object.values(parser.dataSets).forEach((ds: any) => {
|
|
@@ -1477,9 +1657,7 @@ describe('HashTreeParser', () => {
|
|
|
1477
1657
|
parser.handleMessage(sentinelMessage, 'sentinel message');
|
|
1478
1658
|
|
|
1479
1659
|
// Verify callback was called with MEETING_ENDED
|
|
1480
|
-
assert.calledOnceWithExactly(callback, LocusInfoUpdateType.MEETING_ENDED
|
|
1481
|
-
updatedObjects: undefined,
|
|
1482
|
-
});
|
|
1660
|
+
assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.MEETING_ENDED});
|
|
1483
1661
|
|
|
1484
1662
|
// Verify that all timers were stopped
|
|
1485
1663
|
Object.values(parser.dataSets).forEach((ds: any) => {
|
|
@@ -1575,7 +1753,7 @@ describe('HashTreeParser', () => {
|
|
|
1575
1753
|
);
|
|
1576
1754
|
|
|
1577
1755
|
// Verify that callback was called with synced objects
|
|
1578
|
-
assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED,
|
|
1756
|
+
assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.OBJECTS_UPDATED,
|
|
1579
1757
|
updatedObjects: [
|
|
1580
1758
|
{
|
|
1581
1759
|
htMeta: {
|
|
@@ -1637,15 +1815,16 @@ describe('HashTreeParser', () => {
|
|
|
1637
1815
|
await clock.tickAsync(1000);
|
|
1638
1816
|
|
|
1639
1817
|
// Verify callback was called with MEETING_ENDED
|
|
1640
|
-
assert.calledOnceWithExactly(callback, LocusInfoUpdateType.MEETING_ENDED
|
|
1641
|
-
updatedObjects: undefined,
|
|
1642
|
-
});
|
|
1818
|
+
assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.MEETING_ENDED});
|
|
1643
1819
|
|
|
1644
1820
|
// Verify all timers are stopped
|
|
1645
1821
|
Object.values(parser.dataSets).forEach((ds: any) => {
|
|
1646
1822
|
assert.isUndefined(ds.timer);
|
|
1647
1823
|
assert.isUndefined(ds.heartbeatWatchdogTimer);
|
|
1648
1824
|
});
|
|
1825
|
+
|
|
1826
|
+
// Verify no sync failure metric was sent for end-meeting sentinel
|
|
1827
|
+
assert.notCalled(metricsStub);
|
|
1649
1828
|
});
|
|
1650
1829
|
|
|
1651
1830
|
it(`when /sync returns ${statusCode}`, async () => {
|
|
@@ -1702,15 +1881,16 @@ describe('HashTreeParser', () => {
|
|
|
1702
1881
|
await clock.tickAsync(1000);
|
|
1703
1882
|
|
|
1704
1883
|
// Verify callback was called with MEETING_ENDED
|
|
1705
|
-
assert.calledOnceWithExactly(callback, LocusInfoUpdateType.MEETING_ENDED
|
|
1706
|
-
updatedObjects: undefined,
|
|
1707
|
-
});
|
|
1884
|
+
assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.MEETING_ENDED});
|
|
1708
1885
|
|
|
1709
1886
|
// Verify all timers are stopped
|
|
1710
1887
|
Object.values(parser.dataSets).forEach((ds: any) => {
|
|
1711
1888
|
assert.isUndefined(ds.timer);
|
|
1712
1889
|
assert.isUndefined(ds.heartbeatWatchdogTimer);
|
|
1713
1890
|
});
|
|
1891
|
+
|
|
1892
|
+
// Verify no sync failure metric was sent for end-meeting sentinel
|
|
1893
|
+
assert.notCalled(metricsStub);
|
|
1714
1894
|
});
|
|
1715
1895
|
});
|
|
1716
1896
|
});
|
|
@@ -1883,144 +2063,364 @@ describe('HashTreeParser', () => {
|
|
|
1883
2063
|
},
|
|
1884
2064
|
});
|
|
1885
2065
|
});
|
|
1886
|
-
});
|
|
1887
2066
|
|
|
1888
|
-
|
|
1889
|
-
it('handles addition of visible data set (one that does not require async initialization)', async () => {
|
|
1890
|
-
// Create a parser with visible datasets
|
|
2067
|
+
it('restarts the sync timer when sync response is empty so that a future sync can be triggered', async () => {
|
|
1891
2068
|
const parser = createHashTreeParser();
|
|
1892
2069
|
|
|
1893
|
-
//
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
// Send a message with Metadata object that has a new visibleDataSets list
|
|
1897
|
-
const message = {
|
|
1898
|
-
dataSets: [createDataSet('self', 1, 2100), createDataSet('attendees', 8, 4000)],
|
|
1899
|
-
visibleDataSetsUrl,
|
|
1900
|
-
locusUrl,
|
|
1901
|
-
locusStateElements: [
|
|
2070
|
+
// Send a heartbeat with a mismatched root hash to trigger runSyncAlgorithm
|
|
2071
|
+
const heartbeatMessage = {
|
|
2072
|
+
dataSets: [
|
|
1902
2073
|
{
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
type: 'metadata' as const,
|
|
1906
|
-
id: 5,
|
|
1907
|
-
version: 51,
|
|
1908
|
-
},
|
|
1909
|
-
dataSetNames: ['self'],
|
|
1910
|
-
},
|
|
1911
|
-
data: {
|
|
1912
|
-
visibleDataSets: [
|
|
1913
|
-
{
|
|
1914
|
-
name: 'main',
|
|
1915
|
-
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/main',
|
|
1916
|
-
},
|
|
1917
|
-
{
|
|
1918
|
-
name: 'self',
|
|
1919
|
-
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/713e9f99/datasets/self',
|
|
1920
|
-
},
|
|
1921
|
-
{
|
|
1922
|
-
name: 'atd-unmuted',
|
|
1923
|
-
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/atd-unmuted',
|
|
1924
|
-
},
|
|
1925
|
-
{
|
|
1926
|
-
name: 'attendees',
|
|
1927
|
-
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/attendees',
|
|
1928
|
-
},
|
|
1929
|
-
], // added 'attendees'
|
|
1930
|
-
},
|
|
2074
|
+
...createDataSet('main', 16, 1100),
|
|
2075
|
+
root: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1', // different from ours
|
|
1931
2076
|
},
|
|
1932
2077
|
],
|
|
2078
|
+
visibleDataSetsUrl,
|
|
2079
|
+
locusUrl,
|
|
1933
2080
|
};
|
|
1934
2081
|
|
|
1935
|
-
parser.handleMessage(
|
|
2082
|
+
parser.handleMessage(heartbeatMessage, 'heartbeat with mismatch');
|
|
1936
2083
|
|
|
1937
|
-
//
|
|
1938
|
-
expect(parser.
|
|
2084
|
+
// The sync timer should be set
|
|
2085
|
+
expect(parser.dataSets.main.timer).to.not.be.undefined;
|
|
1939
2086
|
|
|
1940
|
-
//
|
|
1941
|
-
|
|
1942
|
-
|
|
2087
|
+
// Mock responses for the first sync - return null (204/empty body)
|
|
2088
|
+
const mainDataSetUrl = parser.dataSets.main.url;
|
|
2089
|
+
mockGetHashesFromLocusResponse(
|
|
2090
|
+
mainDataSetUrl,
|
|
2091
|
+
new Array(16).fill('00000000000000000000000000000000'),
|
|
2092
|
+
{
|
|
2093
|
+
...createDataSet('main', 16, 1101),
|
|
2094
|
+
root: 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', // still mismatched
|
|
2095
|
+
}
|
|
2096
|
+
);
|
|
2097
|
+
mockSendSyncRequestResponse(mainDataSetUrl, null);
|
|
1943
2098
|
|
|
1944
|
-
//
|
|
1945
|
-
|
|
1946
|
-
updatedObjects: [
|
|
1947
|
-
{
|
|
1948
|
-
htMeta: {
|
|
1949
|
-
elementId: {type: 'metadata', id: 5, version: 51},
|
|
1950
|
-
dataSetNames: ['self'],
|
|
1951
|
-
},
|
|
1952
|
-
data: {
|
|
1953
|
-
visibleDataSets: [
|
|
1954
|
-
{
|
|
1955
|
-
name: 'main',
|
|
1956
|
-
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/main',
|
|
1957
|
-
},
|
|
1958
|
-
{
|
|
1959
|
-
name: 'self',
|
|
1960
|
-
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/713e9f99/datasets/self',
|
|
1961
|
-
},
|
|
1962
|
-
{
|
|
1963
|
-
name: 'atd-unmuted',
|
|
1964
|
-
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/atd-unmuted',
|
|
1965
|
-
},
|
|
1966
|
-
{
|
|
1967
|
-
name: 'attendees',
|
|
1968
|
-
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/attendees',
|
|
1969
|
-
},
|
|
1970
|
-
],
|
|
1971
|
-
},
|
|
1972
|
-
},
|
|
1973
|
-
{
|
|
1974
|
-
htMeta: {
|
|
1975
|
-
elementId: {type: 'metadata', id: 5, version: 51},
|
|
1976
|
-
dataSetNames: ['self'],
|
|
1977
|
-
},
|
|
1978
|
-
data: {
|
|
1979
|
-
visibleDataSets: [
|
|
1980
|
-
{
|
|
1981
|
-
name: 'main',
|
|
1982
|
-
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/main',
|
|
1983
|
-
},
|
|
1984
|
-
{
|
|
1985
|
-
name: 'self',
|
|
1986
|
-
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/713e9f99/datasets/self',
|
|
1987
|
-
},
|
|
1988
|
-
{
|
|
1989
|
-
name: 'atd-unmuted',
|
|
1990
|
-
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/atd-unmuted',
|
|
1991
|
-
},
|
|
1992
|
-
{
|
|
1993
|
-
name: 'attendees',
|
|
1994
|
-
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/attendees',
|
|
1995
|
-
},
|
|
1996
|
-
],
|
|
1997
|
-
},
|
|
1998
|
-
},
|
|
1999
|
-
],
|
|
2000
|
-
});
|
|
2001
|
-
});
|
|
2099
|
+
// Advance time to fire the sync timer (idleMs=1000 + backoff=0)
|
|
2100
|
+
await clock.tickAsync(1000);
|
|
2002
2101
|
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2102
|
+
// Verify sync was triggered
|
|
2103
|
+
assert.calledWith(
|
|
2104
|
+
webexRequest,
|
|
2105
|
+
sinon.match({
|
|
2106
|
+
method: 'POST',
|
|
2107
|
+
uri: `${mainDataSetUrl}/sync`,
|
|
2108
|
+
})
|
|
2109
|
+
);
|
|
2006
2110
|
|
|
2007
|
-
//
|
|
2008
|
-
|
|
2111
|
+
// After empty response, runSyncAlgorithm should have been called,
|
|
2112
|
+
// setting a new sync timer as a safety net
|
|
2113
|
+
expect(parser.dataSets.main.timer).to.not.be.undefined;
|
|
2009
2114
|
|
|
2010
|
-
//
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2115
|
+
// Reset and set up mocks for the second sync
|
|
2116
|
+
webexRequest.resetHistory();
|
|
2117
|
+
mockGetHashesFromLocusResponse(
|
|
2118
|
+
mainDataSetUrl,
|
|
2119
|
+
new Array(16).fill('00000000000000000000000000000000'),
|
|
2120
|
+
{
|
|
2121
|
+
...createDataSet('main', 16, 1102),
|
|
2122
|
+
root: 'cccccccccccccccccccccccccccccccc', // still mismatched
|
|
2123
|
+
}
|
|
2124
|
+
);
|
|
2125
|
+
mockSendSyncRequestResponse(mainDataSetUrl, null);
|
|
2126
|
+
|
|
2127
|
+
// Advance time again to fire the second sync timer
|
|
2128
|
+
await clock.tickAsync(1000);
|
|
2129
|
+
|
|
2130
|
+
// Verify a second sync was triggered
|
|
2131
|
+
assert.calledWith(
|
|
2132
|
+
webexRequest,
|
|
2133
|
+
sinon.match({
|
|
2134
|
+
method: 'POST',
|
|
2135
|
+
uri: `${mainDataSetUrl}/sync`,
|
|
2136
|
+
})
|
|
2137
|
+
);
|
|
2138
|
+
});
|
|
2139
|
+
|
|
2140
|
+
it('updates dataSet.leafCount when hash tree is resized during sync so that the sync request has the correct leafCount', async () => {
|
|
2141
|
+
const parser = createHashTreeParser();
|
|
2142
|
+
|
|
2143
|
+
// Send a heartbeat with a mismatched root hash to trigger runSyncAlgorithm
|
|
2144
|
+
const heartbeatMessage = {
|
|
2145
|
+
dataSets: [
|
|
2146
|
+
{
|
|
2147
|
+
...createDataSet('main', 16, 1100),
|
|
2148
|
+
root: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1', // different from ours
|
|
2149
|
+
},
|
|
2150
|
+
],
|
|
2151
|
+
visibleDataSetsUrl,
|
|
2152
|
+
locusUrl,
|
|
2153
|
+
};
|
|
2154
|
+
|
|
2155
|
+
parser.handleMessage(heartbeatMessage, 'heartbeat with mismatch');
|
|
2156
|
+
|
|
2157
|
+
// The sync timer should be set
|
|
2158
|
+
expect(parser.dataSets.main.timer).to.not.be.undefined;
|
|
2159
|
+
|
|
2160
|
+
const mainDataSetUrl = parser.dataSets.main.url;
|
|
2161
|
+
const newLeafCount = 32;
|
|
2162
|
+
|
|
2163
|
+
// Mock getHashesFromLocus response with a DIFFERENT leafCount (32 instead of 16)
|
|
2164
|
+
mockGetHashesFromLocusResponse(
|
|
2165
|
+
mainDataSetUrl,
|
|
2166
|
+
new Array(newLeafCount).fill('00000000000000000000000000000000'),
|
|
2167
|
+
createDataSet('main', newLeafCount, 1101)
|
|
2168
|
+
);
|
|
2169
|
+
|
|
2170
|
+
// Mock the sync request - use matching root hash
|
|
2171
|
+
const syncResponseDataSet = createDataSet('main', newLeafCount, 1102);
|
|
2172
|
+
syncResponseDataSet.root = parser.dataSets.main.hashTree.getRootHash();
|
|
2173
|
+
mockSendSyncRequestResponse(mainDataSetUrl, {
|
|
2174
|
+
dataSets: [syncResponseDataSet],
|
|
2175
|
+
visibleDataSetsUrl,
|
|
2176
|
+
locusUrl,
|
|
2177
|
+
locusStateElements: [],
|
|
2178
|
+
});
|
|
2179
|
+
|
|
2180
|
+
// Advance time to fire the sync timer (idleMs=1000 + backoff=0)
|
|
2181
|
+
await clock.tickAsync(1000);
|
|
2182
|
+
|
|
2183
|
+
// Verify the sync request was sent with the NEW leafCount (32), not the old one (16)
|
|
2184
|
+
assert.calledWith(
|
|
2185
|
+
webexRequest,
|
|
2186
|
+
sinon.match({
|
|
2187
|
+
method: 'POST',
|
|
2188
|
+
uri: `${mainDataSetUrl}/sync`,
|
|
2189
|
+
body: sinon.match({
|
|
2190
|
+
leafCount: newLeafCount,
|
|
2191
|
+
}),
|
|
2192
|
+
})
|
|
2193
|
+
);
|
|
2194
|
+
});
|
|
2195
|
+
|
|
2196
|
+
it('sends HASH_TREE_SYNC_FAILURE metric when GET /hashtree request fails', async () => {
|
|
2197
|
+
const parser = createHashTreeParser();
|
|
2198
|
+
|
|
2199
|
+
// Send a heartbeat with a mismatched root hash to trigger runSyncAlgorithm
|
|
2200
|
+
const heartbeatMessage = {
|
|
2201
|
+
dataSets: [
|
|
2202
|
+
{
|
|
2203
|
+
...createDataSet('main', 16, 1100),
|
|
2204
|
+
root: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1',
|
|
2205
|
+
},
|
|
2206
|
+
],
|
|
2207
|
+
visibleDataSetsUrl,
|
|
2208
|
+
locusUrl,
|
|
2209
|
+
};
|
|
2210
|
+
|
|
2211
|
+
parser.handleMessage(heartbeatMessage, 'heartbeat with mismatch');
|
|
2212
|
+
|
|
2213
|
+
const mainDataSetUrl = parser.dataSets.main.url;
|
|
2214
|
+
const hashTreeError = new Error('server error') as any;
|
|
2215
|
+
hashTreeError.statusCode = 500;
|
|
2216
|
+
|
|
2217
|
+
webexRequest
|
|
2218
|
+
.withArgs(
|
|
2219
|
+
sinon.match({
|
|
2220
|
+
method: 'GET',
|
|
2221
|
+
uri: `${mainDataSetUrl}/hashtree`,
|
|
2222
|
+
})
|
|
2223
|
+
)
|
|
2224
|
+
.rejects(hashTreeError);
|
|
2225
|
+
|
|
2226
|
+
await clock.tickAsync(1000);
|
|
2227
|
+
|
|
2228
|
+
assert.calledOnceWithExactly(metricsStub, BEHAVIORAL_METRICS.HASH_TREE_SYNC_FAILURE, {
|
|
2229
|
+
debugId: 'test',
|
|
2230
|
+
dataSetName: 'main',
|
|
2231
|
+
request: 'GET /hashtree',
|
|
2232
|
+
statusCode: 500,
|
|
2233
|
+
reason: 'server error',
|
|
2234
|
+
});
|
|
2235
|
+
});
|
|
2236
|
+
|
|
2237
|
+
it('sends HASH_TREE_SYNC_FAILURE metric when POST /sync request fails', async () => {
|
|
2238
|
+
const parser = createHashTreeParser();
|
|
2239
|
+
|
|
2240
|
+
// Send a heartbeat with a mismatched root hash to trigger runSyncAlgorithm
|
|
2241
|
+
const heartbeatMessage = {
|
|
2242
|
+
dataSets: [
|
|
2243
|
+
{
|
|
2244
|
+
...createDataSet('main', 16, 1100),
|
|
2245
|
+
root: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1',
|
|
2246
|
+
},
|
|
2247
|
+
],
|
|
2248
|
+
visibleDataSetsUrl,
|
|
2249
|
+
locusUrl,
|
|
2250
|
+
};
|
|
2251
|
+
|
|
2252
|
+
parser.handleMessage(heartbeatMessage, 'heartbeat with mismatch');
|
|
2253
|
+
|
|
2254
|
+
const mainDataSetUrl = parser.dataSets.main.url;
|
|
2255
|
+
|
|
2256
|
+
// Mock getHashesFromLocus to succeed
|
|
2257
|
+
mockGetHashesFromLocusResponse(
|
|
2258
|
+
mainDataSetUrl,
|
|
2259
|
+
new Array(16).fill('00000000000000000000000000000000'),
|
|
2260
|
+
createDataSet('main', 16, 1101)
|
|
2261
|
+
);
|
|
2262
|
+
|
|
2263
|
+
// Mock sendSyncRequestToLocus to fail
|
|
2264
|
+
const syncError = new Error('sync failed') as any;
|
|
2265
|
+
syncError.statusCode = 500;
|
|
2266
|
+
|
|
2267
|
+
webexRequest
|
|
2268
|
+
.withArgs(
|
|
2269
|
+
sinon.match({
|
|
2270
|
+
method: 'POST',
|
|
2271
|
+
uri: `${mainDataSetUrl}/sync`,
|
|
2272
|
+
})
|
|
2273
|
+
)
|
|
2274
|
+
.rejects(syncError);
|
|
2275
|
+
|
|
2276
|
+
await clock.tickAsync(1000);
|
|
2277
|
+
|
|
2278
|
+
assert.calledOnceWithExactly(metricsStub, BEHAVIORAL_METRICS.HASH_TREE_SYNC_FAILURE, {
|
|
2279
|
+
debugId: 'test',
|
|
2280
|
+
dataSetName: 'main',
|
|
2281
|
+
request: 'POST /sync',
|
|
2282
|
+
statusCode: 500,
|
|
2283
|
+
reason: 'sync failed',
|
|
2284
|
+
});
|
|
2285
|
+
});
|
|
2286
|
+
});
|
|
2287
|
+
|
|
2288
|
+
describe('handles visible data sets changes correctly', () => {
|
|
2289
|
+
it('handles addition of visible data set (one that does not require async initialization)', async () => {
|
|
2290
|
+
// Create a parser with visible datasets
|
|
2291
|
+
const parser = createHashTreeParser();
|
|
2292
|
+
|
|
2293
|
+
// Stub updateItems on self hash tree to return true
|
|
2294
|
+
sinon.stub(parser.dataSets.self.hashTree, 'updateItems').returns([true]);
|
|
2295
|
+
|
|
2296
|
+
// Send a message with Metadata object that has a new visibleDataSets list
|
|
2297
|
+
const message = {
|
|
2298
|
+
dataSets: [createDataSet('self', 1, 2100), createDataSet('attendees', 8, 4000)],
|
|
2299
|
+
visibleDataSetsUrl,
|
|
2300
|
+
locusUrl,
|
|
2301
|
+
locusStateElements: [
|
|
2302
|
+
{
|
|
2303
|
+
htMeta: {
|
|
2304
|
+
elementId: {
|
|
2305
|
+
type: 'metadata' as const,
|
|
2306
|
+
id: 5,
|
|
2307
|
+
version: 51,
|
|
2308
|
+
},
|
|
2309
|
+
dataSetNames: ['self'],
|
|
2310
|
+
},
|
|
2311
|
+
data: {
|
|
2312
|
+
visibleDataSets: [
|
|
2313
|
+
{
|
|
2314
|
+
name: 'main',
|
|
2315
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/main',
|
|
2316
|
+
},
|
|
2317
|
+
{
|
|
2318
|
+
name: 'self',
|
|
2319
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/713e9f99/datasets/self',
|
|
2320
|
+
},
|
|
2321
|
+
{
|
|
2322
|
+
name: 'atd-unmuted',
|
|
2323
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/atd-unmuted',
|
|
2324
|
+
},
|
|
2325
|
+
{
|
|
2326
|
+
name: 'attendees',
|
|
2327
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/attendees',
|
|
2328
|
+
},
|
|
2329
|
+
], // added 'attendees'
|
|
2330
|
+
},
|
|
2331
|
+
},
|
|
2332
|
+
],
|
|
2333
|
+
};
|
|
2334
|
+
|
|
2335
|
+
parser.handleMessage(message, 'add visible dataset');
|
|
2336
|
+
|
|
2337
|
+
// Verify that 'attendees' was added to visibleDataSets
|
|
2338
|
+
expect(parser.visibleDataSets.some((vds) => vds.name === 'attendees')).to.be.true;
|
|
2339
|
+
|
|
2340
|
+
// Verify that a hash tree was created for 'attendees'
|
|
2341
|
+
assert.exists(parser.dataSets.attendees.hashTree);
|
|
2342
|
+
assert.equal(parser.dataSets.attendees.hashTree.numLeaves, 8);
|
|
2343
|
+
|
|
2344
|
+
// Verify callback was called with the metadata update (appears twice - processed once for visible dataset changes, once in main loop)
|
|
2345
|
+
assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.OBJECTS_UPDATED,
|
|
2346
|
+
updatedObjects: [
|
|
2347
|
+
{
|
|
2348
|
+
htMeta: {
|
|
2349
|
+
elementId: {type: 'metadata', id: 5, version: 51},
|
|
2350
|
+
dataSetNames: ['self'],
|
|
2351
|
+
},
|
|
2352
|
+
data: {
|
|
2353
|
+
visibleDataSets: [
|
|
2354
|
+
{
|
|
2355
|
+
name: 'main',
|
|
2356
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/main',
|
|
2357
|
+
},
|
|
2358
|
+
{
|
|
2359
|
+
name: 'self',
|
|
2360
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/713e9f99/datasets/self',
|
|
2361
|
+
},
|
|
2362
|
+
{
|
|
2363
|
+
name: 'atd-unmuted',
|
|
2364
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/atd-unmuted',
|
|
2365
|
+
},
|
|
2366
|
+
{
|
|
2367
|
+
name: 'attendees',
|
|
2368
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/attendees',
|
|
2369
|
+
},
|
|
2370
|
+
],
|
|
2371
|
+
},
|
|
2372
|
+
},
|
|
2373
|
+
{
|
|
2374
|
+
htMeta: {
|
|
2375
|
+
elementId: {type: 'metadata', id: 5, version: 51},
|
|
2376
|
+
dataSetNames: ['self'],
|
|
2377
|
+
},
|
|
2378
|
+
data: {
|
|
2379
|
+
visibleDataSets: [
|
|
2380
|
+
{
|
|
2381
|
+
name: 'main',
|
|
2382
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/main',
|
|
2383
|
+
},
|
|
2384
|
+
{
|
|
2385
|
+
name: 'self',
|
|
2386
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/713e9f99/datasets/self',
|
|
2387
|
+
},
|
|
2388
|
+
{
|
|
2389
|
+
name: 'atd-unmuted',
|
|
2390
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/atd-unmuted',
|
|
2391
|
+
},
|
|
2392
|
+
{
|
|
2393
|
+
name: 'attendees',
|
|
2394
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/attendees',
|
|
2395
|
+
},
|
|
2396
|
+
],
|
|
2397
|
+
},
|
|
2398
|
+
},
|
|
2399
|
+
],
|
|
2400
|
+
});
|
|
2401
|
+
});
|
|
2402
|
+
|
|
2403
|
+
it('handles addition of visible data set (one that requires async initialization)', async () => {
|
|
2404
|
+
// Create a parser with visible datasets
|
|
2405
|
+
const parser = createHashTreeParser();
|
|
2406
|
+
|
|
2407
|
+
// Stub updateItems on self hash tree to return true
|
|
2408
|
+
sinon.stub(parser.dataSets.self.hashTree, 'updateItems').returns([true]);
|
|
2409
|
+
|
|
2410
|
+
// Send a message with Metadata object that has a new visibleDataSets list (adding 'new-dataset')
|
|
2411
|
+
// but WITHOUT providing info about the new dataset in dataSets array
|
|
2412
|
+
const message = {
|
|
2413
|
+
dataSets: [createDataSet('self', 1, 2100)],
|
|
2414
|
+
visibleDataSetsUrl,
|
|
2415
|
+
locusUrl,
|
|
2416
|
+
locusStateElements: [
|
|
2417
|
+
{
|
|
2418
|
+
htMeta: {
|
|
2419
|
+
elementId: {
|
|
2420
|
+
type: 'metadata' as const,
|
|
2421
|
+
id: 5,
|
|
2422
|
+
version: 51,
|
|
2423
|
+
},
|
|
2024
2424
|
dataSetNames: ['self'],
|
|
2025
2425
|
},
|
|
2026
2426
|
data: {
|
|
@@ -2062,6 +2462,98 @@ describe('HashTreeParser', () => {
|
|
|
2062
2462
|
await checkAsyncDatasetInitialization(parser, newDataSet);
|
|
2063
2463
|
});
|
|
2064
2464
|
|
|
2465
|
+
it('initializes new visible data sets in priority order', async () => {
|
|
2466
|
+
// Create a parser that only has "self" as visible (no "main")
|
|
2467
|
+
const initialLocusWithoutMain = {
|
|
2468
|
+
dataSets: [createDataSet('self', 1, 2000)],
|
|
2469
|
+
locus: {
|
|
2470
|
+
...exampleInitialLocus.locus,
|
|
2471
|
+
},
|
|
2472
|
+
};
|
|
2473
|
+
const metadataWithoutMain = {
|
|
2474
|
+
...exampleMetadata,
|
|
2475
|
+
visibleDataSets: [
|
|
2476
|
+
{
|
|
2477
|
+
name: 'self',
|
|
2478
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/713e9f99/datasets/self',
|
|
2479
|
+
},
|
|
2480
|
+
],
|
|
2481
|
+
};
|
|
2482
|
+
const parser = createHashTreeParser(initialLocusWithoutMain, metadataWithoutMain);
|
|
2483
|
+
|
|
2484
|
+
// Verify "main" is not visible initially
|
|
2485
|
+
expect(parser.visibleDataSets.some((vds) => vds.name === 'main')).to.be.false;
|
|
2486
|
+
|
|
2487
|
+
// Stub updateItems on self hash tree to return true
|
|
2488
|
+
sinon.stub(parser.dataSets.self.hashTree, 'updateItems').returns([true]);
|
|
2489
|
+
|
|
2490
|
+
// Send a message that adds "main" and "atd-active" as new visible datasets.
|
|
2491
|
+
// Neither has info in dataSets, so both require async initialization.
|
|
2492
|
+
const newMainDataSet = createDataSet('main', 16, 6000);
|
|
2493
|
+
const newAtdActiveDataSet = createDataSet('atd-active', 4, 7000);
|
|
2494
|
+
|
|
2495
|
+
const message = {
|
|
2496
|
+
dataSets: [createDataSet('self', 1, 2100)],
|
|
2497
|
+
visibleDataSetsUrl,
|
|
2498
|
+
locusUrl,
|
|
2499
|
+
locusStateElements: [
|
|
2500
|
+
{
|
|
2501
|
+
htMeta: {
|
|
2502
|
+
elementId: {
|
|
2503
|
+
type: 'metadata' as const,
|
|
2504
|
+
id: 5,
|
|
2505
|
+
version: 51,
|
|
2506
|
+
},
|
|
2507
|
+
dataSetNames: ['self'],
|
|
2508
|
+
},
|
|
2509
|
+
data: {
|
|
2510
|
+
visibleDataSets: [
|
|
2511
|
+
{
|
|
2512
|
+
name: 'self',
|
|
2513
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/713e9f99/datasets/self',
|
|
2514
|
+
},
|
|
2515
|
+
// listed in non-priority order: atd-active before main
|
|
2516
|
+
{name: 'atd-active', url: newAtdActiveDataSet.url},
|
|
2517
|
+
{name: 'main', url: newMainDataSet.url},
|
|
2518
|
+
],
|
|
2519
|
+
},
|
|
2520
|
+
},
|
|
2521
|
+
],
|
|
2522
|
+
};
|
|
2523
|
+
|
|
2524
|
+
// Mock getAllVisibleDataSetsFromLocus to return both new datasets (in non-priority order)
|
|
2525
|
+
mockGetAllDataSetsMetadata(webexRequest, visibleDataSetsUrl, [
|
|
2526
|
+
newAtdActiveDataSet,
|
|
2527
|
+
newMainDataSet,
|
|
2528
|
+
]);
|
|
2529
|
+
mockSyncRequest(webexRequest, newMainDataSet.url);
|
|
2530
|
+
mockSyncRequest(webexRequest, newAtdActiveDataSet.url);
|
|
2531
|
+
|
|
2532
|
+
parser.handleMessage(message, 'add main and atd-active datasets');
|
|
2533
|
+
|
|
2534
|
+
// Wait for the async initialization (queueMicrotask) to complete
|
|
2535
|
+
await clock.tickAsync(0);
|
|
2536
|
+
|
|
2537
|
+
// Verify both datasets are initialized
|
|
2538
|
+
expect(parser.dataSets.main?.hashTree).to.exist;
|
|
2539
|
+
expect(parser.dataSets['atd-active']?.hashTree).to.exist;
|
|
2540
|
+
|
|
2541
|
+
// Verify sync requests were sent in priority order: "main" before "atd-active",
|
|
2542
|
+
// even though atd-active was listed first in both the message and the Locus response
|
|
2543
|
+
const syncCalls = webexRequest
|
|
2544
|
+
.getCalls()
|
|
2545
|
+
.filter(
|
|
2546
|
+
(call) =>
|
|
2547
|
+
call.args[0]?.method === 'POST' &&
|
|
2548
|
+
call.args[0]?.uri?.endsWith('/sync') &&
|
|
2549
|
+
(call.args[0]?.uri?.includes('/main/') || call.args[0]?.uri?.includes('/atd-active/'))
|
|
2550
|
+
);
|
|
2551
|
+
|
|
2552
|
+
expect(syncCalls).to.have.lengthOf(2);
|
|
2553
|
+
expect(syncCalls[0].args[0].uri).to.equal(`${newMainDataSet.url}/sync`);
|
|
2554
|
+
expect(syncCalls[1].args[0].uri).to.equal(`${newAtdActiveDataSet.url}/sync`);
|
|
2555
|
+
});
|
|
2556
|
+
|
|
2065
2557
|
it('emits MEETING_ENDED if async init of a new visible dataset fails with 404', async () => {
|
|
2066
2558
|
const parser = createHashTreeParser();
|
|
2067
2559
|
|
|
@@ -2128,9 +2620,7 @@ describe('HashTreeParser', () => {
|
|
|
2128
2620
|
await clock.tickAsync(0);
|
|
2129
2621
|
|
|
2130
2622
|
// Verify callback was called with MEETING_ENDED
|
|
2131
|
-
assert.calledOnceWithExactly(callback, LocusInfoUpdateType.MEETING_ENDED
|
|
2132
|
-
updatedObjects: undefined,
|
|
2133
|
-
});
|
|
2623
|
+
assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.MEETING_ENDED});
|
|
2134
2624
|
});
|
|
2135
2625
|
|
|
2136
2626
|
it('handles removal of visible data set', async () => {
|
|
@@ -2193,7 +2683,7 @@ describe('HashTreeParser', () => {
|
|
|
2193
2683
|
assert.isUndefined(parser.dataSets['atd-unmuted'].timer);
|
|
2194
2684
|
|
|
2195
2685
|
// Verify callback was called with the metadata update and the removed objects (metadata appears twice - processed once for dataset changes, once in main loop)
|
|
2196
|
-
assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED,
|
|
2686
|
+
assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.OBJECTS_UPDATED,
|
|
2197
2687
|
updatedObjects: [
|
|
2198
2688
|
{
|
|
2199
2689
|
htMeta: {
|
|
@@ -2290,6 +2780,151 @@ describe('HashTreeParser', () => {
|
|
|
2290
2780
|
// Verify callback was NOT called (no updates for non-visible datasets)
|
|
2291
2781
|
assert.notCalled(callback);
|
|
2292
2782
|
});
|
|
2783
|
+
|
|
2784
|
+
it('reports update for object that moves from removed visible dataset to new visible dataset even if version is unchanged', async () => {
|
|
2785
|
+
// The purpose of this test is to verify that when an object
|
|
2786
|
+
// moves from one visible dataset to another without version change,
|
|
2787
|
+
// the parser still reports it as an update.
|
|
2788
|
+
// Locus has some additional signalling for this - the "view" property in htMeta.elementId.
|
|
2789
|
+
// When a view changes, the contents of the object may change even if version doesn't.
|
|
2790
|
+
// HashTreeParser doesn't use the "view" property, because it doesn't need to -
|
|
2791
|
+
// the same functionality is achieved thanks to the fact that a new visible data set means
|
|
2792
|
+
// a new hash tree is created, so HashTreeParser still detects the change as new
|
|
2793
|
+
// object is added to the new hash tree.
|
|
2794
|
+
|
|
2795
|
+
// Setup: parser with visible datasets "self" and "unjoined"
|
|
2796
|
+
const unjoinedDataSet = createDataSet('unjoined', 4, 3000);
|
|
2797
|
+
const selfDataSet = createDataSet('self', 1, 2000);
|
|
2798
|
+
|
|
2799
|
+
// start with Locus that has "info" in both "unjoined" and "main" datasets,
|
|
2800
|
+
// but only "unjoined" is visible.
|
|
2801
|
+
const initialLocus = {
|
|
2802
|
+
dataSets: [selfDataSet, unjoinedDataSet],
|
|
2803
|
+
locus: {
|
|
2804
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f',
|
|
2805
|
+
links: {resources: {visibleDataSets: {url: visibleDataSetsUrl}}},
|
|
2806
|
+
// info object in "unjoined" dataset with version 500
|
|
2807
|
+
info: {
|
|
2808
|
+
htMeta: {
|
|
2809
|
+
elementId: {
|
|
2810
|
+
type: 'info',
|
|
2811
|
+
id: 42,
|
|
2812
|
+
version: 500,
|
|
2813
|
+
view: ['unjoined'], // not used by our code, but here for completeness - that's what real Locus would send
|
|
2814
|
+
},
|
|
2815
|
+
dataSetNames: ['main', 'unjoined'],
|
|
2816
|
+
},
|
|
2817
|
+
someField: 'some-initial-value',
|
|
2818
|
+
},
|
|
2819
|
+
self: {
|
|
2820
|
+
htMeta: {
|
|
2821
|
+
elementId: {
|
|
2822
|
+
type: 'self',
|
|
2823
|
+
id: 4,
|
|
2824
|
+
version: 100,
|
|
2825
|
+
},
|
|
2826
|
+
dataSetNames: ['self'],
|
|
2827
|
+
},
|
|
2828
|
+
},
|
|
2829
|
+
},
|
|
2830
|
+
};
|
|
2831
|
+
|
|
2832
|
+
const metadata = {
|
|
2833
|
+
htMeta: {
|
|
2834
|
+
elementId: {
|
|
2835
|
+
type: 'metadata',
|
|
2836
|
+
id: 5,
|
|
2837
|
+
version: 50,
|
|
2838
|
+
},
|
|
2839
|
+
dataSetNames: ['self'],
|
|
2840
|
+
},
|
|
2841
|
+
visibleDataSets: [
|
|
2842
|
+
{name: 'self', url: selfDataSet.url},
|
|
2843
|
+
{name: 'unjoined', url: unjoinedDataSet.url},
|
|
2844
|
+
],
|
|
2845
|
+
};
|
|
2846
|
+
|
|
2847
|
+
const parser = createHashTreeParser(initialLocus, metadata);
|
|
2848
|
+
|
|
2849
|
+
// Verify initial state: unjoined is visible and has the info object
|
|
2850
|
+
expect(parser.visibleDataSets.some((vds) => vds.name === 'unjoined')).to.be.true;
|
|
2851
|
+
assert.exists(parser.dataSets.unjoined.hashTree);
|
|
2852
|
+
assert.equal(parser.dataSets.unjoined.hashTree?.getItemVersion(42, 'info'), 500);
|
|
2853
|
+
|
|
2854
|
+
// Stub updateItems on self hash tree to return true for metadata update
|
|
2855
|
+
sinon.stub(parser.dataSets.self.hashTree, 'updateItems').returns([true]);
|
|
2856
|
+
|
|
2857
|
+
// Now send a message that:
|
|
2858
|
+
// 1. Changes visible datasets: removes "unjoined", adds "main"
|
|
2859
|
+
// 2. Contains the same info object (same id=42, same version=500) but we see the view from "main" dataset
|
|
2860
|
+
const mainDataSet = createDataSet('main', 16, 1000);
|
|
2861
|
+
|
|
2862
|
+
const message = {
|
|
2863
|
+
dataSets: [selfDataSet, mainDataSet],
|
|
2864
|
+
visibleDataSetsUrl,
|
|
2865
|
+
locusUrl,
|
|
2866
|
+
locusStateElements: [
|
|
2867
|
+
{
|
|
2868
|
+
htMeta: {
|
|
2869
|
+
elementId: {
|
|
2870
|
+
type: 'metadata' as const,
|
|
2871
|
+
id: 5,
|
|
2872
|
+
version: 51,
|
|
2873
|
+
},
|
|
2874
|
+
dataSetNames: ['self'],
|
|
2875
|
+
},
|
|
2876
|
+
data: {
|
|
2877
|
+
visibleDataSets: [
|
|
2878
|
+
{name: 'self', url: selfDataSet.url},
|
|
2879
|
+
{name: 'main', url: mainDataSet.url},
|
|
2880
|
+
// "unjoined" is no longer here
|
|
2881
|
+
],
|
|
2882
|
+
},
|
|
2883
|
+
},
|
|
2884
|
+
{
|
|
2885
|
+
htMeta: {
|
|
2886
|
+
elementId: {
|
|
2887
|
+
type: 'info' as const,
|
|
2888
|
+
id: 42,
|
|
2889
|
+
version: 500, // same version as before
|
|
2890
|
+
view: ['main'], // now points to "main" instead of "unjoined"
|
|
2891
|
+
},
|
|
2892
|
+
dataSetNames: ['main', 'unjoined'], // still in both datasets, but only "main" is visible now
|
|
2893
|
+
},
|
|
2894
|
+
data: {someNewField: 'some-value'},
|
|
2895
|
+
},
|
|
2896
|
+
],
|
|
2897
|
+
};
|
|
2898
|
+
|
|
2899
|
+
parser.handleMessage(message, 'visible dataset swap with same-version object');
|
|
2900
|
+
|
|
2901
|
+
// Verify "unjoined" is no longer visible and "main" is now visible
|
|
2902
|
+
expect(parser.visibleDataSets.some((vds) => vds.name === 'unjoined')).to.be.false;
|
|
2903
|
+
expect(parser.visibleDataSets.some((vds) => vds.name === 'main')).to.be.true;
|
|
2904
|
+
|
|
2905
|
+
// Verify the info object is now in the "main" hash tree
|
|
2906
|
+
assert.exists(parser.dataSets.main.hashTree);
|
|
2907
|
+
assert.equal(parser.dataSets.main.hashTree?.getItemVersion(42, 'info'), 500);
|
|
2908
|
+
|
|
2909
|
+
// The key assertion: callback should be called with the info object update even though
|
|
2910
|
+
// its version hasn't changed - because visible datasets changed (moved from unjoined to main)
|
|
2911
|
+
assert.calledOnce(callback);
|
|
2912
|
+
const callbackArgs = callback.firstCall.args[0];
|
|
2913
|
+
assert.equal(callbackArgs.updateType, LocusInfoUpdateType.OBJECTS_UPDATED);
|
|
2914
|
+
|
|
2915
|
+
// Should contain the info object update (with data)
|
|
2916
|
+
const infoUpdate = callbackArgs.updatedObjects.find(
|
|
2917
|
+
(obj) => obj.htMeta.elementId.type === 'info' && obj.htMeta.elementId.id === 42
|
|
2918
|
+
);
|
|
2919
|
+
assert.exists(infoUpdate);
|
|
2920
|
+
assert.deepEqual(infoUpdate.htMeta.elementId, {
|
|
2921
|
+
type: 'info',
|
|
2922
|
+
id: 42,
|
|
2923
|
+
version: 500,
|
|
2924
|
+
view: ['main'],
|
|
2925
|
+
});
|
|
2926
|
+
assert.deepEqual(infoUpdate.data, {someNewField: 'some-value'});
|
|
2927
|
+
});
|
|
2293
2928
|
});
|
|
2294
2929
|
|
|
2295
2930
|
describe('heartbeat watchdog', () => {
|
|
@@ -2666,56 +3301,126 @@ describe('HashTreeParser', () => {
|
|
|
2666
3301
|
// At 5500ms, 'main' watchdog fires and performSync runs immediately
|
|
2667
3302
|
await clock.tickAsync(1);
|
|
2668
3303
|
|
|
2669
|
-
// main sync should have triggered immediately (GET hashtree + POST sync)
|
|
3304
|
+
// main sync should have triggered immediately (GET hashtree + POST sync)
|
|
3305
|
+
assert.calledWith(
|
|
3306
|
+
webexRequest,
|
|
3307
|
+
sinon.match({
|
|
3308
|
+
method: 'GET',
|
|
3309
|
+
uri: `${parser.dataSets.main.url}/hashtree`,
|
|
3310
|
+
})
|
|
3311
|
+
);
|
|
3312
|
+
|
|
3313
|
+
webexRequest.resetHistory();
|
|
3314
|
+
|
|
3315
|
+
// At 7000ms, 'self' watchdog fires and performSync runs immediately
|
|
3316
|
+
await clock.tickAsync(1500);
|
|
3317
|
+
|
|
3318
|
+
// self sync should have also triggered (POST sync only, leafCount === 1)
|
|
3319
|
+
assert.calledWith(
|
|
3320
|
+
webexRequest,
|
|
3321
|
+
sinon.match({
|
|
3322
|
+
method: 'POST',
|
|
3323
|
+
uri: `${parser.dataSets.self.url}/sync`,
|
|
3324
|
+
})
|
|
3325
|
+
);
|
|
3326
|
+
});
|
|
3327
|
+
|
|
3328
|
+
it('does not set watchdog for data sets without a hash tree', async () => {
|
|
3329
|
+
const parser = createHashTreeParser();
|
|
3330
|
+
const heartbeatIntervalMs = 5000;
|
|
3331
|
+
|
|
3332
|
+
// 'atd-active' is in the initial locus but is not visible (no hash tree)
|
|
3333
|
+
// Send heartbeat mentioning a non-visible dataset
|
|
3334
|
+
const heartbeatMessage = {
|
|
3335
|
+
dataSets: [
|
|
3336
|
+
{
|
|
3337
|
+
...createDataSet('main', 16, 1100),
|
|
3338
|
+
root: parser.dataSets.main.hashTree.getRootHash(),
|
|
3339
|
+
},
|
|
3340
|
+
createDataSet('atd-active', 16, 4000),
|
|
3341
|
+
],
|
|
3342
|
+
visibleDataSetsUrl,
|
|
3343
|
+
locusUrl,
|
|
3344
|
+
heartbeatIntervalMs,
|
|
3345
|
+
};
|
|
3346
|
+
|
|
3347
|
+
parser.handleMessage(heartbeatMessage, 'heartbeat with non-visible dataset');
|
|
3348
|
+
|
|
3349
|
+
// Watchdog set for main (visible) but not for atd-active (no hash tree)
|
|
3350
|
+
expect(parser.dataSets.main.heartbeatWatchdogTimer).to.not.be.undefined;
|
|
3351
|
+
expect(parser.dataSets['atd-active']?.heartbeatWatchdogTimer).to.be.undefined;
|
|
3352
|
+
});
|
|
3353
|
+
|
|
3354
|
+
it('restarts the watchdog timer after it fires so that future missed heartbeats still trigger syncs', async () => {
|
|
3355
|
+
const parser = createHashTreeParser();
|
|
3356
|
+
const heartbeatIntervalMs = 5000;
|
|
3357
|
+
|
|
3358
|
+
// Send initial heartbeat for 'main'
|
|
3359
|
+
const heartbeatMessage = {
|
|
3360
|
+
dataSets: [
|
|
3361
|
+
{
|
|
3362
|
+
...createDataSet('main', 16, 1100),
|
|
3363
|
+
root: parser.dataSets.main.hashTree.getRootHash(),
|
|
3364
|
+
},
|
|
3365
|
+
],
|
|
3366
|
+
visibleDataSetsUrl,
|
|
3367
|
+
locusUrl,
|
|
3368
|
+
heartbeatIntervalMs,
|
|
3369
|
+
};
|
|
3370
|
+
|
|
3371
|
+
parser.handleMessage(heartbeatMessage, 'initial heartbeat');
|
|
3372
|
+
expect(parser.dataSets.main.heartbeatWatchdogTimer).to.not.be.undefined;
|
|
3373
|
+
|
|
3374
|
+
// Mock responses for performSync - return null (204/empty body)
|
|
3375
|
+
const mainDataSetUrl = parser.dataSets.main.url;
|
|
3376
|
+
mockGetHashesFromLocusResponse(
|
|
3377
|
+
mainDataSetUrl,
|
|
3378
|
+
new Array(16).fill('00000000000000000000000000000000'),
|
|
3379
|
+
createDataSet('main', 16, 1101)
|
|
3380
|
+
);
|
|
3381
|
+
mockSendSyncRequestResponse(mainDataSetUrl, null);
|
|
3382
|
+
|
|
3383
|
+
// Advance time past heartbeatIntervalMs to fire the watchdog
|
|
3384
|
+
await clock.tickAsync(heartbeatIntervalMs);
|
|
3385
|
+
|
|
3386
|
+
// Verify sync was triggered
|
|
2670
3387
|
assert.calledWith(
|
|
2671
3388
|
webexRequest,
|
|
2672
3389
|
sinon.match({
|
|
2673
3390
|
method: 'GET',
|
|
2674
|
-
uri: `${
|
|
3391
|
+
uri: `${mainDataSetUrl}/hashtree`,
|
|
2675
3392
|
})
|
|
2676
3393
|
);
|
|
2677
3394
|
|
|
3395
|
+
// The watchdog timer should have been restarted after firing
|
|
3396
|
+
expect(parser.dataSets.main.heartbeatWatchdogTimer).to.not.be.undefined;
|
|
3397
|
+
|
|
3398
|
+
// Reset call history and set up new mock responses for the second sync
|
|
2678
3399
|
webexRequest.resetHistory();
|
|
3400
|
+
mockGetHashesFromLocusResponse(
|
|
3401
|
+
mainDataSetUrl,
|
|
3402
|
+
new Array(16).fill('00000000000000000000000000000000'),
|
|
3403
|
+
createDataSet('main', 16, 1102)
|
|
3404
|
+
);
|
|
3405
|
+
mockSendSyncRequestResponse(mainDataSetUrl, null);
|
|
2679
3406
|
|
|
2680
|
-
//
|
|
2681
|
-
await clock.tickAsync(
|
|
3407
|
+
// Advance time again to fire the watchdog a second time
|
|
3408
|
+
await clock.tickAsync(heartbeatIntervalMs);
|
|
2682
3409
|
|
|
2683
|
-
//
|
|
3410
|
+
// Verify a second sync was triggered
|
|
2684
3411
|
assert.calledWith(
|
|
2685
3412
|
webexRequest,
|
|
2686
3413
|
sinon.match({
|
|
2687
|
-
method: '
|
|
2688
|
-
uri: `${
|
|
3414
|
+
method: 'GET',
|
|
3415
|
+
uri: `${mainDataSetUrl}/hashtree`,
|
|
2689
3416
|
})
|
|
2690
3417
|
);
|
|
2691
|
-
});
|
|
2692
|
-
|
|
2693
|
-
it('does not set watchdog for data sets without a hash tree', async () => {
|
|
2694
|
-
const parser = createHashTreeParser();
|
|
2695
|
-
const heartbeatIntervalMs = 5000;
|
|
2696
|
-
|
|
2697
|
-
// 'atd-active' is in the initial locus but is not visible (no hash tree)
|
|
2698
|
-
// Send heartbeat mentioning a non-visible dataset
|
|
2699
|
-
const heartbeatMessage = {
|
|
2700
|
-
dataSets: [
|
|
2701
|
-
{
|
|
2702
|
-
...createDataSet('main', 16, 1100),
|
|
2703
|
-
root: parser.dataSets.main.hashTree.getRootHash(),
|
|
2704
|
-
},
|
|
2705
|
-
createDataSet('atd-active', 16, 4000),
|
|
2706
|
-
],
|
|
2707
|
-
visibleDataSetsUrl,
|
|
2708
|
-
locusUrl,
|
|
2709
|
-
heartbeatIntervalMs,
|
|
2710
|
-
};
|
|
2711
3418
|
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
// Watchdog set for main (visible) but not for atd-active (no hash tree)
|
|
3419
|
+
// And the watchdog should still be running
|
|
2715
3420
|
expect(parser.dataSets.main.heartbeatWatchdogTimer).to.not.be.undefined;
|
|
2716
|
-
expect(parser.dataSets['atd-active']?.heartbeatWatchdogTimer).to.be.undefined;
|
|
2717
3421
|
});
|
|
2718
3422
|
});
|
|
3423
|
+
|
|
2719
3424
|
});
|
|
2720
3425
|
|
|
2721
3426
|
describe('#callLocusInfoUpdateCallback filtering', () => {
|
|
@@ -2812,7 +3517,7 @@ describe('HashTreeParser', () => {
|
|
|
2812
3517
|
parser.handleMessage(updateMessage, 'update with newer version');
|
|
2813
3518
|
|
|
2814
3519
|
// Callback should be called with the update
|
|
2815
|
-
assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED,
|
|
3520
|
+
assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.OBJECTS_UPDATED,
|
|
2816
3521
|
updatedObjects: [
|
|
2817
3522
|
{
|
|
2818
3523
|
htMeta: {
|
|
@@ -2883,7 +3588,7 @@ describe('HashTreeParser', () => {
|
|
|
2883
3588
|
parser.handleMessage(removalMessage, 'removal of non-existent object');
|
|
2884
3589
|
|
|
2885
3590
|
// Callback should be called with the removal
|
|
2886
|
-
assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED,
|
|
3591
|
+
assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.OBJECTS_UPDATED,
|
|
2887
3592
|
updatedObjects: [
|
|
2888
3593
|
{
|
|
2889
3594
|
htMeta: {
|
|
@@ -3018,7 +3723,7 @@ describe('HashTreeParser', () => {
|
|
|
3018
3723
|
parser.handleMessage(mixedMessage, 'mixed updates');
|
|
3019
3724
|
|
|
3020
3725
|
// Callback should be called with only the valid updates (participant 1 v110 and participant 3 v10)
|
|
3021
|
-
assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED,
|
|
3726
|
+
assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.OBJECTS_UPDATED,
|
|
3022
3727
|
updatedObjects: [
|
|
3023
3728
|
{
|
|
3024
3729
|
htMeta: {
|
|
@@ -3196,9 +3901,7 @@ describe('HashTreeParser', () => {
|
|
|
3196
3901
|
parser.handleMessage(sentinelMessage as any, 'sentinel message');
|
|
3197
3902
|
|
|
3198
3903
|
// Callback should be called with MEETING_ENDED
|
|
3199
|
-
assert.calledOnceWithExactly(callback, LocusInfoUpdateType.MEETING_ENDED
|
|
3200
|
-
updatedObjects: undefined,
|
|
3201
|
-
});
|
|
3904
|
+
assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.MEETING_ENDED});
|
|
3202
3905
|
});
|
|
3203
3906
|
});
|
|
3204
3907
|
|
|
@@ -3404,7 +4107,7 @@ describe('HashTreeParser', () => {
|
|
|
3404
4107
|
});
|
|
3405
4108
|
});
|
|
3406
4109
|
|
|
3407
|
-
describe('#
|
|
4110
|
+
describe('#resumeFromMessage', () => {
|
|
3408
4111
|
const createResumeMessage = (visibleDataSets?, dataSets?) => ({
|
|
3409
4112
|
locusUrl,
|
|
3410
4113
|
visibleDataSetsUrl,
|
|
@@ -3431,7 +4134,7 @@ describe('HashTreeParser', () => {
|
|
|
3431
4134
|
|
|
3432
4135
|
expect(parser.state).to.equal('stopped');
|
|
3433
4136
|
|
|
3434
|
-
parser.
|
|
4137
|
+
parser.resumeFromMessage(createResumeMessage());
|
|
3435
4138
|
|
|
3436
4139
|
expect(parser.state).to.equal('active');
|
|
3437
4140
|
});
|
|
@@ -3440,7 +4143,7 @@ describe('HashTreeParser', () => {
|
|
|
3440
4143
|
const parser = createHashTreeParser();
|
|
3441
4144
|
parser.stop();
|
|
3442
4145
|
|
|
3443
|
-
parser.
|
|
4146
|
+
parser.resumeFromMessage({
|
|
3444
4147
|
locusUrl,
|
|
3445
4148
|
visibleDataSetsUrl,
|
|
3446
4149
|
dataSets: [createDataSet('main', 16, 2000)],
|
|
@@ -3459,7 +4162,7 @@ describe('HashTreeParser', () => {
|
|
|
3459
4162
|
createDataSet('self', 2, 6000),
|
|
3460
4163
|
];
|
|
3461
4164
|
|
|
3462
|
-
parser.
|
|
4165
|
+
parser.resumeFromMessage(createResumeMessage(undefined, newDataSets));
|
|
3463
4166
|
|
|
3464
4167
|
expect(Object.keys(parser.dataSets)).to.have.lengthOf(2);
|
|
3465
4168
|
expect(parser.dataSets.main.leafCount).to.equal(8);
|
|
@@ -3481,7 +4184,7 @@ describe('HashTreeParser', () => {
|
|
|
3481
4184
|
{name: 'self', url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/713e9f99/datasets/self'},
|
|
3482
4185
|
];
|
|
3483
4186
|
|
|
3484
|
-
parser.
|
|
4187
|
+
parser.resumeFromMessage(createResumeMessage(visibleDataSets, dataSets));
|
|
3485
4188
|
|
|
3486
4189
|
expect(parser.dataSets.main.hashTree).to.be.instanceOf(HashTree);
|
|
3487
4190
|
expect(parser.dataSets.self.hashTree).to.be.instanceOf(HashTree);
|
|
@@ -3495,7 +4198,7 @@ describe('HashTreeParser', () => {
|
|
|
3495
4198
|
const handleMessageStub = sinon.stub(parser, 'handleMessage');
|
|
3496
4199
|
|
|
3497
4200
|
const message = createResumeMessage();
|
|
3498
|
-
parser.
|
|
4201
|
+
parser.resumeFromMessage(message);
|
|
3499
4202
|
|
|
3500
4203
|
assert.calledOnceWithExactly(handleMessageStub, message, 'on resume');
|
|
3501
4204
|
});
|
|
@@ -3515,7 +4218,7 @@ describe('HashTreeParser', () => {
|
|
|
3515
4218
|
{name: 'atd-unmuted', url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/atd-unmuted'},
|
|
3516
4219
|
];
|
|
3517
4220
|
|
|
3518
|
-
parser.
|
|
4221
|
+
parser.resumeFromMessage(createResumeMessage(visibleDataSets, dataSets));
|
|
3519
4222
|
|
|
3520
4223
|
expect(parser.visibleDataSets.some((vds) => vds.name === 'atd-unmuted')).to.be.false;
|
|
3521
4224
|
expect(parser.visibleDataSets.some((vds) => vds.name === 'main')).to.be.true;
|
|
@@ -3523,6 +4226,67 @@ describe('HashTreeParser', () => {
|
|
|
3523
4226
|
});
|
|
3524
4227
|
});
|
|
3525
4228
|
|
|
4229
|
+
describe('#resumeFromApiResponse', () => {
|
|
4230
|
+
const exampleLocus = {
|
|
4231
|
+
participants: [],
|
|
4232
|
+
} as any;
|
|
4233
|
+
|
|
4234
|
+
it('should set state to active', async () => {
|
|
4235
|
+
const parser = createHashTreeParser();
|
|
4236
|
+
parser.stop();
|
|
4237
|
+
|
|
4238
|
+
expect(parser.state).to.equal('stopped');
|
|
4239
|
+
|
|
4240
|
+
sinon.stub(parser, 'initializeFromGetLociResponse').resolves();
|
|
4241
|
+
|
|
4242
|
+
await parser.resumeFromApiResponse(exampleLocus);
|
|
4243
|
+
|
|
4244
|
+
expect(parser.state).to.equal('active');
|
|
4245
|
+
});
|
|
4246
|
+
|
|
4247
|
+
it('should reset dataSets to empty', async () => {
|
|
4248
|
+
const parser = createHashTreeParser();
|
|
4249
|
+
|
|
4250
|
+
expect(Object.keys(parser.dataSets).length).to.be.greaterThan(0);
|
|
4251
|
+
|
|
4252
|
+
parser.stop();
|
|
4253
|
+
|
|
4254
|
+
sinon.stub(parser, 'initializeFromGetLociResponse').resolves();
|
|
4255
|
+
|
|
4256
|
+
await parser.resumeFromApiResponse(exampleLocus);
|
|
4257
|
+
|
|
4258
|
+
expect(parser.dataSets).to.deep.equal({});
|
|
4259
|
+
});
|
|
4260
|
+
|
|
4261
|
+
it('should call initializeFromGetLociResponse with the provided locus', async () => {
|
|
4262
|
+
const parser = createHashTreeParser();
|
|
4263
|
+
parser.stop();
|
|
4264
|
+
|
|
4265
|
+
const initStub = sinon.stub(parser, 'initializeFromGetLociResponse').resolves();
|
|
4266
|
+
|
|
4267
|
+
await parser.resumeFromApiResponse(exampleLocus);
|
|
4268
|
+
|
|
4269
|
+
assert.calledOnceWithExactly(initStub, exampleLocus);
|
|
4270
|
+
});
|
|
4271
|
+
|
|
4272
|
+
it('should propagate errors from initializeFromGetLociResponse', async () => {
|
|
4273
|
+
const parser = createHashTreeParser();
|
|
4274
|
+
parser.stop();
|
|
4275
|
+
|
|
4276
|
+
const error = new Error('initialization failed');
|
|
4277
|
+
const initStub = sinon.stub(parser, 'initializeFromGetLociResponse').rejects(error);
|
|
4278
|
+
|
|
4279
|
+
let caughtError: Error | undefined;
|
|
4280
|
+
try {
|
|
4281
|
+
await parser.resumeFromApiResponse(exampleLocus);
|
|
4282
|
+
} catch (e) {
|
|
4283
|
+
caughtError = e;
|
|
4284
|
+
}
|
|
4285
|
+
|
|
4286
|
+
expect(caughtError).to.equal(error);
|
|
4287
|
+
});
|
|
4288
|
+
});
|
|
4289
|
+
|
|
3526
4290
|
describe('#handleLocusUpdate when stopped', () => {
|
|
3527
4291
|
it('should return early without processing when parser is stopped', () => {
|
|
3528
4292
|
const parser = createHashTreeParser();
|
|
@@ -3557,4 +4321,343 @@ describe('HashTreeParser', () => {
|
|
|
3557
4321
|
assert.notCalled(callback);
|
|
3558
4322
|
});
|
|
3559
4323
|
});
|
|
4324
|
+
|
|
4325
|
+
describe('#syncAllDatasets', () => {
|
|
4326
|
+
it('should sync all datasets that have hash trees in priority order', async () => {
|
|
4327
|
+
const parser = createHashTreeParser();
|
|
4328
|
+
|
|
4329
|
+
// parser starts with main (leafCount=16) and self (leafCount=1) as visible datasets with hash trees
|
|
4330
|
+
// atd-unmuted has no hash tree (not visible)
|
|
4331
|
+
expect(parser.dataSets.main.hashTree).to.be.instanceOf(HashTree);
|
|
4332
|
+
expect(parser.dataSets.self.hashTree).to.be.instanceOf(HashTree);
|
|
4333
|
+
|
|
4334
|
+
const mainUrl = parser.dataSets.main.url;
|
|
4335
|
+
const selfUrl = parser.dataSets.self.url;
|
|
4336
|
+
|
|
4337
|
+
// Mock GET hashtree for main (leafCount > 1, so it does GET first)
|
|
4338
|
+
mockGetHashesFromLocusResponse(
|
|
4339
|
+
mainUrl,
|
|
4340
|
+
new Array(16).fill(EMPTY_HASH),
|
|
4341
|
+
createDataSet('main', 16, 1100)
|
|
4342
|
+
);
|
|
4343
|
+
|
|
4344
|
+
// Mock POST sync for main - return matching root hash so no further sync needed
|
|
4345
|
+
const mainSyncDataSet = createDataSet('main', 16, 1100);
|
|
4346
|
+
mainSyncDataSet.root = parser.dataSets.main.hashTree.getRootHash();
|
|
4347
|
+
mockSendSyncRequestResponse(mainUrl, {
|
|
4348
|
+
dataSets: [mainSyncDataSet],
|
|
4349
|
+
visibleDataSetsUrl,
|
|
4350
|
+
locusUrl,
|
|
4351
|
+
locusStateElements: [],
|
|
4352
|
+
});
|
|
4353
|
+
|
|
4354
|
+
// Mock POST sync for self (leafCount=1, skips GET hashtree)
|
|
4355
|
+
const selfSyncDataSet = createDataSet('self', 1, 2100);
|
|
4356
|
+
selfSyncDataSet.root = parser.dataSets.self.hashTree.getRootHash();
|
|
4357
|
+
mockSendSyncRequestResponse(selfUrl, {
|
|
4358
|
+
dataSets: [selfSyncDataSet],
|
|
4359
|
+
visibleDataSetsUrl,
|
|
4360
|
+
locusUrl,
|
|
4361
|
+
locusStateElements: [],
|
|
4362
|
+
});
|
|
4363
|
+
|
|
4364
|
+
await parser.syncAllDatasets();
|
|
4365
|
+
|
|
4366
|
+
// Verify GET hashtree was called for main only (not self, because leafCount=1)
|
|
4367
|
+
assert.calledWith(webexRequest, sinon.match({method: 'GET', uri: `${mainUrl}/hashtree`}));
|
|
4368
|
+
assert.neverCalledWith(webexRequest, sinon.match({method: 'GET', uri: `${selfUrl}/hashtree`}));
|
|
4369
|
+
|
|
4370
|
+
// Verify POST sync was called for both
|
|
4371
|
+
assert.calledWith(webexRequest, sinon.match({method: 'POST', uri: `${mainUrl}/sync`}));
|
|
4372
|
+
assert.calledWith(webexRequest, sinon.match({method: 'POST', uri: `${selfUrl}/sync`}));
|
|
4373
|
+
|
|
4374
|
+
// Verify main was synced before self (priority order)
|
|
4375
|
+
const mainSyncCallIndex = webexRequest.args.findIndex(
|
|
4376
|
+
(args) => args[0]?.method === 'GET' && args[0]?.uri === `${mainUrl}/hashtree`
|
|
4377
|
+
);
|
|
4378
|
+
const selfSyncCallIndex = webexRequest.args.findIndex(
|
|
4379
|
+
(args) => args[0]?.method === 'POST' && args[0]?.uri === `${selfUrl}/sync`
|
|
4380
|
+
);
|
|
4381
|
+
expect(mainSyncCallIndex).to.be.lessThan(selfSyncCallIndex);
|
|
4382
|
+
|
|
4383
|
+
// Verify isSyncAllInProgress is reset
|
|
4384
|
+
expect(parser.isSyncAllInProgress).to.be.false;
|
|
4385
|
+
});
|
|
4386
|
+
|
|
4387
|
+
it('should return immediately when state is stopped', async () => {
|
|
4388
|
+
const parser = createHashTreeParser();
|
|
4389
|
+
parser.stop();
|
|
4390
|
+
|
|
4391
|
+
await parser.syncAllDatasets();
|
|
4392
|
+
|
|
4393
|
+
// No sync requests should have been made (only the initial sync from constructor)
|
|
4394
|
+
// Reset history to clear constructor calls then verify
|
|
4395
|
+
const callCountBefore = webexRequest.callCount;
|
|
4396
|
+
await parser.syncAllDatasets();
|
|
4397
|
+
assert.equal(webexRequest.callCount, callCountBefore);
|
|
4398
|
+
});
|
|
4399
|
+
|
|
4400
|
+
it('should guard against concurrent calls', async () => {
|
|
4401
|
+
const parser = createHashTreeParser();
|
|
4402
|
+
|
|
4403
|
+
const mainUrl = parser.dataSets.main.url;
|
|
4404
|
+
const selfUrl = parser.dataSets.self.url;
|
|
4405
|
+
|
|
4406
|
+
// Use a deferred promise for the main sync to control timing
|
|
4407
|
+
let resolveMainSync;
|
|
4408
|
+
webexRequest
|
|
4409
|
+
.withArgs(sinon.match({method: 'GET', uri: `${mainUrl}/hashtree`}))
|
|
4410
|
+
.returns(new Promise((resolve) => { resolveMainSync = resolve; }));
|
|
4411
|
+
|
|
4412
|
+
mockSendSyncRequestResponse(mainUrl, {
|
|
4413
|
+
dataSets: [createDataSet('main', 16, 1100)],
|
|
4414
|
+
visibleDataSetsUrl,
|
|
4415
|
+
locusUrl,
|
|
4416
|
+
locusStateElements: [],
|
|
4417
|
+
});
|
|
4418
|
+
|
|
4419
|
+
mockSendSyncRequestResponse(selfUrl, {
|
|
4420
|
+
dataSets: [createDataSet('self', 1, 2100)],
|
|
4421
|
+
visibleDataSetsUrl,
|
|
4422
|
+
locusUrl,
|
|
4423
|
+
locusStateElements: [],
|
|
4424
|
+
});
|
|
4425
|
+
|
|
4426
|
+
// Start first call
|
|
4427
|
+
const promise1 = parser.syncAllDatasets();
|
|
4428
|
+
// Start second call while first is in progress
|
|
4429
|
+
const promise2 = parser.syncAllDatasets();
|
|
4430
|
+
|
|
4431
|
+
// Resolve the pending request
|
|
4432
|
+
resolveMainSync({
|
|
4433
|
+
body: {
|
|
4434
|
+
hashes: new Array(16).fill(EMPTY_HASH),
|
|
4435
|
+
dataSet: createDataSet('main', 16, 1100),
|
|
4436
|
+
},
|
|
4437
|
+
});
|
|
4438
|
+
|
|
4439
|
+
await promise1;
|
|
4440
|
+
await promise2;
|
|
4441
|
+
|
|
4442
|
+
// GET hashtree for main should only be called once (second syncAllDatasets returned immediately)
|
|
4443
|
+
const getHashtreeCalls = webexRequest.args.filter(
|
|
4444
|
+
(args) => args[0]?.method === 'GET' && args[0]?.uri === `${mainUrl}/hashtree`
|
|
4445
|
+
);
|
|
4446
|
+
expect(getHashtreeCalls).to.have.lengthOf(1);
|
|
4447
|
+
});
|
|
4448
|
+
|
|
4449
|
+
it('should skip datasets that do not have a hash tree', async () => {
|
|
4450
|
+
// Create parser with metadata that only has main and self as visible (not atd-unmuted)
|
|
4451
|
+
const metadataWithoutAtd = {
|
|
4452
|
+
...exampleMetadata,
|
|
4453
|
+
visibleDataSets: exampleMetadata.visibleDataSets.filter((ds) => ds.name !== 'atd-unmuted'),
|
|
4454
|
+
};
|
|
4455
|
+
const parser = createHashTreeParser(exampleInitialLocus, metadataWithoutAtd);
|
|
4456
|
+
|
|
4457
|
+
// atd-unmuted is in dataSets but has no hashTree (not visible)
|
|
4458
|
+
expect(parser.dataSets['atd-unmuted']).to.exist;
|
|
4459
|
+
expect(parser.dataSets['atd-unmuted'].hashTree).to.be.undefined;
|
|
4460
|
+
|
|
4461
|
+
const atdUrl = parser.dataSets['atd-unmuted'].url;
|
|
4462
|
+
const mainUrl = parser.dataSets.main.url;
|
|
4463
|
+
const selfUrl = parser.dataSets.self.url;
|
|
4464
|
+
|
|
4465
|
+
mockGetHashesFromLocusResponse(
|
|
4466
|
+
mainUrl,
|
|
4467
|
+
new Array(16).fill(EMPTY_HASH),
|
|
4468
|
+
createDataSet('main', 16, 1100)
|
|
4469
|
+
);
|
|
4470
|
+
|
|
4471
|
+
const mainSyncDs = createDataSet('main', 16, 1100);
|
|
4472
|
+
mainSyncDs.root = parser.dataSets.main.hashTree.getRootHash();
|
|
4473
|
+
mockSendSyncRequestResponse(mainUrl, {
|
|
4474
|
+
dataSets: [mainSyncDs],
|
|
4475
|
+
visibleDataSetsUrl,
|
|
4476
|
+
locusUrl,
|
|
4477
|
+
locusStateElements: [],
|
|
4478
|
+
});
|
|
4479
|
+
|
|
4480
|
+
const selfSyncDs = createDataSet('self', 1, 2100);
|
|
4481
|
+
selfSyncDs.root = parser.dataSets.self.hashTree.getRootHash();
|
|
4482
|
+
mockSendSyncRequestResponse(selfUrl, {
|
|
4483
|
+
dataSets: [selfSyncDs],
|
|
4484
|
+
visibleDataSetsUrl,
|
|
4485
|
+
locusUrl,
|
|
4486
|
+
locusStateElements: [],
|
|
4487
|
+
});
|
|
4488
|
+
|
|
4489
|
+
await parser.syncAllDatasets();
|
|
4490
|
+
|
|
4491
|
+
// No requests should have been made for atd-unmuted
|
|
4492
|
+
assert.neverCalledWith(webexRequest, sinon.match({uri: sinon.match(atdUrl)}));
|
|
4493
|
+
});
|
|
4494
|
+
});
|
|
4495
|
+
|
|
4496
|
+
describe('#handleMessage sync queue', () => {
|
|
4497
|
+
it('should deduplicate: not sync the same dataset twice when enqueued multiple times', async () => {
|
|
4498
|
+
const parser = createHashTreeParser();
|
|
4499
|
+
|
|
4500
|
+
const mainUrl = parser.dataSets.main.url;
|
|
4501
|
+
|
|
4502
|
+
// Setup mocks before triggering syncs
|
|
4503
|
+
mockGetHashesFromLocusResponse(
|
|
4504
|
+
mainUrl,
|
|
4505
|
+
new Array(16).fill(EMPTY_HASH),
|
|
4506
|
+
createDataSet('main', 16, 1101)
|
|
4507
|
+
);
|
|
4508
|
+
|
|
4509
|
+
const mainSyncDs = createDataSet('main', 16, 1101);
|
|
4510
|
+
mainSyncDs.root = parser.dataSets.main.hashTree.getRootHash();
|
|
4511
|
+
mockSendSyncRequestResponse(mainUrl, {
|
|
4512
|
+
dataSets: [mainSyncDs],
|
|
4513
|
+
visibleDataSetsUrl,
|
|
4514
|
+
locusUrl,
|
|
4515
|
+
locusStateElements: [],
|
|
4516
|
+
});
|
|
4517
|
+
|
|
4518
|
+
// Send two heartbeat messages (no locusStateElements) with different root hashes for main
|
|
4519
|
+
parser.handleMessage(createHeartbeatMessage('main', 16, 1100, 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1'), 'first');
|
|
4520
|
+
parser.handleMessage(createHeartbeatMessage('main', 16, 1101, 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa2'), 'second');
|
|
4521
|
+
|
|
4522
|
+
// The second call resets the timer. After 1000ms, only one sync fires.
|
|
4523
|
+
await clock.tickAsync(1000);
|
|
4524
|
+
|
|
4525
|
+
// Only one GET hashtree call should have been made for main
|
|
4526
|
+
const getHashtreeCalls = webexRequest.args.filter(
|
|
4527
|
+
(args) => args[0]?.method === 'GET' && args[0]?.uri === `${mainUrl}/hashtree`
|
|
4528
|
+
);
|
|
4529
|
+
expect(getHashtreeCalls).to.have.lengthOf(1);
|
|
4530
|
+
});
|
|
4531
|
+
|
|
4532
|
+
it('should stop processing the sync queue when parser is stopped mid-queue', async () => {
|
|
4533
|
+
const parser = createHashTreeParser();
|
|
4534
|
+
|
|
4535
|
+
const mainUrl = parser.dataSets.main.url;
|
|
4536
|
+
const selfUrl = parser.dataSets.self.url;
|
|
4537
|
+
|
|
4538
|
+
// Mock main GET hashtree with a deferred promise so we can control when it resolves
|
|
4539
|
+
let resolveMainHashtree;
|
|
4540
|
+
webexRequest
|
|
4541
|
+
.withArgs(sinon.match({method: 'GET', uri: `${mainUrl}/hashtree`}))
|
|
4542
|
+
.callsFake(() => new Promise((resolve) => { resolveMainHashtree = resolve; }));
|
|
4543
|
+
|
|
4544
|
+
// Send a heartbeat message that triggers sync timers for both main and self
|
|
4545
|
+
parser.handleMessage(
|
|
4546
|
+
createHeartbeatMessage('main', 16, 1100, 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1'),
|
|
4547
|
+
'trigger main sync'
|
|
4548
|
+
);
|
|
4549
|
+
parser.handleMessage(
|
|
4550
|
+
createHeartbeatMessage('self', 1, 2100, 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb1'),
|
|
4551
|
+
'trigger self sync'
|
|
4552
|
+
);
|
|
4553
|
+
|
|
4554
|
+
// Fire the timers - main sync starts (calls GET hashtree, which blocks)
|
|
4555
|
+
await clock.tickAsync(1000);
|
|
4556
|
+
|
|
4557
|
+
// Stop the parser while main sync is in progress
|
|
4558
|
+
parser.stop();
|
|
4559
|
+
|
|
4560
|
+
// Resolve the pending main GET request
|
|
4561
|
+
resolveMainHashtree({
|
|
4562
|
+
body: {
|
|
4563
|
+
hashes: new Array(16).fill(EMPTY_HASH),
|
|
4564
|
+
dataSet: createDataSet('main', 16, 1100),
|
|
4565
|
+
},
|
|
4566
|
+
});
|
|
4567
|
+
|
|
4568
|
+
await clock.tickAsync(0);
|
|
4569
|
+
|
|
4570
|
+
// Self sync should NOT have been triggered because parser was stopped
|
|
4571
|
+
assert.neverCalledWith(webexRequest, sinon.match({method: 'POST', uri: `${selfUrl}/sync`}));
|
|
4572
|
+
assert.neverCalledWith(webexRequest, sinon.match({method: 'GET', uri: `${selfUrl}/hashtree`}));
|
|
4573
|
+
});
|
|
4574
|
+
});
|
|
4575
|
+
|
|
4576
|
+
describe('#stop sync queue', () => {
|
|
4577
|
+
it('should clear the syncQueue when stopped so remaining queued items are not processed', async () => {
|
|
4578
|
+
const parser = createHashTreeParser();
|
|
4579
|
+
|
|
4580
|
+
const mainUrl = parser.dataSets.main.url;
|
|
4581
|
+
const selfUrl = parser.dataSets.self.url;
|
|
4582
|
+
|
|
4583
|
+
// Mock main GET hashtree with a deferred promise so we can control when it resolves
|
|
4584
|
+
let resolveMainHashtree;
|
|
4585
|
+
webexRequest
|
|
4586
|
+
.withArgs(sinon.match({method: 'GET', uri: `${mainUrl}/hashtree`}))
|
|
4587
|
+
.callsFake(() => new Promise((resolve) => { resolveMainHashtree = resolve; }));
|
|
4588
|
+
|
|
4589
|
+
// Enqueue syncs for both main and self by sending heartbeat messages
|
|
4590
|
+
parser.handleMessage(
|
|
4591
|
+
createHeartbeatMessage('main', 16, 1100, 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1'),
|
|
4592
|
+
'trigger main sync'
|
|
4593
|
+
);
|
|
4594
|
+
parser.handleMessage(
|
|
4595
|
+
createHeartbeatMessage('self', 1, 2100, 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb1'),
|
|
4596
|
+
'trigger self sync'
|
|
4597
|
+
);
|
|
4598
|
+
|
|
4599
|
+
// Fire the timers - main sync starts and blocks on GET hashtree
|
|
4600
|
+
await clock.tickAsync(1000);
|
|
4601
|
+
|
|
4602
|
+
// Verify that self is still in the queue (main is being processed, self is waiting)
|
|
4603
|
+
// Now stop the parser - this should clear the syncQueue
|
|
4604
|
+
parser.stop();
|
|
4605
|
+
|
|
4606
|
+
// Resolve the pending main GET request so the in-flight sync can finish
|
|
4607
|
+
resolveMainHashtree({
|
|
4608
|
+
body: {
|
|
4609
|
+
hashes: new Array(16).fill(EMPTY_HASH),
|
|
4610
|
+
dataSet: createDataSet('main', 16, 1100),
|
|
4611
|
+
},
|
|
4612
|
+
});
|
|
4613
|
+
|
|
4614
|
+
await clock.tickAsync(0);
|
|
4615
|
+
|
|
4616
|
+
// Self should never have been synced because stop() cleared the queue
|
|
4617
|
+
const selfGetCalls = webexRequest.args.filter(
|
|
4618
|
+
(args) => args[0]?.method === 'GET' && args[0]?.uri === `${selfUrl}/hashtree`
|
|
4619
|
+
);
|
|
4620
|
+
expect(selfGetCalls).to.have.lengthOf(0);
|
|
4621
|
+
});
|
|
4622
|
+
});
|
|
4623
|
+
|
|
4624
|
+
describe('#cleanUp', () => {
|
|
4625
|
+
it('should stop the parser, clear all timers and clear all dataSets', () => {
|
|
4626
|
+
const parser = createHashTreeParser();
|
|
4627
|
+
|
|
4628
|
+
// Send a message to set up sync timers via runSyncAlgorithm
|
|
4629
|
+
const message = {
|
|
4630
|
+
dataSets: [
|
|
4631
|
+
{
|
|
4632
|
+
...createDataSet('main', 16, 1100),
|
|
4633
|
+
root: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1',
|
|
4634
|
+
},
|
|
4635
|
+
],
|
|
4636
|
+
visibleDataSetsUrl,
|
|
4637
|
+
locusUrl,
|
|
4638
|
+
heartbeatIntervalMs: 5000,
|
|
4639
|
+
locusStateElements: [
|
|
4640
|
+
{
|
|
4641
|
+
htMeta: {
|
|
4642
|
+
elementId: {type: 'locus' as const, id: 0, version: 201},
|
|
4643
|
+
dataSetNames: ['main'],
|
|
4644
|
+
},
|
|
4645
|
+
data: {someData: 'value'},
|
|
4646
|
+
},
|
|
4647
|
+
],
|
|
4648
|
+
};
|
|
4649
|
+
|
|
4650
|
+
parser.handleMessage(message, 'setup timers');
|
|
4651
|
+
|
|
4652
|
+
// Verify timers were set by handleMessage
|
|
4653
|
+
expect(parser.dataSets.main.timer).to.not.be.undefined;
|
|
4654
|
+
expect(parser.dataSets.main.heartbeatWatchdogTimer).to.not.be.undefined;
|
|
4655
|
+
|
|
4656
|
+
parser.cleanUp();
|
|
4657
|
+
|
|
4658
|
+
expect(parser.state).to.equal('stopped');
|
|
4659
|
+
expect(parser.visibleDataSets).to.deep.equal([]);
|
|
4660
|
+
expect(parser.dataSets).to.deep.equal({});
|
|
4661
|
+
});
|
|
4662
|
+
});
|
|
3560
4663
|
});
|