Skip to content

Commit 9ed2ad1

Browse files
committed
Consolidate RTKQ middleware to simplify stack size
Previously, the RTKQ middleware was made up of 7 "sub-middleware", each encapsulating a different responsibility (polling, caching, etc). However, that meant that each middleware was called on _every_ action, even if it wasn't RTKQ-related. That adds to call stack size. Almost all the logic runs _after_ the action is handled by the reducers. So, I've reworked the files to create handlers (conceptually similar to the `(action) => {}` part of a middleware, with a couple extra args), and rewritten the top-level middleware to run those in a loop.
1 parent eb34fb3 commit 9ed2ad1

File tree

10 files changed

+561
-550
lines changed

10 files changed

+561
-550
lines changed
Lines changed: 24 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { QueryThunk, RejectedAction } from '../buildThunks'
2-
import type { SubMiddlewareBuilder } from './types'
2+
import type { InternalHandlerBuilder } from './types'
33

44
// Copied from https://github.com/feross/queue-microtask
55
let promise: Promise<any>
@@ -14,44 +14,38 @@ const queueMicrotaskShim =
1414
}, 0)
1515
)
1616

17-
export const build: SubMiddlewareBuilder = ({
17+
export const buildBatchedActionsHandler: InternalHandlerBuilder<boolean> = ({
1818
api,
19-
context: { apiUid },
2019
queryThunk,
21-
reducerPath,
2220
}) => {
23-
return (mwApi) => {
24-
let abortedQueryActionsQueue: RejectedAction<QueryThunk, any>[] = []
25-
let dispatchQueued = false
21+
let abortedQueryActionsQueue: RejectedAction<QueryThunk, any>[] = []
22+
let dispatchQueued = false
2623

27-
return (next) => (action) => {
28-
if (queryThunk.rejected.match(action)) {
29-
const { condition, arg } = action.meta
24+
return (action, mwApi) => {
25+
if (queryThunk.rejected.match(action)) {
26+
const { condition, arg } = action.meta
3027

31-
if (condition && arg.subscribe) {
32-
// request was aborted due to condition (another query already running)
33-
// _Don't_ dispatch right away - queue it for a debounced grouped dispatch
34-
abortedQueryActionsQueue.push(action)
28+
if (condition && arg.subscribe) {
29+
// request was aborted due to condition (another query already running)
30+
// _Don't_ dispatch right away - queue it for a debounced grouped dispatch
31+
abortedQueryActionsQueue.push(action)
3532

36-
if (!dispatchQueued) {
37-
queueMicrotaskShim(() => {
38-
mwApi.dispatch(
39-
api.internalActions.subscriptionRequestsRejected(
40-
abortedQueryActionsQueue
41-
)
33+
if (!dispatchQueued) {
34+
queueMicrotaskShim(() => {
35+
mwApi.dispatch(
36+
api.internalActions.subscriptionRequestsRejected(
37+
abortedQueryActionsQueue
4238
)
43-
abortedQueryActionsQueue = []
44-
})
45-
dispatchQueued = true
46-
}
47-
// _Don't_ let the action reach the reducers now!
48-
return
39+
)
40+
abortedQueryActionsQueue = []
41+
})
42+
dispatchQueued = true
4943
}
44+
// _Don't_ let the action reach the reducers now!
45+
return false
5046
}
51-
52-
const result = next(action)
53-
54-
return result
5547
}
48+
49+
return true
5650
}
5751
}
Lines changed: 76 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import type { BaseQueryFn } from '../../baseQueryTypes'
22
import type { QueryDefinition } from '../../endpointDefinitions'
33
import type { ConfigState, QueryCacheKey } from '../apiState'
4-
import { QuerySubstateIdentifier } from '../apiState'
54
import type {
65
QueryStateMeta,
76
SubMiddlewareApi,
8-
SubMiddlewareBuilder,
97
TimeoutId,
8+
InternalHandlerBuilder,
9+
ApiMiddlewareInternalHandler,
1010
} from './types'
1111

1212
export type ReferenceCacheCollection = never
@@ -45,7 +45,11 @@ declare module '../../endpointDefinitions' {
4545
export const THIRTY_TWO_BIT_MAX_INT = 2_147_483_647
4646
export const THIRTY_TWO_BIT_MAX_TIMER_SECONDS = 2_147_483_647 / 1_000 - 1
4747

48-
export const build: SubMiddlewareBuilder = ({ reducerPath, api, context }) => {
48+
export const buildCacheCollectionHandler: InternalHandlerBuilder = ({
49+
reducerPath,
50+
api,
51+
context,
52+
}) => {
4953
const { removeQueryResult, unsubscribeQueryResult } = api.internalActions
5054

5155
function anySubscriptionsRemainingForKey(
@@ -57,88 +61,83 @@ export const build: SubMiddlewareBuilder = ({ reducerPath, api, context }) => {
5761
return !!subscriptions && !isObjectEmpty(subscriptions)
5862
}
5963

60-
return (mwApi) => {
61-
const currentRemovalTimeouts: QueryStateMeta<TimeoutId> = {}
64+
const currentRemovalTimeouts: QueryStateMeta<TimeoutId> = {}
6265

63-
return (next) =>
64-
(action): any => {
65-
const result = next(action)
66+
const handler: ApiMiddlewareInternalHandler = (action, mwApi) => {
67+
if (unsubscribeQueryResult.match(action)) {
68+
const state = mwApi.getState()[reducerPath]
69+
const { queryCacheKey } = action.payload
6670

67-
if (unsubscribeQueryResult.match(action)) {
68-
const state = mwApi.getState()[reducerPath]
69-
const { queryCacheKey } = action.payload
70-
71-
handleUnsubscribe(
72-
queryCacheKey,
73-
state.queries[queryCacheKey]?.endpointName,
74-
mwApi,
75-
state.config
76-
)
77-
}
78-
79-
if (api.util.resetApiState.match(action)) {
80-
for (const [key, timeout] of Object.entries(currentRemovalTimeouts)) {
81-
if (timeout) clearTimeout(timeout)
82-
delete currentRemovalTimeouts[key]
83-
}
84-
}
85-
86-
if (context.hasRehydrationInfo(action)) {
87-
const state = mwApi.getState()[reducerPath]
88-
const { queries } = context.extractRehydrationInfo(action)!
89-
for (const [queryCacheKey, queryState] of Object.entries(queries)) {
90-
// Gotcha:
91-
// If rehydrating before the endpoint has been injected,the global `keepUnusedDataFor`
92-
// will be used instead of the endpoint-specific one.
93-
handleUnsubscribe(
94-
queryCacheKey as QueryCacheKey,
95-
queryState?.endpointName,
96-
mwApi,
97-
state.config
98-
)
99-
}
100-
}
71+
handleUnsubscribe(
72+
queryCacheKey,
73+
state.queries[queryCacheKey]?.endpointName,
74+
mwApi,
75+
state.config
76+
)
77+
}
10178

102-
return result
79+
if (api.util.resetApiState.match(action)) {
80+
for (const [key, timeout] of Object.entries(currentRemovalTimeouts)) {
81+
if (timeout) clearTimeout(timeout)
82+
delete currentRemovalTimeouts[key]
10383
}
84+
}
10485

105-
function handleUnsubscribe(
106-
queryCacheKey: QueryCacheKey,
107-
endpointName: string | undefined,
108-
api: SubMiddlewareApi,
109-
config: ConfigState<string>
110-
) {
111-
const endpointDefinition = context.endpointDefinitions[
112-
endpointName!
113-
] as QueryDefinition<any, any, any, any>
114-
const keepUnusedDataFor =
115-
endpointDefinition?.keepUnusedDataFor ?? config.keepUnusedDataFor
116-
117-
if (keepUnusedDataFor === Infinity) {
118-
// Hey, user said keep this forever!
119-
return
86+
if (context.hasRehydrationInfo(action)) {
87+
const state = mwApi.getState()[reducerPath]
88+
const { queries } = context.extractRehydrationInfo(action)!
89+
for (const [queryCacheKey, queryState] of Object.entries(queries)) {
90+
// Gotcha:
91+
// If rehydrating before the endpoint has been injected,the global `keepUnusedDataFor`
92+
// will be used instead of the endpoint-specific one.
93+
handleUnsubscribe(
94+
queryCacheKey as QueryCacheKey,
95+
queryState?.endpointName,
96+
mwApi,
97+
state.config
98+
)
12099
}
121-
// Prevent `setTimeout` timers from overflowing a 32-bit internal int, by
122-
// clamping the max value to be at most 1000ms less than the 32-bit max.
123-
// Look, a 24.8-day keepalive ought to be enough for anybody, right? :)
124-
// Also avoid negative values too.
125-
const finalKeepUnusedDataFor = Math.max(
126-
0,
127-
Math.min(keepUnusedDataFor, THIRTY_TWO_BIT_MAX_TIMER_SECONDS)
128-
)
100+
}
101+
}
129102

130-
if (!anySubscriptionsRemainingForKey(queryCacheKey, api)) {
131-
const currentTimeout = currentRemovalTimeouts[queryCacheKey]
132-
if (currentTimeout) {
133-
clearTimeout(currentTimeout)
134-
}
135-
currentRemovalTimeouts[queryCacheKey] = setTimeout(() => {
136-
if (!anySubscriptionsRemainingForKey(queryCacheKey, api)) {
137-
api.dispatch(removeQueryResult({ queryCacheKey }))
138-
}
139-
delete currentRemovalTimeouts![queryCacheKey]
140-
}, finalKeepUnusedDataFor * 1000)
103+
function handleUnsubscribe(
104+
queryCacheKey: QueryCacheKey,
105+
endpointName: string | undefined,
106+
api: SubMiddlewareApi,
107+
config: ConfigState<string>
108+
) {
109+
const endpointDefinition = context.endpointDefinitions[
110+
endpointName!
111+
] as QueryDefinition<any, any, any, any>
112+
const keepUnusedDataFor =
113+
endpointDefinition?.keepUnusedDataFor ?? config.keepUnusedDataFor
114+
115+
if (keepUnusedDataFor === Infinity) {
116+
// Hey, user said keep this forever!
117+
return
118+
}
119+
// Prevent `setTimeout` timers from overflowing a 32-bit internal int, by
120+
// clamping the max value to be at most 1000ms less than the 32-bit max.
121+
// Look, a 24.8-day keepalive ought to be enough for anybody, right? :)
122+
// Also avoid negative values too.
123+
const finalKeepUnusedDataFor = Math.max(
124+
0,
125+
Math.min(keepUnusedDataFor, THIRTY_TWO_BIT_MAX_TIMER_SECONDS)
126+
)
127+
128+
if (!anySubscriptionsRemainingForKey(queryCacheKey, api)) {
129+
const currentTimeout = currentRemovalTimeouts[queryCacheKey]
130+
if (currentTimeout) {
131+
clearTimeout(currentTimeout)
141132
}
133+
currentRemovalTimeouts[queryCacheKey] = setTimeout(() => {
134+
if (!anySubscriptionsRemainingForKey(queryCacheKey, api)) {
135+
api.dispatch(removeQueryResult({ queryCacheKey }))
136+
}
137+
delete currentRemovalTimeouts![queryCacheKey]
138+
}, finalKeepUnusedDataFor * 1000)
142139
}
143140
}
141+
142+
return handler
144143
}

0 commit comments

Comments
 (0)