@wordpress/core-data 7.40.2-next.v.202602241322.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 (183) 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 +50 -23
  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 +16 -6
  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 +11 -5
  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 +125 -49
  39. package/build/utils/crdt-blocks.cjs.map +2 -2
  40. package/build/utils/crdt-selection.cjs +50 -19
  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 +38 -29
  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 +53 -24
  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 +16 -6
  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 +7 -3
  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 +124 -49
  85. package/build-module/utils/crdt-blocks.mjs.map +2 -2
  86. package/build-module/utils/crdt-selection.mjs +53 -20
  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 +42 -34
  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 +11 -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 +12 -1
  131. package/build-types/utils/crdt-blocks.d.ts.map +1 -1
  132. package/build-types/utils/crdt-selection.d.ts +10 -0
  133. package/build-types/utils/crdt-selection.d.ts.map +1 -1
  134. package/build-types/utils/crdt-text.d.ts +16 -0
  135. package/build-types/utils/crdt-text.d.ts.map +1 -0
  136. package/build-types/utils/crdt-user-selections.d.ts +17 -4
  137. package/build-types/utils/crdt-user-selections.d.ts.map +1 -1
  138. package/build-types/utils/crdt-utils.d.ts +20 -0
  139. package/build-types/utils/crdt-utils.d.ts.map +1 -1
  140. package/build-types/utils/crdt.d.ts +14 -7
  141. package/build-types/utils/crdt.d.ts.map +1 -1
  142. package/build-types/utils/normalize-query-for-resolution.d.ts +12 -0
  143. package/build-types/utils/normalize-query-for-resolution.d.ts.map +1 -0
  144. package/build-types/utils/save-crdt-doc.d.ts +8 -0
  145. package/build-types/utils/save-crdt-doc.d.ts.map +1 -0
  146. package/build-types/utils/test/crdt-utils.d.ts +2 -0
  147. package/build-types/utils/test/crdt-utils.d.ts.map +1 -0
  148. package/package.json +18 -18
  149. package/src/actions.js +26 -41
  150. package/src/awareness/post-editor-awareness.ts +106 -7
  151. package/src/awareness/test/post-editor-awareness.ts +50 -10
  152. package/src/awareness/types.ts +7 -1
  153. package/src/entities.js +62 -24
  154. package/src/entity-provider.js +24 -11
  155. package/src/hooks/test/use-post-editor-awareness-state.ts +446 -3
  156. package/src/hooks/use-entity-prop.js +43 -3
  157. package/src/hooks/use-post-editor-awareness-state.ts +229 -7
  158. package/src/index.js +0 -6
  159. package/src/private-actions.js +43 -1
  160. package/src/{private-apis.js → private-apis.ts} +21 -3
  161. package/src/private-selectors.ts +43 -0
  162. package/src/queried-data/actions.js +1 -1
  163. package/src/queried-data/reducer.js +26 -14
  164. package/src/reducer.js +36 -1
  165. package/src/resolvers.js +23 -7
  166. package/src/selectors.ts +1 -32
  167. package/src/sync.ts +6 -2
  168. package/src/test/entities.js +231 -14
  169. package/src/test/private-actions.js +1 -1
  170. package/src/test/resolvers.js +64 -11
  171. package/src/test/store.js +116 -0
  172. package/src/types.ts +21 -2
  173. package/src/utils/block-selection-history.ts +5 -2
  174. package/src/utils/crdt-blocks.ts +250 -81
  175. package/src/utils/crdt-selection.ts +92 -26
  176. package/src/utils/crdt-text.ts +43 -0
  177. package/src/utils/crdt-user-selections.ts +29 -8
  178. package/src/utils/crdt-utils.ts +99 -0
  179. package/src/utils/crdt.ts +72 -46
  180. package/src/utils/test/crdt-blocks.ts +1177 -22
  181. package/src/utils/test/crdt-user-selections.ts +5 -0
  182. package/src/utils/test/crdt-utils.ts +387 -0
  183. package/src/utils/test/crdt.ts +423 -60
@@ -1,7 +1,9 @@
1
1
  /**
2
2
  * External dependencies
3
3
  */
4
+ import { usePrevious } from '@wordpress/compose';
4
5
  import { useEffect, useState } from '@wordpress/element';
6
+ import type { Y } from '@wordpress/sync';
5
7
 
6
8
  /**
7
9
  * Internal dependencies
@@ -9,16 +11,12 @@ import { useEffect, useState } from '@wordpress/element';
9
11
  import { getSyncManager } from '../sync';
10
12
  import type {
11
13
  PostEditorAwarenessState as ActiveCollaborator,
14
+ PostSaveEvent,
12
15
  YDocDebugData,
13
16
  } from '../awareness/types';
14
- import type { SelectionState } from '../types';
17
+ import type { SelectionState, ResolvedSelection } from '../types';
15
18
  import type { PostEditorAwareness } from '../awareness/post-editor-awareness';
16
19
 
17
- interface ResolvedSelection {
18
- textIndex: number | null;
19
- localClientId: string | null;
20
- }
21
-
22
20
  interface AwarenessState {
23
21
  activeCollaborators: ActiveCollaborator[];
24
22
  resolveSelection: ( selection: SelectionState ) => ResolvedSelection;
@@ -27,7 +25,7 @@ interface AwarenessState {
27
25
  }
28
26
 
29
27
  const defaultResolvedSelection: ResolvedSelection = {
30
- textIndex: null,
28
+ richTextOffset: null,
31
29
  localClientId: null,
32
30
  };
33
31
 
@@ -156,3 +154,227 @@ export function useIsDisconnected(
156
154
  return usePostEditorAwarenessState( postId, postType )
157
155
  .isCurrentCollaboratorDisconnected;
158
156
  }
157
+
158
+ /**
159
+ * Hook that subscribes to the CRDT state map and returns the most recent
160
+ * save event (timestamp + client ID). The state map is updated by
161
+ * `markEntityAsSaved` in `@wordpress/sync`
162
+ *
163
+ * @param postId The ID of the post.
164
+ * @param postType The type of the post.
165
+ */
166
+ function useLastPostSave(
167
+ postId: number | null,
168
+ postType: string | null
169
+ ): PostSaveEvent | null {
170
+ const [ lastSave, setLastSave ] = useState< PostSaveEvent | null >( null );
171
+
172
+ useEffect( () => {
173
+ if ( null === postId || null === postType ) {
174
+ setLastSave( null );
175
+ return;
176
+ }
177
+
178
+ const awareness = getSyncManager()?.getAwareness< PostEditorAwareness >(
179
+ `postType/${ postType }`,
180
+ postId.toString()
181
+ );
182
+
183
+ if ( ! awareness ) {
184
+ setLastSave( null );
185
+ return;
186
+ }
187
+
188
+ awareness.setUp();
189
+
190
+ const stateMap = awareness.doc.getMap( 'state' );
191
+ const recordMap = awareness.doc.getMap( 'document' );
192
+
193
+ // Only notify for saves that occur after the observer is
194
+ // set up. This prevents false notifications when the Y.Doc
195
+ // syncs historical state on page load or peer reconnect.
196
+ const setupTime = Date.now();
197
+
198
+ const observer = ( event: Y.YMapEvent< unknown > ) => {
199
+ if ( event.keysChanged.has( 'savedAt' ) ) {
200
+ const savedAt = stateMap.get( 'savedAt' ) as number;
201
+ const savedByClientId = stateMap.get( 'savedBy' ) as number;
202
+
203
+ if (
204
+ typeof savedAt === 'number' &&
205
+ typeof savedByClientId === 'number' &&
206
+ savedAt > setupTime
207
+ ) {
208
+ const postStatus = recordMap.get( 'status' ) as
209
+ | string
210
+ | undefined;
211
+ setLastSave( { savedAt, savedByClientId, postStatus } );
212
+ }
213
+ }
214
+ };
215
+
216
+ stateMap.observe( observer );
217
+
218
+ return () => {
219
+ stateMap.unobserve( observer );
220
+ };
221
+ }, [ postId, postType ] );
222
+
223
+ return lastSave;
224
+ }
225
+
226
+ /**
227
+ * Hook that fires a callback when a new collaborator joins the post.
228
+ * Handles initial hydration and state diffing internally—consumers
229
+ * only receive "join" events for collaborators that appear after the
230
+ * initial state has loaded.
231
+ *
232
+ * The callback receives the joining collaborator and, when available,
233
+ * the current user's state (useful for comparing `enteredAt` timestamps).
234
+ *
235
+ * @param postId The ID of the post.
236
+ * @param postType The type of the post.
237
+ * @param callback Invoked for each collaborator that joins.
238
+ */
239
+ export function useOnCollaboratorJoin(
240
+ postId: number | null,
241
+ postType: string | null,
242
+ callback: (
243
+ collaborator: ActiveCollaborator,
244
+ me?: ActiveCollaborator
245
+ ) => void
246
+ ): void {
247
+ const { activeCollaborators } = usePostEditorAwarenessState(
248
+ postId,
249
+ postType
250
+ );
251
+ const prevCollaborators = usePrevious( activeCollaborators );
252
+
253
+ useEffect( () => {
254
+ /*
255
+ * On first render usePrevious returns undefined. On subsequent
256
+ * renders the list may still be empty while the store hydrates.
257
+ * In both cases, skip to avoid spurious "joined" callbacks for
258
+ * users already present.
259
+ */
260
+ if ( ! prevCollaborators || prevCollaborators.length === 0 ) {
261
+ return;
262
+ }
263
+
264
+ const prevMap = new Map< number, ActiveCollaborator >(
265
+ prevCollaborators.map( ( collaborator ) => [
266
+ collaborator.clientId,
267
+ collaborator,
268
+ ] )
269
+ );
270
+ const me = activeCollaborators.find(
271
+ ( collaborator ) => collaborator.isMe
272
+ );
273
+
274
+ for ( const collaborator of activeCollaborators ) {
275
+ if (
276
+ ! prevMap.has( collaborator.clientId ) &&
277
+ ! collaborator.isMe
278
+ ) {
279
+ callback( collaborator, me );
280
+ }
281
+ }
282
+ }, [ activeCollaborators, prevCollaborators, callback ] );
283
+ }
284
+
285
+ /**
286
+ * Hook that fires a callback when a collaborator leaves the post.
287
+ * A "leave" is detected when a previously-connected collaborator either
288
+ * transitions to `isConnected = false` or disappears from the list
289
+ * entirely while still connected. Already-disconnected collaborators
290
+ * that are later removed from the list are silently ignored.
291
+ *
292
+ * @param postId The ID of the post.
293
+ * @param postType The type of the post.
294
+ * @param callback Invoked for each collaborator that leaves.
295
+ */
296
+ export function useOnCollaboratorLeave(
297
+ postId: number | null,
298
+ postType: string | null,
299
+ callback: ( collaborator: ActiveCollaborator ) => void
300
+ ): void {
301
+ const { activeCollaborators } = usePostEditorAwarenessState(
302
+ postId,
303
+ postType
304
+ );
305
+ const prevCollaborators = usePrevious( activeCollaborators );
306
+
307
+ useEffect( () => {
308
+ if ( ! prevCollaborators || prevCollaborators.length === 0 ) {
309
+ return;
310
+ }
311
+
312
+ const newMap = new Map< number, ActiveCollaborator >(
313
+ activeCollaborators.map( ( collaborator ) => [
314
+ collaborator.clientId,
315
+ collaborator,
316
+ ] )
317
+ );
318
+
319
+ for ( const prevCollab of prevCollaborators ) {
320
+ if ( prevCollab.isMe || ! prevCollab.isConnected ) {
321
+ continue;
322
+ }
323
+
324
+ const newCollab = newMap.get( prevCollab.clientId );
325
+ if ( ! newCollab?.isConnected ) {
326
+ callback( prevCollab );
327
+ }
328
+ }
329
+ }, [ activeCollaborators, prevCollaborators, callback ] );
330
+ }
331
+
332
+ /**
333
+ * Hook that fires a callback when a remote collaborator saves the post.
334
+ * Only fires for saves by other collaborators (not the current user).
335
+ * Deduplicates by `savedAt` timestamp so the same save event is never
336
+ * reported twice.
337
+ *
338
+ * @param postId The ID of the post.
339
+ * @param postType The type of the post.
340
+ * @param callback Invoked with the save event, the collaborator who saved,
341
+ * and the previous save event (if any) for transition detection.
342
+ */
343
+ export function useOnPostSave(
344
+ postId: number | null,
345
+ postType: string | null,
346
+ callback: (
347
+ event: PostSaveEvent,
348
+ saver: ActiveCollaborator,
349
+ prevEvent: PostSaveEvent | null
350
+ ) => void
351
+ ): void {
352
+ const { activeCollaborators } = usePostEditorAwarenessState(
353
+ postId,
354
+ postType
355
+ );
356
+ const lastPostSave = useLastPostSave( postId, postType );
357
+ const prevPostSave = usePrevious( lastPostSave );
358
+
359
+ useEffect( () => {
360
+ if ( ! lastPostSave ) {
361
+ return;
362
+ }
363
+
364
+ if ( prevPostSave && lastPostSave.savedAt === prevPostSave.savedAt ) {
365
+ return;
366
+ }
367
+
368
+ const saver = activeCollaborators.find(
369
+ ( collaborator ) =>
370
+ collaborator.clientId === lastPostSave.savedByClientId &&
371
+ ! collaborator.isMe
372
+ );
373
+
374
+ if ( ! saver ) {
375
+ return;
376
+ }
377
+
378
+ callback( lastPostSave, saver, prevPostSave ?? null );
379
+ }, [ lastPostSave, prevPostSave, activeCollaborators, callback ] );
380
+ }
package/src/index.js CHANGED
@@ -133,12 +133,6 @@ unlock( store ).registerPrivateSelectors( privateSelectors );
133
133
  unlock( store ).registerPrivateActions( privateActions );
134
134
  register( store ); // Register store after unlocking private selectors to allow resolvers to use them.
135
135
 
136
- /**
137
- * Enums cannot be exported private without losing the ability to narrow types
138
- * based on their values (they blur to string type).
139
- */
140
- export { SelectionType } from './utils/crdt-user-selections';
141
-
142
136
  export { default as EntityProvider } from './entity-provider';
143
137
  export * from './entity-provider';
144
138
  export * from './entity-types';
@@ -103,7 +103,7 @@ export const editMediaEntity =
103
103
  dispatch.receiveEntityRecords(
104
104
  kind,
105
105
  name,
106
- [ newRecord ],
106
+ newRecord,
107
107
  undefined,
108
108
  true,
109
109
  undefined,
@@ -160,3 +160,45 @@ export function receiveEditorAssets( assets ) {
160
160
  assets,
161
161
  };
162
162
  }
163
+
164
+ /**
165
+ * Returns an action object used to set whether collaboration is supported.
166
+ *
167
+ * @param {boolean} supported Whether collaboration is supported.
168
+ *
169
+ * @return {Object} Action object.
170
+ */
171
+ export const setCollaborationSupported =
172
+ ( supported ) =>
173
+ ( { dispatch } ) => {
174
+ dispatch( { type: 'SET_COLLABORATION_SUPPORTED', supported } );
175
+ };
176
+
177
+ /**
178
+ * Returns an action object used to set the sync connection status for an entity or collection.
179
+ *
180
+ * @param {string} kind Kind of the entity.
181
+ * @param {string} name Name of the entity.
182
+ * @param {number|string|null} key The entity key, or null for collections.
183
+ * @param {Object|null} status The connection state object or null on unload.
184
+ *
185
+ * @return {Object} Action object.
186
+ */
187
+ export function setSyncConnectionStatus( kind, name, key, status ) {
188
+ if ( ! status ) {
189
+ return {
190
+ type: 'CLEAR_SYNC_CONNECTION_STATUS',
191
+ kind,
192
+ name,
193
+ key,
194
+ };
195
+ }
196
+
197
+ return {
198
+ type: 'SET_SYNC_CONNECTION_STATUS',
199
+ kind,
200
+ name,
201
+ key,
202
+ status,
203
+ };
204
+ }
@@ -6,13 +6,31 @@ import { RECEIVE_INTERMEDIATE_RESULTS } from './utils';
6
6
  import {
7
7
  useActiveCollaborators,
8
8
  useResolvedSelection,
9
+ useOnCollaboratorJoin,
10
+ useOnCollaboratorLeave,
11
+ useOnPostSave,
9
12
  } from './hooks/use-post-editor-awareness-state';
10
13
  import { lock } from './lock-unlock';
14
+ import { retrySyncConnection } from './sync';
15
+ import {
16
+ SelectionType,
17
+ SelectionDirection,
18
+ } from './utils/crdt-user-selections';
11
19
 
12
- export const privateApis = {};
13
- lock( privateApis, {
20
+ const lockedApis = {
14
21
  useEntityRecordsWithPermissions,
15
22
  RECEIVE_INTERMEDIATE_RESULTS,
23
+ retrySyncConnection,
16
24
  useActiveCollaborators,
17
25
  useResolvedSelection,
18
- } );
26
+ useOnCollaboratorJoin,
27
+ useOnCollaboratorLeave,
28
+ useOnPostSave,
29
+ SelectionType,
30
+ SelectionDirection,
31
+ };
32
+
33
+ export type CoreDataPrivateApis = typeof lockedApis;
34
+
35
+ export const privateApis = {};
36
+ lock( privateApis, lockedApis );
@@ -2,6 +2,7 @@
2
2
  * WordPress dependencies
3
3
  */
4
4
  import { createSelector, createRegistrySelector } from '@wordpress/data';
5
+ import type { ConnectionStatus } from '@wordpress/sync';
5
6
 
6
7
  /**
7
8
  * Internal dependencies
@@ -304,3 +305,45 @@ export function getEditorSettings(
304
305
  export function getEditorAssets( state: State ): Record< string, any > | null {
305
306
  return state.editorAssets;
306
307
  }
308
+
309
+ /**
310
+ * Returns whether collaboration is supported.
311
+ *
312
+ * @param state Data state.
313
+ * @return Whether collaboration is supported.
314
+ */
315
+ export function isCollaborationSupported( state: State ): boolean {
316
+ return state.collaborationSupported;
317
+ }
318
+
319
+ /**
320
+ * Returns the current sync connection status across all entities. Prioritizes
321
+ * disconnected states, then connecting, then connected.
322
+ *
323
+ * @param state Data state.
324
+ *
325
+ * @return The current sync connection state, prioritized by importance.
326
+ */
327
+ export function getSyncConnectionStatus(
328
+ state: State
329
+ ): ConnectionStatus | undefined {
330
+ if ( ! state.syncConnectionStatuses ) {
331
+ return undefined;
332
+ }
333
+
334
+ const PRIORITIZED_STATUSES = [ 'disconnected', 'connecting', 'connected' ];
335
+
336
+ let coalesced: ConnectionStatus | undefined;
337
+
338
+ for ( const status of Object.values( state.syncConnectionStatuses ) ) {
339
+ if (
340
+ ! coalesced ||
341
+ PRIORITIZED_STATUSES.indexOf( status.status ) <
342
+ PRIORITIZED_STATUSES.indexOf( coalesced.status )
343
+ ) {
344
+ coalesced = status;
345
+ }
346
+ }
347
+
348
+ return coalesced;
349
+ }
@@ -10,7 +10,7 @@
10
10
  export function receiveItems( items, edits, meta ) {
11
11
  return {
12
12
  type: 'RECEIVE_ITEMS',
13
- items: Array.isArray( items ) ? items : [ items ],
13
+ items,
14
14
  persistedEdits: edits,
15
15
  meta,
16
16
  };
@@ -105,19 +105,22 @@ export function items( state = {}, action ) {
105
105
  case 'RECEIVE_ITEMS': {
106
106
  const context = getContextFromAction( action );
107
107
  const key = action.key || DEFAULT_ENTITY_KEY;
108
+ const itemsList = Array.isArray( action.items )
109
+ ? action.items
110
+ : [ action.items ];
108
111
  return {
109
112
  ...state,
110
113
  [ context ]: {
111
114
  ...state[ context ],
112
- ...action.items.reduce( ( accumulator, value ) => {
113
- const itemId = value?.[ key ];
114
-
115
- accumulator[ itemId ] = conservativeMapItem(
116
- state?.[ context ]?.[ itemId ],
117
- value
118
- );
119
- return accumulator;
120
- }, {} ),
115
+ ...Object.fromEntries(
116
+ itemsList.map( ( item ) => [
117
+ item?.[ key ],
118
+ conservativeMapItem(
119
+ state?.[ context ]?.[ item?.[ key ] ],
120
+ item
121
+ ),
122
+ ] )
123
+ ),
121
124
  },
122
125
  };
123
126
  }
@@ -149,6 +152,9 @@ export function itemIsComplete( state = {}, action ) {
149
152
  case 'RECEIVE_ITEMS': {
150
153
  const context = getContextFromAction( action );
151
154
  const { query, key = DEFAULT_ENTITY_KEY } = action;
155
+ const itemsList = Array.isArray( action.items )
156
+ ? action.items
157
+ : [ action.items ];
152
158
 
153
159
  // An item is considered complete if it is received without an associated
154
160
  // fields query. Ideally, this would be implemented in such a way where the
@@ -164,7 +170,7 @@ export function itemIsComplete( state = {}, action ) {
164
170
  ...state,
165
171
  [ context ]: {
166
172
  ...state[ context ],
167
- ...action.items.reduce( ( result, item ) => {
173
+ ...itemsList.reduce( ( result, item ) => {
168
174
  const itemId = item?.[ key ];
169
175
 
170
176
  // Defer to completeness if already assigned. Technically the
@@ -224,18 +230,24 @@ const receiveQueries = compose( [
224
230
  // reducer tracks only a single query object.
225
231
  onSubKey( 'stableKey' ),
226
232
  ] )( ( state = {}, action ) => {
227
- const { type, page, perPage, key = DEFAULT_ENTITY_KEY } = action;
233
+ if ( action.type !== 'RECEIVE_ITEMS' ) {
234
+ return state;
235
+ }
228
236
 
229
- if ( type !== 'RECEIVE_ITEMS' ) {
237
+ // Single items don't have page or total count metadata
238
+ // (only collection query responses do), so skip updating itemIds.
239
+ if ( ! Array.isArray( action.items ) ) {
230
240
  return state;
231
241
  }
232
242
 
243
+ const key = action.key ?? DEFAULT_ENTITY_KEY;
244
+
233
245
  return {
234
246
  itemIds: getMergedItemIds(
235
247
  state?.itemIds || [],
236
248
  action.items.map( ( item ) => item?.[ key ] ).filter( Boolean ),
237
- page,
238
- perPage
249
+ action.page,
250
+ action.perPage
239
251
  ),
240
252
  meta: action.meta,
241
253
  };
package/src/reducer.js CHANGED
@@ -16,6 +16,7 @@ import { createUndoManager } from '@wordpress/undo-manager';
16
16
  import { ifMatchingAction, replaceAction } from './utils';
17
17
  import { reducer as queriedDataReducer } from './queried-data';
18
18
  import { rootEntitiesConfig, DEFAULT_ENTITY_KEY } from './entities';
19
+ import { ConnectionErrorCode } from './sync';
19
20
 
20
21
  /** @typedef {import('./types').AnyFunction} AnyFunction */
21
22
 
@@ -214,8 +215,11 @@ function entity( entityConfig ) {
214
215
  }
215
216
 
216
217
  const nextState = { ...state };
218
+ const itemsList = Array.isArray( action.items )
219
+ ? action.items
220
+ : [ action.items ];
217
221
 
218
- for ( const record of action.items ) {
222
+ for ( const record of itemsList ) {
219
223
  const recordId = record?.[ action.key ];
220
224
  const edits = nextState[ recordId ];
221
225
  if ( ! edits ) {
@@ -687,6 +691,36 @@ export function syncConnectionStatuses( state = {}, action ) {
687
691
  return state;
688
692
  }
689
693
 
694
+ /**
695
+ * Reducer managing whether collaboration is supported.
696
+ *
697
+ * Default to true, as collaboration is supported by default
698
+ * unless explicitly disabled due to unsupported conditions
699
+ * such as metaboxes.
700
+ *
701
+ * @param {boolean} state Current state.
702
+ * @param {Object} action Dispatched action.
703
+ *
704
+ * @return {boolean} Updated state.
705
+ */
706
+ export function collaborationSupported( state = true, action ) {
707
+ switch ( action.type ) {
708
+ case 'SET_COLLABORATION_SUPPORTED':
709
+ return action.supported;
710
+
711
+ case 'SET_SYNC_CONNECTION_STATUS':
712
+ if (
713
+ ConnectionErrorCode.DOCUMENT_SIZE_LIMIT_EXCEEDED ===
714
+ action.status?.error?.code
715
+ ) {
716
+ return false;
717
+ }
718
+
719
+ return state;
720
+ }
721
+ return state;
722
+ }
723
+
690
724
  export default combineReducers( {
691
725
  users,
692
726
  currentTheme,
@@ -710,4 +744,5 @@ export default combineReducers( {
710
744
  editorSettings,
711
745
  editorAssets,
712
746
  syncConnectionStatuses,
747
+ collaborationSupported,
713
748
  } );
package/src/resolvers.js CHANGED
@@ -229,12 +229,26 @@ export const getEntityRecord =
229
229
  query
230
230
  );
231
231
  },
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: () => {
232
+ // Persist the CRDT document.
233
+ //
234
+ // TODO: Currently, persisted CRDT documents are stored in post meta.
235
+ // This effectively means that only post entities support CRDT
236
+ // persistence. As we add support for syncing additional entity,
237
+ // we'll need to revisit where persisted CRDT documents are stored.
238
+ persistCRDTDoc: () => {
235
239
  resolveSelect
236
240
  .getEditedEntityRecord( kind, name, key )
237
241
  .then( ( editedRecord ) => {
242
+ // Don't persist the CRDT document if the record is still an
243
+ // auto-draft or if the entity does not support meta.
244
+ const { meta, status } = editedRecord;
245
+ if ( 'auto-draft' === status || ! meta ) {
246
+ return;
247
+ }
248
+
249
+ // Trigger a save to persist the CRDT document. The entity's
250
+ // pre-persist hooks will create the persisted CRDT document
251
+ // and apply it to the record's meta.
238
252
  dispatch.saveEntityRecord(
239
253
  kind,
240
254
  name,
@@ -985,9 +999,11 @@ export const getDefaultTemplateId =
985
999
  template.id = id;
986
1000
  registry.batch( () => {
987
1001
  dispatch.receiveDefaultTemplateId( query, id );
988
- dispatch.receiveEntityRecords( 'postType', template.type, [
989
- template,
990
- ] );
1002
+ dispatch.receiveEntityRecords(
1003
+ 'postType',
1004
+ template.type,
1005
+ template
1006
+ );
991
1007
  // Avoid further network requests.
992
1008
  dispatch.finishResolution( 'getEntityRecord', [
993
1009
  'postType',
@@ -1099,7 +1115,7 @@ export const getRevisions =
1099
1115
  // When requesting all fields, the list of results can be used to
1100
1116
  // resolve the `getRevision` selector in addition to `getRevisions`.
1101
1117
  if ( ! query?._fields && ! query.context ) {
1102
- const key = entityConfig.key || DEFAULT_ENTITY_KEY;
1118
+ const key = entityConfig.revisionKey || DEFAULT_ENTITY_KEY;
1103
1119
  const resolutionsArgs = records
1104
1120
  .filter( ( record ) => record[ key ] )
1105
1121
  .map( ( record ) => [
package/src/selectors.ts CHANGED
@@ -54,6 +54,7 @@ export interface State {
54
54
  editorSettings: Record< string, any > | null;
55
55
  editorAssets: Record< string, any > | null;
56
56
  syncConnectionStatuses?: Record< string, ConnectionStatus >;
57
+ collaborationSupported: boolean;
57
58
  }
58
59
 
59
60
  type EntityRecordKey = string | number;
@@ -1597,35 +1598,3 @@ export const getRevision = createSelector(
1597
1598
  ];
1598
1599
  }
1599
1600
  );
1600
-
1601
- /**
1602
- * Returns the current sync connection status across all entities. Prioritizes
1603
- * disconnected states, then connecting, then connected.
1604
- *
1605
- * @param state Data state.
1606
- *
1607
- * @return The current sync connection state, prioritized by importance.
1608
- */
1609
- export function getSyncConnectionStatus(
1610
- state: State
1611
- ): ConnectionStatus | undefined {
1612
- if ( ! state.syncConnectionStatuses ) {
1613
- return undefined;
1614
- }
1615
-
1616
- const PRIORITIZED_STATUSES = [ 'disconnected', 'connecting', 'connected' ];
1617
-
1618
- let coalesced: ConnectionStatus | undefined;
1619
-
1620
- for ( const status of Object.values( state.syncConnectionStatuses ) ) {
1621
- if (
1622
- ! coalesced ||
1623
- PRIORITIZED_STATUSES.indexOf( status.status ) <
1624
- PRIORITIZED_STATUSES.indexOf( coalesced.status )
1625
- ) {
1626
- coalesced = status;
1627
- }
1628
- }
1629
-
1630
- return coalesced;
1631
- }
package/src/sync.ts CHANGED
@@ -12,20 +12,24 @@ 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,
18
19
  CRDT_RECORD_MAP_KEY,
19
20
  LOCAL_EDITOR_ORIGIN,
20
- WORDPRESS_META_KEY_FOR_CRDT_DOC_PERSISTENCE,
21
+ LOCAL_UNDO_IGNORED_ORIGIN,
22
+ retrySyncConnection,
21
23
  } = unlock( syncPrivateApis );
22
24
 
23
25
  export {
26
+ ConnectionErrorCode,
24
27
  Delta,
25
28
  CRDT_DOC_META_PERSISTENCE_KEY,
26
29
  CRDT_RECORD_MAP_KEY,
27
30
  LOCAL_EDITOR_ORIGIN,
28
- WORDPRESS_META_KEY_FOR_CRDT_DOC_PERSISTENCE,
31
+ LOCAL_UNDO_IGNORED_ORIGIN,
32
+ retrySyncConnection,
29
33
  };
30
34
 
31
35
  let syncManager: SyncManager;