@wordpress/core-data 7.37.1-next.ba3aee3a2.0 → 7.38.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.
Files changed (65) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/build/actions.cjs +15 -3
  3. package/build/actions.cjs.map +2 -2
  4. package/build/entities.cjs +35 -45
  5. package/build/entities.cjs.map +2 -2
  6. package/build/private-selectors.cjs +2 -4
  7. package/build/private-selectors.cjs.map +2 -2
  8. package/build/reducer.cjs +1 -1
  9. package/build/reducer.cjs.map +1 -1
  10. package/build/resolvers.cjs +11 -2
  11. package/build/resolvers.cjs.map +2 -2
  12. package/build/types.cjs.map +1 -1
  13. package/build/utils/conservative-map-item.cjs +1 -1
  14. package/build/utils/conservative-map-item.cjs.map +1 -1
  15. package/build/utils/crdt-blocks.cjs +64 -42
  16. package/build/utils/crdt-blocks.cjs.map +2 -2
  17. package/build/utils/crdt-utils.cjs +44 -0
  18. package/build/utils/crdt-utils.cjs.map +7 -0
  19. package/build/utils/crdt.cjs +34 -38
  20. package/build/utils/crdt.cjs.map +2 -2
  21. package/build-module/actions.mjs +15 -3
  22. package/build-module/actions.mjs.map +2 -2
  23. package/build-module/entities.mjs +35 -47
  24. package/build-module/entities.mjs.map +2 -2
  25. package/build-module/private-selectors.mjs +2 -4
  26. package/build-module/private-selectors.mjs.map +2 -2
  27. package/build-module/reducer.mjs +1 -1
  28. package/build-module/reducer.mjs.map +1 -1
  29. package/build-module/resolvers.mjs +11 -2
  30. package/build-module/resolvers.mjs.map +2 -2
  31. package/build-module/utils/conservative-map-item.mjs +1 -1
  32. package/build-module/utils/conservative-map-item.mjs.map +1 -1
  33. package/build-module/utils/crdt-blocks.mjs +65 -43
  34. package/build-module/utils/crdt-blocks.mjs.map +2 -2
  35. package/build-module/utils/crdt-utils.mjs +17 -0
  36. package/build-module/utils/crdt-utils.mjs.map +7 -0
  37. package/build-module/utils/crdt.mjs +38 -38
  38. package/build-module/utils/crdt.mjs.map +2 -2
  39. package/build-types/actions.d.ts.map +1 -1
  40. package/build-types/entities.d.ts.map +1 -1
  41. package/build-types/index.d.ts.map +1 -1
  42. package/build-types/private-selectors.d.ts.map +1 -1
  43. package/build-types/resolvers.d.ts.map +1 -1
  44. package/build-types/types.d.ts +0 -5
  45. package/build-types/types.d.ts.map +1 -1
  46. package/build-types/utils/crdt-blocks.d.ts +14 -5
  47. package/build-types/utils/crdt-blocks.d.ts.map +1 -1
  48. package/build-types/utils/crdt-utils.d.ts +53 -0
  49. package/build-types/utils/crdt-utils.d.ts.map +1 -0
  50. package/build-types/utils/crdt.d.ts +19 -1
  51. package/build-types/utils/crdt.d.ts.map +1 -1
  52. package/package.json +18 -18
  53. package/src/actions.js +14 -3
  54. package/src/entities.js +38 -54
  55. package/src/private-selectors.ts +3 -4
  56. package/src/reducer.js +1 -1
  57. package/src/resolvers.js +15 -7
  58. package/src/test/entities.js +11 -9
  59. package/src/test/resolvers.js +3 -45
  60. package/src/types.ts +0 -6
  61. package/src/utils/conservative-map-item.js +1 -1
  62. package/src/utils/crdt-blocks.ts +102 -100
  63. package/src/utils/crdt-utils.ts +77 -0
  64. package/src/utils/crdt.ts +77 -58
  65. package/src/utils/test/crdt.ts +28 -16
package/src/utils/crdt.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * External dependencies
3
3
  */
4
- import fastDeepEqual from 'fast-deep-equal/es6';
4
+ import fastDeepEqual from 'fast-deep-equal/es6/index.js';
5
5
 
6
6
  /**
7
7
  * WordPress dependencies
@@ -26,8 +26,16 @@ import {
26
26
  CRDT_RECORD_MAP_KEY,
27
27
  WORDPRESS_META_KEY_FOR_CRDT_DOC_PERSISTENCE,
28
28
  } from '../sync';
29
- import type { WPBlockSelection, WPSelection } from '../types';
30
-
29
+ import type { WPSelection } from '../types';
30
+ import {
31
+ createYMap,
32
+ getRootMap,
33
+ isYMap,
34
+ type YMapRecord,
35
+ type YMapWrap,
36
+ } from './crdt-utils';
37
+
38
+ // Changes that can be applied to a post entity record.
31
39
  export type PostChanges = Partial< Post > & {
32
40
  blocks?: Block[];
33
41
  excerpt?: Post[ 'excerpt' ] | string;
@@ -35,8 +43,24 @@ export type PostChanges = Partial< Post > & {
35
43
  title?: Post[ 'title' ] | string;
36
44
  };
37
45
 
38
- // Hold a reference to the last known selection to help compute Y.Text deltas.
39
- let lastSelection: WPBlockSelection | null = null;
46
+ // A post record as represented in the CRDT document (Y.Map).
47
+ export interface YPostRecord extends YMapRecord {
48
+ author: number;
49
+ blocks: YBlocks;
50
+ comment_status: string;
51
+ date: string | null;
52
+ excerpt: string;
53
+ featured_media: number;
54
+ format: string;
55
+ meta: YMapWrap< YMapRecord >;
56
+ ping_status: string;
57
+ slug: string;
58
+ status: string;
59
+ sticky: boolean;
60
+ tags: number[];
61
+ template: string;
62
+ title: string;
63
+ }
40
64
 
41
65
  // Properties that are allowed to be synced for a post.
42
66
  const allowedPostProperties = new Set< string >( [
@@ -47,8 +71,8 @@ const allowedPostProperties = new Set< string >( [
47
71
  'excerpt',
48
72
  'featured_media',
49
73
  'format',
50
- 'ping_status',
51
74
  'meta',
75
+ 'ping_status',
52
76
  'slug',
53
77
  'status',
54
78
  'sticky',
@@ -74,7 +98,7 @@ export function defaultApplyChangesToCRDTDoc(
74
98
  ydoc: CRDTDoc,
75
99
  changes: ObjectData
76
100
  ): void {
77
- const ymap = ydoc.getMap( CRDT_RECORD_MAP_KEY );
101
+ const ymap = getRootMap( ydoc, CRDT_RECORD_MAP_KEY );
78
102
 
79
103
  Object.entries( changes ).forEach( ( [ key, newValue ] ) => {
80
104
  // Cannot serialize function values, so cannot sync them.
@@ -82,17 +106,12 @@ export function defaultApplyChangesToCRDTDoc(
82
106
  return;
83
107
  }
84
108
 
85
- // Set the value in the root document.
86
- function setValue< T = unknown >( updatedValue: T ): void {
87
- ymap.set( key, updatedValue );
88
- }
89
-
90
109
  switch ( key ) {
91
110
  // Add support for additional data types here.
92
111
 
93
112
  default: {
94
113
  const currentValue = ymap.get( key );
95
- mergeValue( currentValue, newValue, setValue );
114
+ updateMapValue( ymap, key, currentValue, newValue );
96
115
  }
97
116
  }
98
117
  } );
@@ -112,60 +131,60 @@ export function applyPostChangesToCRDTDoc(
112
131
  changes: PostChanges,
113
132
  _postType: Type // eslint-disable-line @typescript-eslint/no-unused-vars
114
133
  ): void {
115
- const ymap = ydoc.getMap( CRDT_RECORD_MAP_KEY );
134
+ const ymap = getRootMap< YPostRecord >( ydoc, CRDT_RECORD_MAP_KEY );
116
135
 
117
- Object.entries( changes ).forEach( ( [ key, newValue ] ) => {
136
+ Object.keys( changes ).forEach( ( key ) => {
118
137
  if ( ! allowedPostProperties.has( key ) ) {
119
138
  return;
120
139
  }
121
140
 
141
+ const newValue = changes[ key ];
142
+
122
143
  // Cannot serialize function values, so cannot sync them.
123
144
  if ( 'function' === typeof newValue ) {
124
145
  return;
125
146
  }
126
147
 
127
- // Set the value in the root document.
128
- function setValue< T = unknown >( updatedValue: T ): void {
129
- ymap.set( key, updatedValue );
130
- }
131
-
132
148
  switch ( key ) {
133
149
  case 'blocks': {
134
- let currentBlocks = ymap.get( 'blocks' ) as YBlocks;
150
+ let currentBlocks = ymap.get( key );
135
151
 
136
152
  // Initialize.
137
153
  if ( ! ( currentBlocks instanceof Y.Array ) ) {
138
154
  currentBlocks = new Y.Array< YBlock >();
139
- setValue( currentBlocks );
155
+ ymap.set( key, currentBlocks );
140
156
  }
141
157
 
142
158
  // Block[] from local changes.
143
159
  const newBlocks = ( newValue as PostChanges[ 'blocks' ] ) ?? [];
144
160
 
161
+ // Block changes from typing are bundled with a 'selection' update.
162
+ // Pass the resulting cursor position to the mergeCrdtBlocks function.
163
+ const cursorPosition =
164
+ changes.selection?.selectionStart?.offset ?? null;
165
+
145
166
  // Merge blocks does not need `setValue` because it is operating on a
146
167
  // Yjs type that is already in the Y.Doc.
147
- mergeCrdtBlocks( currentBlocks, newBlocks, lastSelection );
168
+ mergeCrdtBlocks( currentBlocks, newBlocks, cursorPosition );
148
169
  break;
149
170
  }
150
171
 
151
172
  case 'excerpt': {
152
- const currentValue = ymap.get( 'excerpt' ) as
153
- | string
154
- | undefined;
173
+ const currentValue = ymap.get( 'excerpt' );
155
174
  const rawNewValue = getRawValue( newValue );
156
175
 
157
- mergeValue( currentValue, rawNewValue, setValue );
176
+ updateMapValue( ymap, key, currentValue, rawNewValue );
158
177
  break;
159
178
  }
160
179
 
161
180
  // "Meta" is overloaded term; here, it refers to post meta.
162
181
  case 'meta': {
163
- let metaMap = ymap.get( 'meta' ) as Y.Map< unknown >;
182
+ let metaMap = ymap.get( 'meta' );
164
183
 
165
184
  // Initialize.
166
- if ( ! ( metaMap instanceof Y.Map ) ) {
167
- metaMap = new Y.Map();
168
- setValue( metaMap );
185
+ if ( ! isYMap( metaMap ) ) {
186
+ metaMap = createYMap< YMapRecord >();
187
+ ymap.set( 'meta', metaMap );
169
188
  }
170
189
 
171
190
  // Iterate over each meta property in the new value and merge it if it
@@ -176,12 +195,11 @@ export function applyPostChangesToCRDTDoc(
176
195
  return;
177
196
  }
178
197
 
179
- mergeValue(
198
+ updateMapValue(
199
+ metaMap,
200
+ metaKey,
180
201
  metaMap.get( metaKey ), // current value in CRDT
181
- metaValue, // new value from changes
182
- ( updatedMetaValue: unknown ): void => {
183
- metaMap.set( metaKey, updatedMetaValue );
184
- }
202
+ metaValue // new value from changes
185
203
  );
186
204
  }
187
205
  );
@@ -195,13 +213,13 @@ export function applyPostChangesToCRDTDoc(
195
213
  break;
196
214
  }
197
215
 
198
- const currentValue = ymap.get( 'slug' ) as string;
199
- mergeValue( currentValue, newValue, setValue );
216
+ const currentValue = ymap.get( key );
217
+ updateMapValue( ymap, key, currentValue, newValue );
200
218
  break;
201
219
  }
202
220
 
203
221
  case 'title': {
204
- const currentValue = ymap.get( 'title' ) as string | undefined;
222
+ const currentValue = ymap.get( key );
205
223
 
206
224
  // Copy logic from prePersistPostType to ensure that the "Auto
207
225
  // Draft" template title is not synced.
@@ -210,27 +228,22 @@ export function applyPostChangesToCRDTDoc(
210
228
  rawNewValue = '';
211
229
  }
212
230
 
213
- mergeValue( currentValue, rawNewValue, setValue );
231
+ updateMapValue( ymap, key, currentValue, rawNewValue );
214
232
  break;
215
233
  }
216
234
 
217
- // Add support for additional data types here.
235
+ // Add support for additional properties here.
218
236
 
219
237
  default: {
220
238
  const currentValue = ymap.get( key );
221
- mergeValue( currentValue, newValue, setValue );
239
+ updateMapValue( ymap, key, currentValue, newValue );
222
240
  }
223
241
  }
224
242
  } );
225
-
226
- // Update the lastSelection for use in computing Y.Text deltas.
227
- if ( 'selection' in changes ) {
228
- lastSelection = changes.selection?.selectionStart ?? null;
229
- }
230
243
  }
231
244
 
232
245
  export function defaultGetChangesFromCRDTDoc( crdtDoc: CRDTDoc ): ObjectData {
233
- return crdtDoc.getMap( CRDT_RECORD_MAP_KEY ).toJSON();
246
+ return getRootMap( crdtDoc, CRDT_RECORD_MAP_KEY ).toJSON();
234
247
  }
235
248
 
236
249
  /**
@@ -248,7 +261,7 @@ export function getPostChangesFromCRDTDoc(
248
261
  editedRecord: Post,
249
262
  _postType: Type // eslint-disable-line @typescript-eslint/no-unused-vars
250
263
  ): PostChanges {
251
- const ymap = ydoc.getMap( CRDT_RECORD_MAP_KEY );
264
+ const ymap = getRootMap< YPostRecord >( ydoc, CRDT_RECORD_MAP_KEY );
252
265
 
253
266
  let allowedMetaChanges: Post[ 'meta' ] = {};
254
267
 
@@ -394,19 +407,25 @@ function getRawValue( value?: unknown ): string | undefined {
394
407
  return undefined;
395
408
  }
396
409
 
397
- function haveValuesChanged< ValueType = any >(
398
- currentValue: ValueType,
399
- newValue: ValueType
410
+ function haveValuesChanged< ValueType >(
411
+ currentValue: ValueType | undefined,
412
+ newValue: ValueType | undefined
400
413
  ): boolean {
401
414
  return ! fastDeepEqual( currentValue, newValue );
402
415
  }
403
416
 
404
- function mergeValue< ValueType = any >(
405
- currentValue: ValueType,
406
- newValue: ValueType,
407
- setValue: ( value: ValueType ) => void
417
+ function updateMapValue< T extends YMapRecord, K extends keyof T >(
418
+ map: YMapWrap< T >,
419
+ key: K,
420
+ currentValue: T[ K ] | undefined,
421
+ newValue: T[ K ] | undefined
408
422
  ): void {
409
- if ( haveValuesChanged< ValueType >( currentValue, newValue ) ) {
410
- setValue( newValue );
423
+ if ( undefined === newValue ) {
424
+ map.delete( key );
425
+ return;
426
+ }
427
+
428
+ if ( haveValuesChanged< T[ K ] >( currentValue, newValue ) ) {
429
+ map.set( key, newValue );
411
430
  }
412
431
  }
@@ -19,17 +19,17 @@ import {
19
19
  applyPostChangesToCRDTDoc,
20
20
  getPostChangesFromCRDTDoc,
21
21
  type PostChanges,
22
+ type YPostRecord,
22
23
  } from '../crdt';
23
24
  import type { YBlock, YBlocks } from '../crdt-blocks';
25
+ import { createYMap, getRootMap, type YMapWrap } from '../crdt-utils';
24
26
  import type { Post, Type } from '../../entity-types';
25
27
 
26
28
  describe( 'crdt', () => {
27
29
  let doc: Y.Doc;
28
- let map: Y.Map< string | object | YBlocks >;
29
30
 
30
31
  beforeEach( () => {
31
32
  doc = new Y.Doc();
32
- map = doc.getMap( CRDT_RECORD_MAP_KEY );
33
33
  jest.clearAllMocks();
34
34
  } );
35
35
 
@@ -40,6 +40,12 @@ describe( 'crdt', () => {
40
40
  describe( 'applyPostChangesToCRDTDoc', () => {
41
41
  const mockPostType = {} as Type;
42
42
 
43
+ let map: YMapWrap< YPostRecord >;
44
+
45
+ beforeEach( () => {
46
+ map = getRootMap< YPostRecord >( doc, CRDT_RECORD_MAP_KEY );
47
+ } );
48
+
43
49
  it( 'applies simple property changes', () => {
44
50
  const changes = {
45
51
  title: 'New Title',
@@ -164,7 +170,7 @@ describe( 'crdt', () => {
164
170
  },
165
171
  };
166
172
 
167
- const metaMap = new Y.Map< unknown >();
173
+ const metaMap = createYMap();
168
174
  metaMap.set( 'some_meta', 'old value' );
169
175
  map.set( 'meta', metaMap );
170
176
 
@@ -180,7 +186,7 @@ describe( 'crdt', () => {
180
186
  },
181
187
  };
182
188
 
183
- const metaMap = new Y.Map< unknown >();
189
+ const metaMap = createYMap();
184
190
  metaMap.set( 'some_meta', 'old value' );
185
191
  map.set( 'meta', metaMap );
186
192
 
@@ -201,9 +207,9 @@ describe( 'crdt', () => {
201
207
 
202
208
  applyPostChangesToCRDTDoc( doc, changes, mockPostType );
203
209
 
204
- const metaMap = map.get( 'meta' ) as Y.Map< unknown >;
210
+ const metaMap = map.get( 'meta' );
205
211
  expect( metaMap ).toBeInstanceOf( Y.Map );
206
- expect( metaMap.get( 'custom_field' ) ).toBe( 'value' );
212
+ expect( metaMap?.get( 'custom_field' ) ).toBe( 'value' );
207
213
  } );
208
214
  } );
209
215
 
@@ -216,7 +222,10 @@ describe( 'crdt', () => {
216
222
  },
217
223
  } as unknown as Type;
218
224
 
225
+ let map: YMapWrap< YPostRecord >;
226
+
219
227
  beforeEach( () => {
228
+ map = getRootMap< YPostRecord >( doc, CRDT_RECORD_MAP_KEY );
220
229
  map.set( 'title', 'CRDT Title' );
221
230
  map.set( 'status', 'draft' );
222
231
  map.set( 'date', '2025-01-01' );
@@ -344,9 +353,9 @@ describe( 'crdt', () => {
344
353
  } );
345
354
 
346
355
  it( 'includes meta in changes', () => {
347
- map.set( 'meta', {
348
- public_meta: 'new value',
349
- } );
356
+ const metaMap = createYMap();
357
+ metaMap.set( 'public_meta', 'new value' );
358
+ map.set( 'meta', metaMap );
350
359
 
351
360
  const editedRecord = {
352
361
  meta: {
@@ -366,9 +375,9 @@ describe( 'crdt', () => {
366
375
  } );
367
376
 
368
377
  it( 'includes non-single meta in changes', () => {
369
- map.set( 'meta', {
370
- public_meta: [ 'value', 'value 2' ],
371
- } );
378
+ const metaMap = createYMap();
379
+ metaMap.set( 'public_meta', [ 'value', 'value 2' ] );
380
+ map.set( 'meta', metaMap );
372
381
 
373
382
  const editedRecord = {
374
383
  meta: {
@@ -388,10 +397,13 @@ describe( 'crdt', () => {
388
397
  } );
389
398
 
390
399
  it( 'excludes disallowed meta keys in changes', () => {
391
- map.set( 'meta', {
392
- public_meta: 'new value',
393
- [ WORDPRESS_META_KEY_FOR_CRDT_DOC_PERSISTENCE ]: 'exclude me',
394
- } );
400
+ const metaMap = createYMap();
401
+ metaMap.set( 'public_meta', 'new value' );
402
+ metaMap.set(
403
+ WORDPRESS_META_KEY_FOR_CRDT_DOC_PERSISTENCE,
404
+ 'exclude me'
405
+ );
406
+ map.set( 'meta', metaMap );
395
407
 
396
408
  const editedRecord = {
397
409
  meta: {