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
2 changes: 2 additions & 0 deletions packages/core-data/src/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
CRDT_RECORD_MAP_KEY,
LOCAL_EDITOR_ORIGIN,
LOCAL_SYNC_MANAGER_ORIGIN,
WORDPRESS_META_KEY_FOR_CRDT_DOC_PERSISTENCE,
type SyncManager,
createSyncManager,
} from '@wordpress/sync';
Expand All @@ -15,6 +16,7 @@ export {
CRDT_RECORD_MAP_KEY,
LOCAL_EDITOR_ORIGIN,
LOCAL_SYNC_MANAGER_ORIGIN,
WORDPRESS_META_KEY_FOR_CRDT_DOC_PERSISTENCE,
};

let syncManager: SyncManager;
Expand Down
83 changes: 77 additions & 6 deletions packages/core-data/src/utils/crdt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@ import {
} from './crdt-blocks';
import { type Post } from '../entity-types/post';
import { type Type } from '../entity-types';
import { CRDT_DOC_META_PERSISTENCE_KEY, CRDT_RECORD_MAP_KEY } from '../sync';
import {
CRDT_DOC_META_PERSISTENCE_KEY,
CRDT_RECORD_MAP_KEY,
WORDPRESS_META_KEY_FOR_CRDT_DOC_PERSISTENCE,
} from '../sync';
import type { WPBlockSelection, WPSelection } from '../types';

export type PostChanges = Partial< Post > & {
Expand All @@ -44,6 +48,7 @@ const allowedPostProperties = new Set< string >( [
'featured_media',
'format',
'ping_status',
'meta',
'slug',
'status',
'sticky',
Expand All @@ -52,6 +57,11 @@ const allowedPostProperties = new Set< string >( [
'title',
] );

// Post meta keys that should *not* be synced.
const disallowedPostMetaKeys = new Set< string >( [
WORDPRESS_META_KEY_FOR_CRDT_DOC_PERSISTENCE,
] );

/**
* Given a set of local changes to a generic entity record, apply those changes
* to the local Y.Doc.
Expand Down Expand Up @@ -94,13 +104,13 @@ export function defaultApplyChangesToCRDTDoc(
*
* @param {CRDTDoc} ydoc
* @param {PostChanges} changes
* @param {Type} postType
* @param {Type} _postType
* @return {void}
*/
export function applyPostChangesToCRDTDoc(
ydoc: CRDTDoc,
changes: PostChanges,
postType: Type // eslint-disable-line @typescript-eslint/no-unused-vars
_postType: Type // eslint-disable-line @typescript-eslint/no-unused-vars
): void {
const ymap = ydoc.getMap( CRDT_RECORD_MAP_KEY );

Expand Down Expand Up @@ -148,6 +158,36 @@ export function applyPostChangesToCRDTDoc(
break;
}

// "Meta" is overloaded term; here, it refers to post meta.
case 'meta': {
let metaMap = ymap.get( 'meta' ) as Y.Map< unknown >;

// Initialize.
if ( ! ( metaMap instanceof Y.Map ) ) {
metaMap = new Y.Map();
setValue( metaMap );
}

// Iterate over each meta property in the new value and merge it if it
// should be synced.
Object.entries( newValue ?? {} ).forEach(
( [ metaKey, metaValue ] ) => {
if ( disallowedPostMetaKeys.has( metaKey ) ) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an empty array, we can remove it as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I merge #72373, I will need to add the CRDT post meta to the set.

return;
}

mergeValue(
metaMap.get( metaKey ), // current value in CRDT
metaValue, // new value from changes
( updatedMetaValue: unknown ): void => {
metaMap.set( metaKey, updatedMetaValue );
}
);
}
);
break;
}

case 'slug': {
// Do not sync an empty slug. This indicates that the post is using
// the default auto-generated slug.
Expand Down Expand Up @@ -200,17 +240,19 @@ export function defaultGetChangesFromCRDTDoc( crdtDoc: CRDTDoc ): ObjectData {
*
* @param {CRDTDoc} ydoc
* @param {Post} editedRecord
* @param {Type} postType
* @param {Type} _postType
* @return {Partial<PostChanges>} The changes that should be applied to the local record.
*/
export function getPostChangesFromCRDTDoc(
ydoc: CRDTDoc,
editedRecord: Post,
postType: Type // eslint-disable-line @typescript-eslint/no-unused-vars
_postType: Type // eslint-disable-line @typescript-eslint/no-unused-vars
): PostChanges {
const ymap = ydoc.getMap( CRDT_RECORD_MAP_KEY );

return Object.fromEntries(
let allowedMetaChanges: Post[ 'meta' ] = {};

const changes = Object.fromEntries(
Object.entries( ymap.toJSON() ).filter( ( [ key, newValue ] ) => {
if ( ! allowedPostProperties.has( key ) ) {
return false;
Expand Down Expand Up @@ -271,6 +313,24 @@ export function getPostChangesFromCRDTDoc(
return haveValuesChanged( currentValue, newValue );
}

case 'meta': {
allowedMetaChanges = Object.fromEntries(
Object.entries( newValue ?? {} ).filter(
( [ metaKey ] ) =>
! disallowedPostMetaKeys.has( metaKey )
)
);

// Merge the allowed meta changes with the current meta values since
// not all meta properties are synced.
const mergedValue = {
...( currentValue as PostChanges[ 'meta' ] ),
...allowedMetaChanges,
};

return haveValuesChanged( currentValue, mergedValue );
}

case 'status': {
// Do not sync an invalid status.
if ( 'auto-draft' === newValue ) {
Expand All @@ -296,6 +356,17 @@ export function getPostChangesFromCRDTDoc(
}
} )
);

// Meta changes must be merged with the edited record since not all meta
// properties are synced.
if ( 'object' === typeof changes.meta ) {
changes.meta = {
...editedRecord.meta,
...allowedMetaChanges,
};
}

return changes;
}

/**
Expand Down
125 changes: 124 additions & 1 deletion packages/core-data/src/utils/test/crdt.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
/**
* WordPress dependencies
*/
import { CRDT_RECORD_MAP_KEY, Y } from '@wordpress/sync';
import {
CRDT_RECORD_MAP_KEY,
WORDPRESS_META_KEY_FOR_CRDT_DOC_PERSISTENCE,
Y,
} from '@wordpress/sync';

/**
* External dependencies
Expand Down Expand Up @@ -152,6 +156,55 @@ describe( 'crdt', () => {
const blocks = map.get( 'blocks' );
expect( blocks ).toBeInstanceOf( Y.Array );
} );

it( 'syncs meta fields', () => {
const changes = {
meta: {
some_meta: 'new value',
},
};

const metaMap = new Y.Map< unknown >();
metaMap.set( 'some_meta', 'old value' );
map.set( 'meta', metaMap );

applyPostChangesToCRDTDoc( doc, changes, mockPostType );

expect( metaMap.get( 'some_meta' ) ).toBe( 'new value' );
} );

it( 'syncs non-single meta fields', () => {
const changes = {
meta: {
some_meta: [ 'value', 'value 2' ],
},
};

const metaMap = new Y.Map< unknown >();
metaMap.set( 'some_meta', 'old value' );
map.set( 'meta', metaMap );

applyPostChangesToCRDTDoc( doc, changes, mockPostType );

expect( metaMap.get( 'some_meta' ) ).toStrictEqual( [
'value',
'value 2',
] );
} );

it( 'initializes meta as Y.Map when not present', () => {
const changes = {
meta: {
custom_field: 'value',
},
};

applyPostChangesToCRDTDoc( doc, changes, mockPostType );

const metaMap = map.get( 'meta' ) as Y.Map< unknown >;
expect( metaMap ).toBeInstanceOf( Y.Map );
expect( metaMap.get( 'custom_field' ) ).toBe( 'value' );
} );
} );

describe( 'getPostChangesFromCRDTDoc', () => {
Expand Down Expand Up @@ -250,5 +303,75 @@ describe( 'crdt', () => {

expect( changes ).toHaveProperty( 'blocks' );
} );

it( 'includes meta in changes', () => {
map.set( 'meta', {
public_meta: 'new value',
} );

const editedRecord = {
meta: {
public_meta: 'old value',
},
} as unknown as Post;

const changes = getPostChangesFromCRDTDoc(
doc,
editedRecord,
mockPostType
);

expect( changes.meta ).toEqual( {
public_meta: 'new value', // from CRDT
} );
} );

it( 'includes non-single meta in changes', () => {
map.set( 'meta', {
public_meta: [ 'value', 'value 2' ],
} );

const editedRecord = {
meta: {
public_meta: 'value',
},
} as unknown as Post;

const changes = getPostChangesFromCRDTDoc(
doc,
editedRecord,
mockPostType
);

expect( changes.meta ).toEqual( {
public_meta: [ 'value', 'value 2' ], // from CRDT
} );
} );

it( 'excludes disallowed meta keys in changes', () => {
map.set( 'meta', {
public_meta: 'new value',
[ WORDPRESS_META_KEY_FOR_CRDT_DOC_PERSISTENCE ]: 'exclude me',
} );

const editedRecord = {
meta: {
public_meta: 'old value',
},
} as unknown as Post;

const changes = getPostChangesFromCRDTDoc(
doc,
editedRecord,
mockPostType
);

expect( changes.meta ).toEqual( {
public_meta: 'new value', // from CRDT
} );
expect( changes.meta ).not.toHaveProperty(
WORDPRESS_META_KEY_FOR_CRDT_DOC_PERSISTENCE
);
} );
} );
} );
4 changes: 4 additions & 0 deletions packages/sync/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ Origin string for CRDT document changes originating from the local editor.

Origin string for CRDT document changes originating from the sync manager.

### WORDPRESS_META_KEY_FOR_CRDT_DOC_PERSISTENCE

WordPress meta key used to persist the CRDT document for an entity.

### Y

Exported copy of Yjs so that consumers of this package don't need to install it.
Expand Down
1 change: 1 addition & 0 deletions packages/sync/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export {
CRDT_RECORD_MAP_KEY,
LOCAL_EDITOR_ORIGIN,
LOCAL_SYNC_MANAGER_ORIGIN,
WORDPRESS_META_KEY_FOR_CRDT_DOC_PERSISTENCE,
} from './config';
export { createSyncManager } from './manager';
export type * from './types';
Loading