This commit is contained in:
291
node_modules/@reduxjs/toolkit/src/immutableStateInvariantMiddleware.ts
generated
vendored
Normal file
291
node_modules/@reduxjs/toolkit/src/immutableStateInvariantMiddleware.ts
generated
vendored
Normal file
@@ -0,0 +1,291 @@
|
||||
import type { Middleware } from 'redux'
|
||||
import { getTimeMeasureUtils } from './utils'
|
||||
|
||||
type EntryProcessor = (key: string, value: any) => any
|
||||
|
||||
const isProduction: boolean = process.env.NODE_ENV === 'production'
|
||||
const prefix: string = 'Invariant failed'
|
||||
|
||||
// Throw an error if the condition fails
|
||||
// Strip out error messages for production
|
||||
// > Not providing an inline default argument for message as the result is smaller
|
||||
function invariant(condition: any, message?: string) {
|
||||
if (condition) {
|
||||
return
|
||||
}
|
||||
// Condition not passed
|
||||
|
||||
// In production we strip the message but still throw
|
||||
if (isProduction) {
|
||||
throw new Error(prefix)
|
||||
}
|
||||
|
||||
// When not in production we allow the message to pass through
|
||||
// *This block will be removed in production builds*
|
||||
throw new Error(`${prefix}: ${message || ''}`)
|
||||
}
|
||||
|
||||
function stringify(
|
||||
obj: any,
|
||||
serializer?: EntryProcessor,
|
||||
indent?: string | number,
|
||||
decycler?: EntryProcessor
|
||||
): string {
|
||||
return JSON.stringify(obj, getSerialize(serializer, decycler), indent)
|
||||
}
|
||||
|
||||
function getSerialize(
|
||||
serializer?: EntryProcessor,
|
||||
decycler?: EntryProcessor
|
||||
): EntryProcessor {
|
||||
let stack: any[] = [],
|
||||
keys: any[] = []
|
||||
|
||||
if (!decycler)
|
||||
decycler = function (_: string, value: any) {
|
||||
if (stack[0] === value) return '[Circular ~]'
|
||||
return (
|
||||
'[Circular ~.' + keys.slice(0, stack.indexOf(value)).join('.') + ']'
|
||||
)
|
||||
}
|
||||
|
||||
return function (this: any, key: string, value: any) {
|
||||
if (stack.length > 0) {
|
||||
var thisPos = stack.indexOf(this)
|
||||
~thisPos ? stack.splice(thisPos + 1) : stack.push(this)
|
||||
~thisPos ? keys.splice(thisPos, Infinity, key) : keys.push(key)
|
||||
if (~stack.indexOf(value)) value = decycler!.call(this, key, value)
|
||||
} else stack.push(value)
|
||||
|
||||
return serializer == null ? value : serializer.call(this, key, value)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The default `isImmutable` function.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function isImmutableDefault(value: unknown): boolean {
|
||||
return typeof value !== 'object' || value == null || Object.isFrozen(value)
|
||||
}
|
||||
|
||||
export function trackForMutations(
|
||||
isImmutable: IsImmutableFunc,
|
||||
ignorePaths: IgnorePaths | undefined,
|
||||
obj: any
|
||||
) {
|
||||
const trackedProperties = trackProperties(isImmutable, ignorePaths, obj)
|
||||
return {
|
||||
detectMutations() {
|
||||
return detectMutations(isImmutable, ignorePaths, trackedProperties, obj)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
interface TrackedProperty {
|
||||
value: any
|
||||
children: Record<string, any>
|
||||
}
|
||||
|
||||
function trackProperties(
|
||||
isImmutable: IsImmutableFunc,
|
||||
ignorePaths: IgnorePaths = [],
|
||||
obj: Record<string, any>,
|
||||
path: string = '',
|
||||
checkedObjects: Set<Record<string, any>> = new Set()
|
||||
) {
|
||||
const tracked: Partial<TrackedProperty> = { value: obj }
|
||||
|
||||
if (!isImmutable(obj) && !checkedObjects.has(obj)) {
|
||||
checkedObjects.add(obj);
|
||||
tracked.children = {}
|
||||
|
||||
for (const key in obj) {
|
||||
const childPath = path ? path + '.' + key : key
|
||||
if (ignorePaths.length && ignorePaths.indexOf(childPath) !== -1) {
|
||||
continue
|
||||
}
|
||||
|
||||
tracked.children[key] = trackProperties(
|
||||
isImmutable,
|
||||
ignorePaths,
|
||||
obj[key],
|
||||
childPath
|
||||
)
|
||||
}
|
||||
}
|
||||
return tracked as TrackedProperty
|
||||
}
|
||||
|
||||
type IgnorePaths = readonly (string | RegExp)[]
|
||||
|
||||
function detectMutations(
|
||||
isImmutable: IsImmutableFunc,
|
||||
ignoredPaths: IgnorePaths = [],
|
||||
trackedProperty: TrackedProperty,
|
||||
obj: any,
|
||||
sameParentRef: boolean = false,
|
||||
path: string = ''
|
||||
): { wasMutated: boolean; path?: string } {
|
||||
const prevObj = trackedProperty ? trackedProperty.value : undefined
|
||||
|
||||
const sameRef = prevObj === obj
|
||||
|
||||
if (sameParentRef && !sameRef && !Number.isNaN(obj)) {
|
||||
return { wasMutated: true, path }
|
||||
}
|
||||
|
||||
if (isImmutable(prevObj) || isImmutable(obj)) {
|
||||
return { wasMutated: false }
|
||||
}
|
||||
|
||||
// Gather all keys from prev (tracked) and after objs
|
||||
const keysToDetect: Record<string, boolean> = {}
|
||||
for (let key in trackedProperty.children) {
|
||||
keysToDetect[key] = true
|
||||
}
|
||||
for (let key in obj) {
|
||||
keysToDetect[key] = true
|
||||
}
|
||||
|
||||
const hasIgnoredPaths = ignoredPaths.length > 0
|
||||
|
||||
for (let key in keysToDetect) {
|
||||
const nestedPath = path ? path + '.' + key : key
|
||||
|
||||
if (hasIgnoredPaths) {
|
||||
const hasMatches = ignoredPaths.some((ignored) => {
|
||||
if (ignored instanceof RegExp) {
|
||||
return ignored.test(nestedPath)
|
||||
}
|
||||
return nestedPath === ignored
|
||||
})
|
||||
if (hasMatches) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
const result = detectMutations(
|
||||
isImmutable,
|
||||
ignoredPaths,
|
||||
trackedProperty.children[key],
|
||||
obj[key],
|
||||
sameRef,
|
||||
nestedPath
|
||||
)
|
||||
|
||||
if (result.wasMutated) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
return { wasMutated: false }
|
||||
}
|
||||
|
||||
type IsImmutableFunc = (value: any) => boolean
|
||||
|
||||
/**
|
||||
* Options for `createImmutableStateInvariantMiddleware()`.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface ImmutableStateInvariantMiddlewareOptions {
|
||||
/**
|
||||
Callback function to check if a value is considered to be immutable.
|
||||
This function is applied recursively to every value contained in the state.
|
||||
The default implementation will return true for primitive types
|
||||
(like numbers, strings, booleans, null and undefined).
|
||||
*/
|
||||
isImmutable?: IsImmutableFunc
|
||||
/**
|
||||
An array of dot-separated path strings that match named nodes from
|
||||
the root state to ignore when checking for immutability.
|
||||
Defaults to undefined
|
||||
*/
|
||||
ignoredPaths?: IgnorePaths
|
||||
/** Print a warning if checks take longer than N ms. Default: 32ms */
|
||||
warnAfter?: number
|
||||
// @deprecated. Use ignoredPaths
|
||||
ignore?: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a middleware that checks whether any state was mutated in between
|
||||
* dispatches or during a dispatch. If any mutations are detected, an error is
|
||||
* thrown.
|
||||
*
|
||||
* @param options Middleware options.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function createImmutableStateInvariantMiddleware(
|
||||
options: ImmutableStateInvariantMiddlewareOptions = {}
|
||||
): Middleware {
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
return () => (next) => (action) => next(action)
|
||||
}
|
||||
|
||||
let {
|
||||
isImmutable = isImmutableDefault,
|
||||
ignoredPaths,
|
||||
warnAfter = 32,
|
||||
ignore,
|
||||
} = options
|
||||
|
||||
// Alias ignore->ignoredPaths, but prefer ignoredPaths if present
|
||||
ignoredPaths = ignoredPaths || ignore
|
||||
|
||||
const track = trackForMutations.bind(null, isImmutable, ignoredPaths)
|
||||
|
||||
return ({ getState }) => {
|
||||
let state = getState()
|
||||
let tracker = track(state)
|
||||
|
||||
let result
|
||||
return (next) => (action) => {
|
||||
const measureUtils = getTimeMeasureUtils(
|
||||
warnAfter,
|
||||
'ImmutableStateInvariantMiddleware'
|
||||
)
|
||||
|
||||
measureUtils.measureTime(() => {
|
||||
state = getState()
|
||||
|
||||
result = tracker.detectMutations()
|
||||
// Track before potentially not meeting the invariant
|
||||
tracker = track(state)
|
||||
|
||||
invariant(
|
||||
!result.wasMutated,
|
||||
`A state mutation was detected between dispatches, in the path '${
|
||||
result.path || ''
|
||||
}'. This may cause incorrect behavior. (https://redux.js.org/style-guide/style-guide#do-not-mutate-state)`
|
||||
)
|
||||
})
|
||||
|
||||
const dispatchedAction = next(action)
|
||||
|
||||
measureUtils.measureTime(() => {
|
||||
state = getState()
|
||||
|
||||
result = tracker.detectMutations()
|
||||
// Track before potentially not meeting the invariant
|
||||
tracker = track(state)
|
||||
|
||||
result.wasMutated &&
|
||||
invariant(
|
||||
!result.wasMutated,
|
||||
`A state mutation was detected inside a dispatch, in the path: ${
|
||||
result.path || ''
|
||||
}. Take a look at the reducer(s) handling the action ${stringify(
|
||||
action
|
||||
)}. (https://redux.js.org/style-guide/style-guide#do-not-mutate-state)`
|
||||
)
|
||||
})
|
||||
|
||||
measureUtils.warnIfExceeded()
|
||||
|
||||
return dispatchedAction
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user