reselect-kit 1.0.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 (67) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +473 -0
  3. package/dist/cjs/index.js +6 -0
  4. package/dist/cjs/reselectKit.development.cjs +683 -0
  5. package/dist/cjs/reselectKit.development.cjs.map +1 -0
  6. package/dist/cjs/reselectKit.production.min.cjs +2 -0
  7. package/dist/cjs/reselectKit.production.min.cjs.map +1 -0
  8. package/dist/reselectKit.d.ts +248 -0
  9. package/dist/reselectKit.mjs +678 -0
  10. package/dist/reselectKit.mjs.map +1 -0
  11. package/package.json +70 -0
  12. package/src/__data__/state.ts +97 -0
  13. package/src/__tests__/createAdaptedSelector.test.ts +134 -0
  14. package/src/__tests__/createBoundSelector.test.ts +351 -0
  15. package/src/__tests__/createCachedSequenceSelector.test.ts +24 -0
  16. package/src/__tests__/createCachedStructuredSelector.test.ts +29 -0
  17. package/src/__tests__/createChainSelector.test.ts +404 -0
  18. package/src/__tests__/createEmptySelector.test.ts +99 -0
  19. package/src/__tests__/createPathSelector.test.ts +81 -0
  20. package/src/__tests__/createPropSelector.test.ts +36 -0
  21. package/src/__tests__/createSegmentSelector.test.ts +31 -0
  22. package/src/__tests__/createSequenceSelector.test.ts +62 -0
  23. package/src/_helpers/__tests__/arePathsEqual.test.ts +27 -0
  24. package/src/_helpers/__tests__/getObjectPaths.test.ts +56 -0
  25. package/src/_helpers/__tests__/temporaryAssign.test.ts +59 -0
  26. package/src/_helpers/arePathsEqual.ts +15 -0
  27. package/src/_helpers/defineDynamicSelectorName.ts +10 -0
  28. package/src/_helpers/generateSelectorKey.ts +15 -0
  29. package/src/_helpers/getObjectPaths.ts +13 -0
  30. package/src/_helpers/getSelectorName.ts +7 -0
  31. package/src/_helpers/isCachedSelector.ts +9 -0
  32. package/src/_helpers/isObject.ts +3 -0
  33. package/src/_helpers/stringifyFunction.ts +3 -0
  34. package/src/_helpers/temporaryAssign.ts +80 -0
  35. package/src/_hooks/__tests__/useMemoWith.test.ts +31 -0
  36. package/src/_hooks/__tests__/useSelector.test.ts +87 -0
  37. package/src/_hooks/useMemoWith.ts +11 -0
  38. package/src/_hooks/useSelector.ts +34 -0
  39. package/src/cache/TreeCache.ts +126 -0
  40. package/src/cache/__tests__/TreeCacheObject.test.ts +188 -0
  41. package/src/cache/__tests__/intervalMapCache.test.ts +133 -0
  42. package/src/cache/intervalMapCache.ts +97 -0
  43. package/src/createAdaptedSelector.ts +75 -0
  44. package/src/createBoundSelector.ts +197 -0
  45. package/src/createCachedSequenceSelector.ts +18 -0
  46. package/src/createCachedStructuredSelector.ts +44 -0
  47. package/src/createChainSelector.ts +230 -0
  48. package/src/createEmptySelector.ts +27 -0
  49. package/src/createPathSelector.ts +208 -0
  50. package/src/createPropSelector.ts +28 -0
  51. package/src/createSegmentSelector.ts +6 -0
  52. package/src/createSequenceSelector.ts +15 -0
  53. package/src/createStructuredSelector.ts +3 -0
  54. package/src/debug/__tests__/debug.test.ts +20 -0
  55. package/src/debug/debug.ts +7 -0
  56. package/src/debug/withDebugName.ts +28 -0
  57. package/src/index.ts +26 -0
  58. package/src/keys/__tests__/arrayComposeKeySelectors.test.ts +52 -0
  59. package/src/keys/__tests__/createKeySelectorCreator.test.ts +246 -0
  60. package/src/keys/__tests__/stringComposeKeySelectors.test.ts +52 -0
  61. package/src/keys/arrayComposeKeySelectors.ts +20 -0
  62. package/src/keys/createKeySelectorComposer.ts +68 -0
  63. package/src/keys/createKeySelectorCreator.ts +106 -0
  64. package/src/keys/defaultKeySelector.ts +1 -0
  65. package/src/keys/stringComposeKeySelectors.ts +15 -0
  66. package/src/types.ts +43 -0
  67. package/src/vitest-globals.d.ts +1 -0
package/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Aleksandr Khizhnyi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,473 @@
1
+ # reselect-kit
2
+
3
+ [![npm version](https://img.shields.io/npm/v/reselect-kit.svg?style=flat-square)](https://www.npmjs.com/package/reselect-kit)
4
+ [![npm downloads](https://img.shields.io/npm/dm/reselect-kit.svg?style=flat-square)](https://www.npmjs.com/package/reselect-kit)
5
+ [![license](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](LICENSE.md)
6
+
7
+ A powerful toolkit that extends the capabilities of @veksa/reselect and @veksa/re-reselect by providing utilities for advanced selector composition, safe property access, and optimized caching. This library helps you build more maintainable and efficient Redux selectors with enhanced type safety and functional programming patterns.
8
+
9
+ > **About this package**
10
+ >
11
+ > `reselect-kit` continues [sgrishchenko/reselect-utils](https://github.com/sgrishchenko/reselect-utils),
12
+ > keeping its full commit history. That project is no longer maintained, so the
13
+ > work moved here under a new name rather than staying on a fork of a dormant
14
+ > repository. The API is the same — only the package name changed, from
15
+ > `@veksa/reselect-utils` to `reselect-kit`.
16
+
17
+ ## Features
18
+
19
+ - **Chain Selector Pattern** - Build selector pipelines with fluent interfaces for complex data transformations
20
+ - **Powerful Path Selectors** - Safely traverse and select nested object properties with protection against null/undefined
21
+ - **Bound Selectors** - Create selectors with predefined parameters for reuse across components
22
+ - **Adapted Selectors** - Transform selector parameters to simplify composition and reuse
23
+ - **Enhanced Structured Selectors** - Better typed alternatives to createStructuredSelector with caching support
24
+ - **Key Selector Composition** - Compose key selectors for complex cache keys in re-reselect
25
+ - **Custom Caching** - Optimize performance with advanced garbage collection and cache strategies
26
+ - **TypeScript Support** - Full TypeScript type definitions with strict typing
27
+
28
+ ## Installation
29
+
30
+ reselect-kit requires **TypeScript 5.8 or later**.
31
+
32
+ ### Using npm or yarn
33
+
34
+ ```bash
35
+ # npm
36
+ npm install reselect-kit
37
+
38
+ # yarn
39
+ yarn add reselect-kit
40
+ ```
41
+
42
+ ## Examples
43
+
44
+ ### Comparing @veksa/reselect, @veksa/re-reselect, and reselect-kit
45
+
46
+ #### Problem: Working with optional nested data
47
+
48
+ Imagine we need to select a nested property from a state object that might not exist at every level.
49
+
50
+ #### @veksa/reselect implementation
51
+
52
+ ```typescript
53
+ import { createSelector } from '@veksa/reselect';
54
+
55
+ const getUser = (state) => state.user;
56
+
57
+ const getUserAddress = createSelector(getUser, (user) => {
58
+ // Need null checks at each level
59
+ if (user && user.contact && user.contact.address) {
60
+ return user.contact.address;
61
+ }
62
+ return undefined;
63
+ });
64
+
65
+ // Usage:
66
+ const address = getUserAddress(state); // May be undefined
67
+ ```
68
+
69
+ With standard @veksa/reselect, you need to handle potential null/undefined values at each level with conditional checks.
70
+
71
+ #### @veksa/re-reselect solution
72
+
73
+ ```typescript
74
+ import { createCachedSelector } from '@veksa/re-reselect';
75
+
76
+ const getUser = (state) => state.user;
77
+ const getUserId = (state, userId) => userId;
78
+
79
+ // Using re-reselect for caching by userId
80
+ const getUserAddressByUserId = createCachedSelector(getUser, getUserId, (user, userId) => {
81
+ // Still need null checks for nested properties
82
+ const targetUser = user[userId];
83
+ if (targetUser && targetUser.contact && targetUser.contact.address) {
84
+ return targetUser.contact.address;
85
+ }
86
+ return undefined;
87
+ })((state, userId) => userId);
88
+
89
+ // Usage with better caching by userId:
90
+ const address = getUserAddressByUserId(state, '123');
91
+ ```
92
+
93
+ @veksa/re-reselect adds caching benefits but still requires the same null checks for property access.
94
+
95
+ #### reselect-kit solution
96
+
97
+ ```typescript
98
+ import { createPathSelector } from 'reselect-kit';
99
+
100
+ // Clean path selection with built-in null handling
101
+ const getUser = createPathSelector((state) => state.user);
102
+
103
+ // For parametric selectors
104
+ const getUserByUserId = createPathSelector((state, userId) => state.users?.[userId]);
105
+
106
+ // Usage:
107
+ const address = getUser(state).address(); // Safely returns undefined if path is broken
108
+ const userAddress = getUserByUserId(state, '123').address(); // With parameters
109
+ ```
110
+
111
+ reselect-kit provides safer property access with path selectors that handle null/undefined values automatically.
112
+
113
+ ### Basic Usage Patterns
114
+
115
+ #### Segment Selector
116
+
117
+ ```typescript
118
+ import { createSegmentSelector } from 'reselect-kit';
119
+
120
+ interface IUserStore {
121
+ address: string;
122
+ }
123
+
124
+ interface IUserSegment {
125
+ user: IUserStore;
126
+ }
127
+
128
+ const defaultUser: IUserStore = { address: 'Default address' };
129
+
130
+ const getUserSegment = createSegmentSelector<IUserSegment, IUserStore>(
131
+ (state) => state.user,
132
+ defaultUser,
133
+ );
134
+ ```
135
+
136
+ #### Path Selector
137
+
138
+ ```typescript
139
+ import { createPathSelector } from 'reselect-kit';
140
+
141
+ const state = {
142
+ user: {
143
+ address: 'Some user address',
144
+ },
145
+ };
146
+
147
+ const getUserAddress = createPathSelector((state) => state.user).address();
148
+ ```
149
+
150
+ #### Prop Selector
151
+
152
+ ```typescript
153
+ import { createPropSelector } from 'reselect-kit';
154
+
155
+ const getUserIdFromProps = createPropSelector<{ userId: number }>().userId();
156
+ ```
157
+
158
+ #### Bound Selector
159
+
160
+ ```typescript
161
+ import { createSelector } from '@veksa/reselect';
162
+ import { createBoundSelector } from 'reselect-kit';
163
+
164
+ const getUserByName = createSelector(
165
+ (state) => state.users,
166
+ (state, props) => props.userName,
167
+ (users, userName) => users[userName],
168
+ );
169
+
170
+ const getAdmin = createBoundSelector(getUserByName, { userName: 'admin' });
171
+
172
+ // Usage:
173
+ const admin = getAdmin(state); // Same as getUserByName(state, {userName: 'admin'})
174
+ ```
175
+
176
+ #### Adapted Selector
177
+
178
+ ```typescript
179
+ import { createSelector } from '@veksa/reselect';
180
+ import { createAdaptedSelector } from 'reselect-kit';
181
+
182
+ const getUserByNameAndRole = createSelector(
183
+ (state) => state.users,
184
+ (state, props) => props.userName,
185
+ (state, props) => props.userRole,
186
+ (users, userName, userRole) => users[userName][userRole],
187
+ );
188
+
189
+ const getAdmin = createAdaptedSelector(getUserByNameAndRole, (props) => ({
190
+ userName: props.userName,
191
+ userRole: 'admin',
192
+ }));
193
+
194
+ // Usage:
195
+ const admin = getAdmin(state); // Same as getUserByName(state, {userName: 'admin'})
196
+ ```
197
+
198
+ #### Chain Selectors
199
+
200
+ ```typescript
201
+ import { createChainSelector } from 'reselect-kit';
202
+
203
+ // Create a base selector
204
+ const getUserData = (state) => state.users;
205
+
206
+ // Build a selector chain
207
+ const getActiveUserEmails = createChainSelector(getUserData)
208
+ .map((users) => users.filter((user) => user.active))
209
+ .map((activeUsers) => activeUsers.map((user) => user.email))
210
+ .build();
211
+
212
+ // Usage:
213
+ const activeEmails = getActiveUserEmails(state); // ['alice@example.com', 'bob@example.com']
214
+ ```
215
+
216
+ ## Advanced Examples
217
+
218
+ ### Using Structured Selectors
219
+
220
+ ```typescript
221
+ import { createCachedStructuredSelector } from 'reselect-kit';
222
+
223
+ const getUserProfile = createCachedStructuredSelector({
224
+ name: (state) => state.user.name,
225
+ email: (state) => state.user.email,
226
+ address: createPathSelector(
227
+ (state) => state.user,
228
+ (user) => user?.contact?.address,
229
+ ),
230
+ })((state, userId) => userId);
231
+
232
+ // Usage:
233
+ const profile = getUserProfile(state, '123');
234
+ // Returns: { name: 'John', email: 'john@example.com', address: { ... } }
235
+ ```
236
+
237
+ ### Key Selector Composition
238
+
239
+ ```typescript
240
+ import { stringComposeKeySelectors } from 'reselect-kit';
241
+
242
+ // Compose several key selectors into a single one. `stringComposeKeySelectors`
243
+ // joins the composed keys with `:`.
244
+ const keySelector = stringComposeKeySelectors(
245
+ (state, props) => props.userId,
246
+ (state, props) => props.view,
247
+ );
248
+
249
+ // Usage:
250
+ // keySelector(state, { userId: '123', view: 'details' }) returns '123:details'
251
+ ```
252
+
253
+ ## API Reference
254
+
255
+ ### Core Functions
256
+
257
+ #### createChainSelector
258
+
259
+ Creates a selector that can be chained with map and chain methods.
260
+
261
+ ```typescript
262
+ createChainSelector(baseSelector).map(transformFn).build();
263
+ ```
264
+
265
+ ##### ChainSelector Methods
266
+
267
+ ###### chain
268
+
269
+ Transforms the output of the previous selector by creating a new selector based on its result. This is where the real power of chain selectors comes in, allowing composition of selectors where the output of one becomes the context for creating the next.
270
+
271
+ ```typescript
272
+ chain<S2, R2>(fn: (result: R1) => Selector<S2, R2>, options?: ChainSelectorOptions): SelectorMonad
273
+ chain<S2, P2, R2>(fn: (result: R1) => Selector<S2, R2, [P2]>, options?: ChainSelectorOptions): SelectorMonad
274
+ ```
275
+
276
+ - **fn** - Function that receives the previous selector's result and returns a new selector
277
+ - **options** - Optional configuration for caching and key selection
278
+
279
+ **Examples:**
280
+
281
+ ```typescript
282
+ // Basic chaining - transform one selector's result into another selector
283
+ const userWithDetails = createChainSelector((state) => state.users)
284
+ .chain((users) => (state) => {
285
+ // This function receives the users result and returns a new selector
286
+ const userIds = Object.keys(users);
287
+ return userIds.map((id) => state.userDetails[id]);
288
+ })
289
+ .build();
290
+
291
+ // Parametric selectors with chain
292
+ const getUserPostsById = createChainSelector(propSelector)
293
+ .chain((props) => {
294
+ // Use the props to create a targeted selector
295
+ return createSelector(
296
+ (state) => state.users[props.userId],
297
+ (state) => state.posts,
298
+ (user, posts) => posts.filter((post) => post.authorId === user.id),
299
+ );
300
+ })
301
+ .build();
302
+
303
+ // Multiple chains
304
+ const getUserStats = createChainSelector((state) => state.users)
305
+ .chain((users) => (state) => Object.values(users).filter((user) => user.active))
306
+ .chain((activeUsers) => (state) => ({
307
+ activeCount: activeUsers.length,
308
+ totalCount: Object.keys(state.users).length,
309
+ activeRatio: activeUsers.length / Object.keys(state.users).length,
310
+ }))
311
+ .build();
312
+ ```
313
+
314
+ ###### map
315
+
316
+ Transforms the output of the selector with a simple transformation function. Unlike `chain`, `map` doesn't create a new selector but just transforms the result of the current one.
317
+
318
+ ```typescript
319
+ map<R2>(fn: (result: R1) => R2, options?: ChainSelectorOptions): SelectorMonad
320
+ ```
321
+
322
+ ###### build
323
+
324
+ Completes the chain and returns the final selector function that can be used in components.
325
+
326
+ ```typescript
327
+ build(): Selector<S, R, Params>
328
+ ```
329
+
330
+ #### createPathSelector
331
+
332
+ Creates a selector that safely accesses nested properties.
333
+
334
+ ```typescript
335
+ createPathSelector(sourceSelector, pathFn);
336
+ ```
337
+
338
+ #### createBoundSelector
339
+
340
+ Creates a selector with predefined parameters.
341
+
342
+ ```typescript
343
+ createBoundSelector(selector, getParams);
344
+ ```
345
+
346
+ #### createAdaptedSelector
347
+
348
+ Adapts a selector to work with a different parameter shape.
349
+
350
+ ```typescript
351
+ createAdaptedSelector(selector, paramsAdapter);
352
+ ```
353
+
354
+ #### createCachedStructuredSelector
355
+
356
+ Creates a structured selector with caching support.
357
+
358
+ ```typescript
359
+ createCachedStructuredSelector(selectors)(keySelector);
360
+ ```
361
+
362
+ #### createEmptySelector
363
+
364
+ Creates a selector that always returns `undefined` regardless of input. Useful as a placeholder or for conditional selection logic.
365
+
366
+ ```typescript
367
+ const emptySelector = createEmptySelector(baseSelector);
368
+ // Always returns undefined while maintaining the original selector's signature
369
+ ```
370
+
371
+ #### createPropSelector
372
+
373
+ Creates a selector that returns the props passed to it, enabling strongly-typed access to props in selector chains.
374
+
375
+ ```typescript
376
+ const propSelector = createPropSelector();
377
+ // Usage: propSelector(state, props) returns props
378
+
379
+ // In a chain
380
+ const userByIdSelector = createChainSelector(propSelector)
381
+ .chain((props) =>
382
+ createPathSelector(
383
+ (state) => state.users[props.userId],
384
+ (user) => user,
385
+ ),
386
+ )
387
+ .build();
388
+ ```
389
+
390
+ #### createSegmentSelector
391
+
392
+ Creates a selector with a default/initial value when the selection returns null or undefined.
393
+
394
+ ```typescript
395
+ const getUserSettings = createSegmentSelector(
396
+ (state) => state.userSettings,
397
+ { theme: 'light', notifications: true }, // Default value if userSettings is null/undefined
398
+ );
399
+ ```
400
+
401
+ #### createSequenceSelector
402
+
403
+ Creates a selector that returns an array of results from multiple selectors.
404
+
405
+ ```typescript
406
+ const getUserStats = createSequenceSelector([
407
+ (state) => state.user.postsCount,
408
+ (state) => state.user.followersCount,
409
+ (state) => state.user.likesCount,
410
+ ]);
411
+
412
+ // Returns [postsCount, followersCount, likesCount]
413
+ ```
414
+
415
+ ### Cache Objects
416
+
417
+ #### TreeCache
418
+
419
+ Implements a hierarchical cache structure for nested key support. Unlike flat cache objects, TreeCache allows for efficiently caching and retrieving selectors with complex, multi-part keys.
420
+
421
+ ```typescript
422
+ import { TreeCache } from 'reselect-kit';
423
+
424
+ // Create a TreeCache instance
425
+ const cache = new TreeCache({
426
+ cacheObjectCreator: () => new FlatObjectCache(), // Optional custom cache factory
427
+ });
428
+
429
+ // Use with complex keys (automatically normalized to arrays)
430
+ const complexKey = ['user', 123, 'profile'];
431
+ cache.set(complexKey, selectorInstance);
432
+ const selector = cache.get(complexKey);
433
+ ```
434
+
435
+ #### IntervalMapCache and Garbage Collection
436
+
437
+ A time-based cache implementation with automatic garbage collection for unused selectors to prevent memory leaks.
438
+
439
+ ```typescript
440
+ import { IntervalMapCache, initGarbageCollector } from 'reselect-kit';
441
+
442
+ // Initialize garbage collector to clean up stale cache entries
443
+ initGarbageCollector();
444
+
445
+ // Create a cache that automatically manages memory
446
+ const cache = new IntervalMapCache();
447
+
448
+ // Items not accessed within the cache lifetime will be automatically purged
449
+ cache.set('key', selectorData);
450
+ ```
451
+
452
+ ### Comparing with @veksa/reselect and @veksa/re-reselect
453
+
454
+ | Feature | @veksa/reselect | @veksa/re-reselect | reselect-kit |
455
+ | --------------------------- | --------------- | ------------------ | -------------------- |
456
+ | Basic memoization | ✓ | ✓ | ✓ (via dependencies) |
457
+ | Parametric memoization | × | ✓ | ✓ (via dependencies) |
458
+ | Safe nested property access | × | × | ✓ |
459
+ | Fluent selector chains | × | × | ✓ |
460
+ | Parameter binding | × | × | ✓ |
461
+ | Parameter adaptation | × | × | ✓ |
462
+ | Advanced key composition | × | Limited | ✓ |
463
+ | Cache garbage collection | × | × | ✓ |
464
+ | Hierarchical caching | × | × | ✓ |
465
+ | Typescript support | ✓ | ✓ | ✓ (enhanced) |
466
+
467
+ ## Contributing
468
+
469
+ This project welcomes contributions and suggestions. Feel free to submit a Pull Request.
470
+
471
+ ## License
472
+
473
+ [MIT](LICENSE.md)
@@ -0,0 +1,6 @@
1
+ 'use strict'
2
+ if (process.env.NODE_ENV === 'production') {
3
+ module.exports = require('./reselectKit.production.min.cjs')
4
+ } else {
5
+ module.exports = require('./reselectKit.development.cjs')
6
+ }