@wordpress/core-data 7.43.0 → 7.44.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 (101) hide show
  1. package/CHANGELOG.md +7 -1
  2. package/build/entities.cjs +28 -20
  3. package/build/entities.cjs.map +2 -2
  4. package/build/hooks/index.cjs +3 -3
  5. package/build/hooks/index.cjs.map +1 -1
  6. package/build/hooks/use-entity-prop.cjs +1 -1
  7. package/build/hooks/use-entity-prop.cjs.map +2 -2
  8. package/build/hooks/use-entity-record.cjs +4 -4
  9. package/build/hooks/use-entity-record.cjs.map +2 -2
  10. package/build/hooks/use-entity-records.cjs +3 -3
  11. package/build/hooks/use-entity-records.cjs.map +2 -2
  12. package/build/hooks/use-query-select.cjs +2 -2
  13. package/build/hooks/use-query-select.cjs.map +2 -2
  14. package/build/hooks/use-resource-permissions.cjs +4 -4
  15. package/build/hooks/use-resource-permissions.cjs.map +2 -2
  16. package/build/queried-data/reducer.cjs +2 -1
  17. package/build/queried-data/reducer.cjs.map +2 -2
  18. package/build/queried-data/selectors.cjs +21 -17
  19. package/build/queried-data/selectors.cjs.map +2 -2
  20. package/build/resolvers.cjs +6 -6
  21. package/build/resolvers.cjs.map +2 -2
  22. package/build/selectors.cjs +4 -2
  23. package/build/selectors.cjs.map +2 -2
  24. package/build/utils/crdt-blocks.cjs +151 -31
  25. package/build/utils/crdt-blocks.cjs.map +2 -2
  26. package/build/utils/crdt-selection.cjs.map +2 -2
  27. package/build/utils/crdt.cjs +8 -0
  28. package/build/utils/crdt.cjs.map +2 -2
  29. package/build-module/entities.mjs +29 -20
  30. package/build-module/entities.mjs.map +2 -2
  31. package/build-module/hooks/index.mjs +6 -6
  32. package/build-module/hooks/index.mjs.map +2 -2
  33. package/build-module/hooks/use-entity-prop.mjs +1 -1
  34. package/build-module/hooks/use-entity-prop.mjs.map +2 -2
  35. package/build-module/hooks/use-entity-record.mjs +3 -3
  36. package/build-module/hooks/use-entity-record.mjs.map +2 -2
  37. package/build-module/hooks/use-entity-records.mjs +2 -2
  38. package/build-module/hooks/use-entity-records.mjs.map +2 -2
  39. package/build-module/hooks/use-query-select.mjs +1 -1
  40. package/build-module/hooks/use-query-select.mjs.map +1 -1
  41. package/build-module/hooks/use-resource-permissions.mjs +3 -3
  42. package/build-module/hooks/use-resource-permissions.mjs.map +2 -2
  43. package/build-module/queried-data/reducer.mjs +2 -1
  44. package/build-module/queried-data/reducer.mjs.map +2 -2
  45. package/build-module/queried-data/selectors.mjs +21 -17
  46. package/build-module/queried-data/selectors.mjs.map +2 -2
  47. package/build-module/resolvers.mjs +6 -6
  48. package/build-module/resolvers.mjs.map +2 -2
  49. package/build-module/selectors.mjs +4 -2
  50. package/build-module/selectors.mjs.map +2 -2
  51. package/build-module/utils/crdt-blocks.mjs +151 -31
  52. package/build-module/utils/crdt-blocks.mjs.map +2 -2
  53. package/build-module/utils/crdt-selection.mjs.map +2 -2
  54. package/build-module/utils/crdt.mjs +7 -0
  55. package/build-module/utils/crdt.mjs.map +2 -2
  56. package/build-types/entities.d.ts +51 -32
  57. package/build-types/entities.d.ts.map +1 -1
  58. package/build-types/hooks/index.d.ts +3 -3
  59. package/build-types/hooks/index.d.ts.map +1 -1
  60. package/build-types/hooks/use-entity-record.d.ts +1 -1
  61. package/build-types/hooks/use-entity-record.d.ts.map +1 -1
  62. package/build-types/hooks/use-entity-records.d.ts +1 -1
  63. package/build-types/hooks/use-entity-records.d.ts.map +1 -1
  64. package/build-types/hooks/use-resource-permissions.d.ts +1 -1
  65. package/build-types/hooks/use-resource-permissions.d.ts.map +1 -1
  66. package/build-types/index.d.ts.map +1 -1
  67. package/build-types/queried-data/reducer.d.ts.map +1 -1
  68. package/build-types/queried-data/selectors.d.ts +5 -3
  69. package/build-types/queried-data/selectors.d.ts.map +1 -1
  70. package/build-types/selectors.d.ts.map +1 -1
  71. package/build-types/utils/crdt-blocks.d.ts.map +1 -1
  72. package/build-types/utils/crdt-selection.d.ts.map +1 -1
  73. package/build-types/utils/crdt.d.ts +7 -0
  74. package/build-types/utils/crdt.d.ts.map +1 -1
  75. package/package.json +18 -18
  76. package/src/entities.js +16 -8
  77. package/src/hooks/index.ts +3 -3
  78. package/src/hooks/test/use-entity-records.js +1 -1
  79. package/src/hooks/use-entity-prop.js +1 -1
  80. package/src/hooks/use-entity-record.ts +1 -1
  81. package/src/hooks/use-entity-records.ts +1 -1
  82. package/src/hooks/use-query-select.ts +1 -1
  83. package/src/hooks/use-resource-permissions.ts +1 -1
  84. package/src/queried-data/reducer.js +3 -1
  85. package/src/queried-data/selectors.js +32 -26
  86. package/src/queried-data/test/reducer.js +7 -0
  87. package/src/queried-data/test/selectors.js +30 -21
  88. package/src/resolvers.js +6 -6
  89. package/src/selectors.ts +8 -2
  90. package/src/utils/crdt-blocks.ts +336 -62
  91. package/src/utils/crdt-selection.ts +0 -1
  92. package/src/utils/crdt.ts +15 -1
  93. package/src/utils/test/crdt-blocks.ts +1328 -6
  94. package/src/utils/test/crdt.ts +39 -1
  95. package/build/hooks/memoize.cjs +0 -38
  96. package/build/hooks/memoize.cjs.map +0 -7
  97. package/build-module/hooks/memoize.mjs +0 -7
  98. package/build-module/hooks/memoize.mjs.map +0 -7
  99. package/build-types/hooks/memoize.d.ts +0 -3
  100. package/build-types/hooks/memoize.d.ts.map +0 -1
  101. package/src/hooks/memoize.js +0 -7
@@ -7,7 +7,6 @@ import fastDeepEqual from 'fast-deep-equal/es6/index.js';
7
7
  /**
8
8
  * WordPress dependencies
9
9
  */
10
- // @ts-expect-error No exported types.
11
10
  import { getBlockTypes } from '@wordpress/blocks';
12
11
  import { RichTextData } from '@wordpress/rich-text';
13
12
  import { Y } from '@wordpress/sync';
@@ -23,14 +22,14 @@ interface BlockAttributes {
23
22
  [ key: string ]: unknown;
24
23
  }
25
24
 
26
- interface BlockAttributeType {
25
+ interface BlockAttributeSchema {
27
26
  role?: string;
28
27
  type?: string;
29
- query?: Record< string, BlockAttributeType >;
28
+ query?: Record< string, BlockAttributeSchema >;
30
29
  }
31
30
 
32
31
  interface BlockType {
33
- attributes?: Record< string, BlockAttributeType >;
32
+ attributes?: Record< string, BlockAttributeSchema >;
34
33
  name: string;
35
34
  }
36
35
 
@@ -134,7 +133,7 @@ function makeBlocksSerializable( blocks: Block[] ): Block[] {
134
133
  * @return The value with rich-text strings replaced by RichTextData.
135
134
  */
136
135
  function deserializeAttributeValue(
137
- schema: BlockAttributeType | undefined,
136
+ schema: BlockAttributeSchema | undefined,
138
137
  value: unknown
139
138
  ): unknown {
140
139
  if ( schema?.type === 'rich-text' && typeof value === 'string' ) {
@@ -184,7 +183,7 @@ export function deserializeBlockAttributes( blocks: Block[] ): Block[] {
184
183
  const newAttributes = { ...attributes };
185
184
 
186
185
  for ( const [ key, value ] of Object.entries( attributes ) ) {
187
- const schema = getBlockAttributeType( name, key );
186
+ const schema = getBlockAttributeSchema( name, key );
188
187
 
189
188
  if ( schema ) {
190
189
  newAttributes[ key ] = deserializeAttributeValue(
@@ -255,14 +254,89 @@ function createNewYAttributeValue(
255
254
  blockName: string,
256
255
  attributeName: string,
257
256
  attributeValue: unknown
258
- ): Y.Text | unknown {
259
- const isRichText = isRichTextAttribute( blockName, attributeName );
257
+ ): Y.Text | Y.Array< unknown > | Y.Map< unknown > | unknown {
258
+ const schema = getBlockAttributeSchema( blockName, attributeName );
259
+ return createYValueFromSchema( schema, attributeValue );
260
+ }
261
+
262
+ /**
263
+ * Recursively create the appropriate Y.js type for a value based on its
264
+ * block-attribute schema.
265
+ *
266
+ * - `rich-text` -> Y.Text
267
+ * - `array` with query -> Y.Array of Y.Maps
268
+ * - `object` with query -> Y.Map
269
+ * - anything else -> plain value (unchanged)
270
+ *
271
+ * @param schema The attribute type definition.
272
+ * @param value The plain JS value to convert.
273
+ * @return A Y.js type or the original value.
274
+ */
275
+ function createYValueFromSchema(
276
+ schema: BlockAttributeSchema | undefined,
277
+ value: unknown
278
+ ): Y.Text | Y.Array< unknown > | Y.Map< unknown > | unknown {
279
+ if ( ! schema ) {
280
+ return value;
281
+ }
282
+
283
+ if ( schema.type === 'rich-text' ) {
284
+ return new Y.Text( value?.toString() ?? '' );
285
+ }
286
+
287
+ if ( schema.type === 'array' && schema.query && Array.isArray( value ) ) {
288
+ const query = schema.query;
289
+ const yArray = new Y.Array< Y.Map< unknown > >();
290
+
291
+ yArray.insert(
292
+ 0,
293
+ value.map( ( item ) => createYMapFromQuery( query, item ) )
294
+ );
295
+
296
+ return yArray;
297
+ }
298
+
299
+ if ( schema.type === 'object' && schema.query && isRecord( value ) ) {
300
+ return createYMapFromQuery( schema.query, value );
301
+ }
302
+
303
+ return value;
304
+ }
260
305
 
261
- if ( isRichText ) {
262
- return new Y.Text( attributeValue?.toString() ?? '' );
306
+ /**
307
+ * Type guard that narrows `unknown` to `Record< string, unknown >`.
308
+ *
309
+ * @param value Value to check.
310
+ * @return True if `value` is a non-null, non-array object.
311
+ */
312
+ function isRecord( value: unknown ): value is Record< string, unknown > {
313
+ return !! value && typeof value === 'object' && ! Array.isArray( value );
314
+ }
315
+
316
+ /**
317
+ * Create a Y.Map from a plain object, using a query schema to decide which
318
+ * properties should become nested Y.js types (Y.Text, Y.Array, Y.Map).
319
+ *
320
+ * @param query The query schema defining the properties.
321
+ * @param obj The plain object to convert.
322
+ * @return A Y.Map with typed values.
323
+ */
324
+ function createYMapFromQuery(
325
+ query: Record< string, BlockAttributeSchema >,
326
+ obj: unknown
327
+ ): Y.Map< unknown > {
328
+ if ( ! isRecord( obj ) ) {
329
+ return new Y.Map();
263
330
  }
264
331
 
265
- return attributeValue;
332
+ const entries: [ string, unknown ][] = Object.entries( obj ).map(
333
+ ( [ key, val ] ): [ string, unknown ] => {
334
+ const subSchema = query[ key ];
335
+ return [ key, createYValueFromSchema( subSchema, val ) ];
336
+ }
337
+ );
338
+
339
+ return new Y.Map( entries );
266
340
  }
267
341
 
268
342
  function createNewYBlock( block: Block ): YBlock {
@@ -383,6 +457,7 @@ export function mergeCrdtBlocks(
383
457
  for ( let i = 0; i < numOfUpdatesNeeded; i++, left++ ) {
384
458
  const block = blocksToSync[ left ];
385
459
  const yblock = yblocks.get( left );
460
+
386
461
  Object.entries( block ).forEach( ( [ key, value ] ) => {
387
462
  switch ( key ) {
388
463
  case 'attributes': {
@@ -408,8 +483,16 @@ export function mergeCrdtBlocks(
408
483
  currentAttribute
409
484
  );
410
485
 
486
+ // Y types (Y.Text, Y.Array, Y.Map) cannot be
487
+ // compared with fastDeepEqual against plain values.
488
+ // Delegate to mergeYValue which handles no-op
489
+ // detection at the edges.
490
+ const isYType =
491
+ currentAttribute instanceof Y.AbstractType;
492
+
411
493
  const isAttributeChanged =
412
494
  ! isExpectedType ||
495
+ isYType ||
413
496
  ! fastDeepEqual(
414
497
  currentAttribute,
415
498
  attributeValue
@@ -499,11 +582,218 @@ export function mergeCrdtBlocks(
499
582
  }
500
583
 
501
584
  /**
502
- * Update a single attribute on a Yjs block attributes map (currentAttributes).
585
+ * Compare a plain array element against a Y.Map element for equality.
586
+ * Used by the left-right sweep diff in mergeYArray.
587
+ *
588
+ * @param newElement The plain object from the incoming array.
589
+ * @param yElement The Y.Map element from the existing Y.Array.
590
+ * @return True if the elements are deeply equal.
591
+ */
592
+ function areArrayElementsEqual(
593
+ newElement: unknown,
594
+ yElement: unknown
595
+ ): boolean {
596
+ if ( yElement instanceof Y.Map && isRecord( newElement ) ) {
597
+ return fastDeepEqual( newElement, yElement.toJSON() );
598
+ }
599
+
600
+ return fastDeepEqual( newElement, yElement );
601
+ }
602
+
603
+ /**
604
+ * Merge an incoming plain array into an existing Y.Array in-place.
605
+ *
606
+ * Uses the same left-right sweep diff approach as mergeCrdtBlocks:
607
+ * equal elements are skipped from both ends, then the middle section
608
+ * is updated, deleted, or inserted as needed. This preserves existing
609
+ * Y.Map/Y.Text objects for unchanged elements, so concurrent edits
610
+ * to those elements are not lost.
611
+ *
612
+ * @param yArray The existing Y.Array to update.
613
+ * @param newValue The new plain array to merge into the Y.Array.
614
+ * @param schema The attribute schema (must have `query`).
615
+ * @param cursorPosition The local cursor position for rich-text delta merges.
616
+ */
617
+ function mergeYArray(
618
+ yArray: Y.Array< unknown >,
619
+ newValue: unknown[],
620
+ schema: BlockAttributeSchema,
621
+ cursorPosition: number | null
622
+ ): void {
623
+ if ( ! schema.query ) {
624
+ return;
625
+ }
626
+
627
+ const query = schema.query;
628
+ const numOfCommonEntries = Math.min( newValue.length, yArray.length );
629
+
630
+ let left = 0;
631
+ let right = 0;
632
+
633
+ // Skip equal elements from left.
634
+ for (
635
+ ;
636
+ left < numOfCommonEntries &&
637
+ areArrayElementsEqual( newValue[ left ], yArray.get( left ) );
638
+ left++
639
+ ) {
640
+ /* nop */
641
+ }
642
+
643
+ // Skip equal elements from right.
644
+ for (
645
+ ;
646
+ right < numOfCommonEntries - left &&
647
+ areArrayElementsEqual(
648
+ newValue[ newValue.length - right - 1 ],
649
+ yArray.get( yArray.length - right - 1 )
650
+ );
651
+ right++
652
+ ) {
653
+ /* nop */
654
+ }
655
+
656
+ // Updates: merge changed elements in-place.
657
+ const numOfUpdatesNeeded = numOfCommonEntries - left - right;
658
+
659
+ for ( let i = 0; i < numOfUpdatesNeeded; i++ ) {
660
+ const currentElement = yArray.get( left + i );
661
+ const newElement = newValue[ left + i ];
662
+
663
+ if ( currentElement instanceof Y.Map && isRecord( newElement ) ) {
664
+ mergeYMapValues(
665
+ currentElement,
666
+ newElement,
667
+ query,
668
+ cursorPosition
669
+ );
670
+ } else {
671
+ // Element is the wrong type (e.g. partial migration) or the
672
+ // incoming value is not an object. Rebuild the entire array.
673
+ yArray.delete( 0, yArray.length );
674
+ yArray.insert(
675
+ 0,
676
+ newValue.map( ( item ) => createYMapFromQuery( query, item ) )
677
+ );
678
+ return;
679
+ }
680
+ }
681
+
682
+ // Deletes.
683
+ const numOfDeletionsNeeded = Math.max( 0, yArray.length - newValue.length );
684
+
685
+ if ( numOfDeletionsNeeded > 0 ) {
686
+ yArray.delete( left + numOfUpdatesNeeded, numOfDeletionsNeeded );
687
+ }
688
+
689
+ // Inserts.
690
+ const numOfInsertionsNeeded = Math.max(
691
+ 0,
692
+ newValue.length - yArray.length
693
+ );
694
+
695
+ if ( numOfInsertionsNeeded > 0 ) {
696
+ const insertAt = left + numOfUpdatesNeeded;
697
+ const itemsToInsert: Y.Map< unknown >[] = new Array(
698
+ numOfInsertionsNeeded
699
+ );
700
+
701
+ for ( let i = 0; i < numOfInsertionsNeeded; i++ ) {
702
+ itemsToInsert[ i ] = createYMapFromQuery(
703
+ query,
704
+ newValue[ insertAt + i ]
705
+ );
706
+ }
707
+
708
+ yArray.insert( insertAt, itemsToInsert );
709
+ }
710
+ }
711
+
712
+ /**
713
+ * Merge a single value into a Y.Map entry, using the attribute schema to
714
+ * decide how to merge.
503
715
  *
504
- * For rich-text attributes that already exist as Y.Text instances, the update
505
- * is applied as a delta merge so that concurrent edits are preserved. All
506
- * other attributes are replaced wholesale via `createNewYAttributeValue`.
716
+ * If the current value is already a matching Y.js type (Y.Text, Y.Array,
717
+ * Y.Map), the update is merged in-place so concurrent edits are preserved.
718
+ * Otherwise the value is replaced wholesale.
719
+ *
720
+ * @param schema The attribute type definition for this value.
721
+ * @param newVal The new value to merge into the Y.Map entry.
722
+ * @param yMap The Y.Map that owns this entry.
723
+ * @param key The key of this entry in the Y.Map.
724
+ * @param cursorPosition The local cursor position for rich-text delta merges.
725
+ */
726
+ function mergeYValue(
727
+ schema: BlockAttributeSchema | undefined,
728
+ newVal: unknown,
729
+ yMap: Y.Map< unknown >,
730
+ key: string,
731
+ cursorPosition: number | null
732
+ ): void {
733
+ const currentVal = yMap.get( key );
734
+ if (
735
+ schema?.type === 'rich-text' &&
736
+ typeof newVal === 'string' &&
737
+ currentVal instanceof Y.Text
738
+ ) {
739
+ mergeRichTextUpdate( currentVal, newVal, cursorPosition );
740
+ } else if (
741
+ schema?.type === 'array' &&
742
+ schema.query &&
743
+ Array.isArray( newVal ) &&
744
+ currentVal instanceof Y.Array
745
+ ) {
746
+ mergeYArray( currentVal, newVal, schema, cursorPosition );
747
+ } else if (
748
+ schema?.type === 'object' &&
749
+ schema.query &&
750
+ isRecord( newVal ) &&
751
+ currentVal instanceof Y.Map
752
+ ) {
753
+ mergeYMapValues( currentVal, newVal, schema.query, cursorPosition );
754
+ } else {
755
+ const newYValue = createYValueFromSchema( schema, newVal );
756
+
757
+ // If createYValueFromSchema wrapped the value into a Y type, the
758
+ // current value is the wrong type and needs upgrading. Otherwise,
759
+ // only replace if the raw value actually changed.
760
+ if ( newYValue !== newVal || ! fastDeepEqual( currentVal, newVal ) ) {
761
+ yMap.set( key, newYValue );
762
+ }
763
+ }
764
+ }
765
+
766
+ /**
767
+ * Merge an incoming plain object into an existing Y.Map in-place, using
768
+ * the query schema to decide how each property should be merged.
769
+ *
770
+ * Properties present in the Y.Map but absent from `newObj` are deleted.
771
+ *
772
+ * @param yMap The existing Y.Map to update.
773
+ * @param newObj The new plain object to merge into the Y.Map.
774
+ * @param query The query schema defining property types.
775
+ * @param cursorPosition The local cursor position for rich-text delta merges.
776
+ */
777
+ function mergeYMapValues(
778
+ yMap: Y.Map< unknown >,
779
+ newObj: Record< string, unknown >,
780
+ query: Record< string, BlockAttributeSchema >,
781
+ cursorPosition: number | null
782
+ ): void {
783
+ for ( const [ key, newVal ] of Object.entries( newObj ) ) {
784
+ mergeYValue( query[ key ], newVal, yMap, key, cursorPosition );
785
+ }
786
+
787
+ // Delete properties absent from the incoming object.
788
+ for ( const key of yMap.keys() ) {
789
+ if ( ! Object.hasOwn( newObj, key ) ) {
790
+ yMap.delete( key );
791
+ }
792
+ }
793
+ }
794
+
795
+ /**
796
+ * Update a single attribute on a Yjs block attributes map (currentAttributes).
507
797
  *
508
798
  * @param blockName The block type name, e.g. 'core/paragraph'.
509
799
  * @param attributeName The name of the attribute to update, e.g. 'content'.
@@ -518,28 +808,22 @@ function updateYBlockAttribute(
518
808
  currentAttributes: YBlockAttributes,
519
809
  cursorPosition: number | null
520
810
  ): void {
521
- const isRichText = isRichTextAttribute( blockName, attributeName );
522
- const currentAttribute = currentAttributes.get( attributeName );
811
+ const schema = getBlockAttributeSchema( blockName, attributeName );
523
812
 
524
- if (
525
- isRichText &&
526
- 'string' === typeof attributeValue &&
527
- currentAttributes.has( attributeName ) &&
528
- currentAttribute instanceof Y.Text
529
- ) {
530
- // Rich text values are stored as persistent Y.Text instances.
531
- // Update the value with a delta in place.
532
- mergeRichTextUpdate( currentAttribute, attributeValue, cursorPosition );
533
- } else {
534
- currentAttributes.set(
535
- attributeName,
536
- createNewYAttributeValue( blockName, attributeName, attributeValue )
537
- );
538
- }
813
+ mergeYValue(
814
+ schema,
815
+ attributeValue,
816
+ currentAttributes,
817
+ attributeName,
818
+ cursorPosition
819
+ );
539
820
  }
540
821
 
541
822
  // Cached block attribute types, populated once from getBlockTypes().
542
- let cachedBlockAttributeTypes: Map< string, Map< string, BlockAttributeType > >;
823
+ let cachedBlockAttributeSchemas: Map<
824
+ string,
825
+ Map< string, BlockAttributeSchema >
826
+ >;
543
827
 
544
828
  /**
545
829
  * Get the attribute type definition for a block attribute.
@@ -548,18 +832,18 @@ let cachedBlockAttributeTypes: Map< string, Map< string, BlockAttributeType > >;
548
832
  * @param attributeName The name of the attribute, e.g. 'content'.
549
833
  * @return The type definition of the attribute.
550
834
  */
551
- function getBlockAttributeType(
835
+ function getBlockAttributeSchema(
552
836
  blockName: string,
553
837
  attributeName: string
554
- ): BlockAttributeType | undefined {
555
- if ( ! cachedBlockAttributeTypes ) {
838
+ ): BlockAttributeSchema | undefined {
839
+ if ( ! cachedBlockAttributeSchemas ) {
556
840
  // Parse the attributes for all blocks once.
557
- cachedBlockAttributeTypes = new Map();
841
+ cachedBlockAttributeSchemas = new Map();
558
842
 
559
843
  for ( const blockType of getBlockTypes() as BlockType[] ) {
560
- cachedBlockAttributeTypes.set(
844
+ cachedBlockAttributeSchemas.set(
561
845
  blockType.name,
562
- new Map< string, BlockAttributeType >(
846
+ new Map< string, BlockAttributeSchema >(
563
847
  Object.entries( blockType.attributes ?? {} ).map(
564
848
  ( [ name, definition ] ) => {
565
849
  const { role, type, query } = definition;
@@ -571,7 +855,7 @@ function getBlockAttributeType(
571
855
  }
572
856
  }
573
857
 
574
- return cachedBlockAttributeTypes.get( blockName )?.get( attributeName );
858
+ return cachedBlockAttributeSchemas.get( blockName )?.get( attributeName );
575
859
  }
576
860
 
577
861
  /**
@@ -587,20 +871,24 @@ function isExpectedAttributeType(
587
871
  attributeName: string,
588
872
  attributeValue: unknown
589
873
  ): boolean {
590
- const expectedAttributeType = getBlockAttributeType(
591
- blockName,
592
- attributeName
593
- )?.type;
874
+ const schema = getBlockAttributeSchema( blockName, attributeName );
594
875
 
595
- if ( expectedAttributeType === 'rich-text' ) {
876
+ if ( schema?.type === 'rich-text' ) {
596
877
  return attributeValue instanceof Y.Text;
597
878
  }
598
879
 
599
- if ( expectedAttributeType === 'string' ) {
880
+ if ( schema?.type === 'string' ) {
600
881
  return typeof attributeValue === 'string';
601
882
  }
602
883
 
603
- // No other types comparisons use special logic.
884
+ if ( schema?.type === 'array' && schema.query ) {
885
+ return attributeValue instanceof Y.Array;
886
+ }
887
+
888
+ if ( schema?.type === 'object' && schema.query ) {
889
+ return attributeValue instanceof Y.Map;
890
+ }
891
+
604
892
  return true;
605
893
  }
606
894
 
@@ -613,22 +901,8 @@ function isExpectedAttributeType(
613
901
  * @return True if the attribute is local, false otherwise.
614
902
  */
615
903
  function isLocalAttribute( blockName: string, attributeName: string ): boolean {
616
- return 'local' === getBlockAttributeType( blockName, attributeName )?.role;
617
- }
618
-
619
- /**
620
- * Given a block name and attribute key, return true if the attribute is rich-text typed.
621
- *
622
- * @param blockName The name of the block, e.g. 'core/paragraph'.
623
- * @param attributeName The name of the attribute to check, e.g. 'content'.
624
- * @return True if the attribute is rich-text typed, false otherwise.
625
- */
626
- function isRichTextAttribute(
627
- blockName: string,
628
- attributeName: string
629
- ): boolean {
630
904
  return (
631
- 'rich-text' === getBlockAttributeType( blockName, attributeName )?.type
905
+ 'local' === getBlockAttributeSchema( blockName, attributeName )?.role
632
906
  );
633
907
  }
634
908
 
@@ -4,7 +4,6 @@
4
4
  import { dispatch, select } from '@wordpress/data';
5
5
  // @ts-expect-error No exported types.
6
6
  import { store as blockEditorStore } from '@wordpress/block-editor';
7
- // @ts-expect-error No exported types.
8
7
  import { isUnmodifiedBlock } from '@wordpress/blocks';
9
8
  import { type CRDTDoc, Y } from '@wordpress/sync';
10
9
 
package/src/utils/crdt.ts CHANGED
@@ -6,11 +6,12 @@ import fastDeepEqual from 'fast-deep-equal/es6/index.js';
6
6
  /**
7
7
  * WordPress dependencies
8
8
  */
9
- // @ts-expect-error No exported types.
10
9
  import { __unstableSerializeAndClean } from '@wordpress/blocks';
11
10
  import {
12
11
  type CRDTDoc,
13
12
  type ObjectData,
13
+ type ObjectID,
14
+ type ObjectType,
14
15
  type SyncConfig,
15
16
  Y,
16
17
  } from '@wordpress/sync';
@@ -428,6 +429,19 @@ export const defaultSyncConfig: SyncConfig = {
428
429
  getChangesFromCRDTDoc: defaultGetChangesFromCRDTDoc,
429
430
  };
430
431
 
432
+ /**
433
+ * This default collection sync config can be used to sync entity collections
434
+ * (e.g., block comments) where we are not interested in merging changes at the
435
+ * individual record level, but instead want to replace the entire collection
436
+ * when changes are detected.
437
+ */
438
+ export const defaultCollectionSyncConfig: SyncConfig = {
439
+ applyChangesToCRDTDoc: () => {},
440
+ getChangesFromCRDTDoc: () => ( {} ),
441
+ shouldSync: ( _: ObjectType, objectId: ObjectID | null ) =>
442
+ null === objectId,
443
+ };
444
+
431
445
  /**
432
446
  * Extract the raw string value from a property that may be a string or an object
433
447
  * with a `raw` property (`RenderedText`).