Latest repo

This commit is contained in:
Marc
2025-06-02 16:42:16 +00:00
parent 53ddf1a329
commit cde5fae175
27907 changed files with 3875388 additions and 1 deletions

View File

@@ -0,0 +1,175 @@
import { describe, expectTypeOf, it } from 'vitest'
import type { OmitKeyof } from '..'
describe('OmitKeyof', () => {
it("'s string key type check", () => {
type A = {
x: string
y: number
}
type ExpectedType = {
x: string
}
// Bad point
// 1. original Omit can use 'z' as type parameter with no type error
// 2. original Omit have no auto complete for 2nd type parameter
expectTypeOf<Omit<A, 'z' | 'y'>>().toEqualTypeOf<ExpectedType>()
// Solution
// 1. strictly
expectTypeOf<
OmitKeyof<
A,
// OmitKeyof can't use 'z' as type parameter with type error because A don't have key 'z'
// @ts-expect-error Type does not satisfy the constraint keyof A
'z' | 'y'
>
>().toEqualTypeOf<ExpectedType>()
expectTypeOf<
OmitKeyof<
A,
// OmitKeyof can't use 'z' as type parameter with type error because A don't have key 'z'
// @ts-expect-error Type does not satisfy the constraint keyof A
'z' | 'y',
'strictly'
>
>().toEqualTypeOf<ExpectedType>()
// 2. safely
expectTypeOf<
OmitKeyof<
A,
// OmitKeyof can't use 'z' as type parameter type error with strictly parameter or default parameter
// @ts-expect-error Type does not satisfy the constraint keyof A
'z' | 'y'
>
>().toEqualTypeOf<ExpectedType>()
expectTypeOf<
OmitKeyof<
A,
// With 'safely', OmitKeyof can use 'z' as type parameter like original Omit but This support autocomplete too yet for DX.
'z' | 'y',
'safely'
>
>().toEqualTypeOf<ExpectedType>()
})
it("'s number key type check", () => {
type A = {
[1]: string
[2]: number
}
type ExpectedType = {
[1]: string
}
// Bad point
// 1. original Omit can use 3 as type parameter with no type error
// 2. original Omit have no auto complete for 2nd type parameter
expectTypeOf<Omit<A, 3 | 2>>().toEqualTypeOf<ExpectedType>()
// Solution
// 1. strictly
expectTypeOf<
OmitKeyof<
A,
// OmitKeyof can't use 3 as type parameter with type error because A don't have key 3
// @ts-expect-error Type does not satisfy the constraint keyof A
3 | 2
>
>().toEqualTypeOf<ExpectedType>()
expectTypeOf<
OmitKeyof<
A,
// OmitKeyof can't use 3 as type parameter with type error because A don't have key 3
// @ts-expect-error Type does not satisfy the constraint keyof A
3 | 2,
'strictly'
>
>().toEqualTypeOf<ExpectedType>()
// 2. safely
expectTypeOf<
OmitKeyof<
A,
// OmitKeyof can't use 3 as type parameter type error with strictly parameter or default parameter
// @ts-expect-error Type does not satisfy the constraint keyof A
3 | 2
>
>().toEqualTypeOf<ExpectedType>()
expectTypeOf<
OmitKeyof<
A,
// With 'safely', OmitKeyof can use 3 as type parameter like original Omit but This support autocomplete too yet for DX.
3 | 2,
'safely'
>
>().toEqualTypeOf<ExpectedType>()
})
it("'s symbol key type check", () => {
const symbol1 = Symbol()
const symbol2 = Symbol()
const symbol3 = Symbol()
type A = {
[symbol1]: string
[symbol2]: number
}
type ExpectedType = {
[symbol1]: string
}
// Bad point
// 1. original Omit can use symbol3 as type parameter with no type error
// 2. original Omit have no auto complete for 2nd type parameter
expectTypeOf<
Omit<A, typeof symbol3 | typeof symbol2>
>().toEqualTypeOf<ExpectedType>()
// Solution
// 1. strictly
expectTypeOf<
OmitKeyof<
A,
// OmitKeyof can't use symbol3 as type parameter with type error because A don't have key symbol3
// @ts-expect-error Type does not satisfy the constraint keyof A
typeof symbol3 | typeof symbol2
>
>().toEqualTypeOf<ExpectedType>()
expectTypeOf<
OmitKeyof<
A,
// OmitKeyof can't use symbol3 as type parameter with type error because A don't have key symbol3
// @ts-expect-error Type does not satisfy the constraint keyof A
typeof symbol3 | typeof symbol2,
'strictly'
>
>().toEqualTypeOf<ExpectedType>()
// 2. safely
expectTypeOf<
OmitKeyof<
A,
// OmitKeyof can't use symbol3 as type parameter type error with strictly parameter or default parameter
// @ts-expect-error Type does not satisfy the constraint keyof A
typeof symbol3 | typeof symbol2
>
>().toEqualTypeOf<ExpectedType>()
expectTypeOf<
OmitKeyof<
A,
// With 'safely', OmitKeyof can use symbol3 as type parameter like original Omit but This support autocomplete too yet for DX.
typeof symbol3 | typeof symbol2,
'safely'
>
>().toEqualTypeOf<ExpectedType>()
})
})

View File

@@ -0,0 +1,163 @@
import { beforeEach, describe, expect, it, test, vi } from 'vitest'
import { sleep } from '../utils'
import { FocusManager } from '../focusManager'
import { setIsServer } from './utils'
describe('focusManager', () => {
let focusManager: FocusManager
beforeEach(() => {
vi.resetModules()
focusManager = new FocusManager()
})
it('should call previous remove handler when replacing an event listener', () => {
const remove1Spy = vi.fn()
const remove2Spy = vi.fn()
focusManager.setEventListener(() => remove1Spy)
focusManager.setEventListener(() => remove2Spy)
expect(remove1Spy).toHaveBeenCalledTimes(1)
expect(remove2Spy).not.toHaveBeenCalled()
})
it('should use focused boolean arg', async () => {
let count = 0
const setup = (setFocused: (focused?: boolean) => void) => {
setTimeout(() => {
count++
setFocused(true)
}, 20)
return () => void 0
}
focusManager.setEventListener(setup)
await sleep(30)
expect(count).toEqual(1)
expect(focusManager.isFocused()).toBeTruthy()
})
it('should return true for isFocused if document is undefined', async () => {
const { document } = globalThis
// @ts-expect-error
delete globalThis.document
focusManager.setFocused()
expect(focusManager.isFocused()).toBeTruthy()
globalThis.document = document
})
test('cleanup (removeEventListener) should not be called if window is not defined', async () => {
const restoreIsServer = setIsServer(true)
const removeEventListenerSpy = vi.spyOn(globalThis, 'removeEventListener')
const unsubscribe = focusManager.subscribe(() => undefined)
unsubscribe()
expect(removeEventListenerSpy).not.toHaveBeenCalled()
restoreIsServer()
})
test('cleanup (removeEventListener) should not be called if window.addEventListener is not defined', async () => {
const { addEventListener } = globalThis.window
// @ts-expect-error
globalThis.window.addEventListener = undefined
const removeEventListenerSpy = vi.spyOn(globalThis, 'removeEventListener')
const unsubscribe = focusManager.subscribe(() => undefined)
unsubscribe()
expect(removeEventListenerSpy).not.toHaveBeenCalled()
globalThis.window.addEventListener = addEventListener
})
it('should replace default window listener when a new event listener is set', async () => {
const unsubscribeSpy = vi.fn().mockImplementation(() => undefined)
const handlerSpy = vi.fn().mockImplementation(() => unsubscribeSpy)
focusManager.setEventListener(() => handlerSpy())
const unsubscribe = focusManager.subscribe(() => undefined)
// Should call the custom event once
expect(handlerSpy).toHaveBeenCalledTimes(1)
unsubscribe()
// Should unsubscribe our event event
expect(unsubscribeSpy).toHaveBeenCalledTimes(1)
handlerSpy.mockRestore()
unsubscribeSpy.mockRestore()
})
test('should call removeEventListener when last listener unsubscribes', () => {
const addEventListenerSpy = vi.spyOn(globalThis.window, 'addEventListener')
const removeEventListenerSpy = vi.spyOn(
globalThis.window,
'removeEventListener',
)
const unsubscribe1 = focusManager.subscribe(() => undefined)
const unsubscribe2 = focusManager.subscribe(() => undefined)
expect(addEventListenerSpy).toHaveBeenCalledTimes(1) // visibilitychange event
unsubscribe1()
expect(removeEventListenerSpy).toHaveBeenCalledTimes(0)
unsubscribe2()
expect(removeEventListenerSpy).toHaveBeenCalledTimes(1) // visibilitychange event
})
test('should keep setup function even if last listener unsubscribes', () => {
const setupSpy = vi.fn().mockImplementation(() => () => undefined)
focusManager.setEventListener(setupSpy)
const unsubscribe1 = focusManager.subscribe(() => undefined)
expect(setupSpy).toHaveBeenCalledTimes(1)
unsubscribe1()
const unsubscribe2 = focusManager.subscribe(() => undefined)
expect(setupSpy).toHaveBeenCalledTimes(2)
unsubscribe2()
})
test('should call listeners when setFocused is called', () => {
const listener = vi.fn()
focusManager.subscribe(listener)
focusManager.setFocused(true)
focusManager.setFocused(true)
expect(listener).toHaveBeenCalledTimes(1)
expect(listener).toHaveBeenNthCalledWith(1, true)
focusManager.setFocused(false)
focusManager.setFocused(false)
expect(listener).toHaveBeenCalledTimes(2)
expect(listener).toHaveBeenNthCalledWith(2, false)
focusManager.setFocused(undefined)
focusManager.setFocused(undefined)
expect(listener).toHaveBeenCalledTimes(3)
expect(listener).toHaveBeenNthCalledWith(3, true)
})
})

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,427 @@
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
import { waitFor } from '@testing-library/react'
import { CancelledError, InfiniteQueryObserver } from '..'
import { createQueryClient, queryKey, sleep } from './utils'
import type {
InfiniteData,
InfiniteQueryObserverResult,
QueryCache,
QueryClient,
} from '..'
describe('InfiniteQueryBehavior', () => {
let queryClient: QueryClient
let queryCache: QueryCache
beforeEach(() => {
queryClient = createQueryClient()
queryCache = queryClient.getQueryCache()
queryClient.mount()
})
afterEach(() => {
queryClient.clear()
})
test('InfiniteQueryBehavior should throw an error if the queryFn is not defined', async () => {
const key = queryKey()
const observer = new InfiniteQueryObserver(queryClient, {
queryKey: key,
retry: false,
initialPageParam: 1,
getNextPageParam: () => 2,
})
let observerResult:
| InfiniteQueryObserverResult<unknown, unknown>
| undefined
const unsubscribe = observer.subscribe((result) => {
observerResult = result
})
await waitFor(() => {
const query = queryCache.find({ queryKey: key })!
return expect(observerResult).toMatchObject({
isError: true,
error: new Error(`Missing queryFn: '${query.queryHash}'`),
})
})
unsubscribe()
})
test('InfiniteQueryBehavior should apply the maxPages option to limit the number of pages', async () => {
const key = queryKey()
let abortSignal: AbortSignal | null = null
const queryFnSpy = vi.fn().mockImplementation(({ pageParam, signal }) => {
abortSignal = signal
return pageParam
})
const observer = new InfiniteQueryObserver<number>(queryClient, {
queryKey: key,
queryFn: queryFnSpy,
getNextPageParam: (lastPage) => lastPage + 1,
getPreviousPageParam: (firstPage) => firstPage - 1,
maxPages: 2,
initialPageParam: 1,
})
let observerResult:
| InfiniteQueryObserverResult<unknown, unknown>
| undefined
const unsubscribe = observer.subscribe((result) => {
observerResult = result
})
// Wait for the first page to be fetched
await waitFor(() =>
expect(observerResult).toMatchObject({
isFetching: false,
data: { pages: [1], pageParams: [1] },
}),
)
expect(queryFnSpy).toHaveBeenNthCalledWith(1, {
queryKey: key,
pageParam: 1,
meta: undefined,
direction: 'forward',
signal: abortSignal,
})
queryFnSpy.mockClear()
// Fetch the second page
await observer.fetchNextPage()
expect(queryFnSpy).toHaveBeenNthCalledWith(1, {
queryKey: key,
pageParam: 2,
direction: 'forward',
meta: undefined,
signal: abortSignal,
})
expect(observerResult).toMatchObject({
isFetching: false,
data: { pages: [1, 2], pageParams: [1, 2] },
})
queryFnSpy.mockClear()
// Fetch the page before the first page
await observer.fetchPreviousPage()
expect(queryFnSpy).toHaveBeenNthCalledWith(1, {
queryKey: key,
pageParam: 0,
direction: 'backward',
meta: undefined,
signal: abortSignal,
})
// Only first two pages should be in the data
expect(observerResult).toMatchObject({
isFetching: false,
data: { pages: [0, 1], pageParams: [0, 1] },
})
queryFnSpy.mockClear()
// Fetch the page before
await observer.fetchPreviousPage()
expect(queryFnSpy).toHaveBeenNthCalledWith(1, {
queryKey: key,
pageParam: -1,
meta: undefined,
direction: 'backward',
signal: abortSignal,
})
expect(observerResult).toMatchObject({
isFetching: false,
data: { pages: [-1, 0], pageParams: [-1, 0] },
})
queryFnSpy.mockClear()
// Fetch the page after
await observer.fetchNextPage()
expect(queryFnSpy).toHaveBeenNthCalledWith(1, {
queryKey: key,
pageParam: 1,
meta: undefined,
direction: 'forward',
signal: abortSignal,
})
expect(observerResult).toMatchObject({
isFetching: false,
data: { pages: [0, 1] },
})
queryFnSpy.mockClear()
// Refetch the infinite query
await observer.refetch()
// Only 2 pages should refetch
expect(queryFnSpy).toHaveBeenCalledTimes(2)
expect(queryFnSpy).toHaveBeenNthCalledWith(1, {
queryKey: key,
pageParam: 0,
meta: undefined,
direction: 'forward',
signal: abortSignal,
})
expect(queryFnSpy).toHaveBeenNthCalledWith(2, {
queryKey: key,
pageParam: 1,
meta: undefined,
direction: 'forward',
signal: abortSignal,
})
unsubscribe()
})
test('InfiniteQueryBehavior should support query cancellation', async () => {
const key = queryKey()
let abortSignal: AbortSignal | null = null
const queryFnSpy = vi.fn().mockImplementation(({ pageParam, signal }) => {
abortSignal = signal
sleep(10)
return pageParam
})
const observer = new InfiniteQueryObserver<number>(queryClient, {
queryKey: key,
queryFn: queryFnSpy,
getNextPageParam: (lastPage) => lastPage + 1,
getPreviousPageParam: (firstPage) => firstPage - 1,
initialPageParam: 1,
})
let observerResult:
| InfiniteQueryObserverResult<unknown, unknown>
| undefined
const unsubscribe = observer.subscribe((result) => {
observerResult = result
})
const query = observer.getCurrentQuery()
query.cancel()
// Wait for the first page to be cancelled
await waitFor(() =>
expect(observerResult).toMatchObject({
isFetching: false,
isError: true,
error: new CancelledError(),
data: undefined,
}),
)
expect(queryFnSpy).toHaveBeenCalledTimes(1)
expect(queryFnSpy).toHaveBeenNthCalledWith(1, {
queryKey: key,
pageParam: 1,
meta: undefined,
direction: 'forward',
signal: abortSignal,
})
unsubscribe()
})
test('InfiniteQueryBehavior should not refetch pages if the query is cancelled', async () => {
const key = queryKey()
let abortSignal: AbortSignal | null = null
let queryFnSpy = vi.fn().mockImplementation(({ pageParam, signal }) => {
abortSignal = signal
return pageParam
})
const observer = new InfiniteQueryObserver<number>(queryClient, {
queryKey: key,
queryFn: queryFnSpy,
getNextPageParam: (lastPage) => lastPage + 1,
getPreviousPageParam: (firstPage) => firstPage - 1,
initialPageParam: 1,
})
let observerResult:
| InfiniteQueryObserverResult<unknown, unknown>
| undefined
const unsubscribe = observer.subscribe((result) => {
observerResult = result
})
// Wait for the first page to be fetched
await waitFor(() =>
expect(observerResult).toMatchObject({
isFetching: false,
data: { pages: [1], pageParams: [1] },
}),
)
queryFnSpy.mockClear()
// Fetch the second page
await observer.fetchNextPage()
expect(observerResult).toMatchObject({
isFetching: false,
data: { pages: [1, 2], pageParams: [1, 2] },
})
expect(queryFnSpy).toHaveBeenCalledTimes(1)
expect(queryFnSpy).toHaveBeenNthCalledWith(1, {
queryKey: key,
pageParam: 2,
meta: undefined,
direction: 'forward',
signal: abortSignal,
})
queryFnSpy = vi.fn().mockImplementation(({ pageParam = 1, signal }) => {
abortSignal = signal
sleep(10)
return pageParam
})
// Refetch the query
observer.refetch()
expect(observerResult).toMatchObject({
isFetching: true,
isError: false,
})
// Cancel the query
const query = observer.getCurrentQuery()
query.cancel()
expect(observerResult).toMatchObject({
isFetching: false,
isError: true,
error: new CancelledError(),
data: { pages: [1, 2], pageParams: [1, 2] },
})
// Pages should not have been fetched
expect(queryFnSpy).toHaveBeenCalledTimes(0)
unsubscribe()
})
test('InfiniteQueryBehavior should not enter an infinite loop when a page errors while retry is on #8046', async () => {
let errorCount = 0
const key = queryKey()
interface TestResponse {
data: Array<{ id: string }>
nextToken?: number
}
const fakeData = [
{ data: [{ id: 'item-1' }], nextToken: 1 },
{ data: [{ id: 'item-2' }], nextToken: 2 },
{ data: [{ id: 'item-3' }], nextToken: 3 },
{ data: [{ id: 'item-4' }] },
]
const fetchData = async ({ nextToken = 0 }: { nextToken?: number }) =>
new Promise<TestResponse>((resolve, reject) => {
setTimeout(() => {
if (nextToken == 2 && errorCount < 3) {
errorCount += 1
reject({ statusCode: 429 })
return
}
resolve(fakeData[nextToken] as TestResponse)
}, 10)
})
const observer = new InfiniteQueryObserver<
TestResponse,
Error,
InfiniteData<TestResponse>,
TestResponse,
typeof key,
number
>(queryClient, {
retry: 5,
staleTime: 0,
retryDelay: 10,
queryKey: key,
initialPageParam: 1,
getNextPageParam: (lastPage) => lastPage.nextToken,
queryFn: ({ pageParam }) => fetchData({ nextToken: pageParam }),
})
// Fetch Page 1
const page1Data = await observer.fetchNextPage()
expect(page1Data.data?.pageParams).toEqual([1])
// Fetch Page 2, as per the queryFn, this will reject 2 times then resolves
const page2Data = await observer.fetchNextPage()
expect(page2Data.data?.pageParams).toEqual([1, 2])
// Fetch Page 3
const page3Data = await observer.fetchNextPage()
expect(page3Data.data?.pageParams).toEqual([1, 2, 3])
// Now the real deal; re-fetching this query **should not** stamp into an
// infinite loop where the retryer every time restarts from page 1
// once it reaches the page where it errors.
// For this to work, we'd need to reset the error count so we actually retry
errorCount = 0
const reFetchedData = await observer.refetch()
expect(reFetchedData.data?.pageParams).toEqual([1, 2, 3])
})
test('should fetch even if initialPageParam is null', async () => {
const key = queryKey()
const observer = new InfiniteQueryObserver(queryClient, {
queryKey: key,
queryFn: async () => 'data',
getNextPageParam: () => null,
initialPageParam: null,
})
let observerResult:
| InfiniteQueryObserverResult<unknown, unknown>
| undefined
const unsubscribe = observer.subscribe((result) => {
observerResult = result
})
await waitFor(() =>
expect(observerResult).toMatchObject({
isFetching: false,
data: { pages: ['data'], pageParams: [null] },
}),
)
unsubscribe()
})
})

View File

@@ -0,0 +1,64 @@
import { afterEach, beforeEach, describe, expectTypeOf, it, vi } from 'vitest'
import { InfiniteQueryObserver } from '..'
import { createQueryClient, queryKey } from './utils'
import type { InfiniteData, QueryClient } from '..'
describe('InfiniteQueryObserver', () => {
let queryClient: QueryClient
beforeEach(() => {
queryClient = createQueryClient()
queryClient.mount()
})
afterEach(() => {
queryClient.clear()
})
it('should be inferred as a correct result type', async () => {
const next: number | undefined = 2
const queryFn = vi.fn(({ pageParam }) => String(pageParam))
const observer = new InfiniteQueryObserver(queryClient, {
queryKey: queryKey(),
queryFn,
initialPageParam: 1,
getNextPageParam: () => next,
})
const result = observer.getCurrentResult()
if (result.isPending) {
expectTypeOf(result.data).toEqualTypeOf<undefined>()
expectTypeOf(result.error).toEqualTypeOf<null>()
expectTypeOf(result.isLoading).toEqualTypeOf<boolean>()
expectTypeOf(result.status).toEqualTypeOf<'pending'>()
}
if (result.isLoading) {
expectTypeOf(result.data).toEqualTypeOf<undefined>()
expectTypeOf(result.error).toEqualTypeOf<null>()
expectTypeOf(result.isPending).toEqualTypeOf<true>()
expectTypeOf(result.status).toEqualTypeOf<'pending'>()
}
if (result.isLoadingError) {
expectTypeOf(result.data).toEqualTypeOf<undefined>()
expectTypeOf(result.error).toEqualTypeOf<Error>()
expectTypeOf(result.status).toEqualTypeOf<'error'>()
}
if (result.isRefetchError) {
expectTypeOf(result.data).toEqualTypeOf<InfiniteData<string, unknown>>()
expectTypeOf(result.error).toEqualTypeOf<Error>()
expectTypeOf(result.status).toEqualTypeOf<'error'>()
expectTypeOf(result.isFetchNextPageError).toEqualTypeOf<boolean>()
expectTypeOf(result.isFetchPreviousPageError).toEqualTypeOf<boolean>()
}
if (result.isSuccess) {
expectTypeOf(result.data).toEqualTypeOf<InfiniteData<string, unknown>>()
expectTypeOf(result.error).toEqualTypeOf<null>()
expectTypeOf(result.status).toEqualTypeOf<'success'>()
}
})
})

View File

@@ -0,0 +1,198 @@
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
import { InfiniteQueryObserver } from '..'
import { createQueryClient, queryKey, sleep } from './utils'
import type { QueryClient } from '..'
describe('InfiniteQueryObserver', () => {
let queryClient: QueryClient
beforeEach(() => {
queryClient = createQueryClient()
queryClient.mount()
})
afterEach(() => {
queryClient.clear()
})
test('InfiniteQueryObserver should be able to fetch an infinite query with selector', async () => {
const key = queryKey()
const observer = new InfiniteQueryObserver(queryClient, {
queryKey: key,
queryFn: () => 1,
select: (data) => ({
pages: data.pages.map((x) => `${x}`),
pageParams: data.pageParams,
}),
initialPageParam: 1,
getNextPageParam: () => 2,
})
let observerResult
const unsubscribe = observer.subscribe((result) => {
observerResult = result
})
await sleep(1)
unsubscribe()
expect(observerResult).toMatchObject({
data: { pages: ['1'], pageParams: [1] },
})
})
test('InfiniteQueryObserver should pass the meta option to the queryFn', async () => {
const meta = {
it: 'works',
}
const key = queryKey()
const queryFn = vi.fn(() => 1)
const observer = new InfiniteQueryObserver(queryClient, {
meta,
queryKey: key,
queryFn,
select: (data) => ({
pages: data.pages.map((x) => `${x}`),
pageParams: data.pageParams,
}),
initialPageParam: 1,
getNextPageParam: () => 2,
})
let observerResult
const unsubscribe = observer.subscribe((result) => {
observerResult = result
})
await sleep(1)
unsubscribe()
expect(observerResult).toMatchObject({
data: { pages: ['1'], pageParams: [1] },
})
expect(queryFn).toBeCalledWith(expect.objectContaining({ meta }))
})
test('getNextPagParam and getPreviousPageParam should receive current pageParams', async () => {
const key = queryKey()
let single: Array<string> = []
let all: Array<string> = []
const observer = new InfiniteQueryObserver(queryClient, {
queryKey: key,
queryFn: ({ pageParam }) => String(pageParam),
initialPageParam: 1,
getNextPageParam: (_, __, lastPageParam, allPageParams) => {
single.push('next' + lastPageParam)
all.push('next' + allPageParams.join(','))
return lastPageParam + 1
},
getPreviousPageParam: (_, __, firstPageParam, allPageParams) => {
single.push('prev' + firstPageParam)
all.push('prev' + allPageParams.join(','))
return firstPageParam - 1
},
})
await observer.fetchNextPage()
await observer.fetchPreviousPage()
expect(single).toEqual(['next1', 'prev1', 'prev1', 'next1', 'prev0'])
expect(all).toEqual(['next1', 'prev1', 'prev1', 'next0,1', 'prev0,1'])
single = []
all = []
await observer.refetch()
expect(single).toEqual(['next0', 'next1', 'prev0'])
expect(all).toEqual(['next0', 'next0,1', 'prev0,1'])
})
test('should not invoke getNextPageParam and getPreviousPageParam on empty pages', async () => {
const key = queryKey()
const getNextPageParam = vi.fn()
const getPreviousPageParam = vi.fn()
const observer = new InfiniteQueryObserver(queryClient, {
queryKey: key,
queryFn: ({ pageParam }) => String(pageParam),
initialPageParam: 1,
getNextPageParam: getNextPageParam.mockImplementation(
(_, __, lastPageParam) => {
return lastPageParam + 1
},
),
getPreviousPageParam: getPreviousPageParam.mockImplementation(
(_, __, firstPageParam) => {
return firstPageParam - 1
},
),
})
const unsubscribe = observer.subscribe(() => {})
getNextPageParam.mockClear()
getPreviousPageParam.mockClear()
queryClient.setQueryData(key, { pages: [], pageParams: [] })
expect(getNextPageParam).toHaveBeenCalledTimes(0)
expect(getPreviousPageParam).toHaveBeenCalledTimes(0)
unsubscribe()
})
test('should stop refetching if undefined is returned from getNextPageParam', async () => {
const key = queryKey()
let next: number | undefined = 2
const queryFn = vi.fn<(...args: Array<any>) => any>(({ pageParam }) =>
String(pageParam),
)
const observer = new InfiniteQueryObserver(queryClient, {
queryKey: key,
queryFn,
initialPageParam: 1,
getNextPageParam: () => next,
})
await observer.fetchNextPage()
await observer.fetchNextPage()
expect(observer.getCurrentResult().data?.pages).toEqual(['1', '2'])
expect(queryFn).toBeCalledTimes(2)
expect(observer.getCurrentResult().hasNextPage).toBe(true)
next = undefined
await observer.refetch()
expect(observer.getCurrentResult().data?.pages).toEqual(['1'])
expect(queryFn).toBeCalledTimes(3)
expect(observer.getCurrentResult().hasNextPage).toBe(false)
})
test('should stop refetching if null is returned from getNextPageParam', async () => {
const key = queryKey()
let next: number | null = 2
const queryFn = vi.fn<(...args: Array<any>) => any>(({ pageParam }) =>
String(pageParam),
)
const observer = new InfiniteQueryObserver(queryClient, {
queryKey: key,
queryFn,
initialPageParam: 1,
getNextPageParam: () => next,
})
await observer.fetchNextPage()
await observer.fetchNextPage()
expect(observer.getCurrentResult().data?.pages).toEqual(['1', '2'])
expect(queryFn).toBeCalledTimes(2)
expect(observer.getCurrentResult().hasNextPage).toBe(true)
next = null
await observer.refetch()
expect(observer.getCurrentResult().data?.pages).toEqual(['1'])
expect(queryFn).toBeCalledTimes(3)
expect(observer.getCurrentResult().hasNextPage).toBe(false)
})
})

View File

@@ -0,0 +1,376 @@
import { describe, expect, test, vi } from 'vitest'
import { waitFor } from '@testing-library/react'
import { MutationCache, MutationObserver } from '..'
import { createQueryClient, executeMutation, queryKey, sleep } from './utils'
describe('mutationCache', () => {
describe('MutationCacheConfig error callbacks', () => {
test('should call onError and onSettled when a mutation errors', async () => {
const key = queryKey()
const onError = vi.fn()
const onSuccess = vi.fn()
const onSettled = vi.fn()
const testCache = new MutationCache({ onError, onSuccess, onSettled })
const testClient = createQueryClient({ mutationCache: testCache })
try {
await executeMutation(
testClient,
{
mutationKey: key,
mutationFn: () => Promise.reject(new Error('error')),
onMutate: () => 'context',
},
'vars',
)
} catch {}
const mutation = testCache.getAll()[0]
expect(onError).toHaveBeenCalledTimes(1)
expect(onError).toHaveBeenCalledWith(
new Error('error'),
'vars',
'context',
mutation,
)
expect(onSuccess).not.toHaveBeenCalled()
expect(onSettled).toHaveBeenCalledTimes(1)
expect(onSettled).toHaveBeenCalledWith(
undefined,
new Error('error'),
'vars',
'context',
mutation,
)
})
test('should be awaited', async () => {
const key = queryKey()
const states: Array<number> = []
const onError = async () => {
states.push(1)
await sleep(1)
states.push(2)
}
const onSettled = async () => {
states.push(5)
await sleep(1)
states.push(6)
}
const testCache = new MutationCache({ onError, onSettled })
const testClient = createQueryClient({ mutationCache: testCache })
try {
await executeMutation(
testClient,
{
mutationKey: key,
mutationFn: () => Promise.reject(new Error('error')),
onError: async () => {
states.push(3)
await sleep(1)
states.push(4)
},
onSettled: async () => {
states.push(7)
await sleep(1)
states.push(8)
},
},
'vars',
)
} catch {}
expect(states).toEqual([1, 2, 3, 4, 5, 6, 7, 8])
})
})
describe('MutationCacheConfig success callbacks', () => {
test('should call onSuccess and onSettled when a mutation is successful', async () => {
const key = queryKey()
const onError = vi.fn()
const onSuccess = vi.fn()
const onSettled = vi.fn()
const testCache = new MutationCache({ onError, onSuccess, onSettled })
const testClient = createQueryClient({ mutationCache: testCache })
try {
await executeMutation(
testClient,
{
mutationKey: key,
mutationFn: () => Promise.resolve({ data: 5 }),
onMutate: () => 'context',
},
'vars',
)
} catch {}
const mutation = testCache.getAll()[0]
expect(onSuccess).toHaveBeenCalledTimes(1)
expect(onSuccess).toHaveBeenCalledWith(
{ data: 5 },
'vars',
'context',
mutation,
)
expect(onError).not.toHaveBeenCalled()
expect(onSettled).toHaveBeenCalledTimes(1)
expect(onSettled).toHaveBeenCalledWith(
{ data: 5 },
null,
'vars',
'context',
mutation,
)
})
test('should be awaited', async () => {
const key = queryKey()
const states: Array<number> = []
const onSuccess = async () => {
states.push(1)
await sleep(1)
states.push(2)
}
const onSettled = async () => {
states.push(5)
await sleep(1)
states.push(6)
}
const testCache = new MutationCache({ onSuccess, onSettled })
const testClient = createQueryClient({ mutationCache: testCache })
await executeMutation(
testClient,
{
mutationKey: key,
mutationFn: () => Promise.resolve({ data: 5 }),
onSuccess: async () => {
states.push(3)
await sleep(1)
states.push(4)
},
onSettled: async () => {
states.push(7)
await sleep(1)
states.push(8)
},
},
'vars',
)
expect(states).toEqual([1, 2, 3, 4, 5, 6, 7, 8])
})
})
describe('MutationCacheConfig.onMutate', () => {
test('should be called before a mutation executes', async () => {
const key = queryKey()
const onMutate = vi.fn()
const testCache = new MutationCache({ onMutate })
const testClient = createQueryClient({ mutationCache: testCache })
try {
await executeMutation(
testClient,
{
mutationKey: key,
mutationFn: () => Promise.resolve({ data: 5 }),
onMutate: () => 'context',
},
'vars',
)
} catch {}
const mutation = testCache.getAll()[0]
expect(onMutate).toHaveBeenCalledWith('vars', mutation)
})
test('should be awaited', async () => {
const key = queryKey()
const states: Array<number> = []
const onMutate = async () => {
states.push(1)
await sleep(1)
states.push(2)
}
const testCache = new MutationCache({ onMutate })
const testClient = createQueryClient({ mutationCache: testCache })
await executeMutation(
testClient,
{
mutationKey: key,
mutationFn: () => Promise.resolve({ data: 5 }),
onMutate: async () => {
states.push(3)
await sleep(1)
states.push(4)
},
},
'vars',
)
expect(states).toEqual([1, 2, 3, 4])
})
})
describe('find', () => {
test('should filter correctly', async () => {
const testCache = new MutationCache()
const testClient = createQueryClient({ mutationCache: testCache })
const key = ['mutation', 'vars']
await executeMutation(
testClient,
{
mutationKey: key,
mutationFn: () => Promise.resolve(),
},
'vars',
)
const [mutation] = testCache.getAll()
expect(testCache.find({ mutationKey: key })).toEqual(mutation)
expect(
testCache.find({ mutationKey: ['mutation'], exact: false }),
).toEqual(mutation)
expect(testCache.find({ mutationKey: ['unknown'] })).toEqual(undefined)
expect(
testCache.find({
predicate: (m) => m.options.mutationKey?.[0] === key[0],
}),
).toEqual(mutation)
})
})
describe('findAll', () => {
test('should filter correctly', async () => {
const testCache = new MutationCache()
const testClient = createQueryClient({ mutationCache: testCache })
await executeMutation(
testClient,
{
mutationKey: ['a', 1],
mutationFn: () => Promise.resolve(),
},
1,
)
await executeMutation(
testClient,
{
mutationKey: ['a', 2],
mutationFn: () => Promise.resolve(),
},
2,
)
await executeMutation(
testClient,
{
mutationKey: ['b'],
mutationFn: () => Promise.resolve(),
},
3,
)
const [mutation1, mutation2] = testCache.getAll()
expect(
testCache.findAll({ mutationKey: ['a'], exact: false }),
).toHaveLength(2)
expect(testCache.find({ mutationKey: ['a', 1] })).toEqual(mutation1)
expect(
testCache.findAll({
predicate: (m) => m.options.mutationKey?.[1] === 2,
}),
).toEqual([mutation2])
expect(testCache.findAll({ mutationKey: ['unknown'] })).toEqual([])
})
})
describe('garbage collection', () => {
test('should remove unused mutations after gcTime has elapsed', async () => {
const testCache = new MutationCache()
const testClient = createQueryClient({ mutationCache: testCache })
const onSuccess = vi.fn()
await executeMutation(
testClient,
{
mutationKey: ['a', 1],
gcTime: 10,
mutationFn: () => Promise.resolve(),
onSuccess,
},
1,
)
expect(testCache.getAll()).toHaveLength(1)
await sleep(10)
await waitFor(() => {
expect(testCache.getAll()).toHaveLength(0)
})
expect(onSuccess).toHaveBeenCalledTimes(1)
})
test('should not remove mutations if there are active observers', async () => {
const queryClient = createQueryClient()
const observer = new MutationObserver(queryClient, {
gcTime: 10,
mutationFn: (input: number) => Promise.resolve(input),
})
const unsubscribe = observer.subscribe(() => undefined)
expect(queryClient.getMutationCache().getAll()).toHaveLength(0)
observer.mutate(1)
expect(queryClient.getMutationCache().getAll()).toHaveLength(1)
await sleep(10)
expect(queryClient.getMutationCache().getAll()).toHaveLength(1)
unsubscribe()
expect(queryClient.getMutationCache().getAll()).toHaveLength(1)
await sleep(10)
await waitFor(() => {
expect(queryClient.getMutationCache().getAll()).toHaveLength(0)
})
})
test('should be garbage collected later when unsubscribed and mutation is pending', async () => {
const queryClient = createQueryClient()
const onSuccess = vi.fn()
const observer = new MutationObserver(queryClient, {
gcTime: 10,
mutationFn: async () => {
await sleep(20)
return 'data'
},
onSuccess,
})
const unsubscribe = observer.subscribe(() => undefined)
observer.mutate(1)
unsubscribe()
expect(queryClient.getMutationCache().getAll()).toHaveLength(1)
await sleep(10)
// unsubscribe should not remove even though gcTime has elapsed b/c mutation is still pending
expect(queryClient.getMutationCache().getAll()).toHaveLength(1)
await sleep(10)
// should be removed after an additional gcTime wait
await waitFor(() => {
expect(queryClient.getMutationCache().getAll()).toHaveLength(0)
})
expect(onSuccess).toHaveBeenCalledTimes(1)
})
test('should call callbacks even with gcTime 0 and mutation still pending', async () => {
const queryClient = createQueryClient()
const onSuccess = vi.fn()
const observer = new MutationObserver(queryClient, {
gcTime: 0,
mutationFn: async () => {
return 'data'
},
onSuccess,
})
const unsubscribe = observer.subscribe(() => undefined)
observer.mutate(1)
unsubscribe()
await waitFor(() => {
expect(queryClient.getMutationCache().getAll()).toHaveLength(0)
})
expect(onSuccess).toHaveBeenCalledTimes(1)
})
})
})

View File

@@ -0,0 +1,326 @@
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
import { waitFor } from '@testing-library/react'
import { MutationObserver } from '..'
import { createQueryClient, queryKey, sleep } from './utils'
import type { QueryClient } from '..'
describe('mutationObserver', () => {
let queryClient: QueryClient
beforeEach(() => {
queryClient = createQueryClient()
queryClient.mount()
})
afterEach(() => {
queryClient.clear()
})
test('onUnsubscribe should not remove the current mutation observer if there is still a subscription', async () => {
const mutation = new MutationObserver(queryClient, {
mutationFn: async (text: string) => {
await sleep(20)
return text
},
})
const subscription1Handler = vi.fn()
const subscription2Handler = vi.fn()
const unsubscribe1 = mutation.subscribe(subscription1Handler)
const unsubscribe2 = mutation.subscribe(subscription2Handler)
mutation.mutate('input')
unsubscribe1()
await waitFor(() => {
// 1 call: loading
expect(subscription1Handler).toBeCalledTimes(1)
// 2 calls: loading, success
expect(subscription2Handler).toBeCalledTimes(2)
})
// Clean-up
unsubscribe2()
})
test('unsubscribe should remove observer to trigger GC', async () => {
const mutation = new MutationObserver(queryClient, {
mutationFn: async (text: string) => {
await sleep(5)
return text
},
gcTime: 10,
})
const subscriptionHandler = vi.fn()
const unsubscribe = mutation.subscribe(subscriptionHandler)
await mutation.mutate('input')
expect(queryClient.getMutationCache().findAll()).toHaveLength(1)
unsubscribe()
await waitFor(() =>
expect(queryClient.getMutationCache().findAll()).toHaveLength(0),
)
})
test('reset should remove observer to trigger GC', async () => {
const mutation = new MutationObserver(queryClient, {
mutationFn: async (text: string) => {
await sleep(5)
return text
},
gcTime: 10,
})
const subscriptionHandler = vi.fn()
const unsubscribe = mutation.subscribe(subscriptionHandler)
await mutation.mutate('input')
expect(queryClient.getMutationCache().findAll()).toHaveLength(1)
mutation.reset()
await waitFor(() =>
expect(queryClient.getMutationCache().findAll()).toHaveLength(0),
)
unsubscribe()
})
test('changing mutation keys should reset the observer', async () => {
const key = queryKey()
const mutation = new MutationObserver(queryClient, {
mutationKey: [...key, '1'],
mutationFn: async (text: string) => {
await sleep(5)
return text
},
})
const subscriptionHandler = vi.fn()
const unsubscribe = mutation.subscribe(subscriptionHandler)
await mutation.mutate('input')
expect(mutation.getCurrentResult()).toMatchObject({
status: 'success',
data: 'input',
})
mutation.setOptions({
mutationKey: [...key, '2'],
})
expect(mutation.getCurrentResult()).toMatchObject({
status: 'idle',
})
unsubscribe()
})
test('changing mutation keys should not affect already existing mutations', async () => {
const key = queryKey()
const mutationObserver = new MutationObserver(queryClient, {
mutationKey: [...key, '1'],
mutationFn: async (text: string) => {
await sleep(5)
return text
},
})
const subscriptionHandler = vi.fn()
const unsubscribe = mutationObserver.subscribe(subscriptionHandler)
await mutationObserver.mutate('input')
expect(
queryClient.getMutationCache().find({ mutationKey: [...key, '1'] }),
).toMatchObject({
options: { mutationKey: [...key, '1'] },
state: {
status: 'success',
data: 'input',
},
})
mutationObserver.setOptions({
mutationKey: [...key, '2'],
})
expect(
queryClient.getMutationCache().find({ mutationKey: [...key, '1'] }),
).toMatchObject({
options: { mutationKey: [...key, '1'] },
state: {
status: 'success',
data: 'input',
},
})
unsubscribe()
})
test('changing mutation meta should not affect successful mutations', async () => {
const mutationObserver = new MutationObserver(queryClient, {
meta: { a: 1 },
mutationFn: async (text: string) => {
await sleep(5)
return text
},
})
const subscriptionHandler = vi.fn()
const unsubscribe = mutationObserver.subscribe(subscriptionHandler)
await mutationObserver.mutate('input')
expect(queryClient.getMutationCache().find({})).toMatchObject({
options: { meta: { a: 1 } },
state: {
status: 'success',
data: 'input',
},
})
mutationObserver.setOptions({
meta: { a: 2 },
})
expect(queryClient.getMutationCache().find({})).toMatchObject({
options: { meta: { a: 1 } },
state: {
status: 'success',
data: 'input',
},
})
unsubscribe()
})
test('mutation cache should have different meta when updated between mutations', async () => {
const mutationFn = async (text: string) => {
await sleep(5)
return text
}
const mutationObserver = new MutationObserver(queryClient, {
meta: { a: 1 },
mutationFn,
})
const subscriptionHandler = vi.fn()
const unsubscribe = mutationObserver.subscribe(subscriptionHandler)
await mutationObserver.mutate('input')
mutationObserver.setOptions({
meta: { a: 2 },
mutationFn,
})
await mutationObserver.mutate('input')
const mutations = queryClient.getMutationCache().findAll()
expect(mutations[0]).toMatchObject({
options: { meta: { a: 1 } },
state: {
status: 'success',
data: 'input',
},
})
expect(mutations[1]).toMatchObject({
options: { meta: { a: 2 } },
state: {
status: 'success',
data: 'input',
},
})
unsubscribe()
})
test('changing mutation meta should not affect rejected mutations', async () => {
const mutationObserver = new MutationObserver(queryClient, {
meta: { a: 1 },
mutationFn: async (_: string) => {
await sleep(5)
return Promise.reject(new Error('err'))
},
})
const subscriptionHandler = vi.fn()
const unsubscribe = mutationObserver.subscribe(subscriptionHandler)
await mutationObserver.mutate('input').catch(() => undefined)
expect(queryClient.getMutationCache().find({})).toMatchObject({
options: { meta: { a: 1 } },
state: {
status: 'error',
},
})
mutationObserver.setOptions({
meta: { a: 2 },
})
expect(queryClient.getMutationCache().find({})).toMatchObject({
options: { meta: { a: 1 } },
state: {
status: 'error',
},
})
unsubscribe()
})
test('changing mutation meta should affect pending mutations', async () => {
const mutationObserver = new MutationObserver(queryClient, {
meta: { a: 1 },
mutationFn: async (text: string) => {
await sleep(20)
return text
},
})
const subscriptionHandler = vi.fn()
const unsubscribe = mutationObserver.subscribe(subscriptionHandler)
mutationObserver.mutate('input')
await sleep(0)
expect(queryClient.getMutationCache().find({})).toMatchObject({
options: { meta: { a: 1 } },
state: {
status: 'pending',
},
})
mutationObserver.setOptions({
meta: { a: 2 },
})
expect(queryClient.getMutationCache().find({})).toMatchObject({
options: { meta: { a: 2 } },
state: {
status: 'pending',
},
})
unsubscribe()
})
})

View File

@@ -0,0 +1,603 @@
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
import { waitFor } from '@testing-library/react'
import { MutationObserver } from '../mutationObserver'
import { createQueryClient, executeMutation, queryKey, sleep } from './utils'
import type { QueryClient } from '..'
import type { MutationState } from '../mutation'
describe('mutations', () => {
let queryClient: QueryClient
beforeEach(() => {
queryClient = createQueryClient()
queryClient.mount()
})
afterEach(() => {
queryClient.clear()
})
test('mutate should accept null values', async () => {
let variables
const mutation = new MutationObserver(queryClient, {
mutationFn: async (vars: unknown) => {
variables = vars
return vars
},
})
await mutation.mutate(null)
expect(variables).toBe(null)
})
test('setMutationDefaults should be able to set defaults', async () => {
const key = queryKey()
const fn = vi.fn()
queryClient.setMutationDefaults(key, {
mutationFn: fn,
})
await executeMutation(
queryClient,
{
mutationKey: key,
},
'vars',
)
expect(fn).toHaveBeenCalledTimes(1)
expect(fn).toHaveBeenCalledWith('vars')
})
test('mutation should set correct success states', async () => {
const mutation = new MutationObserver(queryClient, {
mutationFn: async (text: string) => {
await sleep(10)
return text
},
onMutate: (text) => text,
})
expect(mutation.getCurrentResult()).toEqual({
context: undefined,
data: undefined,
error: null,
failureCount: 0,
failureReason: null,
isError: false,
isIdle: true,
isPending: false,
isPaused: false,
isSuccess: false,
mutate: expect.any(Function),
reset: expect.any(Function),
status: 'idle',
variables: undefined,
submittedAt: 0,
})
const states: Array<MutationState<string, unknown, string, string>> = []
mutation.subscribe((state) => {
states.push(state)
})
mutation.mutate('todo')
await sleep(0)
expect(states[0]).toEqual({
context: undefined,
data: undefined,
error: null,
failureCount: 0,
failureReason: null,
isError: false,
isIdle: false,
isPending: true,
isPaused: false,
isSuccess: false,
mutate: expect.any(Function),
reset: expect.any(Function),
status: 'pending',
variables: 'todo',
submittedAt: expect.any(Number),
})
await sleep(5)
expect(states[1]).toEqual({
context: 'todo',
data: undefined,
error: null,
failureCount: 0,
failureReason: null,
isError: false,
isIdle: false,
isPending: true,
isPaused: false,
isSuccess: false,
mutate: expect.any(Function),
reset: expect.any(Function),
status: 'pending',
variables: 'todo',
submittedAt: expect.any(Number),
})
await sleep(20)
expect(states[2]).toEqual({
context: 'todo',
data: 'todo',
error: null,
failureCount: 0,
failureReason: null,
isError: false,
isIdle: false,
isPending: false,
isPaused: false,
isSuccess: true,
mutate: expect.any(Function),
reset: expect.any(Function),
status: 'success',
variables: 'todo',
submittedAt: expect.any(Number),
})
})
test('mutation should set correct error states', async () => {
const mutation = new MutationObserver(queryClient, {
mutationFn: async (_: string) => {
await sleep(20)
return Promise.reject(new Error('err'))
},
onMutate: (text) => text,
retry: 1,
retryDelay: 1,
})
const states: Array<MutationState<string, unknown, string, string>> = []
mutation.subscribe((state) => {
states.push(state)
})
mutation.mutate('todo').catch(() => undefined)
await sleep(0)
expect(states[0]).toEqual({
context: undefined,
data: undefined,
error: null,
failureCount: 0,
failureReason: null,
isError: false,
isIdle: false,
isPending: true,
isPaused: false,
isSuccess: false,
mutate: expect.any(Function),
reset: expect.any(Function),
status: 'pending',
variables: 'todo',
submittedAt: expect.any(Number),
})
await sleep(10)
expect(states[1]).toEqual({
context: 'todo',
data: undefined,
error: null,
failureCount: 0,
failureReason: null,
isError: false,
isIdle: false,
isPending: true,
isPaused: false,
isSuccess: false,
mutate: expect.any(Function),
reset: expect.any(Function),
status: 'pending',
variables: 'todo',
submittedAt: expect.any(Number),
})
await sleep(20)
expect(states[2]).toEqual({
context: 'todo',
data: undefined,
error: null,
failureCount: 1,
failureReason: new Error('err'),
isError: false,
isIdle: false,
isPending: true,
isPaused: false,
isSuccess: false,
mutate: expect.any(Function),
reset: expect.any(Function),
status: 'pending',
variables: 'todo',
submittedAt: expect.any(Number),
})
await sleep(30)
expect(states[3]).toEqual({
context: 'todo',
data: undefined,
error: new Error('err'),
failureCount: 2,
failureReason: new Error('err'),
isError: true,
isIdle: false,
isPending: false,
isPaused: false,
isSuccess: false,
mutate: expect.any(Function),
reset: expect.any(Function),
status: 'error',
variables: 'todo',
submittedAt: expect.any(Number),
})
})
test('should be able to restore a mutation', async () => {
const key = queryKey()
const onMutate = vi.fn()
const onSuccess = vi.fn()
const onSettled = vi.fn()
queryClient.setMutationDefaults(key, {
mutationFn: async (text: string) => text,
onMutate,
onSuccess,
onSettled,
})
const mutation = queryClient
.getMutationCache()
.build<string, unknown, string, string>(
queryClient,
{
mutationKey: key,
},
{
context: 'todo',
data: undefined,
error: null,
failureCount: 1,
failureReason: 'err',
isPaused: true,
status: 'pending',
variables: 'todo',
submittedAt: 1,
},
)
expect(mutation.state).toEqual({
context: 'todo',
data: undefined,
error: null,
failureCount: 1,
failureReason: 'err',
isPaused: true,
status: 'pending',
variables: 'todo',
submittedAt: 1,
})
await queryClient.resumePausedMutations()
expect(mutation.state).toEqual({
context: 'todo',
data: 'todo',
error: null,
failureCount: 0,
failureReason: null,
isPaused: false,
status: 'success',
variables: 'todo',
submittedAt: 1,
})
expect(onMutate).not.toHaveBeenCalled()
expect(onSuccess).toHaveBeenCalled()
expect(onSettled).toHaveBeenCalled()
})
test('addObserver should not add an existing observer', async () => {
const mutationCache = queryClient.getMutationCache()
const observer = new MutationObserver(queryClient, {})
const currentMutation = mutationCache.build(queryClient, {})
const fn = vi.fn()
const unsubscribe = mutationCache.subscribe((event) => {
fn(event.type)
})
currentMutation.addObserver(observer)
currentMutation.addObserver(observer)
expect(fn).toHaveBeenCalledTimes(1)
expect(fn).toHaveBeenCalledWith('observerAdded')
unsubscribe()
})
test('mutate should throw an error if no mutationFn found', async () => {
const mutation = new MutationObserver(queryClient, {
mutationFn: undefined,
retry: false,
})
let error: any
try {
await mutation.mutate()
} catch (err) {
error = err
}
expect(error).toEqual(new Error('No mutationFn found'))
})
test('mutate update the mutation state even without an active subscription 1', async () => {
const onSuccess = vi.fn()
const onSettled = vi.fn()
const mutation = new MutationObserver(queryClient, {
mutationFn: async () => {
return 'update'
},
})
await mutation.mutate(undefined, { onSuccess, onSettled })
expect(mutation.getCurrentResult().data).toEqual('update')
expect(onSuccess).not.toHaveBeenCalled()
expect(onSettled).not.toHaveBeenCalled()
})
test('mutate update the mutation state even without an active subscription 2', async () => {
const onSuccess = vi.fn()
const onSettled = vi.fn()
const mutation = new MutationObserver(queryClient, {
mutationFn: async () => {
return 'update'
},
})
await mutation.mutate(undefined, { onSuccess, onSettled })
expect(mutation.getCurrentResult().data).toEqual('update')
expect(onSuccess).not.toHaveBeenCalled()
expect(onSettled).not.toHaveBeenCalled()
})
test('mutation callbacks should see updated options', async () => {
const onSuccess = vi.fn()
const mutation = new MutationObserver(queryClient, {
mutationFn: async () => {
sleep(100)
return 'update'
},
onSuccess: () => {
onSuccess(1)
},
})
void mutation.mutate()
mutation.setOptions({
mutationFn: async () => {
sleep(100)
return 'update'
},
onSuccess: () => {
onSuccess(2)
},
})
await waitFor(() => expect(onSuccess).toHaveBeenCalledTimes(1))
expect(onSuccess).toHaveBeenCalledWith(2)
})
describe('scoped mutations', () => {
test('mutations in the same scope should run in serial', async () => {
const key1 = queryKey()
const key2 = queryKey()
const results: Array<string> = []
const execute1 = executeMutation(
queryClient,
{
mutationKey: key1,
scope: {
id: 'scope',
},
mutationFn: async () => {
results.push('start-A')
await sleep(10)
results.push('finish-A')
return 'a'
},
},
'vars1',
)
expect(
queryClient.getMutationCache().find({ mutationKey: key1 })?.state,
).toMatchObject({
status: 'pending',
isPaused: false,
})
const execute2 = executeMutation(
queryClient,
{
mutationKey: key2,
scope: {
id: 'scope',
},
mutationFn: async () => {
results.push('start-B')
await sleep(10)
results.push('finish-B')
return 'b'
},
},
'vars2',
)
expect(
queryClient.getMutationCache().find({ mutationKey: key2 })?.state,
).toMatchObject({
status: 'pending',
isPaused: true,
})
await Promise.all([execute1, execute2])
expect(results).toStrictEqual([
'start-A',
'finish-A',
'start-B',
'finish-B',
])
})
})
test('mutations without scope should run in parallel', async () => {
const key1 = queryKey()
const key2 = queryKey()
const results: Array<string> = []
const execute1 = executeMutation(
queryClient,
{
mutationKey: key1,
mutationFn: async () => {
results.push('start-A')
await sleep(10)
results.push('finish-A')
return 'a'
},
},
'vars1',
)
const execute2 = executeMutation(
queryClient,
{
mutationKey: key2,
mutationFn: async () => {
results.push('start-B')
await sleep(10)
results.push('finish-B')
return 'b'
},
},
'vars2',
)
await Promise.all([execute1, execute2])
expect(results).toStrictEqual([
'start-A',
'start-B',
'finish-A',
'finish-B',
])
})
test('each scope should run should run in parallel, serial within scope', async () => {
const results: Array<string> = []
const execute1 = executeMutation(
queryClient,
{
scope: {
id: '1',
},
mutationFn: async () => {
results.push('start-A1')
await sleep(10)
results.push('finish-A1')
return 'a'
},
},
'vars1',
)
const execute2 = executeMutation(
queryClient,
{
scope: {
id: '1',
},
mutationFn: async () => {
results.push('start-B1')
await sleep(10)
results.push('finish-B1')
return 'b'
},
},
'vars2',
)
const execute3 = executeMutation(
queryClient,
{
scope: {
id: '2',
},
mutationFn: async () => {
results.push('start-A2')
await sleep(10)
results.push('finish-A2')
return 'a'
},
},
'vars1',
)
const execute4 = executeMutation(
queryClient,
{
scope: {
id: '2',
},
mutationFn: async () => {
results.push('start-B2')
await sleep(10)
results.push('finish-B2')
return 'b'
},
},
'vars2',
)
await Promise.all([execute1, execute2, execute3, execute4])
expect(results).toStrictEqual([
'start-A1',
'start-A2',
'finish-A1',
'start-B1',
'finish-A2',
'start-B2',
'finish-B1',
'finish-B2',
])
})
})

View File

@@ -0,0 +1,85 @@
import { describe, expect, it, vi } from 'vitest'
import { createNotifyManager } from '../notifyManager'
import { sleep } from './utils'
describe('notifyManager', () => {
it('should use default notifyFn', async () => {
const notifyManagerTest = createNotifyManager()
const callbackSpy = vi.fn()
notifyManagerTest.schedule(callbackSpy)
await sleep(1)
expect(callbackSpy).toHaveBeenCalled()
})
it('should use default batchNotifyFn', async () => {
const notifyManagerTest = createNotifyManager()
const callbackScheduleSpy = vi
.fn()
.mockImplementation(async () => await sleep(20))
const callbackBatchLevel2Spy = vi.fn().mockImplementation(async () => {
notifyManagerTest.schedule(callbackScheduleSpy)
})
const callbackBatchLevel1Spy = vi.fn().mockImplementation(async () => {
notifyManagerTest.batch(callbackBatchLevel2Spy)
})
notifyManagerTest.batch(callbackBatchLevel1Spy)
await sleep(30)
expect(callbackBatchLevel1Spy).toHaveBeenCalledTimes(1)
expect(callbackBatchLevel2Spy).toHaveBeenCalledTimes(1)
expect(callbackScheduleSpy).toHaveBeenCalledTimes(1)
})
it('should use a custom scheduler when configured', async () => {
const customCallback = vi.fn((cb) => queueMicrotask(cb))
const notifyManagerTest = createNotifyManager()
const notifySpy = vi.fn()
notifyManagerTest.setScheduler(customCallback)
notifyManagerTest.setNotifyFunction(notifySpy)
notifyManagerTest.batch(() => notifyManagerTest.schedule(vi.fn))
expect(customCallback).toHaveBeenCalledOnce()
// wait until the microtask has run
await new Promise<void>((res) => queueMicrotask(res))
expect(notifySpy).toHaveBeenCalledTimes(1)
})
it('should notify if error is thrown', async () => {
const notifyManagerTest = createNotifyManager()
const notifySpy = vi.fn()
notifyManagerTest.setNotifyFunction(notifySpy)
try {
notifyManagerTest.batch(() => {
notifyManagerTest.schedule(vi.fn)
throw new Error('Foo')
})
} catch {}
// needed for setTimeout to kick in
await sleep(1)
expect(notifySpy).toHaveBeenCalledTimes(1)
})
it('typeDefs should catch proper signatures', async () => {
const notifyManagerTest = createNotifyManager()
// we define some fn with its signature:
const fn: (a: string, b: number) => string = (a, b) => a + b
// now someFn expect to be called with args [a: string, b: number]
const someFn = notifyManagerTest.batchCalls(fn)
someFn('im happy', 4)
// @ts-expect-error
someFn('im not happy', false)
})
})

View File

@@ -0,0 +1,168 @@
import { beforeEach, describe, expect, test, vi } from 'vitest'
import { OnlineManager } from '../onlineManager'
import { setIsServer, sleep } from './utils'
describe('onlineManager', () => {
let onlineManager: OnlineManager
beforeEach(() => {
onlineManager = new OnlineManager()
})
test('isOnline should return true if navigator is undefined', () => {
const navigatorSpy = vi.spyOn(globalThis, 'navigator', 'get')
// Force navigator to be undefined
// @ts-expect-error
navigatorSpy.mockImplementation(() => undefined)
expect(onlineManager.isOnline()).toBeTruthy()
navigatorSpy.mockRestore()
})
test('isOnline should return true if navigator.onLine is true', () => {
const navigatorSpy = vi.spyOn(navigator, 'onLine', 'get')
navigatorSpy.mockImplementation(() => true)
expect(onlineManager.isOnline()).toBeTruthy()
navigatorSpy.mockRestore()
})
test('setEventListener should use online boolean arg', async () => {
let count = 0
const setup = (setOnline: (online: boolean) => void) => {
setTimeout(() => {
count++
setOnline(false)
}, 20)
return () => void 0
}
onlineManager.setEventListener(setup)
await sleep(30)
expect(count).toEqual(1)
expect(onlineManager.isOnline()).toBeFalsy()
})
test('setEventListener should call previous remove handler when replacing an event listener', () => {
const remove1Spy = vi.fn()
const remove2Spy = vi.fn()
onlineManager.setEventListener(() => remove1Spy)
onlineManager.setEventListener(() => remove2Spy)
expect(remove1Spy).toHaveBeenCalledTimes(1)
expect(remove2Spy).not.toHaveBeenCalled()
})
test('cleanup (removeEventListener) should not be called if window is not defined', async () => {
const restoreIsServer = setIsServer(true)
const removeEventListenerSpy = vi.spyOn(globalThis, 'removeEventListener')
const unsubscribe = onlineManager.subscribe(() => undefined)
unsubscribe()
expect(removeEventListenerSpy).not.toHaveBeenCalled()
restoreIsServer()
})
test('cleanup (removeEventListener) should not be called if window.addEventListener is not defined', async () => {
const { addEventListener } = globalThis.window
// @ts-expect-error
globalThis.window.addEventListener = undefined
const removeEventListenerSpy = vi.spyOn(globalThis, 'removeEventListener')
const unsubscribe = onlineManager.subscribe(() => undefined)
unsubscribe()
expect(removeEventListenerSpy).not.toHaveBeenCalled()
globalThis.window.addEventListener = addEventListener
})
test('it should replace default window listener when a new event listener is set', async () => {
const addEventListenerSpy = vi.spyOn(globalThis.window, 'addEventListener')
const removeEventListenerSpy = vi.spyOn(
globalThis.window,
'removeEventListener',
)
// Should set the default event listener with window event listeners
const unsubscribe = onlineManager.subscribe(() => undefined)
expect(addEventListenerSpy).toHaveBeenCalledTimes(2)
// Should replace the window default event listener by a new one
// and it should call window.removeEventListener twice
onlineManager.setEventListener(() => {
return () => void 0
})
expect(removeEventListenerSpy).toHaveBeenCalledTimes(2)
unsubscribe()
addEventListenerSpy.mockRestore()
removeEventListenerSpy.mockRestore()
})
test('should call removeEventListener when last listener unsubscribes', () => {
const addEventListenerSpy = vi.spyOn(globalThis.window, 'addEventListener')
const removeEventListenerSpy = vi.spyOn(
globalThis.window,
'removeEventListener',
)
const unsubscribe1 = onlineManager.subscribe(() => undefined)
const unsubscribe2 = onlineManager.subscribe(() => undefined)
expect(addEventListenerSpy).toHaveBeenCalledTimes(2) // online + offline
unsubscribe1()
expect(removeEventListenerSpy).toHaveBeenCalledTimes(0)
unsubscribe2()
expect(removeEventListenerSpy).toHaveBeenCalledTimes(2) // online + offline
})
test('should keep setup function even if last listener unsubscribes', () => {
const setupSpy = vi.fn().mockImplementation(() => () => undefined)
onlineManager.setEventListener(setupSpy)
const unsubscribe1 = onlineManager.subscribe(() => undefined)
expect(setupSpy).toHaveBeenCalledTimes(1)
unsubscribe1()
const unsubscribe2 = onlineManager.subscribe(() => undefined)
expect(setupSpy).toHaveBeenCalledTimes(2)
unsubscribe2()
})
test('should call listeners when setOnline is called', () => {
const listener = vi.fn()
onlineManager.subscribe(listener)
onlineManager.setOnline(false)
onlineManager.setOnline(false)
expect(listener).toHaveBeenNthCalledWith(1, false)
onlineManager.setOnline(true)
onlineManager.setOnline(true)
expect(listener).toHaveBeenCalledTimes(2)
expect(listener).toHaveBeenNthCalledWith(2, true)
})
})

View File

@@ -0,0 +1,267 @@
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
import { waitFor } from '@testing-library/react'
import { QueriesObserver } from '..'
import { createQueryClient, queryKey, sleep } from './utils'
import type { QueryClient, QueryObserverResult } from '..'
describe('queriesObserver', () => {
let queryClient: QueryClient
beforeEach(() => {
queryClient = createQueryClient()
queryClient.mount()
})
afterEach(() => {
queryClient.clear()
})
test('should return an array with all query results', async () => {
const key1 = queryKey()
const key2 = queryKey()
const queryFn1 = vi.fn().mockReturnValue(1)
const queryFn2 = vi.fn().mockReturnValue(2)
const observer = new QueriesObserver(queryClient, [
{ queryKey: key1, queryFn: queryFn1 },
{ queryKey: key2, queryFn: queryFn2 },
])
let observerResult
const unsubscribe = observer.subscribe((result) => {
observerResult = result
})
await sleep(1)
unsubscribe()
expect(observerResult).toMatchObject([{ data: 1 }, { data: 2 }])
})
test('should update when a query updates', async () => {
const key1 = queryKey()
const key2 = queryKey()
const queryFn1 = vi.fn().mockReturnValue(1)
const queryFn2 = vi.fn().mockReturnValue(2)
const observer = new QueriesObserver(queryClient, [
{ queryKey: key1, queryFn: queryFn1 },
{ queryKey: key2, queryFn: queryFn2 },
])
const results: Array<Array<QueryObserverResult>> = []
results.push(observer.getCurrentResult())
const unsubscribe = observer.subscribe((result) => {
results.push(result)
})
await sleep(1)
queryClient.setQueryData(key2, 3)
await sleep(1)
unsubscribe()
expect(results.length).toBe(6)
expect(results[0]).toMatchObject([
{ status: 'pending', fetchStatus: 'idle', data: undefined },
{ status: 'pending', fetchStatus: 'idle', data: undefined },
])
expect(results[1]).toMatchObject([
{ status: 'pending', fetchStatus: 'fetching', data: undefined },
{ status: 'pending', fetchStatus: 'idle', data: undefined },
])
expect(results[2]).toMatchObject([
{ status: 'pending', fetchStatus: 'fetching', data: undefined },
{ status: 'pending', fetchStatus: 'fetching', data: undefined },
])
expect(results[3]).toMatchObject([
{ status: 'success', data: 1 },
{ status: 'pending', fetchStatus: 'fetching', data: undefined },
])
expect(results[4]).toMatchObject([
{ status: 'success', data: 1 },
{ status: 'success', data: 2 },
])
expect(results[5]).toMatchObject([
{ status: 'success', data: 1 },
{ status: 'success', data: 3 },
])
})
test('should update when a query is removed', async () => {
const key1 = queryKey()
const key2 = queryKey()
const queryFn1 = vi.fn().mockReturnValue(1)
const queryFn2 = vi.fn().mockReturnValue(2)
const observer = new QueriesObserver(queryClient, [
{ queryKey: key1, queryFn: queryFn1 },
{ queryKey: key2, queryFn: queryFn2 },
])
const results: Array<Array<QueryObserverResult>> = []
results.push(observer.getCurrentResult())
const unsubscribe = observer.subscribe((result) => {
results.push(result)
})
await sleep(1)
observer.setQueries([{ queryKey: key2, queryFn: queryFn2 }])
await sleep(1)
const queryCache = queryClient.getQueryCache()
expect(queryCache.find({ queryKey: key1, type: 'active' })).toBeUndefined()
expect(queryCache.find({ queryKey: key2, type: 'active' })).toBeDefined()
unsubscribe()
expect(queryCache.find({ queryKey: key1, type: 'active' })).toBeUndefined()
expect(queryCache.find({ queryKey: key2, type: 'active' })).toBeUndefined()
expect(results.length).toBe(6)
expect(results[0]).toMatchObject([
{ status: 'pending', fetchStatus: 'idle', data: undefined },
{ status: 'pending', fetchStatus: 'idle', data: undefined },
])
expect(results[1]).toMatchObject([
{ status: 'pending', fetchStatus: 'fetching', data: undefined },
{ status: 'pending', fetchStatus: 'idle', data: undefined },
])
expect(results[2]).toMatchObject([
{ status: 'pending', fetchStatus: 'fetching', data: undefined },
{ status: 'pending', fetchStatus: 'fetching', data: undefined },
])
expect(results[3]).toMatchObject([
{ status: 'success', data: 1 },
{ status: 'pending', fetchStatus: 'fetching', data: undefined },
])
expect(results[4]).toMatchObject([
{ status: 'success', data: 1 },
{ status: 'success', data: 2 },
])
expect(results[5]).toMatchObject([{ status: 'success', data: 2 }])
})
test('should update when a query changed position', async () => {
const key1 = queryKey()
const key2 = queryKey()
const queryFn1 = vi.fn().mockReturnValue(1)
const queryFn2 = vi.fn().mockReturnValue(2)
const observer = new QueriesObserver(queryClient, [
{ queryKey: key1, queryFn: queryFn1 },
{ queryKey: key2, queryFn: queryFn2 },
])
const results: Array<Array<QueryObserverResult>> = []
results.push(observer.getCurrentResult())
const unsubscribe = observer.subscribe((result) => {
results.push(result)
})
await sleep(1)
observer.setQueries([
{ queryKey: key2, queryFn: queryFn2 },
{ queryKey: key1, queryFn: queryFn1 },
])
await sleep(1)
unsubscribe()
expect(results.length).toBe(6)
expect(results[0]).toMatchObject([
{ status: 'pending', fetchStatus: 'idle', data: undefined },
{ status: 'pending', fetchStatus: 'idle', data: undefined },
])
expect(results[1]).toMatchObject([
{ status: 'pending', fetchStatus: 'fetching', data: undefined },
{ status: 'pending', fetchStatus: 'idle', data: undefined },
])
expect(results[2]).toMatchObject([
{ status: 'pending', fetchStatus: 'fetching', data: undefined },
{ status: 'pending', fetchStatus: 'fetching', data: undefined },
])
expect(results[3]).toMatchObject([
{ status: 'success', data: 1 },
{ status: 'pending', fetchStatus: 'fetching', data: undefined },
])
expect(results[4]).toMatchObject([
{ status: 'success', data: 1 },
{ status: 'success', data: 2 },
])
expect(results[5]).toMatchObject([
{ status: 'success', data: 2 },
{ status: 'success', data: 1 },
])
})
test('should not update when nothing has changed', async () => {
const key1 = queryKey()
const key2 = queryKey()
const queryFn1 = vi.fn().mockReturnValue(1)
const queryFn2 = vi.fn().mockReturnValue(2)
const observer = new QueriesObserver(queryClient, [
{ queryKey: key1, queryFn: queryFn1 },
{ queryKey: key2, queryFn: queryFn2 },
])
const results: Array<Array<QueryObserverResult>> = []
results.push(observer.getCurrentResult())
const unsubscribe = observer.subscribe((result) => {
results.push(result)
})
await sleep(1)
observer.setQueries([
{ queryKey: key1, queryFn: queryFn1 },
{ queryKey: key2, queryFn: queryFn2 },
])
await sleep(1)
unsubscribe()
expect(results.length).toBe(5)
expect(results[0]).toMatchObject([
{ status: 'pending', fetchStatus: 'idle', data: undefined },
{ status: 'pending', fetchStatus: 'idle', data: undefined },
])
expect(results[1]).toMatchObject([
{ status: 'pending', fetchStatus: 'fetching', data: undefined },
{ status: 'pending', fetchStatus: 'idle', data: undefined },
])
expect(results[2]).toMatchObject([
{ status: 'pending', fetchStatus: 'fetching', data: undefined },
{ status: 'pending', fetchStatus: 'fetching', data: undefined },
])
expect(results[3]).toMatchObject([
{ status: 'success', data: 1 },
{ status: 'pending', fetchStatus: 'fetching', data: undefined },
])
expect(results[4]).toMatchObject([
{ status: 'success', data: 1 },
{ status: 'success', data: 2 },
])
})
test('should trigger all fetches when subscribed', async () => {
const key1 = queryKey()
const key2 = queryKey()
const queryFn1 = vi.fn().mockReturnValue(1)
const queryFn2 = vi.fn().mockReturnValue(2)
const observer = new QueriesObserver(queryClient, [
{ queryKey: key1, queryFn: queryFn1 },
{ queryKey: key2, queryFn: queryFn2 },
])
const unsubscribe = observer.subscribe(() => undefined)
await sleep(1)
unsubscribe()
expect(queryFn1).toHaveBeenCalledTimes(1)
expect(queryFn2).toHaveBeenCalledTimes(1)
})
test('should not destroy the observer if there is still a subscription', async () => {
const key1 = queryKey()
const observer = new QueriesObserver(queryClient, [
{
queryKey: key1,
queryFn: async () => {
await sleep(20)
return 1
},
},
])
const subscription1Handler = vi.fn()
const subscription2Handler = vi.fn()
const unsubscribe1 = observer.subscribe(subscription1Handler)
const unsubscribe2 = observer.subscribe(subscription2Handler)
unsubscribe1()
await waitFor(() => {
// 1 call: pending
expect(subscription1Handler).toBeCalledTimes(1)
// 1 call: success
expect(subscription2Handler).toBeCalledTimes(1)
})
// Clean-up
unsubscribe2()
})
})

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,350 @@
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
import { waitFor } from '@testing-library/react'
import { QueryCache, QueryClient, QueryObserver } from '..'
import { createQueryClient, queryKey, sleep } from './utils'
describe('queryCache', () => {
let queryClient: QueryClient
let queryCache: QueryCache
beforeEach(() => {
queryClient = createQueryClient()
queryCache = queryClient.getQueryCache()
})
afterEach(() => {
queryClient.clear()
})
describe('subscribe', () => {
test('should pass the correct query', async () => {
const key = queryKey()
const subscriber = vi.fn()
const unsubscribe = queryCache.subscribe(subscriber)
queryClient.setQueryData(key, 'foo')
const query = queryCache.find({ queryKey: key })
await sleep(1)
expect(subscriber).toHaveBeenCalledWith({ query, type: 'added' })
unsubscribe()
})
test('should notify listeners when new query is added', async () => {
const key = queryKey()
const callback = vi.fn()
queryCache.subscribe(callback)
queryClient.prefetchQuery({ queryKey: key, queryFn: () => 'data' })
await sleep(100)
expect(callback).toHaveBeenCalled()
})
test('should notify query cache when a query becomes stale', async () => {
const key = queryKey()
const events: Array<string> = []
const queries: Array<unknown> = []
const unsubscribe = queryCache.subscribe((event) => {
events.push(event.type)
queries.push(event.query)
})
const observer = new QueryObserver(queryClient, {
queryKey: key,
queryFn: () => 'data',
staleTime: 10,
})
const unsubScribeObserver = observer.subscribe(vi.fn())
await waitFor(() => {
expect(events.length).toBe(8)
})
expect(events).toEqual([
'added', // 1. Query added -> loading
'observerResultsUpdated', // 2. Observer result updated -> loading
'observerAdded', // 3. Observer added
'observerResultsUpdated', // 4. Observer result updated -> fetching
'updated', // 5. Query updated -> fetching
'observerResultsUpdated', // 6. Observer result updated -> success
'updated', // 7. Query updated -> success
'observerResultsUpdated', // 8. Observer result updated -> stale
])
queries.forEach((query) => {
expect(query).toBeDefined()
})
unsubscribe()
unsubScribeObserver()
})
test('should include the queryCache and query when notifying listeners', async () => {
const key = queryKey()
const callback = vi.fn()
queryCache.subscribe(callback)
queryClient.prefetchQuery({ queryKey: key, queryFn: () => 'data' })
const query = queryCache.find({ queryKey: key })
await sleep(100)
expect(callback).toHaveBeenCalledWith({ query, type: 'added' })
})
test('should notify subscribers when new query with initialData is added', async () => {
const key = queryKey()
const callback = vi.fn()
queryCache.subscribe(callback)
queryClient.prefetchQuery({
queryKey: key,
queryFn: () => 'data',
initialData: 'initial',
})
await sleep(100)
expect(callback).toHaveBeenCalled()
})
test('should be able to limit cache size', async () => {
const testCache = new QueryCache()
const unsubscribe = testCache.subscribe((event) => {
if (event.type === 'added') {
if (testCache.getAll().length > 2) {
testCache
.findAll({
type: 'inactive',
predicate: (q) => q !== event.query,
})
.forEach((query) => {
testCache.remove(query)
})
}
}
})
const testClient = new QueryClient({ queryCache: testCache })
await testClient.prefetchQuery({
queryKey: ['key1'],
queryFn: () => 'data1',
})
expect(testCache.findAll().length).toBe(1)
await testClient.prefetchQuery({
queryKey: ['key2'],
queryFn: () => 'data2',
})
expect(testCache.findAll().length).toBe(2)
await testClient.prefetchQuery({
queryKey: ['key3'],
queryFn: () => 'data3',
})
expect(testCache.findAll().length).toBe(1)
expect(testCache.findAll()[0]!.state.data).toBe('data3')
unsubscribe()
})
})
describe('find', () => {
test('find should filter correctly', async () => {
const key = queryKey()
await queryClient.prefetchQuery({ queryKey: key, queryFn: () => 'data1' })
const query = queryCache.find({ queryKey: key })!
expect(query).toBeDefined()
})
test('find should filter correctly with exact set to false', async () => {
const key = queryKey()
await queryClient.prefetchQuery({ queryKey: key, queryFn: () => 'data1' })
const query = queryCache.find({ queryKey: key, exact: false })!
expect(query).toBeDefined()
})
})
describe('findAll', () => {
test('should filter correctly', async () => {
const key1 = queryKey()
const key2 = queryKey()
const keyFetching = queryKey()
await queryClient.prefetchQuery({
queryKey: key1,
queryFn: () => 'data1',
})
await queryClient.prefetchQuery({
queryKey: key2,
queryFn: () => 'data2',
})
await queryClient.prefetchQuery({
queryKey: [{ a: 'a', b: 'b' }],
queryFn: () => 'data3',
})
await queryClient.prefetchQuery({
queryKey: ['posts', 1],
queryFn: () => 'data4',
})
queryClient.invalidateQueries({ queryKey: key2 })
const query1 = queryCache.find({ queryKey: key1 })!
const query2 = queryCache.find({ queryKey: key2 })!
const query3 = queryCache.find({ queryKey: [{ a: 'a', b: 'b' }] })!
const query4 = queryCache.find({ queryKey: ['posts', 1] })!
expect(queryCache.findAll({ queryKey: key1 })).toEqual([query1])
// wrapping in an extra array doesn't yield the same results anymore since v4 because keys need to be an array
expect(queryCache.findAll({ queryKey: [key1] })).toEqual([])
expect(queryCache.findAll()).toEqual([query1, query2, query3, query4])
expect(queryCache.findAll({})).toEqual([query1, query2, query3, query4])
expect(queryCache.findAll({ queryKey: key1, type: 'inactive' })).toEqual([
query1,
])
expect(queryCache.findAll({ queryKey: key1, type: 'active' })).toEqual([])
expect(queryCache.findAll({ queryKey: key1, stale: true })).toEqual([])
expect(queryCache.findAll({ queryKey: key1, stale: false })).toEqual([
query1,
])
expect(
queryCache.findAll({ queryKey: key1, stale: false, type: 'active' }),
).toEqual([])
expect(
queryCache.findAll({ queryKey: key1, stale: false, type: 'inactive' }),
).toEqual([query1])
expect(
queryCache.findAll({
queryKey: key1,
stale: false,
type: 'inactive',
exact: true,
}),
).toEqual([query1])
expect(queryCache.findAll({ queryKey: key2 })).toEqual([query2])
expect(queryCache.findAll({ queryKey: key2, stale: undefined })).toEqual([
query2,
])
expect(queryCache.findAll({ queryKey: key2, stale: true })).toEqual([
query2,
])
expect(queryCache.findAll({ queryKey: key2, stale: false })).toEqual([])
expect(queryCache.findAll({ queryKey: [{ b: 'b' }] })).toEqual([query3])
expect(
queryCache.findAll({ queryKey: [{ a: 'a' }], exact: false }),
).toEqual([query3])
expect(
queryCache.findAll({ queryKey: [{ a: 'a' }], exact: true }),
).toEqual([])
expect(
queryCache.findAll({ queryKey: [{ a: 'a', b: 'b' }], exact: true }),
).toEqual([query3])
expect(queryCache.findAll({ queryKey: [{ a: 'a', b: 'b' }] })).toEqual([
query3,
])
expect(
queryCache.findAll({ queryKey: [{ a: 'a', b: 'b', c: 'c' }] }),
).toEqual([])
expect(
queryCache.findAll({ queryKey: [{ a: 'a' }], stale: false }),
).toEqual([query3])
expect(
queryCache.findAll({ queryKey: [{ a: 'a' }], stale: true }),
).toEqual([])
expect(
queryCache.findAll({ queryKey: [{ a: 'a' }], type: 'active' }),
).toEqual([])
expect(
queryCache.findAll({ queryKey: [{ a: 'a' }], type: 'inactive' }),
).toEqual([query3])
expect(
queryCache.findAll({ predicate: (query) => query === query3 }),
).toEqual([query3])
expect(queryCache.findAll({ queryKey: ['posts'] })).toEqual([query4])
expect(queryCache.findAll({ fetchStatus: 'idle' })).toEqual([
query1,
query2,
query3,
query4,
])
expect(
queryCache.findAll({ queryKey: key2, fetchStatus: undefined }),
).toEqual([query2])
const promise = queryClient.prefetchQuery({
queryKey: keyFetching,
queryFn: async () => {
await sleep(20)
return 'dataFetching'
},
})
expect(queryCache.findAll({ fetchStatus: 'fetching' })).toEqual([
queryCache.find({ queryKey: keyFetching }),
])
await promise
expect(queryCache.findAll({ fetchStatus: 'fetching' })).toEqual([])
})
test('should return all the queries when no filters are defined', async () => {
const key1 = queryKey()
const key2 = queryKey()
await queryClient.prefetchQuery({
queryKey: key1,
queryFn: () => 'data1',
})
await queryClient.prefetchQuery({
queryKey: key2,
queryFn: () => 'data2',
})
expect(queryCache.findAll().length).toBe(2)
})
})
describe('QueryCacheConfig error callbacks', () => {
test('should call onError and onSettled when a query errors', async () => {
const key = queryKey()
const onSuccess = vi.fn()
const onSettled = vi.fn()
const onError = vi.fn()
const testCache = new QueryCache({ onSuccess, onError, onSettled })
const testClient = createQueryClient({ queryCache: testCache })
await testClient.prefetchQuery({
queryKey: key,
queryFn: () => Promise.reject<unknown>('error'),
})
const query = testCache.find({ queryKey: key })
expect(onError).toHaveBeenCalledWith('error', query)
expect(onError).toHaveBeenCalledTimes(1)
expect(onSuccess).not.toHaveBeenCalled()
expect(onSettled).toHaveBeenCalledTimes(1)
expect(onSettled).toHaveBeenCalledWith(undefined, 'error', query)
})
})
describe('QueryCacheConfig success callbacks', () => {
test('should call onSuccess and onSettled when a query is successful', async () => {
const key = queryKey()
const onSuccess = vi.fn()
const onSettled = vi.fn()
const onError = vi.fn()
const testCache = new QueryCache({ onSuccess, onError, onSettled })
const testClient = createQueryClient({ queryCache: testCache })
await testClient.prefetchQuery({
queryKey: key,
queryFn: () => Promise.resolve({ data: 5 }),
})
const query = testCache.find({ queryKey: key })
expect(onSuccess).toHaveBeenCalledWith({ data: 5 }, query)
expect(onSuccess).toHaveBeenCalledTimes(1)
expect(onError).not.toHaveBeenCalled()
expect(onSettled).toHaveBeenCalledTimes(1)
expect(onSettled).toHaveBeenCalledWith({ data: 5 }, null, query)
})
})
describe('QueryCache.add', () => {
test('should not try to add a query already added to the cache', async () => {
const key = queryKey()
await queryClient.prefetchQuery({ queryKey: key, queryFn: () => 'data1' })
const query = queryCache.findAll()[0]!
const queryClone = Object.assign({}, query)
queryCache.add(queryClone)
expect(queryCache.getAll().length).toEqual(1)
})
})
})

View File

@@ -0,0 +1,156 @@
import { describe, expectTypeOf, it } from 'vitest'
import { QueryClient } from '../queryClient'
import type { DataTag, InfiniteData, QueryKey } from '../types'
describe('getQueryData', () => {
it('should be typed if key is tagged', () => {
const queryKey = ['key'] as DataTag<Array<string>, number>
const queryClient = new QueryClient()
const data = queryClient.getQueryData(queryKey)
expectTypeOf(data).toEqualTypeOf<number | undefined>()
})
it('should infer unknown if key is not tagged', () => {
const queryKey = ['key'] as const
const queryClient = new QueryClient()
const data = queryClient.getQueryData(queryKey)
expectTypeOf(data).toEqualTypeOf<unknown>()
})
it('should infer passed generic if passed', () => {
const queryKey = ['key'] as const
const queryClient = new QueryClient()
const data = queryClient.getQueryData<number>(queryKey)
expectTypeOf(data).toEqualTypeOf<number | undefined>()
})
it('should only allow Arrays to be passed', () => {
const queryKey = 'key'
const queryClient = new QueryClient()
// @ts-expect-error TS2345: Argument of type 'string' is not assignable to parameter of type 'QueryKey'
return queryClient.getQueryData(queryKey)
})
})
describe('setQueryData', () => {
it('updater should be typed if key is tagged', () => {
const queryKey = ['key'] as DataTag<Array<string>, number>
const queryClient = new QueryClient()
const data = queryClient.setQueryData(queryKey, (prev) => {
expectTypeOf(prev).toEqualTypeOf<number | undefined>()
return prev
})
expectTypeOf(data).toEqualTypeOf<number | undefined>()
})
it('value should be typed if key is tagged', () => {
const queryKey = ['key'] as DataTag<Array<string>, number>
const queryClient = new QueryClient()
// @ts-expect-error value should be a number
queryClient.setQueryData(queryKey, '1')
// @ts-expect-error value should be a number
queryClient.setQueryData(queryKey, () => '1')
const data = queryClient.setQueryData(queryKey, 1)
expectTypeOf(data).toEqualTypeOf<number | undefined>()
})
it('should infer unknown for updater if key is not tagged', () => {
const queryKey = ['key'] as const
const queryClient = new QueryClient()
const data = queryClient.setQueryData(queryKey, (prev) => {
expectTypeOf(prev).toEqualTypeOf<unknown>()
return prev
})
expectTypeOf(data).toEqualTypeOf<unknown>()
})
it('should infer unknown for value if key is not tagged', () => {
const queryKey = ['key'] as const
const queryClient = new QueryClient()
const data = queryClient.setQueryData(queryKey, 'foo')
expectTypeOf(data).toEqualTypeOf<unknown>()
})
it('should infer passed generic if passed', () => {
const queryKey = ['key'] as const
const queryClient = new QueryClient()
const data = queryClient.setQueryData<string>(queryKey, (prev) => {
expectTypeOf(prev).toEqualTypeOf<string | undefined>()
return prev
})
expectTypeOf(data).toEqualTypeOf<string | undefined>()
})
it('should infer passed generic for value', () => {
const queryKey = ['key'] as const
const queryClient = new QueryClient()
const data = queryClient.setQueryData<string>(queryKey, 'foo')
expectTypeOf(data).toEqualTypeOf<string | undefined>()
})
})
describe('fetchInfiniteQuery', () => {
it('should allow passing pages', async () => {
const data = await new QueryClient().fetchInfiniteQuery({
queryKey: ['key'],
queryFn: () => Promise.resolve('string'),
getNextPageParam: () => 1,
initialPageParam: 1,
pages: 5,
})
expectTypeOf(data).toEqualTypeOf<InfiniteData<string, number>>()
})
it('should not allow passing getNextPageParam without pages', () => {
new QueryClient().fetchInfiniteQuery({
queryKey: ['key'],
queryFn: () => Promise.resolve('string'),
initialPageParam: 1,
getNextPageParam: () => 1,
})
})
it('should not allow passing pages without getNextPageParam', () => {
// @ts-expect-error Property 'getNextPageParam' is missing
return new QueryClient().fetchInfiniteQuery({
queryKey: ['key'],
queryFn: () => Promise.resolve('string'),
initialPageParam: 1,
pages: 5,
})
})
})
describe('defaultOptions', () => {
it('should have a typed QueryFunctionContext', () => {
new QueryClient({
defaultOptions: {
queries: {
queryFn: (context) => {
expectTypeOf(context).toEqualTypeOf<{
queryKey: QueryKey
meta: Record<string, unknown> | undefined
signal: AbortSignal
pageParam?: unknown
direction?: unknown
}>()
return Promise.resolve('data')
},
},
},
})
})
})

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,108 @@
import { afterEach, beforeEach, describe, expectTypeOf, it } from 'vitest'
import { QueryObserver } from '..'
import { createQueryClient, queryKey } from './utils'
import type { QueryClient } from '..'
describe('queryObserver', () => {
let queryClient: QueryClient
beforeEach(() => {
queryClient = createQueryClient()
queryClient.mount()
})
afterEach(() => {
queryClient.clear()
})
it('should be inferred as a correct result type', () => {
const observer = new QueryObserver(queryClient, {
queryKey: queryKey(),
queryFn: () => Promise.resolve({ value: 'data' }),
})
const result = observer.getCurrentResult()
if (result.isPending) {
expectTypeOf(result.data).toEqualTypeOf<undefined>()
expectTypeOf(result.error).toEqualTypeOf<null>()
expectTypeOf(result.isLoading).toEqualTypeOf<boolean>()
expectTypeOf(result.status).toEqualTypeOf<'pending'>()
}
if (result.isLoading) {
expectTypeOf(result.data).toEqualTypeOf<undefined>()
expectTypeOf(result.error).toEqualTypeOf<null>()
expectTypeOf(result.isPending).toEqualTypeOf<true>()
expectTypeOf(result.status).toEqualTypeOf<'pending'>()
}
if (result.isLoadingError) {
expectTypeOf(result.data).toEqualTypeOf<undefined>()
expectTypeOf(result.error).toEqualTypeOf<Error>()
expectTypeOf(result.status).toEqualTypeOf<'error'>()
}
if (result.isRefetchError) {
expectTypeOf(result.data).toEqualTypeOf<{ value: string }>()
expectTypeOf(result.error).toEqualTypeOf<Error>()
expectTypeOf(result.status).toEqualTypeOf<'error'>()
}
if (result.isSuccess) {
expectTypeOf(result.data).toEqualTypeOf<{ value: string }>()
expectTypeOf(result.error).toEqualTypeOf<null>()
expectTypeOf(result.status).toEqualTypeOf<'success'>()
}
})
describe('placeholderData', () => {
it('previousQuery should have typed queryKey', () => {
const testQueryKey = ['SomeQuery', 42, { foo: 'bar' }] as const
new QueryObserver(createQueryClient(), {
queryKey: testQueryKey,
placeholderData: (_, previousQuery) => {
if (previousQuery) {
expectTypeOf(previousQuery.queryKey).toEqualTypeOf<
typeof testQueryKey
>()
}
},
})
})
it('previousQuery should have typed error', () => {
class CustomError extends Error {
name = 'CustomError' as const
}
new QueryObserver<boolean, CustomError>(createQueryClient(), {
queryKey: ['key'],
placeholderData: (_, previousQuery) => {
if (previousQuery) {
expectTypeOf(
previousQuery.state.error,
).toEqualTypeOf<CustomError | null>()
}
return undefined
},
})
})
it('previousData should have the same type as query data', () => {
const queryData = { foo: 'bar' } as const
new QueryObserver(createQueryClient(), {
queryKey: ['key'],
queryFn: () => queryData,
select: (data) => data.foo,
placeholderData: (previousData) => {
expectTypeOf(previousData).toEqualTypeOf<
typeof queryData | undefined
>()
return undefined
},
})
})
})
})

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,468 @@
import { describe, expect, it } from 'vitest'
import {
addToEnd,
addToStart,
isPlainArray,
isPlainObject,
matchMutation,
partialMatchKey,
replaceEqualDeep,
shallowEqualObjects,
} from '../utils'
import { Mutation } from '../mutation'
import { createQueryClient } from './utils'
describe('core/utils', () => {
describe('shallowEqualObjects', () => {
it('should return `true` for shallow equal objects', () => {
expect(shallowEqualObjects({ a: 1 }, { a: 1 })).toEqual(true)
})
it('should return `false` for non shallow equal objects', () => {
expect(shallowEqualObjects({ a: 1 }, { a: 2 })).toEqual(false)
})
it('should return `false` if lengths are not equal', () => {
expect(shallowEqualObjects({ a: 1 }, { a: 1, b: 2 })).toEqual(false)
})
it('should return false if b is undefined', () => {
expect(shallowEqualObjects({ a: 1 }, undefined)).toEqual(false)
})
})
describe('isPlainObject', () => {
it('should return `true` for a plain object', () => {
expect(isPlainObject({})).toEqual(true)
})
it('should return `false` for an array', () => {
expect(isPlainObject([])).toEqual(false)
})
it('should return `false` for null', () => {
expect(isPlainObject(null)).toEqual(false)
})
it('should return `false` for undefined', () => {
expect(isPlainObject(undefined)).toEqual(false)
})
it('should return `true` for object with an undefined constructor', () => {
expect(isPlainObject(Object.create(null))).toBeTruthy()
})
it('should return `false` if constructor does not have an Object-specific method', () => {
class Foo {
abc: any
constructor() {
this.abc = {}
}
}
expect(isPlainObject(new Foo())).toBeFalsy()
})
it('should return `false` if the object has a modified prototype', () => {
function Graph(this: any) {
this.vertices = []
this.edges = []
}
Graph.prototype.addVertex = function (v: any) {
this.vertices.push(v)
}
expect(isPlainObject(Object.create(Graph))).toBeFalsy()
})
it('should return `false` for object with custom prototype', () => {
const CustomProto = Object.create({ a: 1 })
const obj = Object.create(CustomProto)
obj.b = 2
expect(isPlainObject(obj)).toBeFalsy()
})
})
describe('isPlainArray', () => {
it('should return `true` for plain arrays', () => {
expect(isPlainArray([1, 2])).toEqual(true)
})
it('should return `false` for non plain arrays', () => {
expect(isPlainArray(Object.assign([1, 2], { a: 'b' }))).toEqual(false)
})
})
describe('partialMatchKey', () => {
it('should return `true` if a includes b', () => {
const a = [{ a: { b: 'b' }, c: 'c', d: [{ d: 'd ' }] }]
const b = [{ a: { b: 'b' }, c: 'c', d: [] }]
expect(partialMatchKey(a, b)).toEqual(true)
})
it('should return `false` if a does not include b', () => {
const a = [{ a: { b: 'b' }, c: 'c', d: [] }]
const b = [{ a: { b: 'b' }, c: 'c', d: [{ d: 'd ' }] }]
expect(partialMatchKey(a, b)).toEqual(false)
})
it('should return `true` if array a includes array b', () => {
const a = [1, 2, 3]
const b = [1, 2]
expect(partialMatchKey(a, b)).toEqual(true)
})
it('should return `false` if a is null and b is not', () => {
const a = [null]
const b = [{ a: { b: 'b' }, c: 'c', d: [{ d: 'd ' }] }]
expect(partialMatchKey(a, b)).toEqual(false)
})
it('should return `false` if a contains null and b is not', () => {
const a = [{ a: null, c: 'c', d: [] }]
const b = [{ a: { b: 'b' }, c: 'c', d: [{ d: 'd ' }] }]
expect(partialMatchKey(a, b)).toEqual(false)
})
it('should return `false` if b is null and a is not', () => {
const a = [{ a: { b: 'b' }, c: 'c', d: [] }]
const b = [null]
expect(partialMatchKey(a, b)).toEqual(false)
})
it('should return `false` if b contains null and a is not', () => {
const a = [{ a: { b: 'b' }, c: 'c', d: [] }]
const b = [{ a: null, c: 'c', d: [{ d: 'd ' }] }]
expect(partialMatchKey(a, b)).toEqual(false)
})
})
describe('replaceEqualDeep', () => {
it('should return the previous value when the next value is an equal primitive', () => {
expect(replaceEqualDeep(1, 1)).toBe(1)
expect(replaceEqualDeep('1', '1')).toBe('1')
expect(replaceEqualDeep(true, true)).toBe(true)
expect(replaceEqualDeep(false, false)).toBe(false)
expect(replaceEqualDeep(null, null)).toBe(null)
expect(replaceEqualDeep(undefined, undefined)).toBe(undefined)
})
it('should return the next value when the previous value is a different value', () => {
const date1 = new Date()
const date2 = new Date()
expect(replaceEqualDeep(1, 0)).toBe(0)
expect(replaceEqualDeep(1, 2)).toBe(2)
expect(replaceEqualDeep('1', '2')).toBe('2')
expect(replaceEqualDeep(true, false)).toBe(false)
expect(replaceEqualDeep(false, true)).toBe(true)
expect(replaceEqualDeep(date1, date2)).toBe(date2)
})
it('should return the next value when the previous value is a different type', () => {
const array = [1]
const object = { a: 'a' }
expect(replaceEqualDeep(0, undefined)).toBe(undefined)
expect(replaceEqualDeep(undefined, 0)).toBe(0)
expect(replaceEqualDeep(2, undefined)).toBe(undefined)
expect(replaceEqualDeep(undefined, 2)).toBe(2)
expect(replaceEqualDeep(undefined, null)).toBe(null)
expect(replaceEqualDeep(null, undefined)).toBe(undefined)
expect(replaceEqualDeep({}, undefined)).toBe(undefined)
expect(replaceEqualDeep([], undefined)).toBe(undefined)
expect(replaceEqualDeep(array, object)).toBe(object)
expect(replaceEqualDeep(object, array)).toBe(array)
})
it('should return the previous value when the next value is an equal array', () => {
const prev = [1, 2]
const next = [1, 2]
expect(replaceEqualDeep(prev, next)).toBe(prev)
})
it('should return a copy when the previous value is a different array subset', () => {
const prev = [1, 2]
const next = [1, 2, 3]
const result = replaceEqualDeep(prev, next)
expect(result).toEqual(next)
expect(result).not.toBe(prev)
expect(result).not.toBe(next)
})
it('should return a copy when the previous value is a different array superset', () => {
const prev = [1, 2, 3]
const next = [1, 2]
const result = replaceEqualDeep(prev, next)
expect(result).toEqual(next)
expect(result).not.toBe(prev)
expect(result).not.toBe(next)
})
it('should return the previous value when the next value is an equal empty array', () => {
const prev: Array<any> = []
const next: Array<any> = []
expect(replaceEqualDeep(prev, next)).toBe(prev)
})
it('should return the previous value when the next value is an equal empty object', () => {
const prev = {}
const next = {}
expect(replaceEqualDeep(prev, next)).toBe(prev)
})
it('should return the previous value when the next value is an equal object', () => {
const prev = { a: 'a' }
const next = { a: 'a' }
expect(replaceEqualDeep(prev, next)).toBe(prev)
})
it('should replace different values in objects', () => {
const prev = { a: { b: 'b' }, c: 'c' }
const next = { a: { b: 'b' }, c: 'd' }
const result = replaceEqualDeep(prev, next)
expect(result).toEqual(next)
expect(result).not.toBe(prev)
expect(result).not.toBe(next)
expect(result.a).toBe(prev.a)
expect(result.c).toBe(next.c)
})
it('should replace different values in arrays', () => {
const prev = [1, { a: 'a' }, { b: { b: 'b' } }, [1]] as const
const next = [1, { a: 'a' }, { b: { b: 'c' } }, [1]] as const
const result = replaceEqualDeep(prev, next)
expect(result).toEqual(next)
expect(result).not.toBe(prev)
expect(result).not.toBe(next)
expect(result[0]).toBe(prev[0])
expect(result[1]).toBe(prev[1])
expect(result[2]).not.toBe(next[2])
expect(result[2].b.b).toBe(next[2].b.b)
expect(result[3]).toBe(prev[3])
})
it('should replace different values in arrays when the next value is a subset', () => {
const prev = [{ a: 'a' }, { b: 'b' }, { c: 'c' }]
const next = [{ a: 'a' }, { b: 'b' }]
const result = replaceEqualDeep(prev, next)
expect(result).toEqual(next)
expect(result).not.toBe(prev)
expect(result).not.toBe(next)
expect(result[0]).toBe(prev[0])
expect(result[1]).toBe(prev[1])
expect(result[2]).toBeUndefined()
})
it('should replace different values in arrays when the next value is a superset', () => {
const prev = [{ a: 'a' }, { b: 'b' }]
const next = [{ a: 'a' }, { b: 'b' }, { c: 'c' }]
const result = replaceEqualDeep(prev, next)
expect(result).toEqual(next)
expect(result).not.toBe(prev)
expect(result).not.toBe(next)
expect(result[0]).toBe(prev[0])
expect(result[1]).toBe(prev[1])
expect(result[2]).toBe(next[2])
})
it('should copy objects which are not arrays or objects', () => {
const prev = [{ a: 'a' }, { b: 'b' }, { c: 'c' }, 1]
const next = [{ a: 'a' }, new Map(), { c: 'c' }, 2]
const result = replaceEqualDeep(prev, next)
expect(result).not.toBe(prev)
expect(result).not.toBe(next)
expect(result[0]).toBe(prev[0])
expect(result[1]).toBe(next[1])
expect(result[2]).toBe(prev[2])
expect(result[3]).toBe(next[3])
})
it('should support equal objects which are not arrays or objects', () => {
const map = new Map()
const prev = [map, [1]]
const next = [map, [1]]
const result = replaceEqualDeep(prev, next)
expect(result).toBe(prev)
})
it('should support non equal objects which are not arrays or objects', () => {
const map1 = new Map()
const map2 = new Map()
const prev = [map1, [1]]
const next = [map2, [1]]
const result = replaceEqualDeep(prev, next)
expect(result).not.toBe(prev)
expect(result).not.toBe(next)
expect(result[0]).toBe(next[0])
expect(result[1]).toBe(prev[1])
})
it('should support objects which are not plain arrays', () => {
const prev = Object.assign([1, 2], { a: { b: 'b' }, c: 'c' })
const next = Object.assign([1, 2], { a: { b: 'b' }, c: 'c' })
const result = replaceEqualDeep(prev, next)
expect(result).toBe(next)
})
it('should replace all parent objects if some nested value changes', () => {
const prev = {
todo: { id: '1', meta: { createdAt: 0 }, state: { done: false } },
otherTodo: { id: '2', meta: { createdAt: 0 }, state: { done: true } },
}
const next = {
todo: { id: '1', meta: { createdAt: 0 }, state: { done: true } },
otherTodo: { id: '2', meta: { createdAt: 0 }, state: { done: true } },
}
const result = replaceEqualDeep(prev, next)
expect(result).toEqual(next)
expect(result).not.toBe(prev)
expect(result).not.toBe(next)
expect(result.todo).not.toBe(prev.todo)
expect(result.todo).not.toBe(next.todo)
expect(result.todo.id).toBe(next.todo.id)
expect(result.todo.meta).toBe(prev.todo.meta)
expect(result.todo.state).not.toBe(next.todo.state)
expect(result.todo.state.done).toBe(next.todo.state.done)
expect(result.otherTodo).toBe(prev.otherTodo)
})
it('should replace all parent arrays if some nested value changes', () => {
const prev = {
todos: [
{ id: '1', meta: { createdAt: 0 }, state: { done: false } },
{ id: '2', meta: { createdAt: 0 }, state: { done: true } },
],
}
const next = {
todos: [
{ id: '1', meta: { createdAt: 0 }, state: { done: true } },
{ id: '2', meta: { createdAt: 0 }, state: { done: true } },
],
}
const result = replaceEqualDeep(prev, next)
expect(result).toEqual(next)
expect(result).not.toBe(prev)
expect(result).not.toBe(next)
expect(result.todos).not.toBe(prev.todos)
expect(result.todos).not.toBe(next.todos)
expect(result.todos[0]).not.toBe(prev.todos[0])
expect(result.todos[0]).not.toBe(next.todos[0])
expect(result.todos[0]?.id).toBe(next.todos[0]?.id)
expect(result.todos[0]?.meta).toBe(prev.todos[0]?.meta)
expect(result.todos[0]?.state).not.toBe(next.todos[0]?.state)
expect(result.todos[0]?.state.done).toBe(next.todos[0]?.state.done)
expect(result.todos[1]).toBe(prev.todos[1])
})
it('should correctly handle objects with the same number of properties and one property being undefined', () => {
const obj1 = { a: 2, c: 123 }
const obj2 = { a: 2, b: undefined }
const result = replaceEqualDeep(obj1, obj2)
expect(result).toStrictEqual(obj2)
})
it('should be able to share values that contain undefined', () => {
const current = [
{
data: undefined,
foo: true,
},
]
const next = replaceEqualDeep(current, [
{
data: undefined,
foo: true,
},
])
expect(current).toBe(next)
})
it('should return the previous value when both values are an array of undefined', () => {
const current = [undefined]
const next = replaceEqualDeep(current, [undefined])
expect(next).toBe(current)
})
it('should return the previous value when both values are an array that contains undefined', () => {
const current = [{ foo: 1 }, undefined]
const next = replaceEqualDeep(current, [{ foo: 1 }, undefined])
expect(next).toBe(current)
})
})
describe('matchMutation', () => {
it('should return false if mutationKey options is undefined', () => {
const filters = { mutationKey: ['key1'] }
const queryClient = createQueryClient()
const mutation = new Mutation({
mutationId: 1,
mutationCache: queryClient.getMutationCache(),
options: {},
})
expect(matchMutation(filters, mutation)).toBeFalsy()
})
})
describe('addToEnd', () => {
it('should add item to the end of the array', () => {
const items = [1, 2, 3]
const newItems = addToEnd(items, 4)
expect(newItems).toEqual([1, 2, 3, 4])
})
it('should not exceed max if provided', () => {
const items = [1, 2, 3]
const newItems = addToEnd(items, 4, 3)
expect(newItems).toEqual([2, 3, 4])
})
it('should add item to the end of the array when max = 0', () => {
const items = [1, 2, 3]
const item = 4
const max = 0
expect(addToEnd(items, item, max)).toEqual([1, 2, 3, 4])
})
it('should add item to the end of the array when max is undefined', () => {
const items = [1, 2, 3]
const item = 4
const max = undefined
expect(addToEnd(items, item, max)).toEqual([1, 2, 3, 4])
})
})
describe('addToStart', () => {
it('should add an item to the start of the array', () => {
const items = [1, 2, 3]
const item = 4
const newItems = addToStart(items, item)
expect(newItems).toEqual([4, 1, 2, 3])
})
it('should respect the max argument', () => {
const items = [1, 2, 3]
const item = 4
const max = 2
const newItems = addToStart(items, item, max)
expect(newItems).toEqual([4, 1, 2])
})
it('should not remove any items if max = 0', () => {
const items = [1, 2, 3]
const item = 4
const max = 0
const newItems = addToStart(items, item, max)
expect(newItems).toEqual([4, 1, 2, 3])
})
it('should not remove any items if max is undefined', () => {
const items = [1, 2, 3]
const item = 4
const max = 0
const newItems = addToStart(items, item, max)
expect(newItems).toEqual([4, 1, 2, 3])
})
})
})

View File

@@ -0,0 +1,59 @@
import { vi } from 'vitest'
import { QueryClient, onlineManager } from '..'
import * as utils from '../utils'
import type { MockInstance } from 'vitest'
import type { MutationOptions, QueryClientConfig } from '..'
export function createQueryClient(config?: QueryClientConfig): QueryClient {
return new QueryClient(config)
}
export function mockVisibilityState(
value: DocumentVisibilityState,
): MockInstance<() => DocumentVisibilityState> {
return vi.spyOn(document, 'visibilityState', 'get').mockReturnValue(value)
}
export function mockOnlineManagerIsOnline(
value: boolean,
): MockInstance<() => boolean> {
return vi.spyOn(onlineManager, 'isOnline').mockReturnValue(value)
}
let queryKeyCount = 0
export function queryKey(): Array<string> {
queryKeyCount++
return [`query_${queryKeyCount}`]
}
export function sleep(timeout: number): Promise<void> {
return new Promise((resolve, _reject) => {
setTimeout(resolve, timeout)
})
}
export function executeMutation<TVariables>(
queryClient: QueryClient,
options: MutationOptions<any, any, TVariables, any>,
variables: TVariables,
) {
return queryClient
.getMutationCache()
.build(queryClient, options)
.execute(variables)
}
// This monkey-patches the isServer-value from utils,
// so that we can pretend to be in a server environment
export function setIsServer(isServer: boolean) {
const original = utils.isServer
Object.defineProperty(utils, 'isServer', {
get: () => isServer,
})
return () => {
Object.defineProperty(utils, 'isServer', {
get: () => original,
})
}
}

86
node_modules/@tanstack/query-core/src/focusManager.ts generated vendored Normal file
View File

@@ -0,0 +1,86 @@
import { Subscribable } from './subscribable'
import { isServer } from './utils'
type Listener = (focused: boolean) => void
type SetupFn = (
setFocused: (focused?: boolean) => void,
) => (() => void) | undefined
export class FocusManager extends Subscribable<Listener> {
#focused?: boolean
#cleanup?: () => void
#setup: SetupFn
constructor() {
super()
this.#setup = (onFocus) => {
// addEventListener does not exist in React Native, but window does
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (!isServer && window.addEventListener) {
const listener = () => onFocus()
// Listen to visibilitychange
window.addEventListener('visibilitychange', listener, false)
return () => {
// Be sure to unsubscribe if a new handler is set
window.removeEventListener('visibilitychange', listener)
}
}
return
}
}
protected onSubscribe(): void {
if (!this.#cleanup) {
this.setEventListener(this.#setup)
}
}
protected onUnsubscribe() {
if (!this.hasListeners()) {
this.#cleanup?.()
this.#cleanup = undefined
}
}
setEventListener(setup: SetupFn): void {
this.#setup = setup
this.#cleanup?.()
this.#cleanup = setup((focused) => {
if (typeof focused === 'boolean') {
this.setFocused(focused)
} else {
this.onFocus()
}
})
}
setFocused(focused?: boolean): void {
const changed = this.#focused !== focused
if (changed) {
this.#focused = focused
this.onFocus()
}
}
onFocus(): void {
const isFocused = this.isFocused()
this.listeners.forEach((listener) => {
listener(isFocused)
})
}
isFocused(): boolean {
if (typeof this.#focused === 'boolean') {
return this.#focused
}
// document global can be unavailable in react native
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
return globalThis.document?.visibilityState !== 'hidden'
}
}
export const focusManager = new FocusManager()

222
node_modules/@tanstack/query-core/src/hydration.ts generated vendored Normal file
View File

@@ -0,0 +1,222 @@
import type {
DefaultError,
MutationKey,
MutationMeta,
MutationOptions,
MutationScope,
QueryKey,
QueryMeta,
QueryOptions,
} from './types'
import type { QueryClient } from './queryClient'
import type { Query, QueryState } from './query'
import type { Mutation, MutationState } from './mutation'
// TYPES
type TransformerFn = (data: any) => any
function defaultTransformerFn(data: any): any {
return data
}
export interface DehydrateOptions {
serializeData?: TransformerFn
shouldDehydrateMutation?: (mutation: Mutation) => boolean
shouldDehydrateQuery?: (query: Query) => boolean
}
export interface HydrateOptions {
defaultOptions?: {
deserializeData?: TransformerFn
queries?: QueryOptions
mutations?: MutationOptions<unknown, DefaultError, unknown, unknown>
}
}
interface DehydratedMutation {
mutationKey?: MutationKey
state: MutationState
meta?: MutationMeta
scope?: MutationScope
}
interface DehydratedQuery {
queryHash: string
queryKey: QueryKey
state: QueryState
promise?: Promise<unknown>
meta?: QueryMeta
}
export interface DehydratedState {
mutations: Array<DehydratedMutation>
queries: Array<DehydratedQuery>
}
// FUNCTIONS
function dehydrateMutation(mutation: Mutation): DehydratedMutation {
return {
mutationKey: mutation.options.mutationKey,
state: mutation.state,
...(mutation.options.scope && { scope: mutation.options.scope }),
...(mutation.meta && { meta: mutation.meta }),
}
}
// Most config is not dehydrated but instead meant to configure again when
// consuming the de/rehydrated data, typically with useQuery on the client.
// Sometimes it might make sense to prefetch data on the server and include
// in the html-payload, but not consume it on the initial render.
function dehydrateQuery(
query: Query,
serializeData: TransformerFn,
): DehydratedQuery {
return {
state: {
...query.state,
...(query.state.data !== undefined && {
data: serializeData(query.state.data),
}),
},
queryKey: query.queryKey,
queryHash: query.queryHash,
...(query.state.status === 'pending' && {
promise: query.promise?.then(serializeData).catch((error) => {
if (process.env.NODE_ENV !== 'production') {
console.error(
`A query that was dehydrated as pending ended up rejecting. [${query.queryHash}]: ${error}; The error will be redacted in production builds`,
)
}
return Promise.reject(new Error('redacted'))
}),
}),
...(query.meta && { meta: query.meta }),
}
}
export function defaultShouldDehydrateMutation(mutation: Mutation) {
return mutation.state.isPaused
}
export function defaultShouldDehydrateQuery(query: Query) {
return query.state.status === 'success'
}
export function dehydrate(
client: QueryClient,
options: DehydrateOptions = {},
): DehydratedState {
const filterMutation =
options.shouldDehydrateMutation ??
client.getDefaultOptions().dehydrate?.shouldDehydrateMutation ??
defaultShouldDehydrateMutation
const mutations = client
.getMutationCache()
.getAll()
.flatMap((mutation) =>
filterMutation(mutation) ? [dehydrateMutation(mutation)] : [],
)
const filterQuery =
options.shouldDehydrateQuery ??
client.getDefaultOptions().dehydrate?.shouldDehydrateQuery ??
defaultShouldDehydrateQuery
const serializeData =
options.serializeData ??
client.getDefaultOptions().dehydrate?.serializeData ??
defaultTransformerFn
const queries = client
.getQueryCache()
.getAll()
.flatMap((query) =>
filterQuery(query) ? [dehydrateQuery(query, serializeData)] : [],
)
return { mutations, queries }
}
export function hydrate(
client: QueryClient,
dehydratedState: unknown,
options?: HydrateOptions,
): void {
if (typeof dehydratedState !== 'object' || dehydratedState === null) {
return
}
const mutationCache = client.getMutationCache()
const queryCache = client.getQueryCache()
const deserializeData =
options?.defaultOptions?.deserializeData ??
client.getDefaultOptions().hydrate?.deserializeData ??
defaultTransformerFn
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
const mutations = (dehydratedState as DehydratedState).mutations || []
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
const queries = (dehydratedState as DehydratedState).queries || []
mutations.forEach(({ state, ...mutationOptions }) => {
mutationCache.build(
client,
{
...client.getDefaultOptions().hydrate?.mutations,
...options?.defaultOptions?.mutations,
...mutationOptions,
},
state,
)
})
queries.forEach(({ queryKey, state, queryHash, meta, promise }) => {
let query = queryCache.get(queryHash)
const data =
state.data === undefined ? state.data : deserializeData(state.data)
// Do not hydrate if an existing query exists with newer data
if (query) {
if (query.state.dataUpdatedAt < state.dataUpdatedAt) {
// omit fetchStatus from dehydrated state
// so that query stays in its current fetchStatus
const { fetchStatus: _ignored, ...serializedState } = state
query.setState({
...serializedState,
data,
})
}
} else {
// Restore query
query = queryCache.build(
client,
{
...client.getDefaultOptions().hydrate?.queries,
...options?.defaultOptions?.queries,
queryKey,
queryHash,
meta,
},
// Reset fetch status to idle to avoid
// query being stuck in fetching state upon hydration
{
...state,
data,
fetchStatus: 'idle',
},
)
}
if (promise) {
// Note: `Promise.resolve` required cause
// RSC transformed promises are not thenable
const initialPromise = Promise.resolve(promise).then(deserializeData)
// this doesn't actually fetch - it just creates a retryer
// which will re-use the passed `initialPromise`
void query.fetch(undefined, { initialPromise })
}
})
}

45
node_modules/@tanstack/query-core/src/index.ts generated vendored Normal file
View File

@@ -0,0 +1,45 @@
/* istanbul ignore file */
export { CancelledError } from './retryer'
export { QueryCache } from './queryCache'
export type { QueryCacheNotifyEvent } from './queryCache'
export { QueryClient } from './queryClient'
export { QueryObserver } from './queryObserver'
export { QueriesObserver } from './queriesObserver'
export { InfiniteQueryObserver } from './infiniteQueryObserver'
export { MutationCache } from './mutationCache'
export type { MutationCacheNotifyEvent } from './mutationCache'
export { MutationObserver } from './mutationObserver'
export { notifyManager } from './notifyManager'
export { focusManager } from './focusManager'
export { onlineManager } from './onlineManager'
export {
hashKey,
replaceEqualDeep,
isServer,
matchQuery,
matchMutation,
keepPreviousData,
skipToken,
} from './utils'
export type { MutationFilters, QueryFilters, Updater, SkipToken } from './utils'
export { isCancelledError } from './retryer'
export {
dehydrate,
hydrate,
defaultShouldDehydrateQuery,
defaultShouldDehydrateMutation,
} from './hydration'
// Types
export * from './types'
export type { QueryState } from './query'
export { Query } from './query'
export type { MutationState } from './mutation'
export { Mutation } from './mutation'
export type {
DehydrateOptions,
DehydratedState,
HydrateOptions,
} from './hydration'
export type { QueriesObserverOptions } from './queriesObserver'

View File

@@ -0,0 +1,175 @@
import { addToEnd, addToStart, ensureQueryFn } from './utils'
import type { QueryBehavior } from './query'
import type {
InfiniteData,
InfiniteQueryPageParamsOptions,
OmitKeyof,
QueryFunctionContext,
QueryKey,
} from './types'
export function infiniteQueryBehavior<TQueryFnData, TError, TData, TPageParam>(
pages?: number,
): QueryBehavior<TQueryFnData, TError, InfiniteData<TData, TPageParam>> {
return {
onFetch: (context, query) => {
const options = context.options as InfiniteQueryPageParamsOptions<TData>
const direction = context.fetchOptions?.meta?.fetchMore?.direction
const oldPages = context.state.data?.pages || []
const oldPageParams = context.state.data?.pageParams || []
let result: InfiniteData<unknown> = { pages: [], pageParams: [] }
let currentPage = 0
const fetchFn = async () => {
let cancelled = false
const addSignalProperty = (object: unknown) => {
Object.defineProperty(object, 'signal', {
enumerable: true,
get: () => {
if (context.signal.aborted) {
cancelled = true
} else {
context.signal.addEventListener('abort', () => {
cancelled = true
})
}
return context.signal
},
})
}
const queryFn = ensureQueryFn(context.options, context.fetchOptions)
// Create function to fetch a page
const fetchPage = async (
data: InfiniteData<unknown>,
param: unknown,
previous?: boolean,
): Promise<InfiniteData<unknown>> => {
if (cancelled) {
return Promise.reject()
}
if (param == null && data.pages.length) {
return Promise.resolve(data)
}
const queryFnContext: OmitKeyof<
QueryFunctionContext<QueryKey, unknown>,
'signal'
> = {
queryKey: context.queryKey,
pageParam: param,
direction: previous ? 'backward' : 'forward',
meta: context.options.meta,
}
addSignalProperty(queryFnContext)
const page = await queryFn(
queryFnContext as QueryFunctionContext<QueryKey, unknown>,
)
const { maxPages } = context.options
const addTo = previous ? addToStart : addToEnd
return {
pages: addTo(data.pages, page, maxPages),
pageParams: addTo(data.pageParams, param, maxPages),
}
}
// fetch next / previous page?
if (direction && oldPages.length) {
const previous = direction === 'backward'
const pageParamFn = previous ? getPreviousPageParam : getNextPageParam
const oldData = {
pages: oldPages,
pageParams: oldPageParams,
}
const param = pageParamFn(options, oldData)
result = await fetchPage(oldData, param, previous)
} else {
const remainingPages = pages ?? oldPages.length
// Fetch all pages
do {
const param =
currentPage === 0
? (oldPageParams[0] ?? options.initialPageParam)
: getNextPageParam(options, result)
if (currentPage > 0 && param == null) {
break
}
result = await fetchPage(result, param)
currentPage++
} while (currentPage < remainingPages)
}
return result
}
if (context.options.persister) {
context.fetchFn = () => {
return context.options.persister?.(
fetchFn as any,
{
queryKey: context.queryKey,
meta: context.options.meta,
signal: context.signal,
},
query,
)
}
} else {
context.fetchFn = fetchFn
}
},
}
}
function getNextPageParam(
options: InfiniteQueryPageParamsOptions<any>,
{ pages, pageParams }: InfiniteData<unknown>,
): unknown | undefined {
const lastIndex = pages.length - 1
return pages.length > 0
? options.getNextPageParam(
pages[lastIndex],
pages,
pageParams[lastIndex],
pageParams,
)
: undefined
}
function getPreviousPageParam(
options: InfiniteQueryPageParamsOptions<any>,
{ pages, pageParams }: InfiniteData<unknown>,
): unknown | undefined {
return pages.length > 0
? options.getPreviousPageParam?.(pages[0], pages, pageParams[0], pageParams)
: undefined
}
/**
* Checks if there is a next page.
*/
export function hasNextPage(
options: InfiniteQueryPageParamsOptions<any, any>,
data?: InfiniteData<unknown>,
): boolean {
if (!data) return false
return getNextPageParam(options, data) != null
}
/**
* Checks if there is a previous page.
*/
export function hasPreviousPage(
options: InfiniteQueryPageParamsOptions<any, any>,
data?: InfiniteData<unknown>,
): boolean {
if (!data || !options.getPreviousPageParam) return false
return getPreviousPageParam(options, data) != null
}

View File

@@ -0,0 +1,200 @@
import { QueryObserver } from './queryObserver'
import {
hasNextPage,
hasPreviousPage,
infiniteQueryBehavior,
} from './infiniteQueryBehavior'
import type { Subscribable } from './subscribable'
import type {
DefaultError,
DefaultedInfiniteQueryObserverOptions,
FetchNextPageOptions,
FetchPreviousPageOptions,
InfiniteData,
InfiniteQueryObserverBaseResult,
InfiniteQueryObserverOptions,
InfiniteQueryObserverResult,
QueryKey,
} from './types'
import type { QueryClient } from './queryClient'
import type { NotifyOptions } from './queryObserver'
import type { Query } from './query'
type InfiniteQueryObserverListener<TData, TError> = (
result: InfiniteQueryObserverResult<TData, TError>,
) => void
export class InfiniteQueryObserver<
TQueryFnData = unknown,
TError = DefaultError,
TData = InfiniteData<TQueryFnData>,
TQueryData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
TPageParam = unknown,
> extends QueryObserver<
TQueryFnData,
TError,
TData,
InfiniteData<TQueryData, TPageParam>,
TQueryKey
> {
// Type override
subscribe!: Subscribable<
InfiniteQueryObserverListener<TData, TError>
>['subscribe']
// Type override
getCurrentResult!: ReplaceReturnType<
QueryObserver<
TQueryFnData,
TError,
TData,
InfiniteData<TQueryData, TPageParam>,
TQueryKey
>['getCurrentResult'],
InfiniteQueryObserverResult<TData, TError>
>
// Type override
protected fetch!: ReplaceReturnType<
QueryObserver<
TQueryFnData,
TError,
TData,
InfiniteData<TQueryData, TPageParam>,
TQueryKey
>['fetch'],
Promise<InfiniteQueryObserverResult<TData, TError>>
>
constructor(
client: QueryClient,
options: InfiniteQueryObserverOptions<
TQueryFnData,
TError,
TData,
TQueryData,
TQueryKey,
TPageParam
>,
) {
super(client, options)
}
protected bindMethods(): void {
super.bindMethods()
this.fetchNextPage = this.fetchNextPage.bind(this)
this.fetchPreviousPage = this.fetchPreviousPage.bind(this)
}
setOptions(
options: InfiniteQueryObserverOptions<
TQueryFnData,
TError,
TData,
TQueryData,
TQueryKey,
TPageParam
>,
notifyOptions?: NotifyOptions,
): void {
super.setOptions(
{
...options,
behavior: infiniteQueryBehavior(),
},
notifyOptions,
)
}
getOptimisticResult(
options: DefaultedInfiniteQueryObserverOptions<
TQueryFnData,
TError,
TData,
TQueryData,
TQueryKey,
TPageParam
>,
): InfiniteQueryObserverResult<TData, TError> {
options.behavior = infiniteQueryBehavior()
return super.getOptimisticResult(options) as InfiniteQueryObserverResult<
TData,
TError
>
}
fetchNextPage(
options?: FetchNextPageOptions,
): Promise<InfiniteQueryObserverResult<TData, TError>> {
return this.fetch({
...options,
meta: {
fetchMore: { direction: 'forward' },
},
})
}
fetchPreviousPage(
options?: FetchPreviousPageOptions,
): Promise<InfiniteQueryObserverResult<TData, TError>> {
return this.fetch({
...options,
meta: {
fetchMore: { direction: 'backward' },
},
})
}
protected createResult(
query: Query<
TQueryFnData,
TError,
InfiniteData<TQueryData, TPageParam>,
TQueryKey
>,
options: InfiniteQueryObserverOptions<
TQueryFnData,
TError,
TData,
TQueryData,
TQueryKey,
TPageParam
>,
): InfiniteQueryObserverResult<TData, TError> {
const { state } = query
const parentResult = super.createResult(query, options)
const { isFetching, isRefetching, isError, isRefetchError } = parentResult
const fetchDirection = state.fetchMeta?.fetchMore?.direction
const isFetchNextPageError = isError && fetchDirection === 'forward'
const isFetchingNextPage = isFetching && fetchDirection === 'forward'
const isFetchPreviousPageError = isError && fetchDirection === 'backward'
const isFetchingPreviousPage = isFetching && fetchDirection === 'backward'
const result: InfiniteQueryObserverBaseResult<TData, TError> = {
...parentResult,
fetchNextPage: this.fetchNextPage,
fetchPreviousPage: this.fetchPreviousPage,
hasNextPage: hasNextPage(options, state.data),
hasPreviousPage: hasPreviousPage(options, state.data),
isFetchNextPageError,
isFetchingNextPage,
isFetchPreviousPageError,
isFetchingPreviousPage,
isRefetchError:
isRefetchError && !isFetchNextPageError && !isFetchPreviousPageError,
isRefetching:
isRefetching && !isFetchingNextPage && !isFetchingPreviousPage,
}
return result as InfiniteQueryObserverResult<TData, TError>
}
}
type ReplaceReturnType<
TFunction extends (...args: Array<any>) => unknown,
TReturn,
> = (...args: Parameters<TFunction>) => TReturn

361
node_modules/@tanstack/query-core/src/mutation.ts generated vendored Normal file
View File

@@ -0,0 +1,361 @@
import { notifyManager } from './notifyManager'
import { Removable } from './removable'
import { createRetryer } from './retryer'
import type {
DefaultError,
MutationMeta,
MutationOptions,
MutationStatus,
} from './types'
import type { MutationCache } from './mutationCache'
import type { MutationObserver } from './mutationObserver'
import type { Retryer } from './retryer'
// TYPES
interface MutationConfig<TData, TError, TVariables, TContext> {
mutationId: number
mutationCache: MutationCache
options: MutationOptions<TData, TError, TVariables, TContext>
state?: MutationState<TData, TError, TVariables, TContext>
}
export interface MutationState<
TData = unknown,
TError = DefaultError,
TVariables = unknown,
TContext = unknown,
> {
context: TContext | undefined
data: TData | undefined
error: TError | null
failureCount: number
failureReason: TError | null
isPaused: boolean
status: MutationStatus
variables: TVariables | undefined
submittedAt: number
}
interface FailedAction<TError> {
type: 'failed'
failureCount: number
error: TError | null
}
interface PendingAction<TVariables, TContext> {
type: 'pending'
isPaused: boolean
variables?: TVariables
context?: TContext
}
interface SuccessAction<TData> {
type: 'success'
data: TData
}
interface ErrorAction<TError> {
type: 'error'
error: TError
}
interface PauseAction {
type: 'pause'
}
interface ContinueAction {
type: 'continue'
}
export type Action<TData, TError, TVariables, TContext> =
| ContinueAction
| ErrorAction<TError>
| FailedAction<TError>
| PendingAction<TVariables, TContext>
| PauseAction
| SuccessAction<TData>
// CLASS
export class Mutation<
TData = unknown,
TError = DefaultError,
TVariables = unknown,
TContext = unknown,
> extends Removable {
state: MutationState<TData, TError, TVariables, TContext>
options!: MutationOptions<TData, TError, TVariables, TContext>
readonly mutationId: number
#observers: Array<MutationObserver<TData, TError, TVariables, TContext>>
#mutationCache: MutationCache
#retryer?: Retryer<TData>
constructor(config: MutationConfig<TData, TError, TVariables, TContext>) {
super()
this.mutationId = config.mutationId
this.#mutationCache = config.mutationCache
this.#observers = []
this.state = config.state || getDefaultState()
this.setOptions(config.options)
this.scheduleGc()
}
setOptions(
options: MutationOptions<TData, TError, TVariables, TContext>,
): void {
this.options = options
this.updateGcTime(this.options.gcTime)
}
get meta(): MutationMeta | undefined {
return this.options.meta
}
addObserver(observer: MutationObserver<any, any, any, any>): void {
if (!this.#observers.includes(observer)) {
this.#observers.push(observer)
// Stop the mutation from being garbage collected
this.clearGcTimeout()
this.#mutationCache.notify({
type: 'observerAdded',
mutation: this,
observer,
})
}
}
removeObserver(observer: MutationObserver<any, any, any, any>): void {
this.#observers = this.#observers.filter((x) => x !== observer)
this.scheduleGc()
this.#mutationCache.notify({
type: 'observerRemoved',
mutation: this,
observer,
})
}
protected optionalRemove() {
if (!this.#observers.length) {
if (this.state.status === 'pending') {
this.scheduleGc()
} else {
this.#mutationCache.remove(this)
}
}
}
continue(): Promise<unknown> {
return (
this.#retryer?.continue() ??
// continuing a mutation assumes that variables are set, mutation must have been dehydrated before
this.execute(this.state.variables!)
)
}
async execute(variables: TVariables): Promise<TData> {
this.#retryer = createRetryer({
fn: () => {
if (!this.options.mutationFn) {
return Promise.reject(new Error('No mutationFn found'))
}
return this.options.mutationFn(variables)
},
onFail: (failureCount, error) => {
this.#dispatch({ type: 'failed', failureCount, error })
},
onPause: () => {
this.#dispatch({ type: 'pause' })
},
onContinue: () => {
this.#dispatch({ type: 'continue' })
},
retry: this.options.retry ?? 0,
retryDelay: this.options.retryDelay,
networkMode: this.options.networkMode,
canRun: () => this.#mutationCache.canRun(this),
})
const restored = this.state.status === 'pending'
const isPaused = !this.#retryer.canStart()
try {
if (!restored) {
this.#dispatch({ type: 'pending', variables, isPaused })
// Notify cache callback
await this.#mutationCache.config.onMutate?.(
variables,
this as Mutation<unknown, unknown, unknown, unknown>,
)
const context = await this.options.onMutate?.(variables)
if (context !== this.state.context) {
this.#dispatch({
type: 'pending',
context,
variables,
isPaused,
})
}
}
const data = await this.#retryer.start()
// Notify cache callback
await this.#mutationCache.config.onSuccess?.(
data,
variables,
this.state.context,
this as Mutation<unknown, unknown, unknown, unknown>,
)
await this.options.onSuccess?.(data, variables, this.state.context!)
// Notify cache callback
await this.#mutationCache.config.onSettled?.(
data,
null,
this.state.variables,
this.state.context,
this as Mutation<unknown, unknown, unknown, unknown>,
)
await this.options.onSettled?.(data, null, variables, this.state.context)
this.#dispatch({ type: 'success', data })
return data
} catch (error) {
try {
// Notify cache callback
await this.#mutationCache.config.onError?.(
error as any,
variables,
this.state.context,
this as Mutation<unknown, unknown, unknown, unknown>,
)
await this.options.onError?.(
error as TError,
variables,
this.state.context,
)
// Notify cache callback
await this.#mutationCache.config.onSettled?.(
undefined,
error as any,
this.state.variables,
this.state.context,
this as Mutation<unknown, unknown, unknown, unknown>,
)
await this.options.onSettled?.(
undefined,
error as TError,
variables,
this.state.context,
)
throw error
} finally {
this.#dispatch({ type: 'error', error: error as TError })
}
} finally {
this.#mutationCache.runNext(this)
}
}
#dispatch(action: Action<TData, TError, TVariables, TContext>): void {
const reducer = (
state: MutationState<TData, TError, TVariables, TContext>,
): MutationState<TData, TError, TVariables, TContext> => {
switch (action.type) {
case 'failed':
return {
...state,
failureCount: action.failureCount,
failureReason: action.error,
}
case 'pause':
return {
...state,
isPaused: true,
}
case 'continue':
return {
...state,
isPaused: false,
}
case 'pending':
return {
...state,
context: action.context,
data: undefined,
failureCount: 0,
failureReason: null,
error: null,
isPaused: action.isPaused,
status: 'pending',
variables: action.variables,
submittedAt: Date.now(),
}
case 'success':
return {
...state,
data: action.data,
failureCount: 0,
failureReason: null,
error: null,
status: 'success',
isPaused: false,
}
case 'error':
return {
...state,
data: undefined,
error: action.error,
failureCount: state.failureCount + 1,
failureReason: action.error,
isPaused: false,
status: 'error',
}
}
}
this.state = reducer(this.state)
notifyManager.batch(() => {
this.#observers.forEach((observer) => {
observer.onMutationUpdate(action)
})
this.#mutationCache.notify({
mutation: this,
type: 'updated',
action,
})
})
}
}
export function getDefaultState<
TData,
TError,
TVariables,
TContext,
>(): MutationState<TData, TError, TVariables, TContext> {
return {
context: undefined,
data: undefined,
error: null,
failureCount: 0,
failureReason: null,
isPaused: false,
status: 'idle',
variables: undefined,
submittedAt: 0,
}
}

207
node_modules/@tanstack/query-core/src/mutationCache.ts generated vendored Normal file
View File

@@ -0,0 +1,207 @@
import { notifyManager } from './notifyManager'
import { Mutation } from './mutation'
import { matchMutation, noop } from './utils'
import { Subscribable } from './subscribable'
import type { MutationObserver } from './mutationObserver'
import type { DefaultError, MutationOptions, NotifyEvent } from './types'
import type { QueryClient } from './queryClient'
import type { Action, MutationState } from './mutation'
import type { MutationFilters } from './utils'
// TYPES
interface MutationCacheConfig {
onError?: (
error: DefaultError,
variables: unknown,
context: unknown,
mutation: Mutation<unknown, unknown, unknown>,
) => Promise<unknown> | unknown
onSuccess?: (
data: unknown,
variables: unknown,
context: unknown,
mutation: Mutation<unknown, unknown, unknown>,
) => Promise<unknown> | unknown
onMutate?: (
variables: unknown,
mutation: Mutation<unknown, unknown, unknown>,
) => Promise<unknown> | unknown
onSettled?: (
data: unknown | undefined,
error: DefaultError | null,
variables: unknown,
context: unknown,
mutation: Mutation<unknown, unknown, unknown>,
) => Promise<unknown> | unknown
}
interface NotifyEventMutationAdded extends NotifyEvent {
type: 'added'
mutation: Mutation<any, any, any, any>
}
interface NotifyEventMutationRemoved extends NotifyEvent {
type: 'removed'
mutation: Mutation<any, any, any, any>
}
interface NotifyEventMutationObserverAdded extends NotifyEvent {
type: 'observerAdded'
mutation: Mutation<any, any, any, any>
observer: MutationObserver<any, any, any>
}
interface NotifyEventMutationObserverRemoved extends NotifyEvent {
type: 'observerRemoved'
mutation: Mutation<any, any, any, any>
observer: MutationObserver<any, any, any>
}
interface NotifyEventMutationObserverOptionsUpdated extends NotifyEvent {
type: 'observerOptionsUpdated'
mutation?: Mutation<any, any, any, any>
observer: MutationObserver<any, any, any, any>
}
interface NotifyEventMutationUpdated extends NotifyEvent {
type: 'updated'
mutation: Mutation<any, any, any, any>
action: Action<any, any, any, any>
}
export type MutationCacheNotifyEvent =
| NotifyEventMutationAdded
| NotifyEventMutationRemoved
| NotifyEventMutationObserverAdded
| NotifyEventMutationObserverRemoved
| NotifyEventMutationObserverOptionsUpdated
| NotifyEventMutationUpdated
type MutationCacheListener = (event: MutationCacheNotifyEvent) => void
// CLASS
export class MutationCache extends Subscribable<MutationCacheListener> {
#mutations: Map<string, Array<Mutation<any, any, any, any>>>
#mutationId: number
constructor(public config: MutationCacheConfig = {}) {
super()
this.#mutations = new Map()
this.#mutationId = Date.now()
}
build<TData, TError, TVariables, TContext>(
client: QueryClient,
options: MutationOptions<TData, TError, TVariables, TContext>,
state?: MutationState<TData, TError, TVariables, TContext>,
): Mutation<TData, TError, TVariables, TContext> {
const mutation = new Mutation({
mutationCache: this,
mutationId: ++this.#mutationId,
options: client.defaultMutationOptions(options),
state,
})
this.add(mutation)
return mutation
}
add(mutation: Mutation<any, any, any, any>): void {
const scope = scopeFor(mutation)
const mutations = this.#mutations.get(scope) ?? []
mutations.push(mutation)
this.#mutations.set(scope, mutations)
this.notify({ type: 'added', mutation })
}
remove(mutation: Mutation<any, any, any, any>): void {
const scope = scopeFor(mutation)
if (this.#mutations.has(scope)) {
const mutations = this.#mutations
.get(scope)
?.filter((x) => x !== mutation)
if (mutations) {
if (mutations.length === 0) {
this.#mutations.delete(scope)
} else {
this.#mutations.set(scope, mutations)
}
}
}
this.notify({ type: 'removed', mutation })
}
canRun(mutation: Mutation<any, any, any, any>): boolean {
const firstPendingMutation = this.#mutations
.get(scopeFor(mutation))
?.find((m) => m.state.status === 'pending')
// we can run if there is no current pending mutation (start use-case)
// or if WE are the first pending mutation (continue use-case)
return !firstPendingMutation || firstPendingMutation === mutation
}
runNext(mutation: Mutation<any, any, any, any>): Promise<unknown> {
const foundMutation = this.#mutations
.get(scopeFor(mutation))
?.find((m) => m !== mutation && m.state.isPaused)
return foundMutation?.continue() ?? Promise.resolve()
}
clear(): void {
notifyManager.batch(() => {
this.getAll().forEach((mutation) => {
this.remove(mutation)
})
})
}
getAll(): Array<Mutation> {
return [...this.#mutations.values()].flat()
}
find<
TData = unknown,
TError = DefaultError,
TVariables = any,
TContext = unknown,
>(
filters: MutationFilters,
): Mutation<TData, TError, TVariables, TContext> | undefined {
const defaultedFilters = { exact: true, ...filters }
return this.getAll().find((mutation) =>
matchMutation(defaultedFilters, mutation),
) as Mutation<TData, TError, TVariables, TContext> | undefined
}
findAll(filters: MutationFilters = {}): Array<Mutation> {
return this.getAll().filter((mutation) => matchMutation(filters, mutation))
}
notify(event: MutationCacheNotifyEvent) {
notifyManager.batch(() => {
this.listeners.forEach((listener) => {
listener(event)
})
})
}
resumePausedMutations(): Promise<unknown> {
const pausedMutations = this.getAll().filter((x) => x.state.isPaused)
return notifyManager.batch(() =>
Promise.all(
pausedMutations.map((mutation) => mutation.continue().catch(noop)),
),
)
}
}
function scopeFor(mutation: Mutation<any, any, any, any>) {
return mutation.options.scope?.id ?? String(mutation.mutationId)
}

View File

@@ -0,0 +1,171 @@
import { getDefaultState } from './mutation'
import { notifyManager } from './notifyManager'
import { Subscribable } from './subscribable'
import { hashKey, shallowEqualObjects } from './utils'
import type { QueryClient } from './queryClient'
import type {
DefaultError,
MutateOptions,
MutationObserverOptions,
MutationObserverResult,
} from './types'
import type { Action, Mutation } from './mutation'
// TYPES
type MutationObserverListener<TData, TError, TVariables, TContext> = (
result: MutationObserverResult<TData, TError, TVariables, TContext>,
) => void
// CLASS
export class MutationObserver<
TData = unknown,
TError = DefaultError,
TVariables = void,
TContext = unknown,
> extends Subscribable<
MutationObserverListener<TData, TError, TVariables, TContext>
> {
options!: MutationObserverOptions<TData, TError, TVariables, TContext>
#client: QueryClient
#currentResult: MutationObserverResult<TData, TError, TVariables, TContext> =
undefined!
#currentMutation?: Mutation<TData, TError, TVariables, TContext>
#mutateOptions?: MutateOptions<TData, TError, TVariables, TContext>
constructor(
client: QueryClient,
options: MutationObserverOptions<TData, TError, TVariables, TContext>,
) {
super()
this.#client = client
this.setOptions(options)
this.bindMethods()
this.#updateResult()
}
protected bindMethods(): void {
this.mutate = this.mutate.bind(this)
this.reset = this.reset.bind(this)
}
setOptions(
options: MutationObserverOptions<TData, TError, TVariables, TContext>,
) {
const prevOptions = this.options as
| MutationObserverOptions<TData, TError, TVariables, TContext>
| undefined
this.options = this.#client.defaultMutationOptions(options)
if (!shallowEqualObjects(this.options, prevOptions)) {
this.#client.getMutationCache().notify({
type: 'observerOptionsUpdated',
mutation: this.#currentMutation,
observer: this,
})
}
if (
prevOptions?.mutationKey &&
this.options.mutationKey &&
hashKey(prevOptions.mutationKey) !== hashKey(this.options.mutationKey)
) {
this.reset()
} else if (this.#currentMutation?.state.status === 'pending') {
this.#currentMutation.setOptions(this.options)
}
}
protected onUnsubscribe(): void {
if (!this.hasListeners()) {
this.#currentMutation?.removeObserver(this)
}
}
onMutationUpdate(action: Action<TData, TError, TVariables, TContext>): void {
this.#updateResult()
this.#notify(action)
}
getCurrentResult(): MutationObserverResult<
TData,
TError,
TVariables,
TContext
> {
return this.#currentResult
}
reset(): void {
// reset needs to remove the observer from the mutation because there is no way to "get it back"
// another mutate call will yield a new mutation!
this.#currentMutation?.removeObserver(this)
this.#currentMutation = undefined
this.#updateResult()
this.#notify()
}
mutate(
variables: TVariables,
options?: MutateOptions<TData, TError, TVariables, TContext>,
): Promise<TData> {
this.#mutateOptions = options
this.#currentMutation?.removeObserver(this)
this.#currentMutation = this.#client
.getMutationCache()
.build(this.#client, this.options)
this.#currentMutation.addObserver(this)
return this.#currentMutation.execute(variables)
}
#updateResult(): void {
const state =
this.#currentMutation?.state ??
getDefaultState<TData, TError, TVariables, TContext>()
this.#currentResult = {
...state,
isPending: state.status === 'pending',
isSuccess: state.status === 'success',
isError: state.status === 'error',
isIdle: state.status === 'idle',
mutate: this.mutate,
reset: this.reset,
} as MutationObserverResult<TData, TError, TVariables, TContext>
}
#notify(action?: Action<TData, TError, TVariables, TContext>): void {
notifyManager.batch(() => {
// First trigger the mutate callbacks
if (this.#mutateOptions && this.hasListeners()) {
const variables = this.#currentResult.variables!
const context = this.#currentResult.context
if (action?.type === 'success') {
this.#mutateOptions.onSuccess?.(action.data, variables, context!)
this.#mutateOptions.onSettled?.(action.data, null, variables, context)
} else if (action?.type === 'error') {
this.#mutateOptions.onError?.(action.error, variables, context)
this.#mutateOptions.onSettled?.(
undefined,
action.error,
variables,
context,
)
}
}
// Then trigger the listeners
this.listeners.forEach((listener) => {
listener(this.#currentResult)
})
})
}
}

95
node_modules/@tanstack/query-core/src/notifyManager.ts generated vendored Normal file
View File

@@ -0,0 +1,95 @@
// TYPES
type NotifyCallback = () => void
type NotifyFunction = (callback: () => void) => void
type BatchNotifyFunction = (callback: () => void) => void
type BatchCallsCallback<T extends Array<unknown>> = (...args: T) => void
type ScheduleFunction = (callback: () => void) => void
export function createNotifyManager() {
let queue: Array<NotifyCallback> = []
let transactions = 0
let notifyFn: NotifyFunction = (callback) => {
callback()
}
let batchNotifyFn: BatchNotifyFunction = (callback: () => void) => {
callback()
}
let scheduleFn: ScheduleFunction = (cb) => setTimeout(cb, 0)
const schedule = (callback: NotifyCallback): void => {
if (transactions) {
queue.push(callback)
} else {
scheduleFn(() => {
notifyFn(callback)
})
}
}
const flush = (): void => {
const originalQueue = queue
queue = []
if (originalQueue.length) {
scheduleFn(() => {
batchNotifyFn(() => {
originalQueue.forEach((callback) => {
notifyFn(callback)
})
})
})
}
}
return {
batch: <T>(callback: () => T): T => {
let result
transactions++
try {
result = callback()
} finally {
transactions--
if (!transactions) {
flush()
}
}
return result
},
/**
* All calls to the wrapped function will be batched.
*/
batchCalls: <T extends Array<unknown>>(
callback: BatchCallsCallback<T>,
): BatchCallsCallback<T> => {
return (...args) => {
schedule(() => {
callback(...args)
})
}
},
schedule,
/**
* Use this method to set a custom notify function.
* This can be used to for example wrap notifications with `React.act` while running tests.
*/
setNotifyFunction: (fn: NotifyFunction) => {
notifyFn = fn
},
/**
* Use this method to set a custom function to batch notifications together into a single tick.
* By default React Query will use the batch function provided by ReactDOM or React Native.
*/
setBatchNotifyFunction: (fn: BatchNotifyFunction) => {
batchNotifyFn = fn
},
setScheduler: (fn: ScheduleFunction) => {
scheduleFn = fn
},
} as const
}
// SINGLETON
export const notifyManager = createNotifyManager()

71
node_modules/@tanstack/query-core/src/onlineManager.ts generated vendored Normal file
View File

@@ -0,0 +1,71 @@
import { Subscribable } from './subscribable'
import { isServer } from './utils'
type Listener = (online: boolean) => void
type SetupFn = (setOnline: Listener) => (() => void) | undefined
export class OnlineManager extends Subscribable<Listener> {
#online = true
#cleanup?: () => void
#setup: SetupFn
constructor() {
super()
this.#setup = (onOnline) => {
// addEventListener does not exist in React Native, but window does
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (!isServer && window.addEventListener) {
const onlineListener = () => onOnline(true)
const offlineListener = () => onOnline(false)
// Listen to online
window.addEventListener('online', onlineListener, false)
window.addEventListener('offline', offlineListener, false)
return () => {
// Be sure to unsubscribe if a new handler is set
window.removeEventListener('online', onlineListener)
window.removeEventListener('offline', offlineListener)
}
}
return
}
}
protected onSubscribe(): void {
if (!this.#cleanup) {
this.setEventListener(this.#setup)
}
}
protected onUnsubscribe() {
if (!this.hasListeners()) {
this.#cleanup?.()
this.#cleanup = undefined
}
}
setEventListener(setup: SetupFn): void {
this.#setup = setup
this.#cleanup?.()
this.#cleanup = setup(this.setOnline.bind(this))
}
setOnline(online: boolean): void {
const changed = this.#online !== online
if (changed) {
this.#online = online
this.listeners.forEach((listener) => {
listener(online)
})
}
}
isOnline(): boolean {
return this.#online
}
}
export const onlineManager = new OnlineManager()

View File

@@ -0,0 +1,279 @@
import { notifyManager } from './notifyManager'
import { QueryObserver } from './queryObserver'
import { Subscribable } from './subscribable'
import { replaceEqualDeep } from './utils'
import type {
DefaultedQueryObserverOptions,
QueryObserverOptions,
QueryObserverResult,
} from './types'
import type { QueryClient } from './queryClient'
import type { NotifyOptions } from './queryObserver'
function difference<T>(array1: Array<T>, array2: Array<T>): Array<T> {
return array1.filter((x) => !array2.includes(x))
}
function replaceAt<T>(array: Array<T>, index: number, value: T): Array<T> {
const copy = array.slice(0)
copy[index] = value
return copy
}
type QueriesObserverListener = (result: Array<QueryObserverResult>) => void
type CombineFn<TCombinedResult> = (
result: Array<QueryObserverResult>,
) => TCombinedResult
export interface QueriesObserverOptions<
TCombinedResult = Array<QueryObserverResult>,
> {
combine?: CombineFn<TCombinedResult>
}
export class QueriesObserver<
TCombinedResult = Array<QueryObserverResult>,
> extends Subscribable<QueriesObserverListener> {
#client: QueryClient
#result!: Array<QueryObserverResult>
#queries: Array<QueryObserverOptions>
#options?: QueriesObserverOptions<TCombinedResult>
#observers: Array<QueryObserver>
#combinedResult?: TCombinedResult
#lastCombine?: CombineFn<TCombinedResult>
#lastResult?: Array<QueryObserverResult>
constructor(
client: QueryClient,
queries: Array<QueryObserverOptions<any, any, any, any, any>>,
options?: QueriesObserverOptions<TCombinedResult>,
) {
super()
this.#client = client
this.#options = options
this.#queries = []
this.#observers = []
this.#result = []
this.setQueries(queries)
}
protected onSubscribe(): void {
if (this.listeners.size === 1) {
this.#observers.forEach((observer) => {
observer.subscribe((result) => {
this.#onUpdate(observer, result)
})
})
}
}
protected onUnsubscribe(): void {
if (!this.listeners.size) {
this.destroy()
}
}
destroy(): void {
this.listeners = new Set()
this.#observers.forEach((observer) => {
observer.destroy()
})
}
setQueries(
queries: Array<QueryObserverOptions>,
options?: QueriesObserverOptions<TCombinedResult>,
notifyOptions?: NotifyOptions,
): void {
this.#queries = queries
this.#options = options
notifyManager.batch(() => {
const prevObservers = this.#observers
const newObserverMatches = this.#findMatchingObservers(this.#queries)
// set options for the new observers to notify of changes
newObserverMatches.forEach((match) =>
match.observer.setOptions(match.defaultedQueryOptions, notifyOptions),
)
const newObservers = newObserverMatches.map((match) => match.observer)
const newResult = newObservers.map((observer) =>
observer.getCurrentResult(),
)
const hasIndexChange = newObservers.some(
(observer, index) => observer !== prevObservers[index],
)
if (prevObservers.length === newObservers.length && !hasIndexChange) {
return
}
this.#observers = newObservers
this.#result = newResult
if (!this.hasListeners()) {
return
}
difference(prevObservers, newObservers).forEach((observer) => {
observer.destroy()
})
difference(newObservers, prevObservers).forEach((observer) => {
observer.subscribe((result) => {
this.#onUpdate(observer, result)
})
})
this.#notify()
})
}
getCurrentResult(): Array<QueryObserverResult> {
return this.#result
}
getQueries() {
return this.#observers.map((observer) => observer.getCurrentQuery())
}
getObservers() {
return this.#observers
}
getOptimisticResult(
queries: Array<QueryObserverOptions>,
combine: CombineFn<TCombinedResult> | undefined,
): [
rawResult: Array<QueryObserverResult>,
combineResult: (r?: Array<QueryObserverResult>) => TCombinedResult,
trackResult: () => Array<QueryObserverResult>,
] {
const matches = this.#findMatchingObservers(queries)
const result = matches.map((match) =>
match.observer.getOptimisticResult(match.defaultedQueryOptions),
)
return [
result,
(r?: Array<QueryObserverResult>) => {
return this.#combineResult(r ?? result, combine)
},
() => {
return matches.map((match, index) => {
const observerResult = result[index]!
return !match.defaultedQueryOptions.notifyOnChangeProps
? match.observer.trackResult(observerResult, (accessedProp) => {
// track property on all observers to ensure proper (synchronized) tracking (#7000)
matches.forEach((m) => {
m.observer.trackProp(accessedProp)
})
})
: observerResult
})
},
]
}
#combineResult(
input: Array<QueryObserverResult>,
combine: CombineFn<TCombinedResult> | undefined,
): TCombinedResult {
if (combine) {
if (
!this.#combinedResult ||
this.#result !== this.#lastResult ||
combine !== this.#lastCombine
) {
this.#lastCombine = combine
this.#lastResult = this.#result
this.#combinedResult = replaceEqualDeep(
this.#combinedResult,
combine(input),
)
}
return this.#combinedResult
}
return input as any
}
#findMatchingObservers(
queries: Array<QueryObserverOptions>,
): Array<QueryObserverMatch> {
const prevObserversMap = new Map(
this.#observers.map((observer) => [observer.options.queryHash, observer]),
)
const observers: Array<QueryObserverMatch> = []
queries.forEach((options) => {
const defaultedOptions = this.#client.defaultQueryOptions(options)
const match = prevObserversMap.get(defaultedOptions.queryHash)
if (match) {
observers.push({
defaultedQueryOptions: defaultedOptions,
observer: match,
})
} else {
const existingObserver = this.#observers.find(
(o) => o.options.queryHash === defaultedOptions.queryHash,
)
observers.push({
defaultedQueryOptions: defaultedOptions,
observer:
existingObserver ??
new QueryObserver(this.#client, defaultedOptions),
})
}
})
return observers.sort((a, b) => {
return (
queries.findIndex(
(q) => q.queryHash === a.defaultedQueryOptions.queryHash,
) -
queries.findIndex(
(q) => q.queryHash === b.defaultedQueryOptions.queryHash,
)
)
})
}
#onUpdate(observer: QueryObserver, result: QueryObserverResult): void {
const index = this.#observers.indexOf(observer)
if (index !== -1) {
this.#result = replaceAt(this.#result, index, result)
this.#notify()
}
}
#notify(): void {
if (this.hasListeners()) {
const previousResult = this.#combinedResult
const newResult = this.#combineResult(
this.#result,
this.#options?.combine,
)
if (previousResult !== newResult) {
notifyManager.batch(() => {
this.listeners.forEach((listener) => {
listener(this.#result)
})
})
}
}
}
}
type QueryObserverMatch = {
defaultedQueryOptions: DefaultedQueryObserverOptions
observer: QueryObserver
}

683
node_modules/@tanstack/query-core/src/query.ts generated vendored Normal file
View File

@@ -0,0 +1,683 @@
import {
ensureQueryFn,
noop,
replaceData,
resolveEnabled,
skipToken,
timeUntilStale,
} from './utils'
import { notifyManager } from './notifyManager'
import { canFetch, createRetryer, isCancelledError } from './retryer'
import { Removable } from './removable'
import type {
CancelOptions,
DefaultError,
FetchStatus,
InitialDataFunction,
OmitKeyof,
QueryFunction,
QueryFunctionContext,
QueryKey,
QueryMeta,
QueryOptions,
QueryStatus,
SetDataOptions,
} from './types'
import type { QueryCache } from './queryCache'
import type { QueryObserver } from './queryObserver'
import type { Retryer } from './retryer'
// TYPES
interface QueryConfig<
TQueryFnData,
TError,
TData,
TQueryKey extends QueryKey = QueryKey,
> {
cache: QueryCache
queryKey: TQueryKey
queryHash: string
options?: QueryOptions<TQueryFnData, TError, TData, TQueryKey>
defaultOptions?: QueryOptions<TQueryFnData, TError, TData, TQueryKey>
state?: QueryState<TData, TError>
}
export interface QueryState<TData = unknown, TError = DefaultError> {
data: TData | undefined
dataUpdateCount: number
dataUpdatedAt: number
error: TError | null
errorUpdateCount: number
errorUpdatedAt: number
fetchFailureCount: number
fetchFailureReason: TError | null
fetchMeta: FetchMeta | null
isInvalidated: boolean
status: QueryStatus
fetchStatus: FetchStatus
}
export interface FetchContext<
TQueryFnData,
TError,
TData,
TQueryKey extends QueryKey = QueryKey,
> {
fetchFn: () => unknown | Promise<unknown>
fetchOptions?: FetchOptions
signal: AbortSignal
options: QueryOptions<TQueryFnData, TError, TData, any>
queryKey: TQueryKey
state: QueryState<TData, TError>
}
export interface QueryBehavior<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
> {
onFetch: (
context: FetchContext<TQueryFnData, TError, TData, TQueryKey>,
query: Query,
) => void
}
export type FetchDirection = 'forward' | 'backward'
export interface FetchMeta {
fetchMore?: { direction: FetchDirection }
}
export interface FetchOptions<TData = unknown> {
cancelRefetch?: boolean
meta?: FetchMeta
initialPromise?: Promise<TData>
}
interface FailedAction<TError> {
type: 'failed'
failureCount: number
error: TError
}
interface FetchAction {
type: 'fetch'
meta?: FetchMeta
}
interface SuccessAction<TData> {
data: TData | undefined
type: 'success'
dataUpdatedAt?: number
manual?: boolean
}
interface ErrorAction<TError> {
type: 'error'
error: TError
}
interface InvalidateAction {
type: 'invalidate'
}
interface PauseAction {
type: 'pause'
}
interface ContinueAction {
type: 'continue'
}
interface SetStateAction<TData, TError> {
type: 'setState'
state: Partial<QueryState<TData, TError>>
setStateOptions?: SetStateOptions
}
export type Action<TData, TError> =
| ContinueAction
| ErrorAction<TError>
| FailedAction<TError>
| FetchAction
| InvalidateAction
| PauseAction
| SetStateAction<TData, TError>
| SuccessAction<TData>
export interface SetStateOptions {
meta?: any
}
// CLASS
export class Query<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
> extends Removable {
queryKey: TQueryKey
queryHash: string
options!: QueryOptions<TQueryFnData, TError, TData, TQueryKey>
state: QueryState<TData, TError>
#initialState: QueryState<TData, TError>
#revertState?: QueryState<TData, TError>
#cache: QueryCache
#retryer?: Retryer<TData>
observers: Array<QueryObserver<any, any, any, any, any>>
#defaultOptions?: QueryOptions<TQueryFnData, TError, TData, TQueryKey>
#abortSignalConsumed: boolean
constructor(config: QueryConfig<TQueryFnData, TError, TData, TQueryKey>) {
super()
this.#abortSignalConsumed = false
this.#defaultOptions = config.defaultOptions
this.setOptions(config.options)
this.observers = []
this.#cache = config.cache
this.queryKey = config.queryKey
this.queryHash = config.queryHash
this.#initialState = getDefaultState(this.options)
this.state = config.state ?? this.#initialState
this.scheduleGc()
}
get meta(): QueryMeta | undefined {
return this.options.meta
}
get promise(): Promise<TData> | undefined {
return this.#retryer?.promise
}
setOptions(
options?: QueryOptions<TQueryFnData, TError, TData, TQueryKey>,
): void {
this.options = { ...this.#defaultOptions, ...options }
this.updateGcTime(this.options.gcTime)
}
protected optionalRemove() {
if (!this.observers.length && this.state.fetchStatus === 'idle') {
this.#cache.remove(this)
}
}
setData(
newData: TData,
options?: SetDataOptions & { manual: boolean },
): TData {
const data = replaceData(this.state.data, newData, this.options)
// Set data and mark it as cached
this.#dispatch({
data,
type: 'success',
dataUpdatedAt: options?.updatedAt,
manual: options?.manual,
})
return data
}
setState(
state: Partial<QueryState<TData, TError>>,
setStateOptions?: SetStateOptions,
): void {
this.#dispatch({ type: 'setState', state, setStateOptions })
}
cancel(options?: CancelOptions): Promise<void> {
const promise = this.#retryer?.promise
this.#retryer?.cancel(options)
return promise ? promise.then(noop).catch(noop) : Promise.resolve()
}
destroy(): void {
super.destroy()
this.cancel({ silent: true })
}
reset(): void {
this.destroy()
this.setState(this.#initialState)
}
isActive(): boolean {
return this.observers.some(
(observer) => resolveEnabled(observer.options.enabled, this) !== false,
)
}
isDisabled(): boolean {
if (this.getObserversCount() > 0) {
return !this.isActive()
}
// if a query has no observers, it should still be considered disabled if it never attempted a fetch
return (
this.options.queryFn === skipToken ||
this.state.dataUpdateCount + this.state.errorUpdateCount === 0
)
}
isStale(): boolean {
if (this.state.isInvalidated) {
return true
}
if (this.getObserversCount() > 0) {
return this.observers.some(
(observer) => observer.getCurrentResult().isStale,
)
}
return this.state.data === undefined
}
isStaleByTime(staleTime = 0): boolean {
return (
this.state.isInvalidated ||
this.state.data === undefined ||
!timeUntilStale(this.state.dataUpdatedAt, staleTime)
)
}
onFocus(): void {
const observer = this.observers.find((x) => x.shouldFetchOnWindowFocus())
observer?.refetch({ cancelRefetch: false })
// Continue fetch if currently paused
this.#retryer?.continue()
}
onOnline(): void {
const observer = this.observers.find((x) => x.shouldFetchOnReconnect())
observer?.refetch({ cancelRefetch: false })
// Continue fetch if currently paused
this.#retryer?.continue()
}
addObserver(observer: QueryObserver<any, any, any, any, any>): void {
if (!this.observers.includes(observer)) {
this.observers.push(observer)
// Stop the query from being garbage collected
this.clearGcTimeout()
this.#cache.notify({ type: 'observerAdded', query: this, observer })
}
}
removeObserver(observer: QueryObserver<any, any, any, any, any>): void {
if (this.observers.includes(observer)) {
this.observers = this.observers.filter((x) => x !== observer)
if (!this.observers.length) {
// If the transport layer does not support cancellation
// we'll let the query continue so the result can be cached
if (this.#retryer) {
if (this.#abortSignalConsumed) {
this.#retryer.cancel({ revert: true })
} else {
this.#retryer.cancelRetry()
}
}
this.scheduleGc()
}
this.#cache.notify({ type: 'observerRemoved', query: this, observer })
}
}
getObserversCount(): number {
return this.observers.length
}
invalidate(): void {
if (!this.state.isInvalidated) {
this.#dispatch({ type: 'invalidate' })
}
}
fetch(
options?: QueryOptions<TQueryFnData, TError, TData, TQueryKey>,
fetchOptions?: FetchOptions<TQueryFnData>,
): Promise<TData> {
if (this.state.fetchStatus !== 'idle') {
if (this.state.data !== undefined && fetchOptions?.cancelRefetch) {
// Silently cancel current fetch if the user wants to cancel refetch
this.cancel({ silent: true })
} else if (this.#retryer) {
// make sure that retries that were potentially cancelled due to unmounts can continue
this.#retryer.continueRetry()
// Return current promise if we are already fetching
return this.#retryer.promise
}
}
// Update config if passed, otherwise the config from the last execution is used
if (options) {
this.setOptions(options)
}
// Use the options from the first observer with a query function if no function is found.
// This can happen when the query is hydrated or created with setQueryData.
if (!this.options.queryFn) {
const observer = this.observers.find((x) => x.options.queryFn)
if (observer) {
this.setOptions(observer.options)
}
}
if (process.env.NODE_ENV !== 'production') {
if (!Array.isArray(this.options.queryKey)) {
console.error(
`As of v4, queryKey needs to be an Array. If you are using a string like 'repoData', please change it to an Array, e.g. ['repoData']`,
)
}
}
const abortController = new AbortController()
// Adds an enumerable signal property to the object that
// which sets abortSignalConsumed to true when the signal
// is read.
const addSignalProperty = (object: unknown) => {
Object.defineProperty(object, 'signal', {
enumerable: true,
get: () => {
this.#abortSignalConsumed = true
return abortController.signal
},
})
}
// Create fetch function
const fetchFn = () => {
const queryFn = ensureQueryFn(this.options, fetchOptions)
// Create query function context
const queryFnContext: OmitKeyof<
QueryFunctionContext<TQueryKey>,
'signal'
> = {
queryKey: this.queryKey,
meta: this.meta,
}
addSignalProperty(queryFnContext)
this.#abortSignalConsumed = false
if (this.options.persister) {
return this.options.persister(
queryFn as QueryFunction<any>,
queryFnContext as QueryFunctionContext<TQueryKey>,
this as unknown as Query,
)
}
return queryFn(queryFnContext as QueryFunctionContext<TQueryKey>)
}
// Trigger behavior hook
const context: OmitKeyof<
FetchContext<TQueryFnData, TError, TData, TQueryKey>,
'signal'
> = {
fetchOptions,
options: this.options,
queryKey: this.queryKey,
state: this.state,
fetchFn,
}
addSignalProperty(context)
this.options.behavior?.onFetch(
context as FetchContext<TQueryFnData, TError, TData, TQueryKey>,
this as unknown as Query,
)
// Store state in case the current fetch needs to be reverted
this.#revertState = this.state
// Set to fetching state if not already in it
if (
this.state.fetchStatus === 'idle' ||
this.state.fetchMeta !== context.fetchOptions?.meta
) {
this.#dispatch({ type: 'fetch', meta: context.fetchOptions?.meta })
}
const onError = (error: TError | { silent?: boolean }) => {
// Optimistically update state if needed
if (!(isCancelledError(error) && error.silent)) {
this.#dispatch({
type: 'error',
error: error as TError,
})
}
if (!isCancelledError(error)) {
// Notify cache callback
this.#cache.config.onError?.(
error as any,
this as Query<any, any, any, any>,
)
this.#cache.config.onSettled?.(
this.state.data,
error as any,
this as Query<any, any, any, any>,
)
}
// Schedule query gc after fetching
this.scheduleGc()
}
// Try to fetch the data
this.#retryer = createRetryer({
initialPromise: fetchOptions?.initialPromise as
| Promise<TData>
| undefined,
fn: context.fetchFn as () => Promise<TData>,
abort: abortController.abort.bind(abortController),
onSuccess: (data) => {
if (data === undefined) {
if (process.env.NODE_ENV !== 'production') {
console.error(
`Query data cannot be undefined. Please make sure to return a value other than undefined from your query function. Affected query key: ${this.queryHash}`,
)
}
onError(new Error(`${this.queryHash} data is undefined`) as any)
return
}
try {
this.setData(data)
} catch (error) {
onError(error as TError)
return
}
// Notify cache callback
this.#cache.config.onSuccess?.(data, this as Query<any, any, any, any>)
this.#cache.config.onSettled?.(
data,
this.state.error as any,
this as Query<any, any, any, any>,
)
// Schedule query gc after fetching
this.scheduleGc()
},
onError,
onFail: (failureCount, error) => {
this.#dispatch({ type: 'failed', failureCount, error })
},
onPause: () => {
this.#dispatch({ type: 'pause' })
},
onContinue: () => {
this.#dispatch({ type: 'continue' })
},
retry: context.options.retry,
retryDelay: context.options.retryDelay,
networkMode: context.options.networkMode,
canRun: () => true,
})
return this.#retryer.start()
}
#dispatch(action: Action<TData, TError>): void {
const reducer = (
state: QueryState<TData, TError>,
): QueryState<TData, TError> => {
switch (action.type) {
case 'failed':
return {
...state,
fetchFailureCount: action.failureCount,
fetchFailureReason: action.error,
}
case 'pause':
return {
...state,
fetchStatus: 'paused',
}
case 'continue':
return {
...state,
fetchStatus: 'fetching',
}
case 'fetch':
return {
...state,
...fetchState(state.data, this.options),
fetchMeta: action.meta ?? null,
}
case 'success':
return {
...state,
data: action.data,
dataUpdateCount: state.dataUpdateCount + 1,
dataUpdatedAt: action.dataUpdatedAt ?? Date.now(),
error: null,
isInvalidated: false,
status: 'success',
...(!action.manual && {
fetchStatus: 'idle',
fetchFailureCount: 0,
fetchFailureReason: null,
}),
}
case 'error':
const error = action.error
if (isCancelledError(error) && error.revert && this.#revertState) {
return { ...this.#revertState, fetchStatus: 'idle' }
}
return {
...state,
error,
errorUpdateCount: state.errorUpdateCount + 1,
errorUpdatedAt: Date.now(),
fetchFailureCount: state.fetchFailureCount + 1,
fetchFailureReason: error,
fetchStatus: 'idle',
status: 'error',
}
case 'invalidate':
return {
...state,
isInvalidated: true,
}
case 'setState':
return {
...state,
...action.state,
}
}
}
this.state = reducer(this.state)
notifyManager.batch(() => {
this.observers.forEach((observer) => {
observer.onQueryUpdate()
})
this.#cache.notify({ query: this, type: 'updated', action })
})
}
}
export function fetchState<
TQueryFnData,
TError,
TData,
TQueryKey extends QueryKey,
>(
data: TData | undefined,
options: QueryOptions<TQueryFnData, TError, TData, TQueryKey>,
) {
return {
fetchFailureCount: 0,
fetchFailureReason: null,
fetchStatus: canFetch(options.networkMode) ? 'fetching' : 'paused',
...(data === undefined &&
({
error: null,
status: 'pending',
} as const)),
} as const
}
function getDefaultState<
TQueryFnData,
TError,
TData,
TQueryKey extends QueryKey,
>(
options: QueryOptions<TQueryFnData, TError, TData, TQueryKey>,
): QueryState<TData, TError> {
const data =
typeof options.initialData === 'function'
? (options.initialData as InitialDataFunction<TData>)()
: options.initialData
const hasData = data !== undefined
const initialDataUpdatedAt = hasData
? typeof options.initialDataUpdatedAt === 'function'
? (options.initialDataUpdatedAt as () => number | undefined)()
: options.initialDataUpdatedAt
: 0
return {
data,
dataUpdateCount: 0,
dataUpdatedAt: hasData ? (initialDataUpdatedAt ?? Date.now()) : 0,
error: null,
errorUpdateCount: 0,
errorUpdatedAt: 0,
fetchFailureCount: 0,
fetchFailureReason: null,
fetchMeta: null,
isInvalidated: false,
status: hasData ? 'success' : 'pending',
fetchStatus: 'idle',
}
}

223
node_modules/@tanstack/query-core/src/queryCache.ts generated vendored Normal file
View File

@@ -0,0 +1,223 @@
import { hashQueryKeyByOptions, matchQuery } from './utils'
import { Query } from './query'
import { notifyManager } from './notifyManager'
import { Subscribable } from './subscribable'
import type { QueryFilters } from './utils'
import type { Action, QueryState } from './query'
import type {
DefaultError,
NotifyEvent,
QueryKey,
QueryOptions,
WithRequired,
} from './types'
import type { QueryClient } from './queryClient'
import type { QueryObserver } from './queryObserver'
// TYPES
interface QueryCacheConfig {
onError?: (
error: DefaultError,
query: Query<unknown, unknown, unknown>,
) => void
onSuccess?: (data: unknown, query: Query<unknown, unknown, unknown>) => void
onSettled?: (
data: unknown | undefined,
error: DefaultError | null,
query: Query<unknown, unknown, unknown>,
) => void
}
interface NotifyEventQueryAdded extends NotifyEvent {
type: 'added'
query: Query<any, any, any, any>
}
interface NotifyEventQueryRemoved extends NotifyEvent {
type: 'removed'
query: Query<any, any, any, any>
}
interface NotifyEventQueryUpdated extends NotifyEvent {
type: 'updated'
query: Query<any, any, any, any>
action: Action<any, any>
}
interface NotifyEventQueryObserverAdded extends NotifyEvent {
type: 'observerAdded'
query: Query<any, any, any, any>
observer: QueryObserver<any, any, any, any, any>
}
interface NotifyEventQueryObserverRemoved extends NotifyEvent {
type: 'observerRemoved'
query: Query<any, any, any, any>
observer: QueryObserver<any, any, any, any, any>
}
interface NotifyEventQueryObserverResultsUpdated extends NotifyEvent {
type: 'observerResultsUpdated'
query: Query<any, any, any, any>
}
interface NotifyEventQueryObserverOptionsUpdated extends NotifyEvent {
type: 'observerOptionsUpdated'
query: Query<any, any, any, any>
observer: QueryObserver<any, any, any, any, any>
}
export type QueryCacheNotifyEvent =
| NotifyEventQueryAdded
| NotifyEventQueryRemoved
| NotifyEventQueryUpdated
| NotifyEventQueryObserverAdded
| NotifyEventQueryObserverRemoved
| NotifyEventQueryObserverResultsUpdated
| NotifyEventQueryObserverOptionsUpdated
type QueryCacheListener = (event: QueryCacheNotifyEvent) => void
export interface QueryStore {
has: (queryHash: string) => boolean
set: (queryHash: string, query: Query) => void
get: (queryHash: string) => Query | undefined
delete: (queryHash: string) => void
values: () => IterableIterator<Query>
}
// CLASS
export class QueryCache extends Subscribable<QueryCacheListener> {
#queries: QueryStore
constructor(public config: QueryCacheConfig = {}) {
super()
this.#queries = new Map<string, Query>()
}
build<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
>(
client: QueryClient,
options: WithRequired<
QueryOptions<TQueryFnData, TError, TData, TQueryKey>,
'queryKey'
>,
state?: QueryState<TData, TError>,
): Query<TQueryFnData, TError, TData, TQueryKey> {
const queryKey = options.queryKey
const queryHash =
options.queryHash ?? hashQueryKeyByOptions(queryKey, options)
let query = this.get<TQueryFnData, TError, TData, TQueryKey>(queryHash)
if (!query) {
query = new Query({
cache: this,
queryKey,
queryHash,
options: client.defaultQueryOptions(options),
state,
defaultOptions: client.getQueryDefaults(queryKey),
})
this.add(query)
}
return query
}
add(query: Query<any, any, any, any>): void {
if (!this.#queries.has(query.queryHash)) {
this.#queries.set(query.queryHash, query)
this.notify({
type: 'added',
query,
})
}
}
remove(query: Query<any, any, any, any>): void {
const queryInMap = this.#queries.get(query.queryHash)
if (queryInMap) {
query.destroy()
if (queryInMap === query) {
this.#queries.delete(query.queryHash)
}
this.notify({ type: 'removed', query })
}
}
clear(): void {
notifyManager.batch(() => {
this.getAll().forEach((query) => {
this.remove(query)
})
})
}
get<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
>(
queryHash: string,
): Query<TQueryFnData, TError, TData, TQueryKey> | undefined {
return this.#queries.get(queryHash) as
| Query<TQueryFnData, TError, TData, TQueryKey>
| undefined
}
getAll(): Array<Query> {
return [...this.#queries.values()]
}
find<TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData>(
filters: WithRequired<QueryFilters, 'queryKey'>,
): Query<TQueryFnData, TError, TData> | undefined {
const defaultedFilters = { exact: true, ...filters }
return this.getAll().find((query) =>
matchQuery(defaultedFilters, query),
) as Query<TQueryFnData, TError, TData> | undefined
}
findAll(filters: QueryFilters = {}): Array<Query> {
const queries = this.getAll()
return Object.keys(filters).length > 0
? queries.filter((query) => matchQuery(filters, query))
: queries
}
notify(event: QueryCacheNotifyEvent): void {
notifyManager.batch(() => {
this.listeners.forEach((listener) => {
listener(event)
})
})
}
onFocus(): void {
notifyManager.batch(() => {
this.getAll().forEach((query) => {
query.onFocus()
})
})
}
onOnline(): void {
notifyManager.batch(() => {
this.getAll().forEach((query) => {
query.onOnline()
})
})
}
}

633
node_modules/@tanstack/query-core/src/queryClient.ts generated vendored Normal file
View File

@@ -0,0 +1,633 @@
import {
functionalUpdate,
hashKey,
hashQueryKeyByOptions,
noop,
partialMatchKey,
resolveStaleTime,
skipToken,
} from './utils'
import { QueryCache } from './queryCache'
import { MutationCache } from './mutationCache'
import { focusManager } from './focusManager'
import { onlineManager } from './onlineManager'
import { notifyManager } from './notifyManager'
import { infiniteQueryBehavior } from './infiniteQueryBehavior'
import type {
CancelOptions,
DataTag,
DefaultError,
DefaultOptions,
DefaultedQueryObserverOptions,
EnsureInfiniteQueryDataOptions,
EnsureQueryDataOptions,
FetchInfiniteQueryOptions,
FetchQueryOptions,
InfiniteData,
InvalidateOptions,
InvalidateQueryFilters,
MutationKey,
MutationObserverOptions,
MutationOptions,
NoInfer,
OmitKeyof,
QueryClientConfig,
QueryKey,
QueryObserverOptions,
QueryOptions,
RefetchOptions,
RefetchQueryFilters,
ResetOptions,
SetDataOptions,
} from './types'
import type { QueryState } from './query'
import type { MutationFilters, QueryFilters, Updater } from './utils'
// TYPES
interface QueryDefaults {
queryKey: QueryKey
defaultOptions: OmitKeyof<QueryOptions<any, any, any>, 'queryKey'>
}
interface MutationDefaults {
mutationKey: MutationKey
defaultOptions: MutationOptions<any, any, any, any>
}
// CLASS
export class QueryClient {
#queryCache: QueryCache
#mutationCache: MutationCache
#defaultOptions: DefaultOptions
#queryDefaults: Map<string, QueryDefaults>
#mutationDefaults: Map<string, MutationDefaults>
#mountCount: number
#unsubscribeFocus?: () => void
#unsubscribeOnline?: () => void
constructor(config: QueryClientConfig = {}) {
this.#queryCache = config.queryCache || new QueryCache()
this.#mutationCache = config.mutationCache || new MutationCache()
this.#defaultOptions = config.defaultOptions || {}
this.#queryDefaults = new Map()
this.#mutationDefaults = new Map()
this.#mountCount = 0
}
mount(): void {
this.#mountCount++
if (this.#mountCount !== 1) return
this.#unsubscribeFocus = focusManager.subscribe(async (focused) => {
if (focused) {
await this.resumePausedMutations()
this.#queryCache.onFocus()
}
})
this.#unsubscribeOnline = onlineManager.subscribe(async (online) => {
if (online) {
await this.resumePausedMutations()
this.#queryCache.onOnline()
}
})
}
unmount(): void {
this.#mountCount--
if (this.#mountCount !== 0) return
this.#unsubscribeFocus?.()
this.#unsubscribeFocus = undefined
this.#unsubscribeOnline?.()
this.#unsubscribeOnline = undefined
}
isFetching(filters?: QueryFilters): number {
return this.#queryCache.findAll({ ...filters, fetchStatus: 'fetching' })
.length
}
isMutating(filters?: MutationFilters): number {
return this.#mutationCache.findAll({ ...filters, status: 'pending' }).length
}
getQueryData<
TQueryFnData = unknown,
TTaggedQueryKey extends QueryKey = QueryKey,
TInferredQueryFnData = TTaggedQueryKey extends DataTag<
unknown,
infer TaggedValue
>
? TaggedValue
: TQueryFnData,
>(queryKey: TTaggedQueryKey): TInferredQueryFnData | undefined
getQueryData(queryKey: QueryKey) {
const options = this.defaultQueryOptions({ queryKey })
return this.#queryCache.get(options.queryHash)?.state.data
}
ensureQueryData<
TQueryFnData,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
>(
options: EnsureQueryDataOptions<TQueryFnData, TError, TData, TQueryKey>,
): Promise<TData> {
const cachedData = this.getQueryData<TData>(options.queryKey)
if (cachedData === undefined) return this.fetchQuery(options)
else {
const defaultedOptions = this.defaultQueryOptions(options)
const query = this.#queryCache.build(this, defaultedOptions)
if (
options.revalidateIfStale &&
query.isStaleByTime(resolveStaleTime(defaultedOptions.staleTime, query))
) {
void this.prefetchQuery(defaultedOptions)
}
return Promise.resolve(cachedData)
}
}
getQueriesData<TQueryFnData = unknown>(
filters: QueryFilters,
): Array<[QueryKey, TQueryFnData | undefined]> {
return this.#queryCache.findAll(filters).map(({ queryKey, state }) => {
const data = state.data as TQueryFnData | undefined
return [queryKey, data]
})
}
setQueryData<
TQueryFnData = unknown,
TTaggedQueryKey extends QueryKey = QueryKey,
TInferredQueryFnData = TTaggedQueryKey extends DataTag<
unknown,
infer TaggedValue
>
? TaggedValue
: TQueryFnData,
>(
queryKey: TTaggedQueryKey,
updater: Updater<
NoInfer<TInferredQueryFnData> | undefined,
NoInfer<TInferredQueryFnData> | undefined
>,
options?: SetDataOptions,
): TInferredQueryFnData | undefined {
const defaultedOptions = this.defaultQueryOptions<
any,
any,
unknown,
any,
QueryKey
>({ queryKey })
const query = this.#queryCache.get<TInferredQueryFnData>(
defaultedOptions.queryHash,
)
const prevData = query?.state.data
const data = functionalUpdate(updater, prevData)
if (data === undefined) {
return undefined
}
return this.#queryCache
.build(this, defaultedOptions)
.setData(data, { ...options, manual: true })
}
setQueriesData<TQueryFnData>(
filters: QueryFilters,
updater: Updater<TQueryFnData | undefined, TQueryFnData | undefined>,
options?: SetDataOptions,
): Array<[QueryKey, TQueryFnData | undefined]> {
return notifyManager.batch(() =>
this.#queryCache
.findAll(filters)
.map(({ queryKey }) => [
queryKey,
this.setQueryData<TQueryFnData>(queryKey, updater, options),
]),
)
}
getQueryState<
TQueryFnData = unknown,
TError = DefaultError,
TTaggedQueryKey extends QueryKey = QueryKey,
TInferredQueryFnData = TTaggedQueryKey extends DataTag<
unknown,
infer TaggedValue
>
? TaggedValue
: TQueryFnData,
>(
queryKey: TTaggedQueryKey,
): QueryState<TInferredQueryFnData, TError> | undefined {
const options = this.defaultQueryOptions({ queryKey })
return this.#queryCache.get<TInferredQueryFnData, TError>(options.queryHash)
?.state
}
removeQueries(filters?: QueryFilters): void {
const queryCache = this.#queryCache
notifyManager.batch(() => {
queryCache.findAll(filters).forEach((query) => {
queryCache.remove(query)
})
})
}
resetQueries(filters?: QueryFilters, options?: ResetOptions): Promise<void> {
const queryCache = this.#queryCache
const refetchFilters: RefetchQueryFilters = {
type: 'active',
...filters,
}
return notifyManager.batch(() => {
queryCache.findAll(filters).forEach((query) => {
query.reset()
})
return this.refetchQueries(refetchFilters, options)
})
}
cancelQueries(
filters: QueryFilters = {},
cancelOptions: CancelOptions = {},
): Promise<void> {
const defaultedCancelOptions = { revert: true, ...cancelOptions }
const promises = notifyManager.batch(() =>
this.#queryCache
.findAll(filters)
.map((query) => query.cancel(defaultedCancelOptions)),
)
return Promise.all(promises).then(noop).catch(noop)
}
invalidateQueries(
filters: InvalidateQueryFilters = {},
options: InvalidateOptions = {},
): Promise<void> {
return notifyManager.batch(() => {
this.#queryCache.findAll(filters).forEach((query) => {
query.invalidate()
})
if (filters.refetchType === 'none') {
return Promise.resolve()
}
const refetchFilters: RefetchQueryFilters = {
...filters,
type: filters.refetchType ?? filters.type ?? 'active',
}
return this.refetchQueries(refetchFilters, options)
})
}
refetchQueries(
filters: RefetchQueryFilters = {},
options?: RefetchOptions,
): Promise<void> {
const fetchOptions = {
...options,
cancelRefetch: options?.cancelRefetch ?? true,
}
const promises = notifyManager.batch(() =>
this.#queryCache
.findAll(filters)
.filter((query) => !query.isDisabled())
.map((query) => {
let promise = query.fetch(undefined, fetchOptions)
if (!fetchOptions.throwOnError) {
promise = promise.catch(noop)
}
return query.state.fetchStatus === 'paused'
? Promise.resolve()
: promise
}),
)
return Promise.all(promises).then(noop)
}
fetchQuery<
TQueryFnData,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
TPageParam = never,
>(
options: FetchQueryOptions<
TQueryFnData,
TError,
TData,
TQueryKey,
TPageParam
>,
): Promise<TData> {
const defaultedOptions = this.defaultQueryOptions(options)
// https://github.com/tannerlinsley/react-query/issues/652
if (defaultedOptions.retry === undefined) {
defaultedOptions.retry = false
}
const query = this.#queryCache.build(this, defaultedOptions)
return query.isStaleByTime(
resolveStaleTime(defaultedOptions.staleTime, query),
)
? query.fetch(defaultedOptions)
: Promise.resolve(query.state.data as TData)
}
prefetchQuery<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
>(
options: FetchQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
): Promise<void> {
return this.fetchQuery(options).then(noop).catch(noop)
}
fetchInfiniteQuery<
TQueryFnData,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
TPageParam = unknown,
>(
options: FetchInfiniteQueryOptions<
TQueryFnData,
TError,
TData,
TQueryKey,
TPageParam
>,
): Promise<InfiniteData<TData, TPageParam>> {
options.behavior = infiniteQueryBehavior<
TQueryFnData,
TError,
TData,
TPageParam
>(options.pages)
return this.fetchQuery(options as any)
}
prefetchInfiniteQuery<
TQueryFnData,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
TPageParam = unknown,
>(
options: FetchInfiniteQueryOptions<
TQueryFnData,
TError,
TData,
TQueryKey,
TPageParam
>,
): Promise<void> {
return this.fetchInfiniteQuery(options).then(noop).catch(noop)
}
ensureInfiniteQueryData<
TQueryFnData,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
TPageParam = unknown,
>(
options: EnsureInfiniteQueryDataOptions<
TQueryFnData,
TError,
TData,
TQueryKey,
TPageParam
>,
): Promise<InfiniteData<TData, TPageParam>> {
options.behavior = infiniteQueryBehavior<
TQueryFnData,
TError,
TData,
TPageParam
>(options.pages)
return this.ensureQueryData(options as any)
}
resumePausedMutations(): Promise<unknown> {
if (onlineManager.isOnline()) {
return this.#mutationCache.resumePausedMutations()
}
return Promise.resolve()
}
getQueryCache(): QueryCache {
return this.#queryCache
}
getMutationCache(): MutationCache {
return this.#mutationCache
}
getDefaultOptions(): DefaultOptions {
return this.#defaultOptions
}
setDefaultOptions(options: DefaultOptions): void {
this.#defaultOptions = options
}
setQueryDefaults<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryData = TQueryFnData,
>(
queryKey: QueryKey,
options: Partial<
OmitKeyof<
QueryObserverOptions<TQueryFnData, TError, TData, TQueryData>,
'queryKey'
>
>,
): void {
this.#queryDefaults.set(hashKey(queryKey), {
queryKey,
defaultOptions: options,
})
}
getQueryDefaults(
queryKey: QueryKey,
): OmitKeyof<QueryObserverOptions<any, any, any, any, any>, 'queryKey'> {
const defaults = [...this.#queryDefaults.values()]
let result: OmitKeyof<
QueryObserverOptions<any, any, any, any, any>,
'queryKey'
> = {}
defaults.forEach((queryDefault) => {
if (partialMatchKey(queryKey, queryDefault.queryKey)) {
result = { ...result, ...queryDefault.defaultOptions }
}
})
return result
}
setMutationDefaults<
TData = unknown,
TError = DefaultError,
TVariables = void,
TContext = unknown,
>(
mutationKey: MutationKey,
options: OmitKeyof<
MutationObserverOptions<TData, TError, TVariables, TContext>,
'mutationKey'
>,
): void {
this.#mutationDefaults.set(hashKey(mutationKey), {
mutationKey,
defaultOptions: options,
})
}
getMutationDefaults(
mutationKey: MutationKey,
): MutationObserverOptions<any, any, any, any> {
const defaults = [...this.#mutationDefaults.values()]
let result: MutationObserverOptions<any, any, any, any> = {}
defaults.forEach((queryDefault) => {
if (partialMatchKey(mutationKey, queryDefault.mutationKey)) {
result = { ...result, ...queryDefault.defaultOptions }
}
})
return result
}
defaultQueryOptions<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
TPageParam = never,
>(
options:
| QueryObserverOptions<
TQueryFnData,
TError,
TData,
TQueryData,
TQueryKey,
TPageParam
>
| DefaultedQueryObserverOptions<
TQueryFnData,
TError,
TData,
TQueryData,
TQueryKey
>,
): DefaultedQueryObserverOptions<
TQueryFnData,
TError,
TData,
TQueryData,
TQueryKey
> {
if (options._defaulted) {
return options as DefaultedQueryObserverOptions<
TQueryFnData,
TError,
TData,
TQueryData,
TQueryKey
>
}
const defaultedOptions = {
...this.#defaultOptions.queries,
...this.getQueryDefaults(options.queryKey),
...options,
_defaulted: true,
}
if (!defaultedOptions.queryHash) {
defaultedOptions.queryHash = hashQueryKeyByOptions(
defaultedOptions.queryKey,
defaultedOptions,
)
}
// dependent default values
if (defaultedOptions.refetchOnReconnect === undefined) {
defaultedOptions.refetchOnReconnect =
defaultedOptions.networkMode !== 'always'
}
if (defaultedOptions.throwOnError === undefined) {
defaultedOptions.throwOnError = !!defaultedOptions.suspense
}
if (!defaultedOptions.networkMode && defaultedOptions.persister) {
defaultedOptions.networkMode = 'offlineFirst'
}
if (
defaultedOptions.enabled !== true &&
defaultedOptions.queryFn === skipToken
) {
defaultedOptions.enabled = false
}
return defaultedOptions as DefaultedQueryObserverOptions<
TQueryFnData,
TError,
TData,
TQueryData,
TQueryKey
>
}
defaultMutationOptions<T extends MutationOptions<any, any, any, any>>(
options?: T,
): T {
if (options?._defaulted) {
return options
}
return {
...this.#defaultOptions.mutations,
...(options?.mutationKey &&
this.getMutationDefaults(options.mutationKey)),
...options,
_defaulted: true,
} as T
}
clear(): void {
this.#queryCache.clear()
this.#mutationCache.clear()
}
}

842
node_modules/@tanstack/query-core/src/queryObserver.ts generated vendored Normal file
View File

@@ -0,0 +1,842 @@
import { focusManager } from './focusManager'
import { notifyManager } from './notifyManager'
import { fetchState } from './query'
import { Subscribable } from './subscribable'
import { pendingThenable } from './thenable'
import {
isServer,
isValidTimeout,
noop,
replaceData,
resolveEnabled,
resolveStaleTime,
shallowEqualObjects,
timeUntilStale,
} from './utils'
import type { FetchOptions, Query, QueryState } from './query'
import type { QueryClient } from './queryClient'
import type { PendingThenable, Thenable } from './thenable'
import type {
DefaultError,
DefaultedQueryObserverOptions,
PlaceholderDataFunction,
QueryKey,
QueryObserverBaseResult,
QueryObserverOptions,
QueryObserverResult,
QueryOptions,
RefetchOptions,
} from './types'
type QueryObserverListener<TData, TError> = (
result: QueryObserverResult<TData, TError>,
) => void
export interface NotifyOptions {
listeners?: boolean
}
interface ObserverFetchOptions extends FetchOptions {
throwOnError?: boolean
}
export class QueryObserver<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
> extends Subscribable<QueryObserverListener<TData, TError>> {
#client: QueryClient
#currentQuery: Query<TQueryFnData, TError, TQueryData, TQueryKey> = undefined!
#currentQueryInitialState: QueryState<TQueryData, TError> = undefined!
#currentResult: QueryObserverResult<TData, TError> = undefined!
#currentResultState?: QueryState<TQueryData, TError>
#currentResultOptions?: QueryObserverOptions<
TQueryFnData,
TError,
TData,
TQueryData,
TQueryKey
>
#currentThenable: Thenable<TData>
#selectError: TError | null
#selectFn?: (data: TQueryData) => TData
#selectResult?: TData
// This property keeps track of the last query with defined data.
// It will be used to pass the previous data and query to the placeholder function between renders.
#lastQueryWithDefinedData?: Query<TQueryFnData, TError, TQueryData, TQueryKey>
#staleTimeoutId?: ReturnType<typeof setTimeout>
#refetchIntervalId?: ReturnType<typeof setInterval>
#currentRefetchInterval?: number | false
#trackedProps = new Set<keyof QueryObserverResult>()
constructor(
client: QueryClient,
public options: QueryObserverOptions<
TQueryFnData,
TError,
TData,
TQueryData,
TQueryKey
>,
) {
super()
this.#client = client
this.#selectError = null
this.#currentThenable = pendingThenable()
if (!this.options.experimental_prefetchInRender) {
this.#currentThenable.reject(
new Error('experimental_prefetchInRender feature flag is not enabled'),
)
}
this.bindMethods()
this.setOptions(options)
}
protected bindMethods(): void {
this.refetch = this.refetch.bind(this)
}
protected onSubscribe(): void {
if (this.listeners.size === 1) {
this.#currentQuery.addObserver(this)
if (shouldFetchOnMount(this.#currentQuery, this.options)) {
this.#executeFetch()
} else {
this.updateResult()
}
this.#updateTimers()
}
}
protected onUnsubscribe(): void {
if (!this.hasListeners()) {
this.destroy()
}
}
shouldFetchOnReconnect(): boolean {
return shouldFetchOn(
this.#currentQuery,
this.options,
this.options.refetchOnReconnect,
)
}
shouldFetchOnWindowFocus(): boolean {
return shouldFetchOn(
this.#currentQuery,
this.options,
this.options.refetchOnWindowFocus,
)
}
destroy(): void {
this.listeners = new Set()
this.#clearStaleTimeout()
this.#clearRefetchInterval()
this.#currentQuery.removeObserver(this)
}
setOptions(
options: QueryObserverOptions<
TQueryFnData,
TError,
TData,
TQueryData,
TQueryKey
>,
notifyOptions?: NotifyOptions,
): void {
const prevOptions = this.options
const prevQuery = this.#currentQuery
this.options = this.#client.defaultQueryOptions(options)
if (
this.options.enabled !== undefined &&
typeof this.options.enabled !== 'boolean' &&
typeof this.options.enabled !== 'function' &&
typeof resolveEnabled(this.options.enabled, this.#currentQuery) !==
'boolean'
) {
throw new Error(
'Expected enabled to be a boolean or a callback that returns a boolean',
)
}
this.#updateQuery()
this.#currentQuery.setOptions(this.options)
if (
prevOptions._defaulted &&
!shallowEqualObjects(this.options, prevOptions)
) {
this.#client.getQueryCache().notify({
type: 'observerOptionsUpdated',
query: this.#currentQuery,
observer: this,
})
}
const mounted = this.hasListeners()
// Fetch if there are subscribers
if (
mounted &&
shouldFetchOptionally(
this.#currentQuery,
prevQuery,
this.options,
prevOptions,
)
) {
this.#executeFetch()
}
// Update result
this.updateResult(notifyOptions)
// Update stale interval if needed
if (
mounted &&
(this.#currentQuery !== prevQuery ||
resolveEnabled(this.options.enabled, this.#currentQuery) !==
resolveEnabled(prevOptions.enabled, this.#currentQuery) ||
resolveStaleTime(this.options.staleTime, this.#currentQuery) !==
resolveStaleTime(prevOptions.staleTime, this.#currentQuery))
) {
this.#updateStaleTimeout()
}
const nextRefetchInterval = this.#computeRefetchInterval()
// Update refetch interval if needed
if (
mounted &&
(this.#currentQuery !== prevQuery ||
resolveEnabled(this.options.enabled, this.#currentQuery) !==
resolveEnabled(prevOptions.enabled, this.#currentQuery) ||
nextRefetchInterval !== this.#currentRefetchInterval)
) {
this.#updateRefetchInterval(nextRefetchInterval)
}
}
getOptimisticResult(
options: DefaultedQueryObserverOptions<
TQueryFnData,
TError,
TData,
TQueryData,
TQueryKey
>,
): QueryObserverResult<TData, TError> {
const query = this.#client.getQueryCache().build(this.#client, options)
const result = this.createResult(query, options)
if (shouldAssignObserverCurrentProperties(this, result)) {
// this assigns the optimistic result to the current Observer
// because if the query function changes, useQuery will be performing
// an effect where it would fetch again.
// When the fetch finishes, we perform a deep data cloning in order
// to reuse objects references. This deep data clone is performed against
// the `observer.currentResult.data` property
// When QueryKey changes, we refresh the query and get new `optimistic`
// result, while we leave the `observer.currentResult`, so when new data
// arrives, it finds the old `observer.currentResult` which is related
// to the old QueryKey. Which means that currentResult and selectData are
// out of sync already.
// To solve this, we move the cursor of the currentResult every time
// an observer reads an optimistic value.
// When keeping the previous data, the result doesn't change until new
// data arrives.
this.#currentResult = result
this.#currentResultOptions = this.options
this.#currentResultState = this.#currentQuery.state
}
return result
}
getCurrentResult(): QueryObserverResult<TData, TError> {
return this.#currentResult
}
trackResult(
result: QueryObserverResult<TData, TError>,
onPropTracked?: (key: keyof QueryObserverResult) => void,
): QueryObserverResult<TData, TError> {
const trackedResult = {} as QueryObserverResult<TData, TError>
Object.keys(result).forEach((key) => {
Object.defineProperty(trackedResult, key, {
configurable: false,
enumerable: true,
get: () => {
this.trackProp(key as keyof QueryObserverResult)
onPropTracked?.(key as keyof QueryObserverResult)
return result[key as keyof QueryObserverResult]
},
})
})
return trackedResult
}
trackProp(key: keyof QueryObserverResult) {
this.#trackedProps.add(key)
}
getCurrentQuery(): Query<TQueryFnData, TError, TQueryData, TQueryKey> {
return this.#currentQuery
}
refetch({ ...options }: RefetchOptions = {}): Promise<
QueryObserverResult<TData, TError>
> {
return this.fetch({
...options,
})
}
fetchOptimistic(
options: QueryObserverOptions<
TQueryFnData,
TError,
TData,
TQueryData,
TQueryKey
>,
): Promise<QueryObserverResult<TData, TError>> {
const defaultedOptions = this.#client.defaultQueryOptions(options)
const query = this.#client
.getQueryCache()
.build(this.#client, defaultedOptions)
return query.fetch().then(() => this.createResult(query, defaultedOptions))
}
protected fetch(
fetchOptions: ObserverFetchOptions,
): Promise<QueryObserverResult<TData, TError>> {
return this.#executeFetch({
...fetchOptions,
cancelRefetch: fetchOptions.cancelRefetch ?? true,
}).then(() => {
this.updateResult()
return this.#currentResult
})
}
#executeFetch(
fetchOptions?: Omit<ObserverFetchOptions, 'initialPromise'>,
): Promise<TQueryData | undefined> {
// Make sure we reference the latest query as the current one might have been removed
this.#updateQuery()
// Fetch
let promise: Promise<TQueryData | undefined> = this.#currentQuery.fetch(
this.options as QueryOptions<TQueryFnData, TError, TQueryData, TQueryKey>,
fetchOptions,
)
if (!fetchOptions?.throwOnError) {
promise = promise.catch(noop)
}
return promise
}
#updateStaleTimeout(): void {
this.#clearStaleTimeout()
const staleTime = resolveStaleTime(
this.options.staleTime,
this.#currentQuery,
)
if (isServer || this.#currentResult.isStale || !isValidTimeout(staleTime)) {
return
}
const time = timeUntilStale(this.#currentResult.dataUpdatedAt, staleTime)
// The timeout is sometimes triggered 1 ms before the stale time expiration.
// To mitigate this issue we always add 1 ms to the timeout.
const timeout = time + 1
this.#staleTimeoutId = setTimeout(() => {
if (!this.#currentResult.isStale) {
this.updateResult()
}
}, timeout)
}
#computeRefetchInterval() {
return (
(typeof this.options.refetchInterval === 'function'
? this.options.refetchInterval(this.#currentQuery)
: this.options.refetchInterval) ?? false
)
}
#updateRefetchInterval(nextInterval: number | false): void {
this.#clearRefetchInterval()
this.#currentRefetchInterval = nextInterval
if (
isServer ||
resolveEnabled(this.options.enabled, this.#currentQuery) === false ||
!isValidTimeout(this.#currentRefetchInterval) ||
this.#currentRefetchInterval === 0
) {
return
}
this.#refetchIntervalId = setInterval(() => {
if (
this.options.refetchIntervalInBackground ||
focusManager.isFocused()
) {
this.#executeFetch()
}
}, this.#currentRefetchInterval)
}
#updateTimers(): void {
this.#updateStaleTimeout()
this.#updateRefetchInterval(this.#computeRefetchInterval())
}
#clearStaleTimeout(): void {
if (this.#staleTimeoutId) {
clearTimeout(this.#staleTimeoutId)
this.#staleTimeoutId = undefined
}
}
#clearRefetchInterval(): void {
if (this.#refetchIntervalId) {
clearInterval(this.#refetchIntervalId)
this.#refetchIntervalId = undefined
}
}
protected createResult(
query: Query<TQueryFnData, TError, TQueryData, TQueryKey>,
options: QueryObserverOptions<
TQueryFnData,
TError,
TData,
TQueryData,
TQueryKey
>,
): QueryObserverResult<TData, TError> {
const prevQuery = this.#currentQuery
const prevOptions = this.options
const prevResult = this.#currentResult as
| QueryObserverResult<TData, TError>
| undefined
const prevResultState = this.#currentResultState
const prevResultOptions = this.#currentResultOptions
const queryChange = query !== prevQuery
const queryInitialState = queryChange
? query.state
: this.#currentQueryInitialState
const { state } = query
let newState = { ...state }
let isPlaceholderData = false
let data: TData | undefined
// Optimistically set result in fetching state if needed
if (options._optimisticResults) {
const mounted = this.hasListeners()
const fetchOnMount = !mounted && shouldFetchOnMount(query, options)
const fetchOptionally =
mounted && shouldFetchOptionally(query, prevQuery, options, prevOptions)
if (fetchOnMount || fetchOptionally) {
newState = {
...newState,
...fetchState(state.data, query.options),
}
}
if (options._optimisticResults === 'isRestoring') {
newState.fetchStatus = 'idle'
}
}
let { error, errorUpdatedAt, status } = newState
// Select data if needed
if (options.select && newState.data !== undefined) {
// Memoize select result
if (
prevResult &&
newState.data === prevResultState?.data &&
options.select === this.#selectFn
) {
data = this.#selectResult
} else {
try {
this.#selectFn = options.select
data = options.select(newState.data)
data = replaceData(prevResult?.data, data, options)
this.#selectResult = data
this.#selectError = null
} catch (selectError) {
this.#selectError = selectError as TError
}
}
}
// Use query data
else {
data = newState.data as unknown as TData
}
// Show placeholder data if needed
if (
options.placeholderData !== undefined &&
data === undefined &&
status === 'pending'
) {
let placeholderData
// Memoize placeholder data
if (
prevResult?.isPlaceholderData &&
options.placeholderData === prevResultOptions?.placeholderData
) {
placeholderData = prevResult.data
} else {
placeholderData =
typeof options.placeholderData === 'function'
? (
options.placeholderData as unknown as PlaceholderDataFunction<TQueryData>
)(
this.#lastQueryWithDefinedData?.state.data,
this.#lastQueryWithDefinedData as any,
)
: options.placeholderData
if (options.select && placeholderData !== undefined) {
try {
placeholderData = options.select(placeholderData)
this.#selectError = null
} catch (selectError) {
this.#selectError = selectError as TError
}
}
}
if (placeholderData !== undefined) {
status = 'success'
data = replaceData(
prevResult?.data,
placeholderData as unknown,
options,
) as TData
isPlaceholderData = true
}
}
if (this.#selectError) {
error = this.#selectError as any
data = this.#selectResult
errorUpdatedAt = Date.now()
status = 'error'
}
const isFetching = newState.fetchStatus === 'fetching'
const isPending = status === 'pending'
const isError = status === 'error'
const isLoading = isPending && isFetching
const hasData = data !== undefined
const result: QueryObserverBaseResult<TData, TError> = {
status,
fetchStatus: newState.fetchStatus,
isPending,
isSuccess: status === 'success',
isError,
isInitialLoading: isLoading,
isLoading,
data,
dataUpdatedAt: newState.dataUpdatedAt,
error,
errorUpdatedAt,
failureCount: newState.fetchFailureCount,
failureReason: newState.fetchFailureReason,
errorUpdateCount: newState.errorUpdateCount,
isFetched: newState.dataUpdateCount > 0 || newState.errorUpdateCount > 0,
isFetchedAfterMount:
newState.dataUpdateCount > queryInitialState.dataUpdateCount ||
newState.errorUpdateCount > queryInitialState.errorUpdateCount,
isFetching,
isRefetching: isFetching && !isPending,
isLoadingError: isError && !hasData,
isPaused: newState.fetchStatus === 'paused',
isPlaceholderData,
isRefetchError: isError && hasData,
isStale: isStale(query, options),
refetch: this.refetch,
promise: this.#currentThenable,
}
const nextResult = result as QueryObserverResult<TData, TError>
if (this.options.experimental_prefetchInRender) {
const finalizeThenableIfPossible = (thenable: PendingThenable<TData>) => {
if (nextResult.status === 'error') {
thenable.reject(nextResult.error)
} else if (nextResult.data !== undefined) {
thenable.resolve(nextResult.data)
}
}
/**
* Create a new thenable and result promise when the results have changed
*/
const recreateThenable = () => {
const pending =
(this.#currentThenable =
nextResult.promise =
pendingThenable())
finalizeThenableIfPossible(pending)
}
const prevThenable = this.#currentThenable
switch (prevThenable.status) {
case 'pending':
// Finalize the previous thenable if it was pending
// and we are still observing the same query
if (query.queryHash === prevQuery.queryHash) {
finalizeThenableIfPossible(prevThenable)
}
break
case 'fulfilled':
if (
nextResult.status === 'error' ||
nextResult.data !== prevThenable.value
) {
recreateThenable()
}
break
case 'rejected':
if (
nextResult.status !== 'error' ||
nextResult.error !== prevThenable.reason
) {
recreateThenable()
}
break
}
}
return nextResult
}
updateResult(notifyOptions?: NotifyOptions): void {
const prevResult = this.#currentResult as
| QueryObserverResult<TData, TError>
| undefined
const nextResult = this.createResult(this.#currentQuery, this.options)
this.#currentResultState = this.#currentQuery.state
this.#currentResultOptions = this.options
if (this.#currentResultState.data !== undefined) {
this.#lastQueryWithDefinedData = this.#currentQuery
}
// Only notify and update result if something has changed
if (shallowEqualObjects(nextResult, prevResult)) {
return
}
this.#currentResult = nextResult
// Determine which callbacks to trigger
const defaultNotifyOptions: NotifyOptions = {}
const shouldNotifyListeners = (): boolean => {
if (!prevResult) {
return true
}
const { notifyOnChangeProps } = this.options
const notifyOnChangePropsValue =
typeof notifyOnChangeProps === 'function'
? notifyOnChangeProps()
: notifyOnChangeProps
if (
notifyOnChangePropsValue === 'all' ||
(!notifyOnChangePropsValue && !this.#trackedProps.size)
) {
return true
}
const includedProps = new Set(
notifyOnChangePropsValue ?? this.#trackedProps,
)
if (this.options.throwOnError) {
includedProps.add('error')
}
return Object.keys(this.#currentResult).some((key) => {
const typedKey = key as keyof QueryObserverResult
const changed = this.#currentResult[typedKey] !== prevResult[typedKey]
return changed && includedProps.has(typedKey)
})
}
if (notifyOptions?.listeners !== false && shouldNotifyListeners()) {
defaultNotifyOptions.listeners = true
}
this.#notify({ ...defaultNotifyOptions, ...notifyOptions })
}
#updateQuery(): void {
const query = this.#client.getQueryCache().build(this.#client, this.options)
if (query === this.#currentQuery) {
return
}
const prevQuery = this.#currentQuery as
| Query<TQueryFnData, TError, TQueryData, TQueryKey>
| undefined
this.#currentQuery = query
this.#currentQueryInitialState = query.state
if (this.hasListeners()) {
prevQuery?.removeObserver(this)
query.addObserver(this)
}
}
onQueryUpdate(): void {
this.updateResult()
if (this.hasListeners()) {
this.#updateTimers()
}
}
#notify(notifyOptions: NotifyOptions): void {
notifyManager.batch(() => {
// First, trigger the listeners
if (notifyOptions.listeners) {
this.listeners.forEach((listener) => {
listener(this.#currentResult)
})
}
// Then the cache listeners
this.#client.getQueryCache().notify({
query: this.#currentQuery,
type: 'observerResultsUpdated',
})
})
}
}
function shouldLoadOnMount(
query: Query<any, any, any, any>,
options: QueryObserverOptions<any, any, any, any>,
): boolean {
return (
resolveEnabled(options.enabled, query) !== false &&
query.state.data === undefined &&
!(query.state.status === 'error' && options.retryOnMount === false)
)
}
function shouldFetchOnMount(
query: Query<any, any, any, any>,
options: QueryObserverOptions<any, any, any, any, any>,
): boolean {
return (
shouldLoadOnMount(query, options) ||
(query.state.data !== undefined &&
shouldFetchOn(query, options, options.refetchOnMount))
)
}
function shouldFetchOn(
query: Query<any, any, any, any>,
options: QueryObserverOptions<any, any, any, any, any>,
field: (typeof options)['refetchOnMount'] &
(typeof options)['refetchOnWindowFocus'] &
(typeof options)['refetchOnReconnect'],
) {
if (resolveEnabled(options.enabled, query) !== false) {
const value = typeof field === 'function' ? field(query) : field
return value === 'always' || (value !== false && isStale(query, options))
}
return false
}
function shouldFetchOptionally(
query: Query<any, any, any, any>,
prevQuery: Query<any, any, any, any>,
options: QueryObserverOptions<any, any, any, any, any>,
prevOptions: QueryObserverOptions<any, any, any, any, any>,
): boolean {
return (
(query !== prevQuery ||
resolveEnabled(prevOptions.enabled, query) === false) &&
(!options.suspense || query.state.status !== 'error') &&
isStale(query, options)
)
}
function isStale(
query: Query<any, any, any, any>,
options: QueryObserverOptions<any, any, any, any, any>,
): boolean {
return (
resolveEnabled(options.enabled, query) !== false &&
query.isStaleByTime(resolveStaleTime(options.staleTime, query))
)
}
// this function would decide if we will update the observer's 'current'
// properties after an optimistic reading via getOptimisticResult
function shouldAssignObserverCurrentProperties<
TQueryFnData = unknown,
TError = unknown,
TData = TQueryFnData,
TQueryData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
>(
observer: QueryObserver<TQueryFnData, TError, TData, TQueryData, TQueryKey>,
optimisticResult: QueryObserverResult<TData, TError>,
) {
// if the newly created result isn't what the observer is holding as current,
// then we'll need to update the properties as well
if (!shallowEqualObjects(observer.getCurrentResult(), optimisticResult)) {
return true
}
// basically, just keep previous properties if nothing changed
return false
}

37
node_modules/@tanstack/query-core/src/removable.ts generated vendored Normal file
View File

@@ -0,0 +1,37 @@
import { isServer, isValidTimeout } from './utils'
export abstract class Removable {
gcTime!: number
#gcTimeout?: ReturnType<typeof setTimeout>
destroy(): void {
this.clearGcTimeout()
}
protected scheduleGc(): void {
this.clearGcTimeout()
if (isValidTimeout(this.gcTime)) {
this.#gcTimeout = setTimeout(() => {
this.optionalRemove()
}, this.gcTime)
}
}
protected updateGcTime(newGcTime: number | undefined): void {
// Default to 5 minutes (Infinity for server-side) if no gcTime is set
this.gcTime = Math.max(
this.gcTime || 0,
newGcTime ?? (isServer ? Infinity : 5 * 60 * 1000),
)
}
protected clearGcTimeout() {
if (this.#gcTimeout) {
clearTimeout(this.#gcTimeout)
this.#gcTimeout = undefined
}
}
protected abstract optionalRemove(): void
}

225
node_modules/@tanstack/query-core/src/retryer.ts generated vendored Normal file
View File

@@ -0,0 +1,225 @@
import { focusManager } from './focusManager'
import { onlineManager } from './onlineManager'
import { pendingThenable } from './thenable'
import { isServer, sleep } from './utils'
import type { CancelOptions, DefaultError, NetworkMode } from './types'
// TYPES
interface RetryerConfig<TData = unknown, TError = DefaultError> {
fn: () => TData | Promise<TData>
initialPromise?: Promise<TData>
abort?: () => void
onError?: (error: TError) => void
onSuccess?: (data: TData) => void
onFail?: (failureCount: number, error: TError) => void
onPause?: () => void
onContinue?: () => void
retry?: RetryValue<TError>
retryDelay?: RetryDelayValue<TError>
networkMode: NetworkMode | undefined
canRun: () => boolean
}
export interface Retryer<TData = unknown> {
promise: Promise<TData>
cancel: (cancelOptions?: CancelOptions) => void
continue: () => Promise<unknown>
cancelRetry: () => void
continueRetry: () => void
canStart: () => boolean
start: () => Promise<TData>
}
export type RetryValue<TError> = boolean | number | ShouldRetryFunction<TError>
type ShouldRetryFunction<TError = DefaultError> = (
failureCount: number,
error: TError,
) => boolean
export type RetryDelayValue<TError> = number | RetryDelayFunction<TError>
type RetryDelayFunction<TError = DefaultError> = (
failureCount: number,
error: TError,
) => number
function defaultRetryDelay(failureCount: number) {
return Math.min(1000 * 2 ** failureCount, 30000)
}
export function canFetch(networkMode: NetworkMode | undefined): boolean {
return (networkMode ?? 'online') === 'online'
? onlineManager.isOnline()
: true
}
export class CancelledError extends Error {
revert?: boolean
silent?: boolean
constructor(options?: CancelOptions) {
super('CancelledError')
this.revert = options?.revert
this.silent = options?.silent
}
}
export function isCancelledError(value: any): value is CancelledError {
return value instanceof CancelledError
}
export function createRetryer<TData = unknown, TError = DefaultError>(
config: RetryerConfig<TData, TError>,
): Retryer<TData> {
let isRetryCancelled = false
let failureCount = 0
let isResolved = false
let continueFn: ((value?: unknown) => void) | undefined
const thenable = pendingThenable<TData>()
const cancel = (cancelOptions?: CancelOptions): void => {
if (!isResolved) {
reject(new CancelledError(cancelOptions))
config.abort?.()
}
}
const cancelRetry = () => {
isRetryCancelled = true
}
const continueRetry = () => {
isRetryCancelled = false
}
const canContinue = () =>
focusManager.isFocused() &&
(config.networkMode === 'always' || onlineManager.isOnline()) &&
config.canRun()
const canStart = () => canFetch(config.networkMode) && config.canRun()
const resolve = (value: any) => {
if (!isResolved) {
isResolved = true
config.onSuccess?.(value)
continueFn?.()
thenable.resolve(value)
}
}
const reject = (value: any) => {
if (!isResolved) {
isResolved = true
config.onError?.(value)
continueFn?.()
thenable.reject(value)
}
}
const pause = () => {
return new Promise((continueResolve) => {
continueFn = (value) => {
if (isResolved || canContinue()) {
continueResolve(value)
}
}
config.onPause?.()
}).then(() => {
continueFn = undefined
if (!isResolved) {
config.onContinue?.()
}
})
}
// Create loop function
const run = () => {
// Do nothing if already resolved
if (isResolved) {
return
}
let promiseOrValue: any
// we can re-use config.initialPromise on the first call of run()
const initialPromise =
failureCount === 0 ? config.initialPromise : undefined
// Execute query
try {
promiseOrValue = initialPromise ?? config.fn()
} catch (error) {
promiseOrValue = Promise.reject(error)
}
Promise.resolve(promiseOrValue)
.then(resolve)
.catch((error) => {
// Stop if the fetch is already resolved
if (isResolved) {
return
}
// Do we need to retry the request?
const retry = config.retry ?? (isServer ? 0 : 3)
const retryDelay = config.retryDelay ?? defaultRetryDelay
const delay =
typeof retryDelay === 'function'
? retryDelay(failureCount, error)
: retryDelay
const shouldRetry =
retry === true ||
(typeof retry === 'number' && failureCount < retry) ||
(typeof retry === 'function' && retry(failureCount, error))
if (isRetryCancelled || !shouldRetry) {
// We are done if the query does not need to be retried
reject(error)
return
}
failureCount++
// Notify on fail
config.onFail?.(failureCount, error)
// Delay
sleep(delay)
// Pause if the document is not visible or when the device is offline
.then(() => {
return canContinue() ? undefined : pause()
})
.then(() => {
if (isRetryCancelled) {
reject(error)
} else {
run()
}
})
})
}
return {
promise: thenable,
cancel,
continue: () => {
continueFn?.()
return thenable
},
cancelRetry,
continueRetry,
canStart,
start: () => {
// Start loop
if (canStart()) {
run()
} else {
pause().then(run)
}
return thenable
},
}
}

30
node_modules/@tanstack/query-core/src/subscribable.ts generated vendored Normal file
View File

@@ -0,0 +1,30 @@
export class Subscribable<TListener extends Function> {
protected listeners = new Set<TListener>()
constructor() {
this.subscribe = this.subscribe.bind(this)
}
subscribe(listener: TListener): () => void {
this.listeners.add(listener)
this.onSubscribe()
return () => {
this.listeners.delete(listener)
this.onUnsubscribe()
}
}
hasListeners(): boolean {
return this.listeners.size > 0
}
protected onSubscribe(): void {
// Do nothing
}
protected onUnsubscribe(): void {
// Do nothing
}
}

82
node_modules/@tanstack/query-core/src/thenable.ts generated vendored Normal file
View File

@@ -0,0 +1,82 @@
/**
* Thenable types which matches React's types for promises
*
* React seemingly uses `.status`, `.value` and `.reason` properties on a promises to optimistically unwrap data from promises
*
* @see https://github.com/facebook/react/blob/main/packages/shared/ReactTypes.js#L112-L138
* @see https://github.com/facebook/react/blob/4f604941569d2e8947ce1460a0b2997e835f37b9/packages/react-debug-tools/src/ReactDebugHooks.js#L224-L227
*/
interface Fulfilled<T> {
status: 'fulfilled'
value: T
}
interface Rejected {
status: 'rejected'
reason: unknown
}
interface Pending<T> {
status: 'pending'
/**
* Resolve the promise with a value.
* Will remove the `resolve` and `reject` properties from the promise.
*/
resolve: (value: T) => void
/**
* Reject the promise with a reason.
* Will remove the `resolve` and `reject` properties from the promise.
*/
reject: (reason: unknown) => void
}
export type FulfilledThenable<T> = Promise<T> & Fulfilled<T>
export type RejectedThenable<T> = Promise<T> & Rejected
export type PendingThenable<T> = Promise<T> & Pending<T>
export type Thenable<T> =
| FulfilledThenable<T>
| RejectedThenable<T>
| PendingThenable<T>
export function pendingThenable<T>(): PendingThenable<T> {
let resolve: Pending<T>['resolve']
let reject: Pending<T>['reject']
// this could use `Promise.withResolvers()` in the future
const thenable = new Promise((_resolve, _reject) => {
resolve = _resolve
reject = _reject
}) as PendingThenable<T>
thenable.status = 'pending'
thenable.catch(() => {
// prevent unhandled rejection errors
})
function finalize(data: Fulfilled<T> | Rejected) {
Object.assign(thenable, data)
// clear pending props props to avoid calling them twice
delete (thenable as Partial<PendingThenable<T>>).resolve
delete (thenable as Partial<PendingThenable<T>>).reject
}
thenable.resolve = (value) => {
finalize({
status: 'fulfilled',
value,
})
resolve(value)
}
thenable.reject = (reason) => {
finalize({
status: 'rejected',
reason,
})
reject(reason)
}
return thenable
}

1244
node_modules/@tanstack/query-core/src/types.ts generated vendored Normal file

File diff suppressed because it is too large Load Diff

422
node_modules/@tanstack/query-core/src/utils.ts generated vendored Normal file
View File

@@ -0,0 +1,422 @@
import type {
DefaultError,
Enabled,
FetchStatus,
MutationKey,
MutationStatus,
QueryFunction,
QueryKey,
QueryOptions,
StaleTime,
} from './types'
import type { Mutation } from './mutation'
import type { FetchOptions, Query } from './query'
// TYPES
export interface QueryFilters {
/**
* Filter to active queries, inactive queries or all queries
*/
type?: QueryTypeFilter
/**
* Match query key exactly
*/
exact?: boolean
/**
* Include queries matching this predicate function
*/
predicate?: (query: Query) => boolean
/**
* Include queries matching this query key
*/
queryKey?: QueryKey
/**
* Include or exclude stale queries
*/
stale?: boolean
/**
* Include queries matching their fetchStatus
*/
fetchStatus?: FetchStatus
}
export interface MutationFilters {
/**
* Match mutation key exactly
*/
exact?: boolean
/**
* Include mutations matching this predicate function
*/
predicate?: (mutation: Mutation<any, any, any>) => boolean
/**
* Include mutations matching this mutation key
*/
mutationKey?: MutationKey
/**
* Filter by mutation status
*/
status?: MutationStatus
}
export type Updater<TInput, TOutput> = TOutput | ((input: TInput) => TOutput)
export type QueryTypeFilter = 'all' | 'active' | 'inactive'
// UTILS
export const isServer = typeof window === 'undefined' || 'Deno' in globalThis
export function noop(): undefined {
return undefined
}
export function functionalUpdate<TInput, TOutput>(
updater: Updater<TInput, TOutput>,
input: TInput,
): TOutput {
return typeof updater === 'function'
? (updater as (_: TInput) => TOutput)(input)
: updater
}
export function isValidTimeout(value: unknown): value is number {
return typeof value === 'number' && value >= 0 && value !== Infinity
}
export function timeUntilStale(updatedAt: number, staleTime?: number): number {
return Math.max(updatedAt + (staleTime || 0) - Date.now(), 0)
}
export function resolveStaleTime<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
>(
staleTime: undefined | StaleTime<TQueryFnData, TError, TData, TQueryKey>,
query: Query<TQueryFnData, TError, TData, TQueryKey>,
): number | undefined {
return typeof staleTime === 'function' ? staleTime(query) : staleTime
}
export function resolveEnabled<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
>(
enabled: undefined | Enabled<TQueryFnData, TError, TData, TQueryKey>,
query: Query<TQueryFnData, TError, TData, TQueryKey>,
): boolean | undefined {
return typeof enabled === 'function' ? enabled(query) : enabled
}
export function matchQuery(
filters: QueryFilters,
query: Query<any, any, any, any>,
): boolean {
const {
type = 'all',
exact,
fetchStatus,
predicate,
queryKey,
stale,
} = filters
if (queryKey) {
if (exact) {
if (query.queryHash !== hashQueryKeyByOptions(queryKey, query.options)) {
return false
}
} else if (!partialMatchKey(query.queryKey, queryKey)) {
return false
}
}
if (type !== 'all') {
const isActive = query.isActive()
if (type === 'active' && !isActive) {
return false
}
if (type === 'inactive' && isActive) {
return false
}
}
if (typeof stale === 'boolean' && query.isStale() !== stale) {
return false
}
if (fetchStatus && fetchStatus !== query.state.fetchStatus) {
return false
}
if (predicate && !predicate(query)) {
return false
}
return true
}
export function matchMutation(
filters: MutationFilters,
mutation: Mutation<any, any>,
): boolean {
const { exact, status, predicate, mutationKey } = filters
if (mutationKey) {
if (!mutation.options.mutationKey) {
return false
}
if (exact) {
if (hashKey(mutation.options.mutationKey) !== hashKey(mutationKey)) {
return false
}
} else if (!partialMatchKey(mutation.options.mutationKey, mutationKey)) {
return false
}
}
if (status && mutation.state.status !== status) {
return false
}
if (predicate && !predicate(mutation)) {
return false
}
return true
}
export function hashQueryKeyByOptions<TQueryKey extends QueryKey = QueryKey>(
queryKey: TQueryKey,
options?: Pick<QueryOptions<any, any, any, any>, 'queryKeyHashFn'>,
): string {
const hashFn = options?.queryKeyHashFn || hashKey
return hashFn(queryKey)
}
/**
* Default query & mutation keys hash function.
* Hashes the value into a stable hash.
*/
export function hashKey(queryKey: QueryKey | MutationKey): string {
return JSON.stringify(queryKey, (_, val) =>
isPlainObject(val)
? Object.keys(val)
.sort()
.reduce((result, key) => {
result[key] = val[key]
return result
}, {} as any)
: val,
)
}
/**
* Checks if key `b` partially matches with key `a`.
*/
export function partialMatchKey(a: QueryKey, b: QueryKey): boolean
export function partialMatchKey(a: any, b: any): boolean {
if (a === b) {
return true
}
if (typeof a !== typeof b) {
return false
}
if (a && b && typeof a === 'object' && typeof b === 'object') {
return !Object.keys(b).some((key) => !partialMatchKey(a[key], b[key]))
}
return false
}
/**
* This function returns `a` if `b` is deeply equal.
* If not, it will replace any deeply equal children of `b` with those of `a`.
* This can be used for structural sharing between JSON values for example.
*/
export function replaceEqualDeep<T>(a: unknown, b: T): T
export function replaceEqualDeep(a: any, b: any): any {
if (a === b) {
return a
}
const array = isPlainArray(a) && isPlainArray(b)
if (array || (isPlainObject(a) && isPlainObject(b))) {
const aItems = array ? a : Object.keys(a)
const aSize = aItems.length
const bItems = array ? b : Object.keys(b)
const bSize = bItems.length
const copy: any = array ? [] : {}
let equalItems = 0
for (let i = 0; i < bSize; i++) {
const key = array ? i : bItems[i]
if (
((!array && aItems.includes(key)) || array) &&
a[key] === undefined &&
b[key] === undefined
) {
copy[key] = undefined
equalItems++
} else {
copy[key] = replaceEqualDeep(a[key], b[key])
if (copy[key] === a[key] && a[key] !== undefined) {
equalItems++
}
}
}
return aSize === bSize && equalItems === aSize ? a : copy
}
return b
}
/**
* Shallow compare objects.
*/
export function shallowEqualObjects<T extends Record<string, any>>(
a: T,
b: T | undefined,
): boolean {
if (!b || Object.keys(a).length !== Object.keys(b).length) {
return false
}
for (const key in a) {
if (a[key] !== b[key]) {
return false
}
}
return true
}
export function isPlainArray(value: unknown) {
return Array.isArray(value) && value.length === Object.keys(value).length
}
// Copied from: https://github.com/jonschlinkert/is-plain-object
// eslint-disable-next-line @typescript-eslint/no-wrapper-object-types
export function isPlainObject(o: any): o is Object {
if (!hasObjectPrototype(o)) {
return false
}
// If has no constructor
const ctor = o.constructor
if (ctor === undefined) {
return true
}
// If has modified prototype
const prot = ctor.prototype
if (!hasObjectPrototype(prot)) {
return false
}
// If constructor does not have an Object-specific method
if (!prot.hasOwnProperty('isPrototypeOf')) {
return false
}
// Handles Objects created by Object.create(<arbitrary prototype>)
if (Object.getPrototypeOf(o) !== Object.prototype) {
return false
}
// Most likely a plain Object
return true
}
function hasObjectPrototype(o: any): boolean {
return Object.prototype.toString.call(o) === '[object Object]'
}
export function sleep(timeout: number): Promise<void> {
return new Promise((resolve) => {
setTimeout(resolve, timeout)
})
}
export function replaceData<
TData,
TOptions extends QueryOptions<any, any, any, any>,
>(prevData: TData | undefined, data: TData, options: TOptions): TData {
if (typeof options.structuralSharing === 'function') {
return options.structuralSharing(prevData, data) as TData
} else if (options.structuralSharing !== false) {
if (process.env.NODE_ENV !== 'production') {
try {
return replaceEqualDeep(prevData, data)
} catch (error) {
console.error(
`Structural sharing requires data to be JSON serializable. To fix this, turn off structuralSharing or return JSON-serializable data from your queryFn. [${options.queryHash}]: ${error}`,
)
}
}
// Structurally share data between prev and new data if needed
return replaceEqualDeep(prevData, data)
}
return data
}
export function keepPreviousData<T>(
previousData: T | undefined,
): T | undefined {
return previousData
}
export function addToEnd<T>(items: Array<T>, item: T, max = 0): Array<T> {
const newItems = [...items, item]
return max && newItems.length > max ? newItems.slice(1) : newItems
}
export function addToStart<T>(items: Array<T>, item: T, max = 0): Array<T> {
const newItems = [item, ...items]
return max && newItems.length > max ? newItems.slice(0, -1) : newItems
}
export const skipToken = Symbol()
export type SkipToken = typeof skipToken
export function ensureQueryFn<
TQueryFnData = unknown,
TQueryKey extends QueryKey = QueryKey,
>(
options: {
queryFn?: QueryFunction<TQueryFnData, TQueryKey> | SkipToken
queryHash?: string
},
fetchOptions?: FetchOptions<TQueryFnData>,
): QueryFunction<TQueryFnData, TQueryKey> {
if (process.env.NODE_ENV !== 'production') {
if (options.queryFn === skipToken) {
console.error(
`Attempted to invoke queryFn when set to skipToken. This is likely a configuration error. Query hash: '${options.queryHash}'`,
)
}
}
// if we attempt to retry a fetch that was triggered from an initialPromise
// when we don't have a queryFn yet, we can't retry, so we just return the already rejected initialPromise
// if an observer has already mounted, we will be able to retry with that queryFn
if (!options.queryFn && fetchOptions?.initialPromise) {
return () => fetchOptions.initialPromise!
}
if (!options.queryFn || options.queryFn === skipToken) {
return () =>
Promise.reject(new Error(`Missing queryFn: '${options.queryHash}'`))
}
return options.queryFn
}