@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.
- package/README.md +0 -27
- package/build/actions.cjs +9 -26
- package/build/actions.cjs.map +2 -2
- package/build/awareness/post-editor-awareness.cjs +45 -6
- package/build/awareness/post-editor-awareness.cjs.map +2 -2
- package/build/awareness/types.cjs.map +1 -1
- package/build/entities.cjs +50 -23
- package/build/entities.cjs.map +2 -2
- package/build/entity-provider.cjs +15 -6
- package/build/entity-provider.cjs.map +2 -2
- package/build/hooks/use-entity-prop.cjs +33 -2
- package/build/hooks/use-entity-prop.cjs.map +2 -2
- package/build/hooks/use-post-editor-awareness-state.cjs +120 -1
- package/build/hooks/use-post-editor-awareness-state.cjs.map +2 -2
- package/build/index.cjs +0 -3
- package/build/index.cjs.map +2 -2
- package/build/private-actions.cjs +27 -3
- package/build/private-actions.cjs.map +2 -2
- package/build/private-apis.cjs +14 -5
- package/build/private-apis.cjs.map +3 -3
- package/build/private-selectors.cjs +22 -2
- package/build/private-selectors.cjs.map +2 -2
- package/build/queried-data/actions.cjs +1 -1
- package/build/queried-data/actions.cjs.map +2 -2
- package/build/queried-data/reducer.cjs +19 -13
- package/build/queried-data/reducer.cjs.map +2 -2
- package/build/reducer.cjs +19 -2
- package/build/reducer.cjs.map +2 -2
- package/build/resolvers.cjs +16 -6
- package/build/resolvers.cjs.map +2 -2
- package/build/selectors.cjs +0 -15
- package/build/selectors.cjs.map +2 -2
- package/build/sync.cjs +11 -5
- package/build/sync.cjs.map +2 -2
- package/build/types.cjs.map +1 -1
- package/build/utils/block-selection-history.cjs +1 -1
- package/build/utils/block-selection-history.cjs.map +2 -2
- package/build/utils/crdt-blocks.cjs +125 -49
- package/build/utils/crdt-blocks.cjs.map +2 -2
- package/build/utils/crdt-selection.cjs +50 -19
- package/build/utils/crdt-selection.cjs.map +2 -2
- package/build/utils/crdt-text.cjs +52 -0
- package/build/utils/crdt-text.cjs.map +7 -0
- package/build/utils/crdt-user-selections.cjs +16 -6
- package/build/utils/crdt-user-selections.cjs.map +3 -3
- package/build/utils/crdt-utils.cjs +54 -2
- package/build/utils/crdt-utils.cjs.map +2 -2
- package/build/utils/crdt.cjs +38 -29
- package/build/utils/crdt.cjs.map +2 -2
- package/build-module/actions.mjs +14 -26
- package/build-module/actions.mjs.map +2 -2
- package/build-module/awareness/post-editor-awareness.mjs +47 -7
- package/build-module/awareness/post-editor-awareness.mjs.map +2 -2
- package/build-module/entities.mjs +53 -24
- package/build-module/entities.mjs.map +2 -2
- package/build-module/entity-provider.mjs +15 -6
- package/build-module/entity-provider.mjs.map +2 -2
- package/build-module/hooks/use-entity-prop.mjs +34 -3
- package/build-module/hooks/use-entity-prop.mjs.map +2 -2
- package/build-module/hooks/use-post-editor-awareness-state.mjs +117 -1
- package/build-module/hooks/use-post-editor-awareness-state.mjs.map +2 -2
- package/build-module/index.mjs +0 -2
- package/build-module/index.mjs.map +2 -2
- package/build-module/private-actions.mjs +24 -2
- package/build-module/private-actions.mjs.map +2 -2
- package/build-module/private-apis.mjs +21 -6
- package/build-module/private-apis.mjs.map +3 -3
- package/build-module/private-selectors.mjs +19 -1
- package/build-module/private-selectors.mjs.map +2 -2
- package/build-module/queried-data/actions.mjs +1 -1
- package/build-module/queried-data/actions.mjs.map +2 -2
- package/build-module/queried-data/reducer.mjs +19 -13
- package/build-module/queried-data/reducer.mjs.map +2 -2
- package/build-module/reducer.mjs +18 -2
- package/build-module/reducer.mjs.map +2 -2
- package/build-module/resolvers.mjs +16 -6
- package/build-module/resolvers.mjs.map +2 -2
- package/build-module/selectors.mjs +0 -14
- package/build-module/selectors.mjs.map +2 -2
- package/build-module/sync.mjs +7 -3
- package/build-module/sync.mjs.map +2 -2
- package/build-module/utils/block-selection-history.mjs +5 -2
- package/build-module/utils/block-selection-history.mjs.map +2 -2
- package/build-module/utils/crdt-blocks.mjs +124 -49
- package/build-module/utils/crdt-blocks.mjs.map +2 -2
- package/build-module/utils/crdt-selection.mjs +53 -20
- package/build-module/utils/crdt-selection.mjs.map +2 -2
- package/build-module/utils/crdt-text.mjs +26 -0
- package/build-module/utils/crdt-text.mjs.map +7 -0
- package/build-module/utils/crdt-user-selections.mjs +16 -7
- package/build-module/utils/crdt-user-selections.mjs.map +3 -3
- package/build-module/utils/crdt-utils.mjs +51 -1
- package/build-module/utils/crdt-utils.mjs.map +2 -2
- package/build-module/utils/crdt.mjs +42 -34
- package/build-module/utils/crdt.mjs.map +2 -2
- package/build-types/actions.d.ts +0 -11
- package/build-types/actions.d.ts.map +1 -1
- package/build-types/awareness/post-editor-awareness.d.ts +2 -2
- package/build-types/awareness/post-editor-awareness.d.ts.map +1 -1
- package/build-types/awareness/types.d.ts +6 -1
- package/build-types/awareness/types.d.ts.map +1 -1
- package/build-types/entities.d.ts +20 -1
- package/build-types/entities.d.ts.map +1 -1
- package/build-types/entity-provider.d.ts +11 -6
- package/build-types/entity-provider.d.ts.map +1 -1
- package/build-types/hooks/use-entity-prop.d.ts.map +1 -1
- package/build-types/hooks/use-post-editor-awareness-state.d.ts +40 -7
- package/build-types/hooks/use-post-editor-awareness-state.d.ts.map +1 -1
- package/build-types/index.d.ts +0 -3
- package/build-types/index.d.ts.map +1 -1
- package/build-types/parsed-blocks-cache.d.ts +10 -0
- package/build-types/parsed-blocks-cache.d.ts.map +1 -0
- package/build-types/private-actions.d.ts +12 -0
- package/build-types/private-actions.d.ts.map +1 -1
- package/build-types/private-apis.d.ts +21 -1
- package/build-types/private-apis.d.ts.map +1 -1
- package/build-types/private-selectors.d.ts +17 -0
- package/build-types/private-selectors.d.ts.map +1 -1
- package/build-types/queried-data/reducer.d.ts.map +1 -1
- package/build-types/reducer.d.ts +15 -0
- package/build-types/reducer.d.ts.map +1 -1
- package/build-types/resolvers.d.ts.map +1 -1
- package/build-types/selectors.d.ts +1 -9
- package/build-types/selectors.d.ts.map +1 -1
- package/build-types/sync.d.ts +2 -2
- package/build-types/sync.d.ts.map +1 -1
- package/build-types/types.d.ts +11 -2
- package/build-types/types.d.ts.map +1 -1
- package/build-types/utils/block-selection-history.d.ts.map +1 -1
- package/build-types/utils/crdt-blocks.d.ts +12 -1
- package/build-types/utils/crdt-blocks.d.ts.map +1 -1
- package/build-types/utils/crdt-selection.d.ts +10 -0
- package/build-types/utils/crdt-selection.d.ts.map +1 -1
- package/build-types/utils/crdt-text.d.ts +16 -0
- package/build-types/utils/crdt-text.d.ts.map +1 -0
- package/build-types/utils/crdt-user-selections.d.ts +17 -4
- package/build-types/utils/crdt-user-selections.d.ts.map +1 -1
- package/build-types/utils/crdt-utils.d.ts +20 -0
- package/build-types/utils/crdt-utils.d.ts.map +1 -1
- package/build-types/utils/crdt.d.ts +14 -7
- package/build-types/utils/crdt.d.ts.map +1 -1
- package/build-types/utils/normalize-query-for-resolution.d.ts +12 -0
- package/build-types/utils/normalize-query-for-resolution.d.ts.map +1 -0
- package/build-types/utils/save-crdt-doc.d.ts +8 -0
- package/build-types/utils/save-crdt-doc.d.ts.map +1 -0
- package/build-types/utils/test/crdt-utils.d.ts +2 -0
- package/build-types/utils/test/crdt-utils.d.ts.map +1 -0
- package/package.json +18 -18
- package/src/actions.js +26 -41
- package/src/awareness/post-editor-awareness.ts +106 -7
- package/src/awareness/test/post-editor-awareness.ts +50 -10
- package/src/awareness/types.ts +7 -1
- package/src/entities.js +62 -24
- package/src/entity-provider.js +24 -11
- package/src/hooks/test/use-post-editor-awareness-state.ts +446 -3
- package/src/hooks/use-entity-prop.js +43 -3
- package/src/hooks/use-post-editor-awareness-state.ts +229 -7
- package/src/index.js +0 -6
- package/src/private-actions.js +43 -1
- package/src/{private-apis.js → private-apis.ts} +21 -3
- package/src/private-selectors.ts +43 -0
- package/src/queried-data/actions.js +1 -1
- package/src/queried-data/reducer.js +26 -14
- package/src/reducer.js +36 -1
- package/src/resolvers.js +23 -7
- package/src/selectors.ts +1 -32
- package/src/sync.ts +6 -2
- package/src/test/entities.js +231 -14
- package/src/test/private-actions.js +1 -1
- package/src/test/resolvers.js +64 -11
- package/src/test/store.js +116 -0
- package/src/types.ts +21 -2
- package/src/utils/block-selection-history.ts +5 -2
- package/src/utils/crdt-blocks.ts +250 -81
- package/src/utils/crdt-selection.ts +92 -26
- package/src/utils/crdt-text.ts +43 -0
- package/src/utils/crdt-user-selections.ts +29 -8
- package/src/utils/crdt-utils.ts +99 -0
- package/src/utils/crdt.ts +72 -46
- package/src/utils/test/crdt-blocks.ts +1177 -22
- package/src/utils/test/crdt-user-selections.ts +5 -0
- package/src/utils/test/crdt-utils.ts +387 -0
- 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
|
-
|
|
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';
|
package/src/private-actions.js
CHANGED
|
@@ -103,7 +103,7 @@ export const editMediaEntity =
|
|
|
103
103
|
dispatch.receiveEntityRecords(
|
|
104
104
|
kind,
|
|
105
105
|
name,
|
|
106
|
-
|
|
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
|
-
|
|
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 );
|
package/src/private-selectors.ts
CHANGED
|
@@ -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
|
+
}
|
|
@@ -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
|
-
...
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
...
|
|
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
|
-
|
|
233
|
+
if ( action.type !== 'RECEIVE_ITEMS' ) {
|
|
234
|
+
return state;
|
|
235
|
+
}
|
|
228
236
|
|
|
229
|
-
|
|
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
|
|
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
|
-
//
|
|
233
|
-
//
|
|
234
|
-
|
|
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(
|
|
989
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
31
|
+
LOCAL_UNDO_IGNORED_ORIGIN,
|
|
32
|
+
retrySyncConnection,
|
|
29
33
|
};
|
|
30
34
|
|
|
31
35
|
let syncManager: SyncManager;
|