source: node_modules/@reduxjs/toolkit/src/immutableStateInvariantMiddleware.ts@ ba17441

Last change on this file since ba17441 was a762898, checked in by istevanoska <ilinastevanoska@…>, 5 months ago

Added visualizations

  • Property mode set to 100644
File size: 7.3 KB
Line 
1import type { Middleware } from 'redux'
2import type { IgnorePaths } from './serializableStateInvariantMiddleware'
3import { getTimeMeasureUtils } from './utils'
4
5type EntryProcessor = (key: string, value: any) => any
6
7/**
8 * The default `isImmutable` function.
9 *
10 * @public
11 */
12export function isImmutableDefault(value: unknown): boolean {
13 return typeof value !== 'object' || value == null || Object.isFrozen(value)
14}
15
16export function trackForMutations(
17 isImmutable: IsImmutableFunc,
18 ignoredPaths: IgnorePaths | undefined,
19 obj: any,
20) {
21 const trackedProperties = trackProperties(isImmutable, ignoredPaths, obj)
22 return {
23 detectMutations() {
24 return detectMutations(isImmutable, ignoredPaths, trackedProperties, obj)
25 },
26 }
27}
28
29interface TrackedProperty {
30 value: any
31 children: Record<string, any>
32}
33
34function trackProperties(
35 isImmutable: IsImmutableFunc,
36 ignoredPaths: IgnorePaths = [],
37 obj: Record<string, any>,
38 path: string = '',
39 checkedObjects: Set<Record<string, any>> = new Set(),
40) {
41 const tracked: Partial<TrackedProperty> = { value: obj }
42
43 if (!isImmutable(obj) && !checkedObjects.has(obj)) {
44 checkedObjects.add(obj)
45 tracked.children = {}
46
47 const hasIgnoredPaths = ignoredPaths.length > 0
48
49 for (const key in obj) {
50 const nestedPath = path ? path + '.' + key : key
51
52 if (hasIgnoredPaths) {
53 const hasMatches = ignoredPaths.some((ignored) => {
54 if (ignored instanceof RegExp) {
55 return ignored.test(nestedPath)
56 }
57 return nestedPath === ignored
58 })
59 if (hasMatches) {
60 continue
61 }
62 }
63
64 tracked.children[key] = trackProperties(
65 isImmutable,
66 ignoredPaths,
67 obj[key],
68 nestedPath,
69 )
70 }
71 }
72 return tracked as TrackedProperty
73}
74
75function detectMutations(
76 isImmutable: IsImmutableFunc,
77 ignoredPaths: IgnorePaths = [],
78 trackedProperty: TrackedProperty,
79 obj: any,
80 sameParentRef: boolean = false,
81 path: string = '',
82): { wasMutated: boolean; path?: string } {
83 const prevObj = trackedProperty ? trackedProperty.value : undefined
84
85 const sameRef = prevObj === obj
86
87 if (sameParentRef && !sameRef && !Number.isNaN(obj)) {
88 return { wasMutated: true, path }
89 }
90
91 if (isImmutable(prevObj) || isImmutable(obj)) {
92 return { wasMutated: false }
93 }
94
95 // Gather all keys from prev (tracked) and after objs
96 const keysToDetect: Record<string, boolean> = {}
97 for (let key in trackedProperty.children) {
98 keysToDetect[key] = true
99 }
100 for (let key in obj) {
101 keysToDetect[key] = true
102 }
103
104 const hasIgnoredPaths = ignoredPaths.length > 0
105
106 for (let key in keysToDetect) {
107 const nestedPath = path ? path + '.' + key : key
108
109 if (hasIgnoredPaths) {
110 const hasMatches = ignoredPaths.some((ignored) => {
111 if (ignored instanceof RegExp) {
112 return ignored.test(nestedPath)
113 }
114 return nestedPath === ignored
115 })
116 if (hasMatches) {
117 continue
118 }
119 }
120
121 const result = detectMutations(
122 isImmutable,
123 ignoredPaths,
124 trackedProperty.children[key],
125 obj[key],
126 sameRef,
127 nestedPath,
128 )
129
130 if (result.wasMutated) {
131 return result
132 }
133 }
134 return { wasMutated: false }
135}
136
137type IsImmutableFunc = (value: any) => boolean
138
139/**
140 * Options for `createImmutableStateInvariantMiddleware()`.
141 *
142 * @public
143 */
144export interface ImmutableStateInvariantMiddlewareOptions {
145 /**
146 Callback function to check if a value is considered to be immutable.
147 This function is applied recursively to every value contained in the state.
148 The default implementation will return true for primitive types
149 (like numbers, strings, booleans, null and undefined).
150 */
151 isImmutable?: IsImmutableFunc
152 /**
153 An array of dot-separated path strings that match named nodes from
154 the root state to ignore when checking for immutability.
155 Defaults to undefined
156 */
157 ignoredPaths?: IgnorePaths
158 /** Print a warning if checks take longer than N ms. Default: 32ms */
159 warnAfter?: number
160}
161
162/**
163 * Creates a middleware that checks whether any state was mutated in between
164 * dispatches or during a dispatch. If any mutations are detected, an error is
165 * thrown.
166 *
167 * @param options Middleware options.
168 *
169 * @public
170 */
171export function createImmutableStateInvariantMiddleware(
172 options: ImmutableStateInvariantMiddlewareOptions = {},
173): Middleware {
174 if (process.env.NODE_ENV === 'production') {
175 return () => (next) => (action) => next(action)
176 } else {
177 function stringify(
178 obj: any,
179 serializer?: EntryProcessor,
180 indent?: string | number,
181 decycler?: EntryProcessor,
182 ): string {
183 return JSON.stringify(obj, getSerialize(serializer, decycler), indent)
184 }
185
186 function getSerialize(
187 serializer?: EntryProcessor,
188 decycler?: EntryProcessor,
189 ): EntryProcessor {
190 let stack: any[] = [],
191 keys: any[] = []
192
193 if (!decycler)
194 decycler = function (_: string, value: any) {
195 if (stack[0] === value) return '[Circular ~]'
196 return (
197 '[Circular ~.' + keys.slice(0, stack.indexOf(value)).join('.') + ']'
198 )
199 }
200
201 return function (this: any, key: string, value: any) {
202 if (stack.length > 0) {
203 var thisPos = stack.indexOf(this)
204 ~thisPos ? stack.splice(thisPos + 1) : stack.push(this)
205 ~thisPos ? keys.splice(thisPos, Infinity, key) : keys.push(key)
206 if (~stack.indexOf(value)) value = decycler!.call(this, key, value)
207 } else stack.push(value)
208
209 return serializer == null ? value : serializer.call(this, key, value)
210 }
211 }
212
213 let {
214 isImmutable = isImmutableDefault,
215 ignoredPaths,
216 warnAfter = 32,
217 } = options
218
219 const track = trackForMutations.bind(null, isImmutable, ignoredPaths)
220
221 return ({ getState }) => {
222 let state = getState()
223 let tracker = track(state)
224
225 let result
226 return (next) => (action) => {
227 const measureUtils = getTimeMeasureUtils(
228 warnAfter,
229 'ImmutableStateInvariantMiddleware',
230 )
231
232 measureUtils.measureTime(() => {
233 state = getState()
234
235 result = tracker.detectMutations()
236 // Track before potentially not meeting the invariant
237 tracker = track(state)
238
239 if (result.wasMutated) {
240 throw new Error(
241 `A state mutation was detected between dispatches, in the path '${
242 result.path || ''
243 }'. This may cause incorrect behavior. (https://redux.js.org/style-guide/style-guide#do-not-mutate-state)`,
244 )
245 }
246 })
247
248 const dispatchedAction = next(action)
249
250 measureUtils.measureTime(() => {
251 state = getState()
252
253 result = tracker.detectMutations()
254 // Track before potentially not meeting the invariant
255 tracker = track(state)
256
257 if (result.wasMutated) {
258 throw new Error(
259 `A state mutation was detected inside a dispatch, in the path: ${
260 result.path || ''
261 }. Take a look at the reducer(s) handling the action ${stringify(
262 action,
263 )}. (https://redux.js.org/style-guide/style-guide#do-not-mutate-state)`,
264 )
265 }
266 })
267
268 measureUtils.warnIfExceeded()
269
270 return dispatchedAction
271 }
272 }
273 }
274}
Note: See TracBrowser for help on using the repository browser.