@wordpress/core-data 7.40.2-next.v.202602271551.0 → 7.40.2

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 (182) hide show
  1. package/README.md +0 -27
  2. package/build/actions.cjs +9 -26
  3. package/build/actions.cjs.map +2 -2
  4. package/build/awareness/post-editor-awareness.cjs +45 -6
  5. package/build/awareness/post-editor-awareness.cjs.map +2 -2
  6. package/build/awareness/types.cjs.map +1 -1
  7. package/build/entities.cjs +36 -16
  8. package/build/entities.cjs.map +2 -2
  9. package/build/entity-provider.cjs +15 -6
  10. package/build/entity-provider.cjs.map +2 -2
  11. package/build/hooks/use-entity-prop.cjs +33 -2
  12. package/build/hooks/use-entity-prop.cjs.map +2 -2
  13. package/build/hooks/use-post-editor-awareness-state.cjs +120 -1
  14. package/build/hooks/use-post-editor-awareness-state.cjs.map +2 -2
  15. package/build/index.cjs +0 -3
  16. package/build/index.cjs.map +2 -2
  17. package/build/private-actions.cjs +27 -3
  18. package/build/private-actions.cjs.map +2 -2
  19. package/build/private-apis.cjs +14 -5
  20. package/build/private-apis.cjs.map +3 -3
  21. package/build/private-selectors.cjs +22 -2
  22. package/build/private-selectors.cjs.map +2 -2
  23. package/build/queried-data/actions.cjs +1 -1
  24. package/build/queried-data/actions.cjs.map +2 -2
  25. package/build/queried-data/reducer.cjs +19 -13
  26. package/build/queried-data/reducer.cjs.map +2 -2
  27. package/build/reducer.cjs +19 -2
  28. package/build/reducer.cjs.map +2 -2
  29. package/build/resolvers.cjs +24 -17
  30. package/build/resolvers.cjs.map +2 -2
  31. package/build/selectors.cjs +0 -15
  32. package/build/selectors.cjs.map +2 -2
  33. package/build/sync.cjs +12 -3
  34. package/build/sync.cjs.map +2 -2
  35. package/build/types.cjs.map +1 -1
  36. package/build/utils/block-selection-history.cjs +1 -1
  37. package/build/utils/block-selection-history.cjs.map +2 -2
  38. package/build/utils/crdt-blocks.cjs +85 -28
  39. package/build/utils/crdt-blocks.cjs.map +2 -2
  40. package/build/utils/crdt-selection.cjs +4 -1
  41. package/build/utils/crdt-selection.cjs.map +2 -2
  42. package/build/utils/crdt-text.cjs +52 -0
  43. package/build/utils/crdt-text.cjs.map +7 -0
  44. package/build/utils/crdt-user-selections.cjs +16 -6
  45. package/build/utils/crdt-user-selections.cjs.map +3 -3
  46. package/build/utils/crdt-utils.cjs +54 -2
  47. package/build/utils/crdt-utils.cjs.map +2 -2
  48. package/build/utils/crdt.cjs +26 -28
  49. package/build/utils/crdt.cjs.map +2 -2
  50. package/build-module/actions.mjs +14 -26
  51. package/build-module/actions.mjs.map +2 -2
  52. package/build-module/awareness/post-editor-awareness.mjs +47 -7
  53. package/build-module/awareness/post-editor-awareness.mjs.map +2 -2
  54. package/build-module/entities.mjs +37 -16
  55. package/build-module/entities.mjs.map +2 -2
  56. package/build-module/entity-provider.mjs +15 -6
  57. package/build-module/entity-provider.mjs.map +2 -2
  58. package/build-module/hooks/use-entity-prop.mjs +34 -3
  59. package/build-module/hooks/use-entity-prop.mjs.map +2 -2
  60. package/build-module/hooks/use-post-editor-awareness-state.mjs +117 -1
  61. package/build-module/hooks/use-post-editor-awareness-state.mjs.map +2 -2
  62. package/build-module/index.mjs +0 -2
  63. package/build-module/index.mjs.map +2 -2
  64. package/build-module/private-actions.mjs +24 -2
  65. package/build-module/private-actions.mjs.map +2 -2
  66. package/build-module/private-apis.mjs +21 -6
  67. package/build-module/private-apis.mjs.map +3 -3
  68. package/build-module/private-selectors.mjs +19 -1
  69. package/build-module/private-selectors.mjs.map +2 -2
  70. package/build-module/queried-data/actions.mjs +1 -1
  71. package/build-module/queried-data/actions.mjs.map +2 -2
  72. package/build-module/queried-data/reducer.mjs +19 -13
  73. package/build-module/queried-data/reducer.mjs.map +2 -2
  74. package/build-module/reducer.mjs +18 -2
  75. package/build-module/reducer.mjs.map +2 -2
  76. package/build-module/resolvers.mjs +24 -17
  77. package/build-module/resolvers.mjs.map +2 -2
  78. package/build-module/selectors.mjs +0 -14
  79. package/build-module/selectors.mjs.map +2 -2
  80. package/build-module/sync.mjs +8 -2
  81. package/build-module/sync.mjs.map +2 -2
  82. package/build-module/utils/block-selection-history.mjs +5 -2
  83. package/build-module/utils/block-selection-history.mjs.map +2 -2
  84. package/build-module/utils/crdt-blocks.mjs +84 -28
  85. package/build-module/utils/crdt-blocks.mjs.map +2 -2
  86. package/build-module/utils/crdt-selection.mjs +8 -2
  87. package/build-module/utils/crdt-selection.mjs.map +2 -2
  88. package/build-module/utils/crdt-text.mjs +26 -0
  89. package/build-module/utils/crdt-text.mjs.map +7 -0
  90. package/build-module/utils/crdt-user-selections.mjs +16 -7
  91. package/build-module/utils/crdt-user-selections.mjs.map +3 -3
  92. package/build-module/utils/crdt-utils.mjs +51 -1
  93. package/build-module/utils/crdt-utils.mjs.map +2 -2
  94. package/build-module/utils/crdt.mjs +26 -28
  95. package/build-module/utils/crdt.mjs.map +2 -2
  96. package/build-types/actions.d.ts +0 -11
  97. package/build-types/actions.d.ts.map +1 -1
  98. package/build-types/awareness/post-editor-awareness.d.ts +2 -2
  99. package/build-types/awareness/post-editor-awareness.d.ts.map +1 -1
  100. package/build-types/awareness/types.d.ts +6 -1
  101. package/build-types/awareness/types.d.ts.map +1 -1
  102. package/build-types/entities.d.ts +20 -1
  103. package/build-types/entities.d.ts.map +1 -1
  104. package/build-types/entity-provider.d.ts +11 -6
  105. package/build-types/entity-provider.d.ts.map +1 -1
  106. package/build-types/hooks/use-entity-prop.d.ts.map +1 -1
  107. package/build-types/hooks/use-post-editor-awareness-state.d.ts +40 -7
  108. package/build-types/hooks/use-post-editor-awareness-state.d.ts.map +1 -1
  109. package/build-types/index.d.ts +0 -3
  110. package/build-types/index.d.ts.map +1 -1
  111. package/build-types/parsed-blocks-cache.d.ts +10 -0
  112. package/build-types/parsed-blocks-cache.d.ts.map +1 -0
  113. package/build-types/private-actions.d.ts +12 -0
  114. package/build-types/private-actions.d.ts.map +1 -1
  115. package/build-types/private-apis.d.ts +21 -1
  116. package/build-types/private-apis.d.ts.map +1 -1
  117. package/build-types/private-selectors.d.ts +17 -0
  118. package/build-types/private-selectors.d.ts.map +1 -1
  119. package/build-types/queried-data/reducer.d.ts.map +1 -1
  120. package/build-types/reducer.d.ts +15 -0
  121. package/build-types/reducer.d.ts.map +1 -1
  122. package/build-types/resolvers.d.ts.map +1 -1
  123. package/build-types/selectors.d.ts +1 -9
  124. package/build-types/selectors.d.ts.map +1 -1
  125. package/build-types/sync.d.ts +2 -2
  126. package/build-types/sync.d.ts.map +1 -1
  127. package/build-types/types.d.ts +10 -2
  128. package/build-types/types.d.ts.map +1 -1
  129. package/build-types/utils/block-selection-history.d.ts.map +1 -1
  130. package/build-types/utils/crdt-blocks.d.ts +11 -0
  131. package/build-types/utils/crdt-blocks.d.ts.map +1 -1
  132. package/build-types/utils/crdt-selection.d.ts.map +1 -1
  133. package/build-types/utils/crdt-text.d.ts +16 -0
  134. package/build-types/utils/crdt-text.d.ts.map +1 -0
  135. package/build-types/utils/crdt-user-selections.d.ts +17 -4
  136. package/build-types/utils/crdt-user-selections.d.ts.map +1 -1
  137. package/build-types/utils/crdt-utils.d.ts +20 -0
  138. package/build-types/utils/crdt-utils.d.ts.map +1 -1
  139. package/build-types/utils/crdt.d.ts +13 -7
  140. package/build-types/utils/crdt.d.ts.map +1 -1
  141. package/build-types/utils/normalize-query-for-resolution.d.ts +12 -0
  142. package/build-types/utils/normalize-query-for-resolution.d.ts.map +1 -0
  143. package/build-types/utils/save-crdt-doc.d.ts +8 -0
  144. package/build-types/utils/save-crdt-doc.d.ts.map +1 -0
  145. package/build-types/utils/test/crdt-utils.d.ts +2 -0
  146. package/build-types/utils/test/crdt-utils.d.ts.map +1 -0
  147. package/package.json +18 -18
  148. package/src/actions.js +26 -41
  149. package/src/awareness/post-editor-awareness.ts +106 -7
  150. package/src/awareness/test/post-editor-awareness.ts +50 -10
  151. package/src/awareness/types.ts +7 -1
  152. package/src/entities.js +43 -17
  153. package/src/entity-provider.js +24 -11
  154. package/src/hooks/test/use-post-editor-awareness-state.ts +446 -3
  155. package/src/hooks/use-entity-prop.js +43 -3
  156. package/src/hooks/use-post-editor-awareness-state.ts +229 -7
  157. package/src/index.js +0 -6
  158. package/src/private-actions.js +43 -1
  159. package/src/{private-apis.js → private-apis.ts} +21 -3
  160. package/src/private-selectors.ts +43 -0
  161. package/src/queried-data/actions.js +1 -1
  162. package/src/queried-data/reducer.js +26 -14
  163. package/src/reducer.js +36 -1
  164. package/src/resolvers.js +33 -23
  165. package/src/selectors.ts +1 -32
  166. package/src/sync.ts +6 -0
  167. package/src/test/entities.js +204 -16
  168. package/src/test/private-actions.js +1 -1
  169. package/src/test/resolvers.js +144 -57
  170. package/src/test/store.js +116 -0
  171. package/src/types.ts +20 -2
  172. package/src/utils/block-selection-history.ts +5 -2
  173. package/src/utils/crdt-blocks.ts +159 -56
  174. package/src/utils/crdt-selection.ts +8 -2
  175. package/src/utils/crdt-text.ts +43 -0
  176. package/src/utils/crdt-user-selections.ts +29 -8
  177. package/src/utils/crdt-utils.ts +99 -0
  178. package/src/utils/crdt.ts +50 -40
  179. package/src/utils/test/crdt-blocks.ts +588 -24
  180. package/src/utils/test/crdt-user-selections.ts +5 -0
  181. package/src/utils/test/crdt-utils.ts +387 -0
  182. package/src/utils/test/crdt.ts +291 -54
@@ -8,12 +8,56 @@ import { Y } from '@wordpress/sync';
8
8
  */
9
9
  import { describe, expect, it, jest, beforeEach } from '@jest/globals';
10
10
 
11
+ /**
12
+ * Mock getBlockTypes so isRichTextAttribute can identify rich-text attrs.
13
+ */
14
+ jest.mock( '@wordpress/blocks', () => {
15
+ const actual = jest.requireActual( '@wordpress/blocks' ) as Record<
16
+ string,
17
+ unknown
18
+ >;
19
+ return {
20
+ ...actual,
21
+ getBlockTypes: () => [
22
+ {
23
+ name: 'core/paragraph',
24
+ attributes: { content: { type: 'rich-text' } },
25
+ },
26
+ {
27
+ name: 'core/table',
28
+ attributes: {
29
+ hasFixedLayout: { type: 'boolean' },
30
+ caption: { type: 'rich-text' },
31
+ body: {
32
+ type: 'array',
33
+ query: {
34
+ cells: {
35
+ type: 'array',
36
+ query: {
37
+ content: { type: 'rich-text' },
38
+ tag: { type: 'string' },
39
+ },
40
+ },
41
+ },
42
+ },
43
+ },
44
+ },
45
+ ],
46
+ };
47
+ } );
48
+
49
+ /**
50
+ * WordPress dependencies
51
+ */
52
+ import { RichTextData } from '@wordpress/rich-text';
53
+
11
54
  /**
12
55
  * Internal dependencies
13
56
  */
14
57
  import { CRDT_RECORD_MAP_KEY } from '../../sync';
15
58
  import {
16
59
  applyPostChangesToCRDTDoc,
60
+ defaultCollectionSyncConfig,
17
61
  getPostChangesFromCRDTDoc,
18
62
  POST_META_KEY_FOR_CRDT_DOC_PERSISTENCE,
19
63
  type PostChanges,
@@ -22,7 +66,59 @@ import {
22
66
  import type { YBlock, YBlockRecord, YBlocks } from '../crdt-blocks';
23
67
  import { updateSelectionHistory } from '../crdt-selection';
24
68
  import { createYMap, getRootMap, type YMapWrap } from '../crdt-utils';
25
- import type { Post, Type } from '../../entity-types';
69
+ import type { Post } from '../../entity-types';
70
+
71
+ // Default synced properties matching the base set built in entities.js,
72
+ // plus 'categories' and 'tags' as example taxonomy rest_base values.
73
+ const defaultSyncedProperties = new Set< string >( [
74
+ 'blocks',
75
+ 'categories',
76
+ 'content',
77
+ 'date',
78
+ 'excerpt',
79
+ 'meta',
80
+ 'slug',
81
+ 'status',
82
+ 'tags',
83
+ 'title',
84
+ ] );
85
+
86
+ describe( 'defaultCollectionSyncConfig', () => {
87
+ it( 'has no-op applyChangesToCRDTDoc', () => {
88
+ const doc = new Y.Doc();
89
+ // Should not throw and return undefined.
90
+ expect(
91
+ defaultCollectionSyncConfig.applyChangesToCRDTDoc( doc, {
92
+ title: 'test',
93
+ } )
94
+ ).toBeUndefined();
95
+ doc.destroy();
96
+ } );
97
+
98
+ it( 'has getChangesFromCRDTDoc that returns empty object', () => {
99
+ const doc = new Y.Doc();
100
+ const result = defaultCollectionSyncConfig.getChangesFromCRDTDoc( doc, {
101
+ title: 'test',
102
+ } );
103
+ expect( result ).toEqual( {} );
104
+ doc.destroy();
105
+ } );
106
+
107
+ it( 'shouldSync returns true when objectId is null (collection)', () => {
108
+ expect(
109
+ defaultCollectionSyncConfig.shouldSync?.( 'comment', null )
110
+ ).toBe( true );
111
+ } );
112
+
113
+ it( 'shouldSync returns false when objectId is provided (individual record)', () => {
114
+ expect(
115
+ defaultCollectionSyncConfig.shouldSync?.( 'comment', '123' )
116
+ ).toBe( false );
117
+ expect(
118
+ defaultCollectionSyncConfig.shouldSync?.( 'comment', 'foo' )
119
+ ).toBe( false );
120
+ } );
121
+ } );
26
122
 
27
123
  describe( 'crdt', () => {
28
124
  let doc: Y.Doc;
@@ -37,8 +133,6 @@ describe( 'crdt', () => {
37
133
  } );
38
134
 
39
135
  describe( 'applyPostChangesToCRDTDoc', () => {
40
- const mockPostType = {} as Type;
41
-
42
136
  let map: YMapWrap< YPostRecord >;
43
137
 
44
138
  beforeEach( () => {
@@ -50,7 +144,7 @@ describe( 'crdt', () => {
50
144
  title: 'New Title',
51
145
  } as PostChanges;
52
146
 
53
- applyPostChangesToCRDTDoc( doc, changes, mockPostType );
147
+ applyPostChangesToCRDTDoc( doc, changes, defaultSyncedProperties );
54
148
 
55
149
  const title = map.get( 'title' );
56
150
  expect( title ).toBeInstanceOf( Y.Text );
@@ -63,7 +157,7 @@ describe( 'crdt', () => {
63
157
  unsyncedProperty: 'value',
64
158
  } as unknown as PostChanges;
65
159
 
66
- applyPostChangesToCRDTDoc( doc, changes, mockPostType );
160
+ applyPostChangesToCRDTDoc( doc, changes, defaultSyncedProperties );
67
161
 
68
162
  expect( map.has( 'unsyncedProperty' ) ).toBe( false );
69
163
  expect( map.get( 'title' )?.toString() ).toBe( 'New Title' );
@@ -74,7 +168,7 @@ describe( 'crdt', () => {
74
168
  title: () => 'function value',
75
169
  } as unknown as PostChanges;
76
170
 
77
- applyPostChangesToCRDTDoc( doc, changes, mockPostType );
171
+ applyPostChangesToCRDTDoc( doc, changes, defaultSyncedProperties );
78
172
 
79
173
  expect( map.has( 'title' ) ).toBe( false );
80
174
  } );
@@ -84,7 +178,7 @@ describe( 'crdt', () => {
84
178
  title: { raw: 'Raw Title', rendered: 'Rendered Title' },
85
179
  };
86
180
 
87
- applyPostChangesToCRDTDoc( doc, changes, mockPostType );
181
+ applyPostChangesToCRDTDoc( doc, changes, defaultSyncedProperties );
88
182
 
89
183
  const title = map.get( 'title' );
90
184
  expect( title ).toBeInstanceOf( Y.Text );
@@ -96,7 +190,7 @@ describe( 'crdt', () => {
96
190
  title: 'Auto Draft',
97
191
  } as PostChanges;
98
192
 
99
- applyPostChangesToCRDTDoc( doc, changes, mockPostType );
193
+ applyPostChangesToCRDTDoc( doc, changes, defaultSyncedProperties );
100
194
 
101
195
  const title = map.get( 'title' );
102
196
  expect( title ).toBeInstanceOf( Y.Text );
@@ -108,7 +202,7 @@ describe( 'crdt', () => {
108
202
  applyPostChangesToCRDTDoc(
109
203
  doc,
110
204
  { title: '' } as PostChanges,
111
- mockPostType
205
+ defaultSyncedProperties
112
206
  );
113
207
 
114
208
  const title = map.get( 'title' );
@@ -119,7 +213,7 @@ describe( 'crdt', () => {
119
213
  applyPostChangesToCRDTDoc(
120
214
  doc,
121
215
  { title: 'Auto Draft' } as PostChanges,
122
- mockPostType
216
+ defaultSyncedProperties
123
217
  );
124
218
 
125
219
  expect( map.get( 'title' )?.toString() ).toBe( '' );
@@ -134,7 +228,7 @@ describe( 'crdt', () => {
134
228
  },
135
229
  };
136
230
 
137
- applyPostChangesToCRDTDoc( doc, changes, mockPostType );
231
+ applyPostChangesToCRDTDoc( doc, changes, defaultSyncedProperties );
138
232
 
139
233
  const excerpt = map.get( 'excerpt' );
140
234
  expect( excerpt ).toBeInstanceOf( Y.Text );
@@ -146,7 +240,7 @@ describe( 'crdt', () => {
146
240
  slug: '',
147
241
  };
148
242
 
149
- applyPostChangesToCRDTDoc( doc, changes, mockPostType );
243
+ applyPostChangesToCRDTDoc( doc, changes, defaultSyncedProperties );
150
244
 
151
245
  expect( map.has( 'slug' ) ).toBe( false );
152
246
  } );
@@ -156,7 +250,7 @@ describe( 'crdt', () => {
156
250
  slug: 'my-post-slug',
157
251
  };
158
252
 
159
- applyPostChangesToCRDTDoc( doc, changes, mockPostType );
253
+ applyPostChangesToCRDTDoc( doc, changes, defaultSyncedProperties );
160
254
 
161
255
  expect( map.get( 'slug' ) ).toBe( 'my-post-slug' );
162
256
  } );
@@ -174,7 +268,7 @@ describe( 'crdt', () => {
174
268
  ],
175
269
  };
176
270
 
177
- applyPostChangesToCRDTDoc( doc, changes, mockPostType );
271
+ applyPostChangesToCRDTDoc( doc, changes, defaultSyncedProperties );
178
272
 
179
273
  expect( ( map.get( 'blocks' ) as YBlocks ).toJSON() ).toEqual(
180
274
  changes.blocks
@@ -186,7 +280,7 @@ describe( 'crdt', () => {
186
280
  blocks: [],
187
281
  };
188
282
 
189
- applyPostChangesToCRDTDoc( doc, changes, mockPostType );
283
+ applyPostChangesToCRDTDoc( doc, changes, defaultSyncedProperties );
190
284
 
191
285
  const blocks = map.get( 'blocks' );
192
286
  expect( blocks ).toBeInstanceOf( Y.Array );
@@ -200,7 +294,7 @@ describe( 'crdt', () => {
200
294
  blocks: undefined,
201
295
  };
202
296
 
203
- applyPostChangesToCRDTDoc( doc, changes, mockPostType );
297
+ applyPostChangesToCRDTDoc( doc, changes, defaultSyncedProperties );
204
298
 
205
299
  // The key should still exist, but the value should be undefined.
206
300
  expect( map.has( 'blocks' ) ).toBe( true );
@@ -212,7 +306,7 @@ describe( 'crdt', () => {
212
306
  content: 'Hello, world!',
213
307
  } as PostChanges;
214
308
 
215
- applyPostChangesToCRDTDoc( doc, changes, mockPostType );
309
+ applyPostChangesToCRDTDoc( doc, changes, defaultSyncedProperties );
216
310
 
217
311
  const content = map.get( 'content' );
218
312
  expect( content ).toBeInstanceOf( Y.Text );
@@ -227,7 +321,7 @@ describe( 'crdt', () => {
227
321
  },
228
322
  } as PostChanges;
229
323
 
230
- applyPostChangesToCRDTDoc( doc, changes, mockPostType );
324
+ applyPostChangesToCRDTDoc( doc, changes, defaultSyncedProperties );
231
325
 
232
326
  const content = map.get( 'content' );
233
327
  expect( content ).toBeInstanceOf( Y.Text );
@@ -241,7 +335,7 @@ describe( 'crdt', () => {
241
335
  applyPostChangesToCRDTDoc(
242
336
  doc,
243
337
  { title: 'Old Title' } as PostChanges,
244
- mockPostType
338
+ defaultSyncedProperties
245
339
  );
246
340
  const titleRef = map.get( 'title' );
247
341
 
@@ -249,7 +343,7 @@ describe( 'crdt', () => {
249
343
  applyPostChangesToCRDTDoc(
250
344
  doc,
251
345
  { title: 'New Title' } as PostChanges,
252
- mockPostType
346
+ defaultSyncedProperties
253
347
  );
254
348
 
255
349
  expect( map.get( 'title' ) ).toBe( titleRef );
@@ -261,7 +355,7 @@ describe( 'crdt', () => {
261
355
  applyPostChangesToCRDTDoc(
262
356
  doc,
263
357
  { content: 'Old content' } as PostChanges,
264
- mockPostType
358
+ defaultSyncedProperties
265
359
  );
266
360
  const contentRef = map.get( 'content' );
267
361
 
@@ -269,7 +363,7 @@ describe( 'crdt', () => {
269
363
  applyPostChangesToCRDTDoc(
270
364
  doc,
271
365
  { content: 'New content' } as PostChanges,
272
- mockPostType
366
+ defaultSyncedProperties
273
367
  );
274
368
 
275
369
  expect( map.get( 'content' ) ).toBe( contentRef );
@@ -281,7 +375,7 @@ describe( 'crdt', () => {
281
375
  applyPostChangesToCRDTDoc(
282
376
  doc,
283
377
  { excerpt: 'Old excerpt' } as PostChanges,
284
- mockPostType
378
+ defaultSyncedProperties
285
379
  );
286
380
  const excerptRef = map.get( 'excerpt' );
287
381
 
@@ -289,7 +383,7 @@ describe( 'crdt', () => {
289
383
  applyPostChangesToCRDTDoc(
290
384
  doc,
291
385
  { excerpt: 'New excerpt' } as PostChanges,
292
- mockPostType
386
+ defaultSyncedProperties
293
387
  );
294
388
 
295
389
  expect( map.get( 'excerpt' ) ).toBe( excerptRef );
@@ -307,7 +401,7 @@ describe( 'crdt', () => {
307
401
  metaMap.set( 'some_meta', 'old value' );
308
402
  map.set( 'meta', metaMap );
309
403
 
310
- applyPostChangesToCRDTDoc( doc, changes, mockPostType );
404
+ applyPostChangesToCRDTDoc( doc, changes, defaultSyncedProperties );
311
405
 
312
406
  expect( metaMap.get( 'some_meta' ) ).toBe( 'new value' );
313
407
  } );
@@ -323,7 +417,7 @@ describe( 'crdt', () => {
323
417
  metaMap.set( 'some_meta', 'old value' );
324
418
  map.set( 'meta', metaMap );
325
419
 
326
- applyPostChangesToCRDTDoc( doc, changes, mockPostType );
420
+ applyPostChangesToCRDTDoc( doc, changes, defaultSyncedProperties );
327
421
 
328
422
  expect( metaMap.get( 'some_meta' ) ).toStrictEqual( [
329
423
  'value',
@@ -338,23 +432,40 @@ describe( 'crdt', () => {
338
432
  },
339
433
  };
340
434
 
341
- applyPostChangesToCRDTDoc( doc, changes, mockPostType );
435
+ applyPostChangesToCRDTDoc( doc, changes, defaultSyncedProperties );
342
436
 
343
437
  const metaMap = map.get( 'meta' );
344
438
  expect( metaMap ).toBeInstanceOf( Y.Map );
345
439
  expect( metaMap?.get( 'custom_field' ) ).toBe( 'value' );
346
440
  } );
441
+
442
+ it( 'syncs taxonomy rest_base values included in syncedProperties', () => {
443
+ const changes = {
444
+ categories: [ 1, 2, 3 ],
445
+ genre: [ 10, 20 ], // should be ignored
446
+ tags: [ 4, 5 ],
447
+ } as unknown as PostChanges;
448
+
449
+ applyPostChangesToCRDTDoc( doc, changes, defaultSyncedProperties );
450
+
451
+ expect( map.get( 'categories' ) ).toEqual( [ 1, 2, 3 ] );
452
+ expect( map.get( 'genre' ) ).toBeUndefined();
453
+ expect( map.get( 'tags' ) ).toEqual( [ 4, 5 ] );
454
+
455
+ const customSyncedProperties = new Set( [
456
+ ...defaultSyncedProperties,
457
+ 'genre', // now included
458
+ ] );
459
+
460
+ applyPostChangesToCRDTDoc( doc, changes, customSyncedProperties );
461
+
462
+ expect( map.get( 'categories' ) ).toEqual( [ 1, 2, 3 ] );
463
+ expect( map.get( 'genre' ) ).toEqual( [ 10, 20 ] );
464
+ expect( map.get( 'tags' ) ).toEqual( [ 4, 5 ] );
465
+ } );
347
466
  } );
348
467
 
349
468
  describe( 'getPostChangesFromCRDTDoc', () => {
350
- const mockPostType = {
351
- slug: 'post',
352
- supports: {
353
- title: true,
354
- editor: true,
355
- },
356
- } as unknown as Type;
357
-
358
469
  let map: YMapWrap< YPostRecord >;
359
470
 
360
471
  beforeEach( () => {
@@ -373,7 +484,7 @@ describe( 'crdt', () => {
373
484
  const changes = getPostChangesFromCRDTDoc(
374
485
  doc,
375
486
  editedRecord,
376
- mockPostType
487
+ defaultSyncedProperties
377
488
  );
378
489
 
379
490
  expect( changes.title ).toBe( 'CRDT Title' );
@@ -388,7 +499,7 @@ describe( 'crdt', () => {
388
499
  const changes = getPostChangesFromCRDTDoc(
389
500
  doc,
390
501
  editedRecord,
391
- mockPostType
502
+ defaultSyncedProperties
392
503
  );
393
504
 
394
505
  expect( changes ).not.toHaveProperty( 'unsyncedProp' );
@@ -405,7 +516,7 @@ describe( 'crdt', () => {
405
516
  const changes = getPostChangesFromCRDTDoc(
406
517
  doc,
407
518
  editedRecord,
408
- mockPostType
519
+ defaultSyncedProperties
409
520
  );
410
521
 
411
522
  expect( changes ).not.toHaveProperty( 'status' );
@@ -424,7 +535,7 @@ describe( 'crdt', () => {
424
535
  const changesWithEmptyDate = getPostChangesFromCRDTDoc(
425
536
  doc,
426
537
  editedRecord,
427
- mockPostType
538
+ defaultSyncedProperties
428
539
  );
429
540
 
430
541
  expect( changesWithEmptyDate ).not.toHaveProperty( 'date' );
@@ -434,7 +545,7 @@ describe( 'crdt', () => {
434
545
  const changesWithDefinedDate = getPostChangesFromCRDTDoc(
435
546
  doc,
436
547
  editedRecord,
437
- mockPostType
548
+ defaultSyncedProperties
438
549
  );
439
550
 
440
551
  expect( changesWithDefinedDate ).not.toHaveProperty( 'date' );
@@ -453,7 +564,7 @@ describe( 'crdt', () => {
453
564
  const changesWithEmptyDate = getPostChangesFromCRDTDoc(
454
565
  doc,
455
566
  editedRecord,
456
- mockPostType
567
+ defaultSyncedProperties
457
568
  );
458
569
 
459
570
  expect( changesWithEmptyDate ).not.toHaveProperty( 'date' );
@@ -463,7 +574,7 @@ describe( 'crdt', () => {
463
574
  const changesWithDefinedDate = getPostChangesFromCRDTDoc(
464
575
  doc,
465
576
  editedRecord,
466
- mockPostType
577
+ defaultSyncedProperties
467
578
  );
468
579
 
469
580
  expect( changesWithDefinedDate ).not.toHaveProperty( 'date' );
@@ -479,12 +590,74 @@ describe( 'crdt', () => {
479
590
  const changes = getPostChangesFromCRDTDoc(
480
591
  doc,
481
592
  editedRecord,
482
- mockPostType
593
+ defaultSyncedProperties
483
594
  );
484
595
 
485
596
  expect( changes ).toHaveProperty( 'blocks' );
486
597
  } );
487
598
 
599
+ it( 'returns rich-text block attributes as RichTextData, not strings', () => {
600
+ // Simulate User A writing a paragraph block into the CRDT doc.
601
+ addBlockToDoc( map, 'block-1', 'Hello world' );
602
+
603
+ // Simulate User B reading the CRDT doc with no local blocks.
604
+ const editedRecord = { blocks: [] } as unknown as Post;
605
+
606
+ const changes = getPostChangesFromCRDTDoc(
607
+ doc,
608
+ editedRecord,
609
+ defaultSyncedProperties
610
+ );
611
+
612
+ const block = ( changes.blocks as any[] )?.[ 0 ];
613
+ expect( block ).toBeDefined();
614
+ expect( block.attributes.content ).toBeInstanceOf( RichTextData );
615
+ expect( block.attributes.content.text ).toBe( 'Hello world' );
616
+ } );
617
+
618
+ it( 'returns nested rich-text in array attributes as RichTextData', () => {
619
+ // Add a table block to the CRDT doc with nested cell content
620
+ // stored as plain strings.
621
+ let blocks = map.get( 'blocks' );
622
+ if ( ! ( blocks instanceof Y.Array ) ) {
623
+ blocks = new Y.Array< YBlock >();
624
+ map.set( 'blocks', blocks );
625
+ }
626
+
627
+ const tableBlock = createYMap< YBlockRecord >();
628
+ tableBlock.set( 'name', 'core/table' );
629
+ tableBlock.set( 'clientId', 'table-1' );
630
+ const attrs = new Y.Map();
631
+ attrs.set( 'body', [
632
+ {
633
+ cells: [
634
+ { content: '<strong>Cell</strong>', tag: 'td' },
635
+ { content: 'Plain', tag: 'td' },
636
+ ],
637
+ },
638
+ ] );
639
+ tableBlock.set( 'attributes', attrs );
640
+ tableBlock.set( 'innerBlocks', new Y.Array() );
641
+ ( blocks as YBlocks ).push( [ tableBlock ] );
642
+
643
+ const editedRecord = { blocks: [] } as unknown as Post;
644
+
645
+ const changes = getPostChangesFromCRDTDoc(
646
+ doc,
647
+ editedRecord,
648
+ defaultSyncedProperties
649
+ );
650
+
651
+ const block = ( changes.blocks as any[] )?.[ 0 ];
652
+ expect( block ).toBeDefined();
653
+
654
+ const cell = block.attributes.body[ 0 ].cells[ 0 ];
655
+ expect( cell.content ).toBeInstanceOf( RichTextData );
656
+ expect( ( cell.content as RichTextData ).toHTMLString() ).toBe(
657
+ '<strong>Cell</strong>'
658
+ );
659
+ } );
660
+
488
661
  it( 'includes undefined blocks in changes', () => {
489
662
  map.set( 'blocks', undefined );
490
663
 
@@ -501,7 +674,7 @@ describe( 'crdt', () => {
501
674
  const changes = getPostChangesFromCRDTDoc(
502
675
  doc,
503
676
  editedRecord,
504
- mockPostType
677
+ defaultSyncedProperties
505
678
  );
506
679
 
507
680
  expect( changes ).toHaveProperty( 'blocks' );
@@ -518,7 +691,7 @@ describe( 'crdt', () => {
518
691
  const changes = getPostChangesFromCRDTDoc(
519
692
  doc,
520
693
  editedRecord,
521
- mockPostType
694
+ defaultSyncedProperties
522
695
  );
523
696
 
524
697
  expect( changes.content ).toBe( 'New content' );
@@ -534,7 +707,7 @@ describe( 'crdt', () => {
534
707
  const changes = getPostChangesFromCRDTDoc(
535
708
  doc,
536
709
  editedRecord,
537
- mockPostType
710
+ defaultSyncedProperties
538
711
  );
539
712
 
540
713
  expect( changes.content ).toBe( 'New content' );
@@ -550,7 +723,7 @@ describe( 'crdt', () => {
550
723
  const changes = getPostChangesFromCRDTDoc(
551
724
  doc,
552
725
  editedRecord,
553
- mockPostType
726
+ defaultSyncedProperties
554
727
  );
555
728
 
556
729
  expect( changes ).not.toHaveProperty( 'content' );
@@ -570,7 +743,7 @@ describe( 'crdt', () => {
570
743
  const changes = getPostChangesFromCRDTDoc(
571
744
  doc,
572
745
  editedRecord,
573
- mockPostType
746
+ defaultSyncedProperties
574
747
  );
575
748
 
576
749
  expect( changes.meta ).toEqual( {
@@ -592,7 +765,7 @@ describe( 'crdt', () => {
592
765
  const changes = getPostChangesFromCRDTDoc(
593
766
  doc,
594
767
  editedRecord,
595
- mockPostType
768
+ defaultSyncedProperties
596
769
  );
597
770
 
598
771
  expect( changes.meta ).toEqual( {
@@ -600,6 +773,30 @@ describe( 'crdt', () => {
600
773
  } );
601
774
  } );
602
775
 
776
+ it( 'excludes orphaned meta keys not present on the edited record', () => {
777
+ // If post meta is registered, saved (landing in a CRDT doc),
778
+ // then unregistered, it can permanently mark the record dirty.
779
+ // Orphaned values should not show up as a change.
780
+ const metaMap = createYMap();
781
+ metaMap.set( 'registered_meta', 'value' );
782
+ metaMap.set( 'orphaned_meta', 'stale value' );
783
+ map.set( 'meta', metaMap );
784
+
785
+ const editedRecord = {
786
+ meta: {
787
+ registered_meta: 'value',
788
+ },
789
+ } as unknown as Post;
790
+
791
+ const changes = getPostChangesFromCRDTDoc(
792
+ doc,
793
+ editedRecord,
794
+ defaultSyncedProperties
795
+ );
796
+
797
+ expect( changes ).not.toHaveProperty( 'meta' );
798
+ } );
799
+
603
800
  it( 'excludes disallowed meta keys in changes', () => {
604
801
  const metaMap = createYMap();
605
802
  metaMap.set( 'public_meta', 'new value' );
@@ -615,7 +812,7 @@ describe( 'crdt', () => {
615
812
  const changes = getPostChangesFromCRDTDoc(
616
813
  doc,
617
814
  editedRecord,
618
- mockPostType
815
+ defaultSyncedProperties
619
816
  );
620
817
 
621
818
  expect( changes.meta ).toEqual( {
@@ -626,6 +823,43 @@ describe( 'crdt', () => {
626
823
  );
627
824
  } );
628
825
 
826
+ it( 'returns taxonomy rest_base changes when in syncedProperties', () => {
827
+ map.set( 'categories', [ 1, 2 ] );
828
+ map.set( 'genre', [ 10, 20 ] );
829
+ map.set( 'tags', [ 3 ] );
830
+
831
+ const editedRecord = {
832
+ categories: [ 1 ],
833
+ genre: [ 10 ], // should be ignored
834
+ tags: [],
835
+ } as unknown as Post;
836
+
837
+ const changes = getPostChangesFromCRDTDoc(
838
+ doc,
839
+ editedRecord,
840
+ defaultSyncedProperties
841
+ ) as Record< string, unknown >;
842
+
843
+ expect( changes.categories ).toEqual( [ 1, 2 ] );
844
+ expect( changes.genre ).toBeUndefined();
845
+ expect( changes.tags ).toEqual( [ 3 ] );
846
+
847
+ const customSyncedProperties = new Set( [
848
+ ...defaultSyncedProperties,
849
+ 'genre', // now included
850
+ ] );
851
+
852
+ const changes2 = getPostChangesFromCRDTDoc(
853
+ doc,
854
+ editedRecord,
855
+ customSyncedProperties
856
+ ) as Record< string, unknown >;
857
+
858
+ expect( changes2.categories ).toEqual( [ 1, 2 ] );
859
+ expect( changes2.genre ).toEqual( [ 10, 20 ] );
860
+ expect( changes2.tags ).toEqual( [ 3 ] );
861
+ } );
862
+
629
863
  describe( 'selection recalculation', () => {
630
864
  it( 'includes recalculated selection when text is inserted before cursor', () => {
631
865
  const ytext = addBlockToDoc( map, 'block-1', 'Hello world' );
@@ -656,7 +890,7 @@ describe( 'crdt', () => {
656
890
  const changes = getPostChangesFromCRDTDoc(
657
891
  doc,
658
892
  editedRecord,
659
- mockPostType
893
+ defaultSyncedProperties
660
894
  );
661
895
 
662
896
  expect( changes.selection ).toBeDefined();
@@ -699,7 +933,7 @@ describe( 'crdt', () => {
699
933
  const changes = getPostChangesFromCRDTDoc(
700
934
  doc,
701
935
  editedRecord,
702
- mockPostType
936
+ defaultSyncedProperties
703
937
  );
704
938
 
705
939
  expect( changes.selection ).toBeDefined();
@@ -718,7 +952,7 @@ describe( 'crdt', () => {
718
952
  const changes = getPostChangesFromCRDTDoc(
719
953
  doc,
720
954
  editedRecord,
721
- mockPostType
955
+ defaultSyncedProperties
722
956
  );
723
957
 
724
958
  expect( changes.selection ).toBeUndefined();
@@ -734,11 +968,13 @@ describe( 'crdt', () => {
734
968
  * @param map
735
969
  * @param clientId Block client ID.
736
970
  * @param content Initial text content.
971
+ * @param name Block name (default: 'core/paragraph').
737
972
  */
738
973
  function addBlockToDoc(
739
974
  map: YMapWrap< YPostRecord >,
740
975
  clientId: string,
741
- content: string
976
+ content: string,
977
+ name = 'core/paragraph'
742
978
  ): Y.Text {
743
979
  let blocks = map.get( 'blocks' );
744
980
  if ( ! ( blocks instanceof Y.Array ) ) {
@@ -747,6 +983,7 @@ function addBlockToDoc(
747
983
  }
748
984
 
749
985
  const block = createYMap< YBlockRecord >();
986
+ block.set( 'name', name );
750
987
  block.set( 'clientId', clientId );
751
988
  const attrs = new Y.Map();
752
989
  const ytext = new Y.Text( content );