UNPKG

@reduxjs/toolkit

Version:

The official, opinionated, batteries-included toolset for efficient Redux development

208 lines (183 loc) 6.78 kB
import type { InternalHandlerBuilder, SubscriptionSelectors } from './types' import type { SubscriptionInternalState, SubscriptionState } from '../apiState' import { produceWithPatches } from '../../utils/immerImports' import type { Action } from '@reduxjs/toolkit' import { getOrInsertComputed, createNewMap } from '../../utils/getOrInsert' export const buildBatchedActionsHandler: InternalHandlerBuilder< [actionShouldContinue: boolean, returnValue: SubscriptionSelectors | boolean] > = ({ api, queryThunk, internalState, mwApi }) => { const subscriptionsPrefix = `${api.reducerPath}/subscriptions` let previousSubscriptions: SubscriptionState = null as unknown as SubscriptionState let updateSyncTimer: ReturnType<typeof window.setTimeout> | null = null const { updateSubscriptionOptions, unsubscribeQueryResult } = api.internalActions // Actually intentionally mutate the subscriptions state used in the middleware // This is done to speed up perf when loading many components const actuallyMutateSubscriptions = ( currentSubscriptions: SubscriptionInternalState, action: Action, ) => { if (updateSubscriptionOptions.match(action)) { const { queryCacheKey, requestId, options } = action.payload const sub = currentSubscriptions.get(queryCacheKey) if (sub?.has(requestId)) { sub.set(requestId, options) } return true } if (unsubscribeQueryResult.match(action)) { const { queryCacheKey, requestId } = action.payload const sub = currentSubscriptions.get(queryCacheKey) if (sub) { sub.delete(requestId) } return true } if (api.internalActions.removeQueryResult.match(action)) { currentSubscriptions.delete(action.payload.queryCacheKey) return true } if (queryThunk.pending.match(action)) { const { meta: { arg, requestId }, } = action const substate = getOrInsertComputed( currentSubscriptions, arg.queryCacheKey, createNewMap, ) if (arg.subscribe) { substate.set( requestId, arg.subscriptionOptions ?? substate.get(requestId) ?? {}, ) } return true } let mutated = false if (queryThunk.rejected.match(action)) { const { meta: { condition, arg, requestId }, } = action if (condition && arg.subscribe) { const substate = getOrInsertComputed( currentSubscriptions, arg.queryCacheKey, createNewMap, ) substate.set( requestId, arg.subscriptionOptions ?? substate.get(requestId) ?? {}, ) mutated = true } } return mutated } const getSubscriptions = () => internalState.currentSubscriptions const getSubscriptionCount = (queryCacheKey: string) => { const subscriptions = getSubscriptions() const subscriptionsForQueryArg = subscriptions.get(queryCacheKey) return subscriptionsForQueryArg?.size ?? 0 } const isRequestSubscribed = (queryCacheKey: string, requestId: string) => { const subscriptions = getSubscriptions() return !!subscriptions?.get(queryCacheKey)?.get(requestId) } const subscriptionSelectors: SubscriptionSelectors = { getSubscriptions, getSubscriptionCount, isRequestSubscribed, } function serializeSubscriptions( currentSubscriptions: SubscriptionInternalState, ): SubscriptionState { // We now use nested Maps for subscriptions, instead of // plain Records. Stringify this accordingly so we can // convert it to the shape we need for the store. return JSON.parse( JSON.stringify( Object.fromEntries( [...currentSubscriptions].map(([k, v]) => [k, Object.fromEntries(v)]), ), ), ) } return ( action, mwApi, ): [ actionShouldContinue: boolean, result: SubscriptionSelectors | boolean, ] => { if (!previousSubscriptions) { // Initialize it the first time this handler runs previousSubscriptions = serializeSubscriptions( internalState.currentSubscriptions, ) } if (api.util.resetApiState.match(action)) { previousSubscriptions = {} internalState.currentSubscriptions.clear() updateSyncTimer = null return [true, false] } // Intercept requests by hooks to see if they're subscribed // We return the internal state reference so that hooks // can do their own checks to see if they're still active. // It's stupid and hacky, but it does cut down on some dispatch calls. if (api.internalActions.internal_getRTKQSubscriptions.match(action)) { return [false, subscriptionSelectors] } // Update subscription data based on this action const didMutate = actuallyMutateSubscriptions( internalState.currentSubscriptions, action, ) let actionShouldContinue = true // HACK Sneak the test-only polling state back out if ( process.env.NODE_ENV === 'test' && typeof action.type === 'string' && action.type === `${api.reducerPath}/getPolling` ) { return [false, internalState.currentPolls] as any } if (didMutate) { if (!updateSyncTimer) { // We only use the subscription state for the Redux DevTools at this point, // as the real data is kept here in the middleware. // Given that, we can throttle synchronizing this state significantly to // save on overall perf. // In 1.9, it was updated in a microtask, but now we do it at most every 500ms. updateSyncTimer = setTimeout(() => { // Deep clone the current subscription data const newSubscriptions: SubscriptionState = serializeSubscriptions( internalState.currentSubscriptions, ) // Figure out a smaller diff between original and current const [, patches] = produceWithPatches( previousSubscriptions, () => newSubscriptions, ) // Sync the store state for visibility mwApi.next(api.internalActions.subscriptionsUpdated(patches)) // Save the cloned state for later reference previousSubscriptions = newSubscriptions updateSyncTimer = null }, 500) } const isSubscriptionSliceAction = typeof action.type == 'string' && !!action.type.startsWith(subscriptionsPrefix) const isAdditionalSubscriptionAction = queryThunk.rejected.match(action) && action.meta.condition && !!action.meta.arg.subscribe actionShouldContinue = !isSubscriptionSliceAction && !isAdditionalSubscriptionAction } return [actionShouldContinue, false] } }