@webex/plugin-meetings 3.12.0-next.21 → 3.12.0-next.23
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/interceptors/locusRetry.js +23 -8
- package/dist/interceptors/locusRetry.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/interceptors/locusRetry.d.ts +4 -4
- package/dist/types/locus-info/index.d.ts +29 -0
- package/dist/webinar/index.js +1 -1
- package/package.json +1 -1
- package/src/hashTree/hashTreeParser.ts +182 -97
- package/src/interceptors/locusRetry.ts +25 -4
- 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/interceptors/locusRetry.ts +205 -4
- 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();
|
|
@@ -36,6 +36,27 @@ describe('plugin-meetings', () => {
|
|
|
36
36
|
uri: `https://locus-test.webex.com/locus/api/v1/loci/call`,
|
|
37
37
|
body: 'foo'
|
|
38
38
|
};
|
|
39
|
+
|
|
40
|
+
const hashTreeOptions = {
|
|
41
|
+
method: 'GET',
|
|
42
|
+
headers: {
|
|
43
|
+
trackingid: 'test',
|
|
44
|
+
'retry-after': 1000,
|
|
45
|
+
},
|
|
46
|
+
uri: `https://locus-test.webex.com/locus/api/v1/loci/12345/session/abc/datasets/main/hashtree`,
|
|
47
|
+
body: undefined,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const syncOptions = {
|
|
51
|
+
method: 'POST',
|
|
52
|
+
headers: {
|
|
53
|
+
trackingid: 'test',
|
|
54
|
+
'retry-after': 1000,
|
|
55
|
+
},
|
|
56
|
+
uri: `https://locus-test.webex.com/locus/api/v1/loci/12345/session/abc/datasets/main/sync`,
|
|
57
|
+
body: 'foo',
|
|
58
|
+
};
|
|
59
|
+
|
|
39
60
|
const reason1 = new WebexHttpError.MethodNotAllowed({
|
|
40
61
|
statusCode: 403,
|
|
41
62
|
options: {
|
|
@@ -68,14 +89,194 @@ describe('plugin-meetings', () => {
|
|
|
68
89
|
});
|
|
69
90
|
|
|
70
91
|
it('calls handleRetryRequestLocusServiceError with correct retry time when locus service unavailable error', () => {
|
|
71
|
-
|
|
72
|
-
|
|
92
|
+
interceptor.webex.request = sinon.stub().returns(Promise.resolve());
|
|
93
|
+
const handleRetryStub = sinon.stub(
|
|
94
|
+
interceptor,
|
|
95
|
+
'handleRetryRequestLocusServiceError'
|
|
96
|
+
);
|
|
97
|
+
handleRetryStub.returns(Promise.resolve());
|
|
98
|
+
|
|
99
|
+
return interceptor.onResponseError(options, reason2).then(() => {
|
|
100
|
+
expect(handleRetryStub.calledWith(options, 1000)).to.be.true;
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
[429, 500, 502, 503, 504].forEach((statusCode) => {
|
|
105
|
+
it(`does not retry /hashtree requests on ${statusCode}`, () => {
|
|
106
|
+
const reason = new WebexHttpError.MethodNotAllowed({
|
|
107
|
+
statusCode,
|
|
108
|
+
options: {
|
|
109
|
+
headers: {trackingid: 'test', 'retry-after': 1000},
|
|
110
|
+
uri: hashTreeOptions.uri,
|
|
111
|
+
},
|
|
112
|
+
body: {error: `Fake ${statusCode}`},
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const handleRetryStub = sinon.stub(
|
|
116
|
+
interceptor,
|
|
117
|
+
'handleRetryRequestLocusServiceError'
|
|
118
|
+
);
|
|
119
|
+
handleRetryStub.returns(Promise.resolve());
|
|
120
|
+
|
|
121
|
+
return interceptor.onResponseError(hashTreeOptions, reason).then(
|
|
122
|
+
() => assert.fail('Expected promise to be rejected'),
|
|
123
|
+
(err) => {
|
|
124
|
+
expect(err).to.equal(reason);
|
|
125
|
+
expect(handleRetryStub.called).to.be.false;
|
|
126
|
+
handleRetryStub.restore();
|
|
127
|
+
}
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it(`does not retry /sync requests on ${statusCode}`, () => {
|
|
132
|
+
const reason = new WebexHttpError.MethodNotAllowed({
|
|
133
|
+
statusCode,
|
|
134
|
+
options: {
|
|
135
|
+
headers: {trackingid: 'test', 'retry-after': 1000},
|
|
136
|
+
uri: syncOptions.uri,
|
|
137
|
+
},
|
|
138
|
+
body: {error: `Fake ${statusCode}`},
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const handleRetryStub = sinon.stub(
|
|
142
|
+
interceptor,
|
|
143
|
+
'handleRetryRequestLocusServiceError'
|
|
144
|
+
);
|
|
73
145
|
handleRetryStub.returns(Promise.resolve());
|
|
74
146
|
|
|
75
|
-
return interceptor.onResponseError(
|
|
76
|
-
|
|
147
|
+
return interceptor.onResponseError(syncOptions, reason).then(
|
|
148
|
+
() => assert.fail('Expected promise to be rejected'),
|
|
149
|
+
(err) => {
|
|
150
|
+
expect(err).to.equal(reason);
|
|
151
|
+
expect(handleRetryStub.called).to.be.false;
|
|
152
|
+
handleRetryStub.restore();
|
|
153
|
+
}
|
|
154
|
+
);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('still retries other locus requests on 429', () => {
|
|
159
|
+
const reason429 = new WebexHttpError.MethodNotAllowed({
|
|
160
|
+
statusCode: 429,
|
|
161
|
+
options: {
|
|
162
|
+
headers: {trackingid: 'test', 'retry-after': 1000},
|
|
163
|
+
uri: options.uri,
|
|
164
|
+
},
|
|
165
|
+
body: {error: 'Too Many Requests'},
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
interceptor.webex.request = sinon.stub().returns(Promise.resolve());
|
|
169
|
+
const handleRetryStub = sinon.stub(
|
|
170
|
+
interceptor,
|
|
171
|
+
'handleRetryRequestLocusServiceError'
|
|
172
|
+
);
|
|
173
|
+
handleRetryStub.returns(Promise.resolve());
|
|
77
174
|
|
|
175
|
+
return interceptor.onResponseError(options, reason429).then(() => {
|
|
176
|
+
expect(handleRetryStub.calledOnce).to.be.true;
|
|
177
|
+
handleRetryStub.restore();
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('still retries other locus requests on 503', () => {
|
|
182
|
+
interceptor.webex.request = sinon.stub().returns(Promise.resolve());
|
|
183
|
+
const handleRetryStub = sinon.stub(
|
|
184
|
+
interceptor,
|
|
185
|
+
'handleRetryRequestLocusServiceError'
|
|
186
|
+
);
|
|
187
|
+
handleRetryStub.returns(Promise.resolve());
|
|
188
|
+
|
|
189
|
+
return interceptor.onResponseError(options, reason2).then(() => {
|
|
190
|
+
expect(handleRetryStub.calledOnce).to.be.true;
|
|
191
|
+
handleRetryStub.restore();
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe('URI parsing edge cases', () => {
|
|
196
|
+
const make503Reason = (uri) =>
|
|
197
|
+
new WebexHttpError.MethodNotAllowed({
|
|
198
|
+
statusCode: 503,
|
|
199
|
+
options: {headers: {trackingid: 'test', 'retry-after': 1000}, uri},
|
|
200
|
+
body: {error: 'Service Unavailable'},
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const makeOptions = (uri) => ({
|
|
204
|
+
method: 'GET',
|
|
205
|
+
headers: {trackingid: 'test', 'retry-after': 1000},
|
|
206
|
+
uri,
|
|
207
|
+
body: undefined,
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
[
|
|
211
|
+
'https://locus.webex.com/locus/api/v1/loci/123/session/abc/datasets/main/hashtree?rootHash=xyz',
|
|
212
|
+
'https://locus.webex.com/locus/api/v1/loci/123/session/abc/datasets/main/sync?seq=5',
|
|
213
|
+
].forEach((uri) => {
|
|
214
|
+
it(`skips retry even with query params: ${uri.split('/').pop()}`, () => {
|
|
215
|
+
const opts = makeOptions(uri);
|
|
216
|
+
const reason = make503Reason(uri);
|
|
217
|
+
const stub = sinon
|
|
218
|
+
.stub(interceptor, 'handleRetryRequestLocusServiceError')
|
|
219
|
+
.returns(Promise.resolve());
|
|
220
|
+
|
|
221
|
+
return interceptor.onResponseError(opts, reason).then(
|
|
222
|
+
() => assert.fail('Expected promise to be rejected'),
|
|
223
|
+
(err) => {
|
|
224
|
+
expect(err).to.equal(reason);
|
|
225
|
+
expect(stub.called).to.be.false;
|
|
226
|
+
stub.restore();
|
|
227
|
+
}
|
|
228
|
+
);
|
|
78
229
|
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
[
|
|
233
|
+
'https://locus.webex.com/locus/api/v1/loci/123/hashtree-v2',
|
|
234
|
+
'https://locus.webex.com/locus/api/v1/loci/123/syncData',
|
|
235
|
+
'https://locus.webex.com/locus/api/v1/loci/123/async',
|
|
236
|
+
'https://locus.webex.com/locus/api/v1/loci/123/hashtree/metadata',
|
|
237
|
+
].forEach((uri) => {
|
|
238
|
+
it(`still retries when path only partially matches: ${uri
|
|
239
|
+
.split('/')
|
|
240
|
+
.pop()}`, () => {
|
|
241
|
+
const opts = makeOptions(uri);
|
|
242
|
+
const reason = make503Reason(uri);
|
|
243
|
+
interceptor.webex.request = sinon.stub().returns(Promise.resolve());
|
|
244
|
+
const stub = sinon
|
|
245
|
+
.stub(interceptor, 'handleRetryRequestLocusServiceError')
|
|
246
|
+
.returns(Promise.resolve());
|
|
247
|
+
|
|
248
|
+
return interceptor.onResponseError(opts, reason).then(() => {
|
|
249
|
+
expect(stub.calledOnce).to.be.true;
|
|
250
|
+
stub.restore();
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('still retries when /hashtree is on a non-locus host', () => {
|
|
256
|
+
const uri = 'https://other-service.webex.com/api/v1/hashtree';
|
|
257
|
+
const opts = makeOptions(uri);
|
|
258
|
+
const reason = make503Reason(uri);
|
|
259
|
+
|
|
260
|
+
return interceptor.onResponseError(opts, reason).then(
|
|
261
|
+
() => assert.fail('Expected promise to be rejected'),
|
|
262
|
+
(err) => {
|
|
263
|
+
expect(err).to.equal(reason);
|
|
264
|
+
}
|
|
265
|
+
);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('still retries when URI is malformed', () => {
|
|
269
|
+
const uri = 'not-a-valid-url';
|
|
270
|
+
const opts = makeOptions(uri);
|
|
271
|
+
const reason = make503Reason(uri);
|
|
272
|
+
|
|
273
|
+
return interceptor.onResponseError(opts, reason).then(
|
|
274
|
+
() => assert.fail('Expected promise to be rejected'),
|
|
275
|
+
(err) => {
|
|
276
|
+
expect(err).to.equal(reason);
|
|
277
|
+
}
|
|
278
|
+
);
|
|
279
|
+
});
|
|
79
280
|
});
|
|
80
281
|
});
|
|
81
282
|
|