| [a762898] | 1 | import type { StoreEnhancer } from 'redux'
|
|---|
| 2 |
|
|---|
| 3 | export const SHOULD_AUTOBATCH = 'RTK_autoBatch'
|
|---|
| 4 |
|
|---|
| 5 | export const prepareAutoBatched =
|
|---|
| 6 | <T>() =>
|
|---|
| 7 | (payload: T): { payload: T; meta: unknown } => ({
|
|---|
| 8 | payload,
|
|---|
| 9 | meta: { [SHOULD_AUTOBATCH]: true },
|
|---|
| 10 | })
|
|---|
| 11 |
|
|---|
| 12 | const createQueueWithTimer = (timeout: number) => {
|
|---|
| 13 | return (notify: () => void) => {
|
|---|
| 14 | setTimeout(notify, timeout)
|
|---|
| 15 | }
|
|---|
| 16 | }
|
|---|
| 17 |
|
|---|
| 18 | export type AutoBatchOptions =
|
|---|
| 19 | | { type: 'tick' }
|
|---|
| 20 | | { type: 'timer'; timeout: number }
|
|---|
| 21 | | { type: 'raf' }
|
|---|
| 22 | | { type: 'callback'; queueNotification: (notify: () => void) => void }
|
|---|
| 23 |
|
|---|
| 24 | /**
|
|---|
| 25 | * A Redux store enhancer that watches for "low-priority" actions, and delays
|
|---|
| 26 | * notifying subscribers until either the queued callback executes or the
|
|---|
| 27 | * next "standard-priority" action is dispatched.
|
|---|
| 28 | *
|
|---|
| 29 | * This allows dispatching multiple "low-priority" actions in a row with only
|
|---|
| 30 | * a single subscriber notification to the UI after the sequence of actions
|
|---|
| 31 | * is finished, thus improving UI re-render performance.
|
|---|
| 32 | *
|
|---|
| 33 | * Watches for actions with the `action.meta[SHOULD_AUTOBATCH]` attribute.
|
|---|
| 34 | * This can be added to `action.meta` manually, or by using the
|
|---|
| 35 | * `prepareAutoBatched` helper.
|
|---|
| 36 | *
|
|---|
| 37 | * By default, it will queue a notification for the end of the event loop tick.
|
|---|
| 38 | * However, you can pass several other options to configure the behavior:
|
|---|
| 39 | * - `{type: 'tick'}`: queues using `queueMicrotask`
|
|---|
| 40 | * - `{type: 'timer', timeout: number}`: queues using `setTimeout`
|
|---|
| 41 | * - `{type: 'raf'}`: queues using `requestAnimationFrame` (default)
|
|---|
| 42 | * - `{type: 'callback', queueNotification: (notify: () => void) => void}`: lets you provide your own callback
|
|---|
| 43 | *
|
|---|
| 44 | *
|
|---|
| 45 | */
|
|---|
| 46 | export const autoBatchEnhancer =
|
|---|
| 47 | (options: AutoBatchOptions = { type: 'raf' }): StoreEnhancer =>
|
|---|
| 48 | (next) =>
|
|---|
| 49 | (...args) => {
|
|---|
| 50 | const store = next(...args)
|
|---|
| 51 |
|
|---|
| 52 | let notifying = true
|
|---|
| 53 | let shouldNotifyAtEndOfTick = false
|
|---|
| 54 | let notificationQueued = false
|
|---|
| 55 |
|
|---|
| 56 | const listeners = new Set<() => void>()
|
|---|
| 57 |
|
|---|
| 58 | const queueCallback =
|
|---|
| 59 | options.type === 'tick'
|
|---|
| 60 | ? queueMicrotask
|
|---|
| 61 | : options.type === 'raf'
|
|---|
| 62 | ? // requestAnimationFrame won't exist in SSR environments. Fall back to a vague approximation just to keep from erroring.
|
|---|
| 63 | typeof window !== 'undefined' && window.requestAnimationFrame
|
|---|
| 64 | ? window.requestAnimationFrame
|
|---|
| 65 | : createQueueWithTimer(10)
|
|---|
| 66 | : options.type === 'callback'
|
|---|
| 67 | ? options.queueNotification
|
|---|
| 68 | : createQueueWithTimer(options.timeout)
|
|---|
| 69 |
|
|---|
| 70 | const notifyListeners = () => {
|
|---|
| 71 | // We're running at the end of the event loop tick.
|
|---|
| 72 | // Run the real listener callbacks to actually update the UI.
|
|---|
| 73 | notificationQueued = false
|
|---|
| 74 | if (shouldNotifyAtEndOfTick) {
|
|---|
| 75 | shouldNotifyAtEndOfTick = false
|
|---|
| 76 | listeners.forEach((l) => l())
|
|---|
| 77 | }
|
|---|
| 78 | }
|
|---|
| 79 |
|
|---|
| 80 | return Object.assign({}, store, {
|
|---|
| 81 | // Override the base `store.subscribe` method to keep original listeners
|
|---|
| 82 | // from running if we're delaying notifications
|
|---|
| 83 | subscribe(listener: () => void) {
|
|---|
| 84 | // Each wrapped listener will only call the real listener if
|
|---|
| 85 | // the `notifying` flag is currently active when it's called.
|
|---|
| 86 | // This lets the base store work as normal, while the actual UI
|
|---|
| 87 | // update becomes controlled by this enhancer.
|
|---|
| 88 | const wrappedListener: typeof listener = () => notifying && listener()
|
|---|
| 89 | const unsubscribe = store.subscribe(wrappedListener)
|
|---|
| 90 | listeners.add(listener)
|
|---|
| 91 | return () => {
|
|---|
| 92 | unsubscribe()
|
|---|
| 93 | listeners.delete(listener)
|
|---|
| 94 | }
|
|---|
| 95 | },
|
|---|
| 96 | // Override the base `store.dispatch` method so that we can check actions
|
|---|
| 97 | // for the `shouldAutoBatch` flag and determine if batching is active
|
|---|
| 98 | dispatch(action: any) {
|
|---|
| 99 | try {
|
|---|
| 100 | // If the action does _not_ have the `shouldAutoBatch` flag,
|
|---|
| 101 | // we resume/continue normal notify-after-each-dispatch behavior
|
|---|
| 102 | notifying = !action?.meta?.[SHOULD_AUTOBATCH]
|
|---|
| 103 | // If a `notifyListeners` microtask was queued, you can't cancel it.
|
|---|
| 104 | // Instead, we set a flag so that it's a no-op when it does run
|
|---|
| 105 | shouldNotifyAtEndOfTick = !notifying
|
|---|
| 106 | if (shouldNotifyAtEndOfTick) {
|
|---|
| 107 | // We've seen at least 1 action with `SHOULD_AUTOBATCH`. Try to queue
|
|---|
| 108 | // a microtask to notify listeners at the end of the event loop tick.
|
|---|
| 109 | // Make sure we only enqueue this _once_ per tick.
|
|---|
| 110 | if (!notificationQueued) {
|
|---|
| 111 | notificationQueued = true
|
|---|
| 112 | queueCallback(notifyListeners)
|
|---|
| 113 | }
|
|---|
| 114 | }
|
|---|
| 115 | // Go ahead and process the action as usual, including reducers.
|
|---|
| 116 | // If normal notification behavior is enabled, the store will notify
|
|---|
| 117 | // all of its own listeners, and the wrapper callbacks above will
|
|---|
| 118 | // see `notifying` is true and pass on to the real listener callbacks.
|
|---|
| 119 | // If we're "batching" behavior, then the wrapped callbacks will
|
|---|
| 120 | // bail out, causing the base store notification behavior to be no-ops.
|
|---|
| 121 | return store.dispatch(action)
|
|---|
| 122 | } finally {
|
|---|
| 123 | // Assume we're back to normal behavior after each action
|
|---|
| 124 | notifying = true
|
|---|
| 125 | }
|
|---|
| 126 | },
|
|---|
| 127 | })
|
|---|
| 128 | }
|
|---|