Skip to content

Commit 67a69e8

Browse files
authored
Merge pull request #3115 from GeorchW/serializable-state-invariant-middleware-caching
2 parents f5f8bc2 + bcd0615 commit 67a69e8

File tree

2 files changed

+73
-5
lines changed

2 files changed

+73
-5
lines changed

packages/toolkit/src/serializableStateInvariantMiddleware.ts

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ export function findNonSerializableValue(
3838
path: string = '',
3939
isSerializable: (value: unknown) => boolean = isPlain,
4040
getEntries?: (value: unknown) => [string, any][],
41-
ignoredPaths: IgnorePaths = []
41+
ignoredPaths: IgnorePaths = [],
42+
cache?: WeakSet<object>
4243
): NonSerializableValue | false {
4344
let foundNestedSerializable: NonSerializableValue | false
4445

@@ -53,6 +54,8 @@ export function findNonSerializableValue(
5354
return false
5455
}
5556

57+
if (cache?.has(value)) return false
58+
5659
const entries = getEntries != null ? getEntries(value) : Object.entries(value)
5760

5861
const hasIgnoredPaths = ignoredPaths.length > 0
@@ -85,7 +88,8 @@ export function findNonSerializableValue(
8588
nestedPath,
8689
isSerializable,
8790
getEntries,
88-
ignoredPaths
91+
ignoredPaths,
92+
cache
8993
)
9094

9195
if (foundNestedSerializable) {
@@ -94,9 +98,23 @@ export function findNonSerializableValue(
9498
}
9599
}
96100

101+
if (cache && isNestedFrozen(value)) cache.add(value)
102+
97103
return false
98104
}
99105

106+
export function isNestedFrozen(value: object) {
107+
if (!Object.isFrozen(value)) return false
108+
109+
for (const nestedValue of Object.values(value)) {
110+
if (typeof nestedValue !== 'object' || nestedValue === null) continue
111+
112+
if (!isNestedFrozen(nestedValue)) return false
113+
}
114+
115+
return true
116+
}
117+
100118
/**
101119
* Options for `createSerializableStateInvariantMiddleware()`.
102120
*
@@ -150,6 +168,12 @@ export interface SerializableStateInvariantMiddlewareOptions {
150168
* Opt out of checking actions. When set to `true`, other action-related params will be ignored.
151169
*/
152170
ignoreActions?: boolean
171+
172+
/**
173+
* Opt out of caching the results. The cache uses a WeakSet and speeds up repeated checking processes.
174+
* The cache is automatically disabled if no browser support for WeakSet is present.
175+
*/
176+
disableCache?: boolean
153177
}
154178

155179
/**
@@ -176,8 +200,12 @@ export function createSerializableStateInvariantMiddleware(
176200
warnAfter = 32,
177201
ignoreState = false,
178202
ignoreActions = false,
203+
disableCache = false,
179204
} = options
180205

206+
const cache: WeakSet<object> | undefined =
207+
!disableCache && WeakSet ? new WeakSet() : undefined
208+
181209
return (storeAPI) => (next) => (action) => {
182210
const result = next(action)
183211

@@ -196,7 +224,8 @@ export function createSerializableStateInvariantMiddleware(
196224
'',
197225
isSerializable,
198226
getEntries,
199-
ignoredActionPaths
227+
ignoredActionPaths,
228+
cache
200229
)
201230

202231
if (foundActionNonSerializableValue) {
@@ -223,7 +252,8 @@ export function createSerializableStateInvariantMiddleware(
223252
'',
224253
isSerializable,
225254
getEntries,
226-
ignoredPaths
255+
ignoredPaths,
256+
cache
227257
)
228258

229259
if (foundStateNonSerializableValue) {

packages/toolkit/src/tests/serializableStateInvariantMiddleware.test.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@ import {
33
createConsole,
44
getLog,
55
} from 'console-testing-library/pure'
6-
import type { Reducer } from '@reduxjs/toolkit'
6+
import type { AnyAction, Reducer } from '@reduxjs/toolkit'
77
import {
8+
createNextState,
89
configureStore,
910
createSerializableStateInvariantMiddleware,
1011
findNonSerializableValue,
1112
isPlain,
1213
} from '@reduxjs/toolkit'
14+
import { isNestedFrozen } from '@internal/serializableStateInvariantMiddleware'
1315

1416
// Mocking console
1517
let restore = () => {}
@@ -594,4 +596,40 @@ describe('serializableStateInvariantMiddleware', () => {
594596
store.dispatch({ type: 'SOME_ACTION' })
595597
expect(getLog().log).toMatch('')
596598
})
599+
600+
it('Should cache its results', () => {
601+
let numPlainChecks = 0
602+
const countPlainChecks = (x: any) => {
603+
numPlainChecks++
604+
return isPlain(x)
605+
}
606+
607+
const serializableStateInvariantMiddleware =
608+
createSerializableStateInvariantMiddleware({
609+
isSerializable: countPlainChecks,
610+
})
611+
612+
const store = configureStore({
613+
reducer: (state = [], action) => {
614+
if (action.type === 'SET_STATE') return action.payload
615+
return state
616+
},
617+
middleware: [serializableStateInvariantMiddleware],
618+
})
619+
620+
const state = createNextState([], () =>
621+
new Array(50).fill(0).map((x, i) => ({ i }))
622+
)
623+
expect(isNestedFrozen(state)).toBe(true)
624+
625+
store.dispatch({
626+
type: 'SET_STATE',
627+
payload: state,
628+
})
629+
expect(numPlainChecks).toBeGreaterThan(state.length)
630+
631+
numPlainChecks = 0
632+
store.dispatch({ type: 'NOOP' })
633+
expect(numPlainChecks).toBeLessThan(10)
634+
})
597635
})

0 commit comments

Comments
 (0)