| 1 | import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query'
|
|---|
| 2 | import { configureStore } from '@reduxjs/toolkit'
|
|---|
| 3 | import { vi } from 'vitest'
|
|---|
| 4 | 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'
|
|---|
| 9 | import { countObjectKeys } from '../utils/countObjectKeys'
|
|---|
| 10 |
|
|---|
| 11 | beforeAll(() => {
|
|---|
| 12 | vi.useFakeTimers()
|
|---|
| 13 | })
|
|---|
| 14 |
|
|---|
| 15 | const onCleanup = vi.fn()
|
|---|
| 16 |
|
|---|
| 17 | beforeEach(() => {
|
|---|
| 18 | onCleanup.mockClear()
|
|---|
| 19 | })
|
|---|
| 20 |
|
|---|
| 21 | test(`query: await cleanup, defaults`, async () => {
|
|---|
| 22 | const { store, api } = storeForApi(
|
|---|
| 23 | createApi({
|
|---|
| 24 | baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }),
|
|---|
| 25 | endpoints: (build) => ({
|
|---|
| 26 | query: build.query<unknown, string>({
|
|---|
| 27 | query: () => '/success',
|
|---|
| 28 | }),
|
|---|
| 29 | }),
|
|---|
| 30 | }),
|
|---|
| 31 | )
|
|---|
| 32 |
|
|---|
| 33 | const promise = store.dispatch(api.endpoints.query.initiate('arg'))
|
|---|
| 34 | await promise
|
|---|
| 35 | promise.unsubscribe()
|
|---|
| 36 | vi.advanceTimersByTime(59000)
|
|---|
| 37 | expect(onCleanup).not.toHaveBeenCalled()
|
|---|
| 38 | vi.advanceTimersByTime(2000)
|
|---|
| 39 | expect(onCleanup).toHaveBeenCalled()
|
|---|
| 40 | })
|
|---|
| 41 |
|
|---|
| 42 | test(`query: await cleanup, keepUnusedDataFor set`, async () => {
|
|---|
| 43 | const { store, api } = storeForApi(
|
|---|
| 44 | createApi({
|
|---|
| 45 | baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }),
|
|---|
| 46 | endpoints: (build) => ({
|
|---|
| 47 | query: build.query<unknown, string>({
|
|---|
| 48 | query: () => '/success',
|
|---|
| 49 | }),
|
|---|
| 50 | }),
|
|---|
| 51 | keepUnusedDataFor: 29,
|
|---|
| 52 | }),
|
|---|
| 53 | )
|
|---|
| 54 |
|
|---|
| 55 | const promise = store.dispatch(api.endpoints.query.initiate('arg'))
|
|---|
| 56 | await promise
|
|---|
| 57 | promise.unsubscribe()
|
|---|
| 58 | vi.advanceTimersByTime(28000)
|
|---|
| 59 | expect(onCleanup).not.toHaveBeenCalled()
|
|---|
| 60 | vi.advanceTimersByTime(2000)
|
|---|
| 61 | expect(onCleanup).toHaveBeenCalled()
|
|---|
| 62 | })
|
|---|
| 63 |
|
|---|
| 64 | test(`query: handles large keepUnuseDataFor values over 32-bit ms`, async () => {
|
|---|
| 65 | const { store, api } = storeForApi(
|
|---|
| 66 | createApi({
|
|---|
| 67 | baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }),
|
|---|
| 68 | endpoints: (build) => ({
|
|---|
| 69 | query: build.query<unknown, string>({
|
|---|
| 70 | query: () => '/success',
|
|---|
| 71 | }),
|
|---|
| 72 | }),
|
|---|
| 73 | keepUnusedDataFor: THIRTY_TWO_BIT_MAX_TIMER_SECONDS - 10,
|
|---|
| 74 | }),
|
|---|
| 75 | )
|
|---|
| 76 |
|
|---|
| 77 | const promise = store.dispatch(api.endpoints.query.initiate('arg'))
|
|---|
| 78 | await promise
|
|---|
| 79 | promise.unsubscribe()
|
|---|
| 80 |
|
|---|
| 81 | // Shouldn't have been called right away
|
|---|
| 82 | vi.advanceTimersByTime(1000)
|
|---|
| 83 | expect(onCleanup).not.toHaveBeenCalled()
|
|---|
| 84 |
|
|---|
| 85 | // Shouldn't have been called any time in the next few minutes
|
|---|
| 86 | vi.advanceTimersByTime(1_000_000)
|
|---|
| 87 | expect(onCleanup).not.toHaveBeenCalled()
|
|---|
| 88 |
|
|---|
| 89 | // _Should_ be called _wayyyy_ in the future (like 24.8 days from now)
|
|---|
| 90 | vi.advanceTimersByTime(THIRTY_TWO_BIT_MAX_TIMER_SECONDS * 1000),
|
|---|
| 91 | expect(onCleanup).toHaveBeenCalled()
|
|---|
| 92 | })
|
|---|
| 93 |
|
|---|
| 94 | describe(`query: await cleanup, keepUnusedDataFor set`, () => {
|
|---|
| 95 | const { store, api } = storeForApi(
|
|---|
| 96 | createApi({
|
|---|
| 97 | baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }),
|
|---|
| 98 | endpoints: (build) => ({
|
|---|
| 99 | query: build.query<unknown, string>({
|
|---|
| 100 | query: () => '/success',
|
|---|
| 101 | }),
|
|---|
| 102 | query2: build.query<unknown, string>({
|
|---|
| 103 | query: () => '/success',
|
|---|
| 104 | keepUnusedDataFor: 35,
|
|---|
| 105 | }),
|
|---|
| 106 | query3: build.query<unknown, string>({
|
|---|
| 107 | query: () => '/success',
|
|---|
| 108 | keepUnusedDataFor: 0,
|
|---|
| 109 | }),
|
|---|
| 110 | query4: build.query<unknown, string>({
|
|---|
| 111 | query: () => '/success',
|
|---|
| 112 | keepUnusedDataFor: Infinity,
|
|---|
| 113 | }),
|
|---|
| 114 | }),
|
|---|
| 115 | keepUnusedDataFor: 29,
|
|---|
| 116 | }),
|
|---|
| 117 | )
|
|---|
| 118 |
|
|---|
| 119 | test('global keepUnusedDataFor', async () => {
|
|---|
| 120 | const promise = store.dispatch(api.endpoints.query.initiate('arg'))
|
|---|
| 121 | await promise
|
|---|
| 122 | promise.unsubscribe()
|
|---|
| 123 | vi.advanceTimersByTime(28000)
|
|---|
| 124 | expect(onCleanup).not.toHaveBeenCalled()
|
|---|
| 125 | vi.advanceTimersByTime(2000)
|
|---|
| 126 | expect(onCleanup).toHaveBeenCalled()
|
|---|
| 127 | })
|
|---|
| 128 |
|
|---|
| 129 | test('endpoint keepUnusedDataFor', async () => {
|
|---|
| 130 | const promise = store.dispatch(api.endpoints.query2.initiate('arg'))
|
|---|
| 131 | await promise
|
|---|
| 132 | promise.unsubscribe()
|
|---|
| 133 |
|
|---|
| 134 | vi.advanceTimersByTime(34000)
|
|---|
| 135 | expect(onCleanup).not.toHaveBeenCalled()
|
|---|
| 136 | vi.advanceTimersByTime(2000)
|
|---|
| 137 | expect(onCleanup).toHaveBeenCalled()
|
|---|
| 138 | })
|
|---|
| 139 |
|
|---|
| 140 | test('endpoint keepUnusedDataFor: 0 ', async () => {
|
|---|
| 141 | expect(onCleanup).not.toHaveBeenCalled()
|
|---|
| 142 | const promise = store.dispatch(api.endpoints.query3.initiate('arg'))
|
|---|
| 143 | await promise
|
|---|
| 144 | promise.unsubscribe()
|
|---|
| 145 | expect(onCleanup).not.toHaveBeenCalled()
|
|---|
| 146 | vi.advanceTimersByTime(1)
|
|---|
| 147 | expect(onCleanup).toHaveBeenCalled()
|
|---|
| 148 | })
|
|---|
| 149 |
|
|---|
| 150 | test('endpoint keepUnusedDataFor: Infinity', async () => {
|
|---|
| 151 | expect(onCleanup).not.toHaveBeenCalled()
|
|---|
| 152 | store.dispatch(api.endpoints.query4.initiate('arg')).unsubscribe()
|
|---|
| 153 | expect(onCleanup).not.toHaveBeenCalled()
|
|---|
| 154 | vi.advanceTimersByTime(THIRTY_TWO_BIT_MAX_INT)
|
|---|
| 155 | expect(onCleanup).not.toHaveBeenCalled()
|
|---|
| 156 | })
|
|---|
| 157 | })
|
|---|
| 158 |
|
|---|
| 159 | describe('resetApiState cleanup', () => {
|
|---|
| 160 | test('resetApiState aborts multiple running queries and mutations', async () => {
|
|---|
| 161 | const { store, api } = storeForApi(
|
|---|
| 162 | createApi({
|
|---|
| 163 | baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }),
|
|---|
| 164 | endpoints: (build) => ({
|
|---|
| 165 | query1: build.query<unknown, string>({
|
|---|
| 166 | query: () => '/success',
|
|---|
| 167 | }),
|
|---|
| 168 | query2: build.query<unknown, string>({
|
|---|
| 169 | query: () => '/success',
|
|---|
| 170 | }),
|
|---|
| 171 | mutation: build.mutation<unknown, string>({
|
|---|
| 172 | query: () => ({
|
|---|
| 173 | url: '/success',
|
|---|
| 174 | method: 'POST',
|
|---|
| 175 | }),
|
|---|
| 176 | }),
|
|---|
| 177 | }),
|
|---|
| 178 | }),
|
|---|
| 179 | )
|
|---|
| 180 |
|
|---|
| 181 | // Start multiple queries and a mutation
|
|---|
| 182 | const queryPromise1 = store.dispatch(api.endpoints.query1.initiate('arg1'))
|
|---|
| 183 | const queryPromise2 = store.dispatch(api.endpoints.query2.initiate('arg2'))
|
|---|
| 184 | const mutationPromise = store.dispatch(
|
|---|
| 185 | api.endpoints.mutation.initiate('arg'),
|
|---|
| 186 | )
|
|---|
| 187 |
|
|---|
| 188 | // Spy on abort methods
|
|---|
| 189 | queryPromise1.abort = vi.fn(queryPromise1.abort)
|
|---|
| 190 | queryPromise2.abort = vi.fn(queryPromise2.abort)
|
|---|
| 191 | mutationPromise.abort = vi.fn(mutationPromise.abort)
|
|---|
| 192 |
|
|---|
| 193 | // Dispatch resetApiState
|
|---|
| 194 | store.dispatch(api.util.resetApiState())
|
|---|
| 195 |
|
|---|
| 196 | // Verify all aborts were called
|
|---|
| 197 | expect(queryPromise1.abort).toHaveBeenCalled()
|
|---|
| 198 | expect(queryPromise2.abort).toHaveBeenCalled()
|
|---|
| 199 | expect(mutationPromise.abort).toHaveBeenCalled()
|
|---|
| 200 | })
|
|---|
| 201 | })
|
|---|
| 202 |
|
|---|
| 203 | function storeForApi<
|
|---|
| 204 | A extends {
|
|---|
| 205 | reducerPath: 'api'
|
|---|
| 206 | reducer: Reducer<any, any>
|
|---|
| 207 | middleware: Middleware
|
|---|
| 208 | util: { resetApiState(): any }
|
|---|
| 209 | },
|
|---|
| 210 | >(api: A) {
|
|---|
| 211 | const store = configureStore({
|
|---|
| 212 | reducer: { api: api.reducer },
|
|---|
| 213 | middleware: (gdm) =>
|
|---|
| 214 | gdm({ serializableCheck: false, immutableCheck: false }).concat(
|
|---|
| 215 | api.middleware,
|
|---|
| 216 | ),
|
|---|
| 217 | enhancers: (gde) =>
|
|---|
| 218 | gde({
|
|---|
| 219 | autoBatch: false,
|
|---|
| 220 | }),
|
|---|
| 221 | })
|
|---|
| 222 | let hadQueries = false
|
|---|
| 223 | store.subscribe(() => {
|
|---|
| 224 | const queryState = store.getState().api.queries
|
|---|
| 225 | if (hadQueries && countObjectKeys(queryState) === 0) {
|
|---|
| 226 | onCleanup()
|
|---|
| 227 | }
|
|---|
| 228 | hadQueries = hadQueries || countObjectKeys(queryState) > 0
|
|---|
| 229 | })
|
|---|
| 230 | return { api, store }
|
|---|
| 231 | }
|
|---|