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.
- package/LICENSE.md +21 -0
- package/README.md +473 -0
- package/dist/cjs/index.js +6 -0
- package/dist/cjs/reselectKit.development.cjs +683 -0
- package/dist/cjs/reselectKit.development.cjs.map +1 -0
- package/dist/cjs/reselectKit.production.min.cjs +2 -0
- package/dist/cjs/reselectKit.production.min.cjs.map +1 -0
- package/dist/reselectKit.d.ts +248 -0
- package/dist/reselectKit.mjs +678 -0
- package/dist/reselectKit.mjs.map +1 -0
- package/package.json +70 -0
- package/src/__data__/state.ts +97 -0
- package/src/__tests__/createAdaptedSelector.test.ts +134 -0
- package/src/__tests__/createBoundSelector.test.ts +351 -0
- package/src/__tests__/createCachedSequenceSelector.test.ts +24 -0
- package/src/__tests__/createCachedStructuredSelector.test.ts +29 -0
- package/src/__tests__/createChainSelector.test.ts +404 -0
- package/src/__tests__/createEmptySelector.test.ts +99 -0
- package/src/__tests__/createPathSelector.test.ts +81 -0
- package/src/__tests__/createPropSelector.test.ts +36 -0
- package/src/__tests__/createSegmentSelector.test.ts +31 -0
- package/src/__tests__/createSequenceSelector.test.ts +62 -0
- package/src/_helpers/__tests__/arePathsEqual.test.ts +27 -0
- package/src/_helpers/__tests__/getObjectPaths.test.ts +56 -0
- package/src/_helpers/__tests__/temporaryAssign.test.ts +59 -0
- package/src/_helpers/arePathsEqual.ts +15 -0
- package/src/_helpers/defineDynamicSelectorName.ts +10 -0
- package/src/_helpers/generateSelectorKey.ts +15 -0
- package/src/_helpers/getObjectPaths.ts +13 -0
- package/src/_helpers/getSelectorName.ts +7 -0
- package/src/_helpers/isCachedSelector.ts +9 -0
- package/src/_helpers/isObject.ts +3 -0
- package/src/_helpers/stringifyFunction.ts +3 -0
- package/src/_helpers/temporaryAssign.ts +80 -0
- package/src/_hooks/__tests__/useMemoWith.test.ts +31 -0
- package/src/_hooks/__tests__/useSelector.test.ts +87 -0
- package/src/_hooks/useMemoWith.ts +11 -0
- package/src/_hooks/useSelector.ts +34 -0
- package/src/cache/TreeCache.ts +126 -0
- package/src/cache/__tests__/TreeCacheObject.test.ts +188 -0
- package/src/cache/__tests__/intervalMapCache.test.ts +133 -0
- package/src/cache/intervalMapCache.ts +97 -0
- package/src/createAdaptedSelector.ts +75 -0
- package/src/createBoundSelector.ts +197 -0
- package/src/createCachedSequenceSelector.ts +18 -0
- package/src/createCachedStructuredSelector.ts +44 -0
- package/src/createChainSelector.ts +230 -0
- package/src/createEmptySelector.ts +27 -0
- package/src/createPathSelector.ts +208 -0
- package/src/createPropSelector.ts +28 -0
- package/src/createSegmentSelector.ts +6 -0
- package/src/createSequenceSelector.ts +15 -0
- package/src/createStructuredSelector.ts +3 -0
- package/src/debug/__tests__/debug.test.ts +20 -0
- package/src/debug/debug.ts +7 -0
- package/src/debug/withDebugName.ts +28 -0
- package/src/index.ts +26 -0
- package/src/keys/__tests__/arrayComposeKeySelectors.test.ts +52 -0
- package/src/keys/__tests__/createKeySelectorCreator.test.ts +246 -0
- package/src/keys/__tests__/stringComposeKeySelectors.test.ts +52 -0
- package/src/keys/arrayComposeKeySelectors.ts +20 -0
- package/src/keys/createKeySelectorComposer.ts +68 -0
- package/src/keys/createKeySelectorCreator.ts +106 -0
- package/src/keys/defaultKeySelector.ts +1 -0
- package/src/keys/stringComposeKeySelectors.ts +15 -0
- package/src/types.ts +43 -0
- package/src/vitest-globals.d.ts +1 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { FlatMapCache, createCachedSelector } from '@veksa/re-reselect';
|
|
2
|
+
import { IntervalMapCache } from '../intervalMapCache';
|
|
3
|
+
import { TreeCache } from '../TreeCache';
|
|
4
|
+
|
|
5
|
+
describe('TreeCache', () => {
|
|
6
|
+
test('should validate key', () => {
|
|
7
|
+
let cache = new TreeCache({});
|
|
8
|
+
|
|
9
|
+
let actual: boolean;
|
|
10
|
+
actual = cache.isValidCacheKey(['1', 2]);
|
|
11
|
+
expect(actual).toBeTruthy();
|
|
12
|
+
|
|
13
|
+
actual = cache.isValidCacheKey(['1', {}]);
|
|
14
|
+
expect(actual).toBeFalsy();
|
|
15
|
+
|
|
16
|
+
cache = new TreeCache({
|
|
17
|
+
cacheObjectCreator: () => new FlatMapCache(),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
actual = cache.isValidCacheKey(['1', 2]);
|
|
21
|
+
expect(actual).toBeTruthy();
|
|
22
|
+
|
|
23
|
+
actual = cache.isValidCacheKey(['1', {}]);
|
|
24
|
+
expect(actual).toBeTruthy();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('should normalize scalar key', () => {
|
|
28
|
+
let cache = new TreeCache({});
|
|
29
|
+
|
|
30
|
+
let actual: boolean;
|
|
31
|
+
actual = cache.isValidCacheKey({});
|
|
32
|
+
expect(actual).toBeFalsy();
|
|
33
|
+
|
|
34
|
+
cache = new TreeCache({
|
|
35
|
+
cacheObjectCreator: () => new FlatMapCache(),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
actual = cache.isValidCacheKey({});
|
|
39
|
+
expect(actual).toBeTruthy();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('should return undefined if there is not value for key', () => {
|
|
43
|
+
const cache = new TreeCache({});
|
|
44
|
+
|
|
45
|
+
let actual: unknown;
|
|
46
|
+
actual = cache.get(['some', 'deep', 'key']);
|
|
47
|
+
expect(actual).toBeUndefined();
|
|
48
|
+
|
|
49
|
+
cache.set(['some', 'deep'], expect.anything());
|
|
50
|
+
actual = cache.get(['some', 'deep', 'key']);
|
|
51
|
+
expect(actual).toBeUndefined();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('should return undefined if key path is short', () => {
|
|
55
|
+
const cache = new TreeCache({});
|
|
56
|
+
const selectorFn = () => undefined;
|
|
57
|
+
|
|
58
|
+
cache.set(['some', 'deep', 'key'], selectorFn);
|
|
59
|
+
const actual = cache.get(['some', 'deep']);
|
|
60
|
+
expect(actual).toBeUndefined();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('should set value in key path', () => {
|
|
64
|
+
const cache = new TreeCache({});
|
|
65
|
+
const selectorFn = () => undefined;
|
|
66
|
+
|
|
67
|
+
cache.set(['some', 'deep', 'key'], selectorFn);
|
|
68
|
+
const actual = cache.get(['some', 'deep', 'key']);
|
|
69
|
+
expect(actual).toBe(selectorFn);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('should remove value in key path', () => {
|
|
73
|
+
const cache = new TreeCache({});
|
|
74
|
+
const selectorFn = () => undefined;
|
|
75
|
+
|
|
76
|
+
let actual: unknown;
|
|
77
|
+
cache.set(['some', 'deep', 'key'], selectorFn);
|
|
78
|
+
actual = cache.get(['some', 'deep', 'key']);
|
|
79
|
+
expect(actual).toBe(selectorFn);
|
|
80
|
+
|
|
81
|
+
cache.remove(['some', 'deep', 'key']);
|
|
82
|
+
actual = cache.get(['some', 'deep', 'key']);
|
|
83
|
+
expect(actual).toBeUndefined();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('should do nothing if there is not value for removable key', () => {
|
|
87
|
+
const cache = new TreeCache({});
|
|
88
|
+
const selectorFn = () => undefined;
|
|
89
|
+
|
|
90
|
+
cache.set(['some', 'deep'], selectorFn);
|
|
91
|
+
cache.remove(['some', 'deep', 'key']);
|
|
92
|
+
|
|
93
|
+
const actual = cache.get(['some', 'deep']);
|
|
94
|
+
expect(actual).toBe(selectorFn);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('should clear values', () => {
|
|
98
|
+
const cache = new TreeCache({});
|
|
99
|
+
const selectorFn = () => undefined;
|
|
100
|
+
const otherSelectorFn = () => undefined;
|
|
101
|
+
|
|
102
|
+
cache.set(['some', 'deep', 'key'], selectorFn);
|
|
103
|
+
cache.set(['some', 'other', 'key'], otherSelectorFn);
|
|
104
|
+
expect(cache.get(['some', 'deep', 'key'])).toBe(selectorFn);
|
|
105
|
+
expect(cache.get(['some', 'other', 'key'])).toBe(otherSelectorFn);
|
|
106
|
+
|
|
107
|
+
cache.clear();
|
|
108
|
+
expect(cache.get(['some', 'deep', 'key'])).toBeUndefined();
|
|
109
|
+
expect(cache.get(['some', 'other', 'key'])).toBeUndefined();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('should back a cached selector without rebinding its validator', () => {
|
|
113
|
+
/**
|
|
114
|
+
* `TreeCache.isValidCacheKey` reads `this.root`, so it only answers when the
|
|
115
|
+
* cached selector calls it as a method on the cache. A selector that detaches
|
|
116
|
+
* the validator into a local and calls it bare leaves `this` undefined and
|
|
117
|
+
* throws on the very first lookup, which makes a TreeCache-backed selector
|
|
118
|
+
* unusable rather than merely slow — and dropping the validator instead is no
|
|
119
|
+
* escape, since the default one rejects the array keys this cache exists to
|
|
120
|
+
* index.
|
|
121
|
+
*/
|
|
122
|
+
const cache = new TreeCache({
|
|
123
|
+
cacheObjectCreator: () => new IntervalMapCache(),
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
type State = { items: Record<number, number> };
|
|
127
|
+
type Props = { id: number; kind: string };
|
|
128
|
+
|
|
129
|
+
const selector = createCachedSelector(
|
|
130
|
+
(state: State, props: Props) => state.items[props.id],
|
|
131
|
+
(value: number) => value * 2,
|
|
132
|
+
)({
|
|
133
|
+
// A composite key is the whole reason to reach for TreeCache: it indexes the
|
|
134
|
+
// array level by level instead of stringifying it.
|
|
135
|
+
keySelector: (_state: State, props: Props) => [props.id, props.kind],
|
|
136
|
+
cacheObject: cache,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const state = { items: { 1: 21 } };
|
|
140
|
+
|
|
141
|
+
expect(selector(state, { id: 1, kind: 'a' })).toBe(42);
|
|
142
|
+
expect(selector(state, { id: 1, kind: 'a' })).toBe(42);
|
|
143
|
+
expect(selector.recomputations()).toBe(1);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('should treat a scalar key as a one-level path', () => {
|
|
147
|
+
const cache = new TreeCache({});
|
|
148
|
+
const selectorFn = () => undefined;
|
|
149
|
+
|
|
150
|
+
cache.set('key', selectorFn);
|
|
151
|
+
|
|
152
|
+
// The scalar and the single-element path address the same node, so `get` must
|
|
153
|
+
// not be able to tell them apart.
|
|
154
|
+
expect(cache.get('key')).toBe(selectorFn);
|
|
155
|
+
expect(cache.get(['key'])).toBe(selectorFn);
|
|
156
|
+
|
|
157
|
+
expect(cache.get('missing')).toBeUndefined();
|
|
158
|
+
|
|
159
|
+
cache.set(['other'], selectorFn);
|
|
160
|
+
expect(cache.get('other')).toBe(selectorFn);
|
|
161
|
+
|
|
162
|
+
cache.remove('key');
|
|
163
|
+
expect(cache.get('key')).toBeUndefined();
|
|
164
|
+
expect(cache.get(['key'])).toBeUndefined();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test('should return undefined for a scalar key that addresses an inner node', () => {
|
|
168
|
+
const cache = new TreeCache({});
|
|
169
|
+
const selectorFn = () => undefined;
|
|
170
|
+
|
|
171
|
+
cache.set(['some', 'deep'], selectorFn);
|
|
172
|
+
|
|
173
|
+
// `some` exists as a node but holds no selector of its own.
|
|
174
|
+
expect(cache.get('some')).toBeUndefined();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test('should persist values in middle of tree', () => {
|
|
178
|
+
const cache = new TreeCache({});
|
|
179
|
+
const selectorFn = () => undefined;
|
|
180
|
+
const otherSelectorFn = () => undefined;
|
|
181
|
+
|
|
182
|
+
cache.set(['some', 'deep'], selectorFn);
|
|
183
|
+
cache.set(['some', 'deep', 'key'], otherSelectorFn);
|
|
184
|
+
|
|
185
|
+
expect(cache.get(['some', 'deep'])).toBe(selectorFn);
|
|
186
|
+
expect(cache.get(['some', 'deep', 'key'])).toBe(otherSelectorFn);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
type IntervalMapCacheModule = typeof import('../intervalMapCache');
|
|
4
|
+
|
|
5
|
+
describe('IntervalMapCache', () => {
|
|
6
|
+
let IntervalMapCache: IntervalMapCacheModule['IntervalMapCache'];
|
|
7
|
+
let initGarbageCollector: IntervalMapCacheModule['initGarbageCollector'];
|
|
8
|
+
|
|
9
|
+
beforeEach(async () => {
|
|
10
|
+
// Fresh module state per test: the cache store, id counter and the
|
|
11
|
+
// garbage-collector guard all live at module scope.
|
|
12
|
+
vi.resetModules();
|
|
13
|
+
vi.useFakeTimers();
|
|
14
|
+
vi.setSystemTime(0);
|
|
15
|
+
|
|
16
|
+
const mod = await import('../intervalMapCache');
|
|
17
|
+
IntervalMapCache = mod.IntervalMapCache;
|
|
18
|
+
initGarbageCollector = mod.initGarbageCollector;
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
vi.clearAllTimers();
|
|
23
|
+
vi.useRealTimers();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('stores and reads values by key', () => {
|
|
27
|
+
const cache = new IntervalMapCache();
|
|
28
|
+
const value = { value: 1 };
|
|
29
|
+
|
|
30
|
+
cache.set('key', value);
|
|
31
|
+
expect(cache.get('key')).toBe(value);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('returns undefined for a missing key', () => {
|
|
35
|
+
const cache = new IntervalMapCache();
|
|
36
|
+
expect(cache.get('missing')).toBeUndefined();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('removes a value by key', () => {
|
|
40
|
+
const cache = new IntervalMapCache();
|
|
41
|
+
|
|
42
|
+
cache.set('key', { value: 1 });
|
|
43
|
+
cache.remove('key');
|
|
44
|
+
expect(cache.get('key')).toBeUndefined();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('clears all values of the instance', () => {
|
|
48
|
+
const cache = new IntervalMapCache();
|
|
49
|
+
|
|
50
|
+
cache.set('a', 1);
|
|
51
|
+
cache.set('b', 2);
|
|
52
|
+
cache.clear();
|
|
53
|
+
|
|
54
|
+
expect(cache.get('a')).toBeUndefined();
|
|
55
|
+
expect(cache.get('b')).toBeUndefined();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('keeps instances isolated from each other', () => {
|
|
59
|
+
const first = new IntervalMapCache();
|
|
60
|
+
const second = new IntervalMapCache();
|
|
61
|
+
|
|
62
|
+
first.set('key', 'first');
|
|
63
|
+
second.set('key', 'second');
|
|
64
|
+
|
|
65
|
+
expect(first.get('key')).toBe('first');
|
|
66
|
+
expect(second.get('key')).toBe('second');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Bug #3: remove() threw on an instance that was never written to.
|
|
70
|
+
test('remove does nothing for an instance that was never written to', () => {
|
|
71
|
+
const cache = new IntervalMapCache();
|
|
72
|
+
expect(() => cache.remove('missing')).not.toThrow();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Bug #1: the garbage collector iterated the wrong object and never evicted.
|
|
76
|
+
test('garbage collector evicts entries older than the cache lifetime', () => {
|
|
77
|
+
const cache = new IntervalMapCache();
|
|
78
|
+
initGarbageCollector();
|
|
79
|
+
|
|
80
|
+
cache.set('key', { value: 1 });
|
|
81
|
+
expect(cache.get('key')).toEqual({ value: 1 });
|
|
82
|
+
|
|
83
|
+
// Advance past two GC cycles without touching the entry.
|
|
84
|
+
vi.advanceTimersByTime(25000);
|
|
85
|
+
|
|
86
|
+
expect(cache.get('key')).toBeUndefined();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Bug #2: the timestamp was frozen at construction time, so touching an entry
|
|
90
|
+
// via get() must refresh its lifetime and keep it alive across GC cycles.
|
|
91
|
+
test('accessing an entry refreshes its lifetime', () => {
|
|
92
|
+
const cache = new IntervalMapCache();
|
|
93
|
+
initGarbageCollector();
|
|
94
|
+
|
|
95
|
+
cache.set('key', { value: 1 });
|
|
96
|
+
|
|
97
|
+
// Touch the entry within every GC cycle (cacheLifetime = 10000).
|
|
98
|
+
for (let i = 0; i < 3; i += 1) {
|
|
99
|
+
vi.advanceTimersByTime(8000);
|
|
100
|
+
cache.get('key');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
expect(cache.get('key')).toEqual({ value: 1 });
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Bug #2: set() must stamp entries with the current time, not the time the
|
|
107
|
+
// instance was constructed.
|
|
108
|
+
test('set stamps entries with the current time, not the construction time', () => {
|
|
109
|
+
const cache = new IntervalMapCache();
|
|
110
|
+
initGarbageCollector();
|
|
111
|
+
|
|
112
|
+
// Long after construction, write a fresh entry.
|
|
113
|
+
vi.advanceTimersByTime(20000);
|
|
114
|
+
cache.set('key', { value: 1 });
|
|
115
|
+
|
|
116
|
+
// One more GC cycle: the entry is only 10000ms old, so it must survive.
|
|
117
|
+
vi.advanceTimersByTime(10000);
|
|
118
|
+
|
|
119
|
+
expect(cache.get('key')).toEqual({ value: 1 });
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Bug #4: initGarbageCollector() must be idempotent and not stack timers.
|
|
123
|
+
test('initGarbageCollector does not stack collectors when called repeatedly', () => {
|
|
124
|
+
const setTimeoutSpy = vi.spyOn(window, 'setTimeout');
|
|
125
|
+
|
|
126
|
+
initGarbageCollector();
|
|
127
|
+
initGarbageCollector();
|
|
128
|
+
initGarbageCollector();
|
|
129
|
+
|
|
130
|
+
expect(setTimeoutSpy).toHaveBeenCalledTimes(1);
|
|
131
|
+
setTimeoutSpy.mockRestore();
|
|
132
|
+
});
|
|
133
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { ICacheObject } from '@veksa/re-reselect';
|
|
2
|
+
|
|
3
|
+
interface CacheEntry {
|
|
4
|
+
data: unknown;
|
|
5
|
+
time: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Buckets used to live in a module-level `Record<string, …>` keyed by an
|
|
10
|
+
* incrementing numeric id, which meant every `get` and `set` coerced that number to
|
|
11
|
+
* a string and re-read the global record — four times per cache hit in `get`.
|
|
12
|
+
* Holding the entries on the instance removes both, and a `Map` keeps non-string
|
|
13
|
+
* keys as they are instead of stringifying them.
|
|
14
|
+
*/
|
|
15
|
+
const buckets = new Set<IntervalMapCache>();
|
|
16
|
+
|
|
17
|
+
const cacheLifetime = 10000;
|
|
18
|
+
|
|
19
|
+
let garbageCollectorStarted = false;
|
|
20
|
+
|
|
21
|
+
const runGarbageCollector = () => {
|
|
22
|
+
const currentTime = Date.now();
|
|
23
|
+
|
|
24
|
+
buckets.forEach((bucket) => {
|
|
25
|
+
if (bucket.sweep(currentTime)) {
|
|
26
|
+
// A bucket the sweep emptied is dropped from the registry, so a cache whose
|
|
27
|
+
// selector has been discarded stops being reachable from module scope and
|
|
28
|
+
// becomes collectable. Registration happens again on the next `set`, so a
|
|
29
|
+
// cache that is still in use simply re-enters.
|
|
30
|
+
buckets.delete(bucket);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
window.setTimeout(runGarbageCollector, cacheLifetime);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const initGarbageCollector = () => {
|
|
38
|
+
if (garbageCollectorStarted) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (typeof window !== 'undefined') {
|
|
43
|
+
garbageCollectorStarted = true;
|
|
44
|
+
window.setTimeout(runGarbageCollector, cacheLifetime);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export class IntervalMapCache implements ICacheObject {
|
|
49
|
+
private entries = new Map<unknown, CacheEntry>();
|
|
50
|
+
|
|
51
|
+
public set(key: any, data: any) {
|
|
52
|
+
const entry = this.entries.get(key);
|
|
53
|
+
|
|
54
|
+
if (entry === undefined) {
|
|
55
|
+
buckets.add(this);
|
|
56
|
+
this.entries.set(key, { data, time: Date.now() });
|
|
57
|
+
} else {
|
|
58
|
+
entry.data = data;
|
|
59
|
+
entry.time = Date.now();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
public get(key: any) {
|
|
64
|
+
const entry = this.entries.get(key);
|
|
65
|
+
|
|
66
|
+
if (entry === undefined) {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
entry.time = Date.now();
|
|
71
|
+
|
|
72
|
+
return entry.data;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
public remove(key: any) {
|
|
76
|
+
this.entries.delete(key);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
public clear() {
|
|
80
|
+
this.entries.clear();
|
|
81
|
+
buckets.delete(this);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Drops entries untouched for longer than the lifetime. Returns whether the
|
|
86
|
+
* bucket is now empty, which is what lets the collector unregister it.
|
|
87
|
+
*/
|
|
88
|
+
public sweep(currentTime: number) {
|
|
89
|
+
this.entries.forEach((entry, key) => {
|
|
90
|
+
if (currentTime - entry.time > cacheLifetime) {
|
|
91
|
+
this.entries.delete(key);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
return this.entries.size === 0;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Selector } from '@veksa/reselect';
|
|
2
|
+
import { CachedSelector, NamedParametricSelector } from './types';
|
|
3
|
+
import { withDebugName } from './debug/withDebugName';
|
|
4
|
+
import { getSelectorName } from './_helpers/getSelectorName';
|
|
5
|
+
import { isCachedSelector } from './_helpers/isCachedSelector';
|
|
6
|
+
|
|
7
|
+
const generateMappingName = <P1 extends object, P2>(mapping: (props: P2) => P1) => {
|
|
8
|
+
if (mapping.name) {
|
|
9
|
+
return mapping.name;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const structure = mapping(
|
|
13
|
+
new Proxy(
|
|
14
|
+
{},
|
|
15
|
+
{
|
|
16
|
+
get: (_target, key) => key,
|
|
17
|
+
},
|
|
18
|
+
) as P2,
|
|
19
|
+
);
|
|
20
|
+
const structureKeys = Object.keys(structure).join();
|
|
21
|
+
const structureValues = Object.values(structure).join();
|
|
22
|
+
|
|
23
|
+
return `${structureKeys} -> ${structureValues}`;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const innerCreateAdaptedSelector =
|
|
27
|
+
<S, P1, P2, R>(
|
|
28
|
+
baseSelector: Selector<S, R, [P1]>,
|
|
29
|
+
mapping: (props: P2) => P1,
|
|
30
|
+
): NamedParametricSelector<S, P2, R> =>
|
|
31
|
+
(state: S, props: P2) => {
|
|
32
|
+
return baseSelector(state, mapping(props));
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const createAdaptedSelector = <S, P1 extends object, P2, R>(
|
|
36
|
+
baseSelector: Selector<S, R, [P1]>,
|
|
37
|
+
mapping: (props: P2) => P1,
|
|
38
|
+
) => {
|
|
39
|
+
const adaptedSelector = innerCreateAdaptedSelector(baseSelector, mapping);
|
|
40
|
+
|
|
41
|
+
Object.assign(adaptedSelector, baseSelector);
|
|
42
|
+
adaptedSelector.dependencies = [baseSelector];
|
|
43
|
+
|
|
44
|
+
withDebugName(adaptedSelector, () => {
|
|
45
|
+
const baseName = getSelectorName(baseSelector);
|
|
46
|
+
const mappingName = generateMappingName(mapping);
|
|
47
|
+
|
|
48
|
+
return `${baseName} (${mappingName})`;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
if (isCachedSelector(baseSelector)) {
|
|
52
|
+
const cachedAdaptedSelector = adaptedSelector as unknown as CachedSelector;
|
|
53
|
+
|
|
54
|
+
if (baseSelector.getMatchingSelector) {
|
|
55
|
+
cachedAdaptedSelector.getMatchingSelector = innerCreateAdaptedSelector(
|
|
56
|
+
baseSelector.getMatchingSelector,
|
|
57
|
+
mapping,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (baseSelector.removeMatchingSelector) {
|
|
62
|
+
cachedAdaptedSelector.removeMatchingSelector = innerCreateAdaptedSelector(
|
|
63
|
+
baseSelector.removeMatchingSelector,
|
|
64
|
+
mapping,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
cachedAdaptedSelector.keySelector = innerCreateAdaptedSelector(
|
|
69
|
+
baseSelector.keySelector,
|
|
70
|
+
mapping,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return adaptedSelector;
|
|
75
|
+
};
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { Selector } from '@veksa/reselect';
|
|
2
|
+
import { CachedSelector, NamedParametricSelector, NamedSelector } from './types';
|
|
3
|
+
import { isComposedKeySelector, KeySelectorComposer } from './keys/createKeySelectorComposer';
|
|
4
|
+
import { isPropSelector } from './createPropSelector';
|
|
5
|
+
import { excludeDefaultSelectors } from './keys/createKeySelectorCreator';
|
|
6
|
+
import { stringComposeKeySelectors } from './keys/stringComposeKeySelectors';
|
|
7
|
+
import { temporaryAssign } from './_helpers/temporaryAssign';
|
|
8
|
+
import { withDebugName } from './debug/withDebugName';
|
|
9
|
+
import { getSelectorName } from './_helpers/getSelectorName';
|
|
10
|
+
import { isCachedSelector } from './_helpers/isCachedSelector';
|
|
11
|
+
import { isObject } from './_helpers/isObject';
|
|
12
|
+
import { getObjectPaths } from './_helpers/getObjectPaths';
|
|
13
|
+
import { arePathsEqual } from './_helpers/arePathsEqual';
|
|
14
|
+
import { defaultKeySelector } from './keys/defaultKeySelector';
|
|
15
|
+
|
|
16
|
+
const generateBindingName = <P extends object>(binding: P) => {
|
|
17
|
+
const structure = Object.keys(binding).reduce(
|
|
18
|
+
(result, key) => ({
|
|
19
|
+
...result,
|
|
20
|
+
[key]: '[*]',
|
|
21
|
+
}),
|
|
22
|
+
{},
|
|
23
|
+
);
|
|
24
|
+
const structureKeys = Object.keys(structure).join();
|
|
25
|
+
const structureValues = Object.values(structure).join();
|
|
26
|
+
|
|
27
|
+
return `${structureKeys} -> ${structureValues}`;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The special type to prevent binding of non optional props on optional values
|
|
32
|
+
*/
|
|
33
|
+
export type BoundSelector<S, P2, P1 extends Partial<P2>, R> =
|
|
34
|
+
P2 extends Pick<P1, keyof P2>
|
|
35
|
+
? Exclude<keyof P1, keyof P2> extends never
|
|
36
|
+
? NamedSelector<S, R>
|
|
37
|
+
: NamedParametricSelector<S, Omit<P1, keyof P2>, R>
|
|
38
|
+
: never;
|
|
39
|
+
|
|
40
|
+
export type BoundSelectorOptions<S, P2, P1 extends Partial<P2>, R> = {
|
|
41
|
+
bindingStrategy?: (
|
|
42
|
+
baseSelector: Selector<S, R, [P1]>,
|
|
43
|
+
binding: P2,
|
|
44
|
+
) => Selector<S, R, [Omit<P1, keyof P2>]>;
|
|
45
|
+
keySelectorComposer?: KeySelectorComposer;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const innerCreateBoundSelector = <S, P2, P1 extends Partial<P2>, R>(
|
|
49
|
+
baseSelector: Selector<S, R, [P1]>,
|
|
50
|
+
binding: P2,
|
|
51
|
+
) => {
|
|
52
|
+
const boundSelector = (state: S, props: Omit<P1, keyof P2> | void) =>
|
|
53
|
+
baseSelector(state, {
|
|
54
|
+
...props,
|
|
55
|
+
...binding,
|
|
56
|
+
} as unknown as P1);
|
|
57
|
+
|
|
58
|
+
return boundSelector as BoundSelector<S, P2, P1, R>;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const createBoundInnerSelector = <S, P2 extends object, P1 extends Partial<P2>, R, OR extends R>(
|
|
62
|
+
baseSelector: Selector<S, R, [P1]>,
|
|
63
|
+
binding: P2,
|
|
64
|
+
options: BoundSelectorOptions<S, P2, P1, OR> = {},
|
|
65
|
+
): BoundSelector<S, P2, P1, R> => {
|
|
66
|
+
const bindingStrategy =
|
|
67
|
+
(options.bindingStrategy as typeof innerCreateBoundSelector) ?? innerCreateBoundSelector;
|
|
68
|
+
|
|
69
|
+
const keySelectorComposer = options.keySelectorComposer ?? stringComposeKeySelectors;
|
|
70
|
+
|
|
71
|
+
const boundSelector = bindingStrategy(baseSelector, binding);
|
|
72
|
+
|
|
73
|
+
Object.assign(boundSelector, baseSelector);
|
|
74
|
+
boundSelector.dependencies = [baseSelector];
|
|
75
|
+
|
|
76
|
+
withDebugName(boundSelector, () => {
|
|
77
|
+
const baseName = getSelectorName(baseSelector);
|
|
78
|
+
const bindingName = generateBindingName(binding);
|
|
79
|
+
|
|
80
|
+
return `${baseName} (${bindingName})`;
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
if (isCachedSelector(baseSelector)) {
|
|
84
|
+
const cachedBoundSelector = boundSelector as unknown as CachedSelector;
|
|
85
|
+
|
|
86
|
+
if (baseSelector.getMatchingSelector) {
|
|
87
|
+
cachedBoundSelector.getMatchingSelector = bindingStrategy(
|
|
88
|
+
baseSelector.getMatchingSelector,
|
|
89
|
+
binding,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (baseSelector.removeMatchingSelector) {
|
|
94
|
+
cachedBoundSelector.removeMatchingSelector = bindingStrategy(
|
|
95
|
+
baseSelector.removeMatchingSelector,
|
|
96
|
+
binding,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const baseKeySelector = baseSelector.keySelector;
|
|
101
|
+
|
|
102
|
+
if (isObject(binding)) {
|
|
103
|
+
const paths = getObjectPaths(binding);
|
|
104
|
+
let keySelectors = isComposedKeySelector(baseKeySelector)
|
|
105
|
+
? baseKeySelector.dependencies
|
|
106
|
+
: [baseKeySelector];
|
|
107
|
+
|
|
108
|
+
keySelectors = excludeDefaultSelectors(keySelectors);
|
|
109
|
+
|
|
110
|
+
keySelectors = keySelectors
|
|
111
|
+
.filter(
|
|
112
|
+
(keySelector) =>
|
|
113
|
+
!isPropSelector(keySelector) ||
|
|
114
|
+
!paths.some((path) => arePathsEqual(keySelector.path, path)),
|
|
115
|
+
)
|
|
116
|
+
.map((keySelector) =>
|
|
117
|
+
isPropSelector(keySelector) ? keySelector : bindingStrategy(keySelector, binding),
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
if (keySelectors.length === 0) {
|
|
121
|
+
cachedBoundSelector.keySelector = defaultKeySelector;
|
|
122
|
+
} else if (keySelectors.length === 1) {
|
|
123
|
+
const [keySelector] = keySelectors;
|
|
124
|
+
cachedBoundSelector.keySelector = keySelector;
|
|
125
|
+
} else {
|
|
126
|
+
cachedBoundSelector.keySelector = keySelectorComposer(...keySelectors);
|
|
127
|
+
}
|
|
128
|
+
} else {
|
|
129
|
+
cachedBoundSelector.keySelector = bindingStrategy(baseKeySelector, binding);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return boundSelector;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
export const createBoundSelector: typeof createBoundInnerSelector = (baseSelector, binding) => {
|
|
137
|
+
return createBoundInnerSelector(baseSelector, binding, {
|
|
138
|
+
bindingStrategy: (selector, bindingProps) => {
|
|
139
|
+
const source = bindingProps as unknown as Record<string, unknown>;
|
|
140
|
+
const keys = Object.keys(source);
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Almost every binding is a single key — `bound(selector, { positionId })` —
|
|
144
|
+
* and specialising it removes the two allocations the generic path costs per
|
|
145
|
+
* call: the `{ result, rollback }` object and the `rollback` closure. On a
|
|
146
|
+
* graph with a bound selector per row those are the calls that dominate.
|
|
147
|
+
*
|
|
148
|
+
* The `delete` in the restore stays deliberately. Leaving the key behind as
|
|
149
|
+
* `undefined` would be cheaper — it avoids putting the caller's props object
|
|
150
|
+
* into dictionary mode — but it also changes the object's shape, and
|
|
151
|
+
* `useSelector` memoizes props shallowly: a props object that has grown an
|
|
152
|
+
* extra key never again compares equal to a freshly built one, so the
|
|
153
|
+
* memoization the binding exists to protect would be defeated.
|
|
154
|
+
*/
|
|
155
|
+
if (keys.length === 1) {
|
|
156
|
+
const key = keys[0];
|
|
157
|
+
const value = source[key];
|
|
158
|
+
|
|
159
|
+
return (state, props) => {
|
|
160
|
+
const target = props as unknown as Record<string, unknown>;
|
|
161
|
+
|
|
162
|
+
if (typeof target !== 'object' || target === null) {
|
|
163
|
+
return selector(state, source as Parameters<typeof baseSelector>[1]);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const previous = target[key];
|
|
167
|
+
|
|
168
|
+
// A re-entrant call that finds the binding already applied has nothing to
|
|
169
|
+
// save and nothing to restore.
|
|
170
|
+
if (previous === value) {
|
|
171
|
+
return selector(state, props as Parameters<typeof baseSelector>[1]);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const had = Object.prototype.hasOwnProperty.call(target, key);
|
|
175
|
+
target[key] = value;
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
return selector(state, props as Parameters<typeof baseSelector>[1]);
|
|
179
|
+
} finally {
|
|
180
|
+
if (had) {
|
|
181
|
+
target[key] = previous;
|
|
182
|
+
} else {
|
|
183
|
+
delete target[key];
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return (state, props) => {
|
|
190
|
+
const { result: nextProps, rollback } = temporaryAssign(props, source);
|
|
191
|
+
const result = selector(state, nextProps as Parameters<typeof baseSelector>[1]);
|
|
192
|
+
rollback();
|
|
193
|
+
return result;
|
|
194
|
+
};
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
};
|