reselect-kit 1.0.1 → 1.1.0

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