@@ -45,6 +45,10 @@ jest.mock( '@wordpress/hooks', () => ( {
4545jest . mock ( '../config' , ( ) => ( {
4646 ...( jest . requireActual ( '../config' ) as object ) ,
4747 MAX_UPDATE_SIZE_IN_BYTES : 10 ,
48+ // Shrink the per-request room cap so rotation tests don't need 50+
49+ // registered rooms. Existing tests register at most 2 rooms and
50+ // stay well under this cap.
51+ MAX_ROOMS_PER_REQUEST : 10 ,
4852} ) ) ;
4953
5054jest . mock ( '../utils' , ( ) => ( {
@@ -1280,4 +1284,190 @@ describe( 'polling-manager', () => {
12801284 expect ( mockPostSyncUpdate ) . toHaveBeenCalledTimes ( 3 ) ;
12811285 } ) ;
12821286 } ) ;
1287+
1288+ describe ( 'room overflow rotation' , ( ) => {
1289+ // The outer mock sets MAX_ROOMS_PER_REQUEST to 10. Tests in this
1290+ // block register a primary room plus additional "overflow" rooms
1291+ // to exercise the rotation behavior. With cap=10 and the primary
1292+ // pinned, each request carries 9 overflow slots.
1293+ //
1294+ // Note: the first registerRoom call triggers poll() synchronously,
1295+ // so the first poll's payload contains only the primary room.
1296+ // Overflow rooms registered in the same tick are picked up starting
1297+ // with the second poll, which is when rotation behavior kicks in.
1298+
1299+ function registerRoom ( pollingMgr : PollingManager , room : string ) {
1300+ pollingMgr . registerRoom ( {
1301+ room,
1302+ doc : createMockDoc ( 1 ) ,
1303+ awareness : createMockAwareness ( ) ,
1304+ log : jest . fn ( ) ,
1305+ onStatusChange : jest . fn ( ) ,
1306+ onSync : jest . fn ( ) ,
1307+ } ) ;
1308+ }
1309+
1310+ function registerPrimaryAndOverflow (
1311+ pollingMgr : PollingManager ,
1312+ overflowCount : number
1313+ ) : string [ ] {
1314+ registerRoom ( pollingMgr , 'primary' ) ;
1315+ const overflowNames : string [ ] = [ ] ;
1316+ for ( let i = 1 ; i <= overflowCount ; i ++ ) {
1317+ const name = `o${ i } ` ;
1318+ overflowNames . push ( name ) ;
1319+ registerRoom ( pollingMgr , name ) ;
1320+ }
1321+ return overflowNames ;
1322+ }
1323+
1324+ function getRoomNames ( callIndex : number ) : string [ ] {
1325+ const payload = mockPostSyncUpdate . mock . calls [ callIndex ] [ 0 ] as {
1326+ rooms : { room : string } [ ] ;
1327+ } ;
1328+ return payload . rooms . map ( ( r ) => r . room ) ;
1329+ }
1330+
1331+ it ( 'sends every room in a single request when the count is at or under the cap' , async ( ) => {
1332+ mockPostSyncUpdate . mockResolvedValue ( { rooms : [ ] } ) ;
1333+
1334+ // Primary + 9 overflow = 10 rooms, exactly at the cap.
1335+ const overflow = registerPrimaryAndOverflow ( pollingManager , 9 ) ;
1336+
1337+ await jest . advanceTimersByTimeAsync ( 0 ) ;
1338+ await jest . advanceTimersByTimeAsync ( 4000 ) ;
1339+
1340+ expect ( mockPostSyncUpdate ) . toHaveBeenCalledTimes ( 2 ) ;
1341+
1342+ // First poll fires synchronously with only the primary room.
1343+ expect ( getRoomNames ( 0 ) ) . toEqual ( [ 'primary' ] ) ;
1344+
1345+ // Second poll includes every registered room in a single
1346+ // request (fast path since total rooms === cap).
1347+ expect ( getRoomNames ( 1 ) ) . toEqual ( [ 'primary' , ...overflow ] ) ;
1348+ } ) ;
1349+
1350+ it ( 'caps each request at MAX_ROOMS_PER_REQUEST and always includes the primary room' , async ( ) => {
1351+ mockPostSyncUpdate . mockResolvedValue ( { rooms : [ ] } ) ;
1352+
1353+ // Primary + 11 overflow = 12 rooms, over the cap of 10.
1354+ registerPrimaryAndOverflow ( pollingManager , 11 ) ;
1355+
1356+ await jest . advanceTimersByTimeAsync ( 0 ) ;
1357+ await jest . advanceTimersByTimeAsync ( 4000 ) ;
1358+ await jest . advanceTimersByTimeAsync ( 4000 ) ;
1359+
1360+ expect ( mockPostSyncUpdate ) . toHaveBeenCalledTimes ( 3 ) ;
1361+
1362+ // First poll: only the primary room was registered yet.
1363+ expect ( getRoomNames ( 0 ) ) . toEqual ( [ 'primary' ] ) ;
1364+
1365+ // Subsequent polls cap at MAX_ROOMS_PER_REQUEST and pin primary.
1366+ for ( let i = 1 ; i < 3 ; i ++ ) {
1367+ const names = getRoomNames ( i ) ;
1368+ expect ( names ) . toHaveLength ( 10 ) ;
1369+ expect ( names [ 0 ] ) . toBe ( 'primary' ) ;
1370+ }
1371+ } ) ;
1372+
1373+ it ( 'rotates overflow rooms across successive polls until all are covered' , async ( ) => {
1374+ mockPostSyncUpdate . mockResolvedValue ( { rooms : [ ] } ) ;
1375+
1376+ // Primary + 15 overflow = 16 rooms. Skipping the primary-only
1377+ // first poll, two subsequent rotation polls send 18 slots —
1378+ // enough to cover every overflow room at least once.
1379+ const overflow = registerPrimaryAndOverflow ( pollingManager , 15 ) ;
1380+
1381+ await jest . advanceTimersByTimeAsync ( 0 ) ;
1382+ await jest . advanceTimersByTimeAsync ( 4000 ) ;
1383+ await jest . advanceTimersByTimeAsync ( 4000 ) ;
1384+
1385+ const overflowSeen = new Set < string > ( ) ;
1386+ // Skip poll 0 (primary only); inspect rotation polls.
1387+ for ( let i = 1 ; i < 3 ; i ++ ) {
1388+ for ( const name of getRoomNames ( i ) ) {
1389+ if ( name !== 'primary' ) {
1390+ overflowSeen . add ( name ) ;
1391+ }
1392+ }
1393+ }
1394+
1395+ expect ( overflowSeen ) . toEqual ( new Set ( overflow ) ) ;
1396+ } ) ;
1397+
1398+ it ( 'advances the rotation window so successive polls send different overflow rooms' , async ( ) => {
1399+ mockPostSyncUpdate . mockResolvedValue ( { rooms : [ ] } ) ;
1400+
1401+ // Primary + 11 overflow rooms, 9 slots per request.
1402+ registerPrimaryAndOverflow ( pollingManager , 11 ) ;
1403+
1404+ await jest . advanceTimersByTimeAsync ( 0 ) ;
1405+ await jest . advanceTimersByTimeAsync ( 4000 ) ;
1406+ await jest . advanceTimersByTimeAsync ( 4000 ) ;
1407+
1408+ // Compare the two rotation polls (poll 0 is primary-only).
1409+ const first = getRoomNames ( 1 ) . slice ( 1 ) ;
1410+ const second = getRoomNames ( 2 ) . slice ( 1 ) ;
1411+
1412+ expect ( first ) . not . toEqual ( second ) ;
1413+ // Two rotation polls of 9 slots against 11 overflow rooms
1414+ // cover the entire set.
1415+ expect ( new Set ( [ ...first , ...second ] ) . size ) . toBe ( 11 ) ;
1416+ } ) ;
1417+
1418+ it ( 'advances the rotation window even when a poll fails' , async ( ) => {
1419+ // Primary + 11 overflow rooms, 9 slots per request.
1420+ registerPrimaryAndOverflow ( pollingManager , 11 ) ;
1421+
1422+ // Poll 1: primary only (fires synchronously at registration).
1423+ mockPostSyncUpdate . mockResolvedValueOnce ( { rooms : [ ] } ) ;
1424+ await jest . advanceTimersByTimeAsync ( 0 ) ;
1425+ expect ( mockPostSyncUpdate ) . toHaveBeenCalledTimes ( 1 ) ;
1426+ expect ( getRoomNames ( 0 ) ) . toEqual ( [ 'primary' ] ) ;
1427+
1428+ // Poll 2 fails while sending primary + 9 overflow. The
1429+ // rotation offset should still advance past this window.
1430+ mockPostSyncUpdate . mockRejectedValueOnce ( new Error ( 'network' ) ) ;
1431+ await jest . advanceTimersByTimeAsync ( 4000 ) ;
1432+ expect ( mockPostSyncUpdate ) . toHaveBeenCalledTimes ( 2 ) ;
1433+
1434+ const failedSent = getRoomNames ( 1 ) ;
1435+ expect ( failedSent ) . toHaveLength ( 10 ) ;
1436+ expect ( failedSent [ 0 ] ) . toBe ( 'primary' ) ;
1437+
1438+ // Poll 3 retries after the failure and should send a different
1439+ // overflow slice, proving the offset advanced despite the error.
1440+ mockPostSyncUpdate . mockResolvedValueOnce ( { rooms : [ ] } ) ;
1441+ await jest . advanceTimersByTimeAsync ( 2000 ) ;
1442+ expect ( mockPostSyncUpdate ) . toHaveBeenCalledTimes ( 3 ) ;
1443+
1444+ const retrySent = getRoomNames ( 2 ) ;
1445+ expect ( retrySent ) . toHaveLength ( 10 ) ;
1446+ expect ( retrySent [ 0 ] ) . toBe ( 'primary' ) ;
1447+ expect ( retrySent ) . not . toEqual ( failedSent ) ;
1448+ } ) ;
1449+
1450+ it ( 'chunks the page-hide disconnect beacon so each request stays under the cap' , async ( ) => {
1451+ mockPostSyncUpdate . mockResolvedValue ( { rooms : [ ] } ) ;
1452+
1453+ // 21 rooms at cap=10 => three beacons (10 + 10 + 1).
1454+ registerPrimaryAndOverflow ( pollingManager , 20 ) ;
1455+
1456+ // Flush the initial poll so the pagehide test observes
1457+ // postSyncUpdateNonBlocking calls from the page-hide handler only.
1458+ await jest . advanceTimersByTimeAsync ( 0 ) ;
1459+ mockPostSyncUpdateNonBlocking . mockClear ( ) ;
1460+
1461+ window . dispatchEvent ( new Event ( 'pagehide' ) ) ;
1462+
1463+ expect ( mockPostSyncUpdateNonBlocking ) . toHaveBeenCalledTimes ( 3 ) ;
1464+
1465+ const beaconsSent = mockPostSyncUpdateNonBlocking . mock . calls . map (
1466+ ( call ) =>
1467+ ( call [ 0 ] as { rooms : { room : string } [ ] } ) . rooms . length
1468+ ) ;
1469+ expect ( beaconsSent . every ( ( n ) => n <= 10 ) ) . toBe ( true ) ;
1470+ expect ( beaconsSent . reduce ( ( a , b ) => a + b , 0 ) ) . toBe ( 21 ) ;
1471+ } ) ;
1472+ } ) ;
12831473} ) ;
0 commit comments