@wordpress/data 10.24.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 10.26.0 (2025-06-25)
6
+
7
+ ### Bug Fixes
8
+
9
+ - Add support for private selectors to `resolveSelect` and `suspendSelect` ([#52036](https://github.com/WordPress/gutenberg/pull/52036)).
10
+
11
+ ## 10.25.0 (2025-06-04)
12
+
5
13
  ## 10.24.0 (2025-05-22)
6
14
 
7
15
  ## 10.23.0 (2025-05-07)
@@ -106,19 +106,28 @@ function createResolversCache() {
106
106
  }
107
107
  };
108
108
  }
109
- function createBindingCache(bind) {
109
+ function createBindingCache(getItem, bindItem) {
110
110
  const cache = new WeakMap();
111
111
  return {
112
- get(item, itemName) {
112
+ get(itemName) {
113
+ const item = getItem(itemName);
114
+ if (!item) {
115
+ return null;
116
+ }
113
117
  let boundItem = cache.get(item);
114
118
  if (!boundItem) {
115
- boundItem = bind(item, itemName);
119
+ boundItem = bindItem(item, itemName);
116
120
  cache.set(item, boundItem);
117
121
  }
118
122
  return boundItem;
119
123
  }
120
124
  };
121
125
  }
126
+ function createPrivateProxy(publicItems, privateItems) {
127
+ return new Proxy(publicItems, {
128
+ get: (target, itemName) => privateItems.get(itemName) || Reflect.get(target, itemName)
129
+ });
130
+ }
122
131
 
123
132
  /**
124
133
  * Creates a data store descriptor for the provided Redux store configuration containing
@@ -176,42 +185,68 @@ function createReduxStore(key, options) {
176
185
  */
177
186
  const listeners = new Set();
178
187
  const reducer = options.reducer;
188
+
189
+ // Object that every thunk function receives as the first argument. It contains the
190
+ // `registry`, `dispatch`, `select` and `resolveSelect` fields. Some of them are
191
+ // constructed as getters to avoid circular dependencies.
179
192
  const thunkArgs = {
180
193
  registry,
181
194
  get dispatch() {
182
- return thunkActions;
195
+ return thunkDispatch;
183
196
  },
184
197
  get select() {
185
- return thunkSelectors;
198
+ return thunkSelect;
186
199
  },
187
200
  get resolveSelect() {
188
- return getResolveSelectors();
201
+ return resolveSelectors;
189
202
  }
190
203
  };
191
204
  const store = instantiateReduxStore(key, options, registry, thunkArgs);
205
+
192
206
  // Expose the private registration functions on the store
193
207
  // so they can be copied to a sub registry in registry.js.
194
208
  (0, _lockUnlock.lock)(store, privateRegistrationFunctions);
195
209
  const resolversCache = createResolversCache();
210
+
211
+ // Binds an action creator (`action`) to the `store`, making it a callable function.
212
+ // These are the functions that are returned by `useDispatch`, for example.
213
+ // It always returns a `Promise`, although actions are not always async. That's an
214
+ // unfortunate backward compatibility measure.
196
215
  function bindAction(action) {
197
216
  return (...args) => Promise.resolve(store.dispatch(action(...args)));
198
217
  }
218
+
219
+ /*
220
+ * Object with all public actions, both metadata and store actions.
221
+ */
199
222
  const actions = {
200
223
  ...mapValues(metadataActions, bindAction),
201
224
  ...mapValues(options.actions, bindAction)
202
225
  };
203
- const boundPrivateActions = createBindingCache(bindAction);
204
- const allActions = new Proxy(() => {}, {
205
- get: (target, prop) => {
206
- const privateAction = privateActions[prop];
207
- return privateAction ? boundPrivateActions.get(privateAction, prop) : actions[prop];
208
- }
209
- });
210
- const thunkActions = new Proxy(allActions, {
211
- apply: (target, thisArg, [action]) => store.dispatch(action)
226
+
227
+ // Object with both public and private actions. Private actions are accessed through a proxy,
228
+ // which looks them up in real time on the `privateActions` object. That's because private
229
+ // actions can be registered at any time with `registerPrivateActions`. Also once a private
230
+ // action creator is bound to the store, it is cached to give it a stable identity.
231
+ const allActions = createPrivateProxy(actions, createBindingCache(name => privateActions[name], bindAction));
232
+
233
+ // An object that implements the `dispatch` object that is passed to thunk functions.
234
+ // It is callable (`dispatch( action )`) and also has methods (`dispatch.foo()`) that
235
+ // correspond to bound registered actions, both public and private. Implemented with the proxy
236
+ // `get` method, delegating to `allActions`.
237
+ const thunkDispatch = new Proxy(action => store.dispatch(action), {
238
+ get: (target, name) => allActions[name]
212
239
  });
240
+
241
+ // To the public `actions` object, add the "locked" `allActions` object. When used,
242
+ // `unlock( actions )` will return `allActions`, implementing a way how to get at the private actions.
213
243
  (0, _lockUnlock.lock)(actions, allActions);
214
- const resolvers = options.resolvers ? mapResolvers(options.resolvers) : {};
244
+
245
+ // If we have selector resolvers, convert them to a normalized form.
246
+ const resolvers = options.resolvers ? mapValues(options.resolvers, mapResolver) : {};
247
+
248
+ // Bind a selector to the store. Call the selector with the current state, correct registry,
249
+ // and if there is a resolver, attach the resolver logic to the selector.
215
250
  function bindSelector(selector, selectorName) {
216
251
  if (selector.isRegistrySelector) {
217
252
  selector.registry = registry;
@@ -219,8 +254,7 @@ function createReduxStore(key, options) {
219
254
  const boundSelector = (...args) => {
220
255
  args = normalize(selector, args);
221
256
  const state = store.__unstableOriginalGetState();
222
- // Before calling the selector, switch to the correct
223
- // registry.
257
+ // Before calling the selector, switch to the correct registry.
224
258
  if (selector.isRegistrySelector) {
225
259
  selector.registry = registry;
226
260
  }
@@ -236,47 +270,84 @@ function createReduxStore(key, options) {
236
270
  boundSelector.hasResolver = false;
237
271
  return boundSelector;
238
272
  }
239
- return mapSelectorWithResolver(boundSelector, selectorName, resolver, store, resolversCache);
273
+ return mapSelectorWithResolver(boundSelector, selectorName, resolver, store, resolversCache, boundMetadataSelectors);
240
274
  }
241
- function bindMetadataSelector(metaDataSelector) {
242
- const boundSelector = (...args) => {
243
- const state = store.__unstableOriginalGetState();
244
- const originalSelectorName = args && args[0];
245
- const originalSelectorArgs = args && args[1];
246
- const targetSelector = options?.selectors?.[originalSelectorName];
247
275
 
276
+ // Metadata selectors are bound differently: different state (`state.metadata`), no resolvers,
277
+ // normalization depending on the target selector.
278
+ function bindMetadataSelector(metaDataSelector) {
279
+ const boundSelector = (selectorName, selectorArgs, ...args) => {
248
280
  // Normalize the arguments passed to the target selector.
249
- if (originalSelectorName && targetSelector) {
250
- args[1] = normalize(targetSelector, originalSelectorArgs);
281
+ if (selectorName) {
282
+ const targetSelector = options.selectors?.[selectorName];
283
+ if (targetSelector) {
284
+ selectorArgs = normalize(targetSelector, selectorArgs);
285
+ }
251
286
  }
252
- return metaDataSelector(state.metadata, ...args);
287
+ const state = store.__unstableOriginalGetState();
288
+ return metaDataSelector(state.metadata, selectorName, selectorArgs, ...args);
253
289
  };
254
290
  boundSelector.hasResolver = false;
255
291
  return boundSelector;
256
292
  }
293
+
294
+ // Perform binding of both metadata and store selectors and combine them in one
295
+ // `selectors` object. These are all public selectors of the store.
296
+ const boundMetadataSelectors = mapValues(metadataSelectors, bindMetadataSelector);
297
+ const boundSelectors = mapValues(options.selectors, bindSelector);
257
298
  const selectors = {
258
- ...mapValues(metadataSelectors, bindMetadataSelector),
259
- ...mapValues(options.selectors, bindSelector)
299
+ ...boundMetadataSelectors,
300
+ ...boundSelectors
260
301
  };
261
- const boundPrivateSelectors = createBindingCache(bindSelector);
302
+
303
+ // Cache of bould private selectors. They are bound only when first accessed, because
304
+ // new private selectors can be registered at any time (with `registerPrivateSelectors`).
305
+ // Once bound, they are cached to give them a stable identity.
306
+ const boundPrivateSelectors = createBindingCache(name => privateSelectors[name], bindSelector);
307
+ const allSelectors = createPrivateProxy(selectors, boundPrivateSelectors);
262
308
 
263
309
  // Pre-bind the private selectors that have been registered by the time of
264
310
  // instantiation, so that registry selectors are bound to the registry.
265
- for (const [selectorName, selector] of Object.entries(privateSelectors)) {
266
- boundPrivateSelectors.get(selector, selectorName);
311
+ for (const selectorName of Object.keys(privateSelectors)) {
312
+ boundPrivateSelectors.get(selectorName);
267
313
  }
268
- const allSelectors = new Proxy(() => {}, {
269
- get: (target, prop) => {
270
- const privateSelector = privateSelectors[prop];
271
- return privateSelector ? boundPrivateSelectors.get(privateSelector, prop) : selectors[prop];
272
- }
273
- });
274
- const thunkSelectors = new Proxy(allSelectors, {
275
- apply: (target, thisArg, [selector]) => selector(store.__unstableOriginalGetState())
314
+
315
+ // An object that implements the `select` object that is passed to thunk functions.
316
+ // It is callable (`select( selector )`) and also has methods (`select.foo()`) that
317
+ // correspond to bound registered selectors, both public and private. Implemented with the proxy
318
+ // `get` method, delegating to `allSelectors`.
319
+ const thunkSelect = new Proxy(selector => selector(store.__unstableOriginalGetState()), {
320
+ get: (target, name) => allSelectors[name]
276
321
  });
322
+
323
+ // To the public `selectors` object, add the "locked" `allSelectors` object. When used,
324
+ // `unlock( selectors )` will return `allSelectors`, implementing a way how to get at the private selectors.
277
325
  (0, _lockUnlock.lock)(selectors, allSelectors);
278
- const resolveSelectors = mapResolveSelectors(selectors, store);
279
- const suspendSelectors = mapSuspendSelectors(selectors, store);
326
+
327
+ // For each selector, create a function that calls the selector, waits for resolution and returns
328
+ // a promise that resolves when the resolution is finished.
329
+ const bindResolveSelector = mapResolveSelector(store, boundMetadataSelectors);
330
+
331
+ // Now apply this function to all bound selectors, public and private. We are excluding
332
+ // metadata selectors because they don't have resolvers.
333
+ const resolveSelectors = mapValues(boundSelectors, bindResolveSelector);
334
+ const allResolveSelectors = createPrivateProxy(resolveSelectors, createBindingCache(name => boundPrivateSelectors.get(name), bindResolveSelector));
335
+
336
+ // Lock the selectors so that `unlock( resolveSelectors )` returns `allResolveSelectors`.
337
+ (0, _lockUnlock.lock)(resolveSelectors, allResolveSelectors);
338
+
339
+ // Now, in a way very similar to `bindResolveSelector`, we create a function that maps
340
+ // selectors to functions that throw a suspense promise if not yet resolved.
341
+ const bindSuspendSelector = mapSuspendSelector(store, boundMetadataSelectors);
342
+ const suspendSelectors = {
343
+ ...boundMetadataSelectors,
344
+ // no special suspense behavior
345
+ ...mapValues(boundSelectors, bindSuspendSelector)
346
+ };
347
+ const allSuspendSelectors = createPrivateProxy(suspendSelectors, createBindingCache(name => boundPrivateSelectors.get(name), bindSuspendSelector));
348
+
349
+ // Lock the selectors so that `unlock( suspendSelectors )` returns 'allSuspendSelectors`.
350
+ (0, _lockUnlock.lock)(suspendSelectors, allSuspendSelectors);
280
351
  const getSelectors = () => selectors;
281
352
  const getActions = () => actions;
282
353
  const getResolveSelectors = () => resolveSelectors;
@@ -325,7 +396,7 @@ function createReduxStore(key, options) {
325
396
 
326
397
  // Expose the private registration functions on the store
327
398
  // descriptor. That's a natural choice since that's where the
328
- // public actions and selectors are stored .
399
+ // public actions and selectors are stored.
329
400
  (0, _lockUnlock.lock)(storeDescriptor, privateRegistrationFunctions);
330
401
  return storeDescriptor;
331
402
  }
@@ -372,113 +443,99 @@ function instantiateReduxStore(key, options, registry, thunkArgs) {
372
443
  }
373
444
 
374
445
  /**
375
- * Maps selectors to functions that return a resolution promise for them
446
+ * Maps selectors to functions that return a resolution promise for them.
376
447
  *
377
- * @param {Object} selectors Selectors to map.
378
- * @param {Object} store The redux store the selectors select from.
448
+ * @param {Object} store The redux store the selectors are bound to.
449
+ * @param {Object} boundMetadataSelectors The bound metadata selectors.
379
450
  *
380
- * @return {Object} Selectors mapped to their resolution functions.
451
+ * @return {Function} Function that maps selectors to resolvers.
381
452
  */
382
- function mapResolveSelectors(selectors, store) {
383
- const {
384
- getIsResolving,
385
- hasStartedResolution,
386
- hasFinishedResolution,
387
- hasResolutionFailed,
388
- isResolving,
389
- getCachedResolvers,
390
- getResolutionState,
391
- getResolutionError,
392
- hasResolvingSelectors,
393
- countSelectorsByStatus,
394
- ...storeSelectors
395
- } = selectors;
396
- return mapValues(storeSelectors, (selector, selectorName) => {
453
+ function mapResolveSelector(store, boundMetadataSelectors) {
454
+ return (selector, selectorName) => {
397
455
  // If the selector doesn't have a resolver, just convert the return value
398
456
  // (including exceptions) to a Promise, no additional extra behavior is needed.
399
457
  if (!selector.hasResolver) {
400
458
  return async (...args) => selector.apply(null, args);
401
459
  }
402
- return (...args) => {
403
- return new Promise((resolve, reject) => {
404
- const hasFinished = () => selectors.hasFinishedResolution(selectorName, args);
405
- const finalize = result => {
406
- const hasFailed = selectors.hasResolutionFailed(selectorName, args);
407
- if (hasFailed) {
408
- const error = selectors.getResolutionError(selectorName, args);
409
- reject(error);
410
- } else {
411
- resolve(result);
412
- }
413
- };
414
- const getResult = () => selector.apply(null, args);
415
- // Trigger the selector (to trigger the resolver)
416
- const result = getResult();
460
+ return (...args) => new Promise((resolve, reject) => {
461
+ const hasFinished = () => {
462
+ return boundMetadataSelectors.hasFinishedResolution(selectorName, args);
463
+ };
464
+ const finalize = result => {
465
+ const hasFailed = boundMetadataSelectors.hasResolutionFailed(selectorName, args);
466
+ if (hasFailed) {
467
+ const error = boundMetadataSelectors.getResolutionError(selectorName, args);
468
+ reject(error);
469
+ } else {
470
+ resolve(result);
471
+ }
472
+ };
473
+ const getResult = () => selector.apply(null, args);
474
+
475
+ // Trigger the selector (to trigger the resolver)
476
+ const result = getResult();
477
+ if (hasFinished()) {
478
+ return finalize(result);
479
+ }
480
+ const unsubscribe = store.subscribe(() => {
417
481
  if (hasFinished()) {
418
- return finalize(result);
482
+ unsubscribe();
483
+ finalize(getResult());
419
484
  }
420
- const unsubscribe = store.subscribe(() => {
421
- if (hasFinished()) {
422
- unsubscribe();
423
- finalize(getResult());
424
- }
425
- });
426
485
  });
427
- };
428
- });
486
+ });
487
+ };
429
488
  }
430
489
 
431
490
  /**
432
491
  * Maps selectors to functions that throw a suspense promise if not yet resolved.
433
492
  *
434
- * @param {Object} selectors Selectors to map.
435
- * @param {Object} store The redux store the selectors select from.
493
+ * @param {Object} store The redux store the selectors select from.
494
+ * @param {Object} boundMetadataSelectors The bound metadata selectors.
436
495
  *
437
- * @return {Object} Selectors mapped to their suspense functions.
496
+ * @return {Function} Function that maps selectors to their suspending versions.
438
497
  */
439
- function mapSuspendSelectors(selectors, store) {
440
- return mapValues(selectors, (selector, selectorName) => {
498
+ function mapSuspendSelector(store, boundMetadataSelectors) {
499
+ return (selector, selectorName) => {
441
500
  // Selector without a resolver doesn't have any extra suspense behavior.
442
501
  if (!selector.hasResolver) {
443
502
  return selector;
444
503
  }
445
504
  return (...args) => {
446
505
  const result = selector.apply(null, args);
447
- if (selectors.hasFinishedResolution(selectorName, args)) {
448
- if (selectors.hasResolutionFailed(selectorName, args)) {
449
- throw selectors.getResolutionError(selectorName, args);
506
+ if (boundMetadataSelectors.hasFinishedResolution(selectorName, args)) {
507
+ if (boundMetadataSelectors.hasResolutionFailed(selectorName, args)) {
508
+ throw boundMetadataSelectors.getResolutionError(selectorName, args);
450
509
  }
451
510
  return result;
452
511
  }
453
512
  throw new Promise(resolve => {
454
513
  const unsubscribe = store.subscribe(() => {
455
- if (selectors.hasFinishedResolution(selectorName, args)) {
514
+ if (boundMetadataSelectors.hasFinishedResolution(selectorName, args)) {
456
515
  resolve();
457
516
  unsubscribe();
458
517
  }
459
518
  });
460
519
  });
461
520
  };
462
- });
521
+ };
463
522
  }
464
523
 
465
524
  /**
466
- * Convert resolvers to a normalized form, an object with `fulfill` method and
525
+ * Convert a resolver to a normalized form, an object with `fulfill` method and
467
526
  * optional methods like `isFulfilled`.
468
527
  *
469
- * @param {Object} resolvers Resolver to convert
528
+ * @param {Function} resolver Resolver to convert
470
529
  */
471
- function mapResolvers(resolvers) {
472
- return mapValues(resolvers, resolver => {
473
- if (resolver.fulfill) {
474
- return resolver;
475
- }
476
- return {
477
- ...resolver,
478
- // Copy the enumerable properties of the resolver function.
479
- fulfill: resolver // Add the fulfill method.
480
- };
481
- });
530
+ function mapResolver(resolver) {
531
+ if (resolver.fulfill) {
532
+ return resolver;
533
+ }
534
+ return {
535
+ ...resolver,
536
+ // Copy the enumerable properties of the resolver function.
537
+ fulfill: resolver // Add the fulfill method.
538
+ };
482
539
  }
483
540
 
484
541
  /**
@@ -486,22 +543,20 @@ function mapResolvers(resolvers) {
486
543
  * Resolvers are side effects invoked once per argument set of a given selector call,
487
544
  * used in ensuring that the data needs for the selector are satisfied.
488
545
  *
489
- * @param {Object} selector The selector function to be bound.
490
- * @param {string} selectorName The selector name.
491
- * @param {Object} resolver Resolver to call.
492
- * @param {Object} store The redux store to which the resolvers should be mapped.
493
- * @param {Object} resolversCache Resolvers Cache.
546
+ * @param {Object} selector The selector function to be bound.
547
+ * @param {string} selectorName The selector name.
548
+ * @param {Object} resolver Resolver to call.
549
+ * @param {Object} store The redux store to which the resolvers should be mapped.
550
+ * @param {Object} resolversCache Resolvers Cache.
551
+ * @param {Object} boundMetadataSelectors The bound metadata selectors.
494
552
  */
495
- function mapSelectorWithResolver(selector, selectorName, resolver, store, resolversCache) {
553
+ function mapSelectorWithResolver(selector, selectorName, resolver, store, resolversCache, boundMetadataSelectors) {
496
554
  function fulfillSelector(args) {
497
555
  const state = store.getState();
498
556
  if (resolversCache.isRunning(selectorName, args) || typeof resolver.isFulfilled === 'function' && resolver.isFulfilled(state, ...args)) {
499
557
  return;
500
558
  }
501
- const {
502
- metadata
503
- } = store.__unstableOriginalGetState();
504
- if (metadataSelectors.hasStartedResolution(metadata, selectorName, args)) {
559
+ if (boundMetadataSelectors.hasStartedResolution(selectorName, args)) {
505
560
  return;
506
561
  }
507
562
  resolversCache.markAsRunning(selectorName, args);