-
Notifications
You must be signed in to change notification settings - Fork 9
/
makeEnhancer.js
123 lines (99 loc) · 4.03 KB
/
makeEnhancer.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
import invariant from 'invariant';
import { map, compose, uniq, forEach, o } from 'ramda';
import { enhanceStore, makeStoreInterface } from '@redux-tools/injectors';
import {
isActionFromNamespace,
defaultNamespace,
getStateByFeatureAndNamespace,
DEFAULT_FEATURE,
} from '@redux-tools/namespaces';
export const storeInterface = makeStoreInterface('middleware');
const noopEntry = {
path: ['@redux-tools/NOOP_MIDDLEWARE'],
value: () => next => action => next(action),
};
const makeEnhancer = () => {
// NOTE: Keys are entries, values are middleware with bound `dispatch` and `getState`.
let initializedEntries = new Map();
// NOTE: Sadly, because of how enhancers and middleware are structured, we need some escape hatches
// from scopes and closures. This is ugly, but I don't think we can solve this differently.
// NOTE: `outerNext` is either the next middleware in `applyMiddleware` or `store.dispatch`.
let outerNext;
// NOTE: This default implementation is necessary to ensure that the middleware works even without
// any injected middleware.
// NOTE `enhancerNext` calls all injected middleware and then `outerNext`.
let enhancerNext = action => {
invariant(outerNext, 'You need to apply the enhancer to a Redux store.');
return outerNext(action);
};
const injectedMiddleware = () => next => {
invariant(!outerNext, 'You cannot apply the injected middleware to multiple Redux stores.');
outerNext = next;
return action => enhancerNext(action);
};
// NOTE: composeEntries :: [Entry] -> Next
const composeEntries = entries => {
const chain = map(entry => {
const { namespace } = entry;
// NOTE: `innerNext` is either the next injected middleware or `outerNext`.
return innerNext => {
// NOTE: `entryNext` is a wrapper over the currently iterated-over injected middleware.
const entryNext = initializedEntries.get(entry)(innerNext);
return action =>
isActionFromNamespace(namespace, action) ? entryNext(action) : innerNext(action);
};
}, entries);
// NOTE: `pipe` is used to preserve injection order.
return compose(...chain)(outerNext);
};
const enhancer = createStore => (...args) => {
const prevStore = createStore(...args);
// NOTE: All of this logic is just to achieve the following behaviour:
// Every middleware is curried. In standard Redux, the first two arguments are bound immediately.
// However, when injecting the middleware, we are not able to easily provide the second argument
// immediately, because it changes whenever an entry is injected or ejected. That's why we only
// bind the first argument and then provide `next` once per any injection call. This behaviour
// is covered by unit tests, which may help explain this better.
const handleEntriesChanged = () => {
const nextEntries = [
...uniq(storeInterface.getEntries(nextStore)),
// NOTE: This is just a safeguard, because although `R.compose` is variadic,
// it still needs at least one function as an argument.
noopEntry,
];
const nextInitializedEntries = new Map();
// NOTE: We copy all necessary entries because it's simpler/faster than finding what has changed.
forEach(entry => {
const { namespace } = entry;
const { dispatch, getState } = nextStore;
nextInitializedEntries.set(
entry,
initializedEntries.get(entry) ||
entry.value({
namespace,
dispatch: o(dispatch, defaultNamespace(namespace)),
getState: nextStore.getState,
getNamespacedState: namespace
? feature =>
getStateByFeatureAndNamespace(
feature ?? entry.feature ?? DEFAULT_FEATURE,
namespace,
getState()
)
: null,
})
);
}, nextEntries);
initializedEntries = nextInitializedEntries;
enhancerNext = composeEntries(nextEntries);
};
const nextStore = enhanceStore(prevStore, storeInterface, {
onInjected: handleEntriesChanged,
onEjected: handleEntriesChanged,
});
return nextStore;
};
enhancer.injectedMiddleware = injectedMiddleware;
return enhancer;
};
export default makeEnhancer;