reselect-kit 1.0.1 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +307 -36
  2. package/dist/cjs/reselectKit.development.cjs +256 -95
  3. package/dist/cjs/reselectKit.development.cjs.map +1 -1
  4. package/dist/cjs/reselectKit.production.min.cjs +1 -1
  5. package/dist/cjs/reselectKit.production.min.cjs.map +1 -1
  6. package/dist/reselectKit.d.ts +201 -92
  7. package/dist/reselectKit.mjs +257 -97
  8. package/dist/reselectKit.mjs.map +1 -1
  9. package/package.json +6 -6
  10. package/src/__tests__/createAdaptedSelector.test.ts +3 -4
  11. package/src/__tests__/createBoundSelector.test.ts +98 -5
  12. package/src/__tests__/createChainSelector.bench.ts +117 -0
  13. package/src/__tests__/createChainSelector.test.ts +162 -6
  14. package/src/__tests__/createPathSelector.test.ts +14 -0
  15. package/src/__tests__/createSequenceSelector.test.ts +10 -14
  16. package/src/_helpers/__tests__/generateSelectorKey.test.ts +24 -0
  17. package/src/_helpers/__tests__/getSelectorName.test.ts +30 -0
  18. package/src/_helpers/unfreezeProps.ts +14 -0
  19. package/src/_hooks/__tests__/useSelector.bench.ts +133 -0
  20. package/src/_hooks/__tests__/useSelector.test.ts +69 -0
  21. package/src/_hooks/__tests__/useSelectors.bench.ts +160 -0
  22. package/src/_hooks/__tests__/useSelectors.test.ts +219 -0
  23. package/src/_hooks/selectorOptions.ts +11 -0
  24. package/src/_hooks/useSelector.ts +68 -26
  25. package/src/_hooks/useSelectors.ts +162 -0
  26. package/src/cache/__tests__/intervalMapCache.test.ts +11 -0
  27. package/src/createAdaptedSelector.ts +9 -5
  28. package/src/createBoundSelector.ts +92 -84
  29. package/src/createCachedSequenceSelector.ts +12 -8
  30. package/src/createCachedStructuredSelector.ts +11 -20
  31. package/src/createChainSelector.ts +134 -89
  32. package/src/createPathSelector.ts +71 -104
  33. package/src/createPropSelector.ts +9 -8
  34. package/src/createSequenceSelector.ts +14 -9
  35. package/src/index.ts +2 -0
  36. package/src/keys/createKeySelectorComposer.ts +12 -23
  37. package/src/types.ts +115 -7
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- let _veksa_re_reselect = require("@veksa/re-reselect");
3
2
  let _veksa_reselect = require("@veksa/reselect");
3
+ let _veksa_re_reselect = require("@veksa/re-reselect");
4
4
  let _veksa_react_redux = require("@veksa/react-redux");
5
5
  let react = require("react");
6
6
  //#region src/createSegmentSelector.ts
@@ -85,6 +85,15 @@ const innerCreatePathSelector = (baseSelector, path = [], applyMeta = defaultVoi
85
85
  return innerCreatePathSelector(baseSelector, [...path, String(key)], applyMeta);
86
86
  } });
87
87
  };
88
+ /**
89
+ * Builds a proxy that turns property access into a selector reading that path.
90
+ *
91
+ * `Params` is inferred straight from the base selector, so the plain and the
92
+ * parametric case share one signature — and one set of builder types — instead
93
+ * of being told apart by a conditional in the parameter position. It defaults
94
+ * to empty so that `createPathSelector<State, Result>(selector)` keeps working
95
+ * for callers that spell the type arguments out.
96
+ */
88
97
  function createPathSelector(baseSelector) {
89
98
  return innerCreatePathSelector(baseSelector);
90
99
  }
@@ -251,15 +260,54 @@ const generateBindingName = (binding) => {
251
260
  }), {});
252
261
  return `${Object.keys(structure).join()} -> ${Object.values(structure).join()}`;
253
262
  };
254
- const innerCreateBoundSelector = (baseSelector, binding) => {
255
- const boundSelector = (state, props) => baseSelector(state, {
256
- ...props,
257
- ...binding
258
- });
259
- return boundSelector;
263
+ /**
264
+ * The default binding strategy: applies the binding to the props the caller
265
+ * passed, hands them to the base selector, and puts them back as they were.
266
+ *
267
+ * Almost every binding is a single key — `bound(selector, { positionId })` —
268
+ * and specialising it removes the two allocations the generic path costs per
269
+ * call: the `{ result, rollback }` object and the `rollback` closure. On a
270
+ * graph with a bound selector per row those are the calls that dominate.
271
+ *
272
+ * The `delete` in the restore stays deliberately. Leaving the key behind as
273
+ * `undefined` would be cheaper — it avoids putting the caller's props object
274
+ * into dictionary mode — but it also changes the object's shape, and
275
+ * `useSelector` memoizes props shallowly: a props object that has grown an
276
+ * extra key never again compares equal to a freshly built one, so the
277
+ * memoization the binding exists to protect would be defeated.
278
+ */
279
+ const applyBinding = (baseSelector, binding) => {
280
+ const source = binding;
281
+ const keys = Object.keys(source);
282
+ if (keys.length === 1) {
283
+ const key = keys[0];
284
+ const value = source[key];
285
+ return (state, props) => {
286
+ const target = props;
287
+ if (typeof target !== "object" || target === null) return baseSelector(state, source);
288
+ const previous = target[key];
289
+ if (previous === value) return baseSelector(state, props);
290
+ const had = Object.prototype.hasOwnProperty.call(target, key);
291
+ target[key] = value;
292
+ try {
293
+ return baseSelector(state, props);
294
+ } finally {
295
+ if (had) target[key] = previous;
296
+ else delete target[key];
297
+ }
298
+ };
299
+ }
300
+ return (state, props) => {
301
+ const { result: nextProps, rollback } = temporaryAssign(props, source);
302
+ try {
303
+ return baseSelector(state, nextProps);
304
+ } finally {
305
+ rollback();
306
+ }
307
+ };
260
308
  };
261
- const createBoundInnerSelector = (baseSelector, binding, options = {}) => {
262
- const bindingStrategy = options.bindingStrategy ?? innerCreateBoundSelector;
309
+ const createBoundSelector = (baseSelector, binding, options = {}) => {
310
+ const bindingStrategy = options.bindingStrategy ?? applyBinding;
263
311
  const keySelectorComposer = options.keySelectorComposer ?? stringComposeKeySelectors;
264
312
  const boundSelector = bindingStrategy(baseSelector, binding);
265
313
  Object.assign(boundSelector, baseSelector);
@@ -286,49 +334,6 @@ const createBoundInnerSelector = (baseSelector, binding, options = {}) => {
286
334
  }
287
335
  return boundSelector;
288
336
  };
289
- const createBoundSelector = (baseSelector, binding) => {
290
- return createBoundInnerSelector(baseSelector, binding, { bindingStrategy: (selector, bindingProps) => {
291
- const source = bindingProps;
292
- const keys = Object.keys(source);
293
- /**
294
- * Almost every binding is a single key — `bound(selector, { positionId })` —
295
- * and specialising it removes the two allocations the generic path costs per
296
- * call: the `{ result, rollback }` object and the `rollback` closure. On a
297
- * graph with a bound selector per row those are the calls that dominate.
298
- *
299
- * The `delete` in the restore stays deliberately. Leaving the key behind as
300
- * `undefined` would be cheaper — it avoids putting the caller's props object
301
- * into dictionary mode — but it also changes the object's shape, and
302
- * `useSelector` memoizes props shallowly: a props object that has grown an
303
- * extra key never again compares equal to a freshly built one, so the
304
- * memoization the binding exists to protect would be defeated.
305
- */
306
- if (keys.length === 1) {
307
- const key = keys[0];
308
- const value = source[key];
309
- return (state, props) => {
310
- const target = props;
311
- if (typeof target !== "object" || target === null) return selector(state, source);
312
- const previous = target[key];
313
- if (previous === value) return selector(state, props);
314
- const had = Object.prototype.hasOwnProperty.call(target, key);
315
- target[key] = value;
316
- try {
317
- return selector(state, props);
318
- } finally {
319
- if (had) target[key] = previous;
320
- else delete target[key];
321
- }
322
- };
323
- }
324
- return (state, props) => {
325
- const { result: nextProps, rollback } = temporaryAssign(props, source);
326
- const result = selector(state, nextProps);
327
- rollback();
328
- return result;
329
- };
330
- } });
331
- };
332
337
  //#endregion
333
338
  //#region src/createEmptySelector.ts
334
339
  /**
@@ -357,6 +362,11 @@ const generateMappingName = (mapping) => {
357
362
  const innerCreateAdaptedSelector = (baseSelector, mapping) => (state, props) => {
358
363
  return baseSelector(state, mapping(props));
359
364
  };
365
+ /**
366
+ * Rewrites the props a selector reads. Only the props argument changes: the
367
+ * adapted selector takes `[P2]` where the base took `[P1]`, which is the same
368
+ * argument tuple every other creator in the library carries.
369
+ */
360
370
  const createAdaptedSelector = (baseSelector, mapping) => {
361
371
  const adaptedSelector = innerCreateAdaptedSelector(baseSelector, mapping);
362
372
  Object.assign(adaptedSelector, baseSelector);
@@ -391,7 +401,7 @@ const generateSelectorKey = (selector) => {
391
401
  //#endregion
392
402
  //#region src/createChainSelector.ts
393
403
  function createChainSelector(selector, options, prevChain) {
394
- const chain = (fn, chainOptions) => {
404
+ const createLevel = (fn, chainOptions, isDerivedKeyless) => {
395
405
  const combinedOptions = {
396
406
  ...options,
397
407
  ...chainOptions
@@ -399,8 +409,17 @@ function createChainSelector(selector, options, prevChain) {
399
409
  const keySelector = isCachedSelector(selector) ? selector.keySelector : defaultKeySelector;
400
410
  const { createSelectorOptions = () => ({}), keySelectorComposer = stringComposeKeySelectors } = combinedOptions;
401
411
  const keySelectorCreator = createKeySelectorCreator(keySelectorComposer);
402
- const higherOrderSelector = (0, _veksa_re_reselect.createCachedSelector)([selector], fn)({
403
- ...createSelectorOptions(),
412
+ /**
413
+ * Nothing below this level is keyed, so the cache would hold a single entry
414
+ * for the lifetime of the chain and every call would still pay for the key
415
+ * computation and the lookup that guard it. A plain memoized selector is the
416
+ * same mapping — re-reselect under a constant key is one `createSelector`
417
+ * instance — without the layer.
418
+ */
419
+ const isKeyless = keySelector === defaultKeySelector;
420
+ const selectorOptions = createSelectorOptions();
421
+ const higherOrderSelector = isKeyless ? (selectorOptions.selectorCreator ?? _veksa_reselect.createSelector)([selector], fn) : (0, _veksa_re_reselect.createCachedSelector)([selector], fn)({
422
+ ...selectorOptions,
404
423
  keySelector
405
424
  });
406
425
  withDebugName(higherOrderSelector, () => {
@@ -420,37 +439,65 @@ function createChainSelector(selector, options, prevChain) {
420
439
  return derivedSelector(state, props);
421
440
  };
422
441
  combinedSelector.dependencies = [higherOrderSelector];
423
- combinedSelector.cache = higherOrderSelector.cache;
442
+ if (!isKeyless) combinedSelector.cache = higherOrderSelector.cache;
424
443
  /**
425
- * The derived selector's key selector, memoized on the derived selector itself.
426
- *
427
- * This used to be a second `createCachedSelector` over `[higherOrderSelector]`,
428
- * which meant every key computation paid a whole extra cached-selector layer —
429
- * its own key computation, its own cache lookup, and its own evaluation of
430
- * `higherOrderSelector` — to produce a value that depends on nothing but the
431
- * derived selector. A `WeakMap` keyed on that selector is the same mapping with
432
- * none of the layer, and it shares across cache keys rather than per key.
433
- *
434
- * Note what is deliberately *not* memoized: the `(state, props)` pair. `props`
435
- * is mutated in place by `createBoundSelector`, so two calls can pass the same
436
- * object reference with different contents, and a reference-keyed cache would
437
- * hand back a stale selector.
444
+ * Assigned through a local and written to the selector once. Assigning the
445
+ * property in both branches instead reads as two expando declarations of
446
+ * `keySelector` to TypeScript before 7, which rejects it as a duplicate
447
+ * identifier.
438
448
  */
439
- const derivedKeySelectors = /* @__PURE__ */ new WeakMap();
440
- combinedSelector.keySelector = (state, props) => {
441
- const derivedSelector = higherOrderSelector(state, props);
442
- let derivedKeySelector = derivedKeySelectors.get(derivedSelector);
443
- if (derivedKeySelector === void 0) {
444
- derivedKeySelector = keySelectorCreator({ inputSelectors: [higherOrderSelector, derivedSelector] });
445
- derivedKeySelectors.set(derivedSelector, derivedKeySelector);
446
- }
447
- return derivedKeySelector(state, props);
448
- };
449
+ let levelKeySelector;
450
+ if (isDerivedKeyless)
451
+ /**
452
+ * A `map` level derives a plain `() => output`, never a cached selector, so
453
+ * it contributes nothing to the composed key and the whole composition is
454
+ * known at build time. It is the same call the wrapper below makes, minus
455
+ * the derived selector — so the key is composed exactly as before, once,
456
+ * instead of on every key computation of every level above.
457
+ */
458
+ levelKeySelector = keySelectorCreator({ inputSelectors: [higherOrderSelector] });
459
+ else {
460
+ /**
461
+ * The derived selector's key selector, memoized on the derived selector itself.
462
+ *
463
+ * This used to be a second `createCachedSelector` over `[higherOrderSelector]`,
464
+ * which meant every key computation paid a whole extra cached-selector layer —
465
+ * its own key computation, its own cache lookup, and its own evaluation of
466
+ * `higherOrderSelector` — to produce a value that depends on nothing but the
467
+ * derived selector. A `WeakMap` keyed on that selector is the same mapping with
468
+ * none of the layer, and it shares across cache keys rather than per key.
469
+ *
470
+ * Note what is deliberately *not* memoized: the `(state, props)` pair. `props`
471
+ * is mutated in place by `createBoundSelector`, so two calls can pass the same
472
+ * object reference with different contents, and a reference-keyed cache would
473
+ * hand back a stale selector.
474
+ */
475
+ const derivedKeySelectors = /* @__PURE__ */ new WeakMap();
476
+ levelKeySelector = (state, props) => {
477
+ const derivedSelector = higherOrderSelector(state, props);
478
+ let derivedKeySelector = derivedKeySelectors.get(derivedSelector);
479
+ if (derivedKeySelector === void 0) {
480
+ derivedKeySelector = keySelectorCreator({ inputSelectors: [higherOrderSelector, derivedSelector] });
481
+ derivedKeySelectors.set(derivedSelector, derivedKeySelector);
482
+ }
483
+ return derivedKeySelector(state, props);
484
+ };
485
+ }
486
+ /**
487
+ * When nothing keyed is left the composition yields the default key, and
488
+ * leaving the property off is what lets the next level skip its cache too.
489
+ * The wrapper above is a fresh closure, so only a `map` level can land here
490
+ * with the default.
491
+ */
492
+ if (levelKeySelector !== defaultKeySelector) combinedSelector.keySelector = levelKeySelector;
449
493
  withDebugName(combinedSelector, () => {
450
494
  return `${getSelectorName(selector)} (will be chained ${stringifyFunction(fn)})`;
451
495
  });
452
496
  return createChainSelector(combinedSelector, combinedOptions, Object.assign(fn, { parentChain: prevChain }));
453
497
  };
498
+ const chain = (fn, chainOptions) => {
499
+ return createLevel(fn, chainOptions, false);
500
+ };
454
501
  const map = (fn, mapOptions) => {
455
502
  const mapSelector = (result) => {
456
503
  const output = fn(result);
@@ -460,7 +507,7 @@ function createChainSelector(selector, options, prevChain) {
460
507
  });
461
508
  return mapSelector;
462
509
  };
463
- return chain(mapSelector, mapOptions);
510
+ return createLevel(mapSelector, mapOptions, true);
464
511
  };
465
512
  const build = () => {
466
513
  return Object.assign(selector, { chainHierarchy: prevChain });
@@ -637,26 +684,138 @@ var TreeCache = class {
637
684
  }
638
685
  };
639
686
  //#endregion
640
- //#region src/_hooks/useMemoWith.ts
641
- const useMemoWith = (value, comparator) => {
642
- const ref = (0, react.useRef)(value);
643
- if (!comparator(value, ref.current)) ref.current = value;
644
- return ref.current;
687
+ //#region src/_helpers/unfreezeProps.ts
688
+ /**
689
+ * Bound selectors write the binding into the props object in place and roll it
690
+ * back afterwards (see temporaryAssign.ts), which throws on a frozen target in
691
+ * strict mode. React freezes element props in development, and immer-based
692
+ * stores hand out frozen objects in production too, so the copy stays in both
693
+ * builds — the hooks only ever call this when props actually change.
694
+ */
695
+ const unfreezeProps = (props) => {
696
+ if (typeof props !== "object" || props === null || !Object.isFrozen(props)) return props;
697
+ return Array.isArray(props) ? props.slice() : { ...props };
645
698
  };
646
699
  //#endregion
700
+ //#region src/_hooks/selectorOptions.ts
701
+ /**
702
+ * Hoisted: react-redux reads the options object on every render, so there is no
703
+ * reason to allocate a fresh one per call site per render. The dev-mode checks
704
+ * re-run the selector to compare results, which these hooks control themselves.
705
+ */
706
+ const selectorOptions = { devModeChecks: {
707
+ stabilityCheck: "never",
708
+ identityFunctionCheck: "never"
709
+ } };
710
+ //#endregion
647
711
  //#region src/_hooks/useSelector.ts
648
712
  function useSelector(selector, ...params) {
649
713
  const props = params[0];
650
- const memoizedProps = useMemoWith(props, _veksa_react_redux.shallowEqual);
651
- const unfrozenProps = (0, react.useMemo)(() => {
652
- return Object.isFrozen(memoizedProps) ? { ...memoizedProps } : memoizedProps;
653
- }, [memoizedProps]);
654
- return (0, _veksa_react_redux.useSelector)((0, react.useCallback)((state) => {
655
- return selector(state, unfrozenProps);
656
- }, [selector, unfrozenProps]), { devModeChecks: {
657
- stabilityCheck: "never",
658
- identityFunctionCheck: "never"
659
- } });
714
+ const cacheRef = (0, react.useRef)(null);
715
+ let cache = cacheRef.current;
716
+ if (cache === null) {
717
+ const unfrozenProps = unfreezeProps(props);
718
+ cache = cacheRef.current = {
719
+ props,
720
+ unfrozenProps,
721
+ selector,
722
+ boundSelector: (state) => selector(state, unfrozenProps)
723
+ };
724
+ } else {
725
+ const propsChanged = !(0, _veksa_react_redux.shallowEqual)(props, cache.props);
726
+ if (propsChanged) {
727
+ cache.props = props;
728
+ cache.unfrozenProps = unfreezeProps(props);
729
+ }
730
+ if (propsChanged || cache.selector !== selector) {
731
+ const { unfrozenProps } = cache;
732
+ cache.selector = selector;
733
+ cache.boundSelector = (state) => selector(state, unfrozenProps);
734
+ }
735
+ }
736
+ return (0, _veksa_react_redux.useSelector)(cache.boundSelector, selectorOptions);
737
+ }
738
+ //#endregion
739
+ //#region src/_hooks/useSelectors.ts
740
+ const readSelectors = (cache, selectors) => {
741
+ const keys = [];
742
+ const fns = [];
743
+ for (const key in selectors) if (Object.prototype.hasOwnProperty.call(selectors, key)) {
744
+ keys.push(key);
745
+ fns.push(selectors[key]);
746
+ }
747
+ cache.keys = keys;
748
+ cache.selectors = fns;
749
+ cache.results = [];
750
+ cache.value = null;
751
+ };
752
+ /**
753
+ * One pass over the incoming object instead of `shallowEqual`, which would
754
+ * allocate a key array for each side on every render of every call site. Key
755
+ * order is part of the comparison, so a renamed key is caught as well.
756
+ */
757
+ const hasSameSelectors = (cache, selectors) => {
758
+ const { keys, selectors: fns } = cache;
759
+ let index = 0;
760
+ for (const key in selectors) if (Object.prototype.hasOwnProperty.call(selectors, key)) {
761
+ if (index === keys.length || keys[index] !== key || fns[index] !== selectors[key]) return false;
762
+ index++;
763
+ }
764
+ return index === keys.length;
765
+ };
766
+ /**
767
+ * The structured part: every selector runs, but the result object is rebuilt
768
+ * only when some field actually changed. An unchanged batch keeps its previous
769
+ * reference, so react-redux's default reference equality already bails out —
770
+ * no result allocation and no comparison pass on the steady path.
771
+ */
772
+ const select = (cache, state) => {
773
+ const { keys, selectors, unfrozenProps, results } = cache;
774
+ const count = keys.length;
775
+ let changed = cache.value === null;
776
+ for (let index = 0; index < count; index++) {
777
+ const result = selectors[index](state, unfrozenProps);
778
+ if (!Object.is(result, results[index])) {
779
+ results[index] = result;
780
+ changed = true;
781
+ }
782
+ }
783
+ if (!changed) return cache.value;
784
+ const value = {};
785
+ for (let index = 0; index < count; index++) value[keys[index]] = results[index];
786
+ cache.value = value;
787
+ return value;
788
+ };
789
+ function useSelectors(selectors, ...params) {
790
+ const props = params[0];
791
+ const cacheRef = (0, react.useRef)(null);
792
+ let cache = cacheRef.current;
793
+ if (cache === null) {
794
+ const created = {
795
+ keys: [],
796
+ selectors: [],
797
+ props,
798
+ unfrozenProps: unfreezeProps(props),
799
+ results: [],
800
+ value: null,
801
+ boundSelector: (state) => select(created, state)
802
+ };
803
+ readSelectors(created, selectors);
804
+ cache = cacheRef.current = created;
805
+ } else {
806
+ const selectorsChanged = !hasSameSelectors(cache, selectors);
807
+ const propsChanged = !(0, _veksa_react_redux.shallowEqual)(props, cache.props);
808
+ if (selectorsChanged) readSelectors(cache, selectors);
809
+ if (propsChanged) {
810
+ cache.props = props;
811
+ cache.unfrozenProps = unfreezeProps(props);
812
+ }
813
+ if (selectorsChanged || propsChanged) {
814
+ const current = cache;
815
+ cache.boundSelector = (state) => select(current, state);
816
+ }
817
+ }
818
+ return (0, _veksa_react_redux.useSelector)(cache.boundSelector, selectorOptions);
660
819
  }
661
820
  //#endregion
662
821
  exports.IntervalMapCache = IntervalMapCache;
@@ -673,11 +832,13 @@ exports.createKeySelectorCreator = createKeySelectorCreator;
673
832
  exports.createPathSelector = createPathSelector;
674
833
  exports.createPropSelector = createPropSelector;
675
834
  exports.createSegmentSelector = createSegmentSelector;
835
+ exports.createSequenceSelector = createSequenceSelector;
676
836
  exports.defaultKeySelector = defaultKeySelector;
677
837
  exports.initGarbageCollector = initGarbageCollector;
678
838
  exports.isDebugMode = isDebugMode;
679
839
  exports.setDebugMode = setDebugMode;
680
840
  exports.stringComposeKeySelectors = stringComposeKeySelectors;
681
841
  exports.useSelector = useSelector;
842
+ exports.useSelectors = useSelectors;
682
843
 
683
844
  //# sourceMappingURL=reselectKit.development.cjs.map