@webex/plugin-meetings 3.12.0-next.50 → 3.12.0-next.51
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 +54 -14
- 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 +12 -0
- package/dist/locus-info/index.js.map +1 -1
- package/dist/locus-info/types.js +4 -1
- package/dist/locus-info/types.js.map +1 -1
- package/dist/meetings/index.js +7 -1
- package/dist/meetings/index.js.map +1 -1
- package/dist/types/hashTree/hashTreeParser.d.ts +26 -5
- package/dist/types/locus-info/types.d.ts +4 -0
- package/dist/types/meetings/index.d.ts +2 -1
- package/dist/webinar/index.js +1 -1
- package/package.json +1 -1
- package/src/hashTree/hashTreeParser.ts +64 -22
- package/src/locus-info/index.ts +15 -0
- package/src/locus-info/types.ts +6 -0
- package/src/meetings/index.ts +15 -10
- package/test/unit/spec/hashTree/hashTreeParser.ts +130 -32
- package/test/unit/spec/locus-info/index.js +32 -0
- package/test/unit/spec/meetings/index.js +34 -0
|
@@ -43,14 +43,21 @@ type WebexRequestMethod = (options: Record<string, any>) => Promise<any>;
|
|
|
43
43
|
export declare const LocusInfoUpdateType: {
|
|
44
44
|
readonly OBJECTS_UPDATED: "OBJECTS_UPDATED";
|
|
45
45
|
readonly MEETING_ENDED: "MEETING_ENDED";
|
|
46
|
+
readonly LOCUS_NOT_FOUND: "LOCUS_NOT_FOUND";
|
|
46
47
|
};
|
|
47
48
|
export type LocusInfoUpdateType = Enum<typeof LocusInfoUpdateType>;
|
|
49
|
+
interface LocusUpdatePayloads {
|
|
50
|
+
[LocusInfoUpdateType.OBJECTS_UPDATED]: {
|
|
51
|
+
updatedObjects: HashTreeObject[];
|
|
52
|
+
};
|
|
53
|
+
[LocusInfoUpdateType.MEETING_ENDED]: unknown;
|
|
54
|
+
[LocusInfoUpdateType.LOCUS_NOT_FOUND]: unknown;
|
|
55
|
+
}
|
|
48
56
|
export type LocusInfoUpdate = {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
};
|
|
57
|
+
[K in keyof LocusUpdatePayloads]: {
|
|
58
|
+
updateType: K;
|
|
59
|
+
} & LocusUpdatePayloads[K];
|
|
60
|
+
}[keyof LocusUpdatePayloads];
|
|
54
61
|
export type LocusInfoUpdateCallback = (update: LocusInfoUpdate) => void;
|
|
55
62
|
/**
|
|
56
63
|
* This error is thrown if we receive information that the meeting has ended while we're processing some hash messages.
|
|
@@ -58,6 +65,13 @@ export type LocusInfoUpdateCallback = (update: LocusInfoUpdate) => void;
|
|
|
58
65
|
*/
|
|
59
66
|
export declare class MeetingEndedError extends Error {
|
|
60
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* This error is thrown when a 404 is received from Locus hash tree endpoints, indicating that the locus URL
|
|
70
|
+
* is no longer valid (e.g. participant moved to a breakout room, or meeting ended).
|
|
71
|
+
* It's handled internally by HashTreeParser and results in LOCUS_NOT_FOUND being sent up.
|
|
72
|
+
*/
|
|
73
|
+
export declare class LocusNotFoundError extends Error {
|
|
74
|
+
}
|
|
61
75
|
/**
|
|
62
76
|
* Parses hash tree eventing locus data
|
|
63
77
|
*/
|
|
@@ -193,6 +207,13 @@ declare class HashTreeParser {
|
|
|
193
207
|
* @returns {void}
|
|
194
208
|
*/
|
|
195
209
|
private handleRootHashHeartBeatMessage;
|
|
210
|
+
/**
|
|
211
|
+
* Handles known errors that can happen during syncs
|
|
212
|
+
*
|
|
213
|
+
* @param {any} error - The error to handle
|
|
214
|
+
* @returns {boolean} true if the error was recognized and handled, false otherwise
|
|
215
|
+
*/
|
|
216
|
+
private handleSyncErrors;
|
|
196
217
|
/**
|
|
197
218
|
* Asynchronously initializes new visible data sets
|
|
198
219
|
*
|
|
@@ -494,8 +494,9 @@ export default class Meetings extends WebexPlugin {
|
|
|
494
494
|
* @public
|
|
495
495
|
* @memberof Meetings
|
|
496
496
|
*/
|
|
497
|
-
syncMeetings({ keepOnlyLocusMeetings }?: {
|
|
497
|
+
syncMeetings({ keepOnlyLocusMeetings, skipHashTreeSync, }?: {
|
|
498
498
|
keepOnlyLocusMeetings?: boolean;
|
|
499
|
+
skipHashTreeSync?: boolean;
|
|
499
500
|
}): Promise<void>;
|
|
500
501
|
/**
|
|
501
502
|
* sort out locus array for initial creating
|
package/dist/webinar/index.js
CHANGED
package/package.json
CHANGED
|
@@ -6,7 +6,7 @@ import BEHAVIORAL_METRICS from '../metrics/constants';
|
|
|
6
6
|
import {Enum, HTTP_VERBS} from '../constants';
|
|
7
7
|
import {DataSetNames, DATA_SET_INIT_PRIORITY, EMPTY_HASH} from './constants';
|
|
8
8
|
import {ObjectType, HtMeta, HashTreeObject} from './types';
|
|
9
|
-
import {LocusDTO} from '../locus-info/types';
|
|
9
|
+
import {LocusDTO, LocusErrorCodes} from '../locus-info/types';
|
|
10
10
|
import {deleteNestedObjectsWithHtMeta, isMetadata, sortByInitPriority} from './utils';
|
|
11
11
|
|
|
12
12
|
export interface DataSet {
|
|
@@ -56,17 +56,23 @@ type WebexRequestMethod = (options: Record<string, any>) => Promise<any>;
|
|
|
56
56
|
export const LocusInfoUpdateType = {
|
|
57
57
|
OBJECTS_UPDATED: 'OBJECTS_UPDATED',
|
|
58
58
|
MEETING_ENDED: 'MEETING_ENDED',
|
|
59
|
+
LOCUS_NOT_FOUND: 'LOCUS_NOT_FOUND',
|
|
59
60
|
} as const;
|
|
60
61
|
|
|
61
62
|
export type LocusInfoUpdateType = Enum<typeof LocusInfoUpdateType>;
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
63
|
+
|
|
64
|
+
interface LocusUpdatePayloads {
|
|
65
|
+
[LocusInfoUpdateType.OBJECTS_UPDATED]: {updatedObjects: HashTreeObject[]};
|
|
66
|
+
[LocusInfoUpdateType.MEETING_ENDED]: unknown; // No extra data
|
|
67
|
+
[LocusInfoUpdateType.LOCUS_NOT_FOUND]: unknown; // No extra data
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export type LocusInfoUpdate = {
|
|
71
|
+
[K in keyof LocusUpdatePayloads]: {
|
|
72
|
+
updateType: K;
|
|
73
|
+
} & LocusUpdatePayloads[K];
|
|
74
|
+
}[keyof LocusUpdatePayloads];
|
|
75
|
+
|
|
70
76
|
export type LocusInfoUpdateCallback = (update: LocusInfoUpdate) => void;
|
|
71
77
|
|
|
72
78
|
interface LeafInfo {
|
|
@@ -82,6 +88,13 @@ interface LeafInfo {
|
|
|
82
88
|
*/
|
|
83
89
|
export class MeetingEndedError extends Error {}
|
|
84
90
|
|
|
91
|
+
/**
|
|
92
|
+
* This error is thrown when a 404 is received from Locus hash tree endpoints, indicating that the locus URL
|
|
93
|
+
* is no longer valid (e.g. participant moved to a breakout room, or meeting ended).
|
|
94
|
+
* It's handled internally by HashTreeParser and results in LOCUS_NOT_FOUND being sent up.
|
|
95
|
+
*/
|
|
96
|
+
export class LocusNotFoundError extends Error {}
|
|
97
|
+
|
|
85
98
|
/* Currently Locus always sends Metadata objects only in the "self" dataset.
|
|
86
99
|
* If this ever changes, update all the code that relies on this constant.
|
|
87
100
|
*/
|
|
@@ -552,6 +565,32 @@ class HashTreeParser {
|
|
|
552
565
|
});
|
|
553
566
|
}
|
|
554
567
|
|
|
568
|
+
/**
|
|
569
|
+
* Handles known errors that can happen during syncs
|
|
570
|
+
*
|
|
571
|
+
* @param {any} error - The error to handle
|
|
572
|
+
* @returns {boolean} true if the error was recognized and handled, false otherwise
|
|
573
|
+
*/
|
|
574
|
+
private handleSyncErrors(error: any) {
|
|
575
|
+
if (error instanceof MeetingEndedError) {
|
|
576
|
+
this.callLocusInfoUpdateCallback({
|
|
577
|
+
updateType: LocusInfoUpdateType.MEETING_ENDED,
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
return true;
|
|
581
|
+
}
|
|
582
|
+
if (error instanceof LocusNotFoundError) {
|
|
583
|
+
this.callLocusInfoUpdateCallback({
|
|
584
|
+
updateType: LocusInfoUpdateType.LOCUS_NOT_FOUND,
|
|
585
|
+
});
|
|
586
|
+
this.stop();
|
|
587
|
+
|
|
588
|
+
return true;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
return false;
|
|
592
|
+
}
|
|
593
|
+
|
|
555
594
|
/**
|
|
556
595
|
* Asynchronously initializes new visible data sets
|
|
557
596
|
*
|
|
@@ -568,11 +607,7 @@ class HashTreeParser {
|
|
|
568
607
|
);
|
|
569
608
|
queueMicrotask(() => {
|
|
570
609
|
this.initializeNewVisibleDataSets(dataSetsRequiringInitialization).catch((error) => {
|
|
571
|
-
if (error
|
|
572
|
-
this.callLocusInfoUpdateCallback({
|
|
573
|
-
updateType: LocusInfoUpdateType.MEETING_ENDED,
|
|
574
|
-
});
|
|
575
|
-
} else {
|
|
610
|
+
if (!this.handleSyncErrors(error)) {
|
|
576
611
|
LoggerProxy.logger.warn(
|
|
577
612
|
`HashTreeParser#queueInitForNewVisibleDataSets --> ${
|
|
578
613
|
this.debugId
|
|
@@ -1271,11 +1306,7 @@ class HashTreeParser {
|
|
|
1271
1306
|
this.handleMessage(syncResponse, 'via sync API');
|
|
1272
1307
|
}
|
|
1273
1308
|
} catch (error) {
|
|
1274
|
-
if (error
|
|
1275
|
-
this.callLocusInfoUpdateCallback({
|
|
1276
|
-
updateType: LocusInfoUpdateType.MEETING_ENDED,
|
|
1277
|
-
});
|
|
1278
|
-
} else {
|
|
1309
|
+
if (!this.handleSyncErrors(error)) {
|
|
1279
1310
|
LoggerProxy.logger.warn(
|
|
1280
1311
|
`HashTreeParser#performSync --> ${this.debugId} error during sync for data set "${dataSet.name}":`,
|
|
1281
1312
|
error
|
|
@@ -1603,17 +1634,28 @@ class HashTreeParser {
|
|
|
1603
1634
|
}
|
|
1604
1635
|
|
|
1605
1636
|
private checkForSentinelHttpResponse(error: any, dataSetName?: string) {
|
|
1637
|
+
// 404 for any dataset means the locus is no longer available at this URL - could be replaced or ended
|
|
1638
|
+
// if a dataset is just not visible, we would get a 400
|
|
1639
|
+
if (error.statusCode === 404) {
|
|
1640
|
+
LoggerProxy.logger.info(
|
|
1641
|
+
`HashTreeParser#checkForSentinelHttpResponse --> ${this.debugId} Received 404 for data set "${dataSetName}", locus not found`
|
|
1642
|
+
);
|
|
1643
|
+
this.stopAllTimers();
|
|
1644
|
+
|
|
1645
|
+
throw new LocusNotFoundError();
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1606
1648
|
const isValidDataSetForSentinel =
|
|
1607
1649
|
dataSetName === undefined ||
|
|
1608
1650
|
PossibleSentinelMessageDataSetNames.includes(dataSetName.toLowerCase());
|
|
1609
1651
|
|
|
1610
1652
|
if (
|
|
1611
|
-
|
|
1612
|
-
|
|
1653
|
+
error.statusCode === 409 &&
|
|
1654
|
+
error.body?.errorCode === LocusErrorCodes.LOCUS_INACTIVE &&
|
|
1613
1655
|
isValidDataSetForSentinel
|
|
1614
1656
|
) {
|
|
1615
1657
|
LoggerProxy.logger.info(
|
|
1616
|
-
`HashTreeParser#checkForSentinelHttpResponse --> ${this.debugId} Received ${error.statusCode} for data set "${dataSetName}", indicating that the meeting has ended`
|
|
1658
|
+
`HashTreeParser#checkForSentinelHttpResponse --> ${this.debugId} Received ${error.statusCode}/${error.body?.errorCode} for data set "${dataSetName}", indicating that the meeting has ended`
|
|
1617
1659
|
);
|
|
1618
1660
|
this.stopAllTimers();
|
|
1619
1661
|
|
package/src/locus-info/index.ts
CHANGED
|
@@ -1344,6 +1344,21 @@ export default class LocusInfo extends EventsScope {
|
|
|
1344
1344
|
);
|
|
1345
1345
|
this.webex.meetings.destroy(meeting, MEETING_REMOVED_REASON.SELF_REMOVED);
|
|
1346
1346
|
}
|
|
1347
|
+
break;
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
case LocusInfoUpdateType.LOCUS_NOT_FOUND: {
|
|
1351
|
+
LoggerProxy.logger.info(
|
|
1352
|
+
`Locus-info:index#updateFromHashTree --> received LOCUS_NOT_FOUND for ${locusUrl}, triggering syncMeetings`
|
|
1353
|
+
);
|
|
1354
|
+
this.webex.meetings
|
|
1355
|
+
.syncMeetings({keepOnlyLocusMeetings: false, skipHashTreeSync: true})
|
|
1356
|
+
.catch((syncError) => {
|
|
1357
|
+
LoggerProxy.logger.error(
|
|
1358
|
+
`Locus-info:index#updateFromHashTree --> syncMeetings failed after LOCUS_NOT_FOUND: ${syncError}`
|
|
1359
|
+
);
|
|
1360
|
+
});
|
|
1361
|
+
break;
|
|
1347
1362
|
}
|
|
1348
1363
|
}
|
|
1349
1364
|
}
|
package/src/locus-info/types.ts
CHANGED
package/src/meetings/index.ts
CHANGED
|
@@ -1924,7 +1924,10 @@ export default class Meetings extends WebexPlugin {
|
|
|
1924
1924
|
* @public
|
|
1925
1925
|
* @memberof Meetings
|
|
1926
1926
|
*/
|
|
1927
|
-
public async syncMeetings({
|
|
1927
|
+
public async syncMeetings({
|
|
1928
|
+
keepOnlyLocusMeetings = true,
|
|
1929
|
+
skipHashTreeSync = false,
|
|
1930
|
+
} = {}): Promise<void> {
|
|
1928
1931
|
// @ts-ignore
|
|
1929
1932
|
if (this.webex.credentials.isUnverifiedGuest) {
|
|
1930
1933
|
LoggerProxy.logger.info(
|
|
@@ -1984,18 +1987,20 @@ export default class Meetings extends WebexPlugin {
|
|
|
1984
1987
|
}
|
|
1985
1988
|
}
|
|
1986
1989
|
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
+
if (!skipHashTreeSync) {
|
|
1991
|
+
// Trigger hash tree syncs for all remaining meetings
|
|
1992
|
+
const remainingMeetings = this.meetingCollection.getAll();
|
|
1993
|
+
const syncPromises = [];
|
|
1990
1994
|
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1995
|
+
for (const meeting of Object.values(remainingMeetings) as any[]) {
|
|
1996
|
+
if (meeting.locusInfo) {
|
|
1997
|
+
syncPromises.push(meeting.locusInfo.syncAllHashTreeDatasets());
|
|
1998
|
+
}
|
|
1994
1999
|
}
|
|
1995
|
-
}
|
|
1996
2000
|
|
|
1997
|
-
|
|
1998
|
-
|
|
2001
|
+
if (syncPromises.length > 0) {
|
|
2002
|
+
await Promise.all(syncPromises);
|
|
2003
|
+
}
|
|
1999
2004
|
}
|
|
2000
2005
|
}
|
|
2001
2006
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import HashTreeParser, {
|
|
2
2
|
LocusInfoUpdateType,
|
|
3
3
|
MeetingEndedError,
|
|
4
|
+
LocusNotFoundError,
|
|
4
5
|
} from '@webex/plugin-meetings/src/hashTree/hashTreeParser';
|
|
5
6
|
import HashTree from '@webex/plugin-meetings/src/hashTree/hashTree';
|
|
6
7
|
import {expect} from '@webex/test-helper-chai';
|
|
@@ -708,8 +709,11 @@ describe('HashTreeParser', () => {
|
|
|
708
709
|
assert.notCalled(callback);
|
|
709
710
|
});
|
|
710
711
|
|
|
711
|
-
[
|
|
712
|
-
|
|
712
|
+
[
|
|
713
|
+
{errorCode: 404, expectedError: LocusNotFoundError},
|
|
714
|
+
{errorCode: 409, expectedError: MeetingEndedError},
|
|
715
|
+
].forEach(({errorCode, expectedError}) => {
|
|
716
|
+
it(`throws ${expectedError.name} if getting visible datasets returns ${errorCode}`, async () => {
|
|
713
717
|
const minimalInitialLocus = {
|
|
714
718
|
dataSets: [],
|
|
715
719
|
locus: null,
|
|
@@ -732,7 +736,6 @@ describe('HashTreeParser', () => {
|
|
|
732
736
|
)
|
|
733
737
|
.rejects(error);
|
|
734
738
|
|
|
735
|
-
// initializeFromMessage should throw MeetingEndedError
|
|
736
739
|
let thrownError;
|
|
737
740
|
try {
|
|
738
741
|
await parser.initializeFromMessage({
|
|
@@ -744,7 +747,7 @@ describe('HashTreeParser', () => {
|
|
|
744
747
|
thrownError = e;
|
|
745
748
|
}
|
|
746
749
|
|
|
747
|
-
expect(thrownError).to.be.instanceOf(
|
|
750
|
+
expect(thrownError).to.be.instanceOf(expectedError);
|
|
748
751
|
});
|
|
749
752
|
});
|
|
750
753
|
});
|
|
@@ -1766,12 +1769,10 @@ describe('HashTreeParser', () => {
|
|
|
1766
1769
|
});
|
|
1767
1770
|
});
|
|
1768
1771
|
|
|
1769
|
-
describe('emits MEETING_ENDED', () => {
|
|
1770
|
-
|
|
1771
|
-
it(`when /hashtree returns ${statusCode}`, async () => {
|
|
1772
|
+
describe('emits MEETING_ENDED when 409/2403004 is returned', () => {
|
|
1773
|
+
it('when /hashtree returns 409', async () => {
|
|
1772
1774
|
const parser = createHashTreeParser();
|
|
1773
1775
|
|
|
1774
|
-
// Send a message to trigger sync algorithm
|
|
1775
1776
|
const message = {
|
|
1776
1777
|
dataSets: [createDataSet('main', 16, 1100)],
|
|
1777
1778
|
visibleDataSetsUrl,
|
|
@@ -1796,12 +1797,9 @@ describe('HashTreeParser', () => {
|
|
|
1796
1797
|
|
|
1797
1798
|
const mainDataSetUrl = parser.dataSets.main.url;
|
|
1798
1799
|
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
error.
|
|
1802
|
-
if (statusCode === 409) {
|
|
1803
|
-
error.body = {errorCode: 2403004};
|
|
1804
|
-
}
|
|
1800
|
+
const error: any = new Error('Request failed with status 409');
|
|
1801
|
+
error.statusCode = 409;
|
|
1802
|
+
error.body = {errorCode: 2403004};
|
|
1805
1803
|
webexRequest
|
|
1806
1804
|
.withArgs(
|
|
1807
1805
|
sinon.match({
|
|
@@ -1811,13 +1809,10 @@ describe('HashTreeParser', () => {
|
|
|
1811
1809
|
)
|
|
1812
1810
|
.rejects(error);
|
|
1813
1811
|
|
|
1814
|
-
// Trigger sync by advancing time
|
|
1815
1812
|
await clock.tickAsync(1000);
|
|
1816
1813
|
|
|
1817
|
-
// Verify callback was called with MEETING_ENDED
|
|
1818
1814
|
assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.MEETING_ENDED});
|
|
1819
1815
|
|
|
1820
|
-
// Verify all timers are stopped
|
|
1821
1816
|
Object.values(parser.dataSets).forEach((ds: any) => {
|
|
1822
1817
|
assert.isUndefined(ds.timer);
|
|
1823
1818
|
assert.isUndefined(ds.heartbeatWatchdogTimer);
|
|
@@ -1827,10 +1822,9 @@ describe('HashTreeParser', () => {
|
|
|
1827
1822
|
assert.notCalled(metricsStub);
|
|
1828
1823
|
});
|
|
1829
1824
|
|
|
1830
|
-
it(
|
|
1825
|
+
it('when /sync returns 409', async () => {
|
|
1831
1826
|
const parser = createHashTreeParser();
|
|
1832
1827
|
|
|
1833
|
-
// Send a message to trigger sync algorithm
|
|
1834
1828
|
const message = {
|
|
1835
1829
|
dataSets: [createDataSet('main', 16, 1100)],
|
|
1836
1830
|
visibleDataSetsUrl,
|
|
@@ -1855,19 +1849,15 @@ describe('HashTreeParser', () => {
|
|
|
1855
1849
|
|
|
1856
1850
|
const mainDataSetUrl = parser.dataSets.main.url;
|
|
1857
1851
|
|
|
1858
|
-
// Mock getHashesFromLocus to succeed
|
|
1859
1852
|
mockGetHashesFromLocusResponse(
|
|
1860
1853
|
mainDataSetUrl,
|
|
1861
1854
|
new Array(16).fill('00000000000000000000000000000000'),
|
|
1862
1855
|
createDataSet('main', 16, 1101)
|
|
1863
1856
|
);
|
|
1864
1857
|
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
error.
|
|
1868
|
-
if (statusCode === 409) {
|
|
1869
|
-
error.body = {errorCode: 2403004};
|
|
1870
|
-
}
|
|
1858
|
+
const error: any = new Error('Request failed with status 409');
|
|
1859
|
+
error.statusCode = 409;
|
|
1860
|
+
error.body = {errorCode: 2403004};
|
|
1871
1861
|
webexRequest
|
|
1872
1862
|
.withArgs(
|
|
1873
1863
|
sinon.match({
|
|
@@ -1877,12 +1867,121 @@ describe('HashTreeParser', () => {
|
|
|
1877
1867
|
)
|
|
1878
1868
|
.rejects(error);
|
|
1879
1869
|
|
|
1880
|
-
// Trigger sync by advancing time
|
|
1881
1870
|
await clock.tickAsync(1000);
|
|
1882
1871
|
|
|
1883
|
-
// Verify callback was called with MEETING_ENDED
|
|
1884
1872
|
assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.MEETING_ENDED});
|
|
1885
1873
|
|
|
1874
|
+
Object.values(parser.dataSets).forEach((ds: any) => {
|
|
1875
|
+
assert.isUndefined(ds.timer);
|
|
1876
|
+
assert.isUndefined(ds.heartbeatWatchdogTimer);
|
|
1877
|
+
});
|
|
1878
|
+
});
|
|
1879
|
+
});
|
|
1880
|
+
|
|
1881
|
+
describe('emits LOCUS_NOT_FOUND and stops parser when 404 is returned', () => {
|
|
1882
|
+
it('when /hashtree returns 404', async () => {
|
|
1883
|
+
const parser = createHashTreeParser();
|
|
1884
|
+
|
|
1885
|
+
const message = {
|
|
1886
|
+
dataSets: [createDataSet('main', 16, 1100)],
|
|
1887
|
+
visibleDataSetsUrl,
|
|
1888
|
+
locusUrl,
|
|
1889
|
+
locusStateElements: [
|
|
1890
|
+
{
|
|
1891
|
+
htMeta: {
|
|
1892
|
+
elementId: {
|
|
1893
|
+
type: 'locus' as const,
|
|
1894
|
+
id: 0,
|
|
1895
|
+
version: 201,
|
|
1896
|
+
},
|
|
1897
|
+
dataSetNames: ['main'],
|
|
1898
|
+
},
|
|
1899
|
+
data: {info: {id: 'initial-update'}},
|
|
1900
|
+
},
|
|
1901
|
+
],
|
|
1902
|
+
};
|
|
1903
|
+
|
|
1904
|
+
parser.handleMessage(message, 'initial message');
|
|
1905
|
+
callback.resetHistory();
|
|
1906
|
+
|
|
1907
|
+
const mainDataSetUrl = parser.dataSets.main.url;
|
|
1908
|
+
|
|
1909
|
+
const error: any = new Error('Request failed with status 404');
|
|
1910
|
+
error.statusCode = 404;
|
|
1911
|
+
webexRequest
|
|
1912
|
+
.withArgs(
|
|
1913
|
+
sinon.match({
|
|
1914
|
+
method: 'GET',
|
|
1915
|
+
uri: `${mainDataSetUrl}/hashtree`,
|
|
1916
|
+
})
|
|
1917
|
+
)
|
|
1918
|
+
.rejects(error);
|
|
1919
|
+
|
|
1920
|
+
await clock.tickAsync(1000);
|
|
1921
|
+
|
|
1922
|
+
assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.LOCUS_NOT_FOUND});
|
|
1923
|
+
|
|
1924
|
+
// Verify parser is stopped
|
|
1925
|
+
expect(parser.state).to.equal('stopped');
|
|
1926
|
+
|
|
1927
|
+
// Verify all timers are stopped
|
|
1928
|
+
Object.values(parser.dataSets).forEach((ds: any) => {
|
|
1929
|
+
assert.isUndefined(ds.timer);
|
|
1930
|
+
assert.isUndefined(ds.heartbeatWatchdogTimer);
|
|
1931
|
+
});
|
|
1932
|
+
});
|
|
1933
|
+
|
|
1934
|
+
it('when /sync returns 404', async () => {
|
|
1935
|
+
const parser = createHashTreeParser();
|
|
1936
|
+
|
|
1937
|
+
const message = {
|
|
1938
|
+
dataSets: [createDataSet('main', 16, 1100)],
|
|
1939
|
+
visibleDataSetsUrl,
|
|
1940
|
+
locusUrl,
|
|
1941
|
+
locusStateElements: [
|
|
1942
|
+
{
|
|
1943
|
+
htMeta: {
|
|
1944
|
+
elementId: {
|
|
1945
|
+
type: 'locus' as const,
|
|
1946
|
+
id: 0,
|
|
1947
|
+
version: 201,
|
|
1948
|
+
},
|
|
1949
|
+
dataSetNames: ['main'],
|
|
1950
|
+
},
|
|
1951
|
+
data: {info: {id: 'initial-update'}},
|
|
1952
|
+
},
|
|
1953
|
+
],
|
|
1954
|
+
};
|
|
1955
|
+
|
|
1956
|
+
parser.handleMessage(message, 'initial message');
|
|
1957
|
+
callback.resetHistory();
|
|
1958
|
+
|
|
1959
|
+
const mainDataSetUrl = parser.dataSets.main.url;
|
|
1960
|
+
|
|
1961
|
+
mockGetHashesFromLocusResponse(
|
|
1962
|
+
mainDataSetUrl,
|
|
1963
|
+
new Array(16).fill('00000000000000000000000000000000'),
|
|
1964
|
+
createDataSet('main', 16, 1101)
|
|
1965
|
+
);
|
|
1966
|
+
|
|
1967
|
+
const error: any = new Error('Request failed with status 404');
|
|
1968
|
+
error.statusCode = 404;
|
|
1969
|
+
webexRequest
|
|
1970
|
+
.withArgs(
|
|
1971
|
+
sinon.match({
|
|
1972
|
+
method: 'POST',
|
|
1973
|
+
uri: `${mainDataSetUrl}/sync`,
|
|
1974
|
+
})
|
|
1975
|
+
)
|
|
1976
|
+
.rejects(error);
|
|
1977
|
+
|
|
1978
|
+
await clock.tickAsync(1000);
|
|
1979
|
+
|
|
1980
|
+
assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.LOCUS_NOT_FOUND});
|
|
1981
|
+
|
|
1982
|
+
// Verify parser is stopped
|
|
1983
|
+
expect(parser.state).to.equal('stopped');
|
|
1984
|
+
|
|
1886
1985
|
// Verify all timers are stopped
|
|
1887
1986
|
Object.values(parser.dataSets).forEach((ds: any) => {
|
|
1888
1987
|
assert.isUndefined(ds.timer);
|
|
@@ -1892,7 +1991,6 @@ describe('HashTreeParser', () => {
|
|
|
1892
1991
|
// Verify no sync failure metric was sent for end-meeting sentinel
|
|
1893
1992
|
assert.notCalled(metricsStub);
|
|
1894
1993
|
});
|
|
1895
|
-
});
|
|
1896
1994
|
});
|
|
1897
1995
|
|
|
1898
1996
|
it('requests only mismatched hashes during sync', async () => {
|
|
@@ -2554,7 +2652,7 @@ describe('HashTreeParser', () => {
|
|
|
2554
2652
|
expect(syncCalls[1].args[0].uri).to.equal(`${newAtdActiveDataSet.url}/sync`);
|
|
2555
2653
|
});
|
|
2556
2654
|
|
|
2557
|
-
it('emits
|
|
2655
|
+
it('emits LOCUS_NOT_FOUND if async init of a new visible dataset fails with 404', async () => {
|
|
2558
2656
|
const parser = createHashTreeParser();
|
|
2559
2657
|
|
|
2560
2658
|
// Stub updateItems on self hash tree to return true
|
|
@@ -2619,8 +2717,8 @@ describe('HashTreeParser', () => {
|
|
|
2619
2717
|
// Wait for the async initialization (queueMicrotask) to complete
|
|
2620
2718
|
await clock.tickAsync(0);
|
|
2621
2719
|
|
|
2622
|
-
// Verify callback was called with
|
|
2623
|
-
assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.
|
|
2720
|
+
// Verify callback was called with LOCUS_NOT_FOUND (404 means locus URL is stale, not necessarily meeting ended)
|
|
2721
|
+
assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.LOCUS_NOT_FOUND});
|
|
2624
2722
|
});
|
|
2625
2723
|
|
|
2626
2724
|
it('handles removal of visible data set', async () => {
|
|
@@ -332,6 +332,7 @@ describe('plugin-meetings', () => {
|
|
|
332
332
|
describe('should setup correct locusInfoUpdateCallback when creating HashTreeParser', () => {
|
|
333
333
|
const OBJECTS_UPDATED = HashTreeParserModule.LocusInfoUpdateType.OBJECTS_UPDATED;
|
|
334
334
|
const MEETING_ENDED = HashTreeParserModule.LocusInfoUpdateType.MEETING_ENDED;
|
|
335
|
+
const LOCUS_NOT_FOUND = HashTreeParserModule.LocusInfoUpdateType.LOCUS_NOT_FOUND;
|
|
335
336
|
|
|
336
337
|
let locusInfoUpdateCallback;
|
|
337
338
|
let onDeltaLocusStub;
|
|
@@ -1001,6 +1002,37 @@ describe('plugin-meetings', () => {
|
|
|
1001
1002
|
assert.notCalled(destroyStub);
|
|
1002
1003
|
});
|
|
1003
1004
|
|
|
1005
|
+
it('should handle LOCUS_NOT_FOUND by calling syncMeetings with skipHashTreeSync', () => {
|
|
1006
|
+
const syncMeetingsStub = sinon.stub(locusInfo.webex.meetings, 'syncMeetings').resolves();
|
|
1007
|
+
|
|
1008
|
+
locusInfoUpdateCallback({updateType: LOCUS_NOT_FOUND});
|
|
1009
|
+
|
|
1010
|
+
assert.calledOnceWithExactly(syncMeetingsStub, {keepOnlyLocusMeetings: false, skipHashTreeSync: true});
|
|
1011
|
+
});
|
|
1012
|
+
|
|
1013
|
+
it('should handle LOCUS_NOT_FOUND and log error if syncMeetings fails', async () => {
|
|
1014
|
+
const syncError = new Error('sync failed');
|
|
1015
|
+
const syncMeetingsStub = sinon.stub(locusInfo.webex.meetings, 'syncMeetings').rejects(syncError);
|
|
1016
|
+
const logErrorStub = LoggerProxy.logger.error?.isSinonProxy
|
|
1017
|
+
? LoggerProxy.logger.error
|
|
1018
|
+
: sinon.stub(LoggerProxy.logger, 'error');
|
|
1019
|
+
|
|
1020
|
+
logErrorStub.resetHistory();
|
|
1021
|
+
|
|
1022
|
+
locusInfoUpdateCallback({updateType: LOCUS_NOT_FOUND});
|
|
1023
|
+
|
|
1024
|
+
assert.calledOnceWithExactly(syncMeetingsStub, {keepOnlyLocusMeetings: false, skipHashTreeSync: true});
|
|
1025
|
+
|
|
1026
|
+
// wait for the promise rejection to be handled
|
|
1027
|
+
await testUtils.flushPromises();
|
|
1028
|
+
|
|
1029
|
+
assert.calledOnce(logErrorStub);
|
|
1030
|
+
assert.match(
|
|
1031
|
+
logErrorStub.firstCall.args[0],
|
|
1032
|
+
/syncMeetings failed after LOCUS_NOT_FOUND/
|
|
1033
|
+
);
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1004
1036
|
it('should set forceReplaceMembers to true on the first update for a locusUrl (initializedFromHashTree is false)', () => {
|
|
1005
1037
|
const createdHashTreeParser = locusInfo.hashTreeParsers.get('fake-locus-url');
|
|
1006
1038
|
createdHashTreeParser.initializedFromHashTree = false;
|
|
@@ -1714,6 +1714,40 @@ describe('plugin-meetings', () => {
|
|
|
1714
1714
|
});
|
|
1715
1715
|
});
|
|
1716
1716
|
|
|
1717
|
+
describe('skipHashTreeSync parameter', () => {
|
|
1718
|
+
it('should skip syncAllHashTreeDatasets when skipHashTreeSync is true', async () => {
|
|
1719
|
+
const mockLocusInfo = {
|
|
1720
|
+
syncAllHashTreeDatasets: sinon.stub().resolves(),
|
|
1721
|
+
};
|
|
1722
|
+
|
|
1723
|
+
webex.meetings.request.getActiveMeetings = sinon.stub().resolves({loci: []});
|
|
1724
|
+
webex.meetings.meetingCollection.getAll = sinon.stub().returns({
|
|
1725
|
+
meeting1: {locusInfo: mockLocusInfo},
|
|
1726
|
+
});
|
|
1727
|
+
|
|
1728
|
+
await webex.meetings.syncMeetings({keepOnlyLocusMeetings: false, skipHashTreeSync: true});
|
|
1729
|
+
|
|
1730
|
+
assert.calledOnce(webex.meetings.request.getActiveMeetings);
|
|
1731
|
+
assert.notCalled(mockLocusInfo.syncAllHashTreeDatasets);
|
|
1732
|
+
});
|
|
1733
|
+
|
|
1734
|
+
it('should call syncAllHashTreeDatasets when skipHashTreeSync is false (default)', async () => {
|
|
1735
|
+
const mockLocusInfo = {
|
|
1736
|
+
syncAllHashTreeDatasets: sinon.stub().resolves(),
|
|
1737
|
+
};
|
|
1738
|
+
|
|
1739
|
+
webex.meetings.request.getActiveMeetings = sinon.stub().resolves({loci: []});
|
|
1740
|
+
webex.meetings.meetingCollection.getAll = sinon.stub().returns({
|
|
1741
|
+
meeting1: {locusInfo: mockLocusInfo},
|
|
1742
|
+
});
|
|
1743
|
+
|
|
1744
|
+
await webex.meetings.syncMeetings({keepOnlyLocusMeetings: false, skipHashTreeSync: false});
|
|
1745
|
+
|
|
1746
|
+
assert.calledOnce(webex.meetings.request.getActiveMeetings);
|
|
1747
|
+
assert.calledOnce(mockLocusInfo.syncAllHashTreeDatasets);
|
|
1748
|
+
});
|
|
1749
|
+
});
|
|
1750
|
+
|
|
1717
1751
|
describe('syncAllHashTreeDatasets in syncMeetings', () => {
|
|
1718
1752
|
it('should call syncAllHashTreeDatasets for multiple meetings, skipping those without locusInfo', async () => {
|
|
1719
1753
|
const mockLocusInfo1 = {
|