1 | import isPlainObject from './isPlainObject'
|
2 | import type { Middleware } from 'redux'
|
3 | import { getTimeMeasureUtils } from './utils'
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 | export function isPlain(val: any) {
|
15 | const type = typeof val
|
16 | return (
|
17 | type === 'undefined' ||
|
18 | val === null ||
|
19 | type === 'string' ||
|
20 | type === 'boolean' ||
|
21 | type === 'number' ||
|
22 | Array.isArray(val) ||
|
23 | isPlainObject(val)
|
24 | )
|
25 | }
|
26 |
|
27 | interface NonSerializableValue {
|
28 | keyPath: string
|
29 | value: unknown
|
30 | }
|
31 |
|
32 |
|
33 |
|
34 |
|
35 | export function findNonSerializableValue(
|
36 | value: unknown,
|
37 | path: string = '',
|
38 | isSerializable: (value: unknown) => boolean = isPlain,
|
39 | getEntries?: (value: unknown) => [string, any][],
|
40 | ignoredPaths: readonly string[] = []
|
41 | ): NonSerializableValue | false {
|
42 | let foundNestedSerializable: NonSerializableValue | false
|
43 |
|
44 | if (!isSerializable(value)) {
|
45 | return {
|
46 | keyPath: path || '<root>',
|
47 | value: value,
|
48 | }
|
49 | }
|
50 |
|
51 | if (typeof value !== 'object' || value === null) {
|
52 | return false
|
53 | }
|
54 |
|
55 | const entries = getEntries != null ? getEntries(value) : Object.entries(value)
|
56 |
|
57 | const hasIgnoredPaths = ignoredPaths.length > 0
|
58 |
|
59 | for (const [key, nestedValue] of entries) {
|
60 | const nestedPath = path ? path + '.' + key : key
|
61 |
|
62 | if (hasIgnoredPaths && ignoredPaths.indexOf(nestedPath) >= 0) {
|
63 | continue
|
64 | }
|
65 |
|
66 | if (!isSerializable(nestedValue)) {
|
67 | return {
|
68 | keyPath: nestedPath,
|
69 | value: nestedValue,
|
70 | }
|
71 | }
|
72 |
|
73 | if (typeof nestedValue === 'object') {
|
74 | foundNestedSerializable = findNonSerializableValue(
|
75 | nestedValue,
|
76 | nestedPath,
|
77 | isSerializable,
|
78 | getEntries,
|
79 | ignoredPaths
|
80 | )
|
81 |
|
82 | if (foundNestedSerializable) {
|
83 | return foundNestedSerializable
|
84 | }
|
85 | }
|
86 | }
|
87 |
|
88 | return false
|
89 | }
|
90 |
|
91 |
|
92 |
|
93 |
|
94 |
|
95 |
|
96 | export interface SerializableStateInvariantMiddlewareOptions {
|
97 | |
98 |
|
99 |
|
100 |
|
101 |
|
102 | isSerializable?: (value: any) => boolean
|
103 | |
104 |
|
105 |
|
106 |
|
107 |
|
108 | getEntries?: (value: any) => [string, any][]
|
109 |
|
110 | |
111 |
|
112 |
|
113 |
|
114 | ignoredActions?: string[]
|
115 |
|
116 | |
117 |
|
118 |
|
119 |
|
120 | ignoredActionPaths?: string[]
|
121 |
|
122 | |
123 |
|
124 |
|
125 |
|
126 | ignoredPaths?: string[]
|
127 | |
128 |
|
129 |
|
130 |
|
131 |
|
132 | warnAfter?: number
|
133 |
|
134 | |
135 |
|
136 |
|
137 | ignoreState?: boolean
|
138 |
|
139 | |
140 |
|
141 |
|
142 | ignoreActions?: boolean
|
143 | }
|
144 |
|
145 |
|
146 |
|
147 |
|
148 |
|
149 |
|
150 |
|
151 |
|
152 |
|
153 |
|
154 | export function createSerializableStateInvariantMiddleware(
|
155 | options: SerializableStateInvariantMiddlewareOptions = {}
|
156 | ): Middleware {
|
157 | if (process.env.NODE_ENV === 'production') {
|
158 | return () => (next) => (action) => next(action)
|
159 | }
|
160 | const {
|
161 | isSerializable = isPlain,
|
162 | getEntries,
|
163 | ignoredActions = [],
|
164 | ignoredActionPaths = ['meta.arg', 'meta.baseQueryMeta'],
|
165 | ignoredPaths = [],
|
166 | warnAfter = 32,
|
167 | ignoreState = false,
|
168 | ignoreActions = false,
|
169 | } = options
|
170 |
|
171 | return (storeAPI) => (next) => (action) => {
|
172 | const result = next(action)
|
173 |
|
174 | const measureUtils = getTimeMeasureUtils(
|
175 | warnAfter,
|
176 | 'SerializableStateInvariantMiddleware'
|
177 | )
|
178 |
|
179 | if (
|
180 | !ignoreActions &&
|
181 | !(ignoredActions.length && ignoredActions.indexOf(action.type) !== -1)
|
182 | ) {
|
183 | measureUtils.measureTime(() => {
|
184 | const foundActionNonSerializableValue = findNonSerializableValue(
|
185 | action,
|
186 | '',
|
187 | isSerializable,
|
188 | getEntries,
|
189 | ignoredActionPaths
|
190 | )
|
191 |
|
192 | if (foundActionNonSerializableValue) {
|
193 | const { keyPath, value } = foundActionNonSerializableValue
|
194 |
|
195 | console.error(
|
196 | `A non-serializable value was detected in an action, in the path: \`${keyPath}\`. Value:`,
|
197 | value,
|
198 | '\nTake a look at the logic that dispatched this action: ',
|
199 | action,
|
200 | '\n(See https://redux.js.org/faq/actions#why-should-type-be-a-string-or-at-least-serializable-why-should-my-action-types-be-constants)',
|
201 | '\n(To allow non-serializable values see: https://redux-toolkit.js.org/usage/usage-guide#working-with-non-serializable-data)'
|
202 | )
|
203 | }
|
204 | })
|
205 | }
|
206 |
|
207 | if (!ignoreState) {
|
208 | measureUtils.measureTime(() => {
|
209 | const state = storeAPI.getState()
|
210 |
|
211 | const foundStateNonSerializableValue = findNonSerializableValue(
|
212 | state,
|
213 | '',
|
214 | isSerializable,
|
215 | getEntries,
|
216 | ignoredPaths
|
217 | )
|
218 |
|
219 | if (foundStateNonSerializableValue) {
|
220 | const { keyPath, value } = foundStateNonSerializableValue
|
221 |
|
222 | console.error(
|
223 | `A non-serializable value was detected in the state, in the path: \`${keyPath}\`. Value:`,
|
224 | value,
|
225 | `
|
226 | Take a look at the reducer(s) handling this action type: ${action.type}.
|
227 | (See https://redux.js.org/faq/organizing-state#can-i-put-functions-promises-or-other-non-serializable-items-in-my-store-state)`
|
228 | )
|
229 | }
|
230 | })
|
231 |
|
232 | measureUtils.warnIfExceeded()
|
233 | }
|
234 |
|
235 | return result
|
236 | }
|
237 | }
|