Skip to content

Commit 58dab3a

Browse files
author
Georg Wicke-Arndt
committed
Add caching to serializableStateInvariantMiddleware
1 parent d2d0f70 commit 58dab3a

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
@@ -36,7 +36,8 @@ export function findNonSerializableValue(
3636
path: string = '',
3737
isSerializable: (value: unknown) => boolean = isPlain,
3838
getEntries?: (value: unknown) => [string, any][],
39-
ignoredPaths: readonly string[] = []
39+
ignoredPaths: readonly string[] = [],
40+
cache?: WeakSet<object>
4041
): NonSerializableValue | false {
4142
let foundNestedSerializable: NonSerializableValue | false
4243

@@ -51,6 +52,8 @@ export function findNonSerializableValue(
5152
return false
5253
}
5354

55+
if (cache?.has(value)) return false
56+
5457
const entries = getEntries != null ? getEntries(value) : Object.entries(value)
5558

5659
const hasIgnoredPaths = ignoredPaths.length > 0
@@ -75,7 +78,8 @@ export function findNonSerializableValue(
7578
nestedPath,
7679
isSerializable,
7780
getEntries,
78-
ignoredPaths
81+
ignoredPaths,
82+
cache
7983
)
8084

8185
if (foundNestedSerializable) {
@@ -84,9 +88,23 @@ export function findNonSerializableValue(
8488
}
8589
}
8690

91+
if (cache && isNestedFrozen(value)) cache.add(value)
92+
8793
return false
8894
}
8995

96+
export function isNestedFrozen(value: object) {
97+
if (!Object.isFrozen(value)) return false
98+
99+
for (const nestedValue of Object.values(value)) {
100+
if (typeof nestedValue !== 'object' || nestedValue === null) continue
101+
102+
if (!isNestedFrozen(nestedValue)) return false
103+
}
104+
105+
return true
106+
}
107+
90108
/**
91109
* Options for `createSerializableStateInvariantMiddleware()`.
92110
*
@@ -139,6 +157,12 @@ export interface SerializableStateInvariantMiddlewareOptions {
139157
* Opt out of checking actions. When set to `true`, other action-related params will be ignored.
140158
*/
141159
ignoreActions?: boolean
160+
161+
/**
162+
* Opt out of caching the results. The cache uses a WeakSet and speeds up repeated checking processes.
163+
* The cache is automatically disabled if no browser support for WeakSet is present.
164+
*/
165+
disableCache?: boolean
142166
}
143167

144168
/**
@@ -165,8 +189,12 @@ export function createSerializableStateInvariantMiddleware(
165189
warnAfter = 32,
166190
ignoreState = false,
167191
ignoreActions = false,
192+
disableCache = false,
168193
} = options
169194

195+
const cache: WeakSet<object> | undefined =
196+
!disableCache && WeakSet ? new WeakSet() : undefined
197+
170198
return (storeAPI) => (next) => (action) => {
171199
const result = next(action)
172200

@@ -185,7 +213,8 @@ export function createSerializableStateInvariantMiddleware(
185213
'',
186214
isSerializable,
187215
getEntries,
188-
ignoredActionPaths
216+
ignoredActionPaths,
217+
cache
189218
)
190219

191220
if (foundActionNonSerializableValue) {
@@ -212,7 +241,8 @@ export function createSerializableStateInvariantMiddleware(
212241
'',
213242
isSerializable,
214243
getEntries,
215-
ignoredPaths
244+
ignoredPaths,
245+
cache
216246
)
217247

218248
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 {
88
configureStore,
99
createSerializableStateInvariantMiddleware,
1010
findNonSerializableValue,
1111
isPlain,
1212
} from '@reduxjs/toolkit'
13+
import { isNestedFrozen } from '@internal/serializableStateInvariantMiddleware'
14+
import produce from 'immer'
1315

1416
// Mocking console
1517
let restore = () => {}
@@ -572,4 +574,44 @@ describe('serializableStateInvariantMiddleware', () => {
572574
store.dispatch({ type: 'SOME_ACTION' })
573575
expect(getLog().log).toMatch('')
574576
})
577+
578+
it('Should cache its results', () => {
579+
const reducer: Reducer<[], AnyAction> = (state = [], action) => {
580+
if (action.type === 'SET_STATE') return action.payload
581+
return state
582+
}
583+
584+
let numPlainChecks = 0
585+
const countPlainChecks = (x: any) => {
586+
numPlainChecks++
587+
return isPlain(x)
588+
}
589+
590+
const serializableStateInvariantMiddleware =
591+
createSerializableStateInvariantMiddleware({
592+
isSerializable: countPlainChecks,
593+
})
594+
595+
const store = configureStore({
596+
reducer: {
597+
testSlice: reducer,
598+
},
599+
middleware: [serializableStateInvariantMiddleware],
600+
})
601+
602+
const state = produce([], () =>
603+
new Array(50).fill(0).map((x, i) => ({ i }))
604+
)
605+
expect(isNestedFrozen(state)).toBe(true)
606+
607+
store.dispatch({
608+
type: 'SET_STATE',
609+
payload: state,
610+
})
611+
expect(numPlainChecks).toBeGreaterThan(state.length)
612+
613+
numPlainChecks = 0
614+
store.dispatch({ type: 'NOOP' })
615+
expect(numPlainChecks).toBeLessThan(10)
616+
})
575617
})

0 commit comments

Comments
 (0)