@webex/plugin-meetings 3.11.0-next.33 → 3.11.0-next.35
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/constants.js +0 -2
- package/dist/constants.js.map +1 -1
- package/dist/hashTree/constants.js +3 -1
- package/dist/hashTree/constants.js.map +1 -1
- package/dist/hashTree/hashTreeParser.js +293 -301
- 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 +29 -25
- package/dist/locus-info/index.js.map +1 -1
- package/dist/types/constants.d.ts +0 -1
- package/dist/types/hashTree/constants.d.ts +1 -0
- package/dist/types/hashTree/hashTreeParser.d.ts +16 -2
- package/dist/webinar/index.js +1 -1
- package/package.json +1 -1
- package/src/constants.ts +0 -1
- package/src/hashTree/constants.ts +1 -0
- package/src/hashTree/hashTreeParser.ts +158 -117
- package/src/locus-info/index.ts +9 -23
- package/test/unit/spec/hashTree/hashTreeParser.ts +373 -125
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import HashTreeParser, {
|
|
2
2
|
LocusInfoUpdateType,
|
|
3
|
+
MeetingEndedError,
|
|
3
4
|
} from '@webex/plugin-meetings/src/hashTree/hashTreeParser';
|
|
4
5
|
import HashTree from '@webex/plugin-meetings/src/hashTree/hashTree';
|
|
5
6
|
import {expect} from '@webex/test-helper-chai';
|
|
@@ -634,6 +635,46 @@ describe('HashTreeParser', () => {
|
|
|
634
635
|
// callback should not be called, because there are no updates
|
|
635
636
|
assert.notCalled(callback);
|
|
636
637
|
});
|
|
638
|
+
|
|
639
|
+
[404, 409].forEach((errorCode) => {
|
|
640
|
+
it(`emits MeetingEndedError if getting visible datasets returns ${errorCode}`, async () => {
|
|
641
|
+
const minimalInitialLocus = {
|
|
642
|
+
dataSets: [],
|
|
643
|
+
locus: null,
|
|
644
|
+
};
|
|
645
|
+
|
|
646
|
+
const parser = createHashTreeParser(minimalInitialLocus, null);
|
|
647
|
+
|
|
648
|
+
// Mock getAllVisibleDataSetsFromLocus to reject with the error code
|
|
649
|
+
const error: any = new Error(`Request failed with status ${errorCode}`);
|
|
650
|
+
error.statusCode = errorCode;
|
|
651
|
+
if (errorCode === 409) {
|
|
652
|
+
error.body = {errorCode: 2403004};
|
|
653
|
+
}
|
|
654
|
+
webexRequest
|
|
655
|
+
.withArgs(
|
|
656
|
+
sinon.match({
|
|
657
|
+
method: 'GET',
|
|
658
|
+
uri: visibleDataSetsUrl,
|
|
659
|
+
})
|
|
660
|
+
)
|
|
661
|
+
.rejects(error);
|
|
662
|
+
|
|
663
|
+
// initializeFromMessage should throw MeetingEndedError
|
|
664
|
+
let thrownError;
|
|
665
|
+
try {
|
|
666
|
+
await parser.initializeFromMessage({
|
|
667
|
+
dataSets: [],
|
|
668
|
+
visibleDataSetsUrl,
|
|
669
|
+
locusUrl,
|
|
670
|
+
});
|
|
671
|
+
} catch (e) {
|
|
672
|
+
thrownError = e;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
expect(thrownError).to.be.instanceOf(MeetingEndedError);
|
|
676
|
+
});
|
|
677
|
+
});
|
|
637
678
|
});
|
|
638
679
|
|
|
639
680
|
describe('#initializeFromGetLociResponse', () => {
|
|
@@ -1181,7 +1222,7 @@ describe('HashTreeParser', () => {
|
|
|
1181
1222
|
],
|
|
1182
1223
|
};
|
|
1183
1224
|
|
|
1184
|
-
|
|
1225
|
+
parser.handleMessage(normalMessage, 'initial message');
|
|
1185
1226
|
|
|
1186
1227
|
// Verify the timer was set (the sync algorithm should have started)
|
|
1187
1228
|
expect(parser.dataSets.main.timer).to.not.be.undefined;
|
|
@@ -1201,7 +1242,7 @@ describe('HashTreeParser', () => {
|
|
|
1201
1242
|
'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' // still different from our hash
|
|
1202
1243
|
);
|
|
1203
1244
|
|
|
1204
|
-
|
|
1245
|
+
parser.handleMessage(heartbeatMessage, 'heartbeat message');
|
|
1205
1246
|
|
|
1206
1247
|
// Verify the timer was restarted (should still exist)
|
|
1207
1248
|
expect(parser.dataSets.main.timer).to.not.be.undefined;
|
|
@@ -1328,7 +1369,7 @@ describe('HashTreeParser', () => {
|
|
|
1328
1369
|
],
|
|
1329
1370
|
};
|
|
1330
1371
|
|
|
1331
|
-
|
|
1372
|
+
parser.handleMessage(message, 'normal update');
|
|
1332
1373
|
|
|
1333
1374
|
// Verify updateItems was called on main hash tree
|
|
1334
1375
|
assert.calledOnceWithExactly(mainUpdateItemsStub, [
|
|
@@ -1381,43 +1422,71 @@ describe('HashTreeParser', () => {
|
|
|
1381
1422
|
});
|
|
1382
1423
|
});
|
|
1383
1424
|
|
|
1384
|
-
|
|
1385
|
-
|
|
1425
|
+
describe('handles sentinel messages correctly', () => {
|
|
1426
|
+
['main', 'self', 'unjoined'].forEach((dataSetName) => {
|
|
1427
|
+
it('emits MEETING_ENDED for sentinel message with dataset ' + dataSetName, async () => {
|
|
1428
|
+
const parser = createHashTreeParser();
|
|
1429
|
+
|
|
1430
|
+
// Create a sentinel message: leafCount=1, root=EMPTY_HASH, version higher than current
|
|
1431
|
+
const sentinelMessage = createHeartbeatMessage(
|
|
1432
|
+
dataSetName,
|
|
1433
|
+
1,
|
|
1434
|
+
parser.dataSets[dataSetName]?.version
|
|
1435
|
+
? parser.dataSets[dataSetName].version + 1
|
|
1436
|
+
: 10000,
|
|
1437
|
+
EMPTY_HASH
|
|
1438
|
+
);
|
|
1439
|
+
|
|
1440
|
+
// If the dataset doesn't exist yet (e.g. 'unjoined'), create it
|
|
1441
|
+
if (!parser.dataSets[dataSetName]) {
|
|
1442
|
+
parser.dataSets[dataSetName] = {
|
|
1443
|
+
url: `https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/${dataSetName}`,
|
|
1444
|
+
name: dataSetName,
|
|
1445
|
+
version: 1,
|
|
1446
|
+
leafCount: 16,
|
|
1447
|
+
root: '0'.repeat(32),
|
|
1448
|
+
idleMs: 1000,
|
|
1449
|
+
backoff: {maxMs: 1000, exponent: 2},
|
|
1450
|
+
} as any;
|
|
1451
|
+
}
|
|
1386
1452
|
|
|
1387
|
-
|
|
1388
|
-
sinon.stub(parser.dataSets.self.hashTree, 'updateItems').returns([true]);
|
|
1453
|
+
parser.handleMessage(sentinelMessage, 'sentinel message');
|
|
1389
1454
|
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
locusUrl,
|
|
1395
|
-
locusStateElements: [
|
|
1396
|
-
{
|
|
1397
|
-
htMeta: {
|
|
1398
|
-
elementId: {
|
|
1399
|
-
type: 'self' as const,
|
|
1400
|
-
id: 4,
|
|
1401
|
-
version: 102,
|
|
1402
|
-
},
|
|
1403
|
-
dataSetNames: ['self'],
|
|
1404
|
-
},
|
|
1405
|
-
data: undefined, // No data - this indicates roster drop
|
|
1406
|
-
},
|
|
1407
|
-
],
|
|
1408
|
-
};
|
|
1409
|
-
|
|
1410
|
-
await parser.handleMessage(rosterDropMessage, 'roster drop message');
|
|
1455
|
+
// Verify callback was called with MEETING_ENDED
|
|
1456
|
+
assert.calledOnceWithExactly(callback, LocusInfoUpdateType.MEETING_ENDED, {
|
|
1457
|
+
updatedObjects: undefined,
|
|
1458
|
+
});
|
|
1411
1459
|
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1460
|
+
// Verify that all timers were stopped
|
|
1461
|
+
Object.values(parser.dataSets).forEach((ds: any) => {
|
|
1462
|
+
assert.isUndefined(ds.timer);
|
|
1463
|
+
assert.isUndefined(ds.heartbeatWatchdogTimer);
|
|
1464
|
+
});
|
|
1465
|
+
});
|
|
1415
1466
|
});
|
|
1416
1467
|
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1468
|
+
it('emits MEETING_ENDED for sentinel message with unknown dataset', async () => {
|
|
1469
|
+
const parser = createHashTreeParser();
|
|
1470
|
+
|
|
1471
|
+
// 'unjoined' is a valid sentinel dataset name but is not tracked by the parser
|
|
1472
|
+
assert.isUndefined(parser.dataSets['unjoined']);
|
|
1473
|
+
|
|
1474
|
+
// Create a sentinel message for 'unjoined' dataset which the parser has never seen
|
|
1475
|
+
const sentinelMessage = createHeartbeatMessage('unjoined', 1, 10000, EMPTY_HASH);
|
|
1476
|
+
|
|
1477
|
+
parser.handleMessage(sentinelMessage, 'sentinel message');
|
|
1478
|
+
|
|
1479
|
+
// Verify callback was called with MEETING_ENDED
|
|
1480
|
+
assert.calledOnceWithExactly(callback, LocusInfoUpdateType.MEETING_ENDED, {
|
|
1481
|
+
updatedObjects: undefined,
|
|
1482
|
+
});
|
|
1483
|
+
|
|
1484
|
+
// Verify that all timers were stopped
|
|
1485
|
+
Object.values(parser.dataSets).forEach((ds: any) => {
|
|
1486
|
+
assert.isUndefined(ds.timer);
|
|
1487
|
+
assert.isUndefined(ds.heartbeatWatchdogTimer);
|
|
1488
|
+
});
|
|
1489
|
+
});
|
|
1421
1490
|
});
|
|
1422
1491
|
|
|
1423
1492
|
describe('sync algorithm', () => {
|
|
@@ -1448,7 +1517,7 @@ describe('HashTreeParser', () => {
|
|
|
1448
1517
|
],
|
|
1449
1518
|
};
|
|
1450
1519
|
|
|
1451
|
-
|
|
1520
|
+
parser.handleMessage(message, 'initial message');
|
|
1452
1521
|
|
|
1453
1522
|
// Verify callback was called with initial updates
|
|
1454
1523
|
assert.calledOnce(callback);
|
|
@@ -1518,6 +1587,134 @@ describe('HashTreeParser', () => {
|
|
|
1518
1587
|
],
|
|
1519
1588
|
});
|
|
1520
1589
|
});
|
|
1590
|
+
|
|
1591
|
+
describe('emits MEETING_ENDED', () => {
|
|
1592
|
+
[404, 409].forEach((statusCode) => {
|
|
1593
|
+
it(`when /hashtree returns ${statusCode}`, async () => {
|
|
1594
|
+
const parser = createHashTreeParser();
|
|
1595
|
+
|
|
1596
|
+
// Send a message to trigger sync algorithm
|
|
1597
|
+
const message = {
|
|
1598
|
+
dataSets: [createDataSet('main', 16, 1100)],
|
|
1599
|
+
visibleDataSetsUrl,
|
|
1600
|
+
locusUrl,
|
|
1601
|
+
locusStateElements: [
|
|
1602
|
+
{
|
|
1603
|
+
htMeta: {
|
|
1604
|
+
elementId: {
|
|
1605
|
+
type: 'locus' as const,
|
|
1606
|
+
id: 0,
|
|
1607
|
+
version: 201,
|
|
1608
|
+
},
|
|
1609
|
+
dataSetNames: ['main'],
|
|
1610
|
+
},
|
|
1611
|
+
data: {info: {id: 'initial-update'}},
|
|
1612
|
+
},
|
|
1613
|
+
],
|
|
1614
|
+
};
|
|
1615
|
+
|
|
1616
|
+
parser.handleMessage(message, 'initial message');
|
|
1617
|
+
callback.resetHistory();
|
|
1618
|
+
|
|
1619
|
+
const mainDataSetUrl = parser.dataSets.main.url;
|
|
1620
|
+
|
|
1621
|
+
// Mock getHashesFromLocus to reject with the sentinel error
|
|
1622
|
+
const error: any = new Error(`Request failed with status ${statusCode}`);
|
|
1623
|
+
error.statusCode = statusCode;
|
|
1624
|
+
if (statusCode === 409) {
|
|
1625
|
+
error.body = {errorCode: 2403004};
|
|
1626
|
+
}
|
|
1627
|
+
webexRequest
|
|
1628
|
+
.withArgs(
|
|
1629
|
+
sinon.match({
|
|
1630
|
+
method: 'GET',
|
|
1631
|
+
uri: `${mainDataSetUrl}/hashtree`,
|
|
1632
|
+
})
|
|
1633
|
+
)
|
|
1634
|
+
.rejects(error);
|
|
1635
|
+
|
|
1636
|
+
// Trigger sync by advancing time
|
|
1637
|
+
await clock.tickAsync(1000);
|
|
1638
|
+
|
|
1639
|
+
// Verify callback was called with MEETING_ENDED
|
|
1640
|
+
assert.calledOnceWithExactly(callback, LocusInfoUpdateType.MEETING_ENDED, {
|
|
1641
|
+
updatedObjects: undefined,
|
|
1642
|
+
});
|
|
1643
|
+
|
|
1644
|
+
// Verify all timers are stopped
|
|
1645
|
+
Object.values(parser.dataSets).forEach((ds: any) => {
|
|
1646
|
+
assert.isUndefined(ds.timer);
|
|
1647
|
+
assert.isUndefined(ds.heartbeatWatchdogTimer);
|
|
1648
|
+
});
|
|
1649
|
+
});
|
|
1650
|
+
|
|
1651
|
+
it(`when /sync returns ${statusCode}`, async () => {
|
|
1652
|
+
const parser = createHashTreeParser();
|
|
1653
|
+
|
|
1654
|
+
// Send a message to trigger sync algorithm
|
|
1655
|
+
const message = {
|
|
1656
|
+
dataSets: [createDataSet('main', 16, 1100)],
|
|
1657
|
+
visibleDataSetsUrl,
|
|
1658
|
+
locusUrl,
|
|
1659
|
+
locusStateElements: [
|
|
1660
|
+
{
|
|
1661
|
+
htMeta: {
|
|
1662
|
+
elementId: {
|
|
1663
|
+
type: 'locus' as const,
|
|
1664
|
+
id: 0,
|
|
1665
|
+
version: 201,
|
|
1666
|
+
},
|
|
1667
|
+
dataSetNames: ['main'],
|
|
1668
|
+
},
|
|
1669
|
+
data: {info: {id: 'initial-update'}},
|
|
1670
|
+
},
|
|
1671
|
+
],
|
|
1672
|
+
};
|
|
1673
|
+
|
|
1674
|
+
parser.handleMessage(message, 'initial message');
|
|
1675
|
+
callback.resetHistory();
|
|
1676
|
+
|
|
1677
|
+
const mainDataSetUrl = parser.dataSets.main.url;
|
|
1678
|
+
|
|
1679
|
+
// Mock getHashesFromLocus to succeed
|
|
1680
|
+
mockGetHashesFromLocusResponse(
|
|
1681
|
+
mainDataSetUrl,
|
|
1682
|
+
new Array(16).fill('00000000000000000000000000000000'),
|
|
1683
|
+
createDataSet('main', 16, 1101)
|
|
1684
|
+
);
|
|
1685
|
+
|
|
1686
|
+
// Mock sendSyncRequestToLocus to reject with the sentinel error
|
|
1687
|
+
const error: any = new Error(`Request failed with status ${statusCode}`);
|
|
1688
|
+
error.statusCode = statusCode;
|
|
1689
|
+
if (statusCode === 409) {
|
|
1690
|
+
error.body = {errorCode: 2403004};
|
|
1691
|
+
}
|
|
1692
|
+
webexRequest
|
|
1693
|
+
.withArgs(
|
|
1694
|
+
sinon.match({
|
|
1695
|
+
method: 'POST',
|
|
1696
|
+
uri: `${mainDataSetUrl}/sync`,
|
|
1697
|
+
})
|
|
1698
|
+
)
|
|
1699
|
+
.rejects(error);
|
|
1700
|
+
|
|
1701
|
+
// Trigger sync by advancing time
|
|
1702
|
+
await clock.tickAsync(1000);
|
|
1703
|
+
|
|
1704
|
+
// Verify callback was called with MEETING_ENDED
|
|
1705
|
+
assert.calledOnceWithExactly(callback, LocusInfoUpdateType.MEETING_ENDED, {
|
|
1706
|
+
updatedObjects: undefined,
|
|
1707
|
+
});
|
|
1708
|
+
|
|
1709
|
+
// Verify all timers are stopped
|
|
1710
|
+
Object.values(parser.dataSets).forEach((ds: any) => {
|
|
1711
|
+
assert.isUndefined(ds.timer);
|
|
1712
|
+
assert.isUndefined(ds.heartbeatWatchdogTimer);
|
|
1713
|
+
});
|
|
1714
|
+
});
|
|
1715
|
+
});
|
|
1716
|
+
});
|
|
1717
|
+
|
|
1521
1718
|
it('requests only mismatched hashes during sync', async () => {
|
|
1522
1719
|
const parser = createHashTreeParser();
|
|
1523
1720
|
|
|
@@ -1563,7 +1760,7 @@ describe('HashTreeParser', () => {
|
|
|
1563
1760
|
],
|
|
1564
1761
|
};
|
|
1565
1762
|
|
|
1566
|
-
|
|
1763
|
+
parser.handleMessage(message, 'initial message');
|
|
1567
1764
|
|
|
1568
1765
|
callback.resetHistory();
|
|
1569
1766
|
|
|
@@ -1651,7 +1848,7 @@ describe('HashTreeParser', () => {
|
|
|
1651
1848
|
],
|
|
1652
1849
|
};
|
|
1653
1850
|
|
|
1654
|
-
|
|
1851
|
+
parser.handleMessage(message, 'message with self update');
|
|
1655
1852
|
|
|
1656
1853
|
callback.resetHistory();
|
|
1657
1854
|
|
|
@@ -1735,7 +1932,7 @@ describe('HashTreeParser', () => {
|
|
|
1735
1932
|
],
|
|
1736
1933
|
};
|
|
1737
1934
|
|
|
1738
|
-
|
|
1935
|
+
parser.handleMessage(message, 'add visible dataset');
|
|
1739
1936
|
|
|
1740
1937
|
// Verify that 'attendees' was added to visibleDataSets
|
|
1741
1938
|
expect(parser.visibleDataSets.some((vds) => vds.name === 'attendees')).to.be.true;
|
|
@@ -1860,11 +2057,82 @@ describe('HashTreeParser', () => {
|
|
|
1860
2057
|
locusStateElements: [],
|
|
1861
2058
|
});
|
|
1862
2059
|
|
|
1863
|
-
|
|
2060
|
+
parser.handleMessage(message, 'add new dataset requiring async init');
|
|
1864
2061
|
|
|
1865
2062
|
await checkAsyncDatasetInitialization(parser, newDataSet);
|
|
1866
2063
|
});
|
|
1867
2064
|
|
|
2065
|
+
it('emits MEETING_ENDED if async init of a new visible dataset fails with 404', async () => {
|
|
2066
|
+
const parser = createHashTreeParser();
|
|
2067
|
+
|
|
2068
|
+
// Stub updateItems on self hash tree to return true
|
|
2069
|
+
sinon.stub(parser.dataSets.self.hashTree, 'updateItems').returns([true]);
|
|
2070
|
+
|
|
2071
|
+
// Send a message with Metadata object that adds a new visible dataset
|
|
2072
|
+
const message = {
|
|
2073
|
+
dataSets: [createDataSet('self', 1, 2100)],
|
|
2074
|
+
visibleDataSetsUrl,
|
|
2075
|
+
locusUrl,
|
|
2076
|
+
locusStateElements: [
|
|
2077
|
+
{
|
|
2078
|
+
htMeta: {
|
|
2079
|
+
elementId: {
|
|
2080
|
+
type: 'metadata' as const,
|
|
2081
|
+
id: 5,
|
|
2082
|
+
version: 51,
|
|
2083
|
+
},
|
|
2084
|
+
dataSetNames: ['self'],
|
|
2085
|
+
},
|
|
2086
|
+
data: {
|
|
2087
|
+
visibleDataSets: [
|
|
2088
|
+
{
|
|
2089
|
+
name: 'main',
|
|
2090
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/main',
|
|
2091
|
+
},
|
|
2092
|
+
{
|
|
2093
|
+
name: 'self',
|
|
2094
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/713e9f99/datasets/self',
|
|
2095
|
+
},
|
|
2096
|
+
{
|
|
2097
|
+
name: 'atd-unmuted',
|
|
2098
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/atd-unmuted',
|
|
2099
|
+
},
|
|
2100
|
+
{
|
|
2101
|
+
name: 'new-dataset',
|
|
2102
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/datasets/new-dataset',
|
|
2103
|
+
},
|
|
2104
|
+
],
|
|
2105
|
+
},
|
|
2106
|
+
},
|
|
2107
|
+
],
|
|
2108
|
+
};
|
|
2109
|
+
|
|
2110
|
+
// Mock getAllDataSetsMetadata to reject with 404
|
|
2111
|
+
const error: any = new Error('Request failed with status 404');
|
|
2112
|
+
error.statusCode = 404;
|
|
2113
|
+
webexRequest
|
|
2114
|
+
.withArgs(
|
|
2115
|
+
sinon.match({
|
|
2116
|
+
method: 'GET',
|
|
2117
|
+
uri: visibleDataSetsUrl,
|
|
2118
|
+
})
|
|
2119
|
+
)
|
|
2120
|
+
.rejects(error);
|
|
2121
|
+
|
|
2122
|
+
parser.handleMessage(message, 'add new dataset triggering 404');
|
|
2123
|
+
|
|
2124
|
+
// The first callback call is from parseMessage with the metadata update
|
|
2125
|
+
callback.resetHistory();
|
|
2126
|
+
|
|
2127
|
+
// Wait for the async initialization (queueMicrotask) to complete
|
|
2128
|
+
await clock.tickAsync(0);
|
|
2129
|
+
|
|
2130
|
+
// Verify callback was called with MEETING_ENDED
|
|
2131
|
+
assert.calledOnceWithExactly(callback, LocusInfoUpdateType.MEETING_ENDED, {
|
|
2132
|
+
updatedObjects: undefined,
|
|
2133
|
+
});
|
|
2134
|
+
});
|
|
2135
|
+
|
|
1868
2136
|
it('handles removal of visible data set', async () => {
|
|
1869
2137
|
// Create a parser with visible datasets
|
|
1870
2138
|
const parser = createHashTreeParser();
|
|
@@ -1913,7 +2181,7 @@ describe('HashTreeParser', () => {
|
|
|
1913
2181
|
],
|
|
1914
2182
|
};
|
|
1915
2183
|
|
|
1916
|
-
|
|
2184
|
+
parser.handleMessage(message, 'remove visible dataset');
|
|
1917
2185
|
|
|
1918
2186
|
// Verify that 'atd-unmuted' was removed from visibleDataSets
|
|
1919
2187
|
expect(parser.visibleDataSets.some((vds) => vds.name === 'atd-unmuted')).to.be.false;
|
|
@@ -2014,7 +2282,7 @@ describe('HashTreeParser', () => {
|
|
|
2014
2282
|
],
|
|
2015
2283
|
};
|
|
2016
2284
|
|
|
2017
|
-
|
|
2285
|
+
parser.handleMessage(message, 'message with non-visible dataset');
|
|
2018
2286
|
|
|
2019
2287
|
// Verify that no hash tree was created for attendees
|
|
2020
2288
|
assert.isUndefined(parser.dataSets.attendees.hashTree);
|
|
@@ -2042,7 +2310,7 @@ describe('HashTreeParser', () => {
|
|
|
2042
2310
|
heartbeatIntervalMs,
|
|
2043
2311
|
};
|
|
2044
2312
|
|
|
2045
|
-
|
|
2313
|
+
parser.handleMessage(heartbeatMessage, 'initial heartbeat');
|
|
2046
2314
|
|
|
2047
2315
|
// Verify only 'main' watchdog timer is set
|
|
2048
2316
|
expect(parser.dataSets.main.heartbeatWatchdogTimer).to.not.be.undefined;
|
|
@@ -2106,7 +2374,7 @@ describe('HashTreeParser', () => {
|
|
|
2106
2374
|
heartbeatIntervalMs,
|
|
2107
2375
|
};
|
|
2108
2376
|
|
|
2109
|
-
|
|
2377
|
+
parser.handleMessage(heartbeatMessage, 'self heartbeat');
|
|
2110
2378
|
|
|
2111
2379
|
// Mock sync response for self
|
|
2112
2380
|
mockSendSyncRequestResponse(parser.dataSets.self.url, null);
|
|
@@ -2153,7 +2421,7 @@ describe('HashTreeParser', () => {
|
|
|
2153
2421
|
heartbeatIntervalMs,
|
|
2154
2422
|
};
|
|
2155
2423
|
|
|
2156
|
-
|
|
2424
|
+
parser.handleMessage(heartbeatMessage, 'multi-dataset heartbeat');
|
|
2157
2425
|
|
|
2158
2426
|
// Watchdog timers should be set for both datasets in the message
|
|
2159
2427
|
expect(parser.dataSets.main.heartbeatWatchdogTimer).to.not.be.undefined;
|
|
@@ -2179,7 +2447,7 @@ describe('HashTreeParser', () => {
|
|
|
2179
2447
|
heartbeatIntervalMs,
|
|
2180
2448
|
};
|
|
2181
2449
|
|
|
2182
|
-
|
|
2450
|
+
parser.handleMessage(heartbeat1, 'first heartbeat');
|
|
2183
2451
|
|
|
2184
2452
|
const firstTimer = parser.dataSets.main.heartbeatWatchdogTimer;
|
|
2185
2453
|
expect(firstTimer).to.not.be.undefined;
|
|
@@ -2200,7 +2468,7 @@ describe('HashTreeParser', () => {
|
|
|
2200
2468
|
heartbeatIntervalMs,
|
|
2201
2469
|
};
|
|
2202
2470
|
|
|
2203
|
-
|
|
2471
|
+
parser.handleMessage(heartbeat2, 'second heartbeat');
|
|
2204
2472
|
|
|
2205
2473
|
const secondTimer = parser.dataSets.main.heartbeatWatchdogTimer;
|
|
2206
2474
|
expect(secondTimer).to.not.be.undefined;
|
|
@@ -2231,7 +2499,7 @@ describe('HashTreeParser', () => {
|
|
|
2231
2499
|
heartbeatIntervalMs,
|
|
2232
2500
|
};
|
|
2233
2501
|
|
|
2234
|
-
|
|
2502
|
+
parser.handleMessage(heartbeat, 'initial heartbeat');
|
|
2235
2503
|
|
|
2236
2504
|
const firstTimer = parser.dataSets.main.heartbeatWatchdogTimer;
|
|
2237
2505
|
expect(firstTimer).to.not.be.undefined;
|
|
@@ -2259,7 +2527,7 @@ describe('HashTreeParser', () => {
|
|
|
2259
2527
|
heartbeatIntervalMs,
|
|
2260
2528
|
};
|
|
2261
2529
|
|
|
2262
|
-
|
|
2530
|
+
parser.handleMessage(normalMessage, 'normal message');
|
|
2263
2531
|
|
|
2264
2532
|
const secondTimer = parser.dataSets.main.heartbeatWatchdogTimer;
|
|
2265
2533
|
expect(secondTimer).to.not.be.undefined;
|
|
@@ -2277,12 +2545,12 @@ describe('HashTreeParser', () => {
|
|
|
2277
2545
|
parser.dataSets.main.hashTree.getRootHash()
|
|
2278
2546
|
);
|
|
2279
2547
|
|
|
2280
|
-
|
|
2548
|
+
parser.handleMessage(heartbeatMessage, 'heartbeat without interval');
|
|
2281
2549
|
|
|
2282
2550
|
expect(parser.dataSets.main.heartbeatWatchdogTimer).to.be.undefined;
|
|
2283
2551
|
});
|
|
2284
2552
|
|
|
2285
|
-
it('stops all watchdog timers when meeting ends', async () => {
|
|
2553
|
+
it('stops all watchdog timers when meeting ends via sentinel message', async () => {
|
|
2286
2554
|
const parser = createHashTreeParser();
|
|
2287
2555
|
const heartbeatIntervalMs = 5000;
|
|
2288
2556
|
|
|
@@ -2304,34 +2572,22 @@ describe('HashTreeParser', () => {
|
|
|
2304
2572
|
heartbeatIntervalMs,
|
|
2305
2573
|
};
|
|
2306
2574
|
|
|
2307
|
-
|
|
2575
|
+
parser.handleMessage(heartbeat, 'initial heartbeat');
|
|
2308
2576
|
|
|
2309
2577
|
expect(parser.dataSets.main.heartbeatWatchdogTimer).to.not.be.undefined;
|
|
2310
2578
|
expect(parser.dataSets.self.heartbeatWatchdogTimer).to.not.be.undefined;
|
|
2311
2579
|
|
|
2312
|
-
//
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
locusUrl,
|
|
2320
|
-
locusStateElements: [
|
|
2321
|
-
{
|
|
2322
|
-
htMeta: {
|
|
2323
|
-
elementId: {type: 'self' as const, id: 4, version: 102},
|
|
2324
|
-
dataSetNames: ['self'],
|
|
2325
|
-
},
|
|
2326
|
-
data: undefined,
|
|
2327
|
-
},
|
|
2328
|
-
],
|
|
2329
|
-
heartbeatIntervalMs,
|
|
2330
|
-
};
|
|
2580
|
+
// Send a sentinel END MEETING message
|
|
2581
|
+
const sentinelMessage = createHeartbeatMessage(
|
|
2582
|
+
'main',
|
|
2583
|
+
1,
|
|
2584
|
+
parser.dataSets.main.version + 1,
|
|
2585
|
+
EMPTY_HASH
|
|
2586
|
+
);
|
|
2331
2587
|
|
|
2332
|
-
|
|
2588
|
+
parser.handleMessage(sentinelMessage as any, 'sentinel message');
|
|
2333
2589
|
|
|
2334
|
-
// All watchdog timers should have been stopped
|
|
2590
|
+
// All watchdog timers should have been stopped
|
|
2335
2591
|
expect(parser.dataSets.main.heartbeatWatchdogTimer).to.be.undefined;
|
|
2336
2592
|
expect(parser.dataSets.self.heartbeatWatchdogTimer).to.be.undefined;
|
|
2337
2593
|
});
|
|
@@ -2389,7 +2645,7 @@ describe('HashTreeParser', () => {
|
|
|
2389
2645
|
heartbeatIntervalMs,
|
|
2390
2646
|
};
|
|
2391
2647
|
|
|
2392
|
-
|
|
2648
|
+
parser.handleMessage(heartbeat, 'heartbeat');
|
|
2393
2649
|
|
|
2394
2650
|
// 'main' watchdog delay = 5000 + 1^2 * 500 = 5500ms
|
|
2395
2651
|
// 'self' watchdog delay = 5000 + 1^3 * 2000 = 7000ms
|
|
@@ -2453,7 +2709,7 @@ describe('HashTreeParser', () => {
|
|
|
2453
2709
|
heartbeatIntervalMs,
|
|
2454
2710
|
};
|
|
2455
2711
|
|
|
2456
|
-
|
|
2712
|
+
parser.handleMessage(heartbeatMessage, 'heartbeat with non-visible dataset');
|
|
2457
2713
|
|
|
2458
2714
|
// Watchdog set for main (visible) but not for atd-active (no hash tree)
|
|
2459
2715
|
expect(parser.dataSets.main.heartbeatWatchdogTimer).to.not.be.undefined;
|
|
@@ -2464,7 +2720,7 @@ describe('HashTreeParser', () => {
|
|
|
2464
2720
|
|
|
2465
2721
|
describe('#callLocusInfoUpdateCallback filtering', () => {
|
|
2466
2722
|
// Helper to setup parser with initial objects and reset callback history
|
|
2467
|
-
|
|
2723
|
+
function setupParserWithObjects(locusStateElements: any[]) {
|
|
2468
2724
|
const parser = createHashTreeParser();
|
|
2469
2725
|
|
|
2470
2726
|
if (locusStateElements.length > 0) {
|
|
@@ -2486,15 +2742,15 @@ describe('HashTreeParser', () => {
|
|
|
2486
2742
|
locusStateElements,
|
|
2487
2743
|
};
|
|
2488
2744
|
|
|
2489
|
-
|
|
2745
|
+
parser.handleMessage(setupMessage, 'setup');
|
|
2490
2746
|
}
|
|
2491
2747
|
|
|
2492
2748
|
callback.resetHistory();
|
|
2493
2749
|
return parser;
|
|
2494
2750
|
}
|
|
2495
2751
|
|
|
2496
|
-
it('filters out updates when a dataset has a higher version',
|
|
2497
|
-
const parser =
|
|
2752
|
+
it('filters out updates when a dataset has a higher version', () => {
|
|
2753
|
+
const parser = setupParserWithObjects([
|
|
2498
2754
|
{
|
|
2499
2755
|
htMeta: {
|
|
2500
2756
|
elementId: {type: 'locus' as const, id: 5, version: 100},
|
|
@@ -2520,14 +2776,14 @@ describe('HashTreeParser', () => {
|
|
|
2520
2776
|
],
|
|
2521
2777
|
};
|
|
2522
2778
|
|
|
2523
|
-
|
|
2779
|
+
parser.handleMessage(updateMessage, 'update with older version');
|
|
2524
2780
|
|
|
2525
2781
|
// Callback should not be called because the update was filtered out
|
|
2526
2782
|
assert.notCalled(callback);
|
|
2527
2783
|
});
|
|
2528
2784
|
|
|
2529
|
-
it('allows updates when version is newer than existing',
|
|
2530
|
-
const parser =
|
|
2785
|
+
it('allows updates when version is newer than existing', () => {
|
|
2786
|
+
const parser = setupParserWithObjects([
|
|
2531
2787
|
{
|
|
2532
2788
|
htMeta: {
|
|
2533
2789
|
elementId: {type: 'locus' as const, id: 5, version: 100},
|
|
@@ -2553,7 +2809,7 @@ describe('HashTreeParser', () => {
|
|
|
2553
2809
|
],
|
|
2554
2810
|
};
|
|
2555
2811
|
|
|
2556
|
-
|
|
2812
|
+
parser.handleMessage(updateMessage, 'update with newer version');
|
|
2557
2813
|
|
|
2558
2814
|
// Callback should be called with the update
|
|
2559
2815
|
assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED, {
|
|
@@ -2569,8 +2825,8 @@ describe('HashTreeParser', () => {
|
|
|
2569
2825
|
});
|
|
2570
2826
|
});
|
|
2571
2827
|
|
|
2572
|
-
it('filters out removal when object still exists in any dataset',
|
|
2573
|
-
const parser =
|
|
2828
|
+
it('filters out removal when object still exists in any dataset', () => {
|
|
2829
|
+
const parser = setupParserWithObjects([
|
|
2574
2830
|
{
|
|
2575
2831
|
htMeta: {
|
|
2576
2832
|
elementId: {type: 'participant' as const, id: 10, version: 50},
|
|
@@ -2596,14 +2852,14 @@ describe('HashTreeParser', () => {
|
|
|
2596
2852
|
],
|
|
2597
2853
|
};
|
|
2598
2854
|
|
|
2599
|
-
|
|
2855
|
+
parser.handleMessage(removalMessage, 'removal from one dataset');
|
|
2600
2856
|
|
|
2601
2857
|
// Callback should not be called because object still exists in atd-unmuted
|
|
2602
2858
|
assert.notCalled(callback);
|
|
2603
2859
|
});
|
|
2604
2860
|
|
|
2605
|
-
it('allows removal when object does not exist in any dataset',
|
|
2606
|
-
const parser =
|
|
2861
|
+
it('allows removal when object does not exist in any dataset', () => {
|
|
2862
|
+
const parser = setupParserWithObjects([]);
|
|
2607
2863
|
|
|
2608
2864
|
// Stub updateItems to return true (simulating that the removal was "applied")
|
|
2609
2865
|
sinon.stub(parser.dataSets.main.hashTree, 'updateItems').returns([true]);
|
|
@@ -2624,7 +2880,7 @@ describe('HashTreeParser', () => {
|
|
|
2624
2880
|
],
|
|
2625
2881
|
};
|
|
2626
2882
|
|
|
2627
|
-
|
|
2883
|
+
parser.handleMessage(removalMessage, 'removal of non-existent object');
|
|
2628
2884
|
|
|
2629
2885
|
// Callback should be called with the removal
|
|
2630
2886
|
assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED, {
|
|
@@ -2640,11 +2896,11 @@ describe('HashTreeParser', () => {
|
|
|
2640
2896
|
});
|
|
2641
2897
|
});
|
|
2642
2898
|
|
|
2643
|
-
it('filters out removal when object exists in another dataset with newer version',
|
|
2899
|
+
it('filters out removal when object exists in another dataset with newer version', () => {
|
|
2644
2900
|
const parser = createHashTreeParser();
|
|
2645
2901
|
|
|
2646
2902
|
// Setup: Add object to main with version 40
|
|
2647
|
-
|
|
2903
|
+
parser.handleMessage(
|
|
2648
2904
|
{
|
|
2649
2905
|
dataSets: [createDataSet('main', 16, 1100)],
|
|
2650
2906
|
visibleDataSetsUrl,
|
|
@@ -2663,7 +2919,7 @@ describe('HashTreeParser', () => {
|
|
|
2663
2919
|
);
|
|
2664
2920
|
|
|
2665
2921
|
// Add object to atd-unmuted with version 50
|
|
2666
|
-
|
|
2922
|
+
parser.handleMessage(
|
|
2667
2923
|
{
|
|
2668
2924
|
dataSets: [createDataSet('atd-unmuted', 16, 3100)],
|
|
2669
2925
|
visibleDataSetsUrl,
|
|
@@ -2698,14 +2954,14 @@ describe('HashTreeParser', () => {
|
|
|
2698
2954
|
],
|
|
2699
2955
|
};
|
|
2700
2956
|
|
|
2701
|
-
|
|
2957
|
+
parser.handleMessage(removalMessage, 'removal with older version');
|
|
2702
2958
|
|
|
2703
2959
|
// Callback should not be called because object still exists with newer version
|
|
2704
2960
|
assert.notCalled(callback);
|
|
2705
2961
|
});
|
|
2706
2962
|
|
|
2707
|
-
it('filters mixed updates correctly - some pass, some filtered',
|
|
2708
|
-
const parser =
|
|
2963
|
+
it('filters mixed updates correctly - some pass, some filtered', () => {
|
|
2964
|
+
const parser = setupParserWithObjects([
|
|
2709
2965
|
{
|
|
2710
2966
|
htMeta: {
|
|
2711
2967
|
elementId: {type: 'participant' as const, id: 1, version: 100},
|
|
@@ -2759,7 +3015,7 @@ describe('HashTreeParser', () => {
|
|
|
2759
3015
|
],
|
|
2760
3016
|
};
|
|
2761
3017
|
|
|
2762
|
-
|
|
3018
|
+
parser.handleMessage(mixedMessage, 'mixed updates');
|
|
2763
3019
|
|
|
2764
3020
|
// Callback should be called with only the valid updates (participant 1 v110 and participant 3 v10)
|
|
2765
3021
|
assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED, {
|
|
@@ -2782,8 +3038,8 @@ describe('HashTreeParser', () => {
|
|
|
2782
3038
|
});
|
|
2783
3039
|
});
|
|
2784
3040
|
|
|
2785
|
-
it('does not call callback when all updates are filtered out',
|
|
2786
|
-
const parser =
|
|
3041
|
+
it('does not call callback when all updates are filtered out', () => {
|
|
3042
|
+
const parser = setupParserWithObjects([
|
|
2787
3043
|
{
|
|
2788
3044
|
htMeta: {
|
|
2789
3045
|
elementId: {type: 'locus' as const, id: 5, version: 100},
|
|
@@ -2816,17 +3072,17 @@ describe('HashTreeParser', () => {
|
|
|
2816
3072
|
],
|
|
2817
3073
|
};
|
|
2818
3074
|
|
|
2819
|
-
|
|
3075
|
+
parser.handleMessage(updateMessage, 'all filtered updates');
|
|
2820
3076
|
|
|
2821
3077
|
// Callback should not be called at all
|
|
2822
3078
|
assert.notCalled(callback);
|
|
2823
3079
|
});
|
|
2824
3080
|
|
|
2825
|
-
it('checks all visible datasets when filtering',
|
|
3081
|
+
it('checks all visible datasets when filtering', () => {
|
|
2826
3082
|
const parser = createHashTreeParser();
|
|
2827
3083
|
|
|
2828
3084
|
// Setup: Add same object to multiple datasets with different versions
|
|
2829
|
-
|
|
3085
|
+
parser.handleMessage(
|
|
2830
3086
|
{
|
|
2831
3087
|
dataSets: [createDataSet('main', 16, 1100)],
|
|
2832
3088
|
visibleDataSetsUrl,
|
|
@@ -2844,7 +3100,7 @@ describe('HashTreeParser', () => {
|
|
|
2844
3100
|
'setup main'
|
|
2845
3101
|
);
|
|
2846
3102
|
|
|
2847
|
-
|
|
3103
|
+
parser.handleMessage(
|
|
2848
3104
|
{
|
|
2849
3105
|
dataSets: [createDataSet('self', 1, 2100)],
|
|
2850
3106
|
visibleDataSetsUrl,
|
|
@@ -2862,7 +3118,7 @@ describe('HashTreeParser', () => {
|
|
|
2862
3118
|
'setup self'
|
|
2863
3119
|
);
|
|
2864
3120
|
|
|
2865
|
-
|
|
3121
|
+
parser.handleMessage(
|
|
2866
3122
|
{
|
|
2867
3123
|
dataSets: [createDataSet('atd-unmuted', 16, 3100)],
|
|
2868
3124
|
visibleDataSetsUrl,
|
|
@@ -2897,14 +3153,14 @@ describe('HashTreeParser', () => {
|
|
|
2897
3153
|
],
|
|
2898
3154
|
};
|
|
2899
3155
|
|
|
2900
|
-
|
|
3156
|
+
parser.handleMessage(updateMessage, 'update with v115');
|
|
2901
3157
|
|
|
2902
3158
|
// Should be filtered out because self dataset has version 120
|
|
2903
3159
|
assert.notCalled(callback);
|
|
2904
3160
|
});
|
|
2905
3161
|
|
|
2906
|
-
it('does not call callback for empty locusStateElements',
|
|
2907
|
-
const parser =
|
|
3162
|
+
it('does not call callback for empty locusStateElements', () => {
|
|
3163
|
+
const parser = setupParserWithObjects([]);
|
|
2908
3164
|
|
|
2909
3165
|
const emptyMessage = {
|
|
2910
3166
|
dataSets: [createDataSet('main', 16, 1100)],
|
|
@@ -2913,13 +3169,13 @@ describe('HashTreeParser', () => {
|
|
|
2913
3169
|
locusStateElements: [],
|
|
2914
3170
|
};
|
|
2915
3171
|
|
|
2916
|
-
|
|
3172
|
+
parser.handleMessage(emptyMessage, 'empty elements');
|
|
2917
3173
|
|
|
2918
3174
|
assert.notCalled(callback);
|
|
2919
3175
|
});
|
|
2920
3176
|
|
|
2921
|
-
it('always calls callback for MEETING_ENDED regardless of filtering',
|
|
2922
|
-
const parser =
|
|
3177
|
+
it('always calls callback for MEETING_ENDED regardless of filtering', () => {
|
|
3178
|
+
const parser = setupParserWithObjects([
|
|
2923
3179
|
{
|
|
2924
3180
|
htMeta: {
|
|
2925
3181
|
elementId: {type: 'locus' as const, id: 0, version: 100},
|
|
@@ -2929,23 +3185,15 @@ describe('HashTreeParser', () => {
|
|
|
2929
3185
|
},
|
|
2930
3186
|
]);
|
|
2931
3187
|
|
|
2932
|
-
// Send
|
|
2933
|
-
const
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
htMeta: {
|
|
2940
|
-
elementId: {type: 'self' as const, id: 4, version: 102},
|
|
2941
|
-
dataSetNames: ['self'],
|
|
2942
|
-
},
|
|
2943
|
-
data: undefined, // roster drop triggers MEETING_ENDED
|
|
2944
|
-
},
|
|
2945
|
-
],
|
|
2946
|
-
};
|
|
3188
|
+
// Send a sentinel END MEETING message
|
|
3189
|
+
const sentinelMessage = createHeartbeatMessage(
|
|
3190
|
+
'main',
|
|
3191
|
+
1,
|
|
3192
|
+
parser.dataSets.main.version + 1,
|
|
3193
|
+
EMPTY_HASH
|
|
3194
|
+
);
|
|
2947
3195
|
|
|
2948
|
-
|
|
3196
|
+
parser.handleMessage(sentinelMessage as any, 'sentinel message');
|
|
2949
3197
|
|
|
2950
3198
|
// Callback should be called with MEETING_ENDED
|
|
2951
3199
|
assert.calledOnceWithExactly(callback, LocusInfoUpdateType.MEETING_ENDED, {
|