UNPKG

@reduxjs/toolkit

Version:

The official, opinionated, batteries-included toolset for efficient Redux development

251 lines (210 loc) 6.8 kB
import { 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) } } export function isImmutableDefault(value: unknown): boolean { return ( typeof value !== 'object' || value === null || typeof value === 'undefined' ) } export function trackForMutations( isImmutable: IsImmutableFunc, ingorePaths: string[] | undefined, obj: any ) { const trackedProperties = trackProperties(isImmutable, ingorePaths, obj) return { detectMutations() { return detectMutations(isImmutable, ingorePaths, trackedProperties, obj) } } } interface TrackedProperty { value: any children: Record<string, any> } function trackProperties( isImmutable: IsImmutableFunc, ignorePaths: IgnorePaths = [], obj: Record<string, any>, path: string[] = [] ) { const tracked: Partial<TrackedProperty> = { value: obj } if (!isImmutable(obj)) { tracked.children = {} for (const key in obj) { const childPath = path.concat(key) if ( ignorePaths.length && ignorePaths.indexOf(childPath.join('.')) !== -1 ) { continue } tracked.children[key] = trackProperties( isImmutable, ignorePaths, obj[key], childPath ) } } return tracked as TrackedProperty } type IgnorePaths = string[] function detectMutations( isImmutable: IsImmutableFunc, ignorePaths: 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> = {} Object.keys(trackedProperty.children).forEach(key => { keysToDetect[key] = true }) Object.keys(obj).forEach(key => { keysToDetect[key] = true }) const keys = Object.keys(keysToDetect) for (let i = 0; i < keys.length; i++) { const key = keys[i] const childPath = path.concat(key) if (ignorePaths.length && ignorePaths.indexOf(childPath.join('.')) !== -1) { continue } const result = detectMutations( isImmutable, ignorePaths, trackedProperty.children[key], obj[key], sameRef, childPath ) if (result.wasMutated) { return result } } return { wasMutated: false } } type IsImmutableFunc = (value: any) => boolean export interface ImmutableStateInvariantMiddlewareOptions { isImmutable?: IsImmutableFunc ignoredPaths?: string[] warnAfter?: number } export function createImmutableStateInvariantMiddleware( options: ImmutableStateInvariantMiddlewareOptions = {} ): Middleware { if (process.env.NODE_ENV === 'production') { return () => next => action => next(action) } const { isImmutable = isImmutableDefault, ignoredPaths, warnAfter = 32 } = options 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 || [] ).join( '.' )}'. This may cause incorrect behavior. (http://redux.js.org/docs/Troubleshooting.html#never-mutate-reducer-arguments)` ) }) 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 || [] ).join( '.' )}. Take a look at the reducer(s) handling the action ${stringify( action )}. (http://redux.js.org/docs/Troubleshooting.html#never-mutate-reducer-arguments)` ) }) measureUtils.warnIfExceeded() return dispatchedAction } } }