@wordpress/core-data 7.48.0 → 7.48.1
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 +7 -1
- package/build/awareness/block-lookup.cjs +14 -26
- package/build/awareness/block-lookup.cjs.map +2 -2
- package/build/awareness/post-editor-awareness.cjs +4 -3
- package/build/awareness/post-editor-awareness.cjs.map +2 -2
- package/build/entities.cjs +4 -2
- package/build/entities.cjs.map +2 -2
- package/build/hooks/use-post-editor-awareness-state.cjs +8 -2
- package/build/hooks/use-post-editor-awareness-state.cjs.map +2 -2
- package/build/private-actions.cjs +8 -0
- package/build/private-actions.cjs.map +2 -2
- package/build/private-selectors.cjs.map +2 -2
- package/build/reducer.cjs +13 -0
- package/build/reducer.cjs.map +2 -2
- package/build/resolvers.cjs +13 -8
- package/build/resolvers.cjs.map +2 -2
- package/build/selectors.cjs +7 -0
- package/build/selectors.cjs.map +2 -2
- package/build/utils/crdt-blocks.cjs +12 -2
- package/build/utils/crdt-blocks.cjs.map +2 -2
- package/build/utils/crdt.cjs +2 -1
- package/build/utils/crdt.cjs.map +2 -2
- package/build/utils/index.cjs +3 -0
- package/build/utils/index.cjs.map +2 -2
- package/build/utils/save-crdt-doc.cjs +75 -0
- package/build/utils/save-crdt-doc.cjs.map +7 -0
- package/build-module/awareness/block-lookup.mjs +13 -26
- package/build-module/awareness/block-lookup.mjs.map +2 -2
- package/build-module/awareness/post-editor-awareness.mjs +4 -3
- package/build-module/awareness/post-editor-awareness.mjs.map +2 -2
- package/build-module/entities.mjs +4 -2
- package/build-module/entities.mjs.map +2 -2
- package/build-module/hooks/use-post-editor-awareness-state.mjs +9 -3
- package/build-module/hooks/use-post-editor-awareness-state.mjs.map +2 -2
- package/build-module/private-actions.mjs +7 -0
- package/build-module/private-actions.mjs.map +2 -2
- package/build-module/private-selectors.mjs.map +2 -2
- package/build-module/reducer.mjs +12 -0
- package/build-module/reducer.mjs.map +2 -2
- package/build-module/resolvers.mjs +15 -9
- package/build-module/resolvers.mjs.map +2 -2
- package/build-module/selectors.mjs +7 -0
- package/build-module/selectors.mjs.map +2 -2
- package/build-module/utils/crdt-blocks.mjs +12 -2
- package/build-module/utils/crdt-blocks.mjs.map +2 -2
- package/build-module/utils/crdt.mjs +2 -1
- package/build-module/utils/crdt.mjs.map +2 -2
- package/build-module/utils/index.mjs +2 -0
- package/build-module/utils/index.mjs.map +2 -2
- package/build-module/utils/save-crdt-doc.mjs +40 -0
- package/build-module/utils/save-crdt-doc.mjs.map +7 -0
- package/build-types/awareness/block-lookup.d.ts +27 -7
- package/build-types/awareness/block-lookup.d.ts.map +1 -1
- package/build-types/awareness/post-editor-awareness.d.ts +3 -1
- package/build-types/awareness/post-editor-awareness.d.ts.map +1 -1
- package/build-types/entities.d.ts.map +1 -1
- package/build-types/hooks/use-post-editor-awareness-state.d.ts.map +1 -1
- package/build-types/private-actions.d.ts +15 -0
- package/build-types/private-actions.d.ts.map +1 -1
- package/build-types/private-selectors.d.ts +0 -12
- package/build-types/private-selectors.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 +4 -0
- package/build-types/selectors.d.ts.map +1 -1
- package/build-types/utils/crdt-blocks.d.ts +5 -1
- package/build-types/utils/crdt-blocks.d.ts.map +1 -1
- package/build-types/utils/crdt.d.ts.map +1 -1
- package/build-types/utils/index.d.ts +1 -0
- package/build-types/utils/index.d.ts.map +1 -1
- package/build-types/utils/on-sub-key.d.ts +4 -0
- package/build-types/utils/on-sub-key.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/package.json +22 -20
- package/src/awareness/block-lookup.ts +21 -62
- package/src/awareness/post-editor-awareness.ts +8 -3
- package/src/awareness/test/block-lookup.ts +98 -94
- package/src/awareness/test/post-editor-awareness.ts +177 -180
- package/src/entities.js +9 -3
- package/src/hooks/test/use-post-editor-awareness-state.ts +10 -2
- package/src/hooks/use-post-editor-awareness-state.ts +20 -7
- package/src/private-actions.js +18 -0
- package/src/private-selectors.ts +0 -12
- package/src/reducer.js +17 -0
- package/src/resolvers.js +20 -13
- package/src/selectors.ts +11 -0
- package/src/test/private-selectors.js +66 -0
- package/src/test/reducer.js +44 -0
- package/src/test/resolvers.js +121 -113
- package/src/test/selectors.js +48 -0
- package/src/utils/crdt-blocks.ts +27 -22
- package/src/utils/crdt.ts +2 -1
- package/src/utils/index.js +1 -0
- package/src/utils/save-crdt-doc.js +64 -0
- package/src/utils/test/crdt-blocks.ts +57 -2
- package/src/utils/test/rtc-rich-text-cursor-scope.test.js +2 -2
- package/src/utils/test/save-crdt-doc.js +185 -0
package/src/test/selectors.js
CHANGED
|
@@ -24,7 +24,55 @@ import {
|
|
|
24
24
|
getRevisions,
|
|
25
25
|
getRevision,
|
|
26
26
|
hasRevision,
|
|
27
|
+
hasUndo,
|
|
28
|
+
hasRedo,
|
|
27
29
|
} from '../selectors';
|
|
30
|
+
import { getSyncManager } from '../sync';
|
|
31
|
+
|
|
32
|
+
jest.mock( '../sync', () => ( {
|
|
33
|
+
getSyncManager: jest.fn(),
|
|
34
|
+
} ) );
|
|
35
|
+
|
|
36
|
+
describe( 'hasUndo/hasRedo', () => {
|
|
37
|
+
afterEach( () => {
|
|
38
|
+
getSyncManager.mockReset();
|
|
39
|
+
} );
|
|
40
|
+
|
|
41
|
+
it( 'reads undo availability from core-data state when a sync undo manager is available', () => {
|
|
42
|
+
const undoManager = {
|
|
43
|
+
hasUndo: jest.fn( () => false ),
|
|
44
|
+
hasRedo: jest.fn( () => false ),
|
|
45
|
+
};
|
|
46
|
+
getSyncManager.mockReturnValue( { undoManager } );
|
|
47
|
+
|
|
48
|
+
const state = deepFreeze( {
|
|
49
|
+
syncUndoManagerState: {
|
|
50
|
+
hasRedo: true,
|
|
51
|
+
hasUndo: true,
|
|
52
|
+
},
|
|
53
|
+
} );
|
|
54
|
+
|
|
55
|
+
expect( hasUndo( state ) ).toBe( true );
|
|
56
|
+
expect( hasRedo( state ) ).toBe( true );
|
|
57
|
+
expect( undoManager.hasUndo ).not.toHaveBeenCalled();
|
|
58
|
+
expect( undoManager.hasRedo ).not.toHaveBeenCalled();
|
|
59
|
+
} );
|
|
60
|
+
|
|
61
|
+
it( 'falls back to the default undo manager when no sync undo manager is available', () => {
|
|
62
|
+
const undoManager = {
|
|
63
|
+
hasUndo: jest.fn( () => true ),
|
|
64
|
+
hasRedo: jest.fn( () => false ),
|
|
65
|
+
};
|
|
66
|
+
getSyncManager.mockReturnValue( undefined );
|
|
67
|
+
|
|
68
|
+
const state = { undoManager };
|
|
69
|
+
|
|
70
|
+
expect( hasUndo( state ) ).toBe( true );
|
|
71
|
+
expect( hasRedo( state ) ).toBe( false );
|
|
72
|
+
expect( undoManager.hasUndo ).toHaveBeenCalled();
|
|
73
|
+
expect( undoManager.hasRedo ).toHaveBeenCalled();
|
|
74
|
+
} );
|
|
75
|
+
} );
|
|
28
76
|
|
|
29
77
|
describe( 'getEntityRecord', () => {
|
|
30
78
|
describe( 'normalizing Post ID passed as recordKey', () => {
|
package/src/utils/crdt-blocks.ts
CHANGED
|
@@ -69,6 +69,10 @@ export type YBlocks = Y.Array< YBlock >;
|
|
|
69
69
|
// Attribute values will be typed as the union of `Y.Text` and `unknown`.
|
|
70
70
|
export type YBlockAttributes = Y.Map< Y.Text | unknown >;
|
|
71
71
|
|
|
72
|
+
interface MergeCrdtBlocksOptions {
|
|
73
|
+
preserveClientIds?: boolean;
|
|
74
|
+
}
|
|
75
|
+
|
|
72
76
|
/**
|
|
73
77
|
* Optional description of where a cursor falls.
|
|
74
78
|
*
|
|
@@ -420,11 +424,13 @@ function createNewYBlock( block: Block ): YBlock {
|
|
|
420
424
|
* @param attributeCursor When provided, describes a selection cursor falling within a
|
|
421
425
|
* RichText field associated with a specific block and attribute.
|
|
422
426
|
* Derived from the changes that produced the blocks.
|
|
427
|
+
* @param options Optional settings for the merge operation.
|
|
423
428
|
*/
|
|
424
429
|
export function mergeCrdtBlocks(
|
|
425
430
|
yblocks: YBlocks,
|
|
426
431
|
incomingBlocks: Block[],
|
|
427
|
-
attributeCursor: MergeCursorPosition
|
|
432
|
+
attributeCursor: MergeCursorPosition,
|
|
433
|
+
options: MergeCrdtBlocksOptions = {}
|
|
428
434
|
): void {
|
|
429
435
|
// Ensure we are working with serializable block data.
|
|
430
436
|
if ( ! serializableBlocksCache.has( incomingBlocks ) ) {
|
|
@@ -594,32 +600,31 @@ export function mergeCrdtBlocks(
|
|
|
594
600
|
mergeCrdtBlocks(
|
|
595
601
|
yInnerBlocks,
|
|
596
602
|
incomingBlockPropertyValue ?? [],
|
|
597
|
-
attributeCursor
|
|
603
|
+
attributeCursor,
|
|
604
|
+
options
|
|
598
605
|
);
|
|
599
606
|
break;
|
|
600
607
|
}
|
|
601
608
|
|
|
602
609
|
case 'clientId': {
|
|
603
|
-
//
|
|
604
|
-
//
|
|
605
|
-
//
|
|
606
|
-
//
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
//
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
// which is consistent with areBlocksEqual ignoring
|
|
622
|
-
// clientId when diffing.
|
|
610
|
+
// Code Editor changes reparse raw HTML on every
|
|
611
|
+
// keystroke and regenerate fresh clientIds. Keep Y.Doc
|
|
612
|
+
// clientIds stable for the code editor so peers do not
|
|
613
|
+
// remount unchanged blocks on every edit.
|
|
614
|
+
if ( options.preserveClientIds ) {
|
|
615
|
+
break;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Otherwise, accept new clientIds from updates
|
|
619
|
+
if (
|
|
620
|
+
incomingBlockPropertyValue !==
|
|
621
|
+
localYBlock.get( incomingBlockProperty )
|
|
622
|
+
) {
|
|
623
|
+
localYBlock.set(
|
|
624
|
+
incomingBlockProperty,
|
|
625
|
+
incomingBlockPropertyValue
|
|
626
|
+
);
|
|
627
|
+
}
|
|
623
628
|
break;
|
|
624
629
|
}
|
|
625
630
|
|
package/src/utils/crdt.ts
CHANGED
package/src/utils/index.js
CHANGED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WordPress dependencies
|
|
3
|
+
*/
|
|
4
|
+
import apiFetch from '@wordpress/api-fetch';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Internal dependencies
|
|
8
|
+
*/
|
|
9
|
+
import { getSyncManager } from '../sync';
|
|
10
|
+
|
|
11
|
+
const SYNC_SAVE_API_PATH = '/wp-sync/v1/save';
|
|
12
|
+
const saveCRDTDocQueues = new Map();
|
|
13
|
+
|
|
14
|
+
async function serializeAndSaveCRDTDoc( objectType, objectId, room ) {
|
|
15
|
+
const serializedDoc = await getSyncManager()?.createPersistedCRDTDoc(
|
|
16
|
+
objectType,
|
|
17
|
+
objectId
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
if ( ! serializedDoc ) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
await apiFetch( {
|
|
25
|
+
path: SYNC_SAVE_API_PATH,
|
|
26
|
+
method: 'POST',
|
|
27
|
+
data: {
|
|
28
|
+
room,
|
|
29
|
+
doc: serializedDoc,
|
|
30
|
+
},
|
|
31
|
+
} );
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Persist the current CRDT document through the sync /save endpoint.
|
|
36
|
+
*
|
|
37
|
+
* @param {import('@wordpress/sync').ObjectType} objectType Object type.
|
|
38
|
+
* @param {import('@wordpress/sync').ObjectID} objectId Object ID.
|
|
39
|
+
*/
|
|
40
|
+
export async function saveCRDTDoc( objectType, objectId ) {
|
|
41
|
+
const room = `${ objectType }:${ objectId }`;
|
|
42
|
+
|
|
43
|
+
// Saves are chained per-room, which forms a queue.
|
|
44
|
+
// Without a queue, two /save calls might fire close together with a risk
|
|
45
|
+
// that the older serialized CRDT snapshot completes after the newer one and
|
|
46
|
+
// overwrites it with stale data.
|
|
47
|
+
// Wait for the prior request chain to complete before firing the next save.
|
|
48
|
+
const previousSave = saveCRDTDocQueues.get( room ) || Promise.resolve();
|
|
49
|
+
|
|
50
|
+
const currentSave = previousSave
|
|
51
|
+
// A failed save should reject its caller, but not block later saves.
|
|
52
|
+
.catch( () => {} )
|
|
53
|
+
.then( () => serializeAndSaveCRDTDoc( objectType, objectId, room ) );
|
|
54
|
+
|
|
55
|
+
saveCRDTDocQueues.set( room, currentSave );
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
await currentSave;
|
|
59
|
+
} finally {
|
|
60
|
+
if ( saveCRDTDocQueues.get( room ) === currentSave ) {
|
|
61
|
+
saveCRDTDocQueues.delete( room );
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -196,7 +196,40 @@ describe( 'crdt-blocks', () => {
|
|
|
196
196
|
expect( content.toString() ).toBe( 'Updated content' );
|
|
197
197
|
} );
|
|
198
198
|
|
|
199
|
-
it( '
|
|
199
|
+
it( 'updates the clientId when an updated block arrives with a different clientId', () => {
|
|
200
|
+
const initialBlocks: Block[] = [
|
|
201
|
+
{
|
|
202
|
+
name: 'core/paragraph',
|
|
203
|
+
attributes: { content: 'Initial content' },
|
|
204
|
+
innerBlocks: [],
|
|
205
|
+
clientId: 'initial-id',
|
|
206
|
+
},
|
|
207
|
+
];
|
|
208
|
+
|
|
209
|
+
mergeCrdtBlocks( yblocks, initialBlocks, null );
|
|
210
|
+
expect( yblocks.get( 0 ).get( 'clientId' ) ).toBe( 'initial-id' );
|
|
211
|
+
|
|
212
|
+
const updatedBlocks: Block[] = [
|
|
213
|
+
{
|
|
214
|
+
name: 'core/paragraph',
|
|
215
|
+
attributes: { content: 'Updated content' },
|
|
216
|
+
innerBlocks: [],
|
|
217
|
+
clientId: 'updated-id',
|
|
218
|
+
},
|
|
219
|
+
];
|
|
220
|
+
|
|
221
|
+
mergeCrdtBlocks( yblocks, updatedBlocks, null );
|
|
222
|
+
|
|
223
|
+
expect( yblocks.length ).toBe( 1 );
|
|
224
|
+
const block = yblocks.get( 0 );
|
|
225
|
+
expect( block.get( 'clientId' ) ).toBe( 'updated-id' );
|
|
226
|
+
const content = (
|
|
227
|
+
block.get( 'attributes' ) as YBlockAttributes
|
|
228
|
+
).get( 'content' ) as Y.Text;
|
|
229
|
+
expect( content.toString() ).toBe( 'Updated content' );
|
|
230
|
+
} );
|
|
231
|
+
|
|
232
|
+
it( 'preserves the local clientId when requested for reparsed content blocks', () => {
|
|
200
233
|
// Simulates the Code Editor flow: the sender re-parses raw HTML on
|
|
201
234
|
// every keystroke, which mints a fresh clientId for every block.
|
|
202
235
|
// The Y.Doc's clientId should stay stable so remote peers don't
|
|
@@ -222,7 +255,9 @@ describe( 'crdt-blocks', () => {
|
|
|
222
255
|
},
|
|
223
256
|
];
|
|
224
257
|
|
|
225
|
-
mergeCrdtBlocks( yblocks, reparsedBlocks, null
|
|
258
|
+
mergeCrdtBlocks( yblocks, reparsedBlocks, null, {
|
|
259
|
+
preserveClientIds: true,
|
|
260
|
+
} );
|
|
226
261
|
|
|
227
262
|
expect( yblocks.length ).toBe( 1 );
|
|
228
263
|
const block = yblocks.get( 0 );
|
|
@@ -2867,6 +2902,26 @@ describe( 'crdt-blocks', () => {
|
|
|
2867
2902
|
} );
|
|
2868
2903
|
} );
|
|
2869
2904
|
|
|
2905
|
+
describe( 'mergeRichTextUpdate - rapid typing', () => {
|
|
2906
|
+
it( 'appends repeated text one character at a time with cursor hints', () => {
|
|
2907
|
+
const text =
|
|
2908
|
+
'987654321098765432109876543210987654321098765432109876543210';
|
|
2909
|
+
const yText = doc.getText( 'test' );
|
|
2910
|
+
yText.insert( 0, 'p1' );
|
|
2911
|
+
|
|
2912
|
+
for ( let i = 1; i <= text.length; i++ ) {
|
|
2913
|
+
const value = `p1${ text.slice( 0, i ) }`;
|
|
2914
|
+
mergeRichTextUpdate(
|
|
2915
|
+
yText,
|
|
2916
|
+
value,
|
|
2917
|
+
asHtmlStringIndex( value.length )
|
|
2918
|
+
);
|
|
2919
|
+
}
|
|
2920
|
+
|
|
2921
|
+
expect( yText.toString() ).toBe( `p1${ text }` );
|
|
2922
|
+
} );
|
|
2923
|
+
} );
|
|
2924
|
+
|
|
2870
2925
|
describe( 'supplementary plane characters (non-emoji)', () => {
|
|
2871
2926
|
// Characters above U+FFFF are stored as surrogate pairs in UTF-16,
|
|
2872
2927
|
// so .length === 2 per character. The diff library v8 counts them
|
|
@@ -254,8 +254,8 @@ describe( 'RTC rich-text cursor scope bug', () => {
|
|
|
254
254
|
},
|
|
255
255
|
'LOCAL_EDITOR_ORIGIN'
|
|
256
256
|
);
|
|
257
|
-
//
|
|
258
|
-
//
|
|
257
|
+
// Selection history writes are deferred. Wait one tick before
|
|
258
|
+
// inspecting the document.
|
|
259
259
|
await waitForNextTick();
|
|
260
260
|
}
|
|
261
261
|
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WordPress dependencies
|
|
3
|
+
*/
|
|
4
|
+
import apiFetch from '@wordpress/api-fetch';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Internal dependencies
|
|
8
|
+
*/
|
|
9
|
+
import { getSyncManager } from '../../sync';
|
|
10
|
+
import { saveCRDTDoc } from '../save-crdt-doc';
|
|
11
|
+
|
|
12
|
+
jest.mock( '@wordpress/api-fetch' );
|
|
13
|
+
jest.mock( '../../sync', () => ( {
|
|
14
|
+
getSyncManager: jest.fn(),
|
|
15
|
+
} ) );
|
|
16
|
+
|
|
17
|
+
function createDeferred() {
|
|
18
|
+
let resolve;
|
|
19
|
+
let reject;
|
|
20
|
+
const promise = new Promise( ( _resolve, _reject ) => {
|
|
21
|
+
resolve = _resolve;
|
|
22
|
+
reject = _reject;
|
|
23
|
+
} );
|
|
24
|
+
|
|
25
|
+
return { promise, resolve, reject };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function flushPromises() {
|
|
29
|
+
await new Promise( ( resolve ) => setTimeout( resolve, 0 ) );
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe( 'saveCRDTDoc', () => {
|
|
33
|
+
let syncManager;
|
|
34
|
+
|
|
35
|
+
beforeEach( () => {
|
|
36
|
+
apiFetch.mockReset();
|
|
37
|
+
syncManager = {
|
|
38
|
+
createPersistedCRDTDoc: jest.fn(),
|
|
39
|
+
};
|
|
40
|
+
getSyncManager.mockReturnValue( syncManager );
|
|
41
|
+
} );
|
|
42
|
+
|
|
43
|
+
it( 'saves the serialized CRDT document through the sync endpoint', async () => {
|
|
44
|
+
const fetch = createDeferred();
|
|
45
|
+
syncManager.createPersistedCRDTDoc.mockResolvedValue( 'doc' );
|
|
46
|
+
apiFetch.mockImplementation( () => fetch.promise );
|
|
47
|
+
|
|
48
|
+
const save = saveCRDTDoc( 'postType/post', 1 );
|
|
49
|
+
|
|
50
|
+
await flushPromises();
|
|
51
|
+
|
|
52
|
+
fetch.resolve( {} );
|
|
53
|
+
await save;
|
|
54
|
+
|
|
55
|
+
expect( apiFetch ).toHaveBeenCalledWith( {
|
|
56
|
+
path: '/wp-sync/v1/save',
|
|
57
|
+
method: 'POST',
|
|
58
|
+
data: {
|
|
59
|
+
room: 'postType/post:1',
|
|
60
|
+
doc: 'doc',
|
|
61
|
+
},
|
|
62
|
+
} );
|
|
63
|
+
} );
|
|
64
|
+
|
|
65
|
+
it( 'does not call the sync endpoint when there is no serialized CRDT document', async () => {
|
|
66
|
+
syncManager.createPersistedCRDTDoc.mockResolvedValue( null );
|
|
67
|
+
|
|
68
|
+
await saveCRDTDoc( 'postType/post', 1 );
|
|
69
|
+
|
|
70
|
+
expect( apiFetch ).not.toHaveBeenCalled();
|
|
71
|
+
} );
|
|
72
|
+
|
|
73
|
+
it( 'serializes save requests for the same room', async () => {
|
|
74
|
+
const firstFetch = createDeferred();
|
|
75
|
+
syncManager.createPersistedCRDTDoc
|
|
76
|
+
.mockResolvedValueOnce( 'doc-1' )
|
|
77
|
+
.mockResolvedValueOnce( 'doc-2' );
|
|
78
|
+
apiFetch
|
|
79
|
+
.mockImplementationOnce( () => firstFetch.promise )
|
|
80
|
+
.mockResolvedValueOnce( {} );
|
|
81
|
+
|
|
82
|
+
const firstSave = saveCRDTDoc( 'postType/post', 1 );
|
|
83
|
+
const secondSave = saveCRDTDoc( 'postType/post', 1 );
|
|
84
|
+
|
|
85
|
+
await flushPromises();
|
|
86
|
+
|
|
87
|
+
expect( syncManager.createPersistedCRDTDoc ).toHaveBeenCalledTimes( 1 );
|
|
88
|
+
expect( apiFetch ).toHaveBeenCalledTimes( 1 );
|
|
89
|
+
expect( apiFetch ).toHaveBeenLastCalledWith( {
|
|
90
|
+
path: '/wp-sync/v1/save',
|
|
91
|
+
method: 'POST',
|
|
92
|
+
data: {
|
|
93
|
+
room: 'postType/post:1',
|
|
94
|
+
doc: 'doc-1',
|
|
95
|
+
},
|
|
96
|
+
} );
|
|
97
|
+
|
|
98
|
+
firstFetch.resolve( {} );
|
|
99
|
+
await firstSave;
|
|
100
|
+
await flushPromises();
|
|
101
|
+
|
|
102
|
+
expect( syncManager.createPersistedCRDTDoc ).toHaveBeenCalledTimes( 2 );
|
|
103
|
+
expect( apiFetch ).toHaveBeenCalledTimes( 2 );
|
|
104
|
+
expect( apiFetch ).toHaveBeenLastCalledWith( {
|
|
105
|
+
path: '/wp-sync/v1/save',
|
|
106
|
+
method: 'POST',
|
|
107
|
+
data: {
|
|
108
|
+
room: 'postType/post:1',
|
|
109
|
+
doc: 'doc-2',
|
|
110
|
+
},
|
|
111
|
+
} );
|
|
112
|
+
|
|
113
|
+
await secondSave;
|
|
114
|
+
} );
|
|
115
|
+
|
|
116
|
+
it( 'does not serialize save requests for different rooms', async () => {
|
|
117
|
+
const firstFetch = createDeferred();
|
|
118
|
+
syncManager.createPersistedCRDTDoc.mockImplementation(
|
|
119
|
+
( objectType, objectId ) => Promise.resolve( `doc-${ objectId }` )
|
|
120
|
+
);
|
|
121
|
+
apiFetch
|
|
122
|
+
.mockImplementationOnce( () => firstFetch.promise )
|
|
123
|
+
.mockResolvedValueOnce( {} );
|
|
124
|
+
|
|
125
|
+
const firstSave = saveCRDTDoc( 'postType/post', 1 );
|
|
126
|
+
const secondSave = saveCRDTDoc( 'postType/post', 2 );
|
|
127
|
+
|
|
128
|
+
await flushPromises();
|
|
129
|
+
|
|
130
|
+
expect( syncManager.createPersistedCRDTDoc ).toHaveBeenCalledTimes( 2 );
|
|
131
|
+
expect( apiFetch ).toHaveBeenCalledTimes( 2 );
|
|
132
|
+
expect( apiFetch ).toHaveBeenNthCalledWith( 1, {
|
|
133
|
+
path: '/wp-sync/v1/save',
|
|
134
|
+
method: 'POST',
|
|
135
|
+
data: {
|
|
136
|
+
room: 'postType/post:1',
|
|
137
|
+
doc: 'doc-1',
|
|
138
|
+
},
|
|
139
|
+
} );
|
|
140
|
+
expect( apiFetch ).toHaveBeenNthCalledWith( 2, {
|
|
141
|
+
path: '/wp-sync/v1/save',
|
|
142
|
+
method: 'POST',
|
|
143
|
+
data: {
|
|
144
|
+
room: 'postType/post:2',
|
|
145
|
+
doc: 'doc-2',
|
|
146
|
+
},
|
|
147
|
+
} );
|
|
148
|
+
|
|
149
|
+
await secondSave;
|
|
150
|
+
firstFetch.resolve( {} );
|
|
151
|
+
await firstSave;
|
|
152
|
+
} );
|
|
153
|
+
|
|
154
|
+
it( 'continues a same-room queue after a failed save', async () => {
|
|
155
|
+
const firstFetch = createDeferred();
|
|
156
|
+
syncManager.createPersistedCRDTDoc
|
|
157
|
+
.mockResolvedValueOnce( 'doc-1' )
|
|
158
|
+
.mockResolvedValueOnce( 'doc-2' );
|
|
159
|
+
apiFetch
|
|
160
|
+
.mockImplementationOnce( () => firstFetch.promise )
|
|
161
|
+
.mockResolvedValueOnce( {} );
|
|
162
|
+
|
|
163
|
+
const firstSave = saveCRDTDoc( 'postType/post', 1 );
|
|
164
|
+
const secondSave = saveCRDTDoc( 'postType/post', 1 );
|
|
165
|
+
|
|
166
|
+
await flushPromises();
|
|
167
|
+
|
|
168
|
+
firstFetch.reject( new Error( 'save failed' ) );
|
|
169
|
+
await expect( firstSave ).rejects.toThrow( 'save failed' );
|
|
170
|
+
await flushPromises();
|
|
171
|
+
|
|
172
|
+
expect( syncManager.createPersistedCRDTDoc ).toHaveBeenCalledTimes( 2 );
|
|
173
|
+
expect( apiFetch ).toHaveBeenCalledTimes( 2 );
|
|
174
|
+
expect( apiFetch ).toHaveBeenLastCalledWith( {
|
|
175
|
+
path: '/wp-sync/v1/save',
|
|
176
|
+
method: 'POST',
|
|
177
|
+
data: {
|
|
178
|
+
room: 'postType/post:1',
|
|
179
|
+
doc: 'doc-2',
|
|
180
|
+
},
|
|
181
|
+
} );
|
|
182
|
+
|
|
183
|
+
await secondSave;
|
|
184
|
+
} );
|
|
185
|
+
} );
|