@wordpress/core-data 7.33.1 → 7.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/utils/crdt.ts CHANGED
@@ -6,6 +6,8 @@ import fastDeepEqual from 'fast-deep-equal/es6';
6
6
  /**
7
7
  * WordPress dependencies
8
8
  */
9
+ // @ts-expect-error No exported types.
10
+ import { __unstableSerializeAndClean } from '@wordpress/blocks';
9
11
  import { type CRDTDoc, type ObjectData, Y } from '@wordpress/sync';
10
12
 
11
13
  /**
@@ -19,7 +21,11 @@ import {
19
21
  } from './crdt-blocks';
20
22
  import { type Post } from '../entity-types/post';
21
23
  import { type Type } from '../entity-types';
22
- import { CRDT_RECORD_MAP_KEY } from '../sync';
24
+ import {
25
+ CRDT_DOC_META_PERSISTENCE_KEY,
26
+ CRDT_RECORD_MAP_KEY,
27
+ WORDPRESS_META_KEY_FOR_CRDT_DOC_PERSISTENCE,
28
+ } from '../sync';
23
29
  import type { WPBlockSelection, WPSelection } from '../types';
24
30
 
25
31
  export type PostChanges = Partial< Post > & {
@@ -42,6 +48,7 @@ const allowedPostProperties = new Set< string >( [
42
48
  'featured_media',
43
49
  'format',
44
50
  'ping_status',
51
+ 'meta',
45
52
  'slug',
46
53
  'status',
47
54
  'sticky',
@@ -50,6 +57,11 @@ const allowedPostProperties = new Set< string >( [
50
57
  'title',
51
58
  ] );
52
59
 
60
+ // Post meta keys that should *not* be synced.
61
+ const disallowedPostMetaKeys = new Set< string >( [
62
+ WORDPRESS_META_KEY_FOR_CRDT_DOC_PERSISTENCE,
63
+ ] );
64
+
53
65
  /**
54
66
  * Given a set of local changes to a generic entity record, apply those changes
55
67
  * to the local Y.Doc.
@@ -92,13 +104,13 @@ export function defaultApplyChangesToCRDTDoc(
92
104
  *
93
105
  * @param {CRDTDoc} ydoc
94
106
  * @param {PostChanges} changes
95
- * @param {Type} postType
107
+ * @param {Type} _postType
96
108
  * @return {void}
97
109
  */
98
110
  export function applyPostChangesToCRDTDoc(
99
111
  ydoc: CRDTDoc,
100
112
  changes: PostChanges,
101
- postType: Type // eslint-disable-line @typescript-eslint/no-unused-vars
113
+ _postType: Type // eslint-disable-line @typescript-eslint/no-unused-vars
102
114
  ): void {
103
115
  const ymap = ydoc.getMap( CRDT_RECORD_MAP_KEY );
104
116
 
@@ -146,6 +158,36 @@ export function applyPostChangesToCRDTDoc(
146
158
  break;
147
159
  }
148
160
 
161
+ // "Meta" is overloaded term; here, it refers to post meta.
162
+ case 'meta': {
163
+ let metaMap = ymap.get( 'meta' ) as Y.Map< unknown >;
164
+
165
+ // Initialize.
166
+ if ( ! ( metaMap instanceof Y.Map ) ) {
167
+ metaMap = new Y.Map();
168
+ setValue( metaMap );
169
+ }
170
+
171
+ // Iterate over each meta property in the new value and merge it if it
172
+ // should be synced.
173
+ Object.entries( newValue ?? {} ).forEach(
174
+ ( [ metaKey, metaValue ] ) => {
175
+ if ( disallowedPostMetaKeys.has( metaKey ) ) {
176
+ return;
177
+ }
178
+
179
+ mergeValue(
180
+ metaMap.get( metaKey ), // current value in CRDT
181
+ metaValue, // new value from changes
182
+ ( updatedMetaValue: unknown ): void => {
183
+ metaMap.set( metaKey, updatedMetaValue );
184
+ }
185
+ );
186
+ }
187
+ );
188
+ break;
189
+ }
190
+
149
191
  case 'slug': {
150
192
  // Do not sync an empty slug. This indicates that the post is using
151
193
  // the default auto-generated slug.
@@ -198,17 +240,19 @@ export function defaultGetChangesFromCRDTDoc( crdtDoc: CRDTDoc ): ObjectData {
198
240
  *
199
241
  * @param {CRDTDoc} ydoc
200
242
  * @param {Post} editedRecord
201
- * @param {Type} postType
243
+ * @param {Type} _postType
202
244
  * @return {Partial<PostChanges>} The changes that should be applied to the local record.
203
245
  */
204
246
  export function getPostChangesFromCRDTDoc(
205
247
  ydoc: CRDTDoc,
206
248
  editedRecord: Post,
207
- postType: Type // eslint-disable-line @typescript-eslint/no-unused-vars
249
+ _postType: Type // eslint-disable-line @typescript-eslint/no-unused-vars
208
250
  ): PostChanges {
209
251
  const ymap = ydoc.getMap( CRDT_RECORD_MAP_KEY );
210
252
 
211
- return Object.fromEntries(
253
+ let allowedMetaChanges: Post[ 'meta' ] = {};
254
+
255
+ const changes = Object.fromEntries(
212
256
  Object.entries( ymap.toJSON() ).filter( ( [ key, newValue ] ) => {
213
257
  if ( ! allowedPostProperties.has( key ) ) {
214
258
  return false;
@@ -218,6 +262,35 @@ export function getPostChangesFromCRDTDoc(
218
262
 
219
263
  switch ( key ) {
220
264
  case 'blocks': {
265
+ // When we are passed a persisted CRDT document, make a special
266
+ // comparison of the content and blocks.
267
+ //
268
+ // When other fields (besides `blocks`) are mutated outside the block
269
+ // editor, the change is caught by an equality check (see other cases
270
+ // in this `switch` statement). As a transient property, `blocks`
271
+ // cannot be directly mutated outside the block editor -- only
272
+ // `content` can.
273
+ //
274
+ // Therefore, for this special comparison, we serialize the `blocks`
275
+ // from the persisted CRDT document and compare that to the content
276
+ // from the persisted record. If they differ, we know that the content
277
+ // in the database has changed, and therefore the blocks have changed.
278
+ //
279
+ // We cannot directly compare the `blocks` from the CRDT document to
280
+ // the `blocks` derived from the `content` in the persisted record,
281
+ // because the latter will have different client IDs.
282
+ if (
283
+ ydoc.meta?.get( CRDT_DOC_META_PERSISTENCE_KEY ) &&
284
+ editedRecord.content
285
+ ) {
286
+ const blocks = ymap.get( 'blocks' ) as YBlocks;
287
+ return (
288
+ __unstableSerializeAndClean(
289
+ blocks.toJSON()
290
+ ).trim() !== editedRecord.content.raw.trim()
291
+ );
292
+ }
293
+
221
294
  // The consumers of blocks have memoization that renders optimization
222
295
  // here unnecessary.
223
296
  return true;
@@ -240,6 +313,24 @@ export function getPostChangesFromCRDTDoc(
240
313
  return haveValuesChanged( currentValue, newValue );
241
314
  }
242
315
 
316
+ case 'meta': {
317
+ allowedMetaChanges = Object.fromEntries(
318
+ Object.entries( newValue ?? {} ).filter(
319
+ ( [ metaKey ] ) =>
320
+ ! disallowedPostMetaKeys.has( metaKey )
321
+ )
322
+ );
323
+
324
+ // Merge the allowed meta changes with the current meta values since
325
+ // not all meta properties are synced.
326
+ const mergedValue = {
327
+ ...( currentValue as PostChanges[ 'meta' ] ),
328
+ ...allowedMetaChanges,
329
+ };
330
+
331
+ return haveValuesChanged( currentValue, mergedValue );
332
+ }
333
+
243
334
  case 'status': {
244
335
  // Do not sync an invalid status.
245
336
  if ( 'auto-draft' === newValue ) {
@@ -265,6 +356,17 @@ export function getPostChangesFromCRDTDoc(
265
356
  }
266
357
  } )
267
358
  );
359
+
360
+ // Meta changes must be merged with the edited record since not all meta
361
+ // properties are synced.
362
+ if ( 'object' === typeof changes.meta ) {
363
+ changes.meta = {
364
+ ...editedRecord.meta,
365
+ ...allowedMetaChanges,
366
+ };
367
+ }
368
+
369
+ return changes;
268
370
  }
269
371
 
270
372
  /**
@@ -1,7 +1,11 @@
1
1
  /**
2
2
  * WordPress dependencies
3
3
  */
4
- import { CRDT_RECORD_MAP_KEY, Y } from '@wordpress/sync';
4
+ import {
5
+ CRDT_RECORD_MAP_KEY,
6
+ WORDPRESS_META_KEY_FOR_CRDT_DOC_PERSISTENCE,
7
+ Y,
8
+ } from '@wordpress/sync';
5
9
 
6
10
  /**
7
11
  * External dependencies
@@ -152,6 +156,55 @@ describe( 'crdt', () => {
152
156
  const blocks = map.get( 'blocks' );
153
157
  expect( blocks ).toBeInstanceOf( Y.Array );
154
158
  } );
159
+
160
+ it( 'syncs meta fields', () => {
161
+ const changes = {
162
+ meta: {
163
+ some_meta: 'new value',
164
+ },
165
+ };
166
+
167
+ const metaMap = new Y.Map< unknown >();
168
+ metaMap.set( 'some_meta', 'old value' );
169
+ map.set( 'meta', metaMap );
170
+
171
+ applyPostChangesToCRDTDoc( doc, changes, mockPostType );
172
+
173
+ expect( metaMap.get( 'some_meta' ) ).toBe( 'new value' );
174
+ } );
175
+
176
+ it( 'syncs non-single meta fields', () => {
177
+ const changes = {
178
+ meta: {
179
+ some_meta: [ 'value', 'value 2' ],
180
+ },
181
+ };
182
+
183
+ const metaMap = new Y.Map< unknown >();
184
+ metaMap.set( 'some_meta', 'old value' );
185
+ map.set( 'meta', metaMap );
186
+
187
+ applyPostChangesToCRDTDoc( doc, changes, mockPostType );
188
+
189
+ expect( metaMap.get( 'some_meta' ) ).toStrictEqual( [
190
+ 'value',
191
+ 'value 2',
192
+ ] );
193
+ } );
194
+
195
+ it( 'initializes meta as Y.Map when not present', () => {
196
+ const changes = {
197
+ meta: {
198
+ custom_field: 'value',
199
+ },
200
+ };
201
+
202
+ applyPostChangesToCRDTDoc( doc, changes, mockPostType );
203
+
204
+ const metaMap = map.get( 'meta' ) as Y.Map< unknown >;
205
+ expect( metaMap ).toBeInstanceOf( Y.Map );
206
+ expect( metaMap.get( 'custom_field' ) ).toBe( 'value' );
207
+ } );
155
208
  } );
156
209
 
157
210
  describe( 'getPostChangesFromCRDTDoc', () => {
@@ -250,5 +303,75 @@ describe( 'crdt', () => {
250
303
 
251
304
  expect( changes ).toHaveProperty( 'blocks' );
252
305
  } );
306
+
307
+ it( 'includes meta in changes', () => {
308
+ map.set( 'meta', {
309
+ public_meta: 'new value',
310
+ } );
311
+
312
+ const editedRecord = {
313
+ meta: {
314
+ public_meta: 'old value',
315
+ },
316
+ } as unknown as Post;
317
+
318
+ const changes = getPostChangesFromCRDTDoc(
319
+ doc,
320
+ editedRecord,
321
+ mockPostType
322
+ );
323
+
324
+ expect( changes.meta ).toEqual( {
325
+ public_meta: 'new value', // from CRDT
326
+ } );
327
+ } );
328
+
329
+ it( 'includes non-single meta in changes', () => {
330
+ map.set( 'meta', {
331
+ public_meta: [ 'value', 'value 2' ],
332
+ } );
333
+
334
+ const editedRecord = {
335
+ meta: {
336
+ public_meta: 'value',
337
+ },
338
+ } as unknown as Post;
339
+
340
+ const changes = getPostChangesFromCRDTDoc(
341
+ doc,
342
+ editedRecord,
343
+ mockPostType
344
+ );
345
+
346
+ expect( changes.meta ).toEqual( {
347
+ public_meta: [ 'value', 'value 2' ], // from CRDT
348
+ } );
349
+ } );
350
+
351
+ it( 'excludes disallowed meta keys in changes', () => {
352
+ map.set( 'meta', {
353
+ public_meta: 'new value',
354
+ [ WORDPRESS_META_KEY_FOR_CRDT_DOC_PERSISTENCE ]: 'exclude me',
355
+ } );
356
+
357
+ const editedRecord = {
358
+ meta: {
359
+ public_meta: 'old value',
360
+ },
361
+ } as unknown as Post;
362
+
363
+ const changes = getPostChangesFromCRDTDoc(
364
+ doc,
365
+ editedRecord,
366
+ mockPostType
367
+ );
368
+
369
+ expect( changes.meta ).toEqual( {
370
+ public_meta: 'new value', // from CRDT
371
+ } );
372
+ expect( changes.meta ).not.toHaveProperty(
373
+ WORDPRESS_META_KEY_FOR_CRDT_DOC_PERSISTENCE
374
+ );
375
+ } );
253
376
  } );
254
377
  } );