Initial commit: Crypto trader application

This commit is contained in:
2025-12-25 20:20:40 -05:00
commit 07a04c1bb8
47895 changed files with 2042266 additions and 0 deletions

21
frontend/node_modules/@tanstack/query-core/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021-present Tanner Linsley
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,65 @@
{
"name": "@tanstack/query-core",
"version": "5.90.12",
"description": "The framework agnostic core that powers TanStack Query",
"author": "tannerlinsley",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/TanStack/query.git",
"directory": "packages/query-core"
},
"homepage": "https://tanstack.com/query",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"type": "module",
"types": "build/legacy/index.d.ts",
"main": "build/legacy/index.cjs",
"module": "build/legacy/index.js",
"react-native": "src/index.ts",
"exports": {
".": {
"@tanstack/custom-condition": "./src/index.ts",
"import": {
"types": "./build/modern/index.d.ts",
"default": "./build/modern/index.js"
},
"require": {
"types": "./build/modern/index.d.cts",
"default": "./build/modern/index.cjs"
}
},
"./package.json": "./package.json"
},
"sideEffects": false,
"files": [
"build",
"src",
"!src/__tests__"
],
"devDependencies": {
"npm-run-all2": "^5.0.0",
"@tanstack/query-test-utils": "0.0.0"
},
"scripts": {
"clean": "premove ./build ./coverage ./dist-ts",
"compile": "tsc --build",
"test:eslint": "eslint --concurrency=auto ./src",
"test:types": "npm-run-all --serial test:types:*",
"test:types:ts50": "node ../../node_modules/typescript50/lib/tsc.js --build tsconfig.legacy.json",
"test:types:ts51": "node ../../node_modules/typescript51/lib/tsc.js --build tsconfig.legacy.json",
"test:types:ts52": "node ../../node_modules/typescript52/lib/tsc.js --build tsconfig.legacy.json",
"test:types:ts53": "node ../../node_modules/typescript53/lib/tsc.js --build tsconfig.legacy.json",
"test:types:ts54": "node ../../node_modules/typescript54/lib/tsc.js --build tsconfig.legacy.json",
"test:types:ts55": "node ../../node_modules/typescript55/lib/tsc.js --build tsconfig.legacy.json",
"test:types:ts56": "node ../../node_modules/typescript56/lib/tsc.js --build tsconfig.legacy.json",
"test:types:ts57": "node ../../node_modules/typescript57/lib/tsc.js --build tsconfig.legacy.json",
"test:types:tscurrent": "tsc --build",
"test:lib": "vitest",
"test:lib:dev": "pnpm run test:lib --watch",
"test:build": "publint --strict && attw --pack",
"build": "tsup --tsconfig tsconfig.prod.json"
}
}

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()

View File

@@ -0,0 +1,285 @@
import { tryResolveSync } from './thenable'
import { noop } from './utils'
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
shouldRedactErrors?: (error: unknown) => 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
// This is only optional because older versions of Query might have dehydrated
// without it which we need to handle for backwards compatibility.
// This should be changed to required in the future.
dehydratedAt?: number
}
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,
shouldRedactErrors: (error: unknown) => boolean,
): DehydratedQuery {
const dehydratePromise = () => {
const promise = query.promise?.then(serializeData).catch((error) => {
if (!shouldRedactErrors(error)) {
// Reject original error if it should not be redacted
return Promise.reject(error)
}
// If not in production, log original error before rejecting redacted 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'))
})
// Avoid unhandled promise rejections
// We need the promise we dehydrate to reject to get the correct result into
// the query cache, but we also want to avoid unhandled promise rejections
// in whatever environment the prefetches are happening in.
promise?.catch(noop)
return promise
}
return {
dehydratedAt: Date.now(),
state: {
...query.state,
...(query.state.data !== undefined && {
data: serializeData(query.state.data),
}),
},
queryKey: query.queryKey,
queryHash: query.queryHash,
...(query.state.status === 'pending' && {
promise: dehydratePromise(),
}),
...(query.meta && { meta: query.meta }),
}
}
export function defaultShouldDehydrateMutation(mutation: Mutation) {
return mutation.state.isPaused
}
export function defaultShouldDehydrateQuery(query: Query) {
return query.state.status === 'success'
}
function defaultShouldRedactErrors(_: unknown) {
return true
}
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 shouldRedactErrors =
options.shouldRedactErrors ??
client.getDefaultOptions().dehydrate?.shouldRedactErrors ??
defaultShouldRedactErrors
const serializeData =
options.serializeData ??
client.getDefaultOptions().dehydrate?.serializeData ??
defaultTransformerFn
const queries = client
.getQueryCache()
.getAll()
.flatMap((query) =>
filterQuery(query)
? [dehydrateQuery(query, serializeData, shouldRedactErrors)]
: [],
)
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, dehydratedAt }) => {
const syncData = promise ? tryResolveSync(promise) : undefined
const rawData = state.data === undefined ? syncData?.data : state.data
const data = rawData === undefined ? rawData : deserializeData(rawData)
let query = queryCache.get(queryHash)
const existingQueryIsPending = query?.state.status === 'pending'
const existingQueryIsFetching = query?.state.fetchStatus === 'fetching'
// Do not hydrate if an existing query exists with newer data
if (query) {
const hasNewerSyncData =
syncData &&
// We only need this undefined check to handle older dehydration
// payloads that might not have dehydratedAt
dehydratedAt !== undefined &&
dehydratedAt > query.state.dataUpdatedAt
if (
state.dataUpdatedAt > query.state.dataUpdatedAt ||
hasNewerSyncData
) {
// 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',
status: data !== undefined ? 'success' : state.status,
},
)
}
if (
promise &&
!existingQueryIsPending &&
!existingQueryIsFetching &&
// Only hydrate if dehydration is newer than any existing data,
// this is always true for new queries
(dehydratedAt === undefined || dehydratedAt > query.state.dataUpdatedAt)
) {
// This doesn't actually fetch - it just creates a retryer
// which will re-use the passed `initialPromise`
// Note that we need to call these even when data was synchronously
// available, as we still need to set up the retryer
query
.fetch(undefined, {
// RSC transformed promises are not thenable
initialPromise: Promise.resolve(promise).then(deserializeData),
})
// Avoid unhandled promise rejections
.catch(noop)
}
},
)
}

View File

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

View File

@@ -0,0 +1,179 @@
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 createQueryFnContext = () => {
const queryFnContext: OmitKeyof<
QueryFunctionContext<QueryKey, unknown>,
'signal'
> = {
client: context.client,
queryKey: context.queryKey,
pageParam: param,
direction: previous ? 'backward' : 'forward',
meta: context.options.meta,
}
addSignalProperty(queryFnContext)
return queryFnContext as QueryFunctionContext<QueryKey, unknown>
}
const queryFnContext = createQueryFnContext()
const page = await queryFn(queryFnContext)
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,
{
client: context.client,
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,190 @@
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 { Query } from './query'
type InfiniteQueryObserverListener<TData, TError> = (
result: InfiniteQueryObserverResult<TData, TError>,
) => void
export class InfiniteQueryObserver<
TQueryFnData = unknown,
TError = DefaultError,
TData = InfiniteData<TQueryFnData>,
TQueryKey extends QueryKey = QueryKey,
TPageParam = unknown,
> extends QueryObserver<
TQueryFnData,
TError,
TData,
InfiniteData<TQueryFnData, TPageParam>,
TQueryKey
> {
// Type override
subscribe!: Subscribable<
InfiniteQueryObserverListener<TData, TError>
>['subscribe']
// Type override
getCurrentResult!: ReplaceReturnType<
QueryObserver<
TQueryFnData,
TError,
TData,
InfiniteData<TQueryFnData, TPageParam>,
TQueryKey
>['getCurrentResult'],
InfiniteQueryObserverResult<TData, TError>
>
// Type override
protected fetch!: ReplaceReturnType<
QueryObserver<
TQueryFnData,
TError,
TData,
InfiniteData<TQueryFnData, TPageParam>,
TQueryKey
>['fetch'],
Promise<InfiniteQueryObserverResult<TData, TError>>
>
constructor(
client: QueryClient,
options: InfiniteQueryObserverOptions<
TQueryFnData,
TError,
TData,
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,
TQueryKey,
TPageParam
>,
): void {
super.setOptions({
...options,
behavior: infiniteQueryBehavior(),
})
}
getOptimisticResult(
options: DefaultedInfiniteQueryObserverOptions<
TQueryFnData,
TError,
TData,
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<TQueryFnData, TPageParam>,
TQueryKey
>,
options: InfiniteQueryObserverOptions<
TQueryFnData,
TError,
TData,
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

View File

@@ -0,0 +1,403 @@
import { notifyManager } from './notifyManager'
import { Removable } from './removable'
import { createRetryer } from './retryer'
import type {
DefaultError,
MutationFunctionContext,
MutationMeta,
MutationOptions,
MutationStatus,
} from './types'
import type { MutationCache } from './mutationCache'
import type { MutationObserver } from './mutationObserver'
import type { Retryer } from './retryer'
import type { QueryClient } from './queryClient'
// TYPES
interface MutationConfig<TData, TError, TVariables, TOnMutateResult> {
client: QueryClient
mutationId: number
mutationCache: MutationCache
options: MutationOptions<TData, TError, TVariables, TOnMutateResult>
state?: MutationState<TData, TError, TVariables, TOnMutateResult>
}
export interface MutationState<
TData = unknown,
TError = DefaultError,
TVariables = unknown,
TOnMutateResult = unknown,
> {
context: TOnMutateResult | 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, TOnMutateResult> {
type: 'pending'
isPaused: boolean
variables?: TVariables
context?: TOnMutateResult
}
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, TOnMutateResult> =
| ContinueAction
| ErrorAction<TError>
| FailedAction<TError>
| PendingAction<TVariables, TOnMutateResult>
| PauseAction
| SuccessAction<TData>
// CLASS
export class Mutation<
TData = unknown,
TError = DefaultError,
TVariables = unknown,
TOnMutateResult = unknown,
> extends Removable {
state: MutationState<TData, TError, TVariables, TOnMutateResult>
options!: MutationOptions<TData, TError, TVariables, TOnMutateResult>
readonly mutationId: number
#client: QueryClient
#observers: Array<
MutationObserver<TData, TError, TVariables, TOnMutateResult>
>
#mutationCache: MutationCache
#retryer?: Retryer<TData>
constructor(
config: MutationConfig<TData, TError, TVariables, TOnMutateResult>,
) {
super()
this.#client = config.client
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, TOnMutateResult>,
): 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> {
const onContinue = () => {
this.#dispatch({ type: 'continue' })
}
const mutationFnContext = {
client: this.#client,
meta: this.options.meta,
mutationKey: this.options.mutationKey,
} satisfies MutationFunctionContext
this.#retryer = createRetryer({
fn: () => {
if (!this.options.mutationFn) {
return Promise.reject(new Error('No mutationFn found'))
}
return this.options.mutationFn(variables, mutationFnContext)
},
onFail: (failureCount, error) => {
this.#dispatch({ type: 'failed', failureCount, error })
},
onPause: () => {
this.#dispatch({ type: 'pause' })
},
onContinue,
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) {
// Dispatch continue action to unpause restored mutation
onContinue()
} else {
this.#dispatch({ type: 'pending', variables, isPaused })
// Notify cache callback
await this.#mutationCache.config.onMutate?.(
variables,
this as Mutation<unknown, unknown, unknown, unknown>,
mutationFnContext,
)
const context = await this.options.onMutate?.(
variables,
mutationFnContext,
)
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>,
mutationFnContext,
)
await this.options.onSuccess?.(
data,
variables,
this.state.context!,
mutationFnContext,
)
// Notify cache callback
await this.#mutationCache.config.onSettled?.(
data,
null,
this.state.variables,
this.state.context,
this as Mutation<unknown, unknown, unknown, unknown>,
mutationFnContext,
)
await this.options.onSettled?.(
data,
null,
variables,
this.state.context,
mutationFnContext,
)
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>,
mutationFnContext,
)
await this.options.onError?.(
error as TError,
variables,
this.state.context,
mutationFnContext,
)
// 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>,
mutationFnContext,
)
await this.options.onSettled?.(
undefined,
error as TError,
variables,
this.state.context,
mutationFnContext,
)
throw error
} finally {
this.#dispatch({ type: 'error', error: error as TError })
}
} finally {
this.#mutationCache.runNext(this)
}
}
#dispatch(action: Action<TData, TError, TVariables, TOnMutateResult>): void {
const reducer = (
state: MutationState<TData, TError, TVariables, TOnMutateResult>,
): MutationState<TData, TError, TVariables, TOnMutateResult> => {
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,
TOnMutateResult,
>(): MutationState<TData, TError, TVariables, TOnMutateResult> {
return {
context: undefined,
data: undefined,
error: null,
failureCount: 0,
failureReason: null,
isPaused: false,
status: 'idle',
variables: undefined,
submittedAt: 0,
}
}

View File

@@ -0,0 +1,244 @@
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,
MutationFunctionContext,
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,
onMutateResult: unknown,
mutation: Mutation<unknown, unknown, unknown>,
context: MutationFunctionContext,
) => Promise<unknown> | unknown
onSuccess?: (
data: unknown,
variables: unknown,
onMutateResult: unknown,
mutation: Mutation<unknown, unknown, unknown>,
context: MutationFunctionContext,
) => Promise<unknown> | unknown
onMutate?: (
variables: unknown,
mutation: Mutation<unknown, unknown, unknown>,
context: MutationFunctionContext,
) => Promise<unknown> | unknown
onSettled?: (
data: unknown | undefined,
error: DefaultError | null,
variables: unknown,
onMutateResult: unknown,
mutation: Mutation<unknown, unknown, unknown>,
context: MutationFunctionContext,
) => 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: Set<Mutation<any, any, any, any>>
#scopes: Map<string, Array<Mutation<any, any, any, any>>>
#mutationId: number
constructor(public config: MutationCacheConfig = {}) {
super()
this.#mutations = new Set()
this.#scopes = new Map()
this.#mutationId = 0
}
build<TData, TError, TVariables, TOnMutateResult>(
client: QueryClient,
options: MutationOptions<TData, TError, TVariables, TOnMutateResult>,
state?: MutationState<TData, TError, TVariables, TOnMutateResult>,
): Mutation<TData, TError, TVariables, TOnMutateResult> {
const mutation = new Mutation({
client,
mutationCache: this,
mutationId: ++this.#mutationId,
options: client.defaultMutationOptions(options),
state,
})
this.add(mutation)
return mutation
}
add(mutation: Mutation<any, any, any, any>): void {
this.#mutations.add(mutation)
const scope = scopeFor(mutation)
if (typeof scope === 'string') {
const scopedMutations = this.#scopes.get(scope)
if (scopedMutations) {
scopedMutations.push(mutation)
} else {
this.#scopes.set(scope, [mutation])
}
}
this.notify({ type: 'added', mutation })
}
remove(mutation: Mutation<any, any, any, any>): void {
if (this.#mutations.delete(mutation)) {
const scope = scopeFor(mutation)
if (typeof scope === 'string') {
const scopedMutations = this.#scopes.get(scope)
if (scopedMutations) {
if (scopedMutations.length > 1) {
const index = scopedMutations.indexOf(mutation)
if (index !== -1) {
scopedMutations.splice(index, 1)
}
} else if (scopedMutations[0] === mutation) {
this.#scopes.delete(scope)
}
}
}
}
// Currently we notify the removal even if the mutation was already removed.
// Consider making this an error or not notifying of the removal depending on the desired semantics.
this.notify({ type: 'removed', mutation })
}
canRun(mutation: Mutation<any, any, any, any>): boolean {
const scope = scopeFor(mutation)
if (typeof scope === 'string') {
const mutationsWithSameScope = this.#scopes.get(scope)
const firstPendingMutation = mutationsWithSameScope?.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
} else {
// For unscoped mutations there are never any pending mutations in front of the
// current mutation
return true
}
}
runNext(mutation: Mutation<any, any, any, any>): Promise<unknown> {
const scope = scopeFor(mutation)
if (typeof scope === 'string') {
const foundMutation = this.#scopes
.get(scope)
?.find((m) => m !== mutation && m.state.isPaused)
return foundMutation?.continue() ?? Promise.resolve()
} else {
return Promise.resolve()
}
}
clear(): void {
notifyManager.batch(() => {
this.#mutations.forEach((mutation) => {
this.notify({ type: 'removed', mutation })
})
this.#mutations.clear()
this.#scopes.clear()
})
}
getAll(): Array<Mutation> {
return Array.from(this.#mutations)
}
find<
TData = unknown,
TError = DefaultError,
TVariables = any,
TOnMutateResult = unknown,
>(
filters: MutationFilters,
): Mutation<TData, TError, TVariables, TOnMutateResult> | undefined {
const defaultedFilters = { exact: true, ...filters }
return this.getAll().find((mutation) =>
matchMutation(defaultedFilters, mutation),
) as Mutation<TData, TError, TVariables, TOnMutateResult> | 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
}

View File

@@ -0,0 +1,211 @@
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,
MutationFunctionContext,
MutationObserverOptions,
MutationObserverResult,
} from './types'
import type { Action, Mutation } from './mutation'
// TYPES
type MutationObserverListener<TData, TError, TVariables, TOnMutateResult> = (
result: MutationObserverResult<TData, TError, TVariables, TOnMutateResult>,
) => void
// CLASS
export class MutationObserver<
TData = unknown,
TError = DefaultError,
TVariables = void,
TOnMutateResult = unknown,
> extends Subscribable<
MutationObserverListener<TData, TError, TVariables, TOnMutateResult>
> {
options!: MutationObserverOptions<TData, TError, TVariables, TOnMutateResult>
#client: QueryClient
#currentResult: MutationObserverResult<
TData,
TError,
TVariables,
TOnMutateResult
> = undefined!
#currentMutation?: Mutation<TData, TError, TVariables, TOnMutateResult>
#mutateOptions?: MutateOptions<TData, TError, TVariables, TOnMutateResult>
constructor(
client: QueryClient,
options: MutationObserverOptions<
TData,
TError,
TVariables,
TOnMutateResult
>,
) {
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,
TOnMutateResult
>,
) {
const prevOptions = this.options as
| MutationObserverOptions<TData, TError, TVariables, TOnMutateResult>
| 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, TOnMutateResult>,
): void {
this.#updateResult()
this.#notify(action)
}
getCurrentResult(): MutationObserverResult<
TData,
TError,
TVariables,
TOnMutateResult
> {
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, TOnMutateResult>,
): 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, TOnMutateResult>()
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, TOnMutateResult>
}
#notify(action?: Action<TData, TError, TVariables, TOnMutateResult>): void {
notifyManager.batch(() => {
// First trigger the mutate callbacks
if (this.#mutateOptions && this.hasListeners()) {
const variables = this.#currentResult.variables!
const onMutateResult = this.#currentResult.context
const context = {
client: this.#client,
meta: this.options.meta,
mutationKey: this.options.mutationKey,
} satisfies MutationFunctionContext
if (action?.type === 'success') {
this.#mutateOptions.onSuccess?.(
action.data,
variables,
onMutateResult,
context,
)
this.#mutateOptions.onSettled?.(
action.data,
null,
variables,
onMutateResult,
context,
)
} else if (action?.type === 'error') {
this.#mutateOptions.onError?.(
action.error,
variables,
onMutateResult,
context,
)
this.#mutateOptions.onSettled?.(
undefined,
action.error,
variables,
onMutateResult,
context,
)
}
}
// Then trigger the listeners
this.listeners.forEach((listener) => {
listener(this.#currentResult)
})
})
}
}

View File

@@ -0,0 +1,99 @@
// TYPES
import { systemSetTimeoutZero } from './timeoutManager'
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 const defaultScheduler: ScheduleFunction = systemSetTimeoutZero
export function createNotifyManager() {
let queue: Array<NotifyCallback> = []
let transactions = 0
let notifyFn: NotifyFunction = (callback) => {
callback()
}
let batchNotifyFn: BatchNotifyFunction = (callback: () => void) => {
callback()
}
let scheduleFn = defaultScheduler
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()

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,298 @@
import { notifyManager } from './notifyManager'
import { QueryObserver } from './queryObserver'
import { Subscribable } from './subscribable'
import { replaceEqualDeep, shallowEqualObjects } from './utils'
import type {
DefaultedQueryObserverOptions,
QueryObserverOptions,
QueryObserverResult,
} from './types'
import type { QueryClient } from './queryClient'
function difference<T>(array1: Array<T>, array2: Array<T>): Array<T> {
const excludeSet = new Set(array2)
return array1.filter((x) => !excludeSet.has(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>
#observerMatches: Array<QueryObserverMatch> = []
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>,
): void {
this.#queries = queries
this.#options = options
if (process.env.NODE_ENV !== 'production') {
const queryHashes = queries.map(
(query) => this.#client.defaultQueryOptions(query).queryHash,
)
if (new Set(queryHashes).size !== queryHashes.length) {
console.warn(
'[QueriesObserver]: Duplicate Queries found. This might result in unexpected behavior.',
)
}
}
notifyManager.batch(() => {
const prevObservers = this.#observers
const newObserverMatches = this.#findMatchingObservers(this.#queries)
this.#observerMatches = newObserverMatches
// set options for the new observers to notify of changes
newObserverMatches.forEach((match) =>
match.observer.setOptions(match.defaultedQueryOptions),
)
const newObservers = newObserverMatches.map((match) => match.observer)
const newResult = newObservers.map((observer) =>
observer.getCurrentResult(),
)
const hasLengthChange = prevObservers.length !== newObservers.length
const hasIndexChange = newObservers.some(
(observer, index) => observer !== prevObservers[index],
)
const hasStructuralChange = hasLengthChange || hasIndexChange
const hasResultChange = hasStructuralChange
? true
: newResult.some((result, index) => {
const prev = this.#result[index]
return !prev || !shallowEqualObjects(result, prev)
})
if (!hasStructuralChange && !hasResultChange) return
if (hasStructuralChange) {
this.#observers = newObservers
}
this.#result = newResult
if (!this.hasListeners()) return
if (hasStructuralChange) {
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 this.#trackResult(result, matches)
},
]
}
#trackResult(
result: Array<QueryObserverResult>,
matches: Array<QueryObserverMatch>,
) {
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<string, Array<QueryObserver>>()
this.#observers.forEach((observer) => {
const key = observer.options.queryHash
if (!key) return
const previousObservers = prevObserversMap.get(key)
if (previousObservers) {
previousObservers.push(observer)
} else {
prevObserversMap.set(key, [observer])
}
})
const observers: Array<QueryObserverMatch> = []
queries.forEach((options) => {
const defaultedOptions = this.#client.defaultQueryOptions(options)
const match = prevObserversMap.get(defaultedOptions.queryHash)?.shift()
const observer =
match ?? new QueryObserver(this.#client, defaultedOptions)
observers.push({
defaultedQueryOptions: defaultedOptions,
observer,
})
})
return observers
}
#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 newTracked = this.#trackResult(this.#result, this.#observerMatches)
const newResult = this.#combineResult(newTracked, this.#options?.combine)
if (previousResult !== newResult) {
notifyManager.batch(() => {
this.listeners.forEach((listener) => {
listener(this.#result)
})
})
}
}
}
}
type QueryObserverMatch = {
defaultedQueryOptions: DefaultedQueryObserverOptions
observer: QueryObserver
}

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

@@ -0,0 +1,753 @@
import {
ensureQueryFn,
noop,
replaceData,
resolveEnabled,
resolveStaleTime,
skipToken,
timeUntilStale,
} from './utils'
import { notifyManager } from './notifyManager'
import { CancelledError, canFetch, createRetryer } from './retryer'
import { Removable } from './removable'
import type { QueryCache } from './queryCache'
import type { QueryClient } from './queryClient'
import type {
CancelOptions,
DefaultError,
FetchStatus,
InitialDataFunction,
OmitKeyof,
QueryFunctionContext,
QueryKey,
QueryMeta,
QueryOptions,
QueryStatus,
SetDataOptions,
StaleTime,
} from './types'
import type { QueryObserver } from './queryObserver'
import type { Retryer } from './retryer'
// TYPES
interface QueryConfig<
TQueryFnData,
TError,
TData,
TQueryKey extends QueryKey = QueryKey,
> {
client: QueryClient
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>
client: QueryClient
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
#client: QueryClient
#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.#client = config.client
this.#cache = this.#client.getQueryCache()
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)
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (this.state && this.state.data === undefined) {
const defaultState = getDefaultState(this.options)
if (defaultState.data !== undefined) {
this.setState(
successState(defaultState.data, defaultState.dataUpdatedAt),
)
this.#initialState = defaultState
}
}
}
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
)
}
isStatic(): boolean {
if (this.getObserversCount() > 0) {
return this.observers.some(
(observer) =>
resolveStaleTime(observer.options.staleTime, this) === 'static',
)
}
return false
}
isStale(): boolean {
// check observers first, their `isStale` has the source of truth
// calculated with `isStaleByTime` and it takes `enabled` into account
if (this.getObserversCount() > 0) {
return this.observers.some(
(observer) => observer.getCurrentResult().isStale,
)
}
return this.state.data === undefined || this.state.isInvalidated
}
isStaleByTime(staleTime: StaleTime = 0): boolean {
// no data is always stale
if (this.state.data === undefined) {
return true
}
// static is never stale
if (staleTime === 'static') {
return false
}
// if the query is invalidated, it is stale
if (this.state.isInvalidated) {
return true
}
return !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' })
}
}
async fetch(
options?: QueryOptions<TQueryFnData, TError, TData, TQueryKey>,
fetchOptions?: FetchOptions<TQueryFnData>,
): Promise<TData> {
if (
this.state.fetchStatus !== 'idle' &&
// If the promise in the retyer is already rejected, we have to definitely
// re-start the fetch; there is a chance that the query is still in a
// pending state when that happens
this.#retryer?.status() !== 'rejected'
) {
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 createQueryFnContext = (): QueryFunctionContext<TQueryKey> => {
const queryFnContext: OmitKeyof<
QueryFunctionContext<TQueryKey>,
'signal'
> = {
client: this.#client,
queryKey: this.queryKey,
meta: this.meta,
}
addSignalProperty(queryFnContext)
return queryFnContext as QueryFunctionContext<TQueryKey>
}
const queryFnContext = createQueryFnContext()
this.#abortSignalConsumed = false
if (this.options.persister) {
return this.options.persister(
queryFn,
queryFnContext,
this as unknown as Query,
)
}
return queryFn(queryFnContext)
}
// Trigger behavior hook
const createFetchContext = (): FetchContext<
TQueryFnData,
TError,
TData,
TQueryKey
> => {
const context: OmitKeyof<
FetchContext<TQueryFnData, TError, TData, TQueryKey>,
'signal'
> = {
fetchOptions,
options: this.options,
queryKey: this.queryKey,
client: this.#client,
state: this.state,
fetchFn,
}
addSignalProperty(context)
return context as FetchContext<TQueryFnData, TError, TData, TQueryKey>
}
const context = createFetchContext()
this.options.behavior?.onFetch(context, 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 })
}
// Try to fetch the data
this.#retryer = createRetryer({
initialPromise: fetchOptions?.initialPromise as
| Promise<TData>
| undefined,
fn: context.fetchFn as () => Promise<TData>,
onCancel: (error) => {
if (error instanceof CancelledError && error.revert) {
this.setState({
...this.#revertState,
fetchStatus: 'idle' as const,
})
}
abortController.abort()
},
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,
})
try {
const data = await this.#retryer.start()
// this is more of a runtime guard
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
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}`,
)
}
throw new Error(`${this.queryHash} data is undefined`)
}
this.setData(data)
// 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>,
)
return data
} catch (error) {
if (error instanceof CancelledError) {
if (error.silent) {
// silent cancellation implies a new fetch is going to be started,
// so we piggyback onto that promise
return this.#retryer.promise
} else if (error.revert) {
// transform error into reverted state data
// if the initial fetch was cancelled, we have no data, so we have
// to get reject with a CancelledError
if (this.state.data === undefined) {
throw error
}
return this.state.data
}
}
this.#dispatch({
type: 'error',
error: error as TError,
})
// 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>,
)
throw error // rethrow the error for further handling
} finally {
// Schedule query gc after fetching
this.scheduleGc()
}
}
#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':
const newState = {
...state,
...successState(action.data, action.dataUpdatedAt),
dataUpdateCount: state.dataUpdateCount + 1,
...(!action.manual && {
fetchStatus: 'idle' as const,
fetchFailureCount: 0,
fetchFailureReason: null,
}),
}
// If fetching ends successfully, we don't need revertState as a fallback anymore.
// For manual updates, capture the state to revert to it in case of a cancellation.
this.#revertState = action.manual ? newState : undefined
return newState
case 'error':
const error = action.error
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 successState<TData>(data: TData | undefined, dataUpdatedAt?: number) {
return {
data,
dataUpdatedAt: dataUpdatedAt ?? Date.now(),
error: null,
isInvalidated: false,
status: 'success' 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',
}
}

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({
client,
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<any> = {}): 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()
})
})
}
}

View File

@@ -0,0 +1,648 @@
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,
DefaultError,
DefaultOptions,
DefaultedQueryObserverOptions,
EnsureInfiniteQueryDataOptions,
EnsureQueryDataOptions,
FetchInfiniteQueryOptions,
FetchQueryOptions,
InferDataFromTag,
InferErrorFromTag,
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<TQueryFilters extends QueryFilters<any> = QueryFilters>(
filters?: TQueryFilters,
): number {
return this.#queryCache.findAll({ ...filters, fetchStatus: 'fetching' })
.length
}
isMutating<
TMutationFilters extends MutationFilters<any, any> = MutationFilters,
>(filters?: TMutationFilters): number {
return this.#mutationCache.findAll({ ...filters, status: 'pending' }).length
}
/**
* Imperative (non-reactive) way to retrieve data for a QueryKey.
* Should only be used in callbacks or functions where reading the latest data is necessary, e.g. for optimistic updates.
*
* Hint: Do not use this function inside a component, because it won't receive updates.
* Use `useQuery` to create a `QueryObserver` that subscribes to changes.
*/
getQueryData<
TQueryFnData = unknown,
TTaggedQueryKey extends QueryKey = QueryKey,
TInferredQueryFnData = InferDataFromTag<TQueryFnData, TTaggedQueryKey>,
>(queryKey: TTaggedQueryKey): TInferredQueryFnData | undefined {
const options = this.defaultQueryOptions({ queryKey })
return this.#queryCache.get<TInferredQueryFnData>(options.queryHash)?.state
.data
}
ensureQueryData<
TQueryFnData,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
>(
options: EnsureQueryDataOptions<TQueryFnData, TError, TData, TQueryKey>,
): Promise<TData> {
const defaultedOptions = this.defaultQueryOptions(options)
const query = this.#queryCache.build(this, defaultedOptions)
const cachedData = query.state.data
if (cachedData === undefined) {
return this.fetchQuery(options)
}
if (
options.revalidateIfStale &&
query.isStaleByTime(resolveStaleTime(defaultedOptions.staleTime, query))
) {
void this.prefetchQuery(defaultedOptions)
}
return Promise.resolve(cachedData)
}
getQueriesData<
TQueryFnData = unknown,
TQueryFilters extends QueryFilters<any> = QueryFilters,
>(filters: TQueryFilters): 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 = InferDataFromTag<TQueryFnData, TTaggedQueryKey>,
>(
queryKey: TTaggedQueryKey,
updater: Updater<
NoInfer<TInferredQueryFnData> | undefined,
NoInfer<TInferredQueryFnData> | undefined
>,
options?: SetDataOptions,
): NoInfer<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,
TQueryFilters extends QueryFilters<any> = QueryFilters,
>(
filters: TQueryFilters,
updater: Updater<
NoInfer<TQueryFnData> | undefined,
NoInfer<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 = InferDataFromTag<TQueryFnData, TTaggedQueryKey>,
TInferredError = InferErrorFromTag<TError, TTaggedQueryKey>,
>(
queryKey: TTaggedQueryKey,
): QueryState<TInferredQueryFnData, TInferredError> | undefined {
const options = this.defaultQueryOptions({ queryKey })
return this.#queryCache.get<TInferredQueryFnData, TInferredError>(
options.queryHash,
)?.state
}
removeQueries<TTaggedQueryKey extends QueryKey = QueryKey>(
filters?: QueryFilters<TTaggedQueryKey>,
): void {
const queryCache = this.#queryCache
notifyManager.batch(() => {
queryCache.findAll(filters).forEach((query) => {
queryCache.remove(query)
})
})
}
resetQueries<TTaggedQueryKey extends QueryKey = QueryKey>(
filters?: QueryFilters<TTaggedQueryKey>,
options?: ResetOptions,
): Promise<void> {
const queryCache = this.#queryCache
return notifyManager.batch(() => {
queryCache.findAll(filters).forEach((query) => {
query.reset()
})
return this.refetchQueries(
{
type: 'active',
...filters,
},
options,
)
})
}
cancelQueries<TTaggedQueryKey extends QueryKey = QueryKey>(
filters?: QueryFilters<TTaggedQueryKey>,
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<TTaggedQueryKey extends QueryKey = QueryKey>(
filters?: InvalidateQueryFilters<TTaggedQueryKey>,
options: InvalidateOptions = {},
): Promise<void> {
return notifyManager.batch(() => {
this.#queryCache.findAll(filters).forEach((query) => {
query.invalidate()
})
if (filters?.refetchType === 'none') {
return Promise.resolve()
}
return this.refetchQueries(
{
...filters,
type: filters?.refetchType ?? filters?.type ?? 'active',
},
options,
)
})
}
refetchQueries<TTaggedQueryKey extends QueryKey = QueryKey>(
filters?: RefetchQueryFilters<TTaggedQueryKey>,
options: RefetchOptions = {},
): Promise<void> {
const fetchOptions = {
...options,
cancelRefetch: options.cancelRefetch ?? true,
}
const promises = notifyManager.batch(() =>
this.#queryCache
.findAll(filters)
.filter((query) => !query.isDisabled() && !query.isStatic())
.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()]
const result: OmitKeyof<
QueryObserverOptions<any, any, any, any, any>,
'queryKey'
> = {}
defaults.forEach((queryDefault) => {
if (partialMatchKey(queryKey, queryDefault.queryKey)) {
Object.assign(result, queryDefault.defaultOptions)
}
})
return result
}
setMutationDefaults<
TData = unknown,
TError = DefaultError,
TVariables = void,
TOnMutateResult = unknown,
>(
mutationKey: MutationKey,
options: OmitKeyof<
MutationObserverOptions<TData, TError, TVariables, TOnMutateResult>,
'mutationKey'
>,
): void {
this.#mutationDefaults.set(hashKey(mutationKey), {
mutationKey,
defaultOptions: options,
})
}
getMutationDefaults(
mutationKey: MutationKey,
): OmitKeyof<MutationObserverOptions<any, any, any, any>, 'mutationKey'> {
const defaults = [...this.#mutationDefaults.values()]
const result: OmitKeyof<
MutationObserverOptions<any, any, any, any>,
'mutationKey'
> = {}
defaults.forEach((queryDefault) => {
if (partialMatchKey(mutationKey, queryDefault.mutationKey)) {
Object.assign(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.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()
}
}

View File

@@ -0,0 +1,833 @@
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 { timeoutManager } from './timeoutManager'
import type { ManagedTimerId } from './timeoutManager'
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
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?: ManagedTimerId
#refetchIntervalId?: ManagedTimerId
#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()
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
>,
): 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()
// 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> {
return new Proxy(result, {
get: (target, key) => {
this.trackProp(key as keyof QueryObserverResult)
onPropTracked?.(key as keyof QueryObserverResult)
if (key === 'promise') {
this.trackProp('data')
if (
!this.options.experimental_prefetchInRender &&
this.#currentThenable.status === 'pending'
) {
this.#currentThenable.reject(
new Error(
'experimental_prefetchInRender feature flag is not enabled',
),
)
}
}
return Reflect.get(target, key)
},
})
}
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 = timeoutManager.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 = timeoutManager.setInterval(() => {
if (
this.options.refetchIntervalInBackground ||
focusManager.isFocused()
) {
this.#executeFetch()
}
}, this.#currentRefetchInterval)
}
#updateTimers(): void {
this.#updateStaleTimeout()
this.#updateRefetchInterval(this.#computeRefetchInterval())
}
#clearStaleTimeout(): void {
if (this.#staleTimeoutId) {
timeoutManager.clearTimeout(this.#staleTimeoutId)
this.#staleTimeoutId = undefined
}
}
#clearRefetchInterval(): void {
if (this.#refetchIntervalId) {
timeoutManager.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
// Per default, use query data
data = newState.data as unknown as TData
let skipSelect = false
// use placeholderData 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
// we have to skip select when reading this memoization
// because prevResult.data is already "selected"
skipSelect = true
} else {
// compute placeholderData
placeholderData =
typeof options.placeholderData === 'function'
? (
options.placeholderData as unknown as PlaceholderDataFunction<TQueryData>
)(
this.#lastQueryWithDefinedData?.state.data,
this.#lastQueryWithDefinedData as any,
)
: options.placeholderData
}
if (placeholderData !== undefined) {
status = 'success'
data = replaceData(
prevResult?.data,
placeholderData as unknown,
options,
) as TData
isPlaceholderData = true
}
}
// Select data if needed
// this also runs placeholderData through the select function
if (options.select && data !== undefined && !skipSelect) {
// Memoize select result
if (
prevResult &&
data === prevResultState?.data &&
options.select === this.#selectFn
) {
data = this.#selectResult
} else {
try {
this.#selectFn = options.select
data = options.select(data as any)
data = replaceData(prevResult?.data, data, options)
this.#selectResult = data
this.#selectError = null
} catch (selectError) {
this.#selectError = selectError as TError
}
}
}
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,
isEnabled: resolveEnabled(options.enabled, query) !== false,
}
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(): 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
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)
})
}
this.#notify({ listeners: shouldNotifyListeners() })
}
#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: { listeners: boolean }): 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 &&
resolveStaleTime(options.staleTime, query) !== 'static'
) {
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
}

View File

@@ -0,0 +1,39 @@
import { timeoutManager } from './timeoutManager'
import { isServer, isValidTimeout } from './utils'
import type { ManagedTimerId } from './timeoutManager'
export abstract class Removable {
gcTime!: number
#gcTimeout?: ManagedTimerId
destroy(): void {
this.clearGcTimeout()
}
protected scheduleGc(): void {
this.clearGcTimeout()
if (isValidTimeout(this.gcTime)) {
this.#gcTimeout = timeoutManager.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) {
timeoutManager.clearTimeout(this.#gcTimeout)
this.#gcTimeout = undefined
}
}
protected abstract optionalRemove(): void
}

View File

@@ -0,0 +1,228 @@
import { focusManager } from './focusManager'
import { onlineManager } from './onlineManager'
import { pendingThenable } from './thenable'
import { isServer, sleep } from './utils'
import type { Thenable } from './thenable'
import type { CancelOptions, DefaultError, NetworkMode } from './types'
// TYPES
interface RetryerConfig<TData = unknown, TError = DefaultError> {
fn: () => TData | Promise<TData>
initialPromise?: Promise<TData>
onCancel?: (error: TError) => 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>
status: () => 'pending' | 'resolved' | 'rejected'
}
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
}
}
/**
* @deprecated Use instanceof `CancelledError` instead.
*/
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 continueFn: ((value?: unknown) => void) | undefined
const thenable = pendingThenable<TData>()
const isResolved = () =>
(thenable.status as Thenable<TData>['status']) !== 'pending'
const cancel = (cancelOptions?: CancelOptions): void => {
if (!isResolved()) {
const error = new CancelledError(cancelOptions) as TError
reject(error)
config.onCancel?.(error)
}
}
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()) {
continueFn?.()
thenable.resolve(value)
}
}
const reject = (value: any) => {
if (!isResolved()) {
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,
status: () => thenable.status,
cancel,
continue: () => {
continueFn?.()
return thenable
},
cancelRetry,
continueRetry,
canStart,
start: () => {
// Start loop
if (canStart()) {
run()
} else {
pause().then(run)
}
return thenable
},
}
}

View File

@@ -0,0 +1,99 @@
import { addToEnd } from './utils'
import type { QueryFunction, QueryFunctionContext, QueryKey } from './types'
type BaseStreamedQueryParams<TQueryFnData, TQueryKey extends QueryKey> = {
streamFn: (
context: QueryFunctionContext<TQueryKey>,
) => AsyncIterable<TQueryFnData> | Promise<AsyncIterable<TQueryFnData>>
refetchMode?: 'append' | 'reset' | 'replace'
}
type SimpleStreamedQueryParams<
TQueryFnData,
TQueryKey extends QueryKey,
> = BaseStreamedQueryParams<TQueryFnData, TQueryKey> & {
reducer?: never
initialValue?: never
}
type ReducibleStreamedQueryParams<
TQueryFnData,
TData,
TQueryKey extends QueryKey,
> = BaseStreamedQueryParams<TQueryFnData, TQueryKey> & {
reducer: (acc: TData, chunk: TQueryFnData) => TData
initialValue: TData
}
type StreamedQueryParams<TQueryFnData, TData, TQueryKey extends QueryKey> =
| SimpleStreamedQueryParams<TQueryFnData, TQueryKey>
| ReducibleStreamedQueryParams<TQueryFnData, TData, TQueryKey>
/**
* This is a helper function to create a query function that streams data from an AsyncIterable.
* Data will be an Array of all the chunks received.
* The query will be in a 'pending' state until the first chunk of data is received, but will go to 'success' after that.
* The query will stay in fetchStatus 'fetching' until the stream ends.
* @param queryFn - The function that returns an AsyncIterable to stream data from.
* @param refetchMode - Defines how re-fetches are handled.
* Defaults to `'reset'`, erases all data and puts the query back into `pending` state.
* Set to `'append'` to append new data to the existing data.
* Set to `'replace'` to write all data to the cache once the stream ends.
* @param reducer - A function to reduce the streamed chunks into the final data.
* Defaults to a function that appends chunks to the end of the array.
* @param initialValue - Initial value to be used while the first chunk is being fetched, and returned if the stream yields no values.
*/
export function streamedQuery<
TQueryFnData = unknown,
TData = Array<TQueryFnData>,
TQueryKey extends QueryKey = QueryKey,
>({
streamFn,
refetchMode = 'reset',
reducer = (items, chunk) =>
addToEnd(items as Array<TQueryFnData>, chunk) as TData,
initialValue = [] as TData,
}: StreamedQueryParams<TQueryFnData, TData, TQueryKey>): QueryFunction<
TData,
TQueryKey
> {
return async (context) => {
const query = context.client
.getQueryCache()
.find({ queryKey: context.queryKey, exact: true })
const isRefetch = !!query && query.state.data !== undefined
if (isRefetch && refetchMode === 'reset') {
query.setState({
status: 'pending',
data: undefined,
error: null,
fetchStatus: 'fetching',
})
}
let result = initialValue
const stream = await streamFn(context)
for await (const chunk of stream) {
if (context.signal.aborted) {
break
}
// don't append to the cache directly when replace-refetching
if (!isRefetch || refetchMode !== 'replace') {
context.client.setQueryData<TData>(context.queryKey, (prev) =>
reducer(prev === undefined ? initialValue : prev, chunk),
)
}
result = reducer(result, chunk)
}
// finalize result: replace-refetching needs to write to the cache
if (isRefetch && refetchMode === 'replace' && !context.signal.aborted) {
context.client.setQueryData<TData>(context.queryKey, result)
}
return context.client.getQueryData(context.queryKey) ?? initialValue
}
}

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
}
}

View File

@@ -0,0 +1,111 @@
/**
* 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
*/
import { noop } from './utils'
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
}
/**
* This function takes a Promise-like input and detects whether the data
* is synchronously available or not.
*
* It does not inspect .status, .value or .reason properties of the promise,
* as those are not always available, and the .status of React's promises
* should not be considered part of the public API.
*/
export function tryResolveSync(promise: Promise<unknown> | Thenable<unknown>) {
let data: unknown
promise
.then((result) => {
data = result
return result
}, noop)
// .catch can be unavailable on certain kinds of thenable's
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
?.catch(noop)
if (data !== undefined) {
return { data }
}
return undefined
}

View File

@@ -0,0 +1,135 @@
/**
* {@link TimeoutManager} does not support passing arguments to the callback.
*
* `(_: void)` is the argument type inferred by TypeScript's default typings for
* `setTimeout(cb, number)`.
* If we don't accept a single void argument, then
* `new Promise(resolve => timeoutManager.setTimeout(resolve, N))` is a type error.
*/
export type TimeoutCallback = (_: void) => void
/**
* Wrapping `setTimeout` is awkward from a typing perspective because platform
* typings may extend the return type of `setTimeout`. For example, NodeJS
* typings add `NodeJS.Timeout`; but a non-default `timeoutManager` may not be
* able to return such a type.
*/
export type ManagedTimerId = number | { [Symbol.toPrimitive]: () => number }
/**
* Backend for timer functions.
*/
export type TimeoutProvider<TTimerId extends ManagedTimerId = ManagedTimerId> =
{
readonly setTimeout: (callback: TimeoutCallback, delay: number) => TTimerId
readonly clearTimeout: (timeoutId: TTimerId | undefined) => void
readonly setInterval: (callback: TimeoutCallback, delay: number) => TTimerId
readonly clearInterval: (intervalId: TTimerId | undefined) => void
}
export const defaultTimeoutProvider: TimeoutProvider<
ReturnType<typeof setTimeout>
> = {
// We need the wrapper function syntax below instead of direct references to
// global setTimeout etc.
//
// BAD: `setTimeout: setTimeout`
// GOOD: `setTimeout: (cb, delay) => setTimeout(cb, delay)`
//
// If we use direct references here, then anything that wants to spy on or
// replace the global setTimeout (like tests) won't work since we'll already
// have a hard reference to the original implementation at the time when this
// file was imported.
setTimeout: (callback, delay) => setTimeout(callback, delay),
clearTimeout: (timeoutId) => clearTimeout(timeoutId),
setInterval: (callback, delay) => setInterval(callback, delay),
clearInterval: (intervalId) => clearInterval(intervalId),
}
/**
* Allows customization of how timeouts are created.
*
* @tanstack/query-core makes liberal use of timeouts to implement `staleTime`
* and `gcTime`. The default TimeoutManager provider uses the platform's global
* `setTimeout` implementation, which is known to have scalability issues with
* thousands of timeouts on the event loop.
*
* If you hit this limitation, consider providing a custom TimeoutProvider that
* coalesces timeouts.
*/
export class TimeoutManager implements Omit<TimeoutProvider, 'name'> {
// We cannot have TimeoutManager<T> as we must instantiate it with a concrete
// type at app boot; and if we leave that type, then any new timer provider
// would need to support ReturnType<typeof setTimeout>, which is infeasible.
//
// We settle for type safety for the TimeoutProvider type, and accept that
// this class is unsafe internally to allow for extension.
#provider: TimeoutProvider<any> = defaultTimeoutProvider
#providerCalled = false
setTimeoutProvider<TTimerId extends ManagedTimerId>(
provider: TimeoutProvider<TTimerId>,
): void {
if (process.env.NODE_ENV !== 'production') {
if (this.#providerCalled && provider !== this.#provider) {
// After changing providers, `clearTimeout` will not work as expected for
// timeouts from the previous provider.
//
// Since they may allocate the same timeout ID, clearTimeout may cancel an
// arbitrary different timeout, or unexpected no-op.
//
// We could protect against this by mixing the timeout ID bits
// deterministically with some per-provider bits.
//
// We could internally queue `setTimeout` calls to `TimeoutManager` until
// some API call to set the initial provider.
console.error(
`[timeoutManager]: Switching provider after calls to previous provider might result in unexpected behavior.`,
{ previous: this.#provider, provider },
)
}
}
this.#provider = provider
if (process.env.NODE_ENV !== 'production') {
this.#providerCalled = false
}
}
setTimeout(callback: TimeoutCallback, delay: number): ManagedTimerId {
if (process.env.NODE_ENV !== 'production') {
this.#providerCalled = true
}
return this.#provider.setTimeout(callback, delay)
}
clearTimeout(timeoutId: ManagedTimerId | undefined): void {
this.#provider.clearTimeout(timeoutId)
}
setInterval(callback: TimeoutCallback, delay: number): ManagedTimerId {
if (process.env.NODE_ENV !== 'production') {
this.#providerCalled = true
}
return this.#provider.setInterval(callback, delay)
}
clearInterval(intervalId: ManagedTimerId | undefined): void {
this.#provider.clearInterval(intervalId)
}
}
export const timeoutManager = new TimeoutManager()
/**
* In many cases code wants to delay to the next event loop tick; this is not
* mediated by {@link timeoutManager}.
*
* This function is provided to make auditing the `tanstack/query-core` for
* incorrect use of system `setTimeout` easier.
*/
export function systemSetTimeoutZero(callback: TimeoutCallback): void {
setTimeout(callback, 0)
}

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

File diff suppressed because it is too large Load Diff

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

@@ -0,0 +1,467 @@
import { timeoutManager } from './timeoutManager'
import type {
DefaultError,
Enabled,
FetchStatus,
MutationKey,
MutationStatus,
QueryFunction,
QueryKey,
QueryOptions,
StaleTime,
StaleTimeFunction,
} from './types'
import type { Mutation } from './mutation'
import type { FetchOptions, Query } from './query'
// TYPES
type DropLast<T extends ReadonlyArray<unknown>> = T extends readonly [
...infer R,
unknown,
]
? readonly [...R]
: never
type TuplePrefixes<T extends ReadonlyArray<unknown>> = T extends readonly []
? readonly []
: TuplePrefixes<DropLast<T>> | T
export interface QueryFilters<TQueryKey extends QueryKey = QueryKey> {
/**
* 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?: TQueryKey | TuplePrefixes<TQueryKey>
/**
* Include or exclude stale queries
*/
stale?: boolean
/**
* Include queries matching their fetchStatus
*/
fetchStatus?: FetchStatus
}
export interface MutationFilters<
TData = unknown,
TError = DefaultError,
TVariables = unknown,
TOnMutateResult = unknown,
> {
/**
* Match mutation key exactly
*/
exact?: boolean
/**
* Include mutations matching this predicate function
*/
predicate?: (
mutation: Mutation<TData, TError, TVariables, TOnMutateResult>,
) => boolean
/**
* Include mutations matching this mutation key
*/
mutationKey?: TuplePrefixes<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(): void
export function noop(): undefined
export function noop() {}
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
| StaleTimeFunction<TQueryFnData, TError, TData, TQueryKey>,
query: Query<TQueryFnData, TError, TData, TQueryKey>,
): StaleTime | 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).every((key) => partialMatchKey(a[key], b[key]))
}
return false
}
const hasOwn = Object.prototype.hasOwnProperty
/**
* 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))) return 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 ? new Array(bSize) : {}
let equalItems = 0
for (let i = 0; i < bSize; i++) {
const key: any = array ? i : bItems[i]
const aItem = a[key]
const bItem = b[key]
if (aItem === bItem) {
copy[key] = aItem
if (array ? i < aSize : hasOwn.call(a, key)) equalItems++
continue
}
if (
aItem === null ||
bItem === null ||
typeof aItem !== 'object' ||
typeof bItem !== 'object'
) {
copy[key] = bItem
continue
}
const v = replaceEqualDeep(aItem, bItem)
copy[key] = v
if (v === aItem) equalItems++
}
return aSize === bSize && equalItems === aSize ? a : copy
}
/**
* 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): value is Array<unknown> {
return Array.isArray(value) && value.length === Object.keys(value).length
}
// Copied from: https://github.com/jonschlinkert/is-plain-object
export function isPlainObject(o: any): o is Record<PropertyKey, unknown> {
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) => {
timeoutManager.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}`,
)
// Prevent the replaceEqualDeep from being called again down below.
throw 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
}
export function shouldThrowError<T extends (...args: Array<any>) => boolean>(
throwOnError: boolean | T | undefined,
params: Parameters<T>,
): boolean {
// Allow throwOnError function to override throwing behavior on a per-error basis
if (typeof throwOnError === 'function') {
return throwOnError(...params)
}
return !!throwOnError
}