@wordpress/core-data 7.37.1-next.v.0 → 7.38.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.
- package/CHANGELOG.md +2 -0
- package/build/actions.cjs +14 -2
- package/build/actions.cjs.map +2 -2
- package/build/entities.cjs +35 -45
- package/build/entities.cjs.map +2 -2
- package/build/private-selectors.cjs +2 -4
- package/build/private-selectors.cjs.map +2 -2
- package/build/resolvers.cjs +11 -2
- package/build/resolvers.cjs.map +2 -2
- package/build/types.cjs.map +1 -1
- package/build/utils/crdt-blocks.cjs +63 -41
- package/build/utils/crdt-blocks.cjs.map +2 -2
- package/build/utils/crdt-utils.cjs +44 -0
- package/build/utils/crdt-utils.cjs.map +7 -0
- package/build/utils/crdt.cjs +33 -37
- package/build/utils/crdt.cjs.map +2 -2
- package/build-module/actions.mjs +14 -2
- package/build-module/actions.mjs.map +2 -2
- package/build-module/entities.mjs +35 -47
- package/build-module/entities.mjs.map +2 -2
- package/build-module/private-selectors.mjs +2 -4
- package/build-module/private-selectors.mjs.map +2 -2
- package/build-module/resolvers.mjs +11 -2
- package/build-module/resolvers.mjs.map +2 -2
- package/build-module/utils/crdt-blocks.mjs +64 -42
- package/build-module/utils/crdt-blocks.mjs.map +2 -2
- package/build-module/utils/crdt-utils.mjs +17 -0
- package/build-module/utils/crdt-utils.mjs.map +7 -0
- package/build-module/utils/crdt.mjs +37 -37
- package/build-module/utils/crdt.mjs.map +2 -2
- package/build-types/actions.d.ts.map +1 -1
- package/build-types/entities.d.ts.map +1 -1
- package/build-types/index.d.ts.map +1 -1
- package/build-types/private-selectors.d.ts.map +1 -1
- package/build-types/resolvers.d.ts.map +1 -1
- package/build-types/types.d.ts +0 -5
- package/build-types/types.d.ts.map +1 -1
- package/build-types/utils/crdt-blocks.d.ts +14 -5
- package/build-types/utils/crdt-blocks.d.ts.map +1 -1
- package/build-types/utils/crdt-utils.d.ts +53 -0
- package/build-types/utils/crdt-utils.d.ts.map +1 -0
- package/build-types/utils/crdt.d.ts +19 -1
- package/build-types/utils/crdt.d.ts.map +1 -1
- package/package.json +18 -18
- package/src/actions.js +13 -2
- package/src/entities.js +38 -54
- package/src/private-selectors.ts +3 -4
- package/src/resolvers.js +15 -7
- package/src/test/entities.js +11 -9
- package/src/test/resolvers.js +3 -45
- package/src/types.ts +0 -6
- package/src/utils/crdt-blocks.ts +101 -99
- package/src/utils/crdt-utils.ts +77 -0
- package/src/utils/crdt.ts +76 -57
- package/src/utils/test/crdt.ts +28 -16
package/src/utils/crdt-blocks.ts
CHANGED
|
@@ -7,16 +7,15 @@ import fastDeepEqual from 'fast-deep-equal/es6/index.js';
|
|
|
7
7
|
/**
|
|
8
8
|
* WordPress dependencies
|
|
9
9
|
*/
|
|
10
|
-
import { RichTextData } from '@wordpress/rich-text';
|
|
11
|
-
import { Y } from '@wordpress/sync';
|
|
12
|
-
|
|
13
10
|
// @ts-expect-error No exported types.
|
|
14
11
|
import { getBlockTypes } from '@wordpress/blocks';
|
|
12
|
+
import { RichTextData } from '@wordpress/rich-text';
|
|
13
|
+
import { Y, Delta } from '@wordpress/sync';
|
|
15
14
|
|
|
16
15
|
/**
|
|
17
16
|
* Internal dependencies
|
|
18
17
|
*/
|
|
19
|
-
import type
|
|
18
|
+
import { createYMap, type YMapRecord, type YMapWrap } from './crdt-utils';
|
|
20
19
|
|
|
21
20
|
interface BlockAttributes {
|
|
22
21
|
[ key: string ]: unknown;
|
|
@@ -27,34 +26,33 @@ interface BlockType {
|
|
|
27
26
|
attributes?: Record< string, { type?: string } >;
|
|
28
27
|
}
|
|
29
28
|
|
|
29
|
+
// A block as represented in Gutenberg's data store.
|
|
30
30
|
export interface Block {
|
|
31
31
|
attributes: BlockAttributes;
|
|
32
32
|
clientId?: string;
|
|
33
33
|
innerBlocks: Block[];
|
|
34
|
-
|
|
35
|
-
validationIssues?: string[]; // unserializable
|
|
34
|
+
isValid?: boolean;
|
|
36
35
|
name: string;
|
|
36
|
+
originalContent?: string;
|
|
37
|
+
validationIssues?: string[]; // unserializable
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
>;
|
|
40
|
+
// A block as represented in the CRDT document (Y.Map).
|
|
41
|
+
interface YBlockRecord extends YMapRecord {
|
|
42
|
+
attributes: YBlockAttributes;
|
|
43
|
+
clientId: string;
|
|
44
|
+
innerBlocks: YBlocks;
|
|
45
|
+
isValid?: boolean;
|
|
46
|
+
originalContent?: string;
|
|
47
|
+
name: string;
|
|
48
|
+
}
|
|
49
49
|
|
|
50
|
+
export type YBlock = YMapWrap< YBlockRecord >;
|
|
50
51
|
export type YBlocks = Y.Array< YBlock >;
|
|
51
|
-
export type YBlockAttributes = Y.Map< Y.Text | unknown >;
|
|
52
52
|
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
// accessed -- or type casting with `as`.
|
|
57
|
-
// export type YBlock = Y.Map< Block[ keyof Block ] >;
|
|
53
|
+
// Block attribute schema cannot be known at compile time, so we use Y.Map.
|
|
54
|
+
// Attribute values will be typed as the union of `Y.Text` and `unknown`.
|
|
55
|
+
export type YBlockAttributes = Y.Map< Y.Text | unknown >;
|
|
58
56
|
|
|
59
57
|
const serializableBlocksCache = new WeakMap< WeakKey, Block[] >();
|
|
60
58
|
|
|
@@ -70,13 +68,10 @@ function makeBlockAttributesSerializable(
|
|
|
70
68
|
return newAttributes;
|
|
71
69
|
}
|
|
72
70
|
|
|
73
|
-
function makeBlocksSerializable( blocks: Block[]
|
|
74
|
-
return blocks.map( ( block: Block
|
|
75
|
-
const
|
|
76
|
-
const { name, innerBlocks, attributes, ...rest } = blockAsJson;
|
|
71
|
+
function makeBlocksSerializable( blocks: Block[] ): Block[] {
|
|
72
|
+
return blocks.map( ( block: Block ) => {
|
|
73
|
+
const { name, innerBlocks, attributes, ...rest } = block;
|
|
77
74
|
delete rest.validationIssues;
|
|
78
|
-
delete rest.originalContent;
|
|
79
|
-
// delete rest.isValid
|
|
80
75
|
return {
|
|
81
76
|
...rest,
|
|
82
77
|
name,
|
|
@@ -104,10 +99,10 @@ function areBlocksEqual( gblock: Block, yblock: YBlock ): boolean {
|
|
|
104
99
|
Object.assign( {}, yblockAsJson, overwrites )
|
|
105
100
|
);
|
|
106
101
|
const inners = gblock.innerBlocks || [];
|
|
107
|
-
const yinners = yblock.get( 'innerBlocks' )
|
|
102
|
+
const yinners = yblock.get( 'innerBlocks' );
|
|
108
103
|
return (
|
|
109
104
|
res &&
|
|
110
|
-
inners.length === yinners
|
|
105
|
+
inners.length === yinners?.length &&
|
|
111
106
|
inners.every( ( block: Block, i: number ) =>
|
|
112
107
|
areBlocksEqual( block, yinners.get( i ) )
|
|
113
108
|
)
|
|
@@ -149,35 +144,40 @@ function createNewYAttributeValue(
|
|
|
149
144
|
}
|
|
150
145
|
|
|
151
146
|
function createNewYBlock( block: Block ): YBlock {
|
|
152
|
-
return
|
|
153
|
-
Object.
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
147
|
+
return createYMap< YBlockRecord >(
|
|
148
|
+
Object.fromEntries(
|
|
149
|
+
Object.entries( block ).map( ( [ key, value ] ) => {
|
|
150
|
+
switch ( key ) {
|
|
151
|
+
case 'attributes': {
|
|
152
|
+
return [
|
|
153
|
+
key,
|
|
154
|
+
createNewYAttributeMap( block.name, value ),
|
|
155
|
+
];
|
|
156
|
+
}
|
|
158
157
|
|
|
159
|
-
|
|
160
|
-
|
|
158
|
+
case 'innerBlocks': {
|
|
159
|
+
const innerBlocks = new Y.Array();
|
|
160
|
+
|
|
161
|
+
// If not an array, set to empty Y.Array.
|
|
162
|
+
if ( ! Array.isArray( value ) ) {
|
|
163
|
+
return [ key, innerBlocks ];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
innerBlocks.insert(
|
|
167
|
+
0,
|
|
168
|
+
value.map( ( innerBlock: Block ) =>
|
|
169
|
+
createNewYBlock( innerBlock )
|
|
170
|
+
)
|
|
171
|
+
);
|
|
161
172
|
|
|
162
|
-
// If not an array, set to empty Y.Array.
|
|
163
|
-
if ( ! Array.isArray( value ) ) {
|
|
164
173
|
return [ key, innerBlocks ];
|
|
165
174
|
}
|
|
166
175
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
value.map( ( innerBlock: Block ) =>
|
|
170
|
-
createNewYBlock( innerBlock )
|
|
171
|
-
)
|
|
172
|
-
);
|
|
173
|
-
|
|
174
|
-
return [ key, innerBlocks ];
|
|
176
|
+
default:
|
|
177
|
+
return [ key, value ];
|
|
175
178
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
return [ key, value ];
|
|
179
|
-
}
|
|
180
|
-
} )
|
|
179
|
+
} )
|
|
180
|
+
)
|
|
181
181
|
);
|
|
182
182
|
}
|
|
183
183
|
|
|
@@ -186,13 +186,13 @@ function createNewYBlock( block: Block ): YBlock {
|
|
|
186
186
|
* This function is called to sync local block changes to a shared Y.Doc.
|
|
187
187
|
*
|
|
188
188
|
* @param yblocks The blocks in the local Y.Doc.
|
|
189
|
-
* @param incomingBlocks Gutenberg blocks being synced
|
|
190
|
-
* @param
|
|
189
|
+
* @param incomingBlocks Gutenberg blocks being synced.
|
|
190
|
+
* @param cursorPosition The position of the cursor after the change occurs.
|
|
191
191
|
*/
|
|
192
192
|
export function mergeCrdtBlocks(
|
|
193
193
|
yblocks: YBlocks,
|
|
194
194
|
incomingBlocks: Block[],
|
|
195
|
-
|
|
195
|
+
cursorPosition: number | null
|
|
196
196
|
): void {
|
|
197
197
|
// Ensure we are working with serializable block data.
|
|
198
198
|
if ( ! serializableBlocksCache.has( incomingBlocks ) ) {
|
|
@@ -269,9 +269,7 @@ export function mergeCrdtBlocks(
|
|
|
269
269
|
Object.entries( block ).forEach( ( [ key, value ] ) => {
|
|
270
270
|
switch ( key ) {
|
|
271
271
|
case 'attributes': {
|
|
272
|
-
const currentAttributes = yblock.get(
|
|
273
|
-
key
|
|
274
|
-
) as YBlockAttributes;
|
|
272
|
+
const currentAttributes = yblock.get( key );
|
|
275
273
|
|
|
276
274
|
// If attributes are not set on the yblock, use the new values.
|
|
277
275
|
if ( ! currentAttributes ) {
|
|
@@ -293,6 +291,8 @@ export function mergeCrdtBlocks(
|
|
|
293
291
|
return;
|
|
294
292
|
}
|
|
295
293
|
|
|
294
|
+
const currentAttribute =
|
|
295
|
+
currentAttributes.get( attributeName );
|
|
296
296
|
const isRichText = isRichTextAttribute(
|
|
297
297
|
block.name,
|
|
298
298
|
attributeName
|
|
@@ -302,18 +302,14 @@ export function mergeCrdtBlocks(
|
|
|
302
302
|
isRichText &&
|
|
303
303
|
'string' === typeof attributeValue &&
|
|
304
304
|
currentAttributes.has( attributeName ) &&
|
|
305
|
-
|
|
306
|
-
attributeName
|
|
307
|
-
) instanceof Y.Text
|
|
305
|
+
currentAttribute instanceof Y.Text
|
|
308
306
|
) {
|
|
309
307
|
// Rich text values are stored as persistent Y.Text instances.
|
|
310
308
|
// Update the value with a delta in place.
|
|
311
309
|
mergeRichTextUpdate(
|
|
312
|
-
|
|
313
|
-
attributeName
|
|
314
|
-
) as Y.Text,
|
|
310
|
+
currentAttribute,
|
|
315
311
|
attributeValue,
|
|
316
|
-
|
|
312
|
+
cursorPosition
|
|
317
313
|
);
|
|
318
314
|
} else {
|
|
319
315
|
currentAttributes.set(
|
|
@@ -342,8 +338,18 @@ export function mergeCrdtBlocks(
|
|
|
342
338
|
|
|
343
339
|
case 'innerBlocks': {
|
|
344
340
|
// Recursively merge innerBlocks
|
|
345
|
-
|
|
346
|
-
|
|
341
|
+
let yInnerBlocks = yblock.get( key );
|
|
342
|
+
|
|
343
|
+
if ( ! ( yInnerBlocks instanceof Y.Array ) ) {
|
|
344
|
+
yInnerBlocks = new Y.Array< YBlock >();
|
|
345
|
+
yblock.set( key, yInnerBlocks );
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
mergeCrdtBlocks(
|
|
349
|
+
yInnerBlocks,
|
|
350
|
+
value ?? [],
|
|
351
|
+
cursorPosition
|
|
352
|
+
);
|
|
347
353
|
break;
|
|
348
354
|
}
|
|
349
355
|
|
|
@@ -375,7 +381,11 @@ export function mergeCrdtBlocks(
|
|
|
375
381
|
for ( let j = 0; j < yblocks.length; j++ ) {
|
|
376
382
|
const yblock: YBlock = yblocks.get( j );
|
|
377
383
|
|
|
378
|
-
let clientId
|
|
384
|
+
let clientId = yblock.get( 'clientId' );
|
|
385
|
+
|
|
386
|
+
if ( ! clientId ) {
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
379
389
|
|
|
380
390
|
if ( knownClientIds.has( clientId ) ) {
|
|
381
391
|
clientId = uuidv4();
|
|
@@ -452,22 +462,21 @@ function isRichTextAttribute(
|
|
|
452
462
|
);
|
|
453
463
|
}
|
|
454
464
|
|
|
465
|
+
let localDoc: Y.Doc;
|
|
466
|
+
|
|
455
467
|
/**
|
|
456
468
|
* Given a Y.Text object and an updated string value, diff the new value and
|
|
457
469
|
* apply the delta to the Y.Text.
|
|
458
470
|
*
|
|
459
|
-
* @param blockYText
|
|
460
|
-
* @param updatedValue
|
|
461
|
-
* @param
|
|
471
|
+
* @param blockYText The Y.Text to update.
|
|
472
|
+
* @param updatedValue The updated value.
|
|
473
|
+
* @param cursorPosition The position of the cursor after the change occurs.
|
|
462
474
|
*/
|
|
463
475
|
function mergeRichTextUpdate(
|
|
464
476
|
blockYText: Y.Text,
|
|
465
477
|
updatedValue: string,
|
|
466
|
-
|
|
467
|
-
lastSelection: WPBlockSelection | null
|
|
478
|
+
cursorPosition: number | null
|
|
468
479
|
): void {
|
|
469
|
-
// TODO
|
|
470
|
-
// ====
|
|
471
480
|
// Gutenberg does not use Yjs shared types natively, so we can only subscribe
|
|
472
481
|
// to changes from store and apply them to Yjs types that we create and
|
|
473
482
|
// manage. Crucially, for rich-text attributes, we do not receive granular
|
|
@@ -475,31 +484,24 @@ function mergeRichTextUpdate(
|
|
|
475
484
|
// only a single character changed.
|
|
476
485
|
//
|
|
477
486
|
// The code below allows us to compute a delta between the current and new
|
|
478
|
-
// value, then apply it to the Y.Text.
|
|
479
|
-
// (quill-delta) with a licensing issue that we are working to resolve.
|
|
480
|
-
//
|
|
481
|
-
// For now, we simply replace the full text content on each change.
|
|
482
|
-
//
|
|
483
|
-
// if ( ! localDoc ) {
|
|
484
|
-
// // Y.Text must be attached to a Y.Doc to be able to do operations on it.
|
|
485
|
-
// // Create a temporary Y.Text attached to a local Y.Doc for delta computation.
|
|
486
|
-
// localDoc = new Y.Doc();
|
|
487
|
-
// }
|
|
488
|
-
|
|
489
|
-
// const localYText = localDoc.getText( 'temporary-text' );
|
|
490
|
-
// localYText.delete( 0, localYText.length );
|
|
491
|
-
// localYText.insert( 0, updatedValue );
|
|
487
|
+
// value, then apply it to the Y.Text.
|
|
492
488
|
|
|
493
|
-
|
|
494
|
-
|
|
489
|
+
if ( ! localDoc ) {
|
|
490
|
+
// Y.Text must be attached to a Y.Doc to be able to do operations on it.
|
|
491
|
+
// Create a temporary Y.Text attached to a local Y.Doc for delta computation.
|
|
492
|
+
localDoc = new Y.Doc();
|
|
493
|
+
}
|
|
495
494
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
// );
|
|
495
|
+
const localYText = localDoc.getText( 'temporary-text' );
|
|
496
|
+
localYText.delete( 0, localYText.length );
|
|
497
|
+
localYText.insert( 0, updatedValue );
|
|
500
498
|
|
|
501
|
-
|
|
499
|
+
const currentValueAsDelta = new Delta( blockYText.toDelta() );
|
|
500
|
+
const updatedValueAsDelta = new Delta( localYText.toDelta() );
|
|
501
|
+
const deltaDiff = currentValueAsDelta.diffWithCursor(
|
|
502
|
+
updatedValueAsDelta,
|
|
503
|
+
cursorPosition
|
|
504
|
+
);
|
|
502
505
|
|
|
503
|
-
blockYText.
|
|
504
|
-
blockYText.insert( 0, updatedValue );
|
|
506
|
+
blockYText.applyDelta( deltaDiff.ops );
|
|
505
507
|
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WordPress dependencies
|
|
3
|
+
*/
|
|
4
|
+
import { Y } from '@wordpress/sync';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A YMapRecord represents the shape of the data stored in a Y.Map.
|
|
8
|
+
*/
|
|
9
|
+
export type YMapRecord = Record< string, unknown >;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A wrapper around Y.Map to provide type safety. The generic type accepted by
|
|
13
|
+
* Y.Map represents the union of possible values of the map, which are varied in
|
|
14
|
+
* many cases. This type is accurate, but its non-specificity requires aggressive
|
|
15
|
+
* type narrowing or type casting / destruction with `as`.
|
|
16
|
+
*
|
|
17
|
+
* This type provides type enhancements so that the correct value type can be
|
|
18
|
+
* inferred based on the provided key. It is just a type wrap / overlay, and
|
|
19
|
+
* does not change the runtime behavior of Y.Map.
|
|
20
|
+
*
|
|
21
|
+
* This interface cannot extend Y.Map directly due to the limitations of
|
|
22
|
+
* TypeScript's structural typing. One negative consequence of this is that
|
|
23
|
+
* `instanceof` checks against Y.Map continue to work at runtime but will blur
|
|
24
|
+
* the type at compile time. To navigate this, use the `isYMap` function below.
|
|
25
|
+
*/
|
|
26
|
+
export interface YMapWrap< T extends YMapRecord > extends Y.AbstractType< T > {
|
|
27
|
+
delete: < K extends keyof T >( key: K ) => void;
|
|
28
|
+
forEach: (
|
|
29
|
+
callback: (
|
|
30
|
+
value: T[ keyof T ],
|
|
31
|
+
key: keyof T,
|
|
32
|
+
map: YMapWrap< T >
|
|
33
|
+
) => void
|
|
34
|
+
) => void;
|
|
35
|
+
has: < K extends keyof T >( key: K ) => boolean;
|
|
36
|
+
get: < K extends keyof T >( key: K ) => T[ K ] | undefined;
|
|
37
|
+
set: < K extends keyof T >( key: K, value: T[ K ] ) => void;
|
|
38
|
+
toJSON: () => T;
|
|
39
|
+
// add types for other Y.Map methods as needed
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get or create a root-level Map for the given Y.Doc. Use this instead of
|
|
44
|
+
* doc.getMap() for additional type safety.
|
|
45
|
+
*
|
|
46
|
+
* @param doc Y.Doc
|
|
47
|
+
* @param key Map key
|
|
48
|
+
*/
|
|
49
|
+
export function getRootMap< T extends YMapRecord >(
|
|
50
|
+
doc: Y.Doc,
|
|
51
|
+
key: string
|
|
52
|
+
): YMapWrap< T > {
|
|
53
|
+
return doc.getMap< T >( key ) as unknown as YMapWrap< T >;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Create a new Y.Map (provided with YMapWrap type), optionally initialized with
|
|
58
|
+
* data. Use this instead of `new Y.Map()` for additional type safety.
|
|
59
|
+
*
|
|
60
|
+
* @param partial Partial data to initialize the map with.
|
|
61
|
+
*/
|
|
62
|
+
export function createYMap< T extends YMapRecord >(
|
|
63
|
+
partial: Partial< T > = {}
|
|
64
|
+
): YMapWrap< T > {
|
|
65
|
+
return new Y.Map( Object.entries( partial ) ) as unknown as YMapWrap< T >;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Type guard to check if a value is a Y.Map without losing type information.
|
|
70
|
+
*
|
|
71
|
+
* @param value Value to check.
|
|
72
|
+
*/
|
|
73
|
+
export function isYMap< T extends YMapRecord >(
|
|
74
|
+
value: YMapWrap< T > | undefined
|
|
75
|
+
): value is YMapWrap< T > {
|
|
76
|
+
return value instanceof Y.Map;
|
|
77
|
+
}
|
package/src/utils/crdt.ts
CHANGED
|
@@ -26,8 +26,16 @@ import {
|
|
|
26
26
|
CRDT_RECORD_MAP_KEY,
|
|
27
27
|
WORDPRESS_META_KEY_FOR_CRDT_DOC_PERSISTENCE,
|
|
28
28
|
} from '../sync';
|
|
29
|
-
import type {
|
|
30
|
-
|
|
29
|
+
import type { WPSelection } from '../types';
|
|
30
|
+
import {
|
|
31
|
+
createYMap,
|
|
32
|
+
getRootMap,
|
|
33
|
+
isYMap,
|
|
34
|
+
type YMapRecord,
|
|
35
|
+
type YMapWrap,
|
|
36
|
+
} from './crdt-utils';
|
|
37
|
+
|
|
38
|
+
// Changes that can be applied to a post entity record.
|
|
31
39
|
export type PostChanges = Partial< Post > & {
|
|
32
40
|
blocks?: Block[];
|
|
33
41
|
excerpt?: Post[ 'excerpt' ] | string;
|
|
@@ -35,8 +43,24 @@ export type PostChanges = Partial< Post > & {
|
|
|
35
43
|
title?: Post[ 'title' ] | string;
|
|
36
44
|
};
|
|
37
45
|
|
|
38
|
-
//
|
|
39
|
-
|
|
46
|
+
// A post record as represented in the CRDT document (Y.Map).
|
|
47
|
+
export interface YPostRecord extends YMapRecord {
|
|
48
|
+
author: number;
|
|
49
|
+
blocks: YBlocks;
|
|
50
|
+
comment_status: string;
|
|
51
|
+
date: string | null;
|
|
52
|
+
excerpt: string;
|
|
53
|
+
featured_media: number;
|
|
54
|
+
format: string;
|
|
55
|
+
meta: YMapWrap< YMapRecord >;
|
|
56
|
+
ping_status: string;
|
|
57
|
+
slug: string;
|
|
58
|
+
status: string;
|
|
59
|
+
sticky: boolean;
|
|
60
|
+
tags: number[];
|
|
61
|
+
template: string;
|
|
62
|
+
title: string;
|
|
63
|
+
}
|
|
40
64
|
|
|
41
65
|
// Properties that are allowed to be synced for a post.
|
|
42
66
|
const allowedPostProperties = new Set< string >( [
|
|
@@ -47,8 +71,8 @@ const allowedPostProperties = new Set< string >( [
|
|
|
47
71
|
'excerpt',
|
|
48
72
|
'featured_media',
|
|
49
73
|
'format',
|
|
50
|
-
'ping_status',
|
|
51
74
|
'meta',
|
|
75
|
+
'ping_status',
|
|
52
76
|
'slug',
|
|
53
77
|
'status',
|
|
54
78
|
'sticky',
|
|
@@ -74,7 +98,7 @@ export function defaultApplyChangesToCRDTDoc(
|
|
|
74
98
|
ydoc: CRDTDoc,
|
|
75
99
|
changes: ObjectData
|
|
76
100
|
): void {
|
|
77
|
-
const ymap =
|
|
101
|
+
const ymap = getRootMap( ydoc, CRDT_RECORD_MAP_KEY );
|
|
78
102
|
|
|
79
103
|
Object.entries( changes ).forEach( ( [ key, newValue ] ) => {
|
|
80
104
|
// Cannot serialize function values, so cannot sync them.
|
|
@@ -82,17 +106,12 @@ export function defaultApplyChangesToCRDTDoc(
|
|
|
82
106
|
return;
|
|
83
107
|
}
|
|
84
108
|
|
|
85
|
-
// Set the value in the root document.
|
|
86
|
-
function setValue< T = unknown >( updatedValue: T ): void {
|
|
87
|
-
ymap.set( key, updatedValue );
|
|
88
|
-
}
|
|
89
|
-
|
|
90
109
|
switch ( key ) {
|
|
91
110
|
// Add support for additional data types here.
|
|
92
111
|
|
|
93
112
|
default: {
|
|
94
113
|
const currentValue = ymap.get( key );
|
|
95
|
-
|
|
114
|
+
updateMapValue( ymap, key, currentValue, newValue );
|
|
96
115
|
}
|
|
97
116
|
}
|
|
98
117
|
} );
|
|
@@ -112,60 +131,60 @@ export function applyPostChangesToCRDTDoc(
|
|
|
112
131
|
changes: PostChanges,
|
|
113
132
|
_postType: Type // eslint-disable-line @typescript-eslint/no-unused-vars
|
|
114
133
|
): void {
|
|
115
|
-
const ymap =
|
|
134
|
+
const ymap = getRootMap< YPostRecord >( ydoc, CRDT_RECORD_MAP_KEY );
|
|
116
135
|
|
|
117
|
-
Object.
|
|
136
|
+
Object.keys( changes ).forEach( ( key ) => {
|
|
118
137
|
if ( ! allowedPostProperties.has( key ) ) {
|
|
119
138
|
return;
|
|
120
139
|
}
|
|
121
140
|
|
|
141
|
+
const newValue = changes[ key ];
|
|
142
|
+
|
|
122
143
|
// Cannot serialize function values, so cannot sync them.
|
|
123
144
|
if ( 'function' === typeof newValue ) {
|
|
124
145
|
return;
|
|
125
146
|
}
|
|
126
147
|
|
|
127
|
-
// Set the value in the root document.
|
|
128
|
-
function setValue< T = unknown >( updatedValue: T ): void {
|
|
129
|
-
ymap.set( key, updatedValue );
|
|
130
|
-
}
|
|
131
|
-
|
|
132
148
|
switch ( key ) {
|
|
133
149
|
case 'blocks': {
|
|
134
|
-
let currentBlocks = ymap.get(
|
|
150
|
+
let currentBlocks = ymap.get( key );
|
|
135
151
|
|
|
136
152
|
// Initialize.
|
|
137
153
|
if ( ! ( currentBlocks instanceof Y.Array ) ) {
|
|
138
154
|
currentBlocks = new Y.Array< YBlock >();
|
|
139
|
-
|
|
155
|
+
ymap.set( key, currentBlocks );
|
|
140
156
|
}
|
|
141
157
|
|
|
142
158
|
// Block[] from local changes.
|
|
143
159
|
const newBlocks = ( newValue as PostChanges[ 'blocks' ] ) ?? [];
|
|
144
160
|
|
|
161
|
+
// Block changes from typing are bundled with a 'selection' update.
|
|
162
|
+
// Pass the resulting cursor position to the mergeCrdtBlocks function.
|
|
163
|
+
const cursorPosition =
|
|
164
|
+
changes.selection?.selectionStart?.offset ?? null;
|
|
165
|
+
|
|
145
166
|
// Merge blocks does not need `setValue` because it is operating on a
|
|
146
167
|
// Yjs type that is already in the Y.Doc.
|
|
147
|
-
mergeCrdtBlocks( currentBlocks, newBlocks,
|
|
168
|
+
mergeCrdtBlocks( currentBlocks, newBlocks, cursorPosition );
|
|
148
169
|
break;
|
|
149
170
|
}
|
|
150
171
|
|
|
151
172
|
case 'excerpt': {
|
|
152
|
-
const currentValue = ymap.get( 'excerpt' )
|
|
153
|
-
| string
|
|
154
|
-
| undefined;
|
|
173
|
+
const currentValue = ymap.get( 'excerpt' );
|
|
155
174
|
const rawNewValue = getRawValue( newValue );
|
|
156
175
|
|
|
157
|
-
|
|
176
|
+
updateMapValue( ymap, key, currentValue, rawNewValue );
|
|
158
177
|
break;
|
|
159
178
|
}
|
|
160
179
|
|
|
161
180
|
// "Meta" is overloaded term; here, it refers to post meta.
|
|
162
181
|
case 'meta': {
|
|
163
|
-
let metaMap = ymap.get( 'meta' )
|
|
182
|
+
let metaMap = ymap.get( 'meta' );
|
|
164
183
|
|
|
165
184
|
// Initialize.
|
|
166
|
-
if ( ! ( metaMap
|
|
167
|
-
metaMap =
|
|
168
|
-
|
|
185
|
+
if ( ! isYMap( metaMap ) ) {
|
|
186
|
+
metaMap = createYMap< YMapRecord >();
|
|
187
|
+
ymap.set( 'meta', metaMap );
|
|
169
188
|
}
|
|
170
189
|
|
|
171
190
|
// Iterate over each meta property in the new value and merge it if it
|
|
@@ -176,12 +195,11 @@ export function applyPostChangesToCRDTDoc(
|
|
|
176
195
|
return;
|
|
177
196
|
}
|
|
178
197
|
|
|
179
|
-
|
|
198
|
+
updateMapValue(
|
|
199
|
+
metaMap,
|
|
200
|
+
metaKey,
|
|
180
201
|
metaMap.get( metaKey ), // current value in CRDT
|
|
181
|
-
metaValue
|
|
182
|
-
( updatedMetaValue: unknown ): void => {
|
|
183
|
-
metaMap.set( metaKey, updatedMetaValue );
|
|
184
|
-
}
|
|
202
|
+
metaValue // new value from changes
|
|
185
203
|
);
|
|
186
204
|
}
|
|
187
205
|
);
|
|
@@ -195,13 +213,13 @@ export function applyPostChangesToCRDTDoc(
|
|
|
195
213
|
break;
|
|
196
214
|
}
|
|
197
215
|
|
|
198
|
-
const currentValue = ymap.get(
|
|
199
|
-
|
|
216
|
+
const currentValue = ymap.get( key );
|
|
217
|
+
updateMapValue( ymap, key, currentValue, newValue );
|
|
200
218
|
break;
|
|
201
219
|
}
|
|
202
220
|
|
|
203
221
|
case 'title': {
|
|
204
|
-
const currentValue = ymap.get(
|
|
222
|
+
const currentValue = ymap.get( key );
|
|
205
223
|
|
|
206
224
|
// Copy logic from prePersistPostType to ensure that the "Auto
|
|
207
225
|
// Draft" template title is not synced.
|
|
@@ -210,27 +228,22 @@ export function applyPostChangesToCRDTDoc(
|
|
|
210
228
|
rawNewValue = '';
|
|
211
229
|
}
|
|
212
230
|
|
|
213
|
-
|
|
231
|
+
updateMapValue( ymap, key, currentValue, rawNewValue );
|
|
214
232
|
break;
|
|
215
233
|
}
|
|
216
234
|
|
|
217
|
-
// Add support for additional
|
|
235
|
+
// Add support for additional properties here.
|
|
218
236
|
|
|
219
237
|
default: {
|
|
220
238
|
const currentValue = ymap.get( key );
|
|
221
|
-
|
|
239
|
+
updateMapValue( ymap, key, currentValue, newValue );
|
|
222
240
|
}
|
|
223
241
|
}
|
|
224
242
|
} );
|
|
225
|
-
|
|
226
|
-
// Update the lastSelection for use in computing Y.Text deltas.
|
|
227
|
-
if ( 'selection' in changes ) {
|
|
228
|
-
lastSelection = changes.selection?.selectionStart ?? null;
|
|
229
|
-
}
|
|
230
243
|
}
|
|
231
244
|
|
|
232
245
|
export function defaultGetChangesFromCRDTDoc( crdtDoc: CRDTDoc ): ObjectData {
|
|
233
|
-
return
|
|
246
|
+
return getRootMap( crdtDoc, CRDT_RECORD_MAP_KEY ).toJSON();
|
|
234
247
|
}
|
|
235
248
|
|
|
236
249
|
/**
|
|
@@ -248,7 +261,7 @@ export function getPostChangesFromCRDTDoc(
|
|
|
248
261
|
editedRecord: Post,
|
|
249
262
|
_postType: Type // eslint-disable-line @typescript-eslint/no-unused-vars
|
|
250
263
|
): PostChanges {
|
|
251
|
-
const ymap =
|
|
264
|
+
const ymap = getRootMap< YPostRecord >( ydoc, CRDT_RECORD_MAP_KEY );
|
|
252
265
|
|
|
253
266
|
let allowedMetaChanges: Post[ 'meta' ] = {};
|
|
254
267
|
|
|
@@ -394,19 +407,25 @@ function getRawValue( value?: unknown ): string | undefined {
|
|
|
394
407
|
return undefined;
|
|
395
408
|
}
|
|
396
409
|
|
|
397
|
-
function haveValuesChanged< ValueType
|
|
398
|
-
currentValue: ValueType,
|
|
399
|
-
newValue: ValueType
|
|
410
|
+
function haveValuesChanged< ValueType >(
|
|
411
|
+
currentValue: ValueType | undefined,
|
|
412
|
+
newValue: ValueType | undefined
|
|
400
413
|
): boolean {
|
|
401
414
|
return ! fastDeepEqual( currentValue, newValue );
|
|
402
415
|
}
|
|
403
416
|
|
|
404
|
-
function
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
417
|
+
function updateMapValue< T extends YMapRecord, K extends keyof T >(
|
|
418
|
+
map: YMapWrap< T >,
|
|
419
|
+
key: K,
|
|
420
|
+
currentValue: T[ K ] | undefined,
|
|
421
|
+
newValue: T[ K ] | undefined
|
|
408
422
|
): void {
|
|
409
|
-
if (
|
|
410
|
-
|
|
423
|
+
if ( undefined === newValue ) {
|
|
424
|
+
map.delete( key );
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if ( haveValuesChanged< T[ K ] >( currentValue, newValue ) ) {
|
|
429
|
+
map.set( key, newValue );
|
|
411
430
|
}
|
|
412
431
|
}
|