@wordpress/core-data 7.41.2-next.v.202603102151.0 → 7.42.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 (166) hide show
  1. package/CHANGELOG.md +1 -1
  2. package/README.md +19 -0
  3. package/build/actions.cjs +17 -25
  4. package/build/actions.cjs.map +2 -2
  5. package/build/awareness/post-editor-awareness.cjs +46 -6
  6. package/build/awareness/post-editor-awareness.cjs.map +2 -2
  7. package/build/awareness/types.cjs.map +1 -1
  8. package/build/entities.cjs +33 -7
  9. package/build/entities.cjs.map +2 -2
  10. package/build/hooks/use-entity-prop.cjs +2 -1
  11. package/build/hooks/use-entity-prop.cjs.map +2 -2
  12. package/build/hooks/use-post-editor-awareness-state.cjs +84 -3
  13. package/build/hooks/use-post-editor-awareness-state.cjs.map +2 -2
  14. package/build/index.cjs +3 -0
  15. package/build/index.cjs.map +2 -2
  16. package/build/private-apis.cjs +3 -1
  17. package/build/private-apis.cjs.map +2 -2
  18. package/build/queried-data/get-query-parts.cjs +7 -0
  19. package/build/queried-data/get-query-parts.cjs.map +2 -2
  20. package/build/queried-data/selectors.cjs +19 -5
  21. package/build/queried-data/selectors.cjs.map +2 -2
  22. package/build/reducer.cjs +6 -0
  23. package/build/reducer.cjs.map +2 -2
  24. package/build/resolvers.cjs +110 -74
  25. package/build/resolvers.cjs.map +2 -2
  26. package/build/selectors.cjs +29 -0
  27. package/build/selectors.cjs.map +2 -2
  28. package/build/sync.cjs +3 -0
  29. package/build/sync.cjs.map +2 -2
  30. package/build/types.cjs +16 -0
  31. package/build/types.cjs.map +3 -3
  32. package/build/utils/block-selection-history.cjs +1 -1
  33. package/build/utils/block-selection-history.cjs.map +2 -2
  34. package/build/utils/crdt-blocks.cjs +17 -3
  35. package/build/utils/crdt-blocks.cjs.map +2 -2
  36. package/build/utils/crdt-selection.cjs +4 -1
  37. package/build/utils/crdt-selection.cjs.map +2 -2
  38. package/build/utils/crdt-user-selections.cjs +9 -6
  39. package/build/utils/crdt-user-selections.cjs.map +2 -2
  40. package/build/utils/crdt-utils.cjs +54 -2
  41. package/build/utils/crdt-utils.cjs.map +2 -2
  42. package/build/utils/crdt.cjs +4 -23
  43. package/build/utils/crdt.cjs.map +2 -2
  44. package/build/utils/index.cjs +3 -0
  45. package/build/utils/index.cjs.map +2 -2
  46. package/build/utils/normalize-query-for-resolution.cjs +35 -0
  47. package/build/utils/normalize-query-for-resolution.cjs.map +7 -0
  48. package/build-module/actions.mjs +17 -25
  49. package/build-module/actions.mjs.map +2 -2
  50. package/build-module/awareness/post-editor-awareness.mjs +46 -6
  51. package/build-module/awareness/post-editor-awareness.mjs.map +2 -2
  52. package/build-module/entities.mjs +33 -7
  53. package/build-module/entities.mjs.map +2 -2
  54. package/build-module/hooks/use-entity-prop.mjs +2 -1
  55. package/build-module/hooks/use-entity-prop.mjs.map +2 -2
  56. package/build-module/hooks/use-post-editor-awareness-state.mjs +81 -2
  57. package/build-module/hooks/use-post-editor-awareness-state.mjs.map +2 -2
  58. package/build-module/index.mjs +2 -0
  59. package/build-module/index.mjs.map +2 -2
  60. package/build-module/private-apis.mjs +6 -2
  61. package/build-module/private-apis.mjs.map +2 -2
  62. package/build-module/queried-data/get-query-parts.mjs +7 -0
  63. package/build-module/queried-data/get-query-parts.mjs.map +2 -2
  64. package/build-module/queried-data/selectors.mjs +19 -5
  65. package/build-module/queried-data/selectors.mjs.map +2 -2
  66. package/build-module/reducer.mjs +6 -0
  67. package/build-module/reducer.mjs.map +2 -2
  68. package/build-module/resolvers.mjs +112 -75
  69. package/build-module/resolvers.mjs.map +2 -2
  70. package/build-module/selectors.mjs +28 -0
  71. package/build-module/selectors.mjs.map +2 -2
  72. package/build-module/sync.mjs +2 -0
  73. package/build-module/sync.mjs.map +2 -2
  74. package/build-module/types.mjs +9 -0
  75. package/build-module/types.mjs.map +4 -4
  76. package/build-module/utils/block-selection-history.mjs +5 -2
  77. package/build-module/utils/block-selection-history.mjs.map +2 -2
  78. package/build-module/utils/crdt-blocks.mjs +17 -3
  79. package/build-module/utils/crdt-blocks.mjs.map +2 -2
  80. package/build-module/utils/crdt-selection.mjs +8 -2
  81. package/build-module/utils/crdt-selection.mjs.map +2 -2
  82. package/build-module/utils/crdt-user-selections.mjs +10 -7
  83. package/build-module/utils/crdt-user-selections.mjs.map +2 -2
  84. package/build-module/utils/crdt-utils.mjs +51 -1
  85. package/build-module/utils/crdt-utils.mjs.map +2 -2
  86. package/build-module/utils/crdt.mjs +4 -23
  87. package/build-module/utils/crdt.mjs.map +2 -2
  88. package/build-module/utils/index.mjs +2 -0
  89. package/build-module/utils/index.mjs.map +2 -2
  90. package/build-module/utils/normalize-query-for-resolution.mjs +14 -0
  91. package/build-module/utils/normalize-query-for-resolution.mjs.map +7 -0
  92. package/build-types/actions.d.ts.map +1 -1
  93. package/build-types/awareness/post-editor-awareness.d.ts +2 -2
  94. package/build-types/awareness/post-editor-awareness.d.ts.map +1 -1
  95. package/build-types/awareness/types.d.ts +1 -1
  96. package/build-types/awareness/types.d.ts.map +1 -1
  97. package/build-types/entities.d.ts +1 -1
  98. package/build-types/entities.d.ts.map +1 -1
  99. package/build-types/hooks/use-entity-prop.d.ts.map +1 -1
  100. package/build-types/hooks/use-post-editor-awareness-state.d.ts +34 -10
  101. package/build-types/hooks/use-post-editor-awareness-state.d.ts.map +1 -1
  102. package/build-types/index.d.ts +2 -0
  103. package/build-types/index.d.ts.map +1 -1
  104. package/build-types/private-apis.d.ts.map +1 -1
  105. package/build-types/queried-data/get-query-parts.d.ts +7 -0
  106. package/build-types/queried-data/get-query-parts.d.ts.map +1 -1
  107. package/build-types/queried-data/selectors.d.ts.map +1 -1
  108. package/build-types/reducer.d.ts.map +1 -1
  109. package/build-types/resolvers.d.ts +2 -1
  110. package/build-types/resolvers.d.ts.map +1 -1
  111. package/build-types/selectors.d.ts +17 -0
  112. package/build-types/selectors.d.ts.map +1 -1
  113. package/build-types/sync.d.ts +2 -2
  114. package/build-types/sync.d.ts.map +1 -1
  115. package/build-types/types.d.ts +18 -1
  116. package/build-types/types.d.ts.map +1 -1
  117. package/build-types/utils/block-selection-history.d.ts.map +1 -1
  118. package/build-types/utils/crdt-blocks.d.ts.map +1 -1
  119. package/build-types/utils/crdt-selection.d.ts.map +1 -1
  120. package/build-types/utils/crdt-user-selections.d.ts +9 -5
  121. package/build-types/utils/crdt-user-selections.d.ts.map +1 -1
  122. package/build-types/utils/crdt-utils.d.ts +20 -0
  123. package/build-types/utils/crdt-utils.d.ts.map +1 -1
  124. package/build-types/utils/crdt.d.ts +6 -7
  125. package/build-types/utils/crdt.d.ts.map +1 -1
  126. package/build-types/utils/index.d.ts +1 -0
  127. package/build-types/utils/normalize-query-for-resolution.d.ts +12 -0
  128. package/build-types/utils/normalize-query-for-resolution.d.ts.map +1 -0
  129. package/build-types/utils/test/crdt-utils.d.ts +2 -0
  130. package/build-types/utils/test/crdt-utils.d.ts.map +1 -0
  131. package/package.json +18 -18
  132. package/src/actions.js +25 -40
  133. package/src/awareness/post-editor-awareness.ts +106 -7
  134. package/src/awareness/test/post-editor-awareness.ts +50 -10
  135. package/src/awareness/types.ts +1 -1
  136. package/src/entities.js +38 -6
  137. package/src/hooks/test/use-post-editor-awareness-state.ts +446 -3
  138. package/src/hooks/use-entity-prop.js +2 -0
  139. package/src/hooks/use-post-editor-awareness-state.ts +160 -8
  140. package/src/index.js +1 -0
  141. package/src/private-apis.js +6 -2
  142. package/src/queried-data/get-query-parts.js +13 -0
  143. package/src/queried-data/selectors.js +33 -8
  144. package/src/queried-data/test/get-query-parts.js +34 -0
  145. package/src/queried-data/test/selectors.js +183 -0
  146. package/src/reducer.js +11 -0
  147. package/src/resolvers.js +136 -88
  148. package/src/selectors.ts +56 -0
  149. package/src/sync.ts +2 -0
  150. package/src/test/entities.js +185 -1
  151. package/src/test/resolvers.js +64 -11
  152. package/src/test/selectors.js +150 -0
  153. package/src/test/store.js +66 -0
  154. package/src/types.ts +26 -1
  155. package/src/utils/block-selection-history.ts +5 -2
  156. package/src/utils/crdt-blocks.ts +32 -3
  157. package/src/utils/crdt-selection.ts +8 -2
  158. package/src/utils/crdt-user-selections.ts +20 -8
  159. package/src/utils/crdt-utils.ts +99 -0
  160. package/src/utils/crdt.ts +8 -32
  161. package/src/utils/index.js +1 -0
  162. package/src/utils/normalize-query-for-resolution.js +23 -0
  163. package/src/utils/test/crdt-blocks.ts +146 -0
  164. package/src/utils/test/crdt-user-selections.ts +5 -0
  165. package/src/utils/test/crdt-utils.ts +387 -0
  166. package/src/utils/test/crdt.ts +120 -53
package/src/resolvers.js CHANGED
@@ -24,6 +24,7 @@ import {
24
24
  ALLOWED_RESOURCE_ACTIONS,
25
25
  RECEIVE_INTERMEDIATE_RESULTS,
26
26
  isNumericID,
27
+ normalizeQueryForResolution,
27
28
  } from './utils';
28
29
  import { fetchBlockPatterns } from './fetch';
29
30
  import { restoreSelection, getSelectionHistory } from './utils/crdt-selection';
@@ -229,18 +230,26 @@ export const getEntityRecord =
229
230
  query
230
231
  );
231
232
  },
232
- // Save the current entity record, whether or not it has unsaved
233
- // edits. This is used to trigger a persisted CRDT document.
234
- saveRecord: () => {
233
+ // Persist the CRDT document.
234
+ //
235
+ // TODO: Currently, persisted CRDT documents are stored in post meta.
236
+ // This effectively means that only post entities support CRDT
237
+ // persistence. As we add support for syncing additional entity,
238
+ // we'll need to revisit where persisted CRDT documents are stored.
239
+ persistCRDTDoc: () => {
235
240
  resolveSelect
236
241
  .getEditedEntityRecord( kind, name, key )
237
242
  .then( ( editedRecord ) => {
238
- // Don't trigger a save if the record is still an auto-draft.
239
- const { status } = editedRecord;
240
- if ( 'auto-draft' === status ) {
243
+ // Don't persist the CRDT document if the record is still an
244
+ // auto-draft or if the entity does not support meta.
245
+ const { meta, status } = editedRecord;
246
+ if ( 'auto-draft' === status || ! meta ) {
241
247
  return;
242
248
  }
243
249
 
250
+ // Trigger a save to persist the CRDT document. The entity's
251
+ // pre-persist hooks will create the persisted CRDT document
252
+ // and apply it to the record's meta.
244
253
  dispatch.saveEntityRecord(
245
254
  kind,
246
255
  name,
@@ -350,18 +359,14 @@ export const getEntityRecords =
350
359
  const key = entityConfig.key || DEFAULT_ENTITY_KEY;
351
360
 
352
361
  function getResolutionsArgs( records, recordsQuery ) {
353
- const queryArgs = Object.fromEntries(
354
- Object.entries( recordsQuery ).filter( ( [ k, v ] ) => {
355
- return [ 'context', '_fields' ].includes( k ) && !! v;
356
- } )
357
- );
362
+ const normalizedQuery = normalizeQueryForResolution( recordsQuery );
358
363
  return records
359
364
  .filter( ( record ) => record?.[ key ] )
360
365
  .map( ( record ) => [
361
366
  kind,
362
367
  name,
363
368
  record[ key ],
364
- Object.keys( queryArgs ).length > 0 ? queryArgs : undefined,
369
+ normalizedQuery,
365
370
  ] );
366
371
  }
367
372
 
@@ -1036,78 +1041,87 @@ export const getRevisions =
1036
1041
  return;
1037
1042
  }
1038
1043
 
1039
- if ( query._fields ) {
1040
- // If requesting specific fields, items and query association to said
1041
- // records are stored by ID reference. Thus, fields must always include
1042
- // the ID.
1043
- query = {
1044
- ...query,
1045
- _fields: [
1046
- ...new Set( [
1047
- ...( getNormalizedCommaSeparable( query._fields ) ||
1048
- [] ),
1049
- entityConfig.revisionKey || DEFAULT_ENTITY_KEY,
1050
- ] ),
1051
- ].join(),
1052
- };
1053
- }
1054
-
1055
- const path = addQueryArgs(
1056
- entityConfig.getRevisionsUrl( recordKey ),
1057
- query
1044
+ const rawQuery = { ...query };
1045
+ const lock = await dispatch.__unstableAcquireStoreLock(
1046
+ STORE_NAME,
1047
+ [ 'entities', 'records', kind, name, recordKey, 'revisions' ],
1048
+ { exclusive: false }
1058
1049
  );
1059
1050
 
1060
- let records, response;
1061
- const meta = {};
1062
- const isPaginated =
1063
- entityConfig.supportsPagination && query.per_page !== -1;
1064
1051
  try {
1065
- response = await apiFetch( { path, parse: ! isPaginated } );
1066
- } catch ( error ) {
1067
- // Do nothing if our request comes back with an API error.
1068
- return;
1069
- }
1070
-
1071
- if ( response ) {
1072
- if ( isPaginated ) {
1073
- records = Object.values( await response.json() );
1074
- meta.totalItems = parseInt(
1075
- response.headers.get( 'X-WP-Total' )
1076
- );
1077
- } else {
1078
- records = Object.values( response );
1052
+ if ( query._fields ) {
1053
+ // If requesting specific fields, items and query association to said
1054
+ // records are stored by ID reference. Thus, fields must always include
1055
+ // the ID.
1056
+ query = {
1057
+ ...query,
1058
+ _fields: [
1059
+ ...new Set( [
1060
+ ...( getNormalizedCommaSeparable( query._fields ) ||
1061
+ [] ),
1062
+ entityConfig.revisionKey || DEFAULT_ENTITY_KEY,
1063
+ ] ),
1064
+ ].join(),
1065
+ };
1079
1066
  }
1080
1067
 
1081
- // If we request fields but the result doesn't contain the fields,
1082
- // explicitly set these fields as "undefined"
1083
- // that way we consider the query "fulfilled".
1084
- if ( query._fields ) {
1085
- records = records.map( ( record ) => {
1086
- query._fields.split( ',' ).forEach( ( field ) => {
1087
- if ( ! record.hasOwnProperty( field ) ) {
1088
- record[ field ] = undefined;
1089
- }
1090
- } );
1068
+ const path = addQueryArgs(
1069
+ entityConfig.getRevisionsUrl( recordKey ),
1070
+ query
1071
+ );
1091
1072
 
1092
- return record;
1093
- } );
1073
+ let records, response;
1074
+ const meta = {};
1075
+ const isPaginated =
1076
+ entityConfig.supportsPagination && query.per_page !== -1;
1077
+ try {
1078
+ response = await apiFetch( { path, parse: ! isPaginated } );
1079
+ } catch ( error ) {
1080
+ // Do nothing if our request comes back with an API error.
1081
+ return;
1094
1082
  }
1095
1083
 
1096
- registry.batch( () => {
1097
- dispatch.receiveRevisions(
1098
- kind,
1099
- name,
1100
- recordKey,
1101
- records,
1102
- query,
1103
- false,
1104
- meta
1105
- );
1084
+ if ( response ) {
1085
+ if ( isPaginated ) {
1086
+ records = Object.values( await response.json() );
1087
+ meta.totalItems = parseInt(
1088
+ response.headers.get( 'X-WP-Total' )
1089
+ );
1090
+ } else {
1091
+ records = Object.values( response );
1092
+ }
1093
+
1094
+ // If we request fields but the result doesn't contain the fields,
1095
+ // explicitly set these fields as "undefined"
1096
+ // that way we consider the query "fulfilled".
1097
+ if ( query._fields ) {
1098
+ records = records.map( ( record ) => {
1099
+ query._fields.split( ',' ).forEach( ( field ) => {
1100
+ if ( ! record.hasOwnProperty( field ) ) {
1101
+ record[ field ] = undefined;
1102
+ }
1103
+ } );
1104
+
1105
+ return record;
1106
+ } );
1107
+ }
1108
+
1109
+ registry.batch( () => {
1110
+ dispatch.receiveRevisions(
1111
+ kind,
1112
+ name,
1113
+ recordKey,
1114
+ records,
1115
+ query,
1116
+ false,
1117
+ meta
1118
+ );
1106
1119
 
1107
- // When requesting all fields, the list of results can be used to
1108
- // resolve the `getRevision` selector in addition to `getRevisions`.
1109
- if ( ! query?._fields && ! query.context ) {
1120
+ // Mark individual getRevision resolutions as done so that
1121
+ // subsequent getRevision calls skip redundant API fetches.
1110
1122
  const key = entityConfig.revisionKey || DEFAULT_ENTITY_KEY;
1123
+ const normalizedQuery =
1124
+ normalizeQueryForResolution( rawQuery );
1111
1125
  const resolutionsArgs = records
1112
1126
  .filter( ( record ) => record[ key ] )
1113
1127
  .map( ( record ) => [
@@ -1115,14 +1129,17 @@ export const getRevisions =
1115
1129
  name,
1116
1130
  recordKey,
1117
1131
  record[ key ],
1132
+ normalizedQuery,
1118
1133
  ] );
1119
1134
 
1120
1135
  dispatch.finishResolutions(
1121
1136
  'getRevision',
1122
1137
  resolutionsArgs
1123
1138
  );
1124
- }
1125
- } );
1139
+ } );
1140
+ }
1141
+ } finally {
1142
+ dispatch.__unstableReleaseStoreLock( lock );
1126
1143
  }
1127
1144
  };
1128
1145
 
@@ -1147,7 +1164,7 @@ getRevisions.shouldInvalidate = ( action, kind, name, recordKey ) =>
1147
1164
  */
1148
1165
  export const getRevision =
1149
1166
  ( kind, name, recordKey, revisionKey, query ) =>
1150
- async ( { dispatch, resolveSelect } ) => {
1167
+ async ( { select, dispatch, resolveSelect } ) => {
1151
1168
  const configs = await resolveSelect.getEntitiesConfig( kind );
1152
1169
  const entityConfig = configs.find(
1153
1170
  ( config ) => config.name === name && config.kind === kind
@@ -1172,21 +1189,52 @@ export const getRevision =
1172
1189
  ].join(),
1173
1190
  };
1174
1191
  }
1175
- const path = addQueryArgs(
1176
- entityConfig.getRevisionsUrl( recordKey, revisionKey ),
1177
- query
1192
+
1193
+ const lock = await dispatch.__unstableAcquireStoreLock(
1194
+ STORE_NAME,
1195
+ [
1196
+ 'entities',
1197
+ 'records',
1198
+ kind,
1199
+ name,
1200
+ recordKey,
1201
+ 'revisions',
1202
+ revisionKey,
1203
+ ],
1204
+ { exclusive: false }
1178
1205
  );
1179
1206
 
1180
- let record;
1181
1207
  try {
1182
- record = await apiFetch( { path } );
1183
- } catch ( error ) {
1184
- // Do nothing if our request comes back with an API error.
1185
- return;
1186
- }
1208
+ if (
1209
+ select.hasRevision( kind, name, recordKey, revisionKey, query )
1210
+ ) {
1211
+ return;
1212
+ }
1187
1213
 
1188
- if ( record ) {
1189
- dispatch.receiveRevisions( kind, name, recordKey, record, query );
1214
+ const path = addQueryArgs(
1215
+ entityConfig.getRevisionsUrl( recordKey, revisionKey ),
1216
+ query
1217
+ );
1218
+
1219
+ let record;
1220
+ try {
1221
+ record = await apiFetch( { path } );
1222
+ } catch ( error ) {
1223
+ // Do nothing if our request comes back with an API error.
1224
+ return;
1225
+ }
1226
+
1227
+ if ( record ) {
1228
+ dispatch.receiveRevisions(
1229
+ kind,
1230
+ name,
1231
+ recordKey,
1232
+ record,
1233
+ query
1234
+ );
1235
+ }
1236
+ } finally {
1237
+ dispatch.__unstableReleaseStoreLock( lock );
1190
1238
  }
1191
1239
  };
1192
1240
 
package/src/selectors.ts CHANGED
@@ -1524,6 +1524,62 @@ export const getRevisions = (
1524
1524
  return getQueriedItems( queriedStateRevisions, query );
1525
1525
  };
1526
1526
 
1527
+ /**
1528
+ * Returns true if a revision has been received for the given set of parameters,
1529
+ * or false otherwise.
1530
+ *
1531
+ * Note: This does not trigger a request for the revision from the API
1532
+ * if it's not available in the local state.
1533
+ *
1534
+ * @param state State tree
1535
+ * @param kind Entity kind.
1536
+ * @param name Entity name.
1537
+ * @param recordKey The key of the entity record whose revision you want to check.
1538
+ * @param revisionKey The revision's key.
1539
+ * @param query Optional query.
1540
+ *
1541
+ * @return Whether a revision has been received.
1542
+ */
1543
+ export function hasRevision(
1544
+ state: State,
1545
+ kind: string,
1546
+ name: string,
1547
+ recordKey: EntityRecordKey,
1548
+ revisionKey: EntityRecordKey,
1549
+ query?: GetRecordsHttpQuery
1550
+ ): boolean {
1551
+ const queriedState =
1552
+ state.entities.records?.[ kind ]?.[ name ]?.revisions?.[ recordKey ];
1553
+ if ( ! queriedState ) {
1554
+ return false;
1555
+ }
1556
+ const context = query?.context ?? 'default';
1557
+
1558
+ if ( ! query || ! query._fields ) {
1559
+ return !! queriedState.itemIsComplete[ context ]?.[ revisionKey ];
1560
+ }
1561
+
1562
+ const item = queriedState.items[ context ]?.[ revisionKey ];
1563
+ if ( ! item ) {
1564
+ return false;
1565
+ }
1566
+
1567
+ const fields = getNormalizedCommaSeparable( query._fields ) ?? [];
1568
+ for ( let i = 0; i < fields.length; i++ ) {
1569
+ const path = fields[ i ].split( '.' );
1570
+ let value = item;
1571
+ for ( let p = 0; p < path.length; p++ ) {
1572
+ const part = path[ p ];
1573
+ if ( ! value || ! Object.hasOwn( value, part ) ) {
1574
+ return false;
1575
+ }
1576
+ value = value[ part ];
1577
+ }
1578
+ }
1579
+
1580
+ return true;
1581
+ }
1582
+
1527
1583
  /**
1528
1584
  * Returns a single, specific revision of a parent entity.
1529
1585
  *
package/src/sync.ts CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  import { unlock } from './lock-unlock';
13
13
 
14
14
  const {
15
+ ConnectionErrorCode,
15
16
  createSyncManager,
16
17
  Delta,
17
18
  CRDT_DOC_META_PERSISTENCE_KEY,
@@ -22,6 +23,7 @@ const {
22
23
  } = unlock( syncPrivateApis );
23
24
 
24
25
  export {
26
+ ConnectionErrorCode,
25
27
  Delta,
26
28
  CRDT_DOC_META_PERSISTENCE_KEY,
27
29
  CRDT_RECORD_MAP_KEY,
@@ -8,6 +8,10 @@ jest.mock( '../sync', () => ( {
8
8
  ...jest.requireActual( '../sync' ),
9
9
  getSyncManager: jest.fn(),
10
10
  } ) );
11
+ jest.mock( '../utils/crdt', () => ( {
12
+ ...jest.requireActual( '../utils/crdt' ),
13
+ applyPostChangesToCRDTDoc: jest.fn(),
14
+ } ) );
11
15
 
12
16
  /**
13
17
  * Internal dependencies
@@ -19,7 +23,10 @@ import {
19
23
  additionalEntityConfigLoaders,
20
24
  } from '../entities';
21
25
  import { getSyncManager } from '../sync';
22
- import { POST_META_KEY_FOR_CRDT_DOC_PERSISTENCE } from '../utils/crdt';
26
+ import {
27
+ applyPostChangesToCRDTDoc,
28
+ POST_META_KEY_FOR_CRDT_DOC_PERSISTENCE,
29
+ } from '../utils/crdt';
23
30
 
24
31
  describe( 'getMethodName', () => {
25
32
  it( 'should return the right method name for an entity with the root kind', () => {
@@ -127,6 +134,183 @@ describe( 'prePersistPostType', () => {
127
134
  } );
128
135
  } );
129
136
 
137
+ describe( 'loadPostTypeEntities', () => {
138
+ let originalCollaborationEnabled;
139
+
140
+ beforeEach( () => {
141
+ apiFetch.mockReset();
142
+ applyPostChangesToCRDTDoc.mockReset();
143
+ originalCollaborationEnabled = window._wpCollaborationEnabled;
144
+ } );
145
+
146
+ afterEach( () => {
147
+ window._wpCollaborationEnabled = originalCollaborationEnabled;
148
+ } );
149
+
150
+ it( 'should include custom taxonomy rest_bases in synced properties when collaboration is enabled', async () => {
151
+ window._wpCollaborationEnabled = true;
152
+
153
+ const mockPostTypes = {
154
+ book: {
155
+ name: 'Books',
156
+ rest_base: 'books',
157
+ rest_namespace: 'wp/v2',
158
+ taxonomies: [ 'genre', 'audience' ],
159
+ },
160
+ };
161
+ const mockTaxonomies = {
162
+ genre: {
163
+ name: 'Genres',
164
+ rest_base: 'genres',
165
+ rest_namespace: 'wp/v2',
166
+ },
167
+ audience: {
168
+ name: 'Audiences',
169
+ rest_base: 'audiences',
170
+ rest_namespace: 'wp/v2',
171
+ },
172
+ };
173
+
174
+ apiFetch
175
+ .mockResolvedValueOnce( mockPostTypes )
176
+ .mockResolvedValueOnce( mockTaxonomies );
177
+
178
+ const postTypeLoader = additionalEntityConfigLoaders.find(
179
+ ( loader ) => loader.kind === 'postType'
180
+ );
181
+ const entities = await postTypeLoader.loadEntities();
182
+ const bookEntity = entities.find( ( e ) => e.name === 'book' );
183
+
184
+ bookEntity.syncConfig.applyChangesToCRDTDoc( {}, {} );
185
+
186
+ expect( applyPostChangesToCRDTDoc ).toHaveBeenCalledWith(
187
+ {},
188
+ {},
189
+ expect.any( Set )
190
+ );
191
+
192
+ const syncedProperties = applyPostChangesToCRDTDoc.mock.calls[ 0 ][ 2 ];
193
+ expect( syncedProperties ).toContain( 'genres' );
194
+ expect( syncedProperties ).toContain( 'audiences' );
195
+ } );
196
+
197
+ it( 'should not fetch taxonomies when collaboration is disabled', async () => {
198
+ window._wpCollaborationEnabled = false;
199
+
200
+ const mockPostTypes = {
201
+ post: {
202
+ name: 'Posts',
203
+ rest_base: 'posts',
204
+ rest_namespace: 'wp/v2',
205
+ taxonomies: [ 'category', 'post_tag' ],
206
+ },
207
+ };
208
+
209
+ apiFetch.mockResolvedValueOnce( mockPostTypes );
210
+
211
+ const postTypeLoader = additionalEntityConfigLoaders.find(
212
+ ( loader ) => loader.kind === 'postType'
213
+ );
214
+ const entities = await postTypeLoader.loadEntities();
215
+ const postEntity = entities.find( ( e ) => e.name === 'post' );
216
+
217
+ postEntity.syncConfig.applyChangesToCRDTDoc( {}, {} );
218
+
219
+ // Only one apiFetch call (post types), no taxonomy fetch.
220
+ expect( apiFetch ).toHaveBeenCalledTimes( 1 );
221
+
222
+ const syncedProperties = applyPostChangesToCRDTDoc.mock.calls[ 0 ][ 2 ];
223
+ expect( syncedProperties ).not.toContain( 'categories' );
224
+ expect( syncedProperties ).not.toContain( 'tags' );
225
+ } );
226
+
227
+ it( 'should skip taxonomy rest_base when taxonomy is not found in fetched taxonomies', async () => {
228
+ window._wpCollaborationEnabled = true;
229
+
230
+ const mockPostTypes = {
231
+ book: {
232
+ name: 'Books',
233
+ rest_base: 'books',
234
+ rest_namespace: 'wp/v2',
235
+ taxonomies: [ 'genre', 'missing_taxonomy' ],
236
+ },
237
+ };
238
+ const mockTaxonomies = {
239
+ genre: {
240
+ name: 'Genres',
241
+ rest_base: 'genres',
242
+ rest_namespace: 'wp/v2',
243
+ },
244
+ // 'missing_taxonomy' is intentionally absent.
245
+ };
246
+
247
+ apiFetch
248
+ .mockResolvedValueOnce( mockPostTypes )
249
+ .mockResolvedValueOnce( mockTaxonomies );
250
+
251
+ const postTypeLoader = additionalEntityConfigLoaders.find(
252
+ ( loader ) => loader.kind === 'postType'
253
+ );
254
+ const entities = await postTypeLoader.loadEntities();
255
+ const bookEntity = entities.find( ( e ) => e.name === 'book' );
256
+
257
+ bookEntity.syncConfig.applyChangesToCRDTDoc( {}, {} );
258
+
259
+ const syncedProperties = applyPostChangesToCRDTDoc.mock.calls[ 0 ][ 2 ];
260
+ expect( syncedProperties ).toContain( 'genres' );
261
+ // missing_taxonomy has no rest_base entry, so nothing should be added for it.
262
+ expect( syncedProperties.size ).toBe( 16 ); // 15 base + 1 taxonomy (genres)
263
+ } );
264
+
265
+ it( 'should include base synced properties regardless of taxonomies', async () => {
266
+ window._wpCollaborationEnabled = true;
267
+
268
+ const mockPostTypes = {
269
+ page: {
270
+ name: 'Pages',
271
+ rest_base: 'pages',
272
+ rest_namespace: 'wp/v2',
273
+ taxonomies: [],
274
+ },
275
+ };
276
+
277
+ apiFetch
278
+ .mockResolvedValueOnce( mockPostTypes )
279
+ .mockResolvedValueOnce( {} );
280
+
281
+ const postTypeLoader = additionalEntityConfigLoaders.find(
282
+ ( loader ) => loader.kind === 'postType'
283
+ );
284
+ const entities = await postTypeLoader.loadEntities();
285
+ const pageEntity = entities.find( ( e ) => e.name === 'page' );
286
+
287
+ pageEntity.syncConfig.applyChangesToCRDTDoc( {}, {} );
288
+
289
+ const syncedProperties = applyPostChangesToCRDTDoc.mock.calls[ 0 ][ 2 ];
290
+ const expectedBase = [
291
+ 'author',
292
+ 'blocks',
293
+ 'content',
294
+ 'comment_status',
295
+ 'date',
296
+ 'excerpt',
297
+ 'featured_media',
298
+ 'format',
299
+ 'meta',
300
+ 'ping_status',
301
+ 'slug',
302
+ 'status',
303
+ 'sticky',
304
+ 'template',
305
+ 'title',
306
+ ];
307
+ for ( const prop of expectedBase ) {
308
+ expect( syncedProperties ).toContain( prop );
309
+ }
310
+ expect( syncedProperties.size ).toBe( 15 );
311
+ } );
312
+ } );
313
+
130
314
  describe( 'loadTaxonomyEntities', () => {
131
315
  beforeEach( () => {
132
316
  apiFetch.mockReset();