Skip to content

Commit a4a0c2d

Browse files
committed
Merge branch 'master' into v1.9-integration
2 parents f1afad8 + 448607a commit a4a0c2d

File tree

10 files changed

+110
-34
lines changed

10 files changed

+110
-34
lines changed

packages/toolkit/src/configureStore.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import type {
1111
CombinedState,
1212
} from 'redux'
1313
import { createStore, compose, applyMiddleware, combineReducers } from 'redux'
14-
import type { EnhancerOptions as DevToolsOptions } from './devtoolsExtension'
14+
import type { DevToolsEnhancerOptions as DevToolsOptions } from './devtoolsExtension'
1515
import { composeWithDevTools } from './devtoolsExtension'
1616

1717
import isPlainObject from './isPlainObject'

packages/toolkit/src/createReducer.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,7 @@ export function createReducer<S extends NotFunction<any>>(
266266
const draft = previousState as Draft<S> // We can assume this is already a draft
267267
const result = caseReducer(draft, action)
268268

269-
if (typeof result === 'undefined') {
269+
if (result === undefined) {
270270
return previousState
271271
}
272272

@@ -276,7 +276,7 @@ export function createReducer<S extends NotFunction<any>>(
276276
// return the caseReducer func and not wrap it with produce.
277277
const result = caseReducer(previousState as any, action)
278278

279-
if (typeof result === 'undefined') {
279+
if (result === undefined) {
280280
if (previousState === null) {
281281
return previousState
282282
}

packages/toolkit/src/devtoolsExtension.ts

Lines changed: 51 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { compose } from 'redux'
44
/**
55
* @public
66
*/
7-
export interface EnhancerOptions {
7+
export interface DevToolsEnhancerOptions {
88
/**
99
* the instance name to be showed on the monitor page. Default value is `document.title`.
1010
* If not specified and there's no document title, it will consist of `tabId` and `instanceId`.
@@ -29,26 +29,58 @@ export interface EnhancerOptions {
2929
*/
3030
maxAge?: number
3131
/**
32-
* - `undefined` - will use regular `JSON.stringify` to send data (it's the fast mode).
33-
* - `false` - will handle also circular references.
34-
* - `true` - will handle also date, regex, undefined, error objects, symbols, maps, sets and functions.
35-
* - object, which contains `date`, `regex`, `undefined`, `error`, `symbol`, `map`, `set` and `function` keys.
36-
* For each of them you can indicate if to include (by setting as `true`).
37-
* For `function` key you can also specify a custom function which handles serialization.
38-
* See [`jsan`](https://github.com/kolodny/jsan) for more details.
32+
* Customizes how actions and state are serialized and deserialized. Can be a boolean or object. If given a boolean, the behavior is the same as if you
33+
* were to pass an object and specify `options` as a boolean. Giving an object allows fine-grained customization using the `replacer` and `reviver`
34+
* functions.
3935
*/
4036
serialize?:
4137
| boolean
4238
| {
43-
date?: boolean
44-
regex?: boolean
45-
undefined?: boolean
46-
error?: boolean
47-
symbol?: boolean
48-
map?: boolean
49-
set?: boolean
50-
// eslint-disable-next-line @typescript-eslint/ban-types
51-
function?: boolean | Function
39+
/**
40+
* - `undefined` - will use regular `JSON.stringify` to send data (it's the fast mode).
41+
* - `false` - will handle also circular references.
42+
* - `true` - will handle also date, regex, undefined, error objects, symbols, maps, sets and functions.
43+
* - object, which contains `date`, `regex`, `undefined`, `error`, `symbol`, `map`, `set` and `function` keys.
44+
* For each of them you can indicate if to include (by setting as `true`).
45+
* For `function` key you can also specify a custom function which handles serialization.
46+
* See [`jsan`](https://github.com/kolodny/jsan) for more details.
47+
*/
48+
options?:
49+
| undefined
50+
| boolean
51+
| {
52+
date?: true
53+
regex?: true
54+
undefined?: true
55+
error?: true
56+
symbol?: true
57+
map?: true
58+
set?: true
59+
function?: true | ((fn: (...args: any[]) => any) => string)
60+
}
61+
/**
62+
* [JSON replacer function](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#The_replacer_parameter) used for both actions and states stringify.
63+
* In addition, you can specify a data type by adding a [`__serializedType__`](https://github.com/zalmoxisus/remotedev-serialize/blob/master/helpers/index.js#L4)
64+
* key. So you can deserialize it back while importing or persisting data.
65+
* Moreover, it will also [show a nice preview showing the provided custom type](https://cloud.githubusercontent.com/assets/7957859/21814330/a17d556a-d761-11e6-85ef-159dd12f36c5.png):
66+
*/
67+
replacer?: (key: string, value: unknown) => any
68+
/**
69+
* [JSON `reviver` function](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse#Using_the_reviver_parameter)
70+
* used for parsing the imported actions and states. See [`remotedev-serialize`](https://github.com/zalmoxisus/remotedev-serialize/blob/master/immutable/serialize.js#L8-L41)
71+
* as an example on how to serialize special data types and get them back.
72+
*/
73+
reviver?: (key: string, value: unknown) => any
74+
/**
75+
* Automatically serialize/deserialize immutablejs via [remotedev-serialize](https://github.com/zalmoxisus/remotedev-serialize).
76+
* Just pass the Immutable library. It will support all ImmutableJS structures. You can even export them into a file and get them back.
77+
* The only exception is `Record` class, for which you should pass this in addition the references to your classes in `refs`.
78+
*/
79+
immutable?: any
80+
/**
81+
* ImmutableJS `Record` classes used to make possible restore its instances back when importing, persisting...
82+
*/
83+
refs?: any
5284
}
5385
/**
5486
* function which takes `action` object and id number as arguments, and should return `action` object back.
@@ -187,7 +219,7 @@ export interface EnhancerOptions {
187219
type Compose = typeof compose
188220

189221
interface ComposeWithDevTools {
190-
(options: EnhancerOptions): Compose
222+
(options: DevToolsEnhancerOptions): Compose
191223
<StoreExt>(...funcs: StoreEnhancer<StoreExt>[]): StoreEnhancer<StoreExt>
192224
}
193225

@@ -208,7 +240,7 @@ export const composeWithDevTools: ComposeWithDevTools =
208240
* @public
209241
*/
210242
export const devToolsEnhancer: {
211-
(options: EnhancerOptions): StoreEnhancer<any>
243+
(options: DevToolsEnhancerOptions): StoreEnhancer<any>
212244
} =
213245
typeof window !== 'undefined' && (window as any).__REDUX_DEVTOOLS_EXTENSION__
214246
? (window as any).__REDUX_DEVTOOLS_EXTENSION__

packages/toolkit/src/immutableStateInvariantMiddleware.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,7 @@ function getSerialize(
6767
* @public
6868
*/
6969
export function isImmutableDefault(value: unknown): boolean {
70-
return (
71-
typeof value !== 'object' ||
72-
value === null ||
73-
typeof value === 'undefined' ||
74-
Object.isFrozen(value)
75-
)
70+
return typeof value !== 'object' || value == null || Object.isFrozen(value)
7671
}
7772

7873
export function trackForMutations(

packages/toolkit/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export type {
3434
ConfigureStoreOptions,
3535
EnhancedStore,
3636
} from './configureStore'
37+
export type { DevToolsEnhancerOptions } from './devtoolsExtension'
3738
export {
3839
// js
3940
createAction,
@@ -174,6 +175,7 @@ export type {
174175
TaskResolved,
175176
TaskResult,
176177
} from './listenerMiddleware/index'
178+
export type { AnyListenerPredicate } from './listenerMiddleware/types'
177179

178180
export {
179181
createListenerMiddleware,

packages/toolkit/src/query/core/buildMiddleware/cacheCollection.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@ declare module '../../endpointDefinitions' {
2828
}
2929
}
3030

31+
// Per https://developer.mozilla.org/en-US/docs/Web/API/setTimeout#maximum_delay_value , browsers store
32+
// `setTimeout()` timer values in a 32-bit int. If we pass a value in that's larger than that,
33+
// it wraps and ends up executing immediately.
34+
// Our `keepUnusedDataFor` values are in seconds, so adjust the numbers here accordingly.
35+
export const THIRTY_TWO_BIT_MAX_INT = 2_147_483_647
36+
export const THIRTY_TWO_BIT_MAX_TIMER_SECONDS = 2_147_483_647 / 1_000 - 1
37+
3138
export const build: SubMiddlewareBuilder = ({ reducerPath, api, context }) => {
3239
const { removeQueryResult, unsubscribeQueryResult } = api.internalActions
3340

@@ -87,6 +94,14 @@ export const build: SubMiddlewareBuilder = ({ reducerPath, api, context }) => {
8794
] as QueryDefinition<any, any, any, any>
8895
const keepUnusedDataFor =
8996
endpointDefinition?.keepUnusedDataFor ?? config.keepUnusedDataFor
97+
// Prevent `setTimeout` timers from overflowing a 32-bit internal int, by
98+
// clamping the max value to be at most 1000ms less than the 32-bit max.
99+
// Look, a 24.8-day keepalive ought to be enough for anybody, right? :)
100+
// Also avoid negative values too.
101+
const finalKeepUnusedDataFor = Math.max(
102+
0,
103+
Math.min(keepUnusedDataFor, THIRTY_TWO_BIT_MAX_TIMER_SECONDS)
104+
)
90105

91106
const currentTimeout = currentRemovalTimeouts[queryCacheKey]
92107
if (currentTimeout) {
@@ -99,7 +114,7 @@ export const build: SubMiddlewareBuilder = ({ reducerPath, api, context }) => {
99114
api.dispatch(removeQueryResult({ queryCacheKey }))
100115
}
101116
delete currentRemovalTimeouts![queryCacheKey]
102-
}, keepUnusedDataFor * 1000)
117+
}, finalKeepUnusedDataFor * 1000)
103118
}
104119
}
105120
}

packages/toolkit/src/query/fetchBaseQuery.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ function stripUndefined(obj: any) {
9797
}
9898
const copy: Record<string, any> = { ...obj }
9999
for (const [k, v] of Object.entries(copy)) {
100-
if (typeof v === 'undefined') delete copy[k]
100+
if (v === undefined) delete copy[k]
101101
}
102102
return copy
103103
}

packages/toolkit/src/query/react/buildHooks.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,8 @@ import type { BaseQueryFn } from '../baseQueryTypes'
5858
// Copy-pasted from React-Redux
5959
export const useIsomorphicLayoutEffect =
6060
typeof window !== 'undefined' &&
61-
typeof window.document !== 'undefined' &&
62-
typeof window.document.createElement !== 'undefined'
61+
window.document &&
62+
window.document.createElement
6363
? useLayoutEffect
6464
: useEffect
6565

packages/toolkit/src/query/tests/cacheCollection.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query'
22
import { configureStore } from '@reduxjs/toolkit'
33
import { waitMs } from './helpers'
44
import type { Middleware, Reducer } from 'redux'
5+
import {
6+
THIRTY_TWO_BIT_MAX_INT,
7+
THIRTY_TWO_BIT_MAX_TIMER_SECONDS,
8+
} from '../core/buildMiddleware/cacheCollection'
59

610
beforeAll(() => {
711
jest.useFakeTimers('legacy')
@@ -52,6 +56,35 @@ test(`query: await cleanup, keepUnusedDataFor set`, async () => {
5256
expect(onCleanup).toHaveBeenCalled()
5357
})
5458

59+
test(`query: handles large keepUnuseDataFor values over 32-bit ms`, async () => {
60+
const { store, api } = storeForApi(
61+
createApi({
62+
baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }),
63+
endpoints: (build) => ({
64+
query: build.query<unknown, string>({
65+
query: () => '/success',
66+
}),
67+
}),
68+
keepUnusedDataFor: THIRTY_TWO_BIT_MAX_TIMER_SECONDS - 10,
69+
})
70+
)
71+
72+
store.dispatch(api.endpoints.query.initiate('arg')).unsubscribe()
73+
74+
// Shouldn't have been called right away
75+
jest.advanceTimersByTime(1000), await waitMs()
76+
expect(onCleanup).not.toHaveBeenCalled()
77+
78+
// Shouldn't have been called any time in the next few minutes
79+
jest.advanceTimersByTime(1_000_000), await waitMs()
80+
expect(onCleanup).not.toHaveBeenCalled()
81+
82+
// _Should_ be called _wayyyy_ in the future (like 24.8 days from now)
83+
jest.advanceTimersByTime(THIRTY_TWO_BIT_MAX_TIMER_SECONDS * 1000),
84+
await waitMs()
85+
expect(onCleanup).toHaveBeenCalled()
86+
})
87+
5588
describe(`query: await cleanup, keepUnusedDataFor set`, () => {
5689
const { store, api } = storeForApi(
5790
createApi({

packages/toolkit/src/serializableStateInvariantMiddleware.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,7 @@ import { getTimeMeasureUtils } from './utils'
1414
export function isPlain(val: any) {
1515
const type = typeof val
1616
return (
17-
type === 'undefined' ||
18-
val === null ||
17+
val == null ||
1918
type === 'string' ||
2019
type === 'boolean' ||
2120
type === 'number' ||

0 commit comments

Comments
 (0)