Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions packages/core-data/src/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { createUndoManager } from '@wordpress/undo-manager';
import { ifMatchingAction, replaceAction } from './utils';
import { reducer as queriedDataReducer } from './queried-data';
import { rootEntitiesConfig, DEFAULT_ENTITY_KEY } from './entities';
import { ConnectionErrorCode } from './sync';

/** @typedef {import('./types').AnyFunction} AnyFunction */

Expand Down Expand Up @@ -706,6 +707,16 @@ export function collaborationSupported( state = true, action ) {
switch ( action.type ) {
case 'SET_COLLABORATION_SUPPORTED':
return action.supported;

case 'SET_SYNC_CONNECTION_STATUS':
if (
ConnectionErrorCode.DOCUMENT_SIZE_LIMIT_EXCEEDED ===
action.status?.error?.code
) {
return false;
}

return state;
}
return state;
}
Expand Down
2 changes: 2 additions & 0 deletions packages/core-data/src/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
import { unlock } from './lock-unlock';

const {
ConnectionErrorCode,
createSyncManager,
Delta,
CRDT_DOC_META_PERSISTENCE_KEY,
Expand All @@ -22,6 +23,7 @@ const {
} = unlock( syncPrivateApis );

export {
ConnectionErrorCode,
Delta,
CRDT_DOC_META_PERSISTENCE_KEY,
CRDT_RECORD_MAP_KEY,
Expand Down
24 changes: 21 additions & 3 deletions packages/editor/src/components/post-locked-modal/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,39 @@ import { addAction, removeAction } from '@wordpress/hooks';
import { useInstanceId } from '@wordpress/compose';
import { store as coreStore } from '@wordpress/core-data';
import { unlock } from '../../lock-unlock';
import { DOCUMENT_SIZE_LIMIT_EXCEEDED } from '../../utils/sync-error-messages';

/**
* Internal dependencies
*/
import { store as editorStore } from '../../store';

function CollaborationContext() {
const isCollaborationSupported = useSelect( ( select ) => {
return unlock( select( coreStore ) ).isCollaborationSupported();
}, [] );
const { isCollaborationSupported, syncConnectionStatus } = useSelect(
( select ) => {
const selectors = unlock( select( coreStore ) );
return {
isCollaborationSupported: selectors.isCollaborationSupported(),
syncConnectionStatus: selectors.getSyncConnectionStatus(),
};
},
[]
);

if ( isCollaborationSupported ) {
return null;
}

if ( DOCUMENT_SIZE_LIMIT_EXCEEDED === syncConnectionStatus?.error?.code ) {
return (
<p>
{ __(
'Because this post is too large for real-time collaboration, only one person can edit at a time.'
) }
</p>
);
}

return (
<p>
{ __(
Expand Down
30 changes: 19 additions & 11 deletions packages/editor/src/components/sync-connection-modal/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,24 @@ const DISCONNECTED_DEBOUNCE_MS = 2000;
* @return {Element|null} The modal component or null if not disconnected.
*/
export function SyncConnectionModal() {
const { connectionState, postType } = useSelect( ( selectFn ) => {
const currentPostType = selectFn( editorStore ).getCurrentPostType();
return {
connectionState:
selectFn( coreDataStore ).getSyncConnectionStatus() || null,
postType: currentPostType
? selectFn( coreDataStore ).getPostType( currentPostType )
: null,
};
}, [] );
const { connectionState, isCollaborationEnabled, postType } = useSelect(
( selectFn ) => {
const currentPostType =
selectFn( editorStore ).getCurrentPostType();
return {
connectionState:
selectFn( coreDataStore ).getSyncConnectionStatus() || null,
isCollaborationEnabled:
selectFn(
editorStore
).isCollaborationEnabledForCurrentPost(),
postType: currentPostType
? selectFn( coreDataStore ).getPostType( currentPostType )
: null,
};
},
[]
);

const { secondsRemaining, markRetrying } = useRetryCountdown(
connectionState?.retryInMs,
Expand Down Expand Up @@ -115,7 +123,7 @@ export function SyncConnectionModal() {
};
}, [ connectionStatus, connectionErrorCode ] );

if ( ! syncConnectionMessage ) {
if ( ! syncConnectionMessage || ! isCollaborationEnabled ) {
return null;
}

Expand Down
20 changes: 15 additions & 5 deletions packages/editor/src/utils/sync-error-messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,45 @@
*/
import { __ } from '@wordpress/i18n';

// These error codes are defined in the sync package:
// packages/sync/src/errors.ts
export const AUTHENTICATION_FAILED = 'authentication-failed';
export const CONNECTION_EXPIRED = 'connection-expired';
export const CONNECTION_LIMIT_EXCEEDED = 'connection-limit-exceeded';
export const DOCUMENT_SIZE_LIMIT_EXCEEDED = 'document-size-limit-exceeded';
export const UNKNOWN_ERROR = 'unknown-error';

/**
* Default error messages for known error codes.
*/
const ERROR_MESSAGES = {
'authentication-failed': {
[ AUTHENTICATION_FAILED ]: {
title: __( 'Unable to connect' ),
description: __(
"Real-time collaboration couldn't verify your permissions. " +
'Check that you have access to edit this post, or contact your site administrator.'
),
canRetry: false,
},
'connection-expired': {
[ CONNECTION_EXPIRED ]: {
title: __( 'Connection expired' ),
description: __(
'Your connection to real-time collaboration has timed out. ' +
'Editing is paused to prevent conflicts with other editors.'
),
canRetry: true,
},
'connection-limit-exceeded': {
[ CONNECTION_LIMIT_EXCEEDED ]: {
title: __( 'Too many editors connected' ),
description: __(
'Real-time collaboration has reached its connection limit. ' +
'Try again later or contact your site administrator.'
),
canRetry: true,
},
'unknown-error': {
// DOCUMENT_SIZE_LIMIT_EXCEEDED is not included here because it results in
// collaboration being disabled entirely.
[ UNKNOWN_ERROR ]: {
title: __( 'Connection lost' ),
description: __(
'The connection to real-time collaboration was interrupted. ' +
Expand All @@ -54,5 +64,5 @@ export function getSyncErrorMessages( error ) {
return ERROR_MESSAGES[ error.code ];
}

return ERROR_MESSAGES[ 'unknown-error' ];
return ERROR_MESSAGES[ UNKNOWN_ERROR ];
}
1 change: 1 addition & 0 deletions packages/editor/src/utils/test/sync-error-messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ describe( 'getSyncErrorMessages', () => {
'authentication-failed',
'connection-expired',
'connection-limit-exceeded',
'document-size-limit-exceeded',
'unknown-error',
] )(
'should return title, description, and canRetry for "%s"',
Expand Down
17 changes: 17 additions & 0 deletions packages/sync/src/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export enum ConnectionErrorCode {
AUTHENTICATION_FAILED = 'authentication-failed',
CONNECTION_EXPIRED = 'connection-expired',
CONNECTION_LIMIT_EXCEEDED = 'connection-limit-exceeded',
DOCUMENT_SIZE_LIMIT_EXCEEDED = 'document-size-limit-exceeded',
UNKNOWN_ERROR = 'unknown-error',
}

export class ConnectionError extends Error {
constructor(
public code: ConnectionErrorCode = ConnectionErrorCode.UNKNOWN_ERROR,
message?: string
) {
super( message );
this.name = 'ConnectionError';
}
}
2 changes: 2 additions & 0 deletions packages/sync/src/private-apis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
LOCAL_EDITOR_ORIGIN,
LOCAL_UNDO_IGNORED_ORIGIN,
} from './config';
import { ConnectionErrorCode } from './errors';
import { lock } from './lock-unlock';
import { createSyncManager } from './manager';
import { pollingManager } from './providers/http-polling/polling-manager';
Expand All @@ -22,4 +23,5 @@ lock( privateApis, {
LOCAL_EDITOR_ORIGIN,
LOCAL_UNDO_IGNORED_ORIGIN,
retrySyncConnection: () => pollingManager.retryNow(),
ConnectionErrorCode,
} );
7 changes: 7 additions & 0 deletions packages/sync/src/providers/http-polling/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const MAX_ERROR_BACKOFF_IN_MS = 30 * 1000; // 30 seconds
export const MAX_UPDATE_SIZE_IN_BYTES = 1 * 1024 * 1024; // 1 MB
export const POLLING_INTERVAL_IN_MS = 1000; // 1 second or 1000 milliseconds
export const POLLING_INTERVAL_WITH_COLLABORATORS_IN_MS = 250; // 250 milliseconds
// Must be less than the server-side AWARENESS_TIMEOUT (30 s) to avoid
// false disconnects when the tab is in the background.
export const POLLING_INTERVAL_BACKGROUND_TAB_IN_MS = 25 * 1000; // 25 seconds
42 changes: 35 additions & 7 deletions packages/sync/src/providers/http-polling/polling-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ import * as syncProtocol from 'y-protocols/sync';
/**
* Internal dependencies
*/
import {
MAX_ERROR_BACKOFF_IN_MS,
MAX_UPDATE_SIZE_IN_BYTES,
POLLING_INTERVAL_IN_MS,
POLLING_INTERVAL_WITH_COLLABORATORS_IN_MS,
POLLING_INTERVAL_BACKGROUND_TAB_IN_MS,
} from './config';
import { ConnectionError, ConnectionErrorCode } from '../../errors';
import type { ConnectionStatus } from '../../types';
import {
type AwarenessState,
Expand All @@ -28,12 +36,6 @@ import {
postSyncUpdateNonBlocking,
} from './utils';

const POLLING_INTERVAL_IN_MS = 1000; // 1 second or 1000 milliseconds
const POLLING_INTERVAL_WITH_COLLABORATORS_IN_MS = 250; // 250 milliseconds
// Must be less than the server-side AWARENESS_TIMEOUT (30 s) to avoid
// false disconnects when the tab is in the background.
const POLLING_INTERVAL_BACKGROUND_TAB_IN_MS = 25 * 1000; // 25 seconds
const MAX_ERROR_BACKOFF_IN_MS = 30 * 1000; // 30 seconds
const POLLING_MANAGER_ORIGIN = 'polling-manager';

type LogFunction = ( message: string, debug?: object ) => void;
Expand Down Expand Up @@ -146,7 +148,9 @@ function processAwarenessUpdate(

// Removed clients are missing from the server state.
const removed = new Set< number >(
currentStates.keys().filter( ( clientId ) => ! state[ clientId ] )
Array.from( currentStates.keys() ).filter(
( clientId ) => ! state[ clientId ]
)
);

Object.entries( state ).forEach( ( [ clientIdString, awarenessState ] ) => {
Expand Down Expand Up @@ -459,6 +463,7 @@ function poll(): void {
// Start polling.
void start();
}

function registerRoom( {
room,
doc,
Expand All @@ -483,6 +488,29 @@ function registerRoom( {
return;
}

if ( update.byteLength > MAX_UPDATE_SIZE_IN_BYTES ) {
const state = roomStates.get( room );
if ( ! state ) {
return;
}

state.log( 'Document size limit exceeded', {
maxUpdateSizeInBytes: MAX_UPDATE_SIZE_IN_BYTES,
updateSizeInBytes: update.byteLength,
} );

state.onStatusChange( {
status: 'disconnected',
error: new ConnectionError(
ConnectionErrorCode.DOCUMENT_SIZE_LIMIT_EXCEEDED,
'Document size limit exceeded'
),
} );

// This is an unrecoverable error. Unregister the room to prevent syncing.
unregisterRoom( room );
}

// Tag local document changes as 'update' type.
updateQueue.add( createSyncUpdate( update, SyncUpdateType.UPDATE ) );
}
Expand Down
Loading
Loading