Skip to content

Commit 8401993

Browse files
Improve CRDT "merge logic" for post entities (#72262)
* Move post type syncConfig to const This is purely to help avoid future conflicts when we conditionally set syncConfig based on post-type supports * Pass current record to syncConfig functions * Improve CRDT utils with better merge logic * Add tests for crdt and crdt-blocks * Update resolver unit test * Remove unnecessary mocks and improve types of tests * Get type record in resolver and pass through to CRDT utils * Remove persisted record argument from updateCRDTDoc * Remove lib0 as dependency * Revert "Get type record in resolver and pass through to CRDT utils" This reverts commit ea050e1. * Inspect initial record for available fields * Simplify allowed sync properties to enforced allow list --------- Co-authored-by: ingeniumed <ingeniumed@users.noreply.github.com>
1 parent 930ac0b commit 8401993

13 files changed

Lines changed: 1526 additions & 97 deletions

File tree

packages/core-data/src/entities.js

Lines changed: 38 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,15 @@ import { capitalCase, pascalCase } from 'change-case';
99
import apiFetch from '@wordpress/api-fetch';
1010
import { __unstableSerializeAndClean, parse } from '@wordpress/blocks';
1111
import { __ } from '@wordpress/i18n';
12-
import { RichTextData } from '@wordpress/rich-text';
1312

1413
/**
1514
* Internal dependencies
1615
*/
1716
import {
17+
applyPostChangesToCRDTDoc,
1818
defaultApplyChangesToCRDTDoc,
1919
defaultGetChangesFromCRDTDoc,
20+
getPostChangesFromCRDTDoc,
2021
} from './utils/crdt';
2122

2223
export const DEFAULT_ENTITY_KEY = 'id';
@@ -256,29 +257,6 @@ export const prePersistPostType = ( persistedRecord, edits ) => {
256257
return newEdits;
257258
};
258259

259-
const serialisableBlocksCache = new WeakMap();
260-
261-
function makeBlockAttributesSerializable( attributes ) {
262-
const newAttributes = { ...attributes };
263-
for ( const [ key, value ] of Object.entries( attributes ) ) {
264-
if ( value instanceof RichTextData ) {
265-
newAttributes[ key ] = value.valueOf();
266-
}
267-
}
268-
return newAttributes;
269-
}
270-
271-
function makeBlocksSerializable( blocks ) {
272-
return blocks.map( ( block ) => {
273-
const { innerBlocks, attributes, ...rest } = block;
274-
return {
275-
...rest,
276-
attributes: makeBlockAttributesSerializable( attributes ),
277-
innerBlocks: makeBlocksSerializable( innerBlocks ),
278-
};
279-
} );
280-
}
281-
282260
/**
283261
* Returns the list of post type entities.
284262
*
@@ -293,7 +271,41 @@ async function loadPostTypeEntities() {
293271
name
294272
);
295273
const namespace = postType?.rest_namespace ?? 'wp/v2';
296-
const syncedProperties = new Set( [ 'blocks' ] );
274+
275+
/**
276+
* @type {import('@wordpress/sync').SyncConfig}
277+
*/
278+
const syncConfig = {
279+
/**
280+
* Apply changes from the local editor to the local CRDT document so
281+
* that those changes can be synced to other peers (via the provider).
282+
*
283+
* @param {import('@wordpress/sync').CRDTDoc} crdtDoc
284+
* @param {Partial< import('@wordpress/sync').ObjectData >} changes
285+
* @return {void}
286+
*/
287+
applyChangesToCRDTDoc: ( crdtDoc, changes ) =>
288+
applyPostChangesToCRDTDoc( crdtDoc, changes, postType ),
289+
290+
/**
291+
* Extract changes from a CRDT document that can be used to update the
292+
* local editor state.
293+
*
294+
* @param {import('@wordpress/sync').CRDTDoc} crdtDoc
295+
* @param {import('@wordpress/sync').ObjectData} editedRecord
296+
* @return {Partial< import('@wordpress/sync').ObjectData >} Changes to record
297+
*/
298+
getChangesFromCRDTDoc: ( crdtDoc, editedRecord ) =>
299+
getPostChangesFromCRDTDoc( crdtDoc, editedRecord, postType ),
300+
301+
/**
302+
* Sync features supported by the entity.
303+
*
304+
* @type {Record< string, boolean >}
305+
*/
306+
supports: {},
307+
};
308+
297309
return {
298310
kind: 'postType',
299311
baseURL: `/${ namespace }/${ postType.rest_base }`,
@@ -314,58 +326,7 @@ async function loadPostTypeEntities() {
314326
: String( record.id ) ),
315327
__unstablePrePersist: isTemplate ? undefined : prePersistPostType,
316328
__unstable_rest_base: postType.rest_base,
317-
syncConfig: {
318-
/**
319-
* Apply changes from the local editor to the local CRDT document so
320-
* that those changes can be synced to other peers (via the provider).
321-
*
322-
* @param {import('@wordpress/sync').CRDTDoc} crdtDoc
323-
* @param {Partial< import('@wordpress/sync').ObjectData >} changes
324-
* @return {void}
325-
*/
326-
applyChangesToCRDTDoc: ( crdtDoc, changes ) => {
327-
const document = crdtDoc.getMap( 'document' );
328-
329-
Object.entries( changes ).forEach( ( [ key, value ] ) => {
330-
if ( ! syncedProperties.has( key ) ) {
331-
return;
332-
}
333-
334-
if ( typeof value !== 'function' ) {
335-
if ( key === 'blocks' ) {
336-
if ( ! serialisableBlocksCache.has( value ) ) {
337-
serialisableBlocksCache.set(
338-
value,
339-
makeBlocksSerializable( value )
340-
);
341-
}
342-
343-
value = serialisableBlocksCache.get( value );
344-
}
345-
346-
if ( document.get( key ) !== value ) {
347-
document.set( key, value );
348-
}
349-
}
350-
} );
351-
},
352-
353-
/**
354-
* Extract changes from a CRDT document that can be used to update the
355-
* local editor state.
356-
*
357-
* @param {import('@wordpress/sync').CRDTDoc} crdtDoc
358-
* @return {Partial< import('@wordpress/sync').ObjectData >} Changes to record
359-
*/
360-
getChangesFromCRDTDoc: defaultGetChangesFromCRDTDoc,
361-
362-
/**
363-
* Sync features supported by the entity.
364-
*
365-
* @type {Record< string, boolean >}
366-
*/
367-
supports: {},
368-
},
329+
syncConfig,
369330
supportsPagination: true,
370331
getRevisionsUrl: ( parentId, revisionId ) =>
371332
`/${ namespace }/${

packages/core-data/src/resolvers.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,13 @@ export const getEntityRecord =
208208
},
209209
} );
210210
},
211+
// Get the current entity record (with edits)
212+
getEditedRecord: async () =>
213+
await resolveSelect.getEditedEntityRecord(
214+
kind,
215+
name,
216+
key
217+
),
211218
}
212219
);
213220
}

packages/core-data/src/sync.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44
import {
55
CRDT_RECORD_MAP_KEY,
66
LOCAL_EDITOR_ORIGIN,
7+
LOCAL_SYNC_MANAGER_ORIGIN,
78
createSyncManager,
89
} from '@wordpress/sync';
910

10-
export { CRDT_RECORD_MAP_KEY, LOCAL_EDITOR_ORIGIN };
11+
export { CRDT_RECORD_MAP_KEY, LOCAL_EDITOR_ORIGIN, LOCAL_SYNC_MANAGER_ORIGIN };
1112
export const syncManager = createSyncManager();

packages/core-data/src/test/resolvers.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,10 @@ describe( 'getEntityRecord', () => {
168168
'postType/post',
169169
1,
170170
POST_RECORD,
171-
{ editRecord: expect.any( Function ) }
171+
{
172+
editRecord: expect.any( Function ),
173+
getEditedRecord: expect.any( Function ),
174+
}
172175
);
173176
} );
174177

@@ -218,7 +221,10 @@ describe( 'getEntityRecord', () => {
218221
'postType/post',
219222
1,
220223
{ ...POST_RECORD, foo: 'bar' },
221-
{ editRecord: expect.any( Function ) }
224+
{
225+
editRecord: expect.any( Function ),
226+
getEditedRecord: expect.any( Function ),
227+
}
222228
);
223229
} );
224230

packages/core-data/src/types.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,15 @@
11
export interface AnyFunction {
22
( ...args: any[] ): any;
33
}
4+
5+
// Avoid a circular dependency with @wordpress/editor
6+
export interface WPBlockSelection {
7+
clientId: string;
8+
attributeKey: string;
9+
offset: number;
10+
}
11+
12+
export interface WPSelection {
13+
selectionEnd: WPBlockSelection;
14+
selectionStart: WPBlockSelection;
15+
}

0 commit comments

Comments
 (0)