Skip to content
68 changes: 57 additions & 11 deletions packages/sync/src/providers/http-polling/polling-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {

const POLLING_INTERVAL_IN_MS = 1000; // 1 second or 1000 milliseconds
const POLLING_INTERVAL_WITH_COLLABORATORS_IN_MS = 250; // 250 milliseconds
const POLLING_INTERVAL_BACKGROUND_TAB_IN_MS = 30 * 1000; // 30 seconds
const MAX_ERROR_BACKOFF_IN_MS = 30 * 1000; // 30 seconds
const POLLING_MANAGER_ORIGIN = 'polling-manager';

Expand Down Expand Up @@ -239,10 +240,13 @@ function processDocUpdate(
}
}

let areListenersRegistered = false;
let hasCollaborators = false;
let isActiveBrowser = 'visible' === document.visibilityState;
let isPolling = false;
let isUnloadPending = false;
let pollInterval = POLLING_INTERVAL_IN_MS;
let pageHideListenerRegistered = false;
let pollingTimeoutId: ReturnType< typeof setTimeout > | null = null;

/**
* Mark that a page unload has been requested. This fires on
Expand Down Expand Up @@ -276,6 +280,38 @@ function handlePageHide(): void {
postSyncUpdateNonBlocking( { rooms } );
}

/**
* Hangle change in visibility state of browser tab.
*
* Used to trigger a slow down of the collaboration syncs when the
* browser tab becomes inactive (either the user switches tabs or the
* screen saver comes on).
*
* Fires on the document's visibilitychange event.
*/
function handleVisibilityChange() {
const wasActive = isActiveBrowser;
isActiveBrowser = document.visibilityState === 'visible';

if ( isActiveBrowser && ! wasActive ) {
/*
* Remove scheduled polling and repoll immediately when reactivated.
*
* This ensures that any updates by collaborators are immediately reflected
* in the document once the browser tab becomes active. Otherwise there would
* be a delay of 30 seconds before the updates came through.
*/
if ( pollingTimeoutId ) {
clearTimeout( pollingTimeoutId );
pollingTimeoutId = null;
}

if ( isPolling ) {
poll();
}
}
}

function poll(): void {
isPolling = true;

Expand Down Expand Up @@ -314,9 +350,6 @@ function poll(): void {
try {
const { rooms } = await postSyncUpdate( payload );

// Reset poll interval on success.
pollInterval = POLLING_INTERVAL_IN_MS;

// Emit 'connected' status.
roomStates.forEach( ( state ) => {
state.onStatusChange( { status: 'connected' } );
Expand All @@ -336,7 +369,7 @@ function poll(): void {
// If there is another collaborator, resume the queue for the next poll
// and increase polling frequency.
if ( Object.keys( room.awareness ).length > 1 ) {
pollInterval = POLLING_INTERVAL_WITH_COLLABORATORS_IN_MS;
hasCollaborators = true;
roomState.updateQueue.resume();
}

Expand Down Expand Up @@ -367,6 +400,15 @@ function poll(): void {
);
}
} );

// Recalculate polling interval.
if ( isActiveBrowser && hasCollaborators ) {
pollInterval = POLLING_INTERVAL_WITH_COLLABORATORS_IN_MS;
} else if ( isActiveBrowser ) {
pollInterval = POLLING_INTERVAL_IN_MS;
} else {
pollInterval = POLLING_INTERVAL_BACKGROUND_TAB_IN_MS;
}
} catch ( error ) {
// Exponential backoff on error: double the backoff time, up to max
pollInterval = Math.min(
Expand Down Expand Up @@ -401,13 +443,12 @@ function poll(): void {
}
}

setTimeout( poll, pollInterval );
pollingTimeoutId = setTimeout( poll, pollInterval );
}

// Start polling.
void start();
}

function registerRoom( {
room,
doc,
Expand Down Expand Up @@ -465,10 +506,11 @@ function registerRoom( {
awareness.on( 'change', onAwarenessUpdate );
roomStates.set( room, roomState );

if ( ! pageHideListenerRegistered ) {
if ( ! areListenersRegistered ) {
window.addEventListener( 'beforeunload', handleBeforeUnload );
window.addEventListener( 'pagehide', handlePageHide );
pageHideListenerRegistered = true;
document.addEventListener( 'visibilitychange', handleVisibilityChange );
areListenersRegistered = true;
}

if ( ! isPolling ) {
Expand Down Expand Up @@ -496,10 +538,14 @@ function unregisterRoom( room: string ): void {
roomStates.delete( room );
}

if ( roomStates.size === 0 && pageHideListenerRegistered ) {
if ( 0 === roomStates.size && areListenersRegistered ) {
window.removeEventListener( 'beforeunload', handleBeforeUnload );
window.removeEventListener( 'pagehide', handlePageHide );
pageHideListenerRegistered = false;
document.removeEventListener(
'visibilitychange',
handleVisibilityChange
);
areListenersRegistered = false;
}
}

Expand Down
Loading