Skip to content

Commit f9049cc

Browse files
GeorchWmarkerikson
authored andcommitted
Add caching to serializableStateInvariantMiddleware
1 parent f5f8bc2 commit f9049cc

File tree

2 files changed

+77
-5
lines changed

2 files changed

+77
-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: 43 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,44 @@ describe('serializableStateInvariantMiddleware', () => {
594596
store.dispatch({ type: 'SOME_ACTION' })
595597
expect(getLog().log).toMatch('')
596598
})
599+
600+
it('Should cache its results', () => {
601+
const reducer: Reducer<[], AnyAction> = (state = [], action) => {
602+
if (action.type === 'SET_STATE') return action.payload
603+
return state
604+
}
605+
606+
let numPlainChecks = 0
607+
const countPlainChecks = (x: any) => {
608+
numPlainChecks++
609+
return isPlain(x)
610+
}
611+
612+
const serializableStateInvariantMiddleware =
613+
createSerializableStateInvariantMiddleware({
614+
isSerializable: countPlainChecks,
615+
})
616+
617+
const store = configureStore({
618+
reducer: {
619+
testSlice: reducer,
620+
},
621+
middleware: [serializableStateInvariantMiddleware],
622+
})
623+
624+
const state = createNextState([], () =>
625+
new Array(50).fill(0).map((x, i) => ({ i }))
626+
)
627+
expect(isNestedFrozen(state)).toBe(true)
628+
629+
store.dispatch({
630+
type: 'SET_STATE',
631+
payload: state,
632+
})
633+
expect(numPlainChecks).toBeGreaterThan(state.length)
634+
635+
numPlainChecks = 0
636+
store.dispatch({ type: 'NOOP' })
637+
expect(numPlainChecks).toBeLessThan(10)
638+
})
597639
})

0 commit comments

Comments
 (0)