Skip to content

Commit

Permalink
Merge pull request #3115 from GeorchW/serializable-state-invariant-mi…
Browse files Browse the repository at this point in the history
…ddleware-caching
  • Loading branch information
markerikson committed Jan 28, 2023
2 parents f5f8bc2 + bcd0615 commit 67a69e8
Show file tree
Hide file tree
Showing 2 changed files with 73 additions and 5 deletions.
38 changes: 34 additions & 4 deletions packages/toolkit/src/serializableStateInvariantMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ export function findNonSerializableValue(
path: string = '',
isSerializable: (value: unknown) => boolean = isPlain,
getEntries?: (value: unknown) => [string, any][],
ignoredPaths: IgnorePaths = []
ignoredPaths: IgnorePaths = [],
cache?: WeakSet<object>
): NonSerializableValue | false {
let foundNestedSerializable: NonSerializableValue | false

Expand All @@ -53,6 +54,8 @@ export function findNonSerializableValue(
return false
}

if (cache?.has(value)) return false

const entries = getEntries != null ? getEntries(value) : Object.entries(value)

const hasIgnoredPaths = ignoredPaths.length > 0
Expand Down Expand Up @@ -85,7 +88,8 @@ export function findNonSerializableValue(
nestedPath,
isSerializable,
getEntries,
ignoredPaths
ignoredPaths,
cache
)

if (foundNestedSerializable) {
Expand All @@ -94,9 +98,23 @@ export function findNonSerializableValue(
}
}

if (cache && isNestedFrozen(value)) cache.add(value)

return false
}

export function isNestedFrozen(value: object) {
if (!Object.isFrozen(value)) return false

for (const nestedValue of Object.values(value)) {
if (typeof nestedValue !== 'object' || nestedValue === null) continue

if (!isNestedFrozen(nestedValue)) return false
}

return true
}

/**
* Options for `createSerializableStateInvariantMiddleware()`.
*
Expand Down Expand Up @@ -150,6 +168,12 @@ export interface SerializableStateInvariantMiddlewareOptions {
* Opt out of checking actions. When set to `true`, other action-related params will be ignored.
*/
ignoreActions?: boolean

/**
* Opt out of caching the results. The cache uses a WeakSet and speeds up repeated checking processes.
* The cache is automatically disabled if no browser support for WeakSet is present.
*/
disableCache?: boolean
}

/**
Expand All @@ -176,8 +200,12 @@ export function createSerializableStateInvariantMiddleware(
warnAfter = 32,
ignoreState = false,
ignoreActions = false,
disableCache = false,
} = options

const cache: WeakSet<object> | undefined =
!disableCache && WeakSet ? new WeakSet() : undefined

return (storeAPI) => (next) => (action) => {
const result = next(action)

Expand All @@ -196,7 +224,8 @@ export function createSerializableStateInvariantMiddleware(
'',
isSerializable,
getEntries,
ignoredActionPaths
ignoredActionPaths,
cache
)

if (foundActionNonSerializableValue) {
Expand All @@ -223,7 +252,8 @@ export function createSerializableStateInvariantMiddleware(
'',
isSerializable,
getEntries,
ignoredPaths
ignoredPaths,
cache
)

if (foundStateNonSerializableValue) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ import {
createConsole,
getLog,
} from 'console-testing-library/pure'
import type { Reducer } from '@reduxjs/toolkit'
import type { AnyAction, Reducer } from '@reduxjs/toolkit'
import {
createNextState,
configureStore,
createSerializableStateInvariantMiddleware,
findNonSerializableValue,
isPlain,
} from '@reduxjs/toolkit'
import { isNestedFrozen } from '@internal/serializableStateInvariantMiddleware'

// Mocking console
let restore = () => {}
Expand Down Expand Up @@ -594,4 +596,40 @@ describe('serializableStateInvariantMiddleware', () => {
store.dispatch({ type: 'SOME_ACTION' })
expect(getLog().log).toMatch('')
})

it('Should cache its results', () => {
let numPlainChecks = 0
const countPlainChecks = (x: any) => {
numPlainChecks++
return isPlain(x)
}

const serializableStateInvariantMiddleware =
createSerializableStateInvariantMiddleware({
isSerializable: countPlainChecks,
})

const store = configureStore({
reducer: (state = [], action) => {
if (action.type === 'SET_STATE') return action.payload
return state
},
middleware: [serializableStateInvariantMiddleware],
})

const state = createNextState([], () =>
new Array(50).fill(0).map((x, i) => ({ i }))
)
expect(isNestedFrozen(state)).toBe(true)

store.dispatch({
type: 'SET_STATE',
payload: state,
})
expect(numPlainChecks).toBeGreaterThan(state.length)

numPlainChecks = 0
store.dispatch({ type: 'NOOP' })
expect(numPlainChecks).toBeLessThan(10)
})
})

0 comments on commit 67a69e8

Please sign in to comment.