@webex/plugin-meetings 3.12.0-next.20 → 3.12.0-next.22
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 +1 -1
- package/dist/breakouts/breakout.js +1 -1
- package/dist/breakouts/index.js +1 -1
- package/dist/hashTree/hashTreeParser.js +480 -320
- package/dist/hashTree/hashTreeParser.js.map +1 -1
- package/dist/interpretation/index.js +1 -1
- package/dist/interpretation/siLanguage.js +1 -1
- package/dist/locus-info/index.js +170 -36
- package/dist/locus-info/index.js.map +1 -1
- package/dist/meetings/index.js +130 -45
- package/dist/meetings/index.js.map +1 -1
- package/dist/types/hashTree/hashTreeParser.d.ts +40 -12
- package/dist/types/locus-info/index.d.ts +29 -0
- package/dist/webinar/index.js +1 -1
- package/package.json +14 -14
- package/src/hashTree/hashTreeParser.ts +182 -97
- package/src/locus-info/index.ts +176 -48
- package/src/meetings/index.ts +42 -17
- package/test/unit/spec/hashTree/hashTreeParser.ts +372 -7
- package/test/unit/spec/locus-info/index.js +179 -4
- package/test/unit/spec/meetings/index.js +127 -5
|
@@ -567,6 +567,11 @@ describe('HashTreeParser', () => {
|
|
|
567
567
|
},
|
|
568
568
|
data: {info: {id: 'some-fake-locus-info'}},
|
|
569
569
|
},
|
|
570
|
+
],
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
assert.calledWith(callback, {updateType: LocusInfoUpdateType.OBJECTS_UPDATED,
|
|
574
|
+
updatedObjects: [
|
|
570
575
|
{
|
|
571
576
|
htMeta: {
|
|
572
577
|
elementId: {
|
|
@@ -3775,7 +3780,7 @@ describe('HashTreeParser', () => {
|
|
|
3775
3780
|
});
|
|
3776
3781
|
});
|
|
3777
3782
|
|
|
3778
|
-
describe('#
|
|
3783
|
+
describe('#resumeFromMessage', () => {
|
|
3779
3784
|
const createResumeMessage = (visibleDataSets?, dataSets?) => ({
|
|
3780
3785
|
locusUrl,
|
|
3781
3786
|
visibleDataSetsUrl,
|
|
@@ -3802,7 +3807,7 @@ describe('HashTreeParser', () => {
|
|
|
3802
3807
|
|
|
3803
3808
|
expect(parser.state).to.equal('stopped');
|
|
3804
3809
|
|
|
3805
|
-
parser.
|
|
3810
|
+
parser.resumeFromMessage(createResumeMessage());
|
|
3806
3811
|
|
|
3807
3812
|
expect(parser.state).to.equal('active');
|
|
3808
3813
|
});
|
|
@@ -3811,7 +3816,7 @@ describe('HashTreeParser', () => {
|
|
|
3811
3816
|
const parser = createHashTreeParser();
|
|
3812
3817
|
parser.stop();
|
|
3813
3818
|
|
|
3814
|
-
parser.
|
|
3819
|
+
parser.resumeFromMessage({
|
|
3815
3820
|
locusUrl,
|
|
3816
3821
|
visibleDataSetsUrl,
|
|
3817
3822
|
dataSets: [createDataSet('main', 16, 2000)],
|
|
@@ -3830,7 +3835,7 @@ describe('HashTreeParser', () => {
|
|
|
3830
3835
|
createDataSet('self', 2, 6000),
|
|
3831
3836
|
];
|
|
3832
3837
|
|
|
3833
|
-
parser.
|
|
3838
|
+
parser.resumeFromMessage(createResumeMessage(undefined, newDataSets));
|
|
3834
3839
|
|
|
3835
3840
|
expect(Object.keys(parser.dataSets)).to.have.lengthOf(2);
|
|
3836
3841
|
expect(parser.dataSets.main.leafCount).to.equal(8);
|
|
@@ -3852,7 +3857,7 @@ describe('HashTreeParser', () => {
|
|
|
3852
3857
|
{name: 'self', url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/713e9f99/datasets/self'},
|
|
3853
3858
|
];
|
|
3854
3859
|
|
|
3855
|
-
parser.
|
|
3860
|
+
parser.resumeFromMessage(createResumeMessage(visibleDataSets, dataSets));
|
|
3856
3861
|
|
|
3857
3862
|
expect(parser.dataSets.main.hashTree).to.be.instanceOf(HashTree);
|
|
3858
3863
|
expect(parser.dataSets.self.hashTree).to.be.instanceOf(HashTree);
|
|
@@ -3866,7 +3871,7 @@ describe('HashTreeParser', () => {
|
|
|
3866
3871
|
const handleMessageStub = sinon.stub(parser, 'handleMessage');
|
|
3867
3872
|
|
|
3868
3873
|
const message = createResumeMessage();
|
|
3869
|
-
parser.
|
|
3874
|
+
parser.resumeFromMessage(message);
|
|
3870
3875
|
|
|
3871
3876
|
assert.calledOnceWithExactly(handleMessageStub, message, 'on resume');
|
|
3872
3877
|
});
|
|
@@ -3886,7 +3891,7 @@ describe('HashTreeParser', () => {
|
|
|
3886
3891
|
{name: 'atd-unmuted', url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/atd-unmuted'},
|
|
3887
3892
|
];
|
|
3888
3893
|
|
|
3889
|
-
parser.
|
|
3894
|
+
parser.resumeFromMessage(createResumeMessage(visibleDataSets, dataSets));
|
|
3890
3895
|
|
|
3891
3896
|
expect(parser.visibleDataSets.some((vds) => vds.name === 'atd-unmuted')).to.be.false;
|
|
3892
3897
|
expect(parser.visibleDataSets.some((vds) => vds.name === 'main')).to.be.true;
|
|
@@ -3894,6 +3899,67 @@ describe('HashTreeParser', () => {
|
|
|
3894
3899
|
});
|
|
3895
3900
|
});
|
|
3896
3901
|
|
|
3902
|
+
describe('#resumeFromApiResponse', () => {
|
|
3903
|
+
const exampleLocus = {
|
|
3904
|
+
participants: [],
|
|
3905
|
+
} as any;
|
|
3906
|
+
|
|
3907
|
+
it('should set state to active', async () => {
|
|
3908
|
+
const parser = createHashTreeParser();
|
|
3909
|
+
parser.stop();
|
|
3910
|
+
|
|
3911
|
+
expect(parser.state).to.equal('stopped');
|
|
3912
|
+
|
|
3913
|
+
sinon.stub(parser, 'initializeFromGetLociResponse').resolves();
|
|
3914
|
+
|
|
3915
|
+
await parser.resumeFromApiResponse(exampleLocus);
|
|
3916
|
+
|
|
3917
|
+
expect(parser.state).to.equal('active');
|
|
3918
|
+
});
|
|
3919
|
+
|
|
3920
|
+
it('should reset dataSets to empty', async () => {
|
|
3921
|
+
const parser = createHashTreeParser();
|
|
3922
|
+
|
|
3923
|
+
expect(Object.keys(parser.dataSets).length).to.be.greaterThan(0);
|
|
3924
|
+
|
|
3925
|
+
parser.stop();
|
|
3926
|
+
|
|
3927
|
+
sinon.stub(parser, 'initializeFromGetLociResponse').resolves();
|
|
3928
|
+
|
|
3929
|
+
await parser.resumeFromApiResponse(exampleLocus);
|
|
3930
|
+
|
|
3931
|
+
expect(parser.dataSets).to.deep.equal({});
|
|
3932
|
+
});
|
|
3933
|
+
|
|
3934
|
+
it('should call initializeFromGetLociResponse with the provided locus', async () => {
|
|
3935
|
+
const parser = createHashTreeParser();
|
|
3936
|
+
parser.stop();
|
|
3937
|
+
|
|
3938
|
+
const initStub = sinon.stub(parser, 'initializeFromGetLociResponse').resolves();
|
|
3939
|
+
|
|
3940
|
+
await parser.resumeFromApiResponse(exampleLocus);
|
|
3941
|
+
|
|
3942
|
+
assert.calledOnceWithExactly(initStub, exampleLocus);
|
|
3943
|
+
});
|
|
3944
|
+
|
|
3945
|
+
it('should propagate errors from initializeFromGetLociResponse', async () => {
|
|
3946
|
+
const parser = createHashTreeParser();
|
|
3947
|
+
parser.stop();
|
|
3948
|
+
|
|
3949
|
+
const error = new Error('initialization failed');
|
|
3950
|
+
const initStub = sinon.stub(parser, 'initializeFromGetLociResponse').rejects(error);
|
|
3951
|
+
|
|
3952
|
+
let caughtError: Error | undefined;
|
|
3953
|
+
try {
|
|
3954
|
+
await parser.resumeFromApiResponse(exampleLocus);
|
|
3955
|
+
} catch (e) {
|
|
3956
|
+
caughtError = e;
|
|
3957
|
+
}
|
|
3958
|
+
|
|
3959
|
+
expect(caughtError).to.equal(error);
|
|
3960
|
+
});
|
|
3961
|
+
});
|
|
3962
|
+
|
|
3897
3963
|
describe('#handleLocusUpdate when stopped', () => {
|
|
3898
3964
|
it('should return early without processing when parser is stopped', () => {
|
|
3899
3965
|
const parser = createHashTreeParser();
|
|
@@ -3929,6 +3995,305 @@ describe('HashTreeParser', () => {
|
|
|
3929
3995
|
});
|
|
3930
3996
|
});
|
|
3931
3997
|
|
|
3998
|
+
describe('#syncAllDatasets', () => {
|
|
3999
|
+
it('should sync all datasets that have hash trees in priority order', async () => {
|
|
4000
|
+
const parser = createHashTreeParser();
|
|
4001
|
+
|
|
4002
|
+
// parser starts with main (leafCount=16) and self (leafCount=1) as visible datasets with hash trees
|
|
4003
|
+
// atd-unmuted has no hash tree (not visible)
|
|
4004
|
+
expect(parser.dataSets.main.hashTree).to.be.instanceOf(HashTree);
|
|
4005
|
+
expect(parser.dataSets.self.hashTree).to.be.instanceOf(HashTree);
|
|
4006
|
+
|
|
4007
|
+
const mainUrl = parser.dataSets.main.url;
|
|
4008
|
+
const selfUrl = parser.dataSets.self.url;
|
|
4009
|
+
|
|
4010
|
+
// Mock GET hashtree for main (leafCount > 1, so it does GET first)
|
|
4011
|
+
mockGetHashesFromLocusResponse(
|
|
4012
|
+
mainUrl,
|
|
4013
|
+
new Array(16).fill(EMPTY_HASH),
|
|
4014
|
+
createDataSet('main', 16, 1100)
|
|
4015
|
+
);
|
|
4016
|
+
|
|
4017
|
+
// Mock POST sync for main - return matching root hash so no further sync needed
|
|
4018
|
+
const mainSyncDataSet = createDataSet('main', 16, 1100);
|
|
4019
|
+
mainSyncDataSet.root = parser.dataSets.main.hashTree.getRootHash();
|
|
4020
|
+
mockSendSyncRequestResponse(mainUrl, {
|
|
4021
|
+
dataSets: [mainSyncDataSet],
|
|
4022
|
+
visibleDataSetsUrl,
|
|
4023
|
+
locusUrl,
|
|
4024
|
+
locusStateElements: [],
|
|
4025
|
+
});
|
|
4026
|
+
|
|
4027
|
+
// Mock POST sync for self (leafCount=1, skips GET hashtree)
|
|
4028
|
+
const selfSyncDataSet = createDataSet('self', 1, 2100);
|
|
4029
|
+
selfSyncDataSet.root = parser.dataSets.self.hashTree.getRootHash();
|
|
4030
|
+
mockSendSyncRequestResponse(selfUrl, {
|
|
4031
|
+
dataSets: [selfSyncDataSet],
|
|
4032
|
+
visibleDataSetsUrl,
|
|
4033
|
+
locusUrl,
|
|
4034
|
+
locusStateElements: [],
|
|
4035
|
+
});
|
|
4036
|
+
|
|
4037
|
+
await parser.syncAllDatasets();
|
|
4038
|
+
|
|
4039
|
+
// Verify GET hashtree was called for main only (not self, because leafCount=1)
|
|
4040
|
+
assert.calledWith(webexRequest, sinon.match({method: 'GET', uri: `${mainUrl}/hashtree`}));
|
|
4041
|
+
assert.neverCalledWith(webexRequest, sinon.match({method: 'GET', uri: `${selfUrl}/hashtree`}));
|
|
4042
|
+
|
|
4043
|
+
// Verify POST sync was called for both
|
|
4044
|
+
assert.calledWith(webexRequest, sinon.match({method: 'POST', uri: `${mainUrl}/sync`}));
|
|
4045
|
+
assert.calledWith(webexRequest, sinon.match({method: 'POST', uri: `${selfUrl}/sync`}));
|
|
4046
|
+
|
|
4047
|
+
// Verify main was synced before self (priority order)
|
|
4048
|
+
const mainSyncCallIndex = webexRequest.args.findIndex(
|
|
4049
|
+
(args) => args[0]?.method === 'GET' && args[0]?.uri === `${mainUrl}/hashtree`
|
|
4050
|
+
);
|
|
4051
|
+
const selfSyncCallIndex = webexRequest.args.findIndex(
|
|
4052
|
+
(args) => args[0]?.method === 'POST' && args[0]?.uri === `${selfUrl}/sync`
|
|
4053
|
+
);
|
|
4054
|
+
expect(mainSyncCallIndex).to.be.lessThan(selfSyncCallIndex);
|
|
4055
|
+
|
|
4056
|
+
// Verify isSyncAllInProgress is reset
|
|
4057
|
+
expect(parser.isSyncAllInProgress).to.be.false;
|
|
4058
|
+
});
|
|
4059
|
+
|
|
4060
|
+
it('should return immediately when state is stopped', async () => {
|
|
4061
|
+
const parser = createHashTreeParser();
|
|
4062
|
+
parser.stop();
|
|
4063
|
+
|
|
4064
|
+
await parser.syncAllDatasets();
|
|
4065
|
+
|
|
4066
|
+
// No sync requests should have been made (only the initial sync from constructor)
|
|
4067
|
+
// Reset history to clear constructor calls then verify
|
|
4068
|
+
const callCountBefore = webexRequest.callCount;
|
|
4069
|
+
await parser.syncAllDatasets();
|
|
4070
|
+
assert.equal(webexRequest.callCount, callCountBefore);
|
|
4071
|
+
});
|
|
4072
|
+
|
|
4073
|
+
it('should guard against concurrent calls', async () => {
|
|
4074
|
+
const parser = createHashTreeParser();
|
|
4075
|
+
|
|
4076
|
+
const mainUrl = parser.dataSets.main.url;
|
|
4077
|
+
const selfUrl = parser.dataSets.self.url;
|
|
4078
|
+
|
|
4079
|
+
// Use a deferred promise for the main sync to control timing
|
|
4080
|
+
let resolveMainSync;
|
|
4081
|
+
webexRequest
|
|
4082
|
+
.withArgs(sinon.match({method: 'GET', uri: `${mainUrl}/hashtree`}))
|
|
4083
|
+
.returns(new Promise((resolve) => { resolveMainSync = resolve; }));
|
|
4084
|
+
|
|
4085
|
+
mockSendSyncRequestResponse(mainUrl, {
|
|
4086
|
+
dataSets: [createDataSet('main', 16, 1100)],
|
|
4087
|
+
visibleDataSetsUrl,
|
|
4088
|
+
locusUrl,
|
|
4089
|
+
locusStateElements: [],
|
|
4090
|
+
});
|
|
4091
|
+
|
|
4092
|
+
mockSendSyncRequestResponse(selfUrl, {
|
|
4093
|
+
dataSets: [createDataSet('self', 1, 2100)],
|
|
4094
|
+
visibleDataSetsUrl,
|
|
4095
|
+
locusUrl,
|
|
4096
|
+
locusStateElements: [],
|
|
4097
|
+
});
|
|
4098
|
+
|
|
4099
|
+
// Start first call
|
|
4100
|
+
const promise1 = parser.syncAllDatasets();
|
|
4101
|
+
// Start second call while first is in progress
|
|
4102
|
+
const promise2 = parser.syncAllDatasets();
|
|
4103
|
+
|
|
4104
|
+
// Resolve the pending request
|
|
4105
|
+
resolveMainSync({
|
|
4106
|
+
body: {
|
|
4107
|
+
hashes: new Array(16).fill(EMPTY_HASH),
|
|
4108
|
+
dataSet: createDataSet('main', 16, 1100),
|
|
4109
|
+
},
|
|
4110
|
+
});
|
|
4111
|
+
|
|
4112
|
+
await promise1;
|
|
4113
|
+
await promise2;
|
|
4114
|
+
|
|
4115
|
+
// GET hashtree for main should only be called once (second syncAllDatasets returned immediately)
|
|
4116
|
+
const getHashtreeCalls = webexRequest.args.filter(
|
|
4117
|
+
(args) => args[0]?.method === 'GET' && args[0]?.uri === `${mainUrl}/hashtree`
|
|
4118
|
+
);
|
|
4119
|
+
expect(getHashtreeCalls).to.have.lengthOf(1);
|
|
4120
|
+
});
|
|
4121
|
+
|
|
4122
|
+
it('should skip datasets that do not have a hash tree', async () => {
|
|
4123
|
+
// Create parser with metadata that only has main and self as visible (not atd-unmuted)
|
|
4124
|
+
const metadataWithoutAtd = {
|
|
4125
|
+
...exampleMetadata,
|
|
4126
|
+
visibleDataSets: exampleMetadata.visibleDataSets.filter((ds) => ds.name !== 'atd-unmuted'),
|
|
4127
|
+
};
|
|
4128
|
+
const parser = createHashTreeParser(exampleInitialLocus, metadataWithoutAtd);
|
|
4129
|
+
|
|
4130
|
+
// atd-unmuted is in dataSets but has no hashTree (not visible)
|
|
4131
|
+
expect(parser.dataSets['atd-unmuted']).to.exist;
|
|
4132
|
+
expect(parser.dataSets['atd-unmuted'].hashTree).to.be.undefined;
|
|
4133
|
+
|
|
4134
|
+
const atdUrl = parser.dataSets['atd-unmuted'].url;
|
|
4135
|
+
const mainUrl = parser.dataSets.main.url;
|
|
4136
|
+
const selfUrl = parser.dataSets.self.url;
|
|
4137
|
+
|
|
4138
|
+
mockGetHashesFromLocusResponse(
|
|
4139
|
+
mainUrl,
|
|
4140
|
+
new Array(16).fill(EMPTY_HASH),
|
|
4141
|
+
createDataSet('main', 16, 1100)
|
|
4142
|
+
);
|
|
4143
|
+
|
|
4144
|
+
const mainSyncDs = createDataSet('main', 16, 1100);
|
|
4145
|
+
mainSyncDs.root = parser.dataSets.main.hashTree.getRootHash();
|
|
4146
|
+
mockSendSyncRequestResponse(mainUrl, {
|
|
4147
|
+
dataSets: [mainSyncDs],
|
|
4148
|
+
visibleDataSetsUrl,
|
|
4149
|
+
locusUrl,
|
|
4150
|
+
locusStateElements: [],
|
|
4151
|
+
});
|
|
4152
|
+
|
|
4153
|
+
const selfSyncDs = createDataSet('self', 1, 2100);
|
|
4154
|
+
selfSyncDs.root = parser.dataSets.self.hashTree.getRootHash();
|
|
4155
|
+
mockSendSyncRequestResponse(selfUrl, {
|
|
4156
|
+
dataSets: [selfSyncDs],
|
|
4157
|
+
visibleDataSetsUrl,
|
|
4158
|
+
locusUrl,
|
|
4159
|
+
locusStateElements: [],
|
|
4160
|
+
});
|
|
4161
|
+
|
|
4162
|
+
await parser.syncAllDatasets();
|
|
4163
|
+
|
|
4164
|
+
// No requests should have been made for atd-unmuted
|
|
4165
|
+
assert.neverCalledWith(webexRequest, sinon.match({uri: sinon.match(atdUrl)}));
|
|
4166
|
+
});
|
|
4167
|
+
});
|
|
4168
|
+
|
|
4169
|
+
describe('#handleMessage sync queue', () => {
|
|
4170
|
+
it('should deduplicate: not sync the same dataset twice when enqueued multiple times', async () => {
|
|
4171
|
+
const parser = createHashTreeParser();
|
|
4172
|
+
|
|
4173
|
+
const mainUrl = parser.dataSets.main.url;
|
|
4174
|
+
|
|
4175
|
+
// Setup mocks before triggering syncs
|
|
4176
|
+
mockGetHashesFromLocusResponse(
|
|
4177
|
+
mainUrl,
|
|
4178
|
+
new Array(16).fill(EMPTY_HASH),
|
|
4179
|
+
createDataSet('main', 16, 1101)
|
|
4180
|
+
);
|
|
4181
|
+
|
|
4182
|
+
const mainSyncDs = createDataSet('main', 16, 1101);
|
|
4183
|
+
mainSyncDs.root = parser.dataSets.main.hashTree.getRootHash();
|
|
4184
|
+
mockSendSyncRequestResponse(mainUrl, {
|
|
4185
|
+
dataSets: [mainSyncDs],
|
|
4186
|
+
visibleDataSetsUrl,
|
|
4187
|
+
locusUrl,
|
|
4188
|
+
locusStateElements: [],
|
|
4189
|
+
});
|
|
4190
|
+
|
|
4191
|
+
// Send two heartbeat messages (no locusStateElements) with different root hashes for main
|
|
4192
|
+
parser.handleMessage(createHeartbeatMessage('main', 16, 1100, 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1'), 'first');
|
|
4193
|
+
parser.handleMessage(createHeartbeatMessage('main', 16, 1101, 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa2'), 'second');
|
|
4194
|
+
|
|
4195
|
+
// The second call resets the timer. After 1000ms, only one sync fires.
|
|
4196
|
+
await clock.tickAsync(1000);
|
|
4197
|
+
|
|
4198
|
+
// Only one GET hashtree call should have been made for main
|
|
4199
|
+
const getHashtreeCalls = webexRequest.args.filter(
|
|
4200
|
+
(args) => args[0]?.method === 'GET' && args[0]?.uri === `${mainUrl}/hashtree`
|
|
4201
|
+
);
|
|
4202
|
+
expect(getHashtreeCalls).to.have.lengthOf(1);
|
|
4203
|
+
});
|
|
4204
|
+
|
|
4205
|
+
it('should stop processing the sync queue when parser is stopped mid-queue', async () => {
|
|
4206
|
+
const parser = createHashTreeParser();
|
|
4207
|
+
|
|
4208
|
+
const mainUrl = parser.dataSets.main.url;
|
|
4209
|
+
const selfUrl = parser.dataSets.self.url;
|
|
4210
|
+
|
|
4211
|
+
// Mock main GET hashtree with a deferred promise so we can control when it resolves
|
|
4212
|
+
let resolveMainHashtree;
|
|
4213
|
+
webexRequest
|
|
4214
|
+
.withArgs(sinon.match({method: 'GET', uri: `${mainUrl}/hashtree`}))
|
|
4215
|
+
.callsFake(() => new Promise((resolve) => { resolveMainHashtree = resolve; }));
|
|
4216
|
+
|
|
4217
|
+
// Send a heartbeat message that triggers sync timers for both main and self
|
|
4218
|
+
parser.handleMessage(
|
|
4219
|
+
createHeartbeatMessage('main', 16, 1100, 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1'),
|
|
4220
|
+
'trigger main sync'
|
|
4221
|
+
);
|
|
4222
|
+
parser.handleMessage(
|
|
4223
|
+
createHeartbeatMessage('self', 1, 2100, 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb1'),
|
|
4224
|
+
'trigger self sync'
|
|
4225
|
+
);
|
|
4226
|
+
|
|
4227
|
+
// Fire the timers - main sync starts (calls GET hashtree, which blocks)
|
|
4228
|
+
await clock.tickAsync(1000);
|
|
4229
|
+
|
|
4230
|
+
// Stop the parser while main sync is in progress
|
|
4231
|
+
parser.stop();
|
|
4232
|
+
|
|
4233
|
+
// Resolve the pending main GET request
|
|
4234
|
+
resolveMainHashtree({
|
|
4235
|
+
body: {
|
|
4236
|
+
hashes: new Array(16).fill(EMPTY_HASH),
|
|
4237
|
+
dataSet: createDataSet('main', 16, 1100),
|
|
4238
|
+
},
|
|
4239
|
+
});
|
|
4240
|
+
|
|
4241
|
+
await clock.tickAsync(0);
|
|
4242
|
+
|
|
4243
|
+
// Self sync should NOT have been triggered because parser was stopped
|
|
4244
|
+
assert.neverCalledWith(webexRequest, sinon.match({method: 'POST', uri: `${selfUrl}/sync`}));
|
|
4245
|
+
assert.neverCalledWith(webexRequest, sinon.match({method: 'GET', uri: `${selfUrl}/hashtree`}));
|
|
4246
|
+
});
|
|
4247
|
+
});
|
|
4248
|
+
|
|
4249
|
+
describe('#stop sync queue', () => {
|
|
4250
|
+
it('should clear the syncQueue when stopped so remaining queued items are not processed', async () => {
|
|
4251
|
+
const parser = createHashTreeParser();
|
|
4252
|
+
|
|
4253
|
+
const mainUrl = parser.dataSets.main.url;
|
|
4254
|
+
const selfUrl = parser.dataSets.self.url;
|
|
4255
|
+
|
|
4256
|
+
// Mock main GET hashtree with a deferred promise so we can control when it resolves
|
|
4257
|
+
let resolveMainHashtree;
|
|
4258
|
+
webexRequest
|
|
4259
|
+
.withArgs(sinon.match({method: 'GET', uri: `${mainUrl}/hashtree`}))
|
|
4260
|
+
.callsFake(() => new Promise((resolve) => { resolveMainHashtree = resolve; }));
|
|
4261
|
+
|
|
4262
|
+
// Enqueue syncs for both main and self by sending heartbeat messages
|
|
4263
|
+
parser.handleMessage(
|
|
4264
|
+
createHeartbeatMessage('main', 16, 1100, 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1'),
|
|
4265
|
+
'trigger main sync'
|
|
4266
|
+
);
|
|
4267
|
+
parser.handleMessage(
|
|
4268
|
+
createHeartbeatMessage('self', 1, 2100, 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb1'),
|
|
4269
|
+
'trigger self sync'
|
|
4270
|
+
);
|
|
4271
|
+
|
|
4272
|
+
// Fire the timers - main sync starts and blocks on GET hashtree
|
|
4273
|
+
await clock.tickAsync(1000);
|
|
4274
|
+
|
|
4275
|
+
// Verify that self is still in the queue (main is being processed, self is waiting)
|
|
4276
|
+
// Now stop the parser - this should clear the syncQueue
|
|
4277
|
+
parser.stop();
|
|
4278
|
+
|
|
4279
|
+
// Resolve the pending main GET request so the in-flight sync can finish
|
|
4280
|
+
resolveMainHashtree({
|
|
4281
|
+
body: {
|
|
4282
|
+
hashes: new Array(16).fill(EMPTY_HASH),
|
|
4283
|
+
dataSet: createDataSet('main', 16, 1100),
|
|
4284
|
+
},
|
|
4285
|
+
});
|
|
4286
|
+
|
|
4287
|
+
await clock.tickAsync(0);
|
|
4288
|
+
|
|
4289
|
+
// Self should never have been synced because stop() cleared the queue
|
|
4290
|
+
const selfGetCalls = webexRequest.args.filter(
|
|
4291
|
+
(args) => args[0]?.method === 'GET' && args[0]?.uri === `${selfUrl}/hashtree`
|
|
4292
|
+
);
|
|
4293
|
+
expect(selfGetCalls).to.have.lengthOf(0);
|
|
4294
|
+
});
|
|
4295
|
+
});
|
|
4296
|
+
|
|
3932
4297
|
describe('#cleanUp', () => {
|
|
3933
4298
|
it('should stop the parser, clear all timers and clear all dataSets', () => {
|
|
3934
4299
|
const parser = createHashTreeParser();
|
|
@@ -3175,7 +3175,7 @@ describe('plugin-meetings', () => {
|
|
|
3175
3175
|
const createMockParser = (state = 'active') => ({
|
|
3176
3176
|
state,
|
|
3177
3177
|
stop: sinon.stub(),
|
|
3178
|
-
|
|
3178
|
+
resumeFromMessage: sinon.stub(),
|
|
3179
3179
|
handleMessage: sinon.stub(),
|
|
3180
3180
|
});
|
|
3181
3181
|
|
|
@@ -3258,7 +3258,7 @@ describe('plugin-meetings', () => {
|
|
|
3258
3258
|
stateElementsMessage: message,
|
|
3259
3259
|
});
|
|
3260
3260
|
|
|
3261
|
-
assert.calledOnce(parserA.
|
|
3261
|
+
assert.calledOnce(parserA.resumeFromMessage);
|
|
3262
3262
|
assert.calledOnce(parserB.stop);
|
|
3263
3263
|
});
|
|
3264
3264
|
|
|
@@ -3281,7 +3281,7 @@ describe('plugin-meetings', () => {
|
|
|
3281
3281
|
stateElementsMessage: message,
|
|
3282
3282
|
});
|
|
3283
3283
|
|
|
3284
|
-
assert.notCalled(parserA.
|
|
3284
|
+
assert.notCalled(parserA.resumeFromMessage);
|
|
3285
3285
|
assert.notCalled(parserB.stop);
|
|
3286
3286
|
});
|
|
3287
3287
|
|
|
@@ -3300,7 +3300,7 @@ describe('plugin-meetings', () => {
|
|
|
3300
3300
|
stateElementsMessage: message,
|
|
3301
3301
|
});
|
|
3302
3302
|
|
|
3303
|
-
assert.notCalled(parserA.
|
|
3303
|
+
assert.notCalled(parserA.resumeFromMessage);
|
|
3304
3304
|
assert.notCalled(parserA.handleMessage);
|
|
3305
3305
|
});
|
|
3306
3306
|
|
|
@@ -3453,6 +3453,156 @@ describe('plugin-meetings', () => {
|
|
|
3453
3453
|
assert.calledOnce(locusInfo.sendClassicVsHashTreeMismatchMetric);
|
|
3454
3454
|
assert.calledOnce(mockHashTreeParser.handleLocusUpdate);
|
|
3455
3455
|
});
|
|
3456
|
+
|
|
3457
|
+
describe('parser switch via API response', () => {
|
|
3458
|
+
const deviceUrl = 'http://device-url.com';
|
|
3459
|
+
const locusUrlA = 'http://locus-url-A.com';
|
|
3460
|
+
const locusUrlB = 'http://locus-url-B.com';
|
|
3461
|
+
|
|
3462
|
+
let HashTreeParserStub;
|
|
3463
|
+
|
|
3464
|
+
const createMockApiParser = (state = 'active') => ({
|
|
3465
|
+
state,
|
|
3466
|
+
stop: sinon.stub(),
|
|
3467
|
+
resumeFromApiResponse: sinon.stub(),
|
|
3468
|
+
handleLocusUpdate: sinon.stub(),
|
|
3469
|
+
initializeFromGetLociResponse: sinon.stub(),
|
|
3470
|
+
});
|
|
3471
|
+
|
|
3472
|
+
const createLocusWithReplaces = (url, replacedLocusUrl, replacedAt) => ({
|
|
3473
|
+
url,
|
|
3474
|
+
self: {
|
|
3475
|
+
devices: [{url: deviceUrl, replaces: [{locusUrl: replacedLocusUrl, replacedAt}]}],
|
|
3476
|
+
},
|
|
3477
|
+
});
|
|
3478
|
+
|
|
3479
|
+
const createLocusWithoutReplaces = (url) => ({
|
|
3480
|
+
url,
|
|
3481
|
+
self: {devices: [{url: deviceUrl}]},
|
|
3482
|
+
});
|
|
3483
|
+
|
|
3484
|
+
beforeEach(() => {
|
|
3485
|
+
locusInfo.webex.internal.device.url = deviceUrl;
|
|
3486
|
+
HashTreeParserStub = sinon
|
|
3487
|
+
.stub(HashTreeParserModule, 'default')
|
|
3488
|
+
.returns(createMockApiParser());
|
|
3489
|
+
});
|
|
3490
|
+
|
|
3491
|
+
it('should create a new parser and initialize it when no entry exists for the locusUrl', () => {
|
|
3492
|
+
// existing parser for a different url so hashTreeParsers.size > 0
|
|
3493
|
+
locusInfo.hashTreeParsers.set(locusUrlA, {parser: createMockApiParser(), initializedFromHashTree: true});
|
|
3494
|
+
|
|
3495
|
+
const locus = createLocusWithReplaces(locusUrlB, locusUrlA, '2026-01-01T00:00:00Z');
|
|
3496
|
+
sinon.stub(locusInfo, 'handleLocusDelta');
|
|
3497
|
+
|
|
3498
|
+
locusInfo.handleLocusAPIResponse(mockMeeting, {locus});
|
|
3499
|
+
|
|
3500
|
+
assert.isTrue(locusInfo.hashTreeParsers.has(locusUrlB));
|
|
3501
|
+
const newEntry = locusInfo.hashTreeParsers.get(locusUrlB);
|
|
3502
|
+
assert.isFalse(newEntry.initializedFromHashTree);
|
|
3503
|
+
|
|
3504
|
+
// the stub returns the mock, so initializeFromGetLociResponse should be called on it
|
|
3505
|
+
const createdParser = HashTreeParserStub.returnValues[0];
|
|
3506
|
+
assert.calledOnceWithExactly(createdParser.initializeFromGetLociResponse, locus);
|
|
3507
|
+
assert.notCalled(locusInfo.handleLocusDelta);
|
|
3508
|
+
});
|
|
3509
|
+
|
|
3510
|
+
it('should reactivate a stopped parser when replaces info is newer', () => {
|
|
3511
|
+
const parserA = createMockApiParser('stopped');
|
|
3512
|
+
const parserB = createMockApiParser('active');
|
|
3513
|
+
locusInfo.hashTreeParsers.set(locusUrlA, {parser: parserA, replacedAt: '2026-01-01T00:00:00Z', initializedFromHashTree: true});
|
|
3514
|
+
locusInfo.hashTreeParsers.set(locusUrlB, {parser: parserB, initializedFromHashTree: true});
|
|
3515
|
+
|
|
3516
|
+
const locus = createLocusWithReplaces(locusUrlA, locusUrlB, '2026-02-01T00:00:00Z');
|
|
3517
|
+
|
|
3518
|
+
locusInfo.handleLocusAPIResponse(mockMeeting, {locus});
|
|
3519
|
+
|
|
3520
|
+
assert.calledOnce(parserA.resumeFromApiResponse);
|
|
3521
|
+
assert.calledWithExactly(parserA.resumeFromApiResponse, locus);
|
|
3522
|
+
assert.calledOnce(parserB.stop);
|
|
3523
|
+
assert.equal(locusInfo.hashTreeParsers.get(locusUrlB).replacedAt, '2026-02-01T00:00:00Z');
|
|
3524
|
+
assert.isFalse(locusInfo.hashTreeParsers.get(locusUrlA).initializedFromHashTree);
|
|
3525
|
+
});
|
|
3526
|
+
|
|
3527
|
+
it('should not reactivate a stopped parser when replaces info is not newer', () => {
|
|
3528
|
+
const parserA = createMockApiParser('stopped');
|
|
3529
|
+
const parserB = createMockApiParser('active');
|
|
3530
|
+
locusInfo.hashTreeParsers.set(locusUrlA, {parser: parserA, replacedAt: '2026-03-01T00:00:00Z', initializedFromHashTree: true});
|
|
3531
|
+
locusInfo.hashTreeParsers.set(locusUrlB, {parser: parserB, initializedFromHashTree: true});
|
|
3532
|
+
|
|
3533
|
+
const locus = createLocusWithReplaces(locusUrlA, locusUrlB, '2026-01-01T00:00:00Z');
|
|
3534
|
+
|
|
3535
|
+
locusInfo.handleLocusAPIResponse(mockMeeting, {locus});
|
|
3536
|
+
|
|
3537
|
+
assert.notCalled(parserA.resumeFromApiResponse);
|
|
3538
|
+
assert.notCalled(parserB.stop);
|
|
3539
|
+
});
|
|
3540
|
+
|
|
3541
|
+
it('should not reactivate a stopped parser when no replaces info is available', () => {
|
|
3542
|
+
const parserA = createMockApiParser('stopped');
|
|
3543
|
+
locusInfo.hashTreeParsers.set(locusUrlA, {parser: parserA, initializedFromHashTree: true});
|
|
3544
|
+
|
|
3545
|
+
const locus = createLocusWithoutReplaces(locusUrlA);
|
|
3546
|
+
|
|
3547
|
+
locusInfo.handleLocusAPIResponse(mockMeeting, {locus});
|
|
3548
|
+
|
|
3549
|
+
assert.notCalled(parserA.resumeFromApiResponse);
|
|
3550
|
+
});
|
|
3551
|
+
});
|
|
3552
|
+
});
|
|
3553
|
+
|
|
3554
|
+
describe('#syncAllHashTreeDatasets', () => {
|
|
3555
|
+
it('should call syncAllDatasets on each parser that has an entry', async () => {
|
|
3556
|
+
const parser1 = {syncAllDatasets: sinon.stub().resolves()};
|
|
3557
|
+
const parser2 = {syncAllDatasets: sinon.stub().resolves()};
|
|
3558
|
+
locusInfo.hashTreeParsers.set('url1', {parser: parser1});
|
|
3559
|
+
locusInfo.hashTreeParsers.set('url2', {parser: parser2});
|
|
3560
|
+
|
|
3561
|
+
await locusInfo.syncAllHashTreeDatasets();
|
|
3562
|
+
|
|
3563
|
+
assert.calledOnce(parser1.syncAllDatasets);
|
|
3564
|
+
assert.calledOnce(parser2.syncAllDatasets);
|
|
3565
|
+
});
|
|
3566
|
+
|
|
3567
|
+
it('should skip parser entries without a parser object', async () => {
|
|
3568
|
+
const parser1 = {syncAllDatasets: sinon.stub().resolves()};
|
|
3569
|
+
locusInfo.hashTreeParsers.set('url1', {parser: parser1});
|
|
3570
|
+
locusInfo.hashTreeParsers.set('url2', {parser: undefined});
|
|
3571
|
+
|
|
3572
|
+
await locusInfo.syncAllHashTreeDatasets();
|
|
3573
|
+
|
|
3574
|
+
assert.calledOnce(parser1.syncAllDatasets);
|
|
3575
|
+
});
|
|
3576
|
+
|
|
3577
|
+
it('should await each parsers syncAllDatasets sequentially', async () => {
|
|
3578
|
+
const callOrder = [];
|
|
3579
|
+
const parser1 = {syncAllDatasets: sinon.stub().callsFake(() => {
|
|
3580
|
+
callOrder.push('start1');
|
|
3581
|
+
return new Promise((resolve) => {
|
|
3582
|
+
setTimeout(() => {
|
|
3583
|
+
callOrder.push('end1');
|
|
3584
|
+
resolve();
|
|
3585
|
+
}, 100);
|
|
3586
|
+
});
|
|
3587
|
+
})};
|
|
3588
|
+
const parser2 = {syncAllDatasets: sinon.stub().callsFake(() => {
|
|
3589
|
+
callOrder.push('start2');
|
|
3590
|
+
return Promise.resolve();
|
|
3591
|
+
})};
|
|
3592
|
+
locusInfo.hashTreeParsers.set('url1', {parser: parser1});
|
|
3593
|
+
locusInfo.hashTreeParsers.set('url2', {parser: parser2});
|
|
3594
|
+
|
|
3595
|
+
const clock = sinon.useFakeTimers();
|
|
3596
|
+
const promise = locusInfo.syncAllHashTreeDatasets();
|
|
3597
|
+
// parser1 started but parser2 not yet
|
|
3598
|
+
assert.deepEqual(callOrder, ['start1']);
|
|
3599
|
+
|
|
3600
|
+
await clock.tickAsync(100);
|
|
3601
|
+
await promise;
|
|
3602
|
+
// parser1 finished, then parser2 started and finished
|
|
3603
|
+
assert.deepEqual(callOrder, ['start1', 'end1', 'start2']);
|
|
3604
|
+
clock.restore();
|
|
3605
|
+
});
|
|
3456
3606
|
});
|
|
3457
3607
|
|
|
3458
3608
|
describe('#LocusDeltaEvents', () => {
|
|
@@ -5001,6 +5151,31 @@ describe('plugin-meetings', () => {
|
|
|
5001
5151
|
);
|
|
5002
5152
|
assert.notCalled(getTheLocusToUpdateStub);
|
|
5003
5153
|
});
|
|
5154
|
+
|
|
5155
|
+
it('should call handleLocusAPIResponse for SDK_LOCUS_FROM_SYNC_MEETINGS when hash tree parsers exist', () => {
|
|
5156
|
+
const fakeLocusUrl = 'http://locus-url.com';
|
|
5157
|
+
const fakeLocus = {url: fakeLocusUrl, fullState: {state: 'ACTIVE'}};
|
|
5158
|
+
const mockHashTreeParser = {
|
|
5159
|
+
handleMessage: sinon.stub(),
|
|
5160
|
+
handleLocusUpdate: sinon.stub(),
|
|
5161
|
+
};
|
|
5162
|
+
locusInfo.hashTreeParsers.set(fakeLocusUrl, {
|
|
5163
|
+
parser: mockHashTreeParser,
|
|
5164
|
+
initializedFromHashTree: true,
|
|
5165
|
+
});
|
|
5166
|
+
|
|
5167
|
+
sinon.stub(locusInfo, 'handleLocusDelta');
|
|
5168
|
+
|
|
5169
|
+
locusInfo.parse(mockMeeting, {
|
|
5170
|
+
eventType: LOCUSEVENT.SDK_LOCUS_FROM_SYNC_MEETINGS,
|
|
5171
|
+
locus: fakeLocus,
|
|
5172
|
+
});
|
|
5173
|
+
|
|
5174
|
+
// should route through handleLocusAPIResponse which passes unwrapped LocusDTO to parser
|
|
5175
|
+
assert.calledOnce(mockHashTreeParser.handleLocusUpdate);
|
|
5176
|
+
assert.notCalled(mockHashTreeParser.handleMessage);
|
|
5177
|
+
assert.notCalled(locusInfo.handleLocusDelta);
|
|
5178
|
+
});
|
|
5004
5179
|
});
|
|
5005
5180
|
});
|
|
5006
5181
|
|