@wordpress/data 6.0.1-next.5df0cd52b7.0 → 6.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +31 -2
  3. package/build/components/registry-provider/use-registry.js +1 -1
  4. package/build/components/registry-provider/use-registry.js.map +1 -1
  5. package/build/controls.js +23 -15
  6. package/build/controls.js.map +1 -1
  7. package/build/plugins/persistence/index.js +43 -50
  8. package/build/plugins/persistence/index.js.map +1 -1
  9. package/build/registry.js +40 -7
  10. package/build/registry.js.map +1 -1
  11. package/build/utils/emitter.js +57 -0
  12. package/build/utils/emitter.js.map +1 -0
  13. package/build-module/components/registry-provider/use-registry.js +1 -1
  14. package/build-module/components/registry-provider/use-registry.js.map +1 -1
  15. package/build-module/controls.js +22 -15
  16. package/build-module/controls.js.map +1 -1
  17. package/build-module/plugins/persistence/index.js +42 -51
  18. package/build-module/plugins/persistence/index.js.map +1 -1
  19. package/build-module/registry.js +40 -8
  20. package/build-module/registry.js.map +1 -1
  21. package/build-module/utils/emitter.js +50 -0
  22. package/build-module/utils/emitter.js.map +1 -0
  23. package/build-types/redux-store/metadata/selectors.d.ts +1 -3
  24. package/build-types/redux-store/metadata/selectors.d.ts.map +1 -1
  25. package/build-types/utils/emitter.d.ts +7 -0
  26. package/build-types/utils/emitter.d.ts.map +1 -0
  27. package/package.json +8 -8
  28. package/src/components/registry-provider/use-registry.js +1 -1
  29. package/src/controls.js +43 -15
  30. package/src/plugins/persistence/index.js +56 -62
  31. package/src/plugins/persistence/test/index.js +117 -1
  32. package/src/registry.js +38 -8
  33. package/src/test/registry.js +34 -0
  34. package/src/types.d.ts +8 -0
  35. package/src/utils/emitter.js +46 -0
  36. package/tsconfig.json +1 -0
  37. package/tsconfig.tsbuildinfo +1 -826
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["@wordpress/data/src/utils/emitter.js"],"names":["createEmitter","isPaused","isPending","listeners","Set","notifyListeners","Array","from","forEach","listener","subscribe","add","delete","pause","resume","emit"],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASA,aAAT,GAAyB;AAC/B,MAAIC,QAAQ,GAAG,KAAf;AACA,MAAIC,SAAS,GAAG,KAAhB;AACA,QAAMC,SAAS,GAAG,IAAIC,GAAJ,EAAlB;;AACA,QAAMC,eAAe,GAAG,MACvB;AACA;AACA;AACAC,EAAAA,KAAK,CAACC,IAAN,CAAYJ,SAAZ,EAAwBK,OAAxB,CAAmCC,QAAF,IAAgBA,QAAQ,EAAzD,CAJD;;AAMA,SAAO;AACN,QAAIR,QAAJ,GAAe;AACd,aAAOA,QAAP;AACA,KAHK;;AAKNS,IAAAA,SAAS,CAAED,QAAF,EAAa;AACrBN,MAAAA,SAAS,CAACQ,GAAV,CAAeF,QAAf;AACA,aAAO,MAAMN,SAAS,CAACS,MAAV,CAAkBH,QAAlB,CAAb;AACA,KARK;;AAUNI,IAAAA,KAAK,GAAG;AACPZ,MAAAA,QAAQ,GAAG,IAAX;AACA,KAZK;;AAcNa,IAAAA,MAAM,GAAG;AACRb,MAAAA,QAAQ,GAAG,KAAX;;AACA,UAAKC,SAAL,EAAiB;AAChBA,QAAAA,SAAS,GAAG,KAAZ;AACAG,QAAAA,eAAe;AACf;AACD,KApBK;;AAsBNU,IAAAA,IAAI,GAAG;AACN,UAAKd,QAAL,EAAgB;AACfC,QAAAA,SAAS,GAAG,IAAZ;AACA;AACA;;AACDG,MAAAA,eAAe;AACf;;AA5BK,GAAP;AA8BA","sourcesContent":["/**\n * Create an event emitter.\n *\n * @return {import(\"../types\").WPDataEmitter} Emitter.\n */\nexport function createEmitter() {\n\tlet isPaused = false;\n\tlet isPending = false;\n\tconst listeners = new Set();\n\tconst notifyListeners = () =>\n\t\t// We use Array.from to clone the listeners Set\n\t\t// This ensures that we don't run a listener\n\t\t// that was added as a response to another listener.\n\t\tArray.from( listeners ).forEach( ( listener ) => listener() );\n\n\treturn {\n\t\tget isPaused() {\n\t\t\treturn isPaused;\n\t\t},\n\n\t\tsubscribe( listener ) {\n\t\t\tlisteners.add( listener );\n\t\t\treturn () => listeners.delete( listener );\n\t\t},\n\n\t\tpause() {\n\t\t\tisPaused = true;\n\t\t},\n\n\t\tresume() {\n\t\t\tisPaused = false;\n\t\t\tif ( isPending ) {\n\t\t\t\tisPending = false;\n\t\t\t\tnotifyListeners();\n\t\t\t}\n\t\t},\n\n\t\temit() {\n\t\t\tif ( isPaused ) {\n\t\t\t\tisPending = true;\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tnotifyListeners();\n\t\t},\n\t};\n}\n"]}
@@ -53,7 +53,5 @@ export function isResolving(state: State, selectorName: string, args?: unknown[]
53
53
  * @return {State} Resolvers mapped by args and selectorName.
54
54
  */
55
55
  export function getCachedResolvers(state: State): State;
56
- export type State = {
57
- [x: string]: import("./reducer").State;
58
- };
56
+ export type State = Record<string, import('./reducer').State>;
59
57
  //# sourceMappingURL=selectors.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"selectors.d.ts","sourceRoot":"","sources":["../../../src/redux-store/metadata/selectors.js"],"names":[],"mappings":"AAKA,iEAAiE;AAEjE;;;;;;;;;;;GAWG;AACH,sCANW,KAAK,gBACL,MAAM,QACN,OAAO,EAAE,GAER,OAAO,GAAG,SAAS,CAS9B;AAED;;;;;;;;;GASG;AACH,4CANW,KAAK,gBACL,MAAM,iCAGL,OAAO,CAIlB;AAED;;;;;;;;;GASG;AACH,6CANW,KAAK,gBACL,MAAM,iCAGL,OAAO,CAIlB;AAED;;;;;;;;;GASG;AACH,mCANW,KAAK,gBACL,MAAM,iCAGL,OAAO,CAIlB;AAED;;;;;;GAMG;AACH,0CAJW,KAAK,GAEJ,KAAK,CAIhB"}
1
+ {"version":3,"file":"selectors.d.ts","sourceRoot":"","sources":["../../../src/redux-store/metadata/selectors.js"],"names":[],"mappings":"AAKA,iEAAiE;AAEjE;;;;;;;;;;;GAWG;AACH,sCANW,KAAK,gBACL,MAAM,QACN,OAAO,EAAE,GAER,OAAO,GAAG,SAAS,CAS9B;AAED;;;;;;;;;GASG;AACH,4CANW,KAAK,gBACL,MAAM,iCAGL,OAAO,CAIlB;AAED;;;;;;;;;GASG;AACH,6CANW,KAAK,gBACL,MAAM,iCAGL,OAAO,CAIlB;AAED;;;;;;;;;GASG;AACH,mCANW,KAAK,gBACL,MAAM,iCAGL,OAAO,CAIlB;AAED;;;;;;GAMG;AACH,0CAJW,KAAK,GAEJ,KAAK,CAIhB;oBA1Ea,OAAO,MAAM,EAAE,OAAO,WAAW,EAAE,KAAK,CAAC"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Create an event emitter.
3
+ *
4
+ * @return {import("../types").WPDataEmitter} Emitter.
5
+ */
6
+ export function createEmitter(): import("../types").WPDataEmitter;
7
+ //# sourceMappingURL=emitter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emitter.d.ts","sourceRoot":"","sources":["../../src/utils/emitter.js"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,iCAFY,OAAO,UAAU,EAAE,aAAa,CA0C3C"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wordpress/data",
3
- "version": "6.0.1-next.5df0cd52b7.0",
3
+ "version": "6.1.2",
4
4
  "description": "Data module for WordPress.",
5
5
  "author": "The WordPress Contributors",
6
6
  "license": "GPL-2.0-or-later",
@@ -28,12 +28,12 @@
28
28
  "sideEffects": false,
29
29
  "dependencies": {
30
30
  "@babel/runtime": "^7.13.10",
31
- "@wordpress/compose": "^5.0.1-next.5df0cd52b7.0",
32
- "@wordpress/deprecated": "^3.2.2-next.5df0cd52b7.0",
33
- "@wordpress/element": "^4.0.1-next.5df0cd52b7.0",
34
- "@wordpress/is-shallow-equal": "^4.2.1-next.5df0cd52b7.0",
35
- "@wordpress/priority-queue": "^2.2.2-next.5df0cd52b7.0",
36
- "@wordpress/redux-routine": "^4.2.2-next.5df0cd52b7.0",
31
+ "@wordpress/compose": "^5.0.4",
32
+ "@wordpress/deprecated": "^3.2.2",
33
+ "@wordpress/element": "^4.0.3",
34
+ "@wordpress/is-shallow-equal": "^4.2.0",
35
+ "@wordpress/priority-queue": "^2.2.2",
36
+ "@wordpress/redux-routine": "^4.2.1",
37
37
  "equivalent-key-map": "^0.2.2",
38
38
  "is-promise": "^4.0.0",
39
39
  "lodash": "^4.17.21",
@@ -47,5 +47,5 @@
47
47
  "publishConfig": {
48
48
  "access": "public"
49
49
  },
50
- "gitHead": "558d577d12bec15f05e694eb49a094836d885cca"
50
+ "gitHead": "157f4ae53ab98e574af01f72213ae5a9613159ff"
51
51
  }
@@ -33,7 +33,7 @@ import { Context } from './context';
33
33
  * const registry = createRegistry( {} );
34
34
  *
35
35
  * const SomeChildUsingRegistry = ( props ) => {
36
- * const registry = useRegistry( registry );
36
+ * const registry = useRegistry();
37
37
  * // ...logic implementing the registry in other react hooks.
38
38
  * };
39
39
  *
package/src/controls.js CHANGED
@@ -1,8 +1,15 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { isObject } from 'lodash';
5
+
1
6
  /**
2
7
  * Internal dependencies
3
8
  */
4
9
  import { createRegistryControl } from './factory';
5
10
 
11
+ /** @typedef {import('./types').WPDataStore} WPDataStore */
12
+
6
13
  const SELECT = '@@data/SELECT';
7
14
  const RESOLVE_SELECT = '@@data/RESOLVE_SELECT';
8
15
  const DISPATCH = '@@data/DISPATCH';
@@ -13,9 +20,9 @@ const DISPATCH = '@@data/DISPATCH';
13
20
  * Note: This control synchronously returns the current selector value, triggering the
14
21
  * resolution, but not waiting for it.
15
22
  *
16
- * @param {string} storeKey The key for the store the selector belongs to.
17
- * @param {string} selectorName The name of the selector.
18
- * @param {Array} args Arguments for the selector.
23
+ * @param {string|WPDataStore} storeNameOrDefinition Unique namespace identifier for the store
24
+ * @param {string} selectorName The name of the selector.
25
+ * @param {Array} args Arguments for the selector.
19
26
  *
20
27
  * @example
21
28
  * ```js
@@ -30,8 +37,15 @@ const DISPATCH = '@@data/DISPATCH';
30
37
  *
31
38
  * @return {Object} The control descriptor.
32
39
  */
33
- function select( storeKey, selectorName, ...args ) {
34
- return { type: SELECT, storeKey, selectorName, args };
40
+ function select( storeNameOrDefinition, selectorName, ...args ) {
41
+ return {
42
+ type: SELECT,
43
+ storeKey: isObject( storeNameOrDefinition )
44
+ ? storeNameOrDefinition.name
45
+ : storeNameOrDefinition,
46
+ selectorName,
47
+ args,
48
+ };
35
49
  }
36
50
 
37
51
  /**
@@ -41,9 +55,9 @@ function select( storeKey, selectorName, ...args ) {
41
55
  * selectors that may have a resolver. In such case, it will return a `Promise` that resolves
42
56
  * after the selector finishes resolving, with the final result value.
43
57
  *
44
- * @param {string} storeKey The key for the store the selector belongs to
45
- * @param {string} selectorName The name of the selector
46
- * @param {Array} args Arguments for the selector.
58
+ * @param {string|WPDataStore} storeNameOrDefinition Unique namespace identifier for the store
59
+ * @param {string} selectorName The name of the selector
60
+ * @param {Array} args Arguments for the selector.
47
61
  *
48
62
  * @example
49
63
  * ```js
@@ -58,16 +72,23 @@ function select( storeKey, selectorName, ...args ) {
58
72
  *
59
73
  * @return {Object} The control descriptor.
60
74
  */
61
- function resolveSelect( storeKey, selectorName, ...args ) {
62
- return { type: RESOLVE_SELECT, storeKey, selectorName, args };
75
+ function resolveSelect( storeNameOrDefinition, selectorName, ...args ) {
76
+ return {
77
+ type: RESOLVE_SELECT,
78
+ storeKey: isObject( storeNameOrDefinition )
79
+ ? storeNameOrDefinition.name
80
+ : storeNameOrDefinition,
81
+ selectorName,
82
+ args,
83
+ };
63
84
  }
64
85
 
65
86
  /**
66
87
  * Dispatches a control action for triggering a registry dispatch.
67
88
  *
68
- * @param {string} storeKey The key for the store the action belongs to
69
- * @param {string} actionName The name of the action to dispatch
70
- * @param {Array} args Arguments for the dispatch action.
89
+ * @param {string|WPDataStore} storeNameOrDefinition Unique namespace identifier for the store
90
+ * @param {string} actionName The name of the action to dispatch
91
+ * @param {Array} args Arguments for the dispatch action.
71
92
  *
72
93
  * @example
73
94
  * ```js
@@ -82,8 +103,15 @@ function resolveSelect( storeKey, selectorName, ...args ) {
82
103
  *
83
104
  * @return {Object} The control descriptor.
84
105
  */
85
- function dispatch( storeKey, actionName, ...args ) {
86
- return { type: DISPATCH, storeKey, actionName, args };
106
+ function dispatch( storeNameOrDefinition, actionName, ...args ) {
107
+ return {
108
+ type: DISPATCH,
109
+ storeKey: isObject( storeNameOrDefinition )
110
+ ? storeNameOrDefinition.name
111
+ : storeNameOrDefinition,
112
+ actionName,
113
+ args,
114
+ };
87
115
  }
88
116
 
89
117
  export const controls = { select, resolveSelect, dispatch };
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * External dependencies
3
3
  */
4
- import { merge, isPlainObject, get, has } from 'lodash';
4
+ import { merge, isPlainObject } from 'lodash';
5
5
 
6
6
  /**
7
7
  * Internal dependencies
@@ -223,75 +223,69 @@ function persistencePlugin( registry, pluginOptions ) {
223
223
  }
224
224
 
225
225
  /**
226
- * Deprecated: Remove this function and the code in WordPress Core that calls
227
- * it once WordPress 5.4 is released.
226
+ * Move the 'features' object in local storage from the sourceStoreName to the
227
+ * interface store.
228
+ *
229
+ * @param {Object} persistence The persistence interface.
230
+ * @param {string} sourceStoreName The name of the store that has persisted
231
+ * preferences to migrate to the interface
232
+ * package.
228
233
  */
229
-
230
- persistencePlugin.__unstableMigrate = ( pluginOptions ) => {
231
- const persistence = createPersistenceInterface( pluginOptions );
232
-
234
+ export function migrateFeaturePreferencesToInterfaceStore(
235
+ persistence,
236
+ sourceStoreName
237
+ ) {
238
+ const interfaceStoreName = 'core/interface';
233
239
  const state = persistence.get();
240
+ const sourcePreferences = state[ sourceStoreName ]?.preferences;
241
+ const sourceFeatures = sourcePreferences?.features;
242
+
243
+ if ( sourceFeatures ) {
244
+ const targetFeatures =
245
+ state[ interfaceStoreName ]?.preferences?.features;
246
+
247
+ // Avoid migrating features again if they've previously been migrated.
248
+ if ( ! targetFeatures?.[ sourceStoreName ] ) {
249
+ // Set the feature values in the interface store, the features
250
+ // object is keyed by 'scope', which matches the store name for
251
+ // the source.
252
+ persistence.set( interfaceStoreName, {
253
+ preferences: {
254
+ features: {
255
+ ...targetFeatures,
256
+ [ sourceStoreName ]: sourceFeatures,
257
+ },
258
+ },
259
+ } );
234
260
 
235
- // Migrate 'insertUsage' from 'core/editor' to 'core/block-editor'
236
- const editorInsertUsage = state[ 'core/editor' ]?.preferences?.insertUsage;
237
- if ( editorInsertUsage ) {
238
- const blockEditorInsertUsage =
239
- state[ 'core/block-editor' ]?.preferences?.insertUsage;
240
- persistence.set( 'core/block-editor', {
241
- preferences: {
242
- insertUsage: {
243
- ...editorInsertUsage,
244
- ...blockEditorInsertUsage,
261
+ // Remove feature preferences from the source.
262
+ persistence.set( sourceStoreName, {
263
+ preferences: {
264
+ ...sourcePreferences,
265
+ features: undefined,
245
266
  },
246
- },
247
- } );
267
+ } );
268
+ }
248
269
  }
270
+ }
249
271
 
250
- let editPostState = state[ 'core/edit-post' ];
251
-
252
- // Default `fullscreenMode` to `false` if any persisted state had existed
253
- // and the user hadn't made an explicit choice about fullscreen mode. This
254
- // is needed since `fullscreenMode` previously did not have a default value
255
- // and was implicitly false by its absence. It is now `true` by default, but
256
- // this change is not intended to affect upgrades from earlier versions.
257
- const hadPersistedState = Object.keys( state ).length > 0;
258
- const hadFullscreenModePreference = has( state, [
259
- 'core/edit-post',
260
- 'preferences',
261
- 'features',
262
- 'fullscreenMode',
263
- ] );
264
- if ( hadPersistedState && ! hadFullscreenModePreference ) {
265
- editPostState = merge( {}, editPostState, {
266
- preferences: { features: { fullscreenMode: false } },
267
- } );
268
- }
272
+ /**
273
+ * Deprecated: Remove this function and the code in WordPress Core that calls
274
+ * it once WordPress 6.0 is released.
275
+ */
269
276
 
270
- // Migrate 'areTipsEnabled' from 'core/nux' to 'showWelcomeGuide' in 'core/edit-post'
271
- const areTipsEnabled = get( state, [
272
- 'core/nux',
273
- 'preferences',
274
- 'areTipsEnabled',
275
- ] );
276
- const hasWelcomeGuide = has( state, [
277
- 'core/edit-post',
278
- 'preferences',
279
- 'features',
280
- 'welcomeGuide',
281
- ] );
282
- if ( areTipsEnabled !== undefined && ! hasWelcomeGuide ) {
283
- editPostState = merge( {}, editPostState, {
284
- preferences: {
285
- features: {
286
- welcomeGuide: areTipsEnabled,
287
- },
288
- },
289
- } );
290
- }
277
+ persistencePlugin.__unstableMigrate = ( pluginOptions ) => {
278
+ const persistence = createPersistenceInterface( pluginOptions );
291
279
 
292
- if ( editPostState !== state[ 'core/edit-post' ] ) {
293
- persistence.set( 'core/edit-post', editPostState );
294
- }
280
+ migrateFeaturePreferencesToInterfaceStore(
281
+ persistence,
282
+ 'core/edit-widgets'
283
+ );
284
+ migrateFeaturePreferencesToInterfaceStore(
285
+ persistence,
286
+ 'core/customize-widgets'
287
+ );
288
+ migrateFeaturePreferencesToInterfaceStore( persistence, 'core/edit-post' );
295
289
  };
296
290
 
297
291
  export default persistencePlugin;
@@ -6,7 +6,11 @@ import deepFreeze from 'deep-freeze';
6
6
  /**
7
7
  * Internal dependencies
8
8
  */
9
- import plugin, { createPersistenceInterface, withLazySameState } from '../';
9
+ import plugin, {
10
+ createPersistenceInterface,
11
+ withLazySameState,
12
+ migrateFeaturePreferencesToInterfaceStore,
13
+ } from '../';
10
14
  import objectStorage from '../storage/object';
11
15
  import { createRegistry } from '../../../';
12
16
 
@@ -377,3 +381,115 @@ describe( 'persistence', () => {
377
381
  } );
378
382
  } );
379
383
  } );
384
+
385
+ describe( 'migrateFeaturePreferencesToInterfaceStore', () => {
386
+ it( 'migrates preferences from the source to the interface store', () => {
387
+ const persistenceInterface = createPersistenceInterface( {
388
+ storageKey: 'test-username',
389
+ } );
390
+
391
+ const initialState = {
392
+ preferences: {
393
+ features: {
394
+ featureA: true,
395
+ featureB: false,
396
+ featureC: true,
397
+ },
398
+ },
399
+ };
400
+
401
+ persistenceInterface.set( 'core/test', initialState );
402
+
403
+ migrateFeaturePreferencesToInterfaceStore(
404
+ persistenceInterface,
405
+ 'core/test'
406
+ );
407
+
408
+ expect( persistenceInterface.get() ).toEqual( {
409
+ 'core/interface': {
410
+ preferences: {
411
+ features: {
412
+ 'core/test': {
413
+ featureA: true,
414
+ featureB: false,
415
+ featureC: true,
416
+ },
417
+ },
418
+ },
419
+ },
420
+ 'core/test': {
421
+ preferences: {
422
+ features: undefined,
423
+ },
424
+ },
425
+ } );
426
+ } );
427
+
428
+ it( 'handles multiple preferences from different stores to be migrated', () => {
429
+ const persistenceInterface = createPersistenceInterface( {
430
+ storageKey: 'test-username',
431
+ } );
432
+
433
+ const initialStateA = {
434
+ preferences: {
435
+ features: {
436
+ featureA: true,
437
+ featureB: false,
438
+ featureC: true,
439
+ },
440
+ },
441
+ };
442
+
443
+ const initialStateB = {
444
+ preferences: {
445
+ features: {
446
+ featureD: true,
447
+ featureE: false,
448
+ featureF: true,
449
+ },
450
+ },
451
+ };
452
+
453
+ persistenceInterface.set( 'core/test-a', initialStateA );
454
+ persistenceInterface.set( 'core/test-b', initialStateB );
455
+
456
+ migrateFeaturePreferencesToInterfaceStore(
457
+ persistenceInterface,
458
+ 'core/test-a'
459
+ );
460
+
461
+ migrateFeaturePreferencesToInterfaceStore(
462
+ persistenceInterface,
463
+ 'core/test-b'
464
+ );
465
+
466
+ expect( persistenceInterface.get() ).toEqual( {
467
+ 'core/interface': {
468
+ preferences: {
469
+ features: {
470
+ 'core/test-a': {
471
+ featureA: true,
472
+ featureB: false,
473
+ featureC: true,
474
+ },
475
+ 'core/test-b': {
476
+ featureD: true,
477
+ featureE: false,
478
+ featureF: true,
479
+ },
480
+ },
481
+ },
482
+ },
483
+ 'core/test-a': {
484
+ preferences: {
485
+ features: undefined,
486
+ },
487
+ },
488
+ 'core/test-b': {
489
+ preferences: {
490
+ features: undefined,
491
+ },
492
+ },
493
+ } );
494
+ } );
495
+ } );
package/src/registry.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * External dependencies
3
3
  */
4
- import { without, mapValues, isObject } from 'lodash';
4
+ import { mapValues, isObject, forEach } from 'lodash';
5
5
 
6
6
  /**
7
7
  * Internal dependencies
@@ -9,6 +9,7 @@ import { without, mapValues, isObject } from 'lodash';
9
9
  import createReduxStore from './redux-store';
10
10
  import createCoreDataStore from './store';
11
11
  import { STORE_NAME } from './store/name';
12
+ import { createEmitter } from './utils/emitter';
12
13
 
13
14
  /** @typedef {import('./types').WPDataStore} WPDataStore */
14
15
 
@@ -49,14 +50,14 @@ import { STORE_NAME } from './store/name';
49
50
  */
50
51
  export function createRegistry( storeConfigs = {}, parent = null ) {
51
52
  const stores = {};
52
- let listeners = [];
53
+ const emitter = createEmitter();
53
54
  const __experimentalListeningStores = new Set();
54
55
 
55
56
  /**
56
57
  * Global listener called for each store's update.
57
58
  */
58
59
  function globalListener() {
59
- listeners.forEach( ( listener ) => listener() );
60
+ emitter.emit();
60
61
  }
61
62
 
62
63
  /**
@@ -67,11 +68,7 @@ export function createRegistry( storeConfigs = {}, parent = null ) {
67
68
  * @return {Function} Unsubscribe function.
68
69
  */
69
70
  const subscribe = ( listener ) => {
70
- listeners.push( listener );
71
-
72
- return () => {
73
- listeners = without( listeners, listener );
74
- };
71
+ return emitter.subscribe( listener );
75
72
  };
76
73
 
77
74
  /**
@@ -177,6 +174,30 @@ export function createRegistry( storeConfigs = {}, parent = null ) {
177
174
  if ( typeof config.subscribe !== 'function' ) {
178
175
  throw new TypeError( 'config.subscribe must be a function' );
179
176
  }
177
+ // Thi emitter is used to keep track of active listeners when the registry
178
+ // get paused, that way, when resumed we should be able to call all these
179
+ // pending listeners.
180
+ config.emitter = createEmitter();
181
+ const currentSubscribe = config.subscribe;
182
+ config.subscribe = ( listener ) => {
183
+ const unsubscribeFromStoreEmitter = config.emitter.subscribe(
184
+ listener
185
+ );
186
+ const unsubscribeFromRootStore = currentSubscribe( () => {
187
+ if ( config.emitter.isPaused ) {
188
+ config.emitter.emit();
189
+ return;
190
+ }
191
+ listener();
192
+ } );
193
+
194
+ return () => {
195
+ if ( unsubscribeFromRootStore ) {
196
+ unsubscribeFromRootStore();
197
+ }
198
+ unsubscribeFromStoreEmitter();
199
+ };
200
+ };
180
201
  stores[ key ] = config;
181
202
  config.subscribe( globalListener );
182
203
  }
@@ -213,7 +234,16 @@ export function createRegistry( storeConfigs = {}, parent = null ) {
213
234
  return parent.__experimentalSubscribeStore( storeName, handler );
214
235
  }
215
236
 
237
+ function batch( callback ) {
238
+ emitter.pause();
239
+ forEach( stores, ( store ) => store.emitter.pause() );
240
+ callback();
241
+ emitter.resume();
242
+ forEach( stores, ( store ) => store.emitter.resume() );
243
+ }
244
+
216
245
  let registry = {
246
+ batch,
217
247
  registerGenericStore,
218
248
  stores,
219
249
  namespaces: stores, // TODO: Deprecate/remove this.
@@ -722,6 +722,40 @@ describe( 'createRegistry', () => {
722
722
  } );
723
723
  } );
724
724
 
725
+ describe( 'batch', () => {
726
+ it( 'should batch callbacks and only run the subscriber once', () => {
727
+ const store = registry.registerStore( 'myAwesomeReducer', {
728
+ reducer: ( state = 0 ) => state + 1,
729
+ } );
730
+ const listener = jest.fn();
731
+ subscribeWithUnsubscribe( listener );
732
+
733
+ registry.batch( () => {} );
734
+ expect( listener ).not.toHaveBeenCalled();
735
+
736
+ registry.batch( () => {
737
+ store.dispatch( { type: 'dummy' } );
738
+ store.dispatch( { type: 'dummy' } );
739
+ } );
740
+ expect( listener ).toHaveBeenCalledTimes( 1 );
741
+
742
+ const listener2 = jest.fn();
743
+ // useSelect subscribes to the stores differently,
744
+ // This test ensures batching works in this case as well.
745
+ const unsubscribe = registry.__experimentalSubscribeStore(
746
+ 'myAwesomeReducer',
747
+ listener2
748
+ );
749
+ registry.batch( () => {
750
+ store.dispatch( { type: 'dummy' } );
751
+ store.dispatch( { type: 'dummy' } );
752
+ } );
753
+ unsubscribe();
754
+
755
+ expect( listener2 ).toHaveBeenCalledTimes( 1 );
756
+ } );
757
+ } );
758
+
725
759
  describe( 'use', () => {
726
760
  it( 'should pass through options object to plugin', () => {
727
761
  const expectedOptions = {};
package/src/types.d.ts CHANGED
@@ -30,3 +30,11 @@ export interface WPDataReduxStoreConfig {
30
30
  export interface WPDataRegistry {
31
31
  register: ( store: WPDataStore ) => void;
32
32
  }
33
+
34
+ export interface WPDataEmitter {
35
+ emit: () => void;
36
+ subscribe: ( listener: () => void ) => () => void;
37
+ pause: () => void;
38
+ resume: () => void;
39
+ isPaused: boolean;
40
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Create an event emitter.
3
+ *
4
+ * @return {import("../types").WPDataEmitter} Emitter.
5
+ */
6
+ export function createEmitter() {
7
+ let isPaused = false;
8
+ let isPending = false;
9
+ const listeners = new Set();
10
+ const notifyListeners = () =>
11
+ // We use Array.from to clone the listeners Set
12
+ // This ensures that we don't run a listener
13
+ // that was added as a response to another listener.
14
+ Array.from( listeners ).forEach( ( listener ) => listener() );
15
+
16
+ return {
17
+ get isPaused() {
18
+ return isPaused;
19
+ },
20
+
21
+ subscribe( listener ) {
22
+ listeners.add( listener );
23
+ return () => listeners.delete( listener );
24
+ },
25
+
26
+ pause() {
27
+ isPaused = true;
28
+ },
29
+
30
+ resume() {
31
+ isPaused = false;
32
+ if ( isPending ) {
33
+ isPending = false;
34
+ notifyListeners();
35
+ }
36
+ },
37
+
38
+ emit() {
39
+ if ( isPaused ) {
40
+ isPending = true;
41
+ return;
42
+ }
43
+ notifyListeners();
44
+ },
45
+ };
46
+ }
package/tsconfig.json CHANGED
@@ -16,5 +16,6 @@
16
16
  "include": [
17
17
  "src/redux-store/metadata/**/*",
18
18
  "src/promise-middleware.js",
19
+ "src/utils",
19
20
  ]
20
21
  }