@wordpress/data 10.25.0 → 10.26.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.
@@ -108,14 +108,18 @@ function createResolversCache() {
108
108
  };
109
109
  }
110
110
 
111
- function createBindingCache( bind ) {
111
+ function createBindingCache( getItem, bindItem ) {
112
112
  const cache = new WeakMap();
113
113
 
114
114
  return {
115
- get( item, itemName ) {
115
+ get( itemName ) {
116
+ const item = getItem( itemName );
117
+ if ( ! item ) {
118
+ return null;
119
+ }
116
120
  let boundItem = cache.get( item );
117
121
  if ( ! boundItem ) {
118
- boundItem = bind( item, itemName );
122
+ boundItem = bindItem( item, itemName );
119
123
  cache.set( item, boundItem );
120
124
  }
121
125
  return boundItem;
@@ -123,6 +127,13 @@ function createBindingCache( bind ) {
123
127
  };
124
128
  }
125
129
 
130
+ function createPrivateProxy( publicItems, privateItems ) {
131
+ return new Proxy( publicItems, {
132
+ get: ( target, itemName ) =>
133
+ privateItems.get( itemName ) || Reflect.get( target, itemName ),
134
+ } );
135
+ }
136
+
126
137
  /**
127
138
  * Creates a data store descriptor for the provided Redux store configuration containing
128
139
  * properties describing reducer, actions, selectors, controls and resolvers.
@@ -179,16 +190,20 @@ export default function createReduxStore( key, options ) {
179
190
  */
180
191
  const listeners = new Set();
181
192
  const reducer = options.reducer;
193
+
194
+ // Object that every thunk function receives as the first argument. It contains the
195
+ // `registry`, `dispatch`, `select` and `resolveSelect` fields. Some of them are
196
+ // constructed as getters to avoid circular dependencies.
182
197
  const thunkArgs = {
183
198
  registry,
184
199
  get dispatch() {
185
- return thunkActions;
200
+ return thunkDispatch;
186
201
  },
187
202
  get select() {
188
- return thunkSelectors;
203
+ return thunkSelect;
189
204
  },
190
205
  get resolveSelect() {
191
- return getResolveSelectors();
206
+ return resolveSelectors;
192
207
  },
193
208
  };
194
209
 
@@ -198,42 +213,61 @@ export default function createReduxStore( key, options ) {
198
213
  registry,
199
214
  thunkArgs
200
215
  );
216
+
201
217
  // Expose the private registration functions on the store
202
218
  // so they can be copied to a sub registry in registry.js.
203
219
  lock( store, privateRegistrationFunctions );
204
220
  const resolversCache = createResolversCache();
205
221
 
222
+ // Binds an action creator (`action`) to the `store`, making it a callable function.
223
+ // These are the functions that are returned by `useDispatch`, for example.
224
+ // It always returns a `Promise`, although actions are not always async. That's an
225
+ // unfortunate backward compatibility measure.
206
226
  function bindAction( action ) {
207
227
  return ( ...args ) =>
208
228
  Promise.resolve( store.dispatch( action( ...args ) ) );
209
229
  }
210
230
 
231
+ /*
232
+ * Object with all public actions, both metadata and store actions.
233
+ */
211
234
  const actions = {
212
235
  ...mapValues( metadataActions, bindAction ),
213
236
  ...mapValues( options.actions, bindAction ),
214
237
  };
215
238
 
216
- const boundPrivateActions = createBindingCache( bindAction );
217
- const allActions = new Proxy( () => {}, {
218
- get: ( target, prop ) => {
219
- const privateAction = privateActions[ prop ];
220
- return privateAction
221
- ? boundPrivateActions.get( privateAction, prop )
222
- : actions[ prop ];
223
- },
224
- } );
239
+ // Object with both public and private actions. Private actions are accessed through a proxy,
240
+ // which looks them up in real time on the `privateActions` object. That's because private
241
+ // actions can be registered at any time with `registerPrivateActions`. Also once a private
242
+ // action creator is bound to the store, it is cached to give it a stable identity.
243
+ const allActions = createPrivateProxy(
244
+ actions,
245
+ createBindingCache(
246
+ ( name ) => privateActions[ name ],
247
+ bindAction
248
+ )
249
+ );
225
250
 
226
- const thunkActions = new Proxy( allActions, {
227
- apply: ( target, thisArg, [ action ] ) =>
228
- store.dispatch( action ),
229
- } );
251
+ // An object that implements the `dispatch` object that is passed to thunk functions.
252
+ // It is callable (`dispatch( action )`) and also has methods (`dispatch.foo()`) that
253
+ // correspond to bound registered actions, both public and private. Implemented with the proxy
254
+ // `get` method, delegating to `allActions`.
255
+ const thunkDispatch = new Proxy(
256
+ ( action ) => store.dispatch( action ),
257
+ { get: ( target, name ) => allActions[ name ] }
258
+ );
230
259
 
260
+ // To the public `actions` object, add the "locked" `allActions` object. When used,
261
+ // `unlock( actions )` will return `allActions`, implementing a way how to get at the private actions.
231
262
  lock( actions, allActions );
232
263
 
264
+ // If we have selector resolvers, convert them to a normalized form.
233
265
  const resolvers = options.resolvers
234
- ? mapResolvers( options.resolvers )
266
+ ? mapValues( options.resolvers, mapResolver )
235
267
  : {};
236
268
 
269
+ // Bind a selector to the store. Call the selector with the current state, correct registry,
270
+ // and if there is a resolver, attach the resolver logic to the selector.
237
271
  function bindSelector( selector, selectorName ) {
238
272
  if ( selector.isRegistrySelector ) {
239
273
  selector.registry = registry;
@@ -241,8 +275,7 @@ export default function createReduxStore( key, options ) {
241
275
  const boundSelector = ( ...args ) => {
242
276
  args = normalize( selector, args );
243
277
  const state = store.__unstableOriginalGetState();
244
- // Before calling the selector, switch to the correct
245
- // registry.
278
+ // Before calling the selector, switch to the correct registry.
246
279
  if ( selector.isRegistrySelector ) {
247
280
  selector.registry = registry;
248
281
  }
@@ -267,66 +300,137 @@ export default function createReduxStore( key, options ) {
267
300
  selectorName,
268
301
  resolver,
269
302
  store,
270
- resolversCache
303
+ resolversCache,
304
+ boundMetadataSelectors
271
305
  );
272
306
  }
273
307
 
308
+ // Metadata selectors are bound differently: different state (`state.metadata`), no resolvers,
309
+ // normalization depending on the target selector.
274
310
  function bindMetadataSelector( metaDataSelector ) {
275
- const boundSelector = ( ...args ) => {
276
- const state = store.__unstableOriginalGetState();
277
-
278
- const originalSelectorName = args && args[ 0 ];
279
- const originalSelectorArgs = args && args[ 1 ];
280
- const targetSelector =
281
- options?.selectors?.[ originalSelectorName ];
282
-
311
+ const boundSelector = (
312
+ selectorName,
313
+ selectorArgs,
314
+ ...args
315
+ ) => {
283
316
  // Normalize the arguments passed to the target selector.
284
- if ( originalSelectorName && targetSelector ) {
285
- args[ 1 ] = normalize(
286
- targetSelector,
287
- originalSelectorArgs
288
- );
317
+ if ( selectorName ) {
318
+ const targetSelector =
319
+ options.selectors?.[ selectorName ];
320
+ if ( targetSelector ) {
321
+ selectorArgs = normalize(
322
+ targetSelector,
323
+ selectorArgs
324
+ );
325
+ }
289
326
  }
290
327
 
291
- return metaDataSelector( state.metadata, ...args );
328
+ const state = store.__unstableOriginalGetState();
329
+
330
+ return metaDataSelector(
331
+ state.metadata,
332
+ selectorName,
333
+ selectorArgs,
334
+ ...args
335
+ );
292
336
  };
293
337
  boundSelector.hasResolver = false;
294
338
  return boundSelector;
295
339
  }
296
340
 
341
+ // Perform binding of both metadata and store selectors and combine them in one
342
+ // `selectors` object. These are all public selectors of the store.
343
+ const boundMetadataSelectors = mapValues(
344
+ metadataSelectors,
345
+ bindMetadataSelector
346
+ );
347
+
348
+ const boundSelectors = mapValues( options.selectors, bindSelector );
349
+
297
350
  const selectors = {
298
- ...mapValues( metadataSelectors, bindMetadataSelector ),
299
- ...mapValues( options.selectors, bindSelector ),
351
+ ...boundMetadataSelectors,
352
+ ...boundSelectors,
300
353
  };
301
354
 
302
- const boundPrivateSelectors = createBindingCache( bindSelector );
355
+ // Cache of bould private selectors. They are bound only when first accessed, because
356
+ // new private selectors can be registered at any time (with `registerPrivateSelectors`).
357
+ // Once bound, they are cached to give them a stable identity.
358
+ const boundPrivateSelectors = createBindingCache(
359
+ ( name ) => privateSelectors[ name ],
360
+ bindSelector
361
+ );
362
+
363
+ const allSelectors = createPrivateProxy(
364
+ selectors,
365
+ boundPrivateSelectors
366
+ );
303
367
 
304
368
  // Pre-bind the private selectors that have been registered by the time of
305
369
  // instantiation, so that registry selectors are bound to the registry.
306
- for ( const [ selectorName, selector ] of Object.entries(
307
- privateSelectors
308
- ) ) {
309
- boundPrivateSelectors.get( selector, selectorName );
370
+ for ( const selectorName of Object.keys( privateSelectors ) ) {
371
+ boundPrivateSelectors.get( selectorName );
310
372
  }
311
373
 
312
- const allSelectors = new Proxy( () => {}, {
313
- get: ( target, prop ) => {
314
- const privateSelector = privateSelectors[ prop ];
315
- return privateSelector
316
- ? boundPrivateSelectors.get( privateSelector, prop )
317
- : selectors[ prop ];
318
- },
319
- } );
320
-
321
- const thunkSelectors = new Proxy( allSelectors, {
322
- apply: ( target, thisArg, [ selector ] ) =>
323
- selector( store.__unstableOriginalGetState() ),
324
- } );
374
+ // An object that implements the `select` object that is passed to thunk functions.
375
+ // It is callable (`select( selector )`) and also has methods (`select.foo()`) that
376
+ // correspond to bound registered selectors, both public and private. Implemented with the proxy
377
+ // `get` method, delegating to `allSelectors`.
378
+ const thunkSelect = new Proxy(
379
+ ( selector ) => selector( store.__unstableOriginalGetState() ),
380
+ { get: ( target, name ) => allSelectors[ name ] }
381
+ );
325
382
 
383
+ // To the public `selectors` object, add the "locked" `allSelectors` object. When used,
384
+ // `unlock( selectors )` will return `allSelectors`, implementing a way how to get at the private selectors.
326
385
  lock( selectors, allSelectors );
327
386
 
328
- const resolveSelectors = mapResolveSelectors( selectors, store );
329
- const suspendSelectors = mapSuspendSelectors( selectors, store );
387
+ // For each selector, create a function that calls the selector, waits for resolution and returns
388
+ // a promise that resolves when the resolution is finished.
389
+ const bindResolveSelector = mapResolveSelector(
390
+ store,
391
+ boundMetadataSelectors
392
+ );
393
+
394
+ // Now apply this function to all bound selectors, public and private. We are excluding
395
+ // metadata selectors because they don't have resolvers.
396
+ const resolveSelectors = mapValues(
397
+ boundSelectors,
398
+ bindResolveSelector
399
+ );
400
+
401
+ const allResolveSelectors = createPrivateProxy(
402
+ resolveSelectors,
403
+ createBindingCache(
404
+ ( name ) => boundPrivateSelectors.get( name ),
405
+ bindResolveSelector
406
+ )
407
+ );
408
+
409
+ // Lock the selectors so that `unlock( resolveSelectors )` returns `allResolveSelectors`.
410
+ lock( resolveSelectors, allResolveSelectors );
411
+
412
+ // Now, in a way very similar to `bindResolveSelector`, we create a function that maps
413
+ // selectors to functions that throw a suspense promise if not yet resolved.
414
+ const bindSuspendSelector = mapSuspendSelector(
415
+ store,
416
+ boundMetadataSelectors
417
+ );
418
+
419
+ const suspendSelectors = {
420
+ ...boundMetadataSelectors, // no special suspense behavior
421
+ ...mapValues( boundSelectors, bindSuspendSelector ),
422
+ };
423
+
424
+ const allSuspendSelectors = createPrivateProxy(
425
+ suspendSelectors,
426
+ createBindingCache(
427
+ ( name ) => boundPrivateSelectors.get( name ),
428
+ bindSuspendSelector
429
+ )
430
+ );
431
+
432
+ // Lock the selectors so that `unlock( suspendSelectors )` returns 'allSuspendSelectors`.
433
+ lock( suspendSelectors, allSuspendSelectors );
330
434
 
331
435
  const getSelectors = () => selectors;
332
436
  const getActions = () => actions;
@@ -381,7 +485,7 @@ export default function createReduxStore( key, options ) {
381
485
 
382
486
  // Expose the private registration functions on the store
383
487
  // descriptor. That's a natural choice since that's where the
384
- // public actions and selectors are stored .
488
+ // public actions and selectors are stored.
385
489
  lock( storeDescriptor, privateRegistrationFunctions );
386
490
 
387
491
  return storeDescriptor;
@@ -445,46 +549,37 @@ function instantiateReduxStore( key, options, registry, thunkArgs ) {
445
549
  }
446
550
 
447
551
  /**
448
- * Maps selectors to functions that return a resolution promise for them
552
+ * Maps selectors to functions that return a resolution promise for them.
449
553
  *
450
- * @param {Object} selectors Selectors to map.
451
- * @param {Object} store The redux store the selectors select from.
554
+ * @param {Object} store The redux store the selectors are bound to.
555
+ * @param {Object} boundMetadataSelectors The bound metadata selectors.
452
556
  *
453
- * @return {Object} Selectors mapped to their resolution functions.
557
+ * @return {Function} Function that maps selectors to resolvers.
454
558
  */
455
- function mapResolveSelectors( selectors, store ) {
456
- const {
457
- getIsResolving,
458
- hasStartedResolution,
459
- hasFinishedResolution,
460
- hasResolutionFailed,
461
- isResolving,
462
- getCachedResolvers,
463
- getResolutionState,
464
- getResolutionError,
465
- hasResolvingSelectors,
466
- countSelectorsByStatus,
467
- ...storeSelectors
468
- } = selectors;
469
-
470
- return mapValues( storeSelectors, ( selector, selectorName ) => {
559
+ function mapResolveSelector( store, boundMetadataSelectors ) {
560
+ return ( selector, selectorName ) => {
471
561
  // If the selector doesn't have a resolver, just convert the return value
472
562
  // (including exceptions) to a Promise, no additional extra behavior is needed.
473
563
  if ( ! selector.hasResolver ) {
474
564
  return async ( ...args ) => selector.apply( null, args );
475
565
  }
476
566
 
477
- return ( ...args ) => {
478
- return new Promise( ( resolve, reject ) => {
479
- const hasFinished = () =>
480
- selectors.hasFinishedResolution( selectorName, args );
481
- const finalize = ( result ) => {
482
- const hasFailed = selectors.hasResolutionFailed(
567
+ return ( ...args ) =>
568
+ new Promise( ( resolve, reject ) => {
569
+ const hasFinished = () => {
570
+ return boundMetadataSelectors.hasFinishedResolution(
483
571
  selectorName,
484
572
  args
485
573
  );
574
+ };
575
+ const finalize = ( result ) => {
576
+ const hasFailed =
577
+ boundMetadataSelectors.hasResolutionFailed(
578
+ selectorName,
579
+ args
580
+ );
486
581
  if ( hasFailed ) {
487
- const error = selectors.getResolutionError(
582
+ const error = boundMetadataSelectors.getResolutionError(
488
583
  selectorName,
489
584
  args
490
585
  );
@@ -494,6 +589,7 @@ function mapResolveSelectors( selectors, store ) {
494
589
  }
495
590
  };
496
591
  const getResult = () => selector.apply( null, args );
592
+
497
593
  // Trigger the selector (to trigger the resolver)
498
594
  const result = getResult();
499
595
  if ( hasFinished() ) {
@@ -507,20 +603,19 @@ function mapResolveSelectors( selectors, store ) {
507
603
  }
508
604
  } );
509
605
  } );
510
- };
511
- } );
606
+ };
512
607
  }
513
608
 
514
609
  /**
515
610
  * Maps selectors to functions that throw a suspense promise if not yet resolved.
516
611
  *
517
- * @param {Object} selectors Selectors to map.
518
- * @param {Object} store The redux store the selectors select from.
612
+ * @param {Object} store The redux store the selectors select from.
613
+ * @param {Object} boundMetadataSelectors The bound metadata selectors.
519
614
  *
520
- * @return {Object} Selectors mapped to their suspense functions.
615
+ * @return {Function} Function that maps selectors to their suspending versions.
521
616
  */
522
- function mapSuspendSelectors( selectors, store ) {
523
- return mapValues( selectors, ( selector, selectorName ) => {
617
+ function mapSuspendSelector( store, boundMetadataSelectors ) {
618
+ return ( selector, selectorName ) => {
524
619
  // Selector without a resolver doesn't have any extra suspense behavior.
525
620
  if ( ! selector.hasResolver ) {
526
621
  return selector;
@@ -529,9 +624,22 @@ function mapSuspendSelectors( selectors, store ) {
529
624
  return ( ...args ) => {
530
625
  const result = selector.apply( null, args );
531
626
 
532
- if ( selectors.hasFinishedResolution( selectorName, args ) ) {
533
- if ( selectors.hasResolutionFailed( selectorName, args ) ) {
534
- throw selectors.getResolutionError( selectorName, args );
627
+ if (
628
+ boundMetadataSelectors.hasFinishedResolution(
629
+ selectorName,
630
+ args
631
+ )
632
+ ) {
633
+ if (
634
+ boundMetadataSelectors.hasResolutionFailed(
635
+ selectorName,
636
+ args
637
+ )
638
+ ) {
639
+ throw boundMetadataSelectors.getResolutionError(
640
+ selectorName,
641
+ args
642
+ );
535
643
  }
536
644
 
537
645
  return result;
@@ -540,7 +648,10 @@ function mapSuspendSelectors( selectors, store ) {
540
648
  throw new Promise( ( resolve ) => {
541
649
  const unsubscribe = store.subscribe( () => {
542
650
  if (
543
- selectors.hasFinishedResolution( selectorName, args )
651
+ boundMetadataSelectors.hasFinishedResolution(
652
+ selectorName,
653
+ args
654
+ )
544
655
  ) {
545
656
  resolve();
546
657
  unsubscribe();
@@ -548,26 +659,24 @@ function mapSuspendSelectors( selectors, store ) {
548
659
  } );
549
660
  } );
550
661
  };
551
- } );
662
+ };
552
663
  }
553
664
 
554
665
  /**
555
- * Convert resolvers to a normalized form, an object with `fulfill` method and
666
+ * Convert a resolver to a normalized form, an object with `fulfill` method and
556
667
  * optional methods like `isFulfilled`.
557
668
  *
558
- * @param {Object} resolvers Resolver to convert
669
+ * @param {Function} resolver Resolver to convert
559
670
  */
560
- function mapResolvers( resolvers ) {
561
- return mapValues( resolvers, ( resolver ) => {
562
- if ( resolver.fulfill ) {
563
- return resolver;
564
- }
671
+ function mapResolver( resolver ) {
672
+ if ( resolver.fulfill ) {
673
+ return resolver;
674
+ }
565
675
 
566
- return {
567
- ...resolver, // Copy the enumerable properties of the resolver function.
568
- fulfill: resolver, // Add the fulfill method.
569
- };
570
- } );
676
+ return {
677
+ ...resolver, // Copy the enumerable properties of the resolver function.
678
+ fulfill: resolver, // Add the fulfill method.
679
+ };
571
680
  }
572
681
 
573
682
  /**
@@ -575,18 +684,20 @@ function mapResolvers( resolvers ) {
575
684
  * Resolvers are side effects invoked once per argument set of a given selector call,
576
685
  * used in ensuring that the data needs for the selector are satisfied.
577
686
  *
578
- * @param {Object} selector The selector function to be bound.
579
- * @param {string} selectorName The selector name.
580
- * @param {Object} resolver Resolver to call.
581
- * @param {Object} store The redux store to which the resolvers should be mapped.
582
- * @param {Object} resolversCache Resolvers Cache.
687
+ * @param {Object} selector The selector function to be bound.
688
+ * @param {string} selectorName The selector name.
689
+ * @param {Object} resolver Resolver to call.
690
+ * @param {Object} store The redux store to which the resolvers should be mapped.
691
+ * @param {Object} resolversCache Resolvers Cache.
692
+ * @param {Object} boundMetadataSelectors The bound metadata selectors.
583
693
  */
584
694
  function mapSelectorWithResolver(
585
695
  selector,
586
696
  selectorName,
587
697
  resolver,
588
698
  store,
589
- resolversCache
699
+ resolversCache,
700
+ boundMetadataSelectors
590
701
  ) {
591
702
  function fulfillSelector( args ) {
592
703
  const state = store.getState();
@@ -599,14 +710,8 @@ function mapSelectorWithResolver(
599
710
  return;
600
711
  }
601
712
 
602
- const { metadata } = store.__unstableOriginalGetState();
603
-
604
713
  if (
605
- metadataSelectors.hasStartedResolution(
606
- metadata,
607
- selectorName,
608
- args
609
- )
714
+ boundMetadataSelectors.hasStartedResolution( selectorName, args )
610
715
  ) {
611
716
  return;
612
717
  }
@@ -309,10 +309,11 @@ describe( 'normalizing args', () => {
309
309
 
310
310
  expect( normalizingFunction ).toHaveBeenCalledWith( [ 'foo', 'bar' ] );
311
311
 
312
- // Needs to be called twice:
312
+ // Needs to be called three times:
313
313
  // 1. When the selector is called.
314
- // 2. When the resolver is fulfilled.
315
- expect( normalizingFunction ).toHaveBeenCalledTimes( 2 );
314
+ // 2. When the resolver check if it's already running.
315
+ // 3. When the resolver is fulfilled.
316
+ expect( normalizingFunction ).toHaveBeenCalledTimes( 3 );
316
317
  } );
317
318
 
318
319
  it( 'should not call the __unstableNormalizeArgs method if there are no arguments passed to the selector (and thus the resolver)', async () => {
@@ -124,6 +124,52 @@ describe( 'Private data APIs', () => {
124
124
  );
125
125
  } );
126
126
 
127
+ it( 'should support private selectors with resolvers', async () => {
128
+ const testStore = createReduxStore( 'test', {
129
+ reducer: ( state = {}, action ) => {
130
+ if ( action.type === 'RECEIVE' ) {
131
+ return { ...state, [ action.key ]: action.value };
132
+ }
133
+ return state;
134
+ },
135
+ selectors: {},
136
+ resolvers: {
137
+ get:
138
+ ( key ) =>
139
+ async ( { dispatch } ) => {
140
+ const value = await ( 'resolved-' + key );
141
+ dispatch( { type: 'RECEIVE', key, value } );
142
+ },
143
+ },
144
+ } );
145
+ registry.register( testStore );
146
+ unlock( testStore ).registerPrivateSelectors( {
147
+ get: ( state, key ) => state[ key ],
148
+ } );
149
+
150
+ // Verify that the private selector is available in resolveSelect.
151
+ const resolved = await unlock(
152
+ registry.resolveSelect( testStore )
153
+ ).get( 'x' );
154
+ expect( resolved ).toBe( 'resolved-x' );
155
+
156
+ // Verify that the private selector is available in suspendSelect.
157
+ let suspended;
158
+ try {
159
+ unlock( registry.suspendSelect( testStore ) ).get( 'y' );
160
+ } catch ( error ) {
161
+ suspended = error;
162
+ }
163
+
164
+ expect( suspended ).toBeInstanceOf( Promise );
165
+ await suspended;
166
+
167
+ // After the suspense promise resolves, the result should be ready.
168
+ expect(
169
+ unlock( registry.suspendSelect( testStore ) ).get( 'y' )
170
+ ).toBe( 'resolved-y' );
171
+ } );
172
+
127
173
  it( 'should give private selectors access to the state', () => {
128
174
  const groceryStore = createStore();
129
175
  unlock( groceryStore ).registerPrivateSelectors( {