diff --git a/packages/core-data/src/actions.js b/packages/core-data/src/actions.js index 507a5ac8831faa..bd7f8f78fe8427 100644 --- a/packages/core-data/src/actions.js +++ b/packages/core-data/src/actions.js @@ -340,13 +340,11 @@ export const deleteEntityRecord = await dispatch( removeItems( kind, name, recordId, true ) ); - if ( globalThis.IS_GUTENBERG_PLUGIN ) { - if ( entityConfig.syncConfig ) { - const objectType = `${ kind }/${ name }`; - const objectId = recordId; + if ( entityConfig.syncConfig ) { + const objectType = `${ kind }/${ name }`; + const objectId = recordId; - getSyncManager()?.unload( objectType, objectId ); - } + getSyncManager()?.unload( objectType, objectId ); } } catch ( _error ) { hasError = true; @@ -427,35 +425,33 @@ export const editEntityRecord = return acc; }, {} ), }; - if ( globalThis.IS_GUTENBERG_PLUGIN ) { - if ( entityConfig.syncConfig ) { - const objectType = `${ kind }/${ name }`; - const objectId = recordId; - - // Determine whether this edit should create a new undo level. - // - // In Gutenberg, block changes flow through two callbacks: - // - `onInput`: For transient/in-progress changes (e.g., typing each - // character). These use `isCached: true` and get merged into - // the current undo item. - // - `onChange`: For persistent/completed changes (e.g., formatting - // transforms, block insertions). These use `isCached: false` and - // should create a new undo level. - // - // Additionally, `undoIgnore: true` means the change should not - // affect the undo history at all (e.g., selection-only changes). - const isNewUndoLevel = options.undoIgnore - ? false - : ! options.isCached; - - getSyncManager()?.update( - objectType, - objectId, - editsWithMerges, - LOCAL_EDITOR_ORIGIN, - { isNewUndoLevel } - ); - } + if ( entityConfig.syncConfig ) { + const objectType = `${ kind }/${ name }`; + const objectId = recordId; + + // Determine whether this edit should create a new undo level. + // + // In Gutenberg, block changes flow through two callbacks: + // - `onInput`: For transient/in-progress changes (e.g., typing each + // character). These use `isCached: true` and get merged into + // the current undo item. + // - `onChange`: For persistent/completed changes (e.g., formatting + // transforms, block insertions). These use `isCached: false` and + // should create a new undo level. + // + // Additionally, `undoIgnore: true` means the change should not + // affect the undo history at all (e.g., selection-only changes). + const isNewUndoLevel = options.undoIgnore + ? false + : ! options.isCached; + + getSyncManager()?.update( + objectType, + objectId, + editsWithMerges, + LOCAL_EDITOR_ORIGIN, + { isNewUndoLevel } + ); } if ( ! options.undoIgnore ) { select.getUndoManager().addRecord( @@ -796,16 +792,14 @@ export const saveEntityRecord = true, edits ); - if ( globalThis.IS_GUTENBERG_PLUGIN ) { - if ( entityConfig.syncConfig ) { - getSyncManager()?.update( - `${ kind }/${ name }`, - recordId, - updatedRecord, - LOCAL_EDITOR_ORIGIN, - { isSave: true } - ); - } + if ( entityConfig.syncConfig ) { + getSyncManager()?.update( + `${ kind }/${ name }`, + recordId, + updatedRecord, + LOCAL_EDITOR_ORIGIN, + { isSave: true } + ); } } } catch ( _error ) { diff --git a/packages/core-data/src/entities.js b/packages/core-data/src/entities.js index 7b474491548b5c..611da173c642d5 100644 --- a/packages/core-data/src/entities.js +++ b/packages/core-data/src/entities.js @@ -229,10 +229,8 @@ export const rootEntitiesConfig = [ ].map( ( entity ) => { const syncEnabledRootEntities = new Set( [ 'comment' ] ); - if ( globalThis.IS_GUTENBERG_PLUGIN ) { - if ( syncEnabledRootEntities.has( entity.name ) ) { - entity.syncConfig = defaultSyncConfig; - } + if ( syncEnabledRootEntities.has( entity.name ) ) { + entity.syncConfig = defaultSyncConfig; } return entity; } ); @@ -295,16 +293,14 @@ export const prePersistPostType = ( } // Add meta for persisted CRDT document. - if ( globalThis.IS_GUTENBERG_PLUGIN ) { - if ( persistedRecord ) { - const objectType = `postType/${ name }`; - const objectId = persistedRecord.id; - const meta = getSyncManager()?.createMeta( objectType, objectId ); - newEdits.meta = { - ...edits.meta, - ...meta, - }; - } + if ( persistedRecord ) { + const objectType = `postType/${ name }`; + const objectId = persistedRecord.id; + const meta = getSyncManager()?.createMeta( objectType, objectId ); + newEdits.meta = { + ...edits.meta, + ...meta, + }; } return newEdits; @@ -359,60 +355,54 @@ async function loadPostTypeEntities() { : DEFAULT_ENTITY_KEY, }; - if ( globalThis.IS_GUTENBERG_PLUGIN ) { + /** + * @type {import('@wordpress/sync').SyncConfig} + */ + entity.syncConfig = { /** - * @type {import('@wordpress/sync').SyncConfig} + * Apply changes from the local editor to the local CRDT document so + * that those changes can be synced to other peers (via the provider). + * + * @param {import('@wordpress/sync').CRDTDoc} crdtDoc + * @param {Partial< import('@wordpress/sync').ObjectData >} changes + * @return {void} */ - entity.syncConfig = { - /** - * Apply changes from the local editor to the local CRDT document so - * that those changes can be synced to other peers (via the provider). - * - * @param {import('@wordpress/sync').CRDTDoc} crdtDoc - * @param {Partial< import('@wordpress/sync').ObjectData >} changes - * @return {void} - */ - applyChangesToCRDTDoc: ( crdtDoc, changes ) => - applyPostChangesToCRDTDoc( crdtDoc, changes, postType ), + applyChangesToCRDTDoc: ( crdtDoc, changes ) => + applyPostChangesToCRDTDoc( crdtDoc, changes, postType ), - /** - * Create the awareness instance for the entity's CRDT document. - * - * @param {import('@wordpress/sync').CRDTDoc} ydoc - * @param {import('@wordpress/sync').ObjectID} objectId - * @return {import('@wordpress/sync').Awareness} Awareness instance - */ - createAwareness: ( ydoc, objectId ) => { - const kind = 'postType'; - const id = parseInt( objectId, 10 ); - return new PostEditorAwareness( ydoc, kind, name, id ); - }, + /** + * Create the awareness instance for the entity's CRDT document. + * + * @param {import('@wordpress/sync').CRDTDoc} ydoc + * @param {import('@wordpress/sync').ObjectID} objectId + * @return {import('@wordpress/sync').Awareness} Awareness instance + */ + createAwareness: ( ydoc, objectId ) => { + const kind = 'postType'; + const id = parseInt( objectId, 10 ); + return new PostEditorAwareness( ydoc, kind, name, id ); + }, - /** - * Extract changes from a CRDT document that can be used to update the - * local editor state. - * - * @param {import('@wordpress/sync').CRDTDoc} crdtDoc - * @param {import('@wordpress/sync').ObjectData} editedRecord - * @return {Partial< import('@wordpress/sync').ObjectData >} Changes to record - */ - getChangesFromCRDTDoc: ( crdtDoc, editedRecord ) => - getPostChangesFromCRDTDoc( - crdtDoc, - editedRecord, - postType - ), + /** + * Extract changes from a CRDT document that can be used to update the + * local editor state. + * + * @param {import('@wordpress/sync').CRDTDoc} crdtDoc + * @param {import('@wordpress/sync').ObjectData} editedRecord + * @return {Partial< import('@wordpress/sync').ObjectData >} Changes to record + */ + getChangesFromCRDTDoc: ( crdtDoc, editedRecord ) => + getPostChangesFromCRDTDoc( crdtDoc, editedRecord, postType ), - /** - * Sync features supported by the entity. - * - * @type {Record< string, boolean >} - */ - supports: { - crdtPersistence: true, - }, - }; - } + /** + * Sync features supported by the entity. + * + * @type {Record< string, boolean >} + */ + supports: { + crdtPersistence: true, + }, + }; return entity; } ); @@ -439,9 +429,7 @@ async function loadTaxonomyEntities() { supportsPagination: true, }; - if ( globalThis.IS_GUTENBERG_PLUGIN ) { - entity.syncConfig = defaultSyncConfig; - } + entity.syncConfig = defaultSyncConfig; return entity; } ); diff --git a/packages/core-data/src/private-selectors.ts b/packages/core-data/src/private-selectors.ts index 36517505274e42..f9f2df1972151c 100644 --- a/packages/core-data/src/private-selectors.ts +++ b/packages/core-data/src/private-selectors.ts @@ -40,12 +40,8 @@ type EntityRecordKey = string | number; * @return The undo manager. */ export function getUndoManager( state: State ) { - if ( globalThis.IS_GUTENBERG_PLUGIN ) { - // undoManager is undefined until the first sync-enabled entity is loaded. - return getSyncManager()?.undoManager ?? state.undoManager; - } - - return state.undoManager; + // undoManager is undefined until the first sync-enabled entity is loaded. + return getSyncManager()?.undoManager ?? state.undoManager; } /** diff --git a/packages/core-data/src/resolvers.js b/packages/core-data/src/resolvers.js index b633e17027008f..39a6c0f3ac11f3 100644 --- a/packages/core-data/src/resolvers.js +++ b/packages/core-data/src/resolvers.js @@ -156,126 +156,112 @@ export const getEntityRecord = } // Entity supports syncing. - if ( globalThis.IS_GUTENBERG_PLUGIN ) { - if ( - entityConfig.syncConfig && - isNumericID( key ) && - ! query - ) { - const objectType = `${ kind }/${ name }`; - const objectId = key; - - // Use the new transient "read/write" config to compute transients for - // the sync manager. Otherwise these transients are not available - // if / until the record is edited. Use a copy of the record so that - // it does not change the behavior outside this experimental flag. - const recordWithTransients = { ...record }; - Object.entries( entityConfig.transientEdits ?? {} ) - .filter( - ( [ propName, transientConfig ] ) => - undefined === - recordWithTransients[ propName ] && - transientConfig && - 'object' === typeof transientConfig && - 'read' in transientConfig && - 'function' === typeof transientConfig.read - ) - .forEach( ( [ propName, transientConfig ] ) => { - recordWithTransients[ propName ] = - transientConfig.read( recordWithTransients ); - } ); - - // Load the entity record for syncing. Do not await promise. - void getSyncManager()?.load( - entityConfig.syncConfig, - objectType, - objectId, - recordWithTransients, - { - // Handle edits sourced from the sync manager. - editRecord: ( edits, options = {} ) => { - if ( ! Object.keys( edits ).length ) { - return; - } - - dispatch( { - type: 'EDIT_ENTITY_RECORD', - kind, - name, - recordId: key, - edits, - meta: { - undo: undefined, - }, - options, - } ); - }, - // Get the current entity record (with edits) - getEditedRecord: async () => - await resolveSelect.getEditedEntityRecord( - kind, - name, - key - ), - // Handle sync connection status changes. - onStatusChange: ( status ) => { - dispatch.setSyncConnectionStatus( - kind, - name, - key, - status - ); - }, - // Refetch the current entity record from the database. - refetchRecord: async () => { - dispatch.receiveEntityRecords( - kind, - name, - await apiFetch( { path, parse: true } ), - query - ); - }, - // Save the current entity record's unsaved edits. - saveRecord: () => { - dispatch.saveEditedEntityRecord( - kind, - name, - key + if ( entityConfig.syncConfig && isNumericID( key ) && ! query ) { + const objectType = `${ kind }/${ name }`; + const objectId = key; + + // Use the new transient "read/write" config to compute transients for + // the sync manager. Otherwise these transients are not available + // if / until the record is edited. Use a copy of the record so that + // it does not change the behavior outside this experimental flag. + const recordWithTransients = { ...record }; + Object.entries( entityConfig.transientEdits ?? {} ) + .filter( + ( [ propName, transientConfig ] ) => + undefined === recordWithTransients[ propName ] && + transientConfig && + 'object' === typeof transientConfig && + 'read' in transientConfig && + 'function' === typeof transientConfig.read + ) + .forEach( ( [ propName, transientConfig ] ) => { + recordWithTransients[ propName ] = + transientConfig.read( recordWithTransients ); + } ); + + // Load the entity record for syncing. Do not await promise. + void getSyncManager()?.load( + entityConfig.syncConfig, + objectType, + objectId, + recordWithTransients, + { + // Handle edits sourced from the sync manager. + editRecord: ( edits, options = {} ) => { + if ( ! Object.keys( edits ).length ) { + return; + } + + dispatch( { + type: 'EDIT_ENTITY_RECORD', + kind, + name, + recordId: key, + edits, + meta: { + undo: undefined, + }, + options, + } ); + }, + // Get the current entity record (with edits) + getEditedRecord: async () => + await resolveSelect.getEditedEntityRecord( + kind, + name, + key + ), + // Handle sync connection status changes. + onStatusChange: ( status ) => { + dispatch.setSyncConnectionStatus( + kind, + name, + key, + status + ); + }, + // Refetch the current entity record from the database. + refetchRecord: async () => { + dispatch.receiveEntityRecords( + kind, + name, + await apiFetch( { path, parse: true } ), + query + ); + }, + // Save the current entity record's unsaved edits. + saveRecord: () => { + dispatch.saveEditedEntityRecord( kind, name, key ); + }, + addUndoMeta: ( ydoc, meta ) => { + const selectionHistory = + getSelectionHistory( ydoc ); + + if ( selectionHistory ) { + meta.set( + 'selectionHistory', + selectionHistory ); - }, - addUndoMeta: ( ydoc, meta ) => { - const selectionHistory = - getSelectionHistory( ydoc ); - - if ( selectionHistory ) { - meta.set( - 'selectionHistory', - selectionHistory - ); - } - }, - restoreUndoMeta: ( ydoc, meta ) => { - const selectionHistory = - meta.get( 'selectionHistory' ); - - if ( selectionHistory ) { - // Because Yjs initiates an undo, we need to - // wait until the content is restored before - // we can update the selection. - // Use setTimeout() to wait until content is - // finished updating, and then set the correct - // selection. - setTimeout( () => { - restoreSelection( - selectionHistory, - ydoc - ); - }, 0 ); - } - }, - } - ); - } + } + }, + restoreUndoMeta: ( ydoc, meta ) => { + const selectionHistory = + meta.get( 'selectionHistory' ); + + if ( selectionHistory ) { + // Because Yjs initiates an undo, we need to + // wait until the content is restored before + // we can update the selection. + // Use setTimeout() to wait until content is + // finished updating, and then set the correct + // selection. + setTimeout( () => { + restoreSelection( selectionHistory, ydoc ); + }, 0 ); + } + }, + } + ); } registry.batch( () => { @@ -471,32 +457,30 @@ export const getEntityRecords = }; } - if ( globalThis.IS_GUTENBERG_PLUGIN ) { - if ( entityConfig.syncConfig && -1 === query.per_page ) { - const objectType = `${ kind }/${ name }`; - getSyncManager()?.loadCollection( - entityConfig.syncConfig, - objectType, - { - onStatusChange: ( status ) => { - dispatch.setSyncConnectionStatus( - kind, - name, - null, - status - ); - }, - refetchRecords: async () => { - dispatch.receiveEntityRecords( - kind, - name, - await apiFetch( { path, parse: true } ), - query - ); - }, - } - ); - } + if ( entityConfig.syncConfig && -1 === query.per_page ) { + const objectType = `${ kind }/${ name }`; + getSyncManager()?.loadCollection( + entityConfig.syncConfig, + objectType, + { + onStatusChange: ( status ) => { + dispatch.setSyncConnectionStatus( + kind, + name, + null, + status + ); + }, + refetchRecords: async () => { + dispatch.receiveEntityRecords( + kind, + name, + await apiFetch( { path, parse: true } ), + query + ); + }, + } + ); } // If we request fields but the result doesn't contain the fields, diff --git a/packages/core-data/src/test/resolvers.js b/packages/core-data/src/test/resolvers.js index ee93dfafcc938c..b9082bd4adcf3d 100644 --- a/packages/core-data/src/test/resolvers.js +++ b/packages/core-data/src/test/resolvers.js @@ -128,7 +128,7 @@ describe( 'getEntityRecord', () => { ); } ); - it( 'loads entity with sync manager when IS_GUTENBERG_PLUGIN is true', async () => { + it( 'loads entity with sync manager', async () => { const POST_RECORD = { id: 1, title: 'Test Post' }; const POST_RESPONSE = { json: () => Promise.resolve( POST_RECORD ), diff --git a/packages/editor/src/components/post-locked-modal/index.js b/packages/editor/src/components/post-locked-modal/index.js index a98495893edd83..a6f8a50309fdf1 100644 --- a/packages/editor/src/components/post-locked-modal/index.js +++ b/packages/editor/src/components/post-locked-modal/index.js @@ -152,10 +152,8 @@ function PostLockedModal() { } // Avoid sending the modal if sync is supported, but retain functionality around locks etc. - if ( globalThis.IS_GUTENBERG_PLUGIN ) { - if ( isCollaborationEnabled ) { - return null; - } + if ( isCollaborationEnabled ) { + return null; } const userDisplayName = user.name; diff --git a/packages/sync/CODE.md b/packages/sync/CODE.md index 892a65e5b001b9..cb99c2ad014c6c 100644 --- a/packages/sync/CODE.md +++ b/packages/sync/CODE.md @@ -9,10 +9,6 @@ Relevant docs and discussions: - https://github.com/WordPress/gutenberg/discussions/65012 - https://docs.yjs.dev/ -## Availability - -Real-time collaboration is automatically enabled when using the Gutenberg plugin. The `core-data` package checks for `IS_GUTENBERG_PLUGIN` to determine whether entity syncing is available. - ## The data flow Each entity with sync enabled is represented by a CRDT (Yjs) document. Local edits (unsaved changes) to an entity record are applied to its CRDT document, which is synced with other peers via a provider. Those peers use the CRDT document to update their local state.