@wordpress/core-data 7.39.1-next.v.202602091733.0 → 7.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/README.md +41 -0
  3. package/build/actions.cjs +52 -0
  4. package/build/actions.cjs.map +2 -2
  5. package/build/awareness/base-awareness.cjs +1 -8
  6. package/build/awareness/base-awareness.cjs.map +2 -2
  7. package/build/awareness/types.cjs.map +1 -1
  8. package/build/awareness/utils.cjs +8 -51
  9. package/build/awareness/utils.cjs.map +2 -2
  10. package/build/entities.cjs +7 -1
  11. package/build/entities.cjs.map +2 -2
  12. package/build/hooks/use-entity-block-editor.cjs +13 -19
  13. package/build/hooks/use-entity-block-editor.cjs.map +2 -2
  14. package/build/index.cjs +6 -1
  15. package/build/index.cjs.map +2 -2
  16. package/build/private-actions.cjs +8 -0
  17. package/build/private-actions.cjs.map +2 -2
  18. package/build/private-apis.cjs +2 -1
  19. package/build/private-apis.cjs.map +2 -2
  20. package/build/private-selectors.cjs +5 -0
  21. package/build/private-selectors.cjs.map +2 -2
  22. package/build/reducer.cjs +31 -1
  23. package/build/reducer.cjs.map +2 -2
  24. package/build/resolvers.cjs +26 -1
  25. package/build/resolvers.cjs.map +2 -2
  26. package/build/selectors.cjs +15 -0
  27. package/build/selectors.cjs.map +2 -2
  28. package/build/utils/crdt-blocks.cjs +5 -3
  29. package/build/utils/crdt-blocks.cjs.map +2 -2
  30. package/build/utils/crdt.cjs +23 -19
  31. package/build/utils/crdt.cjs.map +2 -2
  32. package/build-module/actions.mjs +50 -0
  33. package/build-module/actions.mjs.map +2 -2
  34. package/build-module/awareness/base-awareness.mjs +1 -8
  35. package/build-module/awareness/base-awareness.mjs.map +2 -2
  36. package/build-module/awareness/utils.mjs +8 -51
  37. package/build-module/awareness/utils.mjs.map +2 -2
  38. package/build-module/entities.mjs +7 -1
  39. package/build-module/entities.mjs.map +2 -2
  40. package/build-module/hooks/use-entity-block-editor.mjs +13 -19
  41. package/build-module/hooks/use-entity-block-editor.mjs.map +2 -2
  42. package/build-module/index.mjs +3 -0
  43. package/build-module/index.mjs.map +2 -2
  44. package/build-module/private-actions.mjs +7 -0
  45. package/build-module/private-actions.mjs.map +2 -2
  46. package/build-module/private-apis.mjs +6 -2
  47. package/build-module/private-apis.mjs.map +2 -2
  48. package/build-module/private-selectors.mjs +8 -1
  49. package/build-module/private-selectors.mjs.map +2 -2
  50. package/build-module/reducer.mjs +29 -1
  51. package/build-module/reducer.mjs.map +2 -2
  52. package/build-module/resolvers.mjs +25 -1
  53. package/build-module/resolvers.mjs.map +2 -2
  54. package/build-module/selectors.mjs +14 -0
  55. package/build-module/selectors.mjs.map +2 -2
  56. package/build-module/utils/crdt-blocks.mjs +3 -2
  57. package/build-module/utils/crdt-blocks.mjs.map +2 -2
  58. package/build-module/utils/crdt.mjs +25 -20
  59. package/build-module/utils/crdt.mjs.map +2 -2
  60. package/build-types/actions.d.ts +12 -0
  61. package/build-types/actions.d.ts.map +1 -1
  62. package/build-types/awareness/base-awareness.d.ts.map +1 -1
  63. package/build-types/awareness/test/awareness-state.d.ts +2 -0
  64. package/build-types/awareness/test/awareness-state.d.ts.map +1 -0
  65. package/build-types/awareness/test/base-awareness.d.ts +2 -0
  66. package/build-types/awareness/test/base-awareness.d.ts.map +1 -0
  67. package/build-types/awareness/test/post-editor-awareness.d.ts +2 -0
  68. package/build-types/awareness/test/post-editor-awareness.d.ts.map +1 -0
  69. package/build-types/awareness/test/typed-awareness.d.ts +2 -0
  70. package/build-types/awareness/test/typed-awareness.d.ts.map +1 -0
  71. package/build-types/awareness/test/utils.d.ts +2 -0
  72. package/build-types/awareness/test/utils.d.ts.map +1 -0
  73. package/build-types/awareness/types.d.ts +0 -1
  74. package/build-types/awareness/types.d.ts.map +1 -1
  75. package/build-types/awareness/utils.d.ts +2 -3
  76. package/build-types/awareness/utils.d.ts.map +1 -1
  77. package/build-types/entities.d.ts.map +1 -1
  78. package/build-types/hooks/test/use-post-editor-awareness-state.d.ts +2 -0
  79. package/build-types/hooks/test/use-post-editor-awareness-state.d.ts.map +1 -0
  80. package/build-types/hooks/use-entity-block-editor.d.ts.map +1 -1
  81. package/build-types/index.d.ts +5 -0
  82. package/build-types/index.d.ts.map +1 -1
  83. package/build-types/private-actions.d.ts +8 -0
  84. package/build-types/private-actions.d.ts.map +1 -1
  85. package/build-types/private-apis.d.ts.map +1 -1
  86. package/build-types/private-selectors.d.ts +8 -1
  87. package/build-types/private-selectors.d.ts.map +1 -1
  88. package/build-types/reducer.d.ts +23 -0
  89. package/build-types/reducer.d.ts.map +1 -1
  90. package/build-types/resolvers.d.ts +3 -0
  91. package/build-types/resolvers.d.ts.map +1 -1
  92. package/build-types/selectors.d.ts +17 -0
  93. package/build-types/selectors.d.ts.map +1 -1
  94. package/build-types/utils/crdt-blocks.d.ts +9 -0
  95. package/build-types/utils/crdt-blocks.d.ts.map +1 -1
  96. package/build-types/utils/crdt.d.ts +6 -4
  97. package/build-types/utils/crdt.d.ts.map +1 -1
  98. package/package.json +18 -18
  99. package/src/actions.js +78 -0
  100. package/src/awareness/base-awareness.ts +1 -14
  101. package/src/awareness/test/awareness-state.ts +417 -0
  102. package/src/awareness/test/base-awareness.ts +321 -0
  103. package/src/awareness/test/post-editor-awareness.ts +561 -0
  104. package/src/awareness/test/typed-awareness.ts +148 -0
  105. package/src/awareness/test/utils.ts +305 -0
  106. package/src/awareness/types.ts +0 -1
  107. package/src/awareness/utils.ts +8 -82
  108. package/src/entities.js +7 -1
  109. package/src/hooks/test/use-post-editor-awareness-state.ts +477 -0
  110. package/src/hooks/use-entity-block-editor.js +15 -21
  111. package/src/index.js +7 -0
  112. package/src/private-actions.js +14 -0
  113. package/src/private-apis.js +5 -1
  114. package/src/private-selectors.ts +16 -1
  115. package/src/reducer.js +45 -0
  116. package/src/resolvers.js +31 -2
  117. package/src/selectors.ts +41 -0
  118. package/src/test/actions.js +79 -0
  119. package/src/test/entity-provider.js +74 -0
  120. package/src/test/resolvers.js +2 -0
  121. package/src/test/store.js +30 -0
  122. package/src/utils/crdt-blocks.ts +2 -2
  123. package/src/utils/crdt.ts +44 -29
  124. package/src/utils/test/crdt.ts +212 -7
@@ -0,0 +1,477 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { act, renderHook, waitFor } from '@testing-library/react';
5
+
6
+ /**
7
+ * Internal dependencies
8
+ */
9
+ import {
10
+ useActiveCollaborators,
11
+ useGetAbsolutePositionIndex,
12
+ useGetDebugData,
13
+ useIsDisconnected,
14
+ } from '../use-post-editor-awareness-state';
15
+ import { getSyncManager } from '../../sync';
16
+ import { SelectionType } from '../../utils/crdt-user-selections';
17
+ import type {
18
+ PostEditorAwarenessState,
19
+ YDocDebugData,
20
+ } from '../../awareness/types';
21
+ import type { SelectionCursor } from '../../types';
22
+
23
+ // Mock the sync module
24
+ jest.mock( '../../sync', () => ( {
25
+ getSyncManager: jest.fn(),
26
+ } ) );
27
+
28
+ const mockAvatarUrls = {
29
+ '24': 'https://example.com/avatar-24.png',
30
+ '48': 'https://example.com/avatar-48.png',
31
+ '96': 'https://example.com/avatar-96.png',
32
+ };
33
+
34
+ const createMockActiveUser = (
35
+ overrides: Partial< PostEditorAwarenessState > = {}
36
+ ): PostEditorAwarenessState => ( {
37
+ clientId: 12345,
38
+ isMe: false,
39
+ isConnected: true,
40
+ collaboratorInfo: {
41
+ id: 1,
42
+ name: 'Test User',
43
+ slug: 'test-user',
44
+ avatar_urls: mockAvatarUrls,
45
+ browserType: 'Chrome',
46
+ enteredAt: 1704067200000,
47
+ },
48
+ editorState: {
49
+ selection: {
50
+ type: SelectionType.None,
51
+ },
52
+ },
53
+ ...overrides,
54
+ } );
55
+
56
+ const createMockDebugData = (): YDocDebugData => ( {
57
+ doc: { testKey: 'testValue' },
58
+ clients: {},
59
+ collaboratorMap: {},
60
+ } );
61
+
62
+ describe( 'use-post-editor-awareness-state hooks', () => {
63
+ let mockAwareness: {
64
+ setUp: jest.Mock;
65
+ getCurrentState: jest.Mock;
66
+ onStateChange: jest.Mock;
67
+ getAbsolutePositionIndex: jest.Mock;
68
+ getDebugData: jest.Mock;
69
+ };
70
+ let mockSyncManager: {
71
+ getAwareness: jest.Mock;
72
+ };
73
+ let stateChangeCallback:
74
+ | ( ( newState: PostEditorAwarenessState[] ) => void )
75
+ | null;
76
+
77
+ beforeEach( () => {
78
+ stateChangeCallback = null;
79
+
80
+ mockAwareness = {
81
+ setUp: jest.fn(),
82
+ getCurrentState: jest.fn().mockReturnValue( [] ),
83
+ onStateChange: jest.fn( ( callback ) => {
84
+ stateChangeCallback = callback;
85
+ return jest.fn(); // unsubscribe function
86
+ } ),
87
+ getAbsolutePositionIndex: jest.fn().mockReturnValue( null ),
88
+ getDebugData: jest.fn().mockReturnValue( createMockDebugData() ),
89
+ };
90
+
91
+ mockSyncManager = {
92
+ getAwareness: jest.fn().mockReturnValue( mockAwareness ),
93
+ };
94
+
95
+ ( getSyncManager as jest.Mock ).mockReturnValue( mockSyncManager );
96
+ } );
97
+
98
+ afterEach( () => {
99
+ jest.clearAllMocks();
100
+ } );
101
+
102
+ describe( 'useActiveUsers', () => {
103
+ test( 'should return empty array when postId is null', () => {
104
+ const { result } = renderHook( () =>
105
+ useActiveCollaborators( null, 'post' )
106
+ );
107
+
108
+ expect( result.current ).toEqual( [] );
109
+ expect( mockSyncManager.getAwareness ).not.toHaveBeenCalled();
110
+ } );
111
+
112
+ test( 'should return empty array when postType is null', () => {
113
+ const { result } = renderHook( () =>
114
+ useActiveCollaborators( 123, null )
115
+ );
116
+
117
+ expect( result.current ).toEqual( [] );
118
+ expect( mockSyncManager.getAwareness ).not.toHaveBeenCalled();
119
+ } );
120
+
121
+ test( 'should return empty array when getSyncManager returns undefined', () => {
122
+ ( getSyncManager as jest.Mock ).mockReturnValue( undefined );
123
+
124
+ const { result } = renderHook( () =>
125
+ useActiveCollaborators( 123, 'post' )
126
+ );
127
+
128
+ expect( result.current ).toEqual( [] );
129
+ } );
130
+
131
+ test( 'should return empty array when awareness is not available', () => {
132
+ mockSyncManager.getAwareness.mockReturnValue( undefined );
133
+
134
+ const { result } = renderHook( () =>
135
+ useActiveCollaborators( 123, 'post' )
136
+ );
137
+
138
+ expect( result.current ).toEqual( [] );
139
+ } );
140
+
141
+ test( 'should call getAwareness with correct parameters', () => {
142
+ renderHook( () => useActiveCollaborators( 123, 'post' ) );
143
+
144
+ expect( mockSyncManager.getAwareness ).toHaveBeenCalledWith(
145
+ 'postType/post',
146
+ '123'
147
+ );
148
+ } );
149
+
150
+ test( 'should call awareness.setUp', () => {
151
+ renderHook( () => useActiveCollaborators( 123, 'post' ) );
152
+
153
+ expect( mockAwareness.setUp ).toHaveBeenCalled();
154
+ } );
155
+
156
+ test( 'should return initial state from getCurrentState', () => {
157
+ const mockUsers = [ createMockActiveUser() ];
158
+ mockAwareness.getCurrentState.mockReturnValue( mockUsers );
159
+
160
+ const { result } = renderHook( () =>
161
+ useActiveCollaborators( 123, 'post' )
162
+ );
163
+
164
+ expect( result.current ).toEqual( mockUsers );
165
+ } );
166
+
167
+ test( 'should subscribe to state changes', () => {
168
+ renderHook( () => useActiveCollaborators( 123, 'post' ) );
169
+
170
+ expect( mockAwareness.onStateChange ).toHaveBeenCalled();
171
+ } );
172
+
173
+ test( 'should update state when awareness emits changes', async () => {
174
+ const initialUsers: PostEditorAwarenessState[] = [];
175
+ const updatedUsers = [ createMockActiveUser() ];
176
+
177
+ mockAwareness.getCurrentState.mockReturnValue( initialUsers );
178
+
179
+ const { result } = renderHook( () =>
180
+ useActiveCollaborators( 123, 'post' )
181
+ );
182
+
183
+ expect( result.current ).toEqual( initialUsers );
184
+
185
+ // Simulate awareness state change
186
+ act( () => {
187
+ stateChangeCallback?.( updatedUsers );
188
+ } );
189
+
190
+ await waitFor( () => {
191
+ expect( result.current ).toEqual( updatedUsers );
192
+ } );
193
+ } );
194
+
195
+ test( 'should unsubscribe when postId changes', () => {
196
+ const unsubscribe = jest.fn();
197
+ mockAwareness.onStateChange.mockReturnValue( unsubscribe );
198
+
199
+ const { rerender } = renderHook(
200
+ ( { postId } ) => useActiveCollaborators( postId, 'post' ),
201
+ { initialProps: { postId: 123 as number | null } }
202
+ );
203
+
204
+ expect( unsubscribe ).not.toHaveBeenCalled();
205
+
206
+ rerender( { postId: 456 } );
207
+
208
+ expect( unsubscribe ).toHaveBeenCalled();
209
+ } );
210
+
211
+ test( 'should unsubscribe when postType changes', () => {
212
+ const unsubscribe = jest.fn();
213
+ mockAwareness.onStateChange.mockReturnValue( unsubscribe );
214
+
215
+ const { rerender } = renderHook(
216
+ ( { postType } ) => useActiveCollaborators( 123, postType ),
217
+ { initialProps: { postType: 'post' as string | null } }
218
+ );
219
+
220
+ expect( unsubscribe ).not.toHaveBeenCalled();
221
+
222
+ rerender( { postType: 'page' } );
223
+
224
+ expect( unsubscribe ).toHaveBeenCalled();
225
+ } );
226
+
227
+ test( 'should reset state when postId becomes null', () => {
228
+ const mockUsers = [ createMockActiveUser() ];
229
+ mockAwareness.getCurrentState.mockReturnValue( mockUsers );
230
+
231
+ const { result, rerender } = renderHook(
232
+ ( { postId } ) => useActiveCollaborators( postId, 'post' ),
233
+ { initialProps: { postId: 123 as number | null } }
234
+ );
235
+
236
+ expect( result.current ).toEqual( mockUsers );
237
+
238
+ rerender( { postId: null } );
239
+
240
+ expect( result.current ).toEqual( [] );
241
+ } );
242
+ } );
243
+
244
+ describe( 'useGetAbsolutePositionIndex', () => {
245
+ test( 'should return function that returns null when postId is null', () => {
246
+ const { result } = renderHook( () =>
247
+ useGetAbsolutePositionIndex( null, 'post' )
248
+ );
249
+
250
+ const mockSelection: SelectionCursor = {
251
+ type: SelectionType.Cursor,
252
+ blockId: 'block-1',
253
+ cursorPosition: {
254
+ relativePosition: {} as any,
255
+ absoluteOffset: 5,
256
+ },
257
+ };
258
+
259
+ expect( result.current( mockSelection ) ).toBeNull();
260
+ } );
261
+
262
+ test( 'should call awareness.getAbsolutePositionIndex with selection', () => {
263
+ const mockSelection: SelectionCursor = {
264
+ type: SelectionType.Cursor,
265
+ blockId: 'block-1',
266
+ cursorPosition: {
267
+ relativePosition: {} as any,
268
+ absoluteOffset: 5,
269
+ },
270
+ };
271
+ mockAwareness.getAbsolutePositionIndex.mockReturnValue( 10 );
272
+
273
+ const { result } = renderHook( () =>
274
+ useGetAbsolutePositionIndex( 123, 'post' )
275
+ );
276
+
277
+ const position = result.current( mockSelection );
278
+
279
+ expect(
280
+ mockAwareness.getAbsolutePositionIndex
281
+ ).toHaveBeenCalledWith( mockSelection );
282
+ expect( position ).toBe( 10 );
283
+ } );
284
+ } );
285
+
286
+ describe( 'useGetDebugData', () => {
287
+ test( 'should return default debug data when postId is null', () => {
288
+ const { result } = renderHook( () =>
289
+ useGetDebugData( null, 'post' )
290
+ );
291
+
292
+ expect( result.current ).toEqual( {
293
+ doc: {},
294
+ clients: {},
295
+ collaboratorMap: {},
296
+ } );
297
+ } );
298
+
299
+ test( 'should call awareness.getDebugData and return result', () => {
300
+ const mockDebugData = createMockDebugData();
301
+ mockAwareness.getDebugData.mockReturnValue( mockDebugData );
302
+
303
+ const { result } = renderHook( () =>
304
+ useGetDebugData( 123, 'post' )
305
+ );
306
+
307
+ expect( result.current ).toEqual( mockDebugData );
308
+ } );
309
+ } );
310
+
311
+ describe( 'useIsDisconnected', () => {
312
+ test( 'should return false when postId is null', () => {
313
+ const { result } = renderHook( () =>
314
+ useIsDisconnected( null, 'post' )
315
+ );
316
+
317
+ expect( result.current ).toBe( false );
318
+ } );
319
+
320
+ test( 'should return false when current user is connected', () => {
321
+ const connectedUser = createMockActiveUser( {
322
+ isMe: true,
323
+ isConnected: true,
324
+ } );
325
+ mockAwareness.getCurrentState.mockReturnValue( [ connectedUser ] );
326
+
327
+ const { result } = renderHook( () =>
328
+ useIsDisconnected( 123, 'post' )
329
+ );
330
+
331
+ expect( result.current ).toBe( false );
332
+ } );
333
+
334
+ test( 'should return true when current user is disconnected', () => {
335
+ const disconnectedUser = createMockActiveUser( {
336
+ isMe: true,
337
+ isConnected: false,
338
+ } );
339
+ mockAwareness.getCurrentState.mockReturnValue( [
340
+ disconnectedUser,
341
+ ] );
342
+
343
+ const { result } = renderHook( () =>
344
+ useIsDisconnected( 123, 'post' )
345
+ );
346
+
347
+ expect( result.current ).toBe( true );
348
+ } );
349
+
350
+ test( 'should return false when no user is marked as me', () => {
351
+ const otherUser = createMockActiveUser( {
352
+ isMe: false,
353
+ isConnected: false,
354
+ } );
355
+ mockAwareness.getCurrentState.mockReturnValue( [ otherUser ] );
356
+
357
+ const { result } = renderHook( () =>
358
+ useIsDisconnected( 123, 'post' )
359
+ );
360
+
361
+ expect( result.current ).toBe( false );
362
+ } );
363
+
364
+ test( 'should update when state changes to disconnected', async () => {
365
+ const connectedUser = createMockActiveUser( {
366
+ isMe: true,
367
+ isConnected: true,
368
+ } );
369
+ const disconnectedUser = createMockActiveUser( {
370
+ isMe: true,
371
+ isConnected: false,
372
+ } );
373
+
374
+ mockAwareness.getCurrentState.mockReturnValue( [ connectedUser ] );
375
+
376
+ const { result } = renderHook( () =>
377
+ useIsDisconnected( 123, 'post' )
378
+ );
379
+
380
+ expect( result.current ).toBe( false );
381
+
382
+ // Simulate disconnection
383
+ act( () => {
384
+ stateChangeCallback?.( [ disconnectedUser ] );
385
+ } );
386
+
387
+ await waitFor( () => {
388
+ expect( result.current ).toBe( true );
389
+ } );
390
+ } );
391
+ } );
392
+
393
+ describe( 'hook cleanup', () => {
394
+ test( 'should unsubscribe on unmount', () => {
395
+ const unsubscribe = jest.fn();
396
+ mockAwareness.onStateChange.mockReturnValue( unsubscribe );
397
+
398
+ const { unmount } = renderHook( () =>
399
+ useActiveCollaborators( 123, 'post' )
400
+ );
401
+
402
+ expect( unsubscribe ).not.toHaveBeenCalled();
403
+
404
+ unmount();
405
+
406
+ expect( unsubscribe ).toHaveBeenCalled();
407
+ } );
408
+ } );
409
+
410
+ describe( 'multiple users scenario', () => {
411
+ test( 'should handle multiple active users', () => {
412
+ const user1 = createMockActiveUser( {
413
+ clientId: 1,
414
+ isMe: true,
415
+ collaboratorInfo: {
416
+ id: 1,
417
+ name: 'User One',
418
+ slug: 'user-one',
419
+ avatar_urls: mockAvatarUrls,
420
+ browserType: 'Chrome',
421
+ enteredAt: 1704067200000,
422
+ },
423
+ } );
424
+ const user2 = createMockActiveUser( {
425
+ clientId: 2,
426
+ isMe: false,
427
+ collaboratorInfo: {
428
+ id: 2,
429
+ name: 'User Two',
430
+ slug: 'user-two',
431
+ avatar_urls: mockAvatarUrls,
432
+ browserType: 'Firefox',
433
+ enteredAt: 1704067300000,
434
+ },
435
+ } );
436
+
437
+ mockAwareness.getCurrentState.mockReturnValue( [ user1, user2 ] );
438
+
439
+ const { result } = renderHook( () =>
440
+ useActiveCollaborators( 123, 'post' )
441
+ );
442
+
443
+ expect( result.current ).toHaveLength( 2 );
444
+ expect( result.current[ 0 ].collaboratorInfo.name ).toBe(
445
+ 'User One'
446
+ );
447
+ expect( result.current[ 1 ].collaboratorInfo.name ).toBe(
448
+ 'User Two'
449
+ );
450
+ } );
451
+
452
+ test( 'should identify correct user as disconnected among multiple', () => {
453
+ const meConnected = createMockActiveUser( {
454
+ clientId: 1,
455
+ isMe: true,
456
+ isConnected: true,
457
+ } );
458
+ const otherDisconnected = createMockActiveUser( {
459
+ clientId: 2,
460
+ isMe: false,
461
+ isConnected: false,
462
+ } );
463
+
464
+ mockAwareness.getCurrentState.mockReturnValue( [
465
+ meConnected,
466
+ otherDisconnected,
467
+ ] );
468
+
469
+ const { result } = renderHook( () =>
470
+ useIsDisconnected( 123, 'post' )
471
+ );
472
+
473
+ // Should be false because *I* am connected (other user's status doesn't matter)
474
+ expect( result.current ).toBe( false );
475
+ } );
476
+ } );
477
+ } );
@@ -13,7 +13,7 @@ import useEntityId from './use-entity-id';
13
13
  import { updateFootnotesFromMeta } from '../footnotes';
14
14
 
15
15
  const EMPTY_ARRAY = [];
16
- const parsedBlocksCache = new WeakMap();
16
+ const parsedBlocksCache = new Map();
17
17
 
18
18
  /**
19
19
  * Hook that returns block content getters and setters for
@@ -36,7 +36,6 @@ const parsedBlocksCache = new WeakMap();
36
36
  export default function useEntityBlockEditor( kind, name, { id: _id } = {} ) {
37
37
  const providerId = useEntityId( kind, name );
38
38
  const id = _id ?? providerId;
39
- const { getEntityRecord, getEntityRecordEdits } = useSelect( STORE_NAME );
40
39
  const { content, editedBlocks, meta } = useSelect(
41
40
  ( select ) => {
42
41
  if ( ! id ) {
@@ -68,28 +67,21 @@ export default function useEntityBlockEditor( kind, name, { id: _id } = {} ) {
68
67
  return EMPTY_ARRAY;
69
68
  }
70
69
 
71
- // If there's an edit, cache the parsed blocks by the edit.
72
- // If not, cache by the original entity record.
73
- const edits = getEntityRecordEdits( kind, name, id );
74
- const isUnedited = ! edits || ! Object.keys( edits ).length;
75
- const cackeKey = isUnedited ? getEntityRecord( kind, name, id ) : edits;
76
- let _blocks = parsedBlocksCache.get( cackeKey );
70
+ // Cache parsed blocks by entity identity. Store the content
71
+ // alongside the blocks so we can validate it hasn't changed.
72
+ const cacheKey = `${ kind }:${ name }:${ id }`;
73
+ const cached = parsedBlocksCache.get( cacheKey );
74
+ let _blocks;
77
75
 
78
- if ( ! _blocks ) {
76
+ if ( cached && cached.content === content ) {
77
+ _blocks = cached.blocks;
78
+ } else {
79
79
  _blocks = parse( content );
80
- parsedBlocksCache.set( cackeKey, _blocks );
80
+ parsedBlocksCache.set( cacheKey, { content, blocks: _blocks } );
81
81
  }
82
82
 
83
83
  return _blocks;
84
- }, [
85
- kind,
86
- name,
87
- id,
88
- editedBlocks,
89
- content,
90
- getEntityRecord,
91
- getEntityRecordEdits,
92
- ] );
84
+ }, [ kind, name, id, editedBlocks, content ] );
93
85
 
94
86
  const onChange = useCallback(
95
87
  ( newBlocks, options ) => {
@@ -128,8 +120,10 @@ export default function useEntityBlockEditor( kind, name, { id: _id } = {} ) {
128
120
  const onInput = useCallback(
129
121
  ( newBlocks, options ) => {
130
122
  const { selection, ...rest } = options;
131
- const footnotesChanges = updateFootnotesFromMeta( newBlocks, meta );
132
- const edits = { selection, ...footnotesChanges };
123
+ const edits = {
124
+ selection,
125
+ ...updateFootnotesFromMeta( newBlocks, meta ),
126
+ };
133
127
 
134
128
  editEntityRecord( kind, name, id, edits, {
135
129
  isCached: true,
package/src/index.js CHANGED
@@ -133,6 +133,12 @@ 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
+
136
142
  export { default as EntityProvider } from './entity-provider';
137
143
  export * from './entity-provider';
138
144
  export * from './entity-types';
@@ -140,3 +146,4 @@ export * from './awareness/types';
140
146
  export * from './fetch';
141
147
  export * from './hooks';
142
148
  export * from './private-apis';
149
+ export * from './types';
@@ -160,3 +160,17 @@ export function receiveEditorAssets( assets ) {
160
160
  assets,
161
161
  };
162
162
  }
163
+
164
+ /**
165
+ * Returns an action object used to receive icons.
166
+ *
167
+ * @param {Array} icons List of icons.
168
+ *
169
+ * @return {Object} Action object.
170
+ */
171
+ export function receiveIcons( icons ) {
172
+ return {
173
+ type: 'RECEIVE_ICONS',
174
+ icons,
175
+ };
176
+ }
@@ -3,7 +3,10 @@
3
3
  */
4
4
  import { useEntityRecordsWithPermissions } from './hooks/use-entity-records';
5
5
  import { RECEIVE_INTERMEDIATE_RESULTS } from './utils';
6
- import { useActiveCollaborators } from './hooks/use-post-editor-awareness-state';
6
+ import {
7
+ useActiveCollaborators,
8
+ useGetAbsolutePositionIndex,
9
+ } from './hooks/use-post-editor-awareness-state';
7
10
  import { lock } from './lock-unlock';
8
11
 
9
12
  export const privateApis = {};
@@ -11,4 +14,5 @@ lock( privateApis, {
11
14
  useEntityRecordsWithPermissions,
12
15
  RECEIVE_INTERMEDIATE_RESULTS,
13
16
  useActiveCollaborators,
17
+ useGetAbsolutePositionIndex,
14
18
  } );
@@ -6,7 +6,12 @@ import { createSelector, createRegistrySelector } from '@wordpress/data';
6
6
  /**
7
7
  * Internal dependencies
8
8
  */
9
- import { getDefaultTemplateId, getEntityRecord, type State } from './selectors';
9
+ import {
10
+ getDefaultTemplateId,
11
+ getEntityRecord,
12
+ type State,
13
+ type Icon,
14
+ } from './selectors';
10
15
  import { STORE_NAME } from './name';
11
16
  import { unlock } from './lock-unlock';
12
17
  import { getSyncManager } from './sync';
@@ -308,3 +313,13 @@ export function getEditorSettings(
308
313
  export function getEditorAssets( state: State ): Record< string, any > | null {
309
314
  return state.editorAssets;
310
315
  }
316
+
317
+ /**
318
+ * Returns the list of available icons.
319
+ *
320
+ * @param state Data state.
321
+ * @return The list of icons or empty array if not loaded.
322
+ */
323
+ export function getIcons( state: State ): Icon[] {
324
+ return state.icons ?? [];
325
+ }
package/src/reducer.js CHANGED
@@ -660,6 +660,49 @@ export function editorAssets( state = null, action ) {
660
660
  return state;
661
661
  }
662
662
 
663
+ /**
664
+ * Reducer managing icons.
665
+ *
666
+ * @param {Array} state Current state.
667
+ * @param {Object} action Action object.
668
+ *
669
+ * @return {Array} Updated state.
670
+ */
671
+ export function icons( state = [], action ) {
672
+ switch ( action.type ) {
673
+ case 'RECEIVE_ICONS':
674
+ return action.icons;
675
+ }
676
+ return state;
677
+ }
678
+
679
+ /**
680
+ * Reducer managing sync connection states for entities.
681
+ * Keyed by "kind/name:id" (e.g., "postType/post:123").
682
+ *
683
+ * @param {Object} state Current state.
684
+ * @param {Object} action Dispatched action.
685
+ *
686
+ * @return {Object} Updated state.
687
+ */
688
+ export function syncConnectionStatuses( state = {}, action ) {
689
+ switch ( action.type ) {
690
+ case 'SET_SYNC_CONNECTION_STATUS': {
691
+ const key = `${ action.kind }/${ action.name }:${ action.key }`;
692
+ return {
693
+ ...state,
694
+ [ key ]: action.status,
695
+ };
696
+ }
697
+ case 'CLEAR_SYNC_CONNECTION_STATUS': {
698
+ const key = `${ action.kind }/${ action.name }:${ action.key }`;
699
+ const { [ key ]: _, ...rest } = state;
700
+ return rest;
701
+ }
702
+ }
703
+ return state;
704
+ }
705
+
663
706
  export default combineReducers( {
664
707
  users,
665
708
  currentTheme,
@@ -682,4 +725,6 @@ export default combineReducers( {
682
725
  registeredPostMeta,
683
726
  editorSettings,
684
727
  editorAssets,
728
+ icons,
729
+ syncConnectionStatuses,
685
730
  } );