mirror of
https://github.com/idrainformatica/PecFlow.git
synced 2026-06-16 12:45:42 +02:00
fase 5
This commit is contained in:
+21
@@ -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.
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"name": "@tanstack/query-core",
|
||||
"version": "5.91.0",
|
||||
"description": "The framework agnostic core that powers TanStack Query",
|
||||
"author": "tannerlinsley",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+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: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:ts58": "node ../../node_modules/typescript58/lib/tsc.js --build tsconfig.legacy.json",
|
||||
"test:types:ts59": "node ../../node_modules/typescript59/lib/tsc.js --build tsconfig.legacy.json",
|
||||
"test:types:tscurrent": "tsc --build",
|
||||
"test:types:ts60": "node ../../node_modules/typescript60/lib/tsc.js --build tsconfig.legacy.json",
|
||||
"test:lib": "vitest",
|
||||
"test:lib:dev": "pnpm run test:lib --watch",
|
||||
"test:build": "publint --strict && attw --pack",
|
||||
"build": "tsup --tsconfig tsconfig.prod.json"
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { isServer } from './utils'
|
||||
|
||||
export type IsServerValue = () => boolean
|
||||
|
||||
/**
|
||||
* Manages environment detection used by TanStack Query internals.
|
||||
*/
|
||||
export const environmentManager = (() => {
|
||||
let isServerFn: IsServerValue = () => isServer
|
||||
|
||||
return {
|
||||
/**
|
||||
* Returns whether the current runtime should be treated as a server environment.
|
||||
*/
|
||||
isServer(): boolean {
|
||||
return isServerFn()
|
||||
},
|
||||
/**
|
||||
* Overrides the server check globally.
|
||||
*/
|
||||
setIsServer(isServerValue: IsServerValue): void {
|
||||
isServerFn = isServerValue
|
||||
},
|
||||
}
|
||||
})()
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
import { Subscribable } from './subscribable'
|
||||
|
||||
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 (typeof window !== 'undefined' && 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()
|
||||
+285
@@ -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)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
/* istanbul ignore file */
|
||||
|
||||
export { focusManager } from './focusManager'
|
||||
export { environmentManager } from './environmentManager'
|
||||
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'
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
import {
|
||||
addConsumeAwareSignal,
|
||||
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) => {
|
||||
addConsumeAwareSignal(
|
||||
object,
|
||||
() => context.signal,
|
||||
() => (cancelled = true),
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
+190
@@ -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
|
||||
+419
@@ -0,0 +1,419 @@
|
||||
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
|
||||
if (this.#mutationCache.config.onMutate) {
|
||||
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,
|
||||
)
|
||||
} catch (e) {
|
||||
void Promise.reject(e)
|
||||
}
|
||||
|
||||
try {
|
||||
await this.options.onError?.(
|
||||
error as TError,
|
||||
variables,
|
||||
this.state.context,
|
||||
mutationFnContext,
|
||||
)
|
||||
} catch (e) {
|
||||
void Promise.reject(e)
|
||||
}
|
||||
|
||||
try {
|
||||
// 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,
|
||||
)
|
||||
} catch (e) {
|
||||
void Promise.reject(e)
|
||||
}
|
||||
|
||||
try {
|
||||
await this.options.onSettled?.(
|
||||
undefined,
|
||||
error as TError,
|
||||
variables,
|
||||
this.state.context,
|
||||
mutationFnContext,
|
||||
)
|
||||
} catch (e) {
|
||||
void Promise.reject(e)
|
||||
}
|
||||
|
||||
this.#dispatch({ type: 'error', error: error as TError })
|
||||
throw error
|
||||
} 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,
|
||||
}
|
||||
}
|
||||
+244
@@ -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
|
||||
}
|
||||
+227
@@ -0,0 +1,227 @@
|
||||
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') {
|
||||
try {
|
||||
this.#mutateOptions.onSuccess?.(
|
||||
action.data,
|
||||
variables,
|
||||
onMutateResult,
|
||||
context,
|
||||
)
|
||||
} catch (e) {
|
||||
void Promise.reject(e)
|
||||
}
|
||||
try {
|
||||
this.#mutateOptions.onSettled?.(
|
||||
action.data,
|
||||
null,
|
||||
variables,
|
||||
onMutateResult,
|
||||
context,
|
||||
)
|
||||
} catch (e) {
|
||||
void Promise.reject(e)
|
||||
}
|
||||
} else if (action?.type === 'error') {
|
||||
try {
|
||||
this.#mutateOptions.onError?.(
|
||||
action.error,
|
||||
variables,
|
||||
onMutateResult,
|
||||
context,
|
||||
)
|
||||
} catch (e) {
|
||||
void Promise.reject(e)
|
||||
}
|
||||
try {
|
||||
this.#mutateOptions.onSettled?.(
|
||||
undefined,
|
||||
action.error,
|
||||
variables,
|
||||
onMutateResult,
|
||||
context,
|
||||
)
|
||||
} catch (e) {
|
||||
void Promise.reject(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Then trigger the listeners
|
||||
this.listeners.forEach((listener) => {
|
||||
listener(this.#currentResult)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
+99
@@ -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()
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
import { Subscribable } from './subscribable'
|
||||
|
||||
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 (typeof window !== 'undefined' && 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()
|
||||
+315
@@ -0,0 +1,315 @@
|
||||
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>
|
||||
#lastQueryHashes?: Array<string>
|
||||
#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)
|
||||
|
||||
// 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.#observerMatches = newObserverMatches
|
||||
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),
|
||||
)
|
||||
const queryHashes = matches.map(
|
||||
(match) => match.defaultedQueryOptions.queryHash,
|
||||
)
|
||||
|
||||
return [
|
||||
result,
|
||||
(r?: Array<QueryObserverResult>) => {
|
||||
return this.#combineResult(r ?? result, combine, queryHashes)
|
||||
},
|
||||
() => {
|
||||
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,
|
||||
queryHashes?: Array<string>,
|
||||
): TCombinedResult {
|
||||
if (combine) {
|
||||
const lastHashes = this.#lastQueryHashes
|
||||
const queryHashesChanged =
|
||||
queryHashes !== undefined &&
|
||||
lastHashes !== undefined &&
|
||||
(lastHashes.length !== queryHashes.length ||
|
||||
queryHashes.some((hash, i) => hash !== lastHashes[i]))
|
||||
|
||||
if (
|
||||
!this.#combinedResult ||
|
||||
this.#result !== this.#lastResult ||
|
||||
queryHashesChanged ||
|
||||
combine !== this.#lastCombine
|
||||
) {
|
||||
this.#lastCombine = combine
|
||||
this.#lastResult = this.#result
|
||||
|
||||
if (queryHashes !== undefined) {
|
||||
this.#lastQueryHashes = queryHashes
|
||||
}
|
||||
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
|
||||
}
|
||||
+756
@@ -0,0 +1,756 @@
|
||||
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 retryer 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',
|
||||
// flag existing data as invalidated if we get a background error
|
||||
// note that "no data" always means stale so we can set unconditionally here
|
||||
isInvalidated: true,
|
||||
}
|
||||
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()
|
||||
: options.initialDataUpdatedAt
|
||||
: 0
|
||||
|
||||
return {
|
||||
data,
|
||||
dataUpdateCount: 0,
|
||||
dataUpdatedAt: hasData ? (initialDataUpdatedAt ?? Date.now()) : 0,
|
||||
error: null,
|
||||
errorUpdateCount: 0,
|
||||
errorUpdatedAt: 0,
|
||||
fetchFailureCount: 0,
|
||||
fetchFailureReason: null,
|
||||
fetchMeta: null,
|
||||
isInvalidated: false,
|
||||
status: hasData ? 'success' : 'pending',
|
||||
fetchStatus: 'idle',
|
||||
}
|
||||
}
|
||||
+223
@@ -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()
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
+648
@@ -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()
|
||||
}
|
||||
}
|
||||
+832
@@ -0,0 +1,832 @@
|
||||
import { focusManager } from './focusManager'
|
||||
import { environmentManager } from './environmentManager'
|
||||
import { notifyManager } from './notifyManager'
|
||||
import { fetchState } from './query'
|
||||
import { Subscribable } from './subscribable'
|
||||
import { pendingThenable } from './thenable'
|
||||
import {
|
||||
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,
|
||||
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,
|
||||
fetchOptions,
|
||||
)
|
||||
|
||||
if (!fetchOptions?.throwOnError) {
|
||||
promise = promise.catch(noop)
|
||||
}
|
||||
|
||||
return promise
|
||||
}
|
||||
|
||||
#updateStaleTimeout(): void {
|
||||
this.#clearStaleTimeout()
|
||||
const staleTime = resolveStaleTime(
|
||||
this.options.staleTime,
|
||||
this.#currentQuery,
|
||||
)
|
||||
|
||||
if (
|
||||
environmentManager.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 (
|
||||
environmentManager.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
|
||||
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 hasResultData = nextResult.data !== undefined
|
||||
const isErrorWithoutData = nextResult.status === 'error' && !hasResultData
|
||||
const finalizeThenableIfPossible = (thenable: PendingThenable<TData>) => {
|
||||
if (isErrorWithoutData) {
|
||||
thenable.reject(nextResult.error)
|
||||
} else if (hasResultData) {
|
||||
thenable.resolve(nextResult.data as TData)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 (isErrorWithoutData || nextResult.data !== prevThenable.value) {
|
||||
recreateThenable()
|
||||
}
|
||||
break
|
||||
case 'rejected':
|
||||
if (!isErrorWithoutData || 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
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
import { timeoutManager } from './timeoutManager'
|
||||
import { environmentManager } from './environmentManager'
|
||||
import { 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 ?? (environmentManager.isServer() ? Infinity : 5 * 60 * 1000),
|
||||
)
|
||||
}
|
||||
|
||||
protected clearGcTimeout() {
|
||||
if (this.#gcTimeout) {
|
||||
timeoutManager.clearTimeout(this.#gcTimeout)
|
||||
this.#gcTimeout = undefined
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract optionalRemove(): void
|
||||
}
|
||||
+229
@@ -0,0 +1,229 @@
|
||||
import { focusManager } from './focusManager'
|
||||
import { onlineManager } from './onlineManager'
|
||||
import { pendingThenable } from './thenable'
|
||||
import { environmentManager } from './environmentManager'
|
||||
import { 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 ?? (environmentManager.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
|
||||
},
|
||||
}
|
||||
}
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
import { addConsumeAwareSignal, addToEnd } from './utils'
|
||||
import type {
|
||||
OmitKeyof,
|
||||
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
|
||||
|
||||
let cancelled: boolean = false as boolean
|
||||
const streamFnContext = addConsumeAwareSignal<
|
||||
OmitKeyof<typeof context, 'signal'>
|
||||
>(
|
||||
{
|
||||
client: context.client,
|
||||
meta: context.meta,
|
||||
queryKey: context.queryKey,
|
||||
pageParam: context.pageParam,
|
||||
direction: context.direction,
|
||||
},
|
||||
() => context.signal,
|
||||
() => (cancelled = true),
|
||||
)
|
||||
|
||||
const stream = await streamFn(streamFnContext)
|
||||
|
||||
const isReplaceRefetch = isRefetch && refetchMode === 'replace'
|
||||
|
||||
for await (const chunk of stream) {
|
||||
if (cancelled) {
|
||||
break
|
||||
}
|
||||
|
||||
if (isReplaceRefetch) {
|
||||
// don't append to the cache directly when replace-refetching
|
||||
result = reducer(result, chunk)
|
||||
} else {
|
||||
context.client.setQueryData<TData>(context.queryKey, (prev) =>
|
||||
reducer(prev === undefined ? initialValue : prev, chunk),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// finalize result: replace-refetching needs to write to the cache
|
||||
if (isReplaceRefetch && !cancelled) {
|
||||
context.client.setQueryData<TData>(context.queryKey, result)
|
||||
}
|
||||
|
||||
return context.client.getQueryData(context.queryKey) ?? initialValue
|
||||
}
|
||||
}
|
||||
+30
@@ -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
|
||||
}
|
||||
}
|
||||
+111
@@ -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 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
|
||||
}
|
||||
+135
@@ -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)
|
||||
}
|
||||
+1391
File diff suppressed because it is too large
Load Diff
+502
@@ -0,0 +1,502 @@
|
||||
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
|
||||
|
||||
/** @deprecated
|
||||
* use `environmentManager.isServer()` instead.
|
||||
*/
|
||||
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, depth?: number): T
|
||||
export function replaceEqualDeep(a: any, b: any, depth = 0): any {
|
||||
if (a === b) {
|
||||
return a
|
||||
}
|
||||
|
||||
if (depth > 500) return b
|
||||
|
||||
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, depth + 1)
|
||||
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
|
||||
}
|
||||
|
||||
export function addConsumeAwareSignal<T>(
|
||||
object: T,
|
||||
getSignal: () => AbortSignal,
|
||||
onCancelled: VoidFunction,
|
||||
): T & { signal: AbortSignal } {
|
||||
let consumed = false
|
||||
let signal: AbortSignal | undefined
|
||||
|
||||
Object.defineProperty(object, 'signal', {
|
||||
enumerable: true,
|
||||
get: () => {
|
||||
signal ??= getSignal()
|
||||
if (consumed) {
|
||||
return signal
|
||||
}
|
||||
|
||||
consumed = true
|
||||
if (signal.aborted) {
|
||||
onCancelled()
|
||||
} else {
|
||||
signal.addEventListener('abort', onCancelled, { once: true })
|
||||
}
|
||||
|
||||
return signal
|
||||
},
|
||||
})
|
||||
|
||||
return object as T & { signal: AbortSignal }
|
||||
}
|
||||
+21
@@ -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.
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
{
|
||||
"name": "@tanstack/query-devtools",
|
||||
"version": "5.93.0",
|
||||
"description": "Developer tools to interact with and visualize the TanStack Query cache",
|
||||
"author": "tannerlinsley",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/TanStack/query.git",
|
||||
"directory": "packages/query-devtools"
|
||||
},
|
||||
"homepage": "https://tanstack.com/query",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "./build/index.cjs",
|
||||
"module": "./build/index.js",
|
||||
"types": "./build/index.d.ts",
|
||||
"browser": {},
|
||||
"exports": {
|
||||
"@tanstack/custom-condition": "./src/index.ts",
|
||||
"solid": {
|
||||
"development": "./build/index.js",
|
||||
"import": "./build/index.js"
|
||||
},
|
||||
"development": {
|
||||
"import": {
|
||||
"types": "./build/index.d.ts",
|
||||
"default": "./build/dev.js"
|
||||
},
|
||||
"require": "./build/dev.cjs"
|
||||
},
|
||||
"import": {
|
||||
"types": "./build/index.d.ts",
|
||||
"default": "./build/index.js"
|
||||
},
|
||||
"require": "./build/index.cjs"
|
||||
},
|
||||
"files": [
|
||||
"build",
|
||||
"src",
|
||||
"!src/__tests__"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@kobalte/core": "^0.13.4",
|
||||
"@solid-primitives/keyed": "^1.2.2",
|
||||
"@solid-primitives/resize-observer": "^2.0.26",
|
||||
"@solid-primitives/storage": "^1.3.11",
|
||||
"@tanstack/match-sorter-utils": "^8.19.4",
|
||||
"clsx": "^2.1.1",
|
||||
"goober": "^2.1.16",
|
||||
"npm-run-all2": "^5.0.0",
|
||||
"solid-js": "^1.9.7",
|
||||
"solid-transition-group": "^0.2.3",
|
||||
"superjson": "^2.2.2",
|
||||
"tsup-preset-solid": "^2.2.0",
|
||||
"vite-plugin-solid": "^2.11.6",
|
||||
"@tanstack/query-core": "5.90.20"
|
||||
},
|
||||
"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",
|
||||
"test:types:ts51": "node ../../node_modules/typescript51/lib/tsc.js --build",
|
||||
"test:types:ts52": "node ../../node_modules/typescript52/lib/tsc.js --build",
|
||||
"test:types:ts53": "node ../../node_modules/typescript53/lib/tsc.js --build",
|
||||
"test:types:ts54": "node ../../node_modules/typescript54/lib/tsc.js --build",
|
||||
"test:types:ts55": "node ../../node_modules/typescript55/lib/tsc.js --build",
|
||||
"test:types:ts56": "node ../../node_modules/typescript56/lib/tsc.js --build",
|
||||
"test:types:ts57": "node ../../node_modules/typescript57/lib/tsc.js --build",
|
||||
"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",
|
||||
"build:dev": "tsup --watch"
|
||||
}
|
||||
}
|
||||
+3779
File diff suppressed because it is too large
Load Diff
+36
@@ -0,0 +1,36 @@
|
||||
import { createLocalStorage } from '@solid-primitives/storage'
|
||||
import { createMemo } from 'solid-js'
|
||||
import { Devtools } from './Devtools'
|
||||
import { getPreferredColorScheme } from './utils'
|
||||
import { THEME_PREFERENCE } from './constants'
|
||||
import { PiPProvider, QueryDevtoolsContext, ThemeContext } from './contexts'
|
||||
import type { Theme } from './contexts'
|
||||
import type { DevtoolsComponentType } from './Devtools'
|
||||
|
||||
const DevtoolsComponent: DevtoolsComponentType = (props) => {
|
||||
const [localStore, setLocalStore] = createLocalStorage({
|
||||
prefix: 'TanstackQueryDevtools',
|
||||
})
|
||||
|
||||
const colorScheme = getPreferredColorScheme()
|
||||
|
||||
const theme = createMemo(() => {
|
||||
const preference = (props.theme ||
|
||||
localStore.theme_preference ||
|
||||
THEME_PREFERENCE) as Theme
|
||||
if (preference !== 'system') return preference
|
||||
return colorScheme()
|
||||
})
|
||||
|
||||
return (
|
||||
<QueryDevtoolsContext.Provider value={props}>
|
||||
<PiPProvider localStore={localStore} setLocalStore={setLocalStore}>
|
||||
<ThemeContext.Provider value={theme}>
|
||||
<Devtools localStore={localStore} setLocalStore={setLocalStore} />
|
||||
</ThemeContext.Provider>
|
||||
</PiPProvider>
|
||||
</QueryDevtoolsContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export default DevtoolsComponent
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
import { createLocalStorage } from '@solid-primitives/storage'
|
||||
import { createMemo } from 'solid-js'
|
||||
import { ContentView, ParentPanel } from './Devtools'
|
||||
import { getPreferredColorScheme } from './utils'
|
||||
import { THEME_PREFERENCE } from './constants'
|
||||
import { PiPProvider, QueryDevtoolsContext, ThemeContext } from './contexts'
|
||||
import type { Theme } from './contexts'
|
||||
import type { DevtoolsComponentType } from './Devtools'
|
||||
|
||||
const DevtoolsPanelComponent: DevtoolsComponentType = (props) => {
|
||||
const [localStore, setLocalStore] = createLocalStorage({
|
||||
prefix: 'TanstackQueryDevtools',
|
||||
})
|
||||
|
||||
const colorScheme = getPreferredColorScheme()
|
||||
|
||||
const theme = createMemo(() => {
|
||||
const preference = (props.theme ||
|
||||
localStore.theme_preference ||
|
||||
THEME_PREFERENCE) as Theme
|
||||
if (preference !== 'system') return preference
|
||||
return colorScheme()
|
||||
})
|
||||
|
||||
return (
|
||||
<QueryDevtoolsContext.Provider value={props}>
|
||||
<PiPProvider
|
||||
disabled
|
||||
localStore={localStore}
|
||||
setLocalStore={setLocalStore}
|
||||
>
|
||||
<ThemeContext.Provider value={theme}>
|
||||
<ParentPanel>
|
||||
<ContentView
|
||||
localStore={localStore}
|
||||
setLocalStore={setLocalStore}
|
||||
onClose={props.onClose}
|
||||
showPanelViewOnly
|
||||
/>
|
||||
</ParentPanel>
|
||||
</ThemeContext.Provider>
|
||||
</PiPProvider>
|
||||
</QueryDevtoolsContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export default DevtoolsPanelComponent
|
||||
+674
@@ -0,0 +1,674 @@
|
||||
import { serialize, stringify } from 'superjson'
|
||||
import { clsx as cx } from 'clsx'
|
||||
import {
|
||||
Index,
|
||||
Match,
|
||||
Show,
|
||||
Switch,
|
||||
createMemo,
|
||||
createSignal,
|
||||
createUniqueId,
|
||||
} from 'solid-js'
|
||||
import { Key } from '@solid-primitives/keyed'
|
||||
import * as goober from 'goober'
|
||||
import { tokens } from './theme'
|
||||
import {
|
||||
deleteNestedDataByPath,
|
||||
displayValue,
|
||||
updateNestedDataByPath,
|
||||
} from './utils'
|
||||
import {
|
||||
Check,
|
||||
CopiedCopier,
|
||||
Copier,
|
||||
ErrorCopier,
|
||||
List,
|
||||
Pencil,
|
||||
Trash,
|
||||
} from './icons'
|
||||
import { useQueryDevtoolsContext, useTheme } from './contexts'
|
||||
import type { Query } from '@tanstack/query-core'
|
||||
|
||||
/**
|
||||
* Chunk elements in the array by size
|
||||
*
|
||||
* when the array cannot be chunked evenly by size, the last chunk will be
|
||||
* filled with the remaining elements
|
||||
*
|
||||
* @example
|
||||
* chunkArray(['a','b', 'c', 'd', 'e'], 2) // returns [['a','b'], ['c', 'd'], ['e']]
|
||||
*/
|
||||
function chunkArray<T extends { label: string; value: unknown }>(
|
||||
array: Array<T>,
|
||||
size: number,
|
||||
): Array<Array<T>> {
|
||||
if (size < 1) return []
|
||||
let i = 0
|
||||
const result: Array<Array<T>> = []
|
||||
while (i < array.length) {
|
||||
result.push(array.slice(i, i + size))
|
||||
i = i + size
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
const Expander = (props: { expanded: boolean }) => {
|
||||
const theme = useTheme()
|
||||
const css = useQueryDevtoolsContext().shadowDOMTarget
|
||||
? goober.css.bind({ target: useQueryDevtoolsContext().shadowDOMTarget })
|
||||
: goober.css
|
||||
const styles = createMemo(() => {
|
||||
return theme() === 'dark' ? darkStyles(css) : lightStyles(css)
|
||||
})
|
||||
|
||||
return (
|
||||
<span
|
||||
class={cx(
|
||||
styles().expander,
|
||||
css`
|
||||
transform: rotate(${props.expanded ? 90 : 0}deg);
|
||||
`,
|
||||
props.expanded &&
|
||||
css`
|
||||
& svg {
|
||||
top: -1px;
|
||||
}
|
||||
`,
|
||||
)}
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6 12L10 8L6 4"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
type CopyState = 'NoCopy' | 'SuccessCopy' | 'ErrorCopy'
|
||||
const CopyButton = (props: { value: unknown }) => {
|
||||
const theme = useTheme()
|
||||
const css = useQueryDevtoolsContext().shadowDOMTarget
|
||||
? goober.css.bind({ target: useQueryDevtoolsContext().shadowDOMTarget })
|
||||
: goober.css
|
||||
const styles = createMemo(() => {
|
||||
return theme() === 'dark' ? darkStyles(css) : lightStyles(css)
|
||||
})
|
||||
const [copyState, setCopyState] = createSignal<CopyState>('NoCopy')
|
||||
|
||||
return (
|
||||
<button
|
||||
class={styles().actionButton}
|
||||
title="Copy object to clipboard"
|
||||
aria-label={`${
|
||||
copyState() === 'NoCopy'
|
||||
? 'Copy object to clipboard'
|
||||
: copyState() === 'SuccessCopy'
|
||||
? 'Object copied to clipboard'
|
||||
: 'Error copying object to clipboard'
|
||||
}`}
|
||||
onClick={
|
||||
copyState() === 'NoCopy'
|
||||
? () => {
|
||||
navigator.clipboard.writeText(stringify(props.value)).then(
|
||||
() => {
|
||||
setCopyState('SuccessCopy')
|
||||
setTimeout(() => {
|
||||
setCopyState('NoCopy')
|
||||
}, 1500)
|
||||
},
|
||||
(err) => {
|
||||
console.error('Failed to copy: ', err)
|
||||
setCopyState('ErrorCopy')
|
||||
setTimeout(() => {
|
||||
setCopyState('NoCopy')
|
||||
}, 1500)
|
||||
},
|
||||
)
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<Switch>
|
||||
<Match when={copyState() === 'NoCopy'}>
|
||||
<Copier />
|
||||
</Match>
|
||||
<Match when={copyState() === 'SuccessCopy'}>
|
||||
<CopiedCopier theme={theme()} />
|
||||
</Match>
|
||||
<Match when={copyState() === 'ErrorCopy'}>
|
||||
<ErrorCopier />
|
||||
</Match>
|
||||
</Switch>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
const ClearArrayButton = (props: {
|
||||
dataPath: Array<string>
|
||||
activeQuery: Query
|
||||
}) => {
|
||||
const theme = useTheme()
|
||||
const css = useQueryDevtoolsContext().shadowDOMTarget
|
||||
? goober.css.bind({ target: useQueryDevtoolsContext().shadowDOMTarget })
|
||||
: goober.css
|
||||
const styles = createMemo(() => {
|
||||
return theme() === 'dark' ? darkStyles(css) : lightStyles(css)
|
||||
})
|
||||
const queryClient = useQueryDevtoolsContext().client
|
||||
|
||||
return (
|
||||
<button
|
||||
class={styles().actionButton}
|
||||
title={'Remove all items'}
|
||||
aria-label={'Remove all items'}
|
||||
onClick={() => {
|
||||
const oldData = props.activeQuery.state.data
|
||||
const newData = updateNestedDataByPath(oldData, props.dataPath, [])
|
||||
queryClient.setQueryData(props.activeQuery.queryKey, newData)
|
||||
}}
|
||||
>
|
||||
<List />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
const DeleteItemButton = (props: {
|
||||
dataPath: Array<string>
|
||||
activeQuery: Query
|
||||
}) => {
|
||||
const theme = useTheme()
|
||||
const css = useQueryDevtoolsContext().shadowDOMTarget
|
||||
? goober.css.bind({ target: useQueryDevtoolsContext().shadowDOMTarget })
|
||||
: goober.css
|
||||
const styles = createMemo(() => {
|
||||
return theme() === 'dark' ? darkStyles(css) : lightStyles(css)
|
||||
})
|
||||
const queryClient = useQueryDevtoolsContext().client
|
||||
|
||||
return (
|
||||
<button
|
||||
class={cx(styles().actionButton)}
|
||||
title={'Delete item'}
|
||||
aria-label={'Delete item'}
|
||||
onClick={() => {
|
||||
const oldData = props.activeQuery.state.data
|
||||
const newData = deleteNestedDataByPath(oldData, props.dataPath)
|
||||
queryClient.setQueryData(props.activeQuery.queryKey, newData)
|
||||
}}
|
||||
>
|
||||
<Trash />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
const ToggleValueButton = (props: {
|
||||
dataPath: Array<string>
|
||||
activeQuery: Query
|
||||
value: boolean
|
||||
}) => {
|
||||
const theme = useTheme()
|
||||
const css = useQueryDevtoolsContext().shadowDOMTarget
|
||||
? goober.css.bind({ target: useQueryDevtoolsContext().shadowDOMTarget })
|
||||
: goober.css
|
||||
const styles = createMemo(() => {
|
||||
return theme() === 'dark' ? darkStyles(css) : lightStyles(css)
|
||||
})
|
||||
const queryClient = useQueryDevtoolsContext().client
|
||||
|
||||
return (
|
||||
<button
|
||||
class={cx(
|
||||
styles().actionButton,
|
||||
css`
|
||||
width: ${tokens.size[3.5]};
|
||||
height: ${tokens.size[3.5]};
|
||||
`,
|
||||
)}
|
||||
title={'Toggle value'}
|
||||
aria-label={'Toggle value'}
|
||||
onClick={() => {
|
||||
const oldData = props.activeQuery.state.data
|
||||
const newData = updateNestedDataByPath(
|
||||
oldData,
|
||||
props.dataPath,
|
||||
!props.value,
|
||||
)
|
||||
queryClient.setQueryData(props.activeQuery.queryKey, newData)
|
||||
}}
|
||||
>
|
||||
<Check theme={theme()} checked={props.value} />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
type ExplorerProps = {
|
||||
editable?: boolean
|
||||
label: string
|
||||
value: unknown
|
||||
defaultExpanded?: Array<string>
|
||||
dataPath?: Array<string>
|
||||
activeQuery?: Query
|
||||
itemsDeletable?: boolean
|
||||
onEdit?: () => void
|
||||
}
|
||||
|
||||
function isIterable(x: any): x is Iterable<unknown> {
|
||||
return Symbol.iterator in x
|
||||
}
|
||||
|
||||
export default function Explorer(props: ExplorerProps) {
|
||||
const theme = useTheme()
|
||||
const css = useQueryDevtoolsContext().shadowDOMTarget
|
||||
? goober.css.bind({ target: useQueryDevtoolsContext().shadowDOMTarget })
|
||||
: goober.css
|
||||
const styles = createMemo(() => {
|
||||
return theme() === 'dark' ? darkStyles(css) : lightStyles(css)
|
||||
})
|
||||
const queryClient = useQueryDevtoolsContext().client
|
||||
|
||||
const [expanded, setExpanded] = createSignal(
|
||||
(props.defaultExpanded || []).includes(props.label),
|
||||
)
|
||||
const toggleExpanded = () => setExpanded((old) => !old)
|
||||
const [expandedPages, setExpandedPages] = createSignal<Array<number>>([])
|
||||
|
||||
const subEntries = createMemo(() => {
|
||||
if (Array.isArray(props.value)) {
|
||||
return props.value.map((d, i) => ({
|
||||
label: i.toString(),
|
||||
value: d,
|
||||
}))
|
||||
} else if (
|
||||
props.value !== null &&
|
||||
typeof props.value === 'object' &&
|
||||
isIterable(props.value) &&
|
||||
typeof props.value[Symbol.iterator] === 'function'
|
||||
) {
|
||||
if (props.value instanceof Map) {
|
||||
return Array.from(props.value, ([key, val]) => ({
|
||||
label: key,
|
||||
value: val,
|
||||
}))
|
||||
}
|
||||
return Array.from(props.value, (val, i) => ({
|
||||
label: i.toString(),
|
||||
value: val,
|
||||
}))
|
||||
} else if (typeof props.value === 'object' && props.value !== null) {
|
||||
return Object.entries(props.value).map(([key, val]) => ({
|
||||
label: key,
|
||||
value: val,
|
||||
}))
|
||||
}
|
||||
return []
|
||||
})
|
||||
|
||||
const type = createMemo<string>(() => {
|
||||
if (Array.isArray(props.value)) {
|
||||
return 'array'
|
||||
} else if (
|
||||
props.value !== null &&
|
||||
typeof props.value === 'object' &&
|
||||
isIterable(props.value) &&
|
||||
typeof props.value[Symbol.iterator] === 'function'
|
||||
) {
|
||||
return 'Iterable'
|
||||
} else if (typeof props.value === 'object' && props.value !== null) {
|
||||
return 'object'
|
||||
}
|
||||
return typeof props.value
|
||||
})
|
||||
|
||||
const subEntryPages = createMemo(() => chunkArray(subEntries(), 100))
|
||||
|
||||
const currentDataPath = props.dataPath ?? []
|
||||
|
||||
const inputId = createUniqueId()
|
||||
|
||||
return (
|
||||
<div class={styles().entry}>
|
||||
<Show when={subEntryPages().length}>
|
||||
<div class={styles().expanderButtonContainer}>
|
||||
<button
|
||||
class={styles().expanderButton}
|
||||
onClick={() => toggleExpanded()}
|
||||
aria-expanded={expanded() ? 'true' : 'false'}
|
||||
>
|
||||
<Expander expanded={expanded()} /> <span>{props.label}</span>{' '}
|
||||
<span class={styles().info}>
|
||||
{String(type()).toLowerCase() === 'iterable' ? '(Iterable) ' : ''}
|
||||
{subEntries().length} {subEntries().length > 1 ? `items` : `item`}
|
||||
</span>
|
||||
</button>
|
||||
<Show when={props.editable}>
|
||||
<div class={styles().actions}>
|
||||
<CopyButton value={props.value} />
|
||||
|
||||
<Show
|
||||
when={props.itemsDeletable && props.activeQuery !== undefined}
|
||||
>
|
||||
<DeleteItemButton
|
||||
activeQuery={props.activeQuery!}
|
||||
dataPath={currentDataPath}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
<Show
|
||||
when={type() === 'array' && props.activeQuery !== undefined}
|
||||
>
|
||||
<ClearArrayButton
|
||||
activeQuery={props.activeQuery!}
|
||||
dataPath={currentDataPath}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
<Show when={!!props.onEdit && !serialize(props.value).meta}>
|
||||
<button
|
||||
class={styles().actionButton}
|
||||
title={'Bulk Edit Data'}
|
||||
aria-label={'Bulk Edit Data'}
|
||||
onClick={() => {
|
||||
props.onEdit?.()
|
||||
}}
|
||||
>
|
||||
<Pencil />
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={expanded()}>
|
||||
<Show when={subEntryPages().length === 1}>
|
||||
<div class={styles().subEntry}>
|
||||
<Key each={subEntries()} by={(item) => item.label}>
|
||||
{(entry) => {
|
||||
return (
|
||||
<Explorer
|
||||
defaultExpanded={props.defaultExpanded}
|
||||
label={entry().label}
|
||||
value={entry().value}
|
||||
editable={props.editable}
|
||||
dataPath={[...currentDataPath, entry().label]}
|
||||
activeQuery={props.activeQuery}
|
||||
itemsDeletable={
|
||||
type() === 'array' ||
|
||||
type() === 'Iterable' ||
|
||||
type() === 'object'
|
||||
}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
</Key>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={subEntryPages().length > 1}>
|
||||
<div class={styles().subEntry}>
|
||||
<Index each={subEntryPages()}>
|
||||
{(entries, index) => (
|
||||
<div>
|
||||
<div class={styles().entry}>
|
||||
<button
|
||||
onClick={() =>
|
||||
setExpandedPages((old) =>
|
||||
old.includes(index)
|
||||
? old.filter((d) => d !== index)
|
||||
: [...old, index],
|
||||
)
|
||||
}
|
||||
class={styles().expanderButton}
|
||||
>
|
||||
<Expander expanded={expandedPages().includes(index)} />{' '}
|
||||
[{index * 100}...
|
||||
{index * 100 + 100 - 1}]
|
||||
</button>
|
||||
<Show when={expandedPages().includes(index)}>
|
||||
<div class={styles().subEntry}>
|
||||
<Key each={entries()} by={(entry) => entry.label}>
|
||||
{(entry) => (
|
||||
<Explorer
|
||||
defaultExpanded={props.defaultExpanded}
|
||||
label={entry().label}
|
||||
value={entry().value}
|
||||
editable={props.editable}
|
||||
dataPath={[...currentDataPath, entry().label]}
|
||||
activeQuery={props.activeQuery}
|
||||
/>
|
||||
)}
|
||||
</Key>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Index>
|
||||
</div>
|
||||
</Show>
|
||||
</Show>
|
||||
</Show>
|
||||
<Show when={subEntryPages().length === 0}>
|
||||
<div class={styles().row}>
|
||||
<label for={inputId} class={styles().label}>
|
||||
{props.label}:
|
||||
</label>
|
||||
<Show
|
||||
when={
|
||||
props.editable &&
|
||||
props.activeQuery !== undefined &&
|
||||
(type() === 'string' ||
|
||||
type() === 'number' ||
|
||||
type() === 'boolean')
|
||||
}
|
||||
fallback={
|
||||
<span class={styles().value}>{displayValue(props.value)}</span>
|
||||
}
|
||||
>
|
||||
<Show
|
||||
when={
|
||||
props.editable &&
|
||||
props.activeQuery !== undefined &&
|
||||
(type() === 'string' || type() === 'number')
|
||||
}
|
||||
>
|
||||
<input
|
||||
id={inputId}
|
||||
type={type() === 'number' ? 'number' : 'text'}
|
||||
class={cx(styles().value, styles().editableInput)}
|
||||
value={props.value as string | number}
|
||||
onChange={(changeEvent) => {
|
||||
const oldData = props.activeQuery!.state.data
|
||||
|
||||
const newData = updateNestedDataByPath(
|
||||
oldData,
|
||||
currentDataPath,
|
||||
type() === 'number'
|
||||
? changeEvent.target.valueAsNumber
|
||||
: changeEvent.target.value,
|
||||
)
|
||||
|
||||
queryClient.setQueryData(props.activeQuery!.queryKey, newData)
|
||||
}}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
<Show when={type() === 'boolean'}>
|
||||
<span
|
||||
class={cx(
|
||||
styles().value,
|
||||
styles().actions,
|
||||
styles().editableInput,
|
||||
)}
|
||||
>
|
||||
<ToggleValueButton
|
||||
activeQuery={props.activeQuery!}
|
||||
dataPath={currentDataPath}
|
||||
value={props.value as boolean}
|
||||
/>
|
||||
{displayValue(props.value)}
|
||||
</span>
|
||||
</Show>
|
||||
</Show>
|
||||
|
||||
<Show
|
||||
when={
|
||||
props.editable &&
|
||||
props.itemsDeletable &&
|
||||
props.activeQuery !== undefined
|
||||
}
|
||||
>
|
||||
<DeleteItemButton
|
||||
activeQuery={props.activeQuery!}
|
||||
dataPath={currentDataPath}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const stylesFactory = (
|
||||
theme: 'light' | 'dark',
|
||||
css: (typeof goober)['css'],
|
||||
) => {
|
||||
const { colors, font, size, border } = tokens
|
||||
const t = (light: string, dark: string) => (theme === 'light' ? light : dark)
|
||||
return {
|
||||
entry: css`
|
||||
& * {
|
||||
font-size: ${font.size.xs};
|
||||
font-family:
|
||||
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
||||
'Liberation Mono', 'Courier New', monospace;
|
||||
}
|
||||
position: relative;
|
||||
outline: none;
|
||||
word-break: break-word;
|
||||
`,
|
||||
subEntry: css`
|
||||
margin: 0 0 0 0.5em;
|
||||
padding-left: 0.75em;
|
||||
border-left: 2px solid ${t(colors.gray[300], colors.darkGray[400])};
|
||||
/* outline: 1px solid ${colors.teal[400]}; */
|
||||
`,
|
||||
expander: css`
|
||||
& path {
|
||||
stroke: ${colors.gray[400]};
|
||||
}
|
||||
& svg {
|
||||
width: ${size[3]};
|
||||
height: ${size[3]};
|
||||
}
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
transition: all 0.1s ease;
|
||||
/* outline: 1px solid ${colors.blue[400]}; */
|
||||
`,
|
||||
expanderButtonContainer: css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: ${size[4]};
|
||||
min-height: ${size[4]};
|
||||
gap: ${size[2]};
|
||||
`,
|
||||
expanderButton: css`
|
||||
cursor: pointer;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
outline: inherit;
|
||||
height: ${size[5]};
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: ${size[1]};
|
||||
position: relative;
|
||||
/* outline: 1px solid ${colors.green[400]}; */
|
||||
|
||||
&:focus-visible {
|
||||
border-radius: ${border.radius.xs};
|
||||
outline: 2px solid ${colors.blue[800]};
|
||||
}
|
||||
|
||||
& svg {
|
||||
position: relative;
|
||||
left: 1px;
|
||||
}
|
||||
`,
|
||||
info: css`
|
||||
color: ${t(colors.gray[500], colors.gray[500])};
|
||||
font-size: ${font.size.xs};
|
||||
margin-left: ${size[1]};
|
||||
/* outline: 1px solid ${colors.yellow[400]}; */
|
||||
`,
|
||||
label: css`
|
||||
color: ${t(colors.gray[700], colors.gray[300])};
|
||||
white-space: nowrap;
|
||||
`,
|
||||
value: css`
|
||||
color: ${t(colors.purple[600], colors.purple[400])};
|
||||
flex-grow: 1;
|
||||
`,
|
||||
actions: css`
|
||||
display: inline-flex;
|
||||
gap: ${size[2]};
|
||||
align-items: center;
|
||||
`,
|
||||
row: css`
|
||||
display: inline-flex;
|
||||
gap: ${size[2]};
|
||||
width: 100%;
|
||||
margin: ${size[0.25]} 0px;
|
||||
line-height: ${size[4.5]};
|
||||
align-items: center;
|
||||
`,
|
||||
editableInput: css`
|
||||
border: none;
|
||||
padding: ${size[0.5]} ${size[1]} ${size[0.5]} ${size[1.5]};
|
||||
flex-grow: 1;
|
||||
border-radius: ${border.radius.xs};
|
||||
background-color: ${t(colors.gray[200], colors.darkGray[500])};
|
||||
|
||||
&:hover {
|
||||
background-color: ${t(colors.gray[300], colors.darkGray[600])};
|
||||
}
|
||||
`,
|
||||
actionButton: css`
|
||||
background-color: transparent;
|
||||
color: ${t(colors.gray[500], colors.gray[500])};
|
||||
border: none;
|
||||
display: inline-flex;
|
||||
padding: 0px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
width: ${size[3]};
|
||||
height: ${size[3]};
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
&:hover svg {
|
||||
color: ${t(colors.gray[600], colors.gray[400])};
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
border-radius: ${border.radius.xs};
|
||||
outline: 2px solid ${colors.blue[800]};
|
||||
outline-offset: 2px;
|
||||
}
|
||||
`,
|
||||
}
|
||||
}
|
||||
|
||||
const lightStyles = (css: (typeof goober)['css']) => stylesFactory('light', css)
|
||||
const darkStyles = (css: (typeof goober)['css']) => stylesFactory('dark', css)
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
import { render } from 'solid-js/web'
|
||||
import { createSignal, lazy } from 'solid-js'
|
||||
import { setupStyleSheet } from './utils'
|
||||
import type {
|
||||
QueryClient,
|
||||
onlineManager as TOnlineManager,
|
||||
} from '@tanstack/query-core'
|
||||
import type { DevtoolsComponentType } from './Devtools'
|
||||
import type {
|
||||
DevtoolsButtonPosition,
|
||||
DevtoolsErrorType,
|
||||
DevtoolsPosition,
|
||||
QueryDevtoolsProps,
|
||||
Theme,
|
||||
} from './contexts'
|
||||
import type { Signal } from 'solid-js'
|
||||
|
||||
export interface TanstackQueryDevtoolsConfig extends QueryDevtoolsProps {
|
||||
styleNonce?: string
|
||||
shadowDOMTarget?: ShadowRoot
|
||||
}
|
||||
|
||||
class TanstackQueryDevtools {
|
||||
#client: Signal<QueryClient>
|
||||
#onlineManager: typeof TOnlineManager
|
||||
#queryFlavor: string
|
||||
#version: string
|
||||
#isMounted = false
|
||||
#styleNonce?: string
|
||||
#shadowDOMTarget?: ShadowRoot
|
||||
#buttonPosition: Signal<DevtoolsButtonPosition | undefined>
|
||||
#position: Signal<DevtoolsPosition | undefined>
|
||||
#initialIsOpen: Signal<boolean | undefined>
|
||||
#errorTypes: Signal<Array<DevtoolsErrorType> | undefined>
|
||||
#hideDisabledQueries: Signal<boolean | undefined>
|
||||
#Component: DevtoolsComponentType | undefined
|
||||
#theme: Signal<Theme | undefined>
|
||||
#dispose?: () => void
|
||||
|
||||
constructor(config: TanstackQueryDevtoolsConfig) {
|
||||
const {
|
||||
client,
|
||||
queryFlavor,
|
||||
version,
|
||||
onlineManager,
|
||||
buttonPosition,
|
||||
position,
|
||||
initialIsOpen,
|
||||
errorTypes,
|
||||
styleNonce,
|
||||
shadowDOMTarget,
|
||||
hideDisabledQueries,
|
||||
theme,
|
||||
} = config
|
||||
this.#client = createSignal(client)
|
||||
this.#queryFlavor = queryFlavor
|
||||
this.#version = version
|
||||
this.#onlineManager = onlineManager
|
||||
this.#styleNonce = styleNonce
|
||||
this.#shadowDOMTarget = shadowDOMTarget
|
||||
this.#buttonPosition = createSignal(buttonPosition)
|
||||
this.#position = createSignal(position)
|
||||
this.#initialIsOpen = createSignal(initialIsOpen)
|
||||
this.#errorTypes = createSignal(errorTypes)
|
||||
this.#hideDisabledQueries = createSignal(hideDisabledQueries)
|
||||
this.#theme = createSignal(theme)
|
||||
}
|
||||
|
||||
setButtonPosition(position: DevtoolsButtonPosition) {
|
||||
this.#buttonPosition[1](position)
|
||||
}
|
||||
|
||||
setPosition(position: DevtoolsPosition) {
|
||||
this.#position[1](position)
|
||||
}
|
||||
|
||||
setInitialIsOpen(isOpen: boolean) {
|
||||
this.#initialIsOpen[1](isOpen)
|
||||
}
|
||||
|
||||
setErrorTypes(errorTypes: Array<DevtoolsErrorType>) {
|
||||
this.#errorTypes[1](errorTypes)
|
||||
}
|
||||
|
||||
setClient(client: QueryClient) {
|
||||
this.#client[1](client)
|
||||
}
|
||||
|
||||
setTheme(theme?: Theme) {
|
||||
this.#theme[1](theme)
|
||||
}
|
||||
|
||||
mount<T extends HTMLElement>(el: T) {
|
||||
if (this.#isMounted) {
|
||||
throw new Error('Devtools is already mounted')
|
||||
}
|
||||
const dispose = render(() => {
|
||||
const [btnPosition] = this.#buttonPosition
|
||||
const [pos] = this.#position
|
||||
const [isOpen] = this.#initialIsOpen
|
||||
const [errors] = this.#errorTypes
|
||||
const [hideDisabledQueries] = this.#hideDisabledQueries
|
||||
const [queryClient] = this.#client
|
||||
const [theme] = this.#theme
|
||||
let Devtools: DevtoolsComponentType
|
||||
|
||||
if (this.#Component) {
|
||||
Devtools = this.#Component
|
||||
} else {
|
||||
Devtools = lazy(() => import('./DevtoolsComponent'))
|
||||
this.#Component = Devtools
|
||||
}
|
||||
|
||||
setupStyleSheet(this.#styleNonce, this.#shadowDOMTarget)
|
||||
return (
|
||||
<Devtools
|
||||
queryFlavor={this.#queryFlavor}
|
||||
version={this.#version}
|
||||
onlineManager={this.#onlineManager}
|
||||
shadowDOMTarget={this.#shadowDOMTarget}
|
||||
{...{
|
||||
get client() {
|
||||
return queryClient()
|
||||
},
|
||||
get buttonPosition() {
|
||||
return btnPosition()
|
||||
},
|
||||
get position() {
|
||||
return pos()
|
||||
},
|
||||
get initialIsOpen() {
|
||||
return isOpen()
|
||||
},
|
||||
get errorTypes() {
|
||||
return errors()
|
||||
},
|
||||
get hideDisabledQueries() {
|
||||
return hideDisabledQueries()
|
||||
},
|
||||
get theme() {
|
||||
return theme()
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}, el)
|
||||
this.#isMounted = true
|
||||
this.#dispose = dispose
|
||||
}
|
||||
|
||||
unmount() {
|
||||
if (!this.#isMounted) {
|
||||
throw new Error('Devtools is not mounted')
|
||||
}
|
||||
this.#dispose?.()
|
||||
this.#isMounted = false
|
||||
}
|
||||
}
|
||||
|
||||
export { TanstackQueryDevtools }
|
||||
Generated
Vendored
+172
@@ -0,0 +1,172 @@
|
||||
import { render } from 'solid-js/web'
|
||||
import { createSignal, lazy } from 'solid-js'
|
||||
import { setupStyleSheet } from './utils'
|
||||
import type {
|
||||
QueryClient,
|
||||
onlineManager as TOnlineManager,
|
||||
} from '@tanstack/query-core'
|
||||
import type { DevtoolsComponentType } from './Devtools'
|
||||
import type {
|
||||
DevtoolsButtonPosition,
|
||||
DevtoolsErrorType,
|
||||
DevtoolsPosition,
|
||||
QueryDevtoolsProps,
|
||||
Theme,
|
||||
} from './contexts'
|
||||
import type { Signal } from 'solid-js'
|
||||
|
||||
export interface TanstackQueryDevtoolsPanelConfig extends QueryDevtoolsProps {
|
||||
styleNonce?: string
|
||||
shadowDOMTarget?: ShadowRoot
|
||||
onClose?: () => unknown
|
||||
}
|
||||
|
||||
class TanstackQueryDevtoolsPanel {
|
||||
#client: Signal<QueryClient>
|
||||
#onlineManager: typeof TOnlineManager
|
||||
#queryFlavor: string
|
||||
#version: string
|
||||
#isMounted = false
|
||||
#styleNonce?: string
|
||||
#shadowDOMTarget?: ShadowRoot
|
||||
#buttonPosition: Signal<DevtoolsButtonPosition | undefined>
|
||||
#position: Signal<DevtoolsPosition | undefined>
|
||||
#initialIsOpen: Signal<boolean | undefined>
|
||||
#errorTypes: Signal<Array<DevtoolsErrorType> | undefined>
|
||||
#hideDisabledQueries: Signal<boolean | undefined>
|
||||
#onClose: Signal<(() => unknown) | undefined>
|
||||
#Component: DevtoolsComponentType | undefined
|
||||
#theme: Signal<Theme | undefined>
|
||||
#dispose?: () => void
|
||||
|
||||
constructor(config: TanstackQueryDevtoolsPanelConfig) {
|
||||
const {
|
||||
client,
|
||||
queryFlavor,
|
||||
version,
|
||||
onlineManager,
|
||||
buttonPosition,
|
||||
position,
|
||||
initialIsOpen,
|
||||
errorTypes,
|
||||
styleNonce,
|
||||
shadowDOMTarget,
|
||||
onClose,
|
||||
hideDisabledQueries,
|
||||
theme,
|
||||
} = config
|
||||
this.#client = createSignal(client)
|
||||
this.#queryFlavor = queryFlavor
|
||||
this.#version = version
|
||||
this.#onlineManager = onlineManager
|
||||
this.#styleNonce = styleNonce
|
||||
this.#shadowDOMTarget = shadowDOMTarget
|
||||
this.#buttonPosition = createSignal(buttonPosition)
|
||||
this.#position = createSignal(position)
|
||||
this.#initialIsOpen = createSignal(initialIsOpen)
|
||||
this.#errorTypes = createSignal(errorTypes)
|
||||
this.#hideDisabledQueries = createSignal(hideDisabledQueries)
|
||||
this.#onClose = createSignal(onClose)
|
||||
this.#theme = createSignal(theme)
|
||||
}
|
||||
|
||||
setButtonPosition(position: DevtoolsButtonPosition) {
|
||||
this.#buttonPosition[1](position)
|
||||
}
|
||||
|
||||
setPosition(position: DevtoolsPosition) {
|
||||
this.#position[1](position)
|
||||
}
|
||||
|
||||
setInitialIsOpen(isOpen: boolean) {
|
||||
this.#initialIsOpen[1](isOpen)
|
||||
}
|
||||
|
||||
setErrorTypes(errorTypes: Array<DevtoolsErrorType>) {
|
||||
this.#errorTypes[1](errorTypes)
|
||||
}
|
||||
|
||||
setClient(client: QueryClient) {
|
||||
this.#client[1](client)
|
||||
}
|
||||
|
||||
setOnClose(onClose: () => unknown) {
|
||||
this.#onClose[1](() => onClose)
|
||||
}
|
||||
|
||||
setTheme(theme?: Theme) {
|
||||
this.#theme[1](theme)
|
||||
}
|
||||
|
||||
mount<T extends HTMLElement>(el: T) {
|
||||
if (this.#isMounted) {
|
||||
throw new Error('Devtools is already mounted')
|
||||
}
|
||||
const dispose = render(() => {
|
||||
const [btnPosition] = this.#buttonPosition
|
||||
const [pos] = this.#position
|
||||
const [isOpen] = this.#initialIsOpen
|
||||
const [errors] = this.#errorTypes
|
||||
const [hideDisabledQueries] = this.#hideDisabledQueries
|
||||
const [queryClient] = this.#client
|
||||
const [onClose] = this.#onClose
|
||||
const [theme] = this.#theme
|
||||
let Devtools: DevtoolsComponentType
|
||||
|
||||
if (this.#Component) {
|
||||
Devtools = this.#Component
|
||||
} else {
|
||||
Devtools = lazy(() => import('./DevtoolsPanelComponent'))
|
||||
this.#Component = Devtools
|
||||
}
|
||||
|
||||
setupStyleSheet(this.#styleNonce, this.#shadowDOMTarget)
|
||||
return (
|
||||
<Devtools
|
||||
queryFlavor={this.#queryFlavor}
|
||||
version={this.#version}
|
||||
onlineManager={this.#onlineManager}
|
||||
shadowDOMTarget={this.#shadowDOMTarget}
|
||||
{...{
|
||||
get client() {
|
||||
return queryClient()
|
||||
},
|
||||
get buttonPosition() {
|
||||
return btnPosition()
|
||||
},
|
||||
get position() {
|
||||
return pos()
|
||||
},
|
||||
get initialIsOpen() {
|
||||
return isOpen()
|
||||
},
|
||||
get errorTypes() {
|
||||
return errors()
|
||||
},
|
||||
get hideDisabledQueries() {
|
||||
return hideDisabledQueries()
|
||||
},
|
||||
get onClose() {
|
||||
return onClose()
|
||||
},
|
||||
get theme() {
|
||||
return theme()
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}, el)
|
||||
this.#isMounted = true
|
||||
this.#dispose = dispose
|
||||
}
|
||||
|
||||
unmount() {
|
||||
if (!this.#isMounted) {
|
||||
throw new Error('Devtools is not mounted')
|
||||
}
|
||||
this.#dispose?.()
|
||||
this.#isMounted = false
|
||||
}
|
||||
}
|
||||
|
||||
export { TanstackQueryDevtoolsPanel }
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import { mutationSortFns, sortFns } from './utils'
|
||||
import type { DevtoolsButtonPosition, DevtoolsPosition } from './contexts'
|
||||
|
||||
export const firstBreakpoint = 1024
|
||||
export const secondBreakpoint = 796
|
||||
export const thirdBreakpoint = 700
|
||||
|
||||
export const BUTTON_POSITION: DevtoolsButtonPosition = 'bottom-right'
|
||||
export const POSITION: DevtoolsPosition = 'bottom'
|
||||
export const THEME_PREFERENCE = 'system'
|
||||
export const INITIAL_IS_OPEN = false
|
||||
export const DEFAULT_HEIGHT = 500
|
||||
export const PIP_DEFAULT_HEIGHT = 500
|
||||
export const DEFAULT_WIDTH = 500
|
||||
export const DEFAULT_SORT_FN_NAME = Object.keys(sortFns)[0]
|
||||
export const DEFAULT_SORT_ORDER = 1
|
||||
export const DEFAULT_MUTATION_SORT_FN_NAME = Object.keys(mutationSortFns)[0]
|
||||
+203
@@ -0,0 +1,203 @@
|
||||
import {
|
||||
createContext,
|
||||
createEffect,
|
||||
createMemo,
|
||||
createSignal,
|
||||
onCleanup,
|
||||
useContext,
|
||||
} from 'solid-js'
|
||||
import { clearDelegatedEvents, delegateEvents } from 'solid-js/web'
|
||||
import { PIP_DEFAULT_HEIGHT } from '../constants'
|
||||
import { useQueryDevtoolsContext } from './QueryDevtoolsContext'
|
||||
import type { Accessor, JSX } from 'solid-js'
|
||||
import type { StorageObject, StorageSetter } from '@solid-primitives/storage'
|
||||
|
||||
interface PiPProviderProps {
|
||||
children: JSX.Element
|
||||
localStore: StorageObject<string>
|
||||
setLocalStore: StorageSetter<string, unknown>
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
type PiPContextType = {
|
||||
pipWindow: Window | null
|
||||
requestPipWindow: (width: number, height: number) => void
|
||||
closePipWindow: () => void
|
||||
disabled: boolean
|
||||
}
|
||||
|
||||
class PipOpenError extends Error {}
|
||||
|
||||
const PiPContext = createContext<Accessor<PiPContextType> | undefined>(
|
||||
undefined,
|
||||
)
|
||||
|
||||
export const PiPProvider = (props: PiPProviderProps) => {
|
||||
// Expose pipWindow that is currently active
|
||||
const [pipWindow, setPipWindow] = createSignal<Window | null>(null)
|
||||
|
||||
// Close pipWindow programmatically
|
||||
const closePipWindow = () => {
|
||||
const w = pipWindow()
|
||||
if (w != null) {
|
||||
w.close()
|
||||
setPipWindow(null)
|
||||
}
|
||||
}
|
||||
|
||||
// Open new pipWindow
|
||||
const requestPipWindow = (width: number, height: number) => {
|
||||
// We don't want to allow multiple requests.
|
||||
if (pipWindow() != null) {
|
||||
return
|
||||
}
|
||||
|
||||
const pip = window.open(
|
||||
'',
|
||||
'TSQD-Devtools-Panel',
|
||||
`width=${width},height=${height},popup`,
|
||||
)
|
||||
|
||||
if (!pip) {
|
||||
throw new PipOpenError(
|
||||
'Failed to open popup. Please allow popups for this site to view the devtools in picture-in-picture mode.',
|
||||
)
|
||||
}
|
||||
|
||||
// Remove existing styles
|
||||
pip.document.head.innerHTML = ''
|
||||
// Remove existing body
|
||||
pip.document.body.innerHTML = ''
|
||||
// Clear Delegated Events
|
||||
clearDelegatedEvents(pip.document)
|
||||
|
||||
pip.document.title = 'TanStack Query Devtools'
|
||||
pip.document.body.style.margin = '0'
|
||||
|
||||
// Detect when window is closed by user
|
||||
pip.addEventListener('pagehide', () => {
|
||||
props.setLocalStore('pip_open', 'false')
|
||||
setPipWindow(null)
|
||||
})
|
||||
|
||||
// It is important to copy all parent window styles. Otherwise, there would be no CSS available at all
|
||||
// https://developer.chrome.com/docs/web-platform/document-picture-in-picture/#copy-style-sheets-to-the-picture-in-picture-window
|
||||
;[
|
||||
...(useQueryDevtoolsContext().shadowDOMTarget || document).styleSheets,
|
||||
].forEach((styleSheet) => {
|
||||
try {
|
||||
const cssRules = [...styleSheet.cssRules]
|
||||
.map((rule) => rule.cssText)
|
||||
.join('')
|
||||
const style = document.createElement('style')
|
||||
const style_node = styleSheet.ownerNode
|
||||
let style_id = ''
|
||||
|
||||
if (style_node && 'id' in style_node) {
|
||||
style_id = style_node.id
|
||||
}
|
||||
|
||||
if (style_id) {
|
||||
style.setAttribute('id', style_id)
|
||||
}
|
||||
style.textContent = cssRules
|
||||
pip.document.head.appendChild(style)
|
||||
} catch (e) {
|
||||
const link = document.createElement('link')
|
||||
if (styleSheet.href == null) {
|
||||
return
|
||||
}
|
||||
|
||||
link.rel = 'stylesheet'
|
||||
link.type = styleSheet.type
|
||||
link.media = styleSheet.media.toString()
|
||||
link.href = styleSheet.href
|
||||
pip.document.head.appendChild(link)
|
||||
}
|
||||
})
|
||||
delegateEvents(
|
||||
[
|
||||
'focusin',
|
||||
'focusout',
|
||||
'pointermove',
|
||||
'keydown',
|
||||
'pointerdown',
|
||||
'pointerup',
|
||||
'click',
|
||||
'mousedown',
|
||||
'input',
|
||||
],
|
||||
pip.document,
|
||||
)
|
||||
props.setLocalStore('pip_open', 'true')
|
||||
setPipWindow(pip)
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
const pip_open = (props.localStore.pip_open ?? 'false') as 'true' | 'false'
|
||||
if (pip_open === 'true' && !props.disabled) {
|
||||
try {
|
||||
requestPipWindow(
|
||||
Number(window.innerWidth),
|
||||
Number(props.localStore.height || PIP_DEFAULT_HEIGHT),
|
||||
)
|
||||
} catch (error) {
|
||||
if (error instanceof PipOpenError) {
|
||||
console.error(error.message)
|
||||
props.setLocalStore('pip_open', 'false')
|
||||
props.setLocalStore('open', 'false')
|
||||
return
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
// Setup mutation observer for goober styles with id `_goober
|
||||
const gooberStyles = (
|
||||
useQueryDevtoolsContext().shadowDOMTarget || document
|
||||
).querySelector('#_goober')
|
||||
const w = pipWindow()
|
||||
if (gooberStyles && w) {
|
||||
const observer = new MutationObserver(() => {
|
||||
const pip_style = (
|
||||
useQueryDevtoolsContext().shadowDOMTarget || w.document
|
||||
).querySelector('#_goober')
|
||||
if (pip_style) {
|
||||
pip_style.textContent = gooberStyles.textContent
|
||||
}
|
||||
})
|
||||
observer.observe(gooberStyles, {
|
||||
childList: true, // observe direct children
|
||||
subtree: true, // and lower descendants too
|
||||
characterDataOldValue: true, // pass old data to callback
|
||||
})
|
||||
onCleanup(() => {
|
||||
observer.disconnect()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const value = createMemo(() => ({
|
||||
pipWindow: pipWindow(),
|
||||
requestPipWindow,
|
||||
closePipWindow,
|
||||
disabled: props.disabled ?? false,
|
||||
}))
|
||||
|
||||
return (
|
||||
<PiPContext.Provider value={value}>{props.children}</PiPContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const usePiPWindow = () => {
|
||||
const context = createMemo(() => {
|
||||
const ctx = useContext(PiPContext)
|
||||
if (!ctx) {
|
||||
throw new Error('usePiPWindow must be used within a PiPProvider')
|
||||
}
|
||||
return ctx()
|
||||
})
|
||||
return context
|
||||
}
|
||||
Generated
Vendored
+47
@@ -0,0 +1,47 @@
|
||||
import { createContext, useContext } from 'solid-js'
|
||||
import type { Query, QueryClient, onlineManager } from '@tanstack/query-core'
|
||||
|
||||
type XPosition = 'left' | 'right'
|
||||
type YPosition = 'top' | 'bottom'
|
||||
export type DevtoolsPosition = XPosition | YPosition
|
||||
export type DevtoolsButtonPosition = `${YPosition}-${XPosition}` | 'relative'
|
||||
export type Theme = 'dark' | 'light' | 'system'
|
||||
|
||||
export interface DevtoolsErrorType {
|
||||
/**
|
||||
* The name of the error.
|
||||
*/
|
||||
name: string
|
||||
/**
|
||||
* How the error is initialized.
|
||||
*/
|
||||
initializer: (query: Query) => Error
|
||||
}
|
||||
|
||||
export interface QueryDevtoolsProps {
|
||||
readonly client: QueryClient
|
||||
queryFlavor: string
|
||||
version: string
|
||||
onlineManager: typeof onlineManager
|
||||
|
||||
buttonPosition?: DevtoolsButtonPosition
|
||||
position?: DevtoolsPosition
|
||||
initialIsOpen?: boolean
|
||||
errorTypes?: Array<DevtoolsErrorType>
|
||||
shadowDOMTarget?: ShadowRoot
|
||||
onClose?: () => unknown
|
||||
hideDisabledQueries?: boolean
|
||||
theme?: Theme
|
||||
}
|
||||
|
||||
export const QueryDevtoolsContext = createContext<QueryDevtoolsProps>({
|
||||
client: undefined as unknown as QueryClient,
|
||||
onlineManager: undefined as unknown as typeof onlineManager,
|
||||
queryFlavor: '',
|
||||
version: '',
|
||||
shadowDOMTarget: undefined,
|
||||
})
|
||||
|
||||
export function useQueryDevtoolsContext() {
|
||||
return useContext(QueryDevtoolsContext)
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { createContext, useContext } from 'solid-js'
|
||||
import type { Accessor } from 'solid-js'
|
||||
|
||||
export const ThemeContext = createContext<Accessor<'light' | 'dark'>>(
|
||||
() => 'dark' as const,
|
||||
)
|
||||
|
||||
export function useTheme() {
|
||||
return useContext(ThemeContext)
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
export * from './PiPContext'
|
||||
export * from './QueryDevtoolsContext'
|
||||
export * from './ThemeContext'
|
||||
+1385
File diff suppressed because it is too large
Load Diff
+14
@@ -0,0 +1,14 @@
|
||||
export type {
|
||||
DevtoolsButtonPosition,
|
||||
DevtoolsErrorType,
|
||||
DevtoolsPosition,
|
||||
Theme,
|
||||
} from './contexts'
|
||||
export {
|
||||
TanstackQueryDevtools,
|
||||
type TanstackQueryDevtoolsConfig,
|
||||
} from './TanstackQueryDevtools'
|
||||
export {
|
||||
TanstackQueryDevtoolsPanel,
|
||||
type TanstackQueryDevtoolsPanelConfig,
|
||||
} from './TanstackQueryDevtoolsPanel'
|
||||
+299
@@ -0,0 +1,299 @@
|
||||
export const tokens = {
|
||||
colors: {
|
||||
inherit: 'inherit',
|
||||
current: 'currentColor',
|
||||
transparent: 'transparent',
|
||||
black: '#000000',
|
||||
white: '#ffffff',
|
||||
neutral: {
|
||||
50: '#f9fafb',
|
||||
100: '#f2f4f7',
|
||||
200: '#eaecf0',
|
||||
300: '#d0d5dd',
|
||||
400: '#98a2b3',
|
||||
500: '#667085',
|
||||
600: '#475467',
|
||||
700: '#344054',
|
||||
800: '#1d2939',
|
||||
900: '#101828',
|
||||
},
|
||||
darkGray: {
|
||||
50: '#525c7a',
|
||||
100: '#49536e',
|
||||
200: '#414962',
|
||||
300: '#394056',
|
||||
400: '#313749',
|
||||
500: '#292e3d',
|
||||
600: '#212530',
|
||||
700: '#191c24',
|
||||
800: '#111318',
|
||||
900: '#0b0d10',
|
||||
},
|
||||
gray: {
|
||||
50: '#f9fafb',
|
||||
100: '#f2f4f7',
|
||||
200: '#eaecf0',
|
||||
300: '#d0d5dd',
|
||||
400: '#98a2b3',
|
||||
500: '#667085',
|
||||
600: '#475467',
|
||||
700: '#344054',
|
||||
800: '#1d2939',
|
||||
900: '#101828',
|
||||
},
|
||||
blue: {
|
||||
25: '#F5FAFF',
|
||||
50: '#EFF8FF',
|
||||
100: '#D1E9FF',
|
||||
200: '#B2DDFF',
|
||||
300: '#84CAFF',
|
||||
400: '#53B1FD',
|
||||
500: '#2E90FA',
|
||||
600: '#1570EF',
|
||||
700: '#175CD3',
|
||||
800: '#1849A9',
|
||||
900: '#194185',
|
||||
},
|
||||
green: {
|
||||
25: '#F6FEF9',
|
||||
50: '#ECFDF3',
|
||||
100: '#D1FADF',
|
||||
200: '#A6F4C5',
|
||||
300: '#6CE9A6',
|
||||
400: '#32D583',
|
||||
500: '#12B76A',
|
||||
600: '#039855',
|
||||
700: '#027A48',
|
||||
800: '#05603A',
|
||||
900: '#054F31',
|
||||
},
|
||||
red: {
|
||||
50: '#fef2f2',
|
||||
100: '#fee2e2',
|
||||
200: '#fecaca',
|
||||
300: '#fca5a5',
|
||||
400: '#f87171',
|
||||
500: '#ef4444',
|
||||
600: '#dc2626',
|
||||
700: '#b91c1c',
|
||||
800: '#991b1b',
|
||||
900: '#7f1d1d',
|
||||
950: '#450a0a',
|
||||
},
|
||||
yellow: {
|
||||
25: '#FFFCF5',
|
||||
50: '#FFFAEB',
|
||||
100: '#FEF0C7',
|
||||
200: '#FEDF89',
|
||||
300: '#FEC84B',
|
||||
400: '#FDB022',
|
||||
500: '#F79009',
|
||||
600: '#DC6803',
|
||||
700: '#B54708',
|
||||
800: '#93370D',
|
||||
900: '#7A2E0E',
|
||||
},
|
||||
purple: {
|
||||
25: '#FAFAFF',
|
||||
50: '#F4F3FF',
|
||||
100: '#EBE9FE',
|
||||
200: '#D9D6FE',
|
||||
300: '#BDB4FE',
|
||||
400: '#9B8AFB',
|
||||
500: '#7A5AF8',
|
||||
600: '#6938EF',
|
||||
700: '#5925DC',
|
||||
800: '#4A1FB8',
|
||||
900: '#3E1C96',
|
||||
},
|
||||
teal: {
|
||||
25: '#F6FEFC',
|
||||
50: '#F0FDF9',
|
||||
100: '#CCFBEF',
|
||||
200: '#99F6E0',
|
||||
300: '#5FE9D0',
|
||||
400: '#2ED3B7',
|
||||
500: '#15B79E',
|
||||
600: '#0E9384',
|
||||
700: '#107569',
|
||||
800: '#125D56',
|
||||
900: '#134E48',
|
||||
},
|
||||
pink: {
|
||||
25: '#fdf2f8',
|
||||
50: '#fce7f3',
|
||||
100: '#fbcfe8',
|
||||
200: '#f9a8d4',
|
||||
300: '#f472b6',
|
||||
400: '#ec4899',
|
||||
500: '#db2777',
|
||||
600: '#be185d',
|
||||
700: '#9d174d',
|
||||
800: '#831843',
|
||||
900: '#500724',
|
||||
},
|
||||
cyan: {
|
||||
25: '#ecfeff',
|
||||
50: '#cffafe',
|
||||
100: '#a5f3fc',
|
||||
200: '#67e8f9',
|
||||
300: '#22d3ee',
|
||||
400: '#06b6d4',
|
||||
500: '#0891b2',
|
||||
600: '#0e7490',
|
||||
700: '#155e75',
|
||||
800: '#164e63',
|
||||
900: '#083344',
|
||||
},
|
||||
},
|
||||
alpha: {
|
||||
100: 'ff',
|
||||
90: 'e5',
|
||||
80: 'cc',
|
||||
70: 'b3',
|
||||
60: '99',
|
||||
50: '80',
|
||||
40: '66',
|
||||
30: '4d',
|
||||
20: '33',
|
||||
10: '1a',
|
||||
0: '00',
|
||||
},
|
||||
font: {
|
||||
size: {
|
||||
'2xs': 'calc(var(--tsqd-font-size) * 0.625)',
|
||||
xs: 'calc(var(--tsqd-font-size) * 0.75)',
|
||||
sm: 'calc(var(--tsqd-font-size) * 0.875)',
|
||||
md: 'var(--tsqd-font-size)',
|
||||
lg: 'calc(var(--tsqd-font-size) * 1.125)',
|
||||
xl: 'calc(var(--tsqd-font-size) * 1.25)',
|
||||
'2xl': 'calc(var(--tsqd-font-size) * 1.5)',
|
||||
'3xl': 'calc(var(--tsqd-font-size) * 1.875)',
|
||||
'4xl': 'calc(var(--tsqd-font-size) * 2.25)',
|
||||
'5xl': 'calc(var(--tsqd-font-size) * 3)',
|
||||
'6xl': 'calc(var(--tsqd-font-size) * 3.75)',
|
||||
'7xl': 'calc(var(--tsqd-font-size) * 4.5)',
|
||||
'8xl': 'calc(var(--tsqd-font-size) * 6)',
|
||||
'9xl': 'calc(var(--tsqd-font-size) * 8)',
|
||||
},
|
||||
lineHeight: {
|
||||
xs: 'calc(var(--tsqd-font-size) * 1)',
|
||||
sm: 'calc(var(--tsqd-font-size) * 1.25)',
|
||||
md: 'calc(var(--tsqd-font-size) * 1.5)',
|
||||
lg: 'calc(var(--tsqd-font-size) * 1.75)',
|
||||
xl: 'calc(var(--tsqd-font-size) * 2)',
|
||||
'2xl': 'calc(var(--tsqd-font-size) * 2.25)',
|
||||
'3xl': 'calc(var(--tsqd-font-size) * 2.5)',
|
||||
'4xl': 'calc(var(--tsqd-font-size) * 2.75)',
|
||||
'5xl': 'calc(var(--tsqd-font-size) * 3)',
|
||||
'6xl': 'calc(var(--tsqd-font-size) * 3.25)',
|
||||
'7xl': 'calc(var(--tsqd-font-size) * 3.5)',
|
||||
'8xl': 'calc(var(--tsqd-font-size) * 3.75)',
|
||||
'9xl': 'calc(var(--tsqd-font-size) * 4)',
|
||||
},
|
||||
weight: {
|
||||
thin: '100',
|
||||
extralight: '200',
|
||||
light: '300',
|
||||
normal: '400',
|
||||
medium: '500',
|
||||
semibold: '600',
|
||||
bold: '700',
|
||||
extrabold: '800',
|
||||
black: '900',
|
||||
},
|
||||
},
|
||||
breakpoints: {
|
||||
xs: '320px',
|
||||
sm: '640px',
|
||||
md: '768px',
|
||||
lg: '1024px',
|
||||
xl: '1280px',
|
||||
'2xl': '1536px',
|
||||
},
|
||||
border: {
|
||||
radius: {
|
||||
none: '0px',
|
||||
xs: 'calc(var(--tsqd-font-size) * 0.125)',
|
||||
sm: 'calc(var(--tsqd-font-size) * 0.25)',
|
||||
md: 'calc(var(--tsqd-font-size) * 0.375)',
|
||||
lg: 'calc(var(--tsqd-font-size) * 0.5)',
|
||||
xl: 'calc(var(--tsqd-font-size) * 0.75)',
|
||||
'2xl': 'calc(var(--tsqd-font-size) * 1)',
|
||||
'3xl': 'calc(var(--tsqd-font-size) * 1.5)',
|
||||
full: '9999px',
|
||||
},
|
||||
},
|
||||
size: {
|
||||
0: '0px',
|
||||
0.25: 'calc(var(--tsqd-font-size) * 0.0625)',
|
||||
0.5: 'calc(var(--tsqd-font-size) * 0.125)',
|
||||
1: 'calc(var(--tsqd-font-size) * 0.25)',
|
||||
1.5: 'calc(var(--tsqd-font-size) * 0.375)',
|
||||
2: 'calc(var(--tsqd-font-size) * 0.5)',
|
||||
2.5: 'calc(var(--tsqd-font-size) * 0.625)',
|
||||
3: 'calc(var(--tsqd-font-size) * 0.75)',
|
||||
3.5: 'calc(var(--tsqd-font-size) * 0.875)',
|
||||
4: 'calc(var(--tsqd-font-size) * 1)',
|
||||
4.5: 'calc(var(--tsqd-font-size) * 1.125)',
|
||||
5: 'calc(var(--tsqd-font-size) * 1.25)',
|
||||
5.5: 'calc(var(--tsqd-font-size) * 1.375)',
|
||||
6: 'calc(var(--tsqd-font-size) * 1.5)',
|
||||
6.5: 'calc(var(--tsqd-font-size) * 1.625)',
|
||||
7: 'calc(var(--tsqd-font-size) * 1.75)',
|
||||
8: 'calc(var(--tsqd-font-size) * 2)',
|
||||
9: 'calc(var(--tsqd-font-size) * 2.25)',
|
||||
10: 'calc(var(--tsqd-font-size) * 2.5)',
|
||||
11: 'calc(var(--tsqd-font-size) * 2.75)',
|
||||
12: 'calc(var(--tsqd-font-size) * 3)',
|
||||
14: 'calc(var(--tsqd-font-size) * 3.5)',
|
||||
16: 'calc(var(--tsqd-font-size) * 4)',
|
||||
20: 'calc(var(--tsqd-font-size) * 5)',
|
||||
24: 'calc(var(--tsqd-font-size) * 6)',
|
||||
28: 'calc(var(--tsqd-font-size) * 7)',
|
||||
32: 'calc(var(--tsqd-font-size) * 8)',
|
||||
36: 'calc(var(--tsqd-font-size) * 9)',
|
||||
40: 'calc(var(--tsqd-font-size) * 10)',
|
||||
44: 'calc(var(--tsqd-font-size) * 11)',
|
||||
48: 'calc(var(--tsqd-font-size) * 12)',
|
||||
52: 'calc(var(--tsqd-font-size) * 13)',
|
||||
56: 'calc(var(--tsqd-font-size) * 14)',
|
||||
60: 'calc(var(--tsqd-font-size) * 15)',
|
||||
64: 'calc(var(--tsqd-font-size) * 16)',
|
||||
72: 'calc(var(--tsqd-font-size) * 18)',
|
||||
80: 'calc(var(--tsqd-font-size) * 20)',
|
||||
96: 'calc(var(--tsqd-font-size) * 24)',
|
||||
},
|
||||
shadow: {
|
||||
xs: (_: string = 'rgb(0 0 0 / 0.1)') =>
|
||||
`0 1px 2px 0 rgb(0 0 0 / 0.05)` as const,
|
||||
sm: (color: string = 'rgb(0 0 0 / 0.1)') =>
|
||||
`0 1px 3px 0 ${color}, 0 1px 2px -1px ${color}` as const,
|
||||
md: (color: string = 'rgb(0 0 0 / 0.1)') =>
|
||||
`0 4px 6px -1px ${color}, 0 2px 4px -2px ${color}` as const,
|
||||
lg: (color: string = 'rgb(0 0 0 / 0.1)') =>
|
||||
`0 10px 15px -3px ${color}, 0 4px 6px -4px ${color}` as const,
|
||||
xl: (color: string = 'rgb(0 0 0 / 0.1)') =>
|
||||
`0 20px 25px -5px ${color}, 0 8px 10px -6px ${color}` as const,
|
||||
'2xl': (color: string = 'rgb(0 0 0 / 0.25)') =>
|
||||
`0 25px 50px -12px ${color}` as const,
|
||||
inner: (color: string = 'rgb(0 0 0 / 0.05)') =>
|
||||
`inset 0 2px 4px 0 ${color}` as const,
|
||||
none: () => `none` as const,
|
||||
},
|
||||
zIndices: {
|
||||
hide: -1,
|
||||
auto: 'auto',
|
||||
base: 0,
|
||||
docked: 10,
|
||||
dropdown: 1000,
|
||||
sticky: 1100,
|
||||
banner: 1200,
|
||||
overlay: 1300,
|
||||
modal: 1400,
|
||||
popover: 1500,
|
||||
skipLink: 1600,
|
||||
toast: 1700,
|
||||
tooltip: 1800,
|
||||
},
|
||||
} as const
|
||||
+323
@@ -0,0 +1,323 @@
|
||||
import { serialize } from 'superjson'
|
||||
import { createSignal, onCleanup, onMount } from 'solid-js'
|
||||
import type { Mutation, Query } from '@tanstack/query-core'
|
||||
import type { DevtoolsPosition } from './contexts'
|
||||
|
||||
export function getQueryStatusLabel(query: Query) {
|
||||
return query.state.fetchStatus === 'fetching'
|
||||
? 'fetching'
|
||||
: !query.getObserversCount()
|
||||
? 'inactive'
|
||||
: query.state.fetchStatus === 'paused'
|
||||
? 'paused'
|
||||
: query.isStale()
|
||||
? 'stale'
|
||||
: 'fresh'
|
||||
}
|
||||
|
||||
type QueryStatusLabel = 'fresh' | 'stale' | 'paused' | 'inactive' | 'fetching'
|
||||
|
||||
export function getSidedProp<T extends string>(
|
||||
prop: T,
|
||||
side: DevtoolsPosition,
|
||||
) {
|
||||
return `${prop}${
|
||||
side.charAt(0).toUpperCase() + side.slice(1)
|
||||
}` as `${T}${Capitalize<DevtoolsPosition>}`
|
||||
}
|
||||
|
||||
export function getQueryStatusColor({
|
||||
queryState,
|
||||
observerCount,
|
||||
isStale,
|
||||
}: {
|
||||
queryState: Query['state']
|
||||
observerCount: number
|
||||
isStale: boolean
|
||||
}) {
|
||||
return queryState.fetchStatus === 'fetching'
|
||||
? 'blue'
|
||||
: !observerCount
|
||||
? 'gray'
|
||||
: queryState.fetchStatus === 'paused'
|
||||
? 'purple'
|
||||
: isStale
|
||||
? 'yellow'
|
||||
: 'green'
|
||||
}
|
||||
|
||||
export function getMutationStatusColor({
|
||||
status,
|
||||
isPaused,
|
||||
}: {
|
||||
status: Mutation['state']['status']
|
||||
isPaused: boolean
|
||||
}) {
|
||||
return isPaused
|
||||
? 'purple'
|
||||
: status === 'error'
|
||||
? 'red'
|
||||
: status === 'pending'
|
||||
? 'yellow'
|
||||
: status === 'success'
|
||||
? 'green'
|
||||
: 'gray'
|
||||
}
|
||||
|
||||
export function getQueryStatusColorByLabel(label: QueryStatusLabel) {
|
||||
return label === 'fresh'
|
||||
? 'green'
|
||||
: label === 'stale'
|
||||
? 'yellow'
|
||||
: label === 'paused'
|
||||
? 'purple'
|
||||
: label === 'inactive'
|
||||
? 'gray'
|
||||
: 'blue'
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a string regardless the type of the data
|
||||
* @param {unknown} value Value to be stringified
|
||||
* @param {boolean} beautify Formats json to multiline
|
||||
*/
|
||||
export const displayValue = (value: unknown, beautify: boolean = false) => {
|
||||
const { json } = serialize(value)
|
||||
|
||||
return JSON.stringify(json, null, beautify ? 2 : undefined)
|
||||
}
|
||||
|
||||
// Sorting functions
|
||||
type SortFn = (a: Query, b: Query) => number
|
||||
|
||||
const getStatusRank = (q: Query) =>
|
||||
q.state.fetchStatus !== 'idle'
|
||||
? 0
|
||||
: !q.getObserversCount()
|
||||
? 3
|
||||
: q.isStale()
|
||||
? 2
|
||||
: 1
|
||||
|
||||
const queryHashSort: SortFn = (a, b) => a.queryHash.localeCompare(b.queryHash)
|
||||
|
||||
const dateSort: SortFn = (a, b) =>
|
||||
a.state.dataUpdatedAt < b.state.dataUpdatedAt ? 1 : -1
|
||||
|
||||
const statusAndDateSort: SortFn = (a, b) => {
|
||||
if (getStatusRank(a) === getStatusRank(b)) {
|
||||
return dateSort(a, b)
|
||||
}
|
||||
|
||||
return getStatusRank(a) > getStatusRank(b) ? 1 : -1
|
||||
}
|
||||
|
||||
export const sortFns: Record<string, SortFn> = {
|
||||
status: statusAndDateSort,
|
||||
'query hash': queryHashSort,
|
||||
'last updated': dateSort,
|
||||
}
|
||||
|
||||
type MutationSortFn = (a: Mutation, b: Mutation) => number
|
||||
|
||||
const getMutationStatusRank = (m: Mutation) =>
|
||||
m.state.isPaused
|
||||
? 0
|
||||
: m.state.status === 'error'
|
||||
? 2
|
||||
: m.state.status === 'pending'
|
||||
? 1
|
||||
: 3
|
||||
|
||||
const mutationDateSort: MutationSortFn = (a, b) =>
|
||||
a.state.submittedAt < b.state.submittedAt ? 1 : -1
|
||||
|
||||
const mutationStatusSort: MutationSortFn = (a, b) => {
|
||||
if (getMutationStatusRank(a) === getMutationStatusRank(b)) {
|
||||
return mutationDateSort(a, b)
|
||||
}
|
||||
|
||||
return getMutationStatusRank(a) > getMutationStatusRank(b) ? 1 : -1
|
||||
}
|
||||
|
||||
export const mutationSortFns: Record<string, MutationSortFn> = {
|
||||
status: mutationStatusSort,
|
||||
'last updated': mutationDateSort,
|
||||
}
|
||||
|
||||
export const convertRemToPixels = (rem: number) => {
|
||||
return rem * parseFloat(getComputedStyle(document.documentElement).fontSize)
|
||||
}
|
||||
|
||||
export const getPreferredColorScheme = () => {
|
||||
const [colorScheme, setColorScheme] = createSignal<'light' | 'dark'>('dark')
|
||||
|
||||
onMount(() => {
|
||||
const query = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
setColorScheme(query.matches ? 'dark' : 'light')
|
||||
const listener = (e: MediaQueryListEvent) => {
|
||||
setColorScheme(e.matches ? 'dark' : 'light')
|
||||
}
|
||||
query.addEventListener('change', listener)
|
||||
onCleanup(() => query.removeEventListener('change', listener))
|
||||
})
|
||||
|
||||
return colorScheme
|
||||
}
|
||||
|
||||
/**
|
||||
* updates nested data by path
|
||||
*
|
||||
* @param {unknown} oldData Data to be updated
|
||||
* @param {Array<string>} updatePath Path to the data to be updated
|
||||
* @param {unknown} value New value
|
||||
*/
|
||||
export const updateNestedDataByPath = (
|
||||
oldData: unknown,
|
||||
updatePath: Array<string>,
|
||||
value: unknown,
|
||||
): any => {
|
||||
if (updatePath.length === 0) {
|
||||
return value
|
||||
}
|
||||
|
||||
if (oldData instanceof Map) {
|
||||
const newData = new Map(oldData)
|
||||
|
||||
if (updatePath.length === 1) {
|
||||
newData.set(updatePath[0], value)
|
||||
return newData
|
||||
}
|
||||
|
||||
const [head, ...tail] = updatePath
|
||||
newData.set(head, updateNestedDataByPath(newData.get(head), tail, value))
|
||||
return newData
|
||||
}
|
||||
|
||||
if (oldData instanceof Set) {
|
||||
const setAsArray = updateNestedDataByPath(
|
||||
Array.from(oldData),
|
||||
updatePath,
|
||||
value,
|
||||
)
|
||||
|
||||
return new Set(setAsArray)
|
||||
}
|
||||
|
||||
if (Array.isArray(oldData)) {
|
||||
const newData = [...oldData]
|
||||
|
||||
if (updatePath.length === 1) {
|
||||
// @ts-expect-error
|
||||
newData[updatePath[0]] = value
|
||||
return newData
|
||||
}
|
||||
|
||||
const [head, ...tail] = updatePath
|
||||
// @ts-expect-error
|
||||
newData[head] = updateNestedDataByPath(newData[head], tail, value)
|
||||
|
||||
return newData
|
||||
}
|
||||
|
||||
if (oldData instanceof Object) {
|
||||
const newData = { ...oldData }
|
||||
|
||||
if (updatePath.length === 1) {
|
||||
// @ts-expect-error
|
||||
newData[updatePath[0]] = value
|
||||
return newData
|
||||
}
|
||||
|
||||
const [head, ...tail] = updatePath
|
||||
// @ts-expect-error
|
||||
newData[head] = updateNestedDataByPath(newData[head], tail, value)
|
||||
|
||||
return newData
|
||||
}
|
||||
|
||||
return oldData
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes nested data by path
|
||||
*
|
||||
* @param {unknown} oldData Data to be updated
|
||||
* @param {Array<string>} deletePath Path to the data to be deleted
|
||||
* @returns newData without the deleted items by path
|
||||
*/
|
||||
export const deleteNestedDataByPath = (
|
||||
oldData: unknown,
|
||||
deletePath: Array<string>,
|
||||
): any => {
|
||||
if (oldData instanceof Map) {
|
||||
const newData = new Map(oldData)
|
||||
|
||||
if (deletePath.length === 1) {
|
||||
newData.delete(deletePath[0])
|
||||
return newData
|
||||
}
|
||||
|
||||
const [head, ...tail] = deletePath
|
||||
newData.set(head, deleteNestedDataByPath(newData.get(head), tail))
|
||||
return newData
|
||||
}
|
||||
|
||||
if (oldData instanceof Set) {
|
||||
const setAsArray = deleteNestedDataByPath(Array.from(oldData), deletePath)
|
||||
return new Set(setAsArray)
|
||||
}
|
||||
|
||||
if (Array.isArray(oldData)) {
|
||||
const newData = [...oldData]
|
||||
|
||||
if (deletePath.length === 1) {
|
||||
return newData.filter((_, idx) => idx.toString() !== deletePath[0])
|
||||
}
|
||||
|
||||
const [head, ...tail] = deletePath
|
||||
|
||||
// @ts-expect-error
|
||||
newData[head] = deleteNestedDataByPath(newData[head], tail)
|
||||
|
||||
return newData
|
||||
}
|
||||
|
||||
if (oldData instanceof Object) {
|
||||
const newData = { ...oldData }
|
||||
|
||||
if (deletePath.length === 1) {
|
||||
// @ts-expect-error
|
||||
delete newData[deletePath[0]]
|
||||
return newData
|
||||
}
|
||||
|
||||
const [head, ...tail] = deletePath
|
||||
// @ts-expect-error
|
||||
newData[head] = deleteNestedDataByPath(newData[head], tail)
|
||||
|
||||
return newData
|
||||
}
|
||||
|
||||
return oldData
|
||||
}
|
||||
|
||||
// Sets up the goober stylesheet
|
||||
// Adds a nonce to the style tag if needed
|
||||
export const setupStyleSheet = (nonce?: string, target?: ShadowRoot) => {
|
||||
if (!nonce) return
|
||||
const styleExists =
|
||||
document.querySelector('#_goober') || target?.querySelector('#_goober')
|
||||
|
||||
if (styleExists) return
|
||||
const styleTag = document.createElement('style')
|
||||
const textNode = document.createTextNode('')
|
||||
styleTag.appendChild(textNode)
|
||||
styleTag.id = '_goober'
|
||||
styleTag.setAttribute('nonce', nonce)
|
||||
if (target) {
|
||||
target.appendChild(styleTag)
|
||||
} else {
|
||||
document.head.appendChild(styleTag)
|
||||
}
|
||||
}
|
||||
+21
@@ -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.
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
{
|
||||
"name": "@tanstack/react-query-devtools",
|
||||
"version": "5.91.3",
|
||||
"description": "Developer tools to interact with and visualize the TanStack/react-query cache",
|
||||
"author": "tannerlinsley",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/TanStack/query.git",
|
||||
"directory": "packages/react-query-devtools"
|
||||
},
|
||||
"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",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"./production": {
|
||||
"import": {
|
||||
"types": "./build/modern/production.d.ts",
|
||||
"default": "./build/modern/production.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./build/modern/production.d.cts",
|
||||
"default": "./build/modern/production.cjs"
|
||||
}
|
||||
},
|
||||
"./build/modern/production.js": {
|
||||
"import": {
|
||||
"types": "./build/modern/production.d.ts",
|
||||
"default": "./build/modern/production.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./build/modern/production.d.cts",
|
||||
"default": "./build/modern/production.cjs"
|
||||
}
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"sideEffects": false,
|
||||
"files": [
|
||||
"build",
|
||||
"src",
|
||||
"!src/__tests__"
|
||||
],
|
||||
"dependencies": {
|
||||
"@tanstack/query-devtools": "5.93.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/react": "^16.1.0",
|
||||
"@types/react": "^19.2.7",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"npm-run-all2": "^5.0.0",
|
||||
"react": "^19.2.1",
|
||||
"@tanstack/react-query": "5.90.20"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18 || ^19",
|
||||
"@tanstack/react-query": "^5.90.20"
|
||||
},
|
||||
"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",
|
||||
"build:dev": "tsup --watch"
|
||||
}
|
||||
}
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
'use client'
|
||||
import * as React from 'react'
|
||||
import { onlineManager, useQueryClient } from '@tanstack/react-query'
|
||||
import { TanstackQueryDevtools } from '@tanstack/query-devtools'
|
||||
import type {
|
||||
DevtoolsButtonPosition,
|
||||
DevtoolsErrorType,
|
||||
DevtoolsPosition,
|
||||
Theme,
|
||||
} from '@tanstack/query-devtools'
|
||||
import type { QueryClient } from '@tanstack/react-query'
|
||||
|
||||
export interface DevtoolsOptions {
|
||||
/**
|
||||
* Set this true if you want the dev tools to default to being open
|
||||
*/
|
||||
initialIsOpen?: boolean
|
||||
/**
|
||||
* The position of the React Query logo to open and close the devtools panel.
|
||||
* 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'
|
||||
* Defaults to 'bottom-right'.
|
||||
*/
|
||||
buttonPosition?: DevtoolsButtonPosition
|
||||
/**
|
||||
* The position of the React Query devtools panel.
|
||||
* 'top' | 'bottom' | 'left' | 'right'
|
||||
* Defaults to 'bottom'.
|
||||
*/
|
||||
position?: DevtoolsPosition
|
||||
/**
|
||||
* Custom instance of QueryClient
|
||||
*/
|
||||
client?: QueryClient
|
||||
/**
|
||||
* Use this so you can define custom errors that can be shown in the devtools.
|
||||
*/
|
||||
errorTypes?: Array<DevtoolsErrorType>
|
||||
/**
|
||||
* Use this to pass a nonce to the style tag that is added to the document head. This is useful if you are using a Content Security Policy (CSP) nonce to allow inline styles.
|
||||
*/
|
||||
styleNonce?: string
|
||||
/**
|
||||
* Use this so you can attach the devtool's styles to specific element in the DOM.
|
||||
*/
|
||||
shadowDOMTarget?: ShadowRoot
|
||||
/**
|
||||
* Set this to true to hide disabled queries from the devtools panel.
|
||||
*/
|
||||
hideDisabledQueries?: boolean
|
||||
/**
|
||||
* Set this to 'light', 'dark', or 'system' to change the theme of the devtools panel.
|
||||
* Defaults to 'system'.
|
||||
*/
|
||||
theme?: Theme
|
||||
}
|
||||
|
||||
export function ReactQueryDevtools(
|
||||
props: DevtoolsOptions,
|
||||
): React.ReactElement | null {
|
||||
const queryClient = useQueryClient(props.client)
|
||||
const ref = React.useRef<HTMLDivElement>(null)
|
||||
const {
|
||||
buttonPosition,
|
||||
position,
|
||||
initialIsOpen,
|
||||
errorTypes,
|
||||
styleNonce,
|
||||
shadowDOMTarget,
|
||||
hideDisabledQueries,
|
||||
theme,
|
||||
} = props
|
||||
const [devtools] = React.useState(
|
||||
new TanstackQueryDevtools({
|
||||
client: queryClient,
|
||||
queryFlavor: 'React Query',
|
||||
version: '5',
|
||||
onlineManager,
|
||||
buttonPosition,
|
||||
position,
|
||||
initialIsOpen,
|
||||
errorTypes,
|
||||
styleNonce,
|
||||
shadowDOMTarget,
|
||||
hideDisabledQueries,
|
||||
theme,
|
||||
}),
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
devtools.setClient(queryClient)
|
||||
}, [queryClient, devtools])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (buttonPosition) {
|
||||
devtools.setButtonPosition(buttonPosition)
|
||||
}
|
||||
}, [buttonPosition, devtools])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (position) {
|
||||
devtools.setPosition(position)
|
||||
}
|
||||
}, [position, devtools])
|
||||
|
||||
React.useEffect(() => {
|
||||
devtools.setInitialIsOpen(initialIsOpen || false)
|
||||
}, [initialIsOpen, devtools])
|
||||
|
||||
React.useEffect(() => {
|
||||
devtools.setErrorTypes(errorTypes || [])
|
||||
}, [errorTypes, devtools])
|
||||
|
||||
React.useEffect(() => {
|
||||
devtools.setTheme(theme)
|
||||
}, [theme, devtools])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (ref.current) {
|
||||
devtools.mount(ref.current)
|
||||
}
|
||||
|
||||
return () => {
|
||||
devtools.unmount()
|
||||
}
|
||||
}, [devtools])
|
||||
|
||||
return <div dir="ltr" className="tsqd-parent-container" ref={ref}></div>
|
||||
}
|
||||
Generated
Vendored
+112
@@ -0,0 +1,112 @@
|
||||
'use client'
|
||||
import * as React from 'react'
|
||||
import { onlineManager, useQueryClient } from '@tanstack/react-query'
|
||||
import { TanstackQueryDevtoolsPanel } from '@tanstack/query-devtools'
|
||||
import type { DevtoolsErrorType, Theme } from '@tanstack/query-devtools'
|
||||
import type { QueryClient } from '@tanstack/react-query'
|
||||
|
||||
export interface DevtoolsPanelOptions {
|
||||
/**
|
||||
* Custom instance of QueryClient
|
||||
*/
|
||||
client?: QueryClient
|
||||
/**
|
||||
* Use this so you can define custom errors that can be shown in the devtools.
|
||||
*/
|
||||
errorTypes?: Array<DevtoolsErrorType>
|
||||
/**
|
||||
* Use this to pass a nonce to the style tag that is added to the document head. This is useful if you are using a Content Security Policy (CSP) nonce to allow inline styles.
|
||||
*/
|
||||
styleNonce?: string
|
||||
/**
|
||||
* Use this so you can attach the devtool's styles to specific element in the DOM.
|
||||
*/
|
||||
shadowDOMTarget?: ShadowRoot
|
||||
|
||||
/**
|
||||
* Custom styles for the devtools panel
|
||||
* @default { height: '500px' }
|
||||
* @example { height: '100%' }
|
||||
* @example { height: '100%', width: '100%' }
|
||||
*/
|
||||
style?: React.CSSProperties
|
||||
|
||||
/**
|
||||
* Callback function that is called when the devtools panel is closed
|
||||
*/
|
||||
onClose?: () => unknown
|
||||
/**
|
||||
* Set this to true to hide disabled queries from the devtools panel.
|
||||
*/
|
||||
hideDisabledQueries?: boolean
|
||||
/**
|
||||
* Set this to 'light', 'dark', or 'system' to change the theme of the devtools panel.
|
||||
* Defaults to 'system'.
|
||||
*/
|
||||
theme?: Theme
|
||||
}
|
||||
|
||||
export function ReactQueryDevtoolsPanel(
|
||||
props: DevtoolsPanelOptions,
|
||||
): React.ReactElement | null {
|
||||
const queryClient = useQueryClient(props.client)
|
||||
const ref = React.useRef<HTMLDivElement>(null)
|
||||
const {
|
||||
errorTypes,
|
||||
styleNonce,
|
||||
shadowDOMTarget,
|
||||
hideDisabledQueries,
|
||||
theme,
|
||||
} = props
|
||||
const [devtools] = React.useState(
|
||||
new TanstackQueryDevtoolsPanel({
|
||||
client: queryClient,
|
||||
queryFlavor: 'React Query',
|
||||
version: '5',
|
||||
onlineManager,
|
||||
buttonPosition: 'bottom-left',
|
||||
position: 'bottom',
|
||||
initialIsOpen: true,
|
||||
errorTypes,
|
||||
styleNonce,
|
||||
shadowDOMTarget,
|
||||
onClose: props.onClose,
|
||||
hideDisabledQueries,
|
||||
theme,
|
||||
}),
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
devtools.setClient(queryClient)
|
||||
}, [queryClient, devtools])
|
||||
|
||||
React.useEffect(() => {
|
||||
devtools.setOnClose(props.onClose ?? (() => {}))
|
||||
}, [props.onClose, devtools])
|
||||
|
||||
React.useEffect(() => {
|
||||
devtools.setErrorTypes(errorTypes || [])
|
||||
}, [errorTypes, devtools])
|
||||
|
||||
React.useEffect(() => {
|
||||
devtools.setTheme(theme)
|
||||
}, [theme, devtools])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (ref.current) {
|
||||
devtools.mount(ref.current)
|
||||
}
|
||||
|
||||
return () => {
|
||||
devtools.unmount()
|
||||
}
|
||||
}, [devtools])
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{ height: '500px', ...props.style }}
|
||||
className="tsqd-parent-container"
|
||||
ref={ref}
|
||||
></div>
|
||||
)
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
'use client'
|
||||
|
||||
import * as Devtools from './ReactQueryDevtools'
|
||||
import * as DevtoolsPanel from './ReactQueryDevtoolsPanel'
|
||||
|
||||
export const ReactQueryDevtools: (typeof Devtools)['ReactQueryDevtools'] =
|
||||
process.env.NODE_ENV !== 'development'
|
||||
? function () {
|
||||
return null
|
||||
}
|
||||
: Devtools.ReactQueryDevtools
|
||||
|
||||
export const ReactQueryDevtoolsPanel: (typeof DevtoolsPanel)['ReactQueryDevtoolsPanel'] =
|
||||
process.env.NODE_ENV !== 'development'
|
||||
? function () {
|
||||
return null
|
||||
}
|
||||
: DevtoolsPanel.ReactQueryDevtoolsPanel
|
||||
|
||||
export type DevtoolsPanelOptions = DevtoolsPanel.DevtoolsPanelOptions
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import * as Devtools from './ReactQueryDevtools'
|
||||
import * as DevtoolsPanel from './ReactQueryDevtoolsPanel'
|
||||
|
||||
export const ReactQueryDevtools = Devtools.ReactQueryDevtools
|
||||
export const ReactQueryDevtoolsPanel = DevtoolsPanel.ReactQueryDevtoolsPanel
|
||||
+21
@@ -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.
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
<img src="https://static.scarf.sh/a.png?x-pxid=be2d8a11-9712-4c1d-9963-580b2d4fb133" />
|
||||
|
||||

|
||||
|
||||
Hooks for fetching, caching and updating asynchronous data in React
|
||||
|
||||
<a href="https://twitter.com/intent/tweet?button_hashtag=TanStack" target="\_parent">
|
||||
<img alt="#TanStack" src="https://img.shields.io/twitter/url?color=%2308a0e9&label=%23TanStack&style=social&url=https%3A%2F%2Ftwitter.com%2Fintent%2Ftweet%3Fbutton_hashtag%3DTanStack">
|
||||
</a><a href="https://discord.com/invite/WrRKjPJ" target="\_parent">
|
||||
<img alt="" src="https://img.shields.io/badge/Discord-TanStack-%235865F2" />
|
||||
</a><a href="https://github.com/TanStack/query/actions?query=workflow%3A%22react-query+tests%22">
|
||||
<img src="https://github.com/TanStack/query/workflows/react-query%20tests/badge.svg" />
|
||||
</a><a href="https://www.npmjs.com/package/@tanstack/query-core" target="\_parent">
|
||||
<img alt="" src="https://img.shields.io/npm/dm/@tanstack/query-core.svg" />
|
||||
</a><a href="https://bundlejs.com/?q=%40tanstack%2Freact-query&config=%7B%22esbuild%22%3A%7B%22external%22%3A%5B%22react%22%2C%22react-dom%22%5D%7D%7D&badge=" target="\_parent">
|
||||
<img alt="" src="https://deno.bundlejs.com/?q=@tanstack/react-query&config={%22esbuild%22:{%22external%22:[%22react%22,%22react-dom%22]}}&badge=detailed" />
|
||||
</a><a href="#badge">
|
||||
<img alt="semantic-release" src="https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg">
|
||||
</a><a href="https://github.com/TanStack/query/discussions">
|
||||
<img alt="Join the discussion on Github" src="https://img.shields.io/badge/Github%20Discussions%20%26%20Support-Chat%20now!-blue" />
|
||||
</a><a href="https://bestofjs.org/projects/tanstack-query"><img alt="Best of JS" src="https://img.shields.io/endpoint?url=https://bestofjs-serverless.now.sh/api/project-badge?fullName=TanStack%2Fquery%26since=daily" /></a><a href="https://github.com/TanStack/query/" target="\_parent">
|
||||
<img alt="" src="https://img.shields.io/github/stars/TanStack/query.svg?style=social&label=Star" />
|
||||
</a><a href="https://twitter.com/tannerlinsley" target="\_parent">
|
||||
<img alt="" src="https://img.shields.io/twitter/follow/tannerlinsley.svg?style=social&label=Follow" />
|
||||
</a> <a href="https://gitpod.io/from-referrer/">
|
||||
<img src="https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod" alt="Gitpod Ready-to-Code"/>
|
||||
</a>
|
||||
|
||||
Enjoy this library? Try the entire [TanStack](https://tanstack.com)! [TanStack Table](https://github.com/TanStack/table), [TanStack Router](https://github.com/tanstack/router), [TanStack Virtual](https://github.com/tanstack/virtual), [React Charts](https://github.com/TanStack/react-charts), [React Ranger](https://github.com/TanStack/ranger)
|
||||
|
||||
## Visit [tanstack.com/query](https://tanstack.com/query) for docs, guides, API and more!
|
||||
|
||||
## Quick Features
|
||||
|
||||
- Transport/protocol/backend agnostic data fetching (REST, GraphQL, promises, whatever!)
|
||||
- Auto Caching + Refetching (stale-while-revalidate, Window Refocus, Polling/Realtime)
|
||||
- Parallel + Dependent Queries
|
||||
- Mutations + Reactive Query Refetching
|
||||
- Multi-layer Cache + Automatic Garbage Collection
|
||||
- Paginated + Cursor-based Queries
|
||||
- Load-More + Infinite Scroll Queries w/ Scroll Recovery
|
||||
- Request Cancellation
|
||||
- [React Suspense](https://react.dev/reference/react/Suspense) + Fetch-As-You-Render Query Prefetching
|
||||
- Dedicated Devtools
|
||||
|
||||
### [Become a Sponsor!](https://github.com/sponsors/tannerlinsley/)
|
||||
|
||||
<!-- Use the force, Luke -->
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"name": "@tanstack/react-query",
|
||||
"version": "5.91.0",
|
||||
"description": "Hooks for managing, caching and syncing asynchronous and remote data in React",
|
||||
"author": "tannerlinsley",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/TanStack/query.git",
|
||||
"directory": "packages/react-query"
|
||||
},
|
||||
"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__",
|
||||
"!build/codemods/node_modules",
|
||||
"!build/codemods/vite.config.ts",
|
||||
"!build/codemods/**/__testfixtures__",
|
||||
"!build/codemods/**/__tests__"
|
||||
],
|
||||
"dependencies": {
|
||||
"@tanstack/query-core": "5.91.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/react": "^16.1.0",
|
||||
"@testing-library/react-render-stream": "^2.0.2",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"cpy-cli": "^5.0.0",
|
||||
"npm-run-all2": "^5.0.0",
|
||||
"react": "^19.2.1",
|
||||
"react-dom": "^19.2.1",
|
||||
"react-error-boundary": "^4.1.2",
|
||||
"@tanstack/query-persist-client-core": "5.92.2",
|
||||
"@tanstack/query-test-utils": "0.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18 || ^19"
|
||||
},
|
||||
"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: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:ts58": "node ../../node_modules/typescript58/lib/tsc.js --build tsconfig.legacy.json",
|
||||
"test:types:ts59": "node ../../node_modules/typescript59/lib/tsc.js --build tsconfig.legacy.json",
|
||||
"test:types:tscurrent": "tsc --build",
|
||||
"test:types:ts60": "node ../../node_modules/typescript60/lib/tsc.js --build",
|
||||
"test:lib": "vitest",
|
||||
"test:lib:dev": "pnpm run test:lib --watch",
|
||||
"test:build": "publint --strict && attw --pack",
|
||||
"build": "pnpm build:tsup && pnpm build:codemods",
|
||||
"build:tsup": "tsup --tsconfig tsconfig.prod.json",
|
||||
"build:codemods": "cpy ../query-codemods/* ./build/codemods"
|
||||
}
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
'use client'
|
||||
import * as React from 'react'
|
||||
|
||||
import { hydrate } from '@tanstack/query-core'
|
||||
import { useQueryClient } from './QueryClientProvider'
|
||||
import type {
|
||||
DehydratedState,
|
||||
HydrateOptions,
|
||||
OmitKeyof,
|
||||
QueryClient,
|
||||
} from '@tanstack/query-core'
|
||||
|
||||
export interface HydrationBoundaryProps {
|
||||
state: DehydratedState | null | undefined
|
||||
options?: OmitKeyof<HydrateOptions, 'defaultOptions'> & {
|
||||
defaultOptions?: OmitKeyof<
|
||||
Exclude<HydrateOptions['defaultOptions'], undefined>,
|
||||
'mutations'
|
||||
>
|
||||
}
|
||||
children?: React.ReactNode
|
||||
queryClient?: QueryClient
|
||||
}
|
||||
|
||||
export const HydrationBoundary = ({
|
||||
children,
|
||||
options = {},
|
||||
state,
|
||||
queryClient,
|
||||
}: HydrationBoundaryProps) => {
|
||||
const client = useQueryClient(queryClient)
|
||||
|
||||
const optionsRef = React.useRef(options)
|
||||
React.useEffect(() => {
|
||||
optionsRef.current = options
|
||||
})
|
||||
|
||||
// This useMemo is for performance reasons only, everything inside it must
|
||||
// be safe to run in every render and code here should be read as "in render".
|
||||
//
|
||||
// This code needs to happen during the render phase, because after initial
|
||||
// SSR, hydration needs to happen _before_ children render. Also, if hydrating
|
||||
// during a transition, we want to hydrate as much as is safe in render so
|
||||
// we can prerender as much as possible.
|
||||
//
|
||||
// For any queries that already exist in the cache, we want to hold back on
|
||||
// hydrating until _after_ the render phase. The reason for this is that during
|
||||
// transitions, we don't want the existing queries and observers to update to
|
||||
// the new data on the current page, only _after_ the transition is committed.
|
||||
// If the transition is aborted, we will have hydrated any _new_ queries, but
|
||||
// we throw away the fresh data for any existing ones to avoid unexpectedly
|
||||
// updating the UI.
|
||||
const hydrationQueue: DehydratedState['queries'] | undefined =
|
||||
React.useMemo(() => {
|
||||
if (state) {
|
||||
if (typeof state !== 'object') {
|
||||
return
|
||||
}
|
||||
|
||||
const queryCache = client.getQueryCache()
|
||||
// State is supplied from the outside and we might as well fail
|
||||
// gracefully if it has the wrong shape, so while we type `queries`
|
||||
// as required, we still provide a fallback.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
const queries = state.queries || []
|
||||
|
||||
const newQueries: DehydratedState['queries'] = []
|
||||
const existingQueries: DehydratedState['queries'] = []
|
||||
for (const dehydratedQuery of queries) {
|
||||
const existingQuery = queryCache.get(dehydratedQuery.queryHash)
|
||||
|
||||
if (!existingQuery) {
|
||||
newQueries.push(dehydratedQuery)
|
||||
} else {
|
||||
const hydrationIsNewer =
|
||||
dehydratedQuery.state.dataUpdatedAt >
|
||||
existingQuery.state.dataUpdatedAt ||
|
||||
(dehydratedQuery.promise &&
|
||||
existingQuery.state.status !== 'pending' &&
|
||||
existingQuery.state.fetchStatus !== 'fetching' &&
|
||||
dehydratedQuery.dehydratedAt !== undefined &&
|
||||
dehydratedQuery.dehydratedAt >
|
||||
existingQuery.state.dataUpdatedAt)
|
||||
|
||||
if (hydrationIsNewer) {
|
||||
existingQueries.push(dehydratedQuery)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (newQueries.length > 0) {
|
||||
// It's actually fine to call this with queries/state that already exists
|
||||
// in the cache, or is older. hydrate() is idempotent for queries.
|
||||
// eslint-disable-next-line react-hooks/refs
|
||||
hydrate(client, { queries: newQueries }, optionsRef.current)
|
||||
}
|
||||
if (existingQueries.length > 0) {
|
||||
return existingQueries
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}, [client, state])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (hydrationQueue) {
|
||||
hydrate(client, { queries: hydrationQueue }, optionsRef.current)
|
||||
}
|
||||
}, [client, hydrationQueue])
|
||||
|
||||
return children as React.ReactElement
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
'use client'
|
||||
import * as React from 'react'
|
||||
|
||||
const IsRestoringContext = React.createContext(false)
|
||||
|
||||
export const useIsRestoring = () => React.useContext(IsRestoringContext)
|
||||
export const IsRestoringProvider = IsRestoringContext.Provider
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
'use client'
|
||||
import * as React from 'react'
|
||||
|
||||
import type { QueryClient } from '@tanstack/query-core'
|
||||
|
||||
export const QueryClientContext = React.createContext<QueryClient | undefined>(
|
||||
undefined,
|
||||
)
|
||||
|
||||
export const useQueryClient = (queryClient?: QueryClient) => {
|
||||
const client = React.useContext(QueryClientContext)
|
||||
|
||||
if (queryClient) {
|
||||
return queryClient
|
||||
}
|
||||
|
||||
if (!client) {
|
||||
throw new Error('No QueryClient set, use QueryClientProvider to set one')
|
||||
}
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
export type QueryClientProviderProps = {
|
||||
client: QueryClient
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
export const QueryClientProvider = ({
|
||||
client,
|
||||
children,
|
||||
}: QueryClientProviderProps): React.JSX.Element => {
|
||||
React.useEffect(() => {
|
||||
client.mount()
|
||||
return () => {
|
||||
client.unmount()
|
||||
}
|
||||
}, [client])
|
||||
|
||||
return (
|
||||
<QueryClientContext.Provider value={client}>
|
||||
{children}
|
||||
</QueryClientContext.Provider>
|
||||
)
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
'use client'
|
||||
import * as React from 'react'
|
||||
|
||||
// CONTEXT
|
||||
export type QueryErrorResetFunction = () => void
|
||||
export type QueryErrorIsResetFunction = () => boolean
|
||||
export type QueryErrorClearResetFunction = () => void
|
||||
|
||||
export interface QueryErrorResetBoundaryValue {
|
||||
clearReset: QueryErrorClearResetFunction
|
||||
isReset: QueryErrorIsResetFunction
|
||||
reset: QueryErrorResetFunction
|
||||
}
|
||||
|
||||
function createValue(): QueryErrorResetBoundaryValue {
|
||||
let isReset = false
|
||||
return {
|
||||
clearReset: () => {
|
||||
isReset = false
|
||||
},
|
||||
reset: () => {
|
||||
isReset = true
|
||||
},
|
||||
isReset: () => {
|
||||
return isReset
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const QueryErrorResetBoundaryContext = React.createContext(createValue())
|
||||
|
||||
// HOOK
|
||||
|
||||
export const useQueryErrorResetBoundary = () =>
|
||||
React.useContext(QueryErrorResetBoundaryContext)
|
||||
|
||||
// COMPONENT
|
||||
|
||||
export type QueryErrorResetBoundaryFunction = (
|
||||
value: QueryErrorResetBoundaryValue,
|
||||
) => React.ReactNode
|
||||
|
||||
export interface QueryErrorResetBoundaryProps {
|
||||
children: QueryErrorResetBoundaryFunction | React.ReactNode
|
||||
}
|
||||
|
||||
export const QueryErrorResetBoundary = ({
|
||||
children,
|
||||
}: QueryErrorResetBoundaryProps) => {
|
||||
const [value] = React.useState(() => createValue())
|
||||
return (
|
||||
<QueryErrorResetBoundaryContext.Provider value={value}>
|
||||
{typeof children === 'function' ? children(value) : children}
|
||||
</QueryErrorResetBoundaryContext.Provider>
|
||||
)
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
'use client'
|
||||
import * as React from 'react'
|
||||
import { shouldThrowError } from '@tanstack/query-core'
|
||||
import type {
|
||||
DefaultedQueryObserverOptions,
|
||||
Query,
|
||||
QueryKey,
|
||||
QueryObserverResult,
|
||||
ThrowOnError,
|
||||
} from '@tanstack/query-core'
|
||||
import type { QueryErrorResetBoundaryValue } from './QueryErrorResetBoundary'
|
||||
|
||||
export const ensurePreventErrorBoundaryRetry = <
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryData,
|
||||
TQueryKey extends QueryKey,
|
||||
>(
|
||||
options: DefaultedQueryObserverOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryData,
|
||||
TQueryKey
|
||||
>,
|
||||
errorResetBoundary: QueryErrorResetBoundaryValue,
|
||||
query: Query<TQueryFnData, TError, TQueryData, TQueryKey> | undefined,
|
||||
) => {
|
||||
const throwOnError =
|
||||
query?.state.error && typeof options.throwOnError === 'function'
|
||||
? shouldThrowError(options.throwOnError, [query.state.error, query])
|
||||
: options.throwOnError
|
||||
|
||||
if (
|
||||
options.suspense ||
|
||||
options.experimental_prefetchInRender ||
|
||||
throwOnError
|
||||
) {
|
||||
// Prevent retrying failed query if the error boundary has not been reset yet
|
||||
if (!errorResetBoundary.isReset()) {
|
||||
options.retryOnMount = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const useClearResetErrorBoundary = (
|
||||
errorResetBoundary: QueryErrorResetBoundaryValue,
|
||||
) => {
|
||||
React.useEffect(() => {
|
||||
errorResetBoundary.clearReset()
|
||||
}, [errorResetBoundary])
|
||||
}
|
||||
|
||||
export const getHasError = <
|
||||
TData,
|
||||
TError,
|
||||
TQueryFnData,
|
||||
TQueryData,
|
||||
TQueryKey extends QueryKey,
|
||||
>({
|
||||
result,
|
||||
errorResetBoundary,
|
||||
throwOnError,
|
||||
query,
|
||||
suspense,
|
||||
}: {
|
||||
result: QueryObserverResult<TData, TError>
|
||||
errorResetBoundary: QueryErrorResetBoundaryValue
|
||||
throwOnError: ThrowOnError<TQueryFnData, TError, TQueryData, TQueryKey>
|
||||
query: Query<TQueryFnData, TError, TQueryData, TQueryKey> | undefined
|
||||
suspense: boolean | undefined
|
||||
}) => {
|
||||
return (
|
||||
result.isError &&
|
||||
!errorResetBoundary.isReset() &&
|
||||
!result.isFetching &&
|
||||
query &&
|
||||
((suspense && result.data === undefined) ||
|
||||
shouldThrowError(throwOnError, [result.error, query]))
|
||||
)
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
/* istanbul ignore file */
|
||||
|
||||
// Re-export core
|
||||
export * from '@tanstack/query-core'
|
||||
|
||||
// React Query
|
||||
export * from './types'
|
||||
export { useQueries } from './useQueries'
|
||||
export type { QueriesResults, QueriesOptions } from './useQueries'
|
||||
export { useQuery } from './useQuery'
|
||||
export { useSuspenseQuery } from './useSuspenseQuery'
|
||||
export { useSuspenseInfiniteQuery } from './useSuspenseInfiniteQuery'
|
||||
export { useSuspenseQueries } from './useSuspenseQueries'
|
||||
export type {
|
||||
SuspenseQueriesResults,
|
||||
SuspenseQueriesOptions,
|
||||
} from './useSuspenseQueries'
|
||||
export { usePrefetchQuery } from './usePrefetchQuery'
|
||||
export { usePrefetchInfiniteQuery } from './usePrefetchInfiniteQuery'
|
||||
export { queryOptions } from './queryOptions'
|
||||
export type {
|
||||
DefinedInitialDataOptions,
|
||||
UndefinedInitialDataOptions,
|
||||
UnusedSkipTokenOptions,
|
||||
} from './queryOptions'
|
||||
export { infiniteQueryOptions } from './infiniteQueryOptions'
|
||||
export type {
|
||||
DefinedInitialDataInfiniteOptions,
|
||||
UndefinedInitialDataInfiniteOptions,
|
||||
UnusedSkipTokenInfiniteOptions,
|
||||
} from './infiniteQueryOptions'
|
||||
export {
|
||||
QueryClientContext,
|
||||
QueryClientProvider,
|
||||
useQueryClient,
|
||||
} from './QueryClientProvider'
|
||||
export type { QueryClientProviderProps } from './QueryClientProvider'
|
||||
export type { QueryErrorResetBoundaryProps } from './QueryErrorResetBoundary'
|
||||
export { HydrationBoundary } from './HydrationBoundary'
|
||||
export type { HydrationBoundaryProps } from './HydrationBoundary'
|
||||
export type {
|
||||
QueryErrorClearResetFunction,
|
||||
QueryErrorIsResetFunction,
|
||||
QueryErrorResetBoundaryFunction,
|
||||
QueryErrorResetFunction,
|
||||
} from './QueryErrorResetBoundary'
|
||||
export {
|
||||
QueryErrorResetBoundary,
|
||||
useQueryErrorResetBoundary,
|
||||
} from './QueryErrorResetBoundary'
|
||||
export { useIsFetching } from './useIsFetching'
|
||||
export { useIsMutating, useMutationState } from './useMutationState'
|
||||
export { useMutation } from './useMutation'
|
||||
export { mutationOptions } from './mutationOptions'
|
||||
export { useInfiniteQuery } from './useInfiniteQuery'
|
||||
export { useIsRestoring, IsRestoringProvider } from './IsRestoringProvider'
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
import type {
|
||||
DataTag,
|
||||
DefaultError,
|
||||
InfiniteData,
|
||||
InitialDataFunction,
|
||||
NonUndefinedGuard,
|
||||
OmitKeyof,
|
||||
QueryKey,
|
||||
SkipToken,
|
||||
} from '@tanstack/query-core'
|
||||
import type { UseInfiniteQueryOptions } from './types'
|
||||
|
||||
export type UndefinedInitialDataInfiniteOptions<
|
||||
TQueryFnData,
|
||||
TError = DefaultError,
|
||||
TData = InfiniteData<TQueryFnData>,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
TPageParam = unknown,
|
||||
> = UseInfiniteQueryOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey,
|
||||
TPageParam
|
||||
> & {
|
||||
initialData?:
|
||||
| undefined
|
||||
| NonUndefinedGuard<InfiniteData<TQueryFnData, TPageParam>>
|
||||
| InitialDataFunction<
|
||||
NonUndefinedGuard<InfiniteData<TQueryFnData, TPageParam>>
|
||||
>
|
||||
}
|
||||
|
||||
export type UnusedSkipTokenInfiniteOptions<
|
||||
TQueryFnData,
|
||||
TError = DefaultError,
|
||||
TData = InfiniteData<TQueryFnData>,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
TPageParam = unknown,
|
||||
> = OmitKeyof<
|
||||
UseInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>,
|
||||
'queryFn'
|
||||
> & {
|
||||
queryFn?: Exclude<
|
||||
UseInfiniteQueryOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey,
|
||||
TPageParam
|
||||
>['queryFn'],
|
||||
SkipToken | undefined
|
||||
>
|
||||
}
|
||||
|
||||
export type DefinedInitialDataInfiniteOptions<
|
||||
TQueryFnData,
|
||||
TError = DefaultError,
|
||||
TData = InfiniteData<TQueryFnData>,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
TPageParam = unknown,
|
||||
> = UseInfiniteQueryOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey,
|
||||
TPageParam
|
||||
> & {
|
||||
initialData:
|
||||
| NonUndefinedGuard<InfiniteData<TQueryFnData, TPageParam>>
|
||||
| (() => NonUndefinedGuard<InfiniteData<TQueryFnData, TPageParam>>)
|
||||
| undefined
|
||||
}
|
||||
|
||||
export function infiniteQueryOptions<
|
||||
TQueryFnData,
|
||||
TError = DefaultError,
|
||||
TData = InfiniteData<TQueryFnData>,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
TPageParam = unknown,
|
||||
>(
|
||||
options: DefinedInitialDataInfiniteOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey,
|
||||
TPageParam
|
||||
>,
|
||||
): DefinedInitialDataInfiniteOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey,
|
||||
TPageParam
|
||||
> & {
|
||||
queryKey: DataTag<TQueryKey, InfiniteData<TQueryFnData>, TError>
|
||||
}
|
||||
|
||||
export function infiniteQueryOptions<
|
||||
TQueryFnData,
|
||||
TError = DefaultError,
|
||||
TData = InfiniteData<TQueryFnData>,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
TPageParam = unknown,
|
||||
>(
|
||||
options: UnusedSkipTokenInfiniteOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey,
|
||||
TPageParam
|
||||
>,
|
||||
): UnusedSkipTokenInfiniteOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey,
|
||||
TPageParam
|
||||
> & {
|
||||
queryKey: DataTag<TQueryKey, InfiniteData<TQueryFnData>, TError>
|
||||
}
|
||||
|
||||
export function infiniteQueryOptions<
|
||||
TQueryFnData,
|
||||
TError = DefaultError,
|
||||
TData = InfiniteData<TQueryFnData>,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
TPageParam = unknown,
|
||||
>(
|
||||
options: UndefinedInitialDataInfiniteOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey,
|
||||
TPageParam
|
||||
>,
|
||||
): UndefinedInitialDataInfiniteOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey,
|
||||
TPageParam
|
||||
> & {
|
||||
queryKey: DataTag<TQueryKey, InfiniteData<TQueryFnData>, TError>
|
||||
}
|
||||
|
||||
export function infiniteQueryOptions(options: unknown) {
|
||||
return options
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import type { DefaultError, WithRequired } from '@tanstack/query-core'
|
||||
import type { UseMutationOptions } from './types'
|
||||
|
||||
export function mutationOptions<
|
||||
TData = unknown,
|
||||
TError = DefaultError,
|
||||
TVariables = void,
|
||||
TOnMutateResult = unknown,
|
||||
>(
|
||||
options: WithRequired<
|
||||
UseMutationOptions<TData, TError, TVariables, TOnMutateResult>,
|
||||
'mutationKey'
|
||||
>,
|
||||
): WithRequired<
|
||||
UseMutationOptions<TData, TError, TVariables, TOnMutateResult>,
|
||||
'mutationKey'
|
||||
>
|
||||
export function mutationOptions<
|
||||
TData = unknown,
|
||||
TError = DefaultError,
|
||||
TVariables = void,
|
||||
TOnMutateResult = unknown,
|
||||
>(
|
||||
options: Omit<
|
||||
UseMutationOptions<TData, TError, TVariables, TOnMutateResult>,
|
||||
'mutationKey'
|
||||
>,
|
||||
): Omit<
|
||||
UseMutationOptions<TData, TError, TVariables, TOnMutateResult>,
|
||||
'mutationKey'
|
||||
>
|
||||
export function mutationOptions<
|
||||
TData = unknown,
|
||||
TError = DefaultError,
|
||||
TVariables = void,
|
||||
TOnMutateResult = unknown,
|
||||
>(
|
||||
options: UseMutationOptions<TData, TError, TVariables, TOnMutateResult>,
|
||||
): UseMutationOptions<TData, TError, TVariables, TOnMutateResult> {
|
||||
return options
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
import type {
|
||||
DataTag,
|
||||
DefaultError,
|
||||
InitialDataFunction,
|
||||
NonUndefinedGuard,
|
||||
OmitKeyof,
|
||||
QueryFunction,
|
||||
QueryKey,
|
||||
SkipToken,
|
||||
} from '@tanstack/query-core'
|
||||
import type { UseQueryOptions } from './types'
|
||||
|
||||
export type UndefinedInitialDataOptions<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
> = UseQueryOptions<TQueryFnData, TError, TData, TQueryKey> & {
|
||||
initialData?:
|
||||
| undefined
|
||||
| InitialDataFunction<NonUndefinedGuard<TQueryFnData>>
|
||||
| NonUndefinedGuard<TQueryFnData>
|
||||
}
|
||||
|
||||
export type UnusedSkipTokenOptions<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
> = OmitKeyof<
|
||||
UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
|
||||
'queryFn'
|
||||
> & {
|
||||
queryFn?: Exclude<
|
||||
UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>['queryFn'],
|
||||
SkipToken | undefined
|
||||
>
|
||||
}
|
||||
|
||||
export type DefinedInitialDataOptions<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
> = Omit<UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>, 'queryFn'> & {
|
||||
initialData:
|
||||
| NonUndefinedGuard<TQueryFnData>
|
||||
| (() => NonUndefinedGuard<TQueryFnData>)
|
||||
queryFn?: QueryFunction<TQueryFnData, TQueryKey>
|
||||
}
|
||||
|
||||
export function queryOptions<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
>(
|
||||
options: DefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey>,
|
||||
): DefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey> & {
|
||||
queryKey: DataTag<TQueryKey, TQueryFnData, TError>
|
||||
}
|
||||
|
||||
export function queryOptions<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
>(
|
||||
options: UnusedSkipTokenOptions<TQueryFnData, TError, TData, TQueryKey>,
|
||||
): UnusedSkipTokenOptions<TQueryFnData, TError, TData, TQueryKey> & {
|
||||
queryKey: DataTag<TQueryKey, TQueryFnData, TError>
|
||||
}
|
||||
|
||||
export function queryOptions<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
>(
|
||||
options: UndefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey>,
|
||||
): UndefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey> & {
|
||||
queryKey: DataTag<TQueryKey, TQueryFnData, TError>
|
||||
}
|
||||
|
||||
export function queryOptions(options: unknown) {
|
||||
return options
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
import type {
|
||||
DefaultError,
|
||||
DefaultedQueryObserverOptions,
|
||||
Query,
|
||||
QueryKey,
|
||||
QueryObserver,
|
||||
QueryObserverResult,
|
||||
} from '@tanstack/query-core'
|
||||
import type { QueryErrorResetBoundaryValue } from './QueryErrorResetBoundary'
|
||||
|
||||
export const defaultThrowOnError = <
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
>(
|
||||
_error: TError,
|
||||
query: Query<TQueryFnData, TError, TData, TQueryKey>,
|
||||
) => query.state.data === undefined
|
||||
|
||||
export const ensureSuspenseTimers = (
|
||||
defaultedOptions: DefaultedQueryObserverOptions<any, any, any, any, any>,
|
||||
) => {
|
||||
if (defaultedOptions.suspense) {
|
||||
// Handle staleTime to ensure minimum 1000ms in Suspense mode
|
||||
// This prevents unnecessary refetching when components remount after suspending
|
||||
const MIN_SUSPENSE_TIME_MS = 1000
|
||||
|
||||
const clamp = (value: number | 'static' | undefined) =>
|
||||
value === 'static'
|
||||
? value
|
||||
: Math.max(value ?? MIN_SUSPENSE_TIME_MS, MIN_SUSPENSE_TIME_MS)
|
||||
|
||||
const originalStaleTime = defaultedOptions.staleTime
|
||||
defaultedOptions.staleTime =
|
||||
typeof originalStaleTime === 'function'
|
||||
? (...args) => clamp(originalStaleTime(...args))
|
||||
: clamp(originalStaleTime)
|
||||
|
||||
if (typeof defaultedOptions.gcTime === 'number') {
|
||||
defaultedOptions.gcTime = Math.max(
|
||||
defaultedOptions.gcTime,
|
||||
MIN_SUSPENSE_TIME_MS,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const willFetch = (
|
||||
result: QueryObserverResult<any, any>,
|
||||
isRestoring: boolean,
|
||||
) => result.isLoading && result.isFetching && !isRestoring
|
||||
|
||||
export const shouldSuspend = (
|
||||
defaultedOptions:
|
||||
| DefaultedQueryObserverOptions<any, any, any, any, any>
|
||||
| undefined,
|
||||
result: QueryObserverResult<any, any>,
|
||||
) => defaultedOptions?.suspense && result.isPending
|
||||
|
||||
export const fetchOptimistic = <
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryData,
|
||||
TQueryKey extends QueryKey,
|
||||
>(
|
||||
defaultedOptions: DefaultedQueryObserverOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryData,
|
||||
TQueryKey
|
||||
>,
|
||||
observer: QueryObserver<TQueryFnData, TError, TData, TQueryData, TQueryKey>,
|
||||
errorResetBoundary: QueryErrorResetBoundaryValue,
|
||||
) =>
|
||||
observer.fetchOptimistic(defaultedOptions).catch(() => {
|
||||
errorResetBoundary.clearReset()
|
||||
})
|
||||
+242
@@ -0,0 +1,242 @@
|
||||
/* istanbul ignore file */
|
||||
|
||||
import type {
|
||||
DefaultError,
|
||||
DefinedInfiniteQueryObserverResult,
|
||||
DefinedQueryObserverResult,
|
||||
DistributiveOmit,
|
||||
FetchQueryOptions,
|
||||
InfiniteQueryObserverOptions,
|
||||
InfiniteQueryObserverResult,
|
||||
MutateFunction,
|
||||
MutationObserverOptions,
|
||||
MutationObserverResult,
|
||||
OmitKeyof,
|
||||
Override,
|
||||
QueryKey,
|
||||
QueryObserverOptions,
|
||||
QueryObserverResult,
|
||||
SkipToken,
|
||||
} from '@tanstack/query-core'
|
||||
|
||||
export type AnyUseBaseQueryOptions = UseBaseQueryOptions<
|
||||
any,
|
||||
any,
|
||||
any,
|
||||
any,
|
||||
any
|
||||
>
|
||||
export interface UseBaseQueryOptions<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
> extends QueryObserverOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryData,
|
||||
TQueryKey
|
||||
> {
|
||||
/**
|
||||
* Set this to `false` to unsubscribe this observer from updates to the query cache.
|
||||
* Defaults to `true`.
|
||||
*/
|
||||
subscribed?: boolean
|
||||
}
|
||||
|
||||
export interface UsePrefetchQueryOptions<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
> extends OmitKeyof<
|
||||
FetchQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
|
||||
'queryFn'
|
||||
> {
|
||||
queryFn?: Exclude<
|
||||
FetchQueryOptions<TQueryFnData, TError, TData, TQueryKey>['queryFn'],
|
||||
SkipToken
|
||||
>
|
||||
}
|
||||
|
||||
export type AnyUseQueryOptions = UseQueryOptions<any, any, any, any>
|
||||
export interface UseQueryOptions<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
> extends OmitKeyof<
|
||||
UseBaseQueryOptions<TQueryFnData, TError, TData, TQueryFnData, TQueryKey>,
|
||||
'suspense'
|
||||
> {}
|
||||
|
||||
export type AnyUseSuspenseQueryOptions = UseSuspenseQueryOptions<
|
||||
any,
|
||||
any,
|
||||
any,
|
||||
any
|
||||
>
|
||||
export interface UseSuspenseQueryOptions<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
> extends OmitKeyof<
|
||||
UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
|
||||
'queryFn' | 'enabled' | 'throwOnError' | 'placeholderData'
|
||||
> {
|
||||
queryFn?: Exclude<
|
||||
UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>['queryFn'],
|
||||
SkipToken
|
||||
>
|
||||
}
|
||||
|
||||
export type AnyUseInfiniteQueryOptions = UseInfiniteQueryOptions<
|
||||
any,
|
||||
any,
|
||||
any,
|
||||
any,
|
||||
any
|
||||
>
|
||||
export interface UseInfiniteQueryOptions<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
TPageParam = unknown,
|
||||
> extends OmitKeyof<
|
||||
InfiniteQueryObserverOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey,
|
||||
TPageParam
|
||||
>,
|
||||
'suspense'
|
||||
> {
|
||||
/**
|
||||
* Set this to `false` to unsubscribe this observer from updates to the query cache.
|
||||
* Defaults to `true`.
|
||||
*/
|
||||
subscribed?: boolean
|
||||
}
|
||||
|
||||
export type AnyUseSuspenseInfiniteQueryOptions =
|
||||
UseSuspenseInfiniteQueryOptions<any, any, any, any, any>
|
||||
export interface UseSuspenseInfiniteQueryOptions<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
TPageParam = unknown,
|
||||
> extends OmitKeyof<
|
||||
UseInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>,
|
||||
'queryFn' | 'enabled' | 'throwOnError' | 'placeholderData'
|
||||
> {
|
||||
queryFn?: Exclude<
|
||||
UseInfiniteQueryOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey,
|
||||
TPageParam
|
||||
>['queryFn'],
|
||||
SkipToken
|
||||
>
|
||||
}
|
||||
|
||||
export type UseBaseQueryResult<
|
||||
TData = unknown,
|
||||
TError = DefaultError,
|
||||
> = QueryObserverResult<TData, TError>
|
||||
|
||||
export type UseQueryResult<
|
||||
TData = unknown,
|
||||
TError = DefaultError,
|
||||
> = UseBaseQueryResult<TData, TError>
|
||||
|
||||
export type UseSuspenseQueryResult<
|
||||
TData = unknown,
|
||||
TError = DefaultError,
|
||||
> = DistributiveOmit<
|
||||
DefinedQueryObserverResult<TData, TError>,
|
||||
'isPlaceholderData' | 'promise'
|
||||
>
|
||||
|
||||
export type DefinedUseQueryResult<
|
||||
TData = unknown,
|
||||
TError = DefaultError,
|
||||
> = DefinedQueryObserverResult<TData, TError>
|
||||
|
||||
export type UseInfiniteQueryResult<
|
||||
TData = unknown,
|
||||
TError = DefaultError,
|
||||
> = InfiniteQueryObserverResult<TData, TError>
|
||||
|
||||
export type DefinedUseInfiniteQueryResult<
|
||||
TData = unknown,
|
||||
TError = DefaultError,
|
||||
> = DefinedInfiniteQueryObserverResult<TData, TError>
|
||||
|
||||
export type UseSuspenseInfiniteQueryResult<
|
||||
TData = unknown,
|
||||
TError = DefaultError,
|
||||
> = OmitKeyof<
|
||||
DefinedInfiniteQueryObserverResult<TData, TError>,
|
||||
'isPlaceholderData' | 'promise'
|
||||
>
|
||||
|
||||
export type AnyUseMutationOptions = UseMutationOptions<any, any, any, any>
|
||||
export interface UseMutationOptions<
|
||||
TData = unknown,
|
||||
TError = DefaultError,
|
||||
TVariables = void,
|
||||
TOnMutateResult = unknown,
|
||||
> extends OmitKeyof<
|
||||
MutationObserverOptions<TData, TError, TVariables, TOnMutateResult>,
|
||||
'_defaulted'
|
||||
> {}
|
||||
|
||||
export type UseMutateFunction<
|
||||
TData = unknown,
|
||||
TError = DefaultError,
|
||||
TVariables = void,
|
||||
TOnMutateResult = unknown,
|
||||
> = (
|
||||
...args: Parameters<
|
||||
MutateFunction<TData, TError, TVariables, TOnMutateResult>
|
||||
>
|
||||
) => void
|
||||
|
||||
export type UseMutateAsyncFunction<
|
||||
TData = unknown,
|
||||
TError = DefaultError,
|
||||
TVariables = void,
|
||||
TOnMutateResult = unknown,
|
||||
> = MutateFunction<TData, TError, TVariables, TOnMutateResult>
|
||||
|
||||
export type UseBaseMutationResult<
|
||||
TData = unknown,
|
||||
TError = DefaultError,
|
||||
TVariables = unknown,
|
||||
TOnMutateResult = unknown,
|
||||
> = Override<
|
||||
MutationObserverResult<TData, TError, TVariables, TOnMutateResult>,
|
||||
{ mutate: UseMutateFunction<TData, TError, TVariables, TOnMutateResult> }
|
||||
> & {
|
||||
mutateAsync: UseMutateAsyncFunction<
|
||||
TData,
|
||||
TError,
|
||||
TVariables,
|
||||
TOnMutateResult
|
||||
>
|
||||
}
|
||||
|
||||
export type UseMutationResult<
|
||||
TData = unknown,
|
||||
TError = DefaultError,
|
||||
TVariables = unknown,
|
||||
TOnMutateResult = unknown,
|
||||
> = UseBaseMutationResult<TData, TError, TVariables, TOnMutateResult>
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
'use client'
|
||||
import * as React from 'react'
|
||||
|
||||
import { environmentManager, noop, notifyManager } from '@tanstack/query-core'
|
||||
import { useQueryClient } from './QueryClientProvider'
|
||||
import { useQueryErrorResetBoundary } from './QueryErrorResetBoundary'
|
||||
import {
|
||||
ensurePreventErrorBoundaryRetry,
|
||||
getHasError,
|
||||
useClearResetErrorBoundary,
|
||||
} from './errorBoundaryUtils'
|
||||
import { useIsRestoring } from './IsRestoringProvider'
|
||||
import {
|
||||
ensureSuspenseTimers,
|
||||
fetchOptimistic,
|
||||
shouldSuspend,
|
||||
willFetch,
|
||||
} from './suspense'
|
||||
import type {
|
||||
QueryClient,
|
||||
QueryKey,
|
||||
QueryObserver,
|
||||
QueryObserverResult,
|
||||
} from '@tanstack/query-core'
|
||||
import type { UseBaseQueryOptions } from './types'
|
||||
|
||||
export function useBaseQuery<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryData,
|
||||
TQueryKey extends QueryKey,
|
||||
>(
|
||||
options: UseBaseQueryOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryData,
|
||||
TQueryKey
|
||||
>,
|
||||
Observer: typeof QueryObserver,
|
||||
queryClient?: QueryClient,
|
||||
): QueryObserverResult<TData, TError> {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
if (typeof options !== 'object' || Array.isArray(options)) {
|
||||
throw new Error(
|
||||
'Bad argument type. Starting with v5, only the "Object" form is allowed when calling query related functions. Please use the error stack to find the culprit call. More info here: https://tanstack.com/query/latest/docs/react/guides/migrating-to-v5#supports-a-single-signature-one-object',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const isRestoring = useIsRestoring()
|
||||
const errorResetBoundary = useQueryErrorResetBoundary()
|
||||
const client = useQueryClient(queryClient)
|
||||
const defaultedOptions = client.defaultQueryOptions(options)
|
||||
;(client.getDefaultOptions().queries as any)?._experimental_beforeQuery?.(
|
||||
defaultedOptions,
|
||||
)
|
||||
|
||||
const query = client
|
||||
.getQueryCache()
|
||||
.get<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TQueryData,
|
||||
TQueryKey
|
||||
>(defaultedOptions.queryHash)
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
if (!defaultedOptions.queryFn) {
|
||||
console.error(
|
||||
`[${defaultedOptions.queryHash}]: No queryFn was passed as an option, and no default queryFn was found. The queryFn parameter is only optional when using a default queryFn. More info here: https://tanstack.com/query/latest/docs/framework/react/guides/default-query-function`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure results are optimistically set in fetching state before subscribing or updating options
|
||||
defaultedOptions._optimisticResults = isRestoring
|
||||
? 'isRestoring'
|
||||
: 'optimistic'
|
||||
|
||||
ensureSuspenseTimers(defaultedOptions)
|
||||
ensurePreventErrorBoundaryRetry(defaultedOptions, errorResetBoundary, query)
|
||||
useClearResetErrorBoundary(errorResetBoundary)
|
||||
|
||||
// this needs to be invoked before creating the Observer because that can create a cache entry
|
||||
const isNewCacheEntry = !client
|
||||
.getQueryCache()
|
||||
.get(defaultedOptions.queryHash)
|
||||
|
||||
const [observer] = React.useState(
|
||||
() =>
|
||||
new Observer<TQueryFnData, TError, TData, TQueryData, TQueryKey>(
|
||||
client,
|
||||
defaultedOptions,
|
||||
),
|
||||
)
|
||||
|
||||
// note: this must be called before useSyncExternalStore
|
||||
const result = observer.getOptimisticResult(defaultedOptions)
|
||||
|
||||
const shouldSubscribe = !isRestoring && options.subscribed !== false
|
||||
React.useSyncExternalStore(
|
||||
React.useCallback(
|
||||
(onStoreChange) => {
|
||||
const unsubscribe = shouldSubscribe
|
||||
? observer.subscribe(notifyManager.batchCalls(onStoreChange))
|
||||
: noop
|
||||
|
||||
// Update result to make sure we did not miss any query updates
|
||||
// between creating the observer and subscribing to it.
|
||||
observer.updateResult()
|
||||
|
||||
return unsubscribe
|
||||
},
|
||||
[observer, shouldSubscribe],
|
||||
),
|
||||
() => observer.getCurrentResult(),
|
||||
() => observer.getCurrentResult(),
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
observer.setOptions(defaultedOptions)
|
||||
}, [defaultedOptions, observer])
|
||||
|
||||
// Handle suspense
|
||||
if (shouldSuspend(defaultedOptions, result)) {
|
||||
throw fetchOptimistic(defaultedOptions, observer, errorResetBoundary)
|
||||
}
|
||||
|
||||
// Handle error boundary
|
||||
if (
|
||||
getHasError({
|
||||
result,
|
||||
errorResetBoundary,
|
||||
throwOnError: defaultedOptions.throwOnError,
|
||||
query,
|
||||
suspense: defaultedOptions.suspense,
|
||||
})
|
||||
) {
|
||||
throw result.error
|
||||
}
|
||||
|
||||
;(client.getDefaultOptions().queries as any)?._experimental_afterQuery?.(
|
||||
defaultedOptions,
|
||||
result,
|
||||
)
|
||||
|
||||
if (
|
||||
defaultedOptions.experimental_prefetchInRender &&
|
||||
!environmentManager.isServer() &&
|
||||
willFetch(result, isRestoring)
|
||||
) {
|
||||
const promise = isNewCacheEntry
|
||||
? // Fetch immediately on render in order to ensure `.promise` is resolved even if the component is unmounted
|
||||
fetchOptimistic(defaultedOptions, observer, errorResetBoundary)
|
||||
: // subscribe to the "cache promise" so that we can finalize the currentThenable once data comes in
|
||||
query?.promise
|
||||
|
||||
promise?.catch(noop).finally(() => {
|
||||
// `.updateResult()` will trigger `.#currentThenable` to finalize
|
||||
observer.updateResult()
|
||||
})
|
||||
}
|
||||
|
||||
// Handle result property usage tracking
|
||||
return !defaultedOptions.notifyOnChangeProps
|
||||
? observer.trackResult(result)
|
||||
: result
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
'use client'
|
||||
import { InfiniteQueryObserver } from '@tanstack/query-core'
|
||||
import { useBaseQuery } from './useBaseQuery'
|
||||
import type {
|
||||
DefaultError,
|
||||
InfiniteData,
|
||||
QueryClient,
|
||||
QueryKey,
|
||||
QueryObserver,
|
||||
} from '@tanstack/query-core'
|
||||
import type {
|
||||
DefinedUseInfiniteQueryResult,
|
||||
UseInfiniteQueryOptions,
|
||||
UseInfiniteQueryResult,
|
||||
} from './types'
|
||||
import type {
|
||||
DefinedInitialDataInfiniteOptions,
|
||||
UndefinedInitialDataInfiniteOptions,
|
||||
} from './infiniteQueryOptions'
|
||||
|
||||
export function useInfiniteQuery<
|
||||
TQueryFnData,
|
||||
TError = DefaultError,
|
||||
TData = InfiniteData<TQueryFnData>,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
TPageParam = unknown,
|
||||
>(
|
||||
options: DefinedInitialDataInfiniteOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey,
|
||||
TPageParam
|
||||
>,
|
||||
queryClient?: QueryClient,
|
||||
): DefinedUseInfiniteQueryResult<TData, TError>
|
||||
|
||||
export function useInfiniteQuery<
|
||||
TQueryFnData,
|
||||
TError = DefaultError,
|
||||
TData = InfiniteData<TQueryFnData>,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
TPageParam = unknown,
|
||||
>(
|
||||
options: UndefinedInitialDataInfiniteOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey,
|
||||
TPageParam
|
||||
>,
|
||||
queryClient?: QueryClient,
|
||||
): UseInfiniteQueryResult<TData, TError>
|
||||
|
||||
export function useInfiniteQuery<
|
||||
TQueryFnData,
|
||||
TError = DefaultError,
|
||||
TData = InfiniteData<TQueryFnData>,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
TPageParam = unknown,
|
||||
>(
|
||||
options: UseInfiniteQueryOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey,
|
||||
TPageParam
|
||||
>,
|
||||
queryClient?: QueryClient,
|
||||
): UseInfiniteQueryResult<TData, TError>
|
||||
|
||||
export function useInfiniteQuery(
|
||||
options: UseInfiniteQueryOptions,
|
||||
queryClient?: QueryClient,
|
||||
) {
|
||||
return useBaseQuery(
|
||||
options,
|
||||
InfiniteQueryObserver as typeof QueryObserver,
|
||||
queryClient,
|
||||
)
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
'use client'
|
||||
import * as React from 'react'
|
||||
import { notifyManager } from '@tanstack/query-core'
|
||||
|
||||
import { useQueryClient } from './QueryClientProvider'
|
||||
import type { QueryClient, QueryFilters } from '@tanstack/query-core'
|
||||
|
||||
export function useIsFetching(
|
||||
filters?: QueryFilters,
|
||||
queryClient?: QueryClient,
|
||||
): number {
|
||||
const client = useQueryClient(queryClient)
|
||||
const queryCache = client.getQueryCache()
|
||||
|
||||
return React.useSyncExternalStore(
|
||||
React.useCallback(
|
||||
(onStoreChange) =>
|
||||
queryCache.subscribe(notifyManager.batchCalls(onStoreChange)),
|
||||
[queryCache],
|
||||
),
|
||||
() => client.isFetching(filters),
|
||||
() => client.isFetching(filters),
|
||||
)
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
'use client'
|
||||
import * as React from 'react'
|
||||
import {
|
||||
MutationObserver,
|
||||
noop,
|
||||
notifyManager,
|
||||
shouldThrowError,
|
||||
} from '@tanstack/query-core'
|
||||
import { useQueryClient } from './QueryClientProvider'
|
||||
import type {
|
||||
UseMutateFunction,
|
||||
UseMutationOptions,
|
||||
UseMutationResult,
|
||||
} from './types'
|
||||
import type { DefaultError, QueryClient } from '@tanstack/query-core'
|
||||
|
||||
// HOOK
|
||||
|
||||
export function useMutation<
|
||||
TData = unknown,
|
||||
TError = DefaultError,
|
||||
TVariables = void,
|
||||
TOnMutateResult = unknown,
|
||||
>(
|
||||
options: UseMutationOptions<TData, TError, TVariables, TOnMutateResult>,
|
||||
queryClient?: QueryClient,
|
||||
): UseMutationResult<TData, TError, TVariables, TOnMutateResult> {
|
||||
const client = useQueryClient(queryClient)
|
||||
|
||||
const [observer] = React.useState(
|
||||
() =>
|
||||
new MutationObserver<TData, TError, TVariables, TOnMutateResult>(
|
||||
client,
|
||||
options,
|
||||
),
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
observer.setOptions(options)
|
||||
}, [observer, options])
|
||||
|
||||
const result = React.useSyncExternalStore(
|
||||
React.useCallback(
|
||||
(onStoreChange) =>
|
||||
observer.subscribe(notifyManager.batchCalls(onStoreChange)),
|
||||
[observer],
|
||||
),
|
||||
() => observer.getCurrentResult(),
|
||||
() => observer.getCurrentResult(),
|
||||
)
|
||||
|
||||
const mutate = React.useCallback<
|
||||
UseMutateFunction<TData, TError, TVariables, TOnMutateResult>
|
||||
>(
|
||||
(variables, mutateOptions) => {
|
||||
observer.mutate(variables, mutateOptions).catch(noop)
|
||||
},
|
||||
[observer],
|
||||
)
|
||||
|
||||
if (
|
||||
result.error &&
|
||||
shouldThrowError(observer.options.throwOnError, [result.error])
|
||||
) {
|
||||
throw result.error
|
||||
}
|
||||
|
||||
return { ...result, mutate, mutateAsync: result.mutate }
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
'use client'
|
||||
import * as React from 'react'
|
||||
|
||||
import { notifyManager, replaceEqualDeep } from '@tanstack/query-core'
|
||||
import { useQueryClient } from './QueryClientProvider'
|
||||
import type {
|
||||
Mutation,
|
||||
MutationCache,
|
||||
MutationFilters,
|
||||
MutationState,
|
||||
QueryClient,
|
||||
} from '@tanstack/query-core'
|
||||
|
||||
export function useIsMutating(
|
||||
filters?: MutationFilters,
|
||||
queryClient?: QueryClient,
|
||||
): number {
|
||||
const client = useQueryClient(queryClient)
|
||||
return useMutationState(
|
||||
{ filters: { ...filters, status: 'pending' } },
|
||||
client,
|
||||
).length
|
||||
}
|
||||
|
||||
type MutationStateOptions<TResult = MutationState> = {
|
||||
filters?: MutationFilters
|
||||
select?: (mutation: Mutation) => TResult
|
||||
}
|
||||
|
||||
function getResult<TResult = MutationState>(
|
||||
mutationCache: MutationCache,
|
||||
options: MutationStateOptions<TResult>,
|
||||
): Array<TResult> {
|
||||
return mutationCache
|
||||
.findAll(options.filters)
|
||||
.map(
|
||||
(mutation): TResult =>
|
||||
(options.select ? options.select(mutation) : mutation.state) as TResult,
|
||||
)
|
||||
}
|
||||
|
||||
export function useMutationState<TResult = MutationState>(
|
||||
options: MutationStateOptions<TResult> = {},
|
||||
queryClient?: QueryClient,
|
||||
): Array<TResult> {
|
||||
const mutationCache = useQueryClient(queryClient).getMutationCache()
|
||||
const optionsRef = React.useRef(options)
|
||||
const result = React.useRef<Array<TResult>>(null)
|
||||
if (result.current === null) {
|
||||
result.current = getResult(mutationCache, options)
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
optionsRef.current = options
|
||||
})
|
||||
|
||||
return React.useSyncExternalStore(
|
||||
React.useCallback(
|
||||
(onStoreChange) =>
|
||||
mutationCache.subscribe(() => {
|
||||
const nextResult = replaceEqualDeep(
|
||||
result.current,
|
||||
getResult(mutationCache, optionsRef.current),
|
||||
)
|
||||
if (result.current !== nextResult) {
|
||||
result.current = nextResult
|
||||
notifyManager.schedule(onStoreChange)
|
||||
}
|
||||
}),
|
||||
[mutationCache],
|
||||
),
|
||||
() => result.current,
|
||||
() => result.current,
|
||||
)!
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import { useQueryClient } from './QueryClientProvider'
|
||||
import type {
|
||||
DefaultError,
|
||||
FetchInfiniteQueryOptions,
|
||||
QueryClient,
|
||||
QueryKey,
|
||||
} from '@tanstack/query-core'
|
||||
|
||||
export function usePrefetchInfiniteQuery<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
TPageParam = unknown,
|
||||
>(
|
||||
options: FetchInfiniteQueryOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey,
|
||||
TPageParam
|
||||
>,
|
||||
queryClient?: QueryClient,
|
||||
) {
|
||||
const client = useQueryClient(queryClient)
|
||||
|
||||
if (!client.getQueryState(options.queryKey)) {
|
||||
client.prefetchInfiniteQuery(options)
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import { useQueryClient } from './QueryClientProvider'
|
||||
import type { DefaultError, QueryClient, QueryKey } from '@tanstack/query-core'
|
||||
import type { UsePrefetchQueryOptions } from './types'
|
||||
|
||||
export function usePrefetchQuery<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
>(
|
||||
options: UsePrefetchQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
|
||||
queryClient?: QueryClient,
|
||||
) {
|
||||
const client = useQueryClient(queryClient)
|
||||
|
||||
if (!client.getQueryState(options.queryKey)) {
|
||||
client.prefetchQuery(options)
|
||||
}
|
||||
}
|
||||
+328
@@ -0,0 +1,328 @@
|
||||
'use client'
|
||||
import * as React from 'react'
|
||||
|
||||
import {
|
||||
QueriesObserver,
|
||||
QueryObserver,
|
||||
noop,
|
||||
notifyManager,
|
||||
} from '@tanstack/query-core'
|
||||
import { useQueryClient } from './QueryClientProvider'
|
||||
import { useIsRestoring } from './IsRestoringProvider'
|
||||
import { useQueryErrorResetBoundary } from './QueryErrorResetBoundary'
|
||||
import {
|
||||
ensurePreventErrorBoundaryRetry,
|
||||
getHasError,
|
||||
useClearResetErrorBoundary,
|
||||
} from './errorBoundaryUtils'
|
||||
import {
|
||||
ensureSuspenseTimers,
|
||||
fetchOptimistic,
|
||||
shouldSuspend,
|
||||
} from './suspense'
|
||||
import type {
|
||||
DefinedUseQueryResult,
|
||||
UseQueryOptions,
|
||||
UseQueryResult,
|
||||
} from './types'
|
||||
import type {
|
||||
DefaultError,
|
||||
OmitKeyof,
|
||||
QueriesObserverOptions,
|
||||
QueriesPlaceholderDataFunction,
|
||||
QueryClient,
|
||||
QueryFunction,
|
||||
QueryKey,
|
||||
QueryObserverOptions,
|
||||
ThrowOnError,
|
||||
} from '@tanstack/query-core'
|
||||
|
||||
// This defines the `UseQueryOptions` that are accepted in `QueriesOptions` & `GetOptions`.
|
||||
// `placeholderData` function always gets undefined passed
|
||||
type UseQueryOptionsForUseQueries<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
> = OmitKeyof<
|
||||
UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
|
||||
'placeholderData' | 'subscribed'
|
||||
> & {
|
||||
placeholderData?: TQueryFnData | QueriesPlaceholderDataFunction<TQueryFnData>
|
||||
}
|
||||
|
||||
// Avoid TS depth-limit error in case of large array literal
|
||||
type MAXIMUM_DEPTH = 20
|
||||
|
||||
// Widen the type of the symbol to enable type inference even if skipToken is not immutable.
|
||||
type SkipTokenForUseQueries = symbol
|
||||
|
||||
type GetUseQueryOptionsForUseQueries<T> =
|
||||
// Part 1: responsible for applying explicit type parameter to function arguments, if object { queryFnData: TQueryFnData, error: TError, data: TData }
|
||||
T extends {
|
||||
queryFnData: infer TQueryFnData
|
||||
error?: infer TError
|
||||
data: infer TData
|
||||
}
|
||||
? UseQueryOptionsForUseQueries<TQueryFnData, TError, TData>
|
||||
: T extends { queryFnData: infer TQueryFnData; error?: infer TError }
|
||||
? UseQueryOptionsForUseQueries<TQueryFnData, TError>
|
||||
: T extends { data: infer TData; error?: infer TError }
|
||||
? UseQueryOptionsForUseQueries<unknown, TError, TData>
|
||||
: // Part 2: responsible for applying explicit type parameter to function arguments, if tuple [TQueryFnData, TError, TData]
|
||||
T extends [infer TQueryFnData, infer TError, infer TData]
|
||||
? UseQueryOptionsForUseQueries<TQueryFnData, TError, TData>
|
||||
: T extends [infer TQueryFnData, infer TError]
|
||||
? UseQueryOptionsForUseQueries<TQueryFnData, TError>
|
||||
: T extends [infer TQueryFnData]
|
||||
? UseQueryOptionsForUseQueries<TQueryFnData>
|
||||
: // Part 3: responsible for inferring and enforcing type if no explicit parameter was provided
|
||||
T extends {
|
||||
queryFn?:
|
||||
| QueryFunction<infer TQueryFnData, infer TQueryKey>
|
||||
| SkipTokenForUseQueries
|
||||
select?: (data: any) => infer TData
|
||||
throwOnError?: ThrowOnError<any, infer TError, any, any>
|
||||
}
|
||||
? UseQueryOptionsForUseQueries<
|
||||
TQueryFnData,
|
||||
unknown extends TError ? DefaultError : TError,
|
||||
unknown extends TData ? TQueryFnData : TData,
|
||||
TQueryKey
|
||||
>
|
||||
: // Fallback
|
||||
UseQueryOptionsForUseQueries
|
||||
|
||||
// A defined initialData setting should return a DefinedUseQueryResult rather than UseQueryResult
|
||||
type GetDefinedOrUndefinedQueryResult<T, TData, TError = unknown> = T extends {
|
||||
initialData?: infer TInitialData
|
||||
}
|
||||
? unknown extends TInitialData
|
||||
? UseQueryResult<TData, TError>
|
||||
: TInitialData extends TData
|
||||
? DefinedUseQueryResult<TData, TError>
|
||||
: TInitialData extends () => infer TInitialDataResult
|
||||
? unknown extends TInitialDataResult
|
||||
? UseQueryResult<TData, TError>
|
||||
: TInitialDataResult extends TData
|
||||
? DefinedUseQueryResult<TData, TError>
|
||||
: UseQueryResult<TData, TError>
|
||||
: UseQueryResult<TData, TError>
|
||||
: UseQueryResult<TData, TError>
|
||||
|
||||
type GetUseQueryResult<T> =
|
||||
// Part 1: responsible for mapping explicit type parameter to function result, if object
|
||||
T extends { queryFnData: any; error?: infer TError; data: infer TData }
|
||||
? GetDefinedOrUndefinedQueryResult<T, TData, TError>
|
||||
: T extends { queryFnData: infer TQueryFnData; error?: infer TError }
|
||||
? GetDefinedOrUndefinedQueryResult<T, TQueryFnData, TError>
|
||||
: T extends { data: infer TData; error?: infer TError }
|
||||
? GetDefinedOrUndefinedQueryResult<T, TData, TError>
|
||||
: // Part 2: responsible for mapping explicit type parameter to function result, if tuple
|
||||
T extends [any, infer TError, infer TData]
|
||||
? GetDefinedOrUndefinedQueryResult<T, TData, TError>
|
||||
: T extends [infer TQueryFnData, infer TError]
|
||||
? GetDefinedOrUndefinedQueryResult<T, TQueryFnData, TError>
|
||||
: T extends [infer TQueryFnData]
|
||||
? GetDefinedOrUndefinedQueryResult<T, TQueryFnData>
|
||||
: // Part 3: responsible for mapping inferred type to results, if no explicit parameter was provided
|
||||
T extends {
|
||||
queryFn?:
|
||||
| QueryFunction<infer TQueryFnData, any>
|
||||
| SkipTokenForUseQueries
|
||||
select?: (data: any) => infer TData
|
||||
throwOnError?: ThrowOnError<any, infer TError, any, any>
|
||||
}
|
||||
? GetDefinedOrUndefinedQueryResult<
|
||||
T,
|
||||
unknown extends TData ? TQueryFnData : TData,
|
||||
unknown extends TError ? DefaultError : TError
|
||||
>
|
||||
: // Fallback
|
||||
UseQueryResult
|
||||
|
||||
/**
|
||||
* QueriesOptions reducer recursively unwraps function arguments to infer/enforce type param
|
||||
*/
|
||||
export type QueriesOptions<
|
||||
T extends Array<any>,
|
||||
TResults extends Array<any> = [],
|
||||
TDepth extends ReadonlyArray<number> = [],
|
||||
> = TDepth['length'] extends MAXIMUM_DEPTH
|
||||
? Array<UseQueryOptionsForUseQueries>
|
||||
: T extends []
|
||||
? []
|
||||
: T extends [infer Head]
|
||||
? [...TResults, GetUseQueryOptionsForUseQueries<Head>]
|
||||
: T extends [infer Head, ...infer Tails]
|
||||
? QueriesOptions<
|
||||
[...Tails],
|
||||
[...TResults, GetUseQueryOptionsForUseQueries<Head>],
|
||||
[...TDepth, 1]
|
||||
>
|
||||
: ReadonlyArray<unknown> extends T
|
||||
? T
|
||||
: // If T is *some* array but we couldn't assign unknown[] to it, then it must hold some known/homogenous type!
|
||||
// use this to infer the param types in the case of Array.map() argument
|
||||
T extends Array<
|
||||
UseQueryOptionsForUseQueries<
|
||||
infer TQueryFnData,
|
||||
infer TError,
|
||||
infer TData,
|
||||
infer TQueryKey
|
||||
>
|
||||
>
|
||||
? Array<
|
||||
UseQueryOptionsForUseQueries<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey
|
||||
>
|
||||
>
|
||||
: // Fallback
|
||||
Array<UseQueryOptionsForUseQueries>
|
||||
|
||||
/**
|
||||
* QueriesResults reducer recursively maps type param to results
|
||||
*/
|
||||
export type QueriesResults<
|
||||
T extends Array<any>,
|
||||
TResults extends Array<any> = [],
|
||||
TDepth extends ReadonlyArray<number> = [],
|
||||
> = TDepth['length'] extends MAXIMUM_DEPTH
|
||||
? Array<UseQueryResult>
|
||||
: T extends []
|
||||
? []
|
||||
: T extends [infer Head]
|
||||
? [...TResults, GetUseQueryResult<Head>]
|
||||
: T extends [infer Head, ...infer Tails]
|
||||
? QueriesResults<
|
||||
[...Tails],
|
||||
[...TResults, GetUseQueryResult<Head>],
|
||||
[...TDepth, 1]
|
||||
>
|
||||
: { [K in keyof T]: GetUseQueryResult<T[K]> }
|
||||
|
||||
export function useQueries<
|
||||
T extends Array<any>,
|
||||
TCombinedResult = QueriesResults<T>,
|
||||
>(
|
||||
{
|
||||
queries,
|
||||
...options
|
||||
}: {
|
||||
queries:
|
||||
| readonly [...QueriesOptions<T>]
|
||||
| readonly [...{ [K in keyof T]: GetUseQueryOptionsForUseQueries<T[K]> }]
|
||||
combine?: (result: QueriesResults<T>) => TCombinedResult
|
||||
subscribed?: boolean
|
||||
},
|
||||
queryClient?: QueryClient,
|
||||
): TCombinedResult {
|
||||
const client = useQueryClient(queryClient)
|
||||
const isRestoring = useIsRestoring()
|
||||
const errorResetBoundary = useQueryErrorResetBoundary()
|
||||
|
||||
const defaultedQueries = React.useMemo(
|
||||
() =>
|
||||
queries.map((opts) => {
|
||||
const defaultedOptions = client.defaultQueryOptions(
|
||||
opts as QueryObserverOptions,
|
||||
)
|
||||
|
||||
// Make sure the results are already in fetching state before subscribing or updating options
|
||||
defaultedOptions._optimisticResults = isRestoring
|
||||
? 'isRestoring'
|
||||
: 'optimistic'
|
||||
|
||||
return defaultedOptions
|
||||
}),
|
||||
[queries, client, isRestoring],
|
||||
)
|
||||
|
||||
defaultedQueries.forEach((queryOptions) => {
|
||||
ensureSuspenseTimers(queryOptions)
|
||||
const query = client.getQueryCache().get(queryOptions.queryHash)
|
||||
ensurePreventErrorBoundaryRetry(queryOptions, errorResetBoundary, query)
|
||||
})
|
||||
|
||||
useClearResetErrorBoundary(errorResetBoundary)
|
||||
|
||||
const [observer] = React.useState(
|
||||
() =>
|
||||
new QueriesObserver<TCombinedResult>(
|
||||
client,
|
||||
defaultedQueries,
|
||||
options as QueriesObserverOptions<TCombinedResult>,
|
||||
),
|
||||
)
|
||||
|
||||
// note: this must be called before useSyncExternalStore
|
||||
const [optimisticResult, getCombinedResult, trackResult] =
|
||||
observer.getOptimisticResult(
|
||||
defaultedQueries,
|
||||
(options as QueriesObserverOptions<TCombinedResult>).combine,
|
||||
)
|
||||
|
||||
const shouldSubscribe = !isRestoring && options.subscribed !== false
|
||||
React.useSyncExternalStore(
|
||||
React.useCallback(
|
||||
(onStoreChange) =>
|
||||
shouldSubscribe
|
||||
? observer.subscribe(notifyManager.batchCalls(onStoreChange))
|
||||
: noop,
|
||||
[observer, shouldSubscribe],
|
||||
),
|
||||
() => observer.getCurrentResult(),
|
||||
() => observer.getCurrentResult(),
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
observer.setQueries(
|
||||
defaultedQueries,
|
||||
options as QueriesObserverOptions<TCombinedResult>,
|
||||
)
|
||||
}, [defaultedQueries, options, observer])
|
||||
|
||||
const shouldAtLeastOneSuspend = optimisticResult.some((result, index) =>
|
||||
shouldSuspend(defaultedQueries[index], result),
|
||||
)
|
||||
|
||||
const suspensePromises = shouldAtLeastOneSuspend
|
||||
? optimisticResult.flatMap((result, index) => {
|
||||
const opts = defaultedQueries[index]
|
||||
|
||||
if (opts && shouldSuspend(opts, result)) {
|
||||
const queryObserver = new QueryObserver(client, opts)
|
||||
return fetchOptimistic(opts, queryObserver, errorResetBoundary)
|
||||
}
|
||||
return []
|
||||
})
|
||||
: []
|
||||
|
||||
if (suspensePromises.length > 0) {
|
||||
throw Promise.all(suspensePromises)
|
||||
}
|
||||
const firstSingleResultWhichShouldThrow = optimisticResult.find(
|
||||
(result, index) => {
|
||||
const query = defaultedQueries[index]
|
||||
return (
|
||||
query &&
|
||||
getHasError({
|
||||
result,
|
||||
errorResetBoundary,
|
||||
throwOnError: query.throwOnError,
|
||||
query: client.getQueryCache().get(query.queryHash),
|
||||
suspense: query.suspense,
|
||||
})
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
if (firstSingleResultWhichShouldThrow?.error) {
|
||||
throw firstSingleResultWhichShouldThrow.error
|
||||
}
|
||||
|
||||
return getCombinedResult(trackResult())
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
'use client'
|
||||
import { QueryObserver } from '@tanstack/query-core'
|
||||
import { useBaseQuery } from './useBaseQuery'
|
||||
import type {
|
||||
DefaultError,
|
||||
NoInfer,
|
||||
QueryClient,
|
||||
QueryKey,
|
||||
} from '@tanstack/query-core'
|
||||
import type {
|
||||
DefinedUseQueryResult,
|
||||
UseQueryOptions,
|
||||
UseQueryResult,
|
||||
} from './types'
|
||||
import type {
|
||||
DefinedInitialDataOptions,
|
||||
UndefinedInitialDataOptions,
|
||||
} from './queryOptions'
|
||||
|
||||
export function useQuery<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
>(
|
||||
options: DefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey>,
|
||||
queryClient?: QueryClient,
|
||||
): DefinedUseQueryResult<NoInfer<TData>, TError>
|
||||
|
||||
export function useQuery<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
>(
|
||||
options: UndefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey>,
|
||||
queryClient?: QueryClient,
|
||||
): UseQueryResult<NoInfer<TData>, TError>
|
||||
|
||||
export function useQuery<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
>(
|
||||
options: UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
|
||||
queryClient?: QueryClient,
|
||||
): UseQueryResult<NoInfer<TData>, TError>
|
||||
|
||||
export function useQuery(options: UseQueryOptions, queryClient?: QueryClient) {
|
||||
return useBaseQuery(options, QueryObserver, queryClient)
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
'use client'
|
||||
import { InfiniteQueryObserver, skipToken } from '@tanstack/query-core'
|
||||
import { useBaseQuery } from './useBaseQuery'
|
||||
import { defaultThrowOnError } from './suspense'
|
||||
import type {
|
||||
DefaultError,
|
||||
InfiniteData,
|
||||
InfiniteQueryObserverSuccessResult,
|
||||
QueryClient,
|
||||
QueryKey,
|
||||
QueryObserver,
|
||||
} from '@tanstack/query-core'
|
||||
import type {
|
||||
UseSuspenseInfiniteQueryOptions,
|
||||
UseSuspenseInfiniteQueryResult,
|
||||
} from './types'
|
||||
|
||||
export function useSuspenseInfiniteQuery<
|
||||
TQueryFnData,
|
||||
TError = DefaultError,
|
||||
TData = InfiniteData<TQueryFnData>,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
TPageParam = unknown,
|
||||
>(
|
||||
options: UseSuspenseInfiniteQueryOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey,
|
||||
TPageParam
|
||||
>,
|
||||
queryClient?: QueryClient,
|
||||
): UseSuspenseInfiniteQueryResult<TData, TError> {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
if ((options.queryFn as any) === skipToken) {
|
||||
console.error('skipToken is not allowed for useSuspenseInfiniteQuery')
|
||||
}
|
||||
}
|
||||
|
||||
return useBaseQuery(
|
||||
{
|
||||
...options,
|
||||
enabled: true,
|
||||
suspense: true,
|
||||
throwOnError: defaultThrowOnError,
|
||||
},
|
||||
InfiniteQueryObserver as typeof QueryObserver,
|
||||
queryClient,
|
||||
) as InfiniteQueryObserverSuccessResult<TData, TError>
|
||||
}
|
||||
+211
@@ -0,0 +1,211 @@
|
||||
'use client'
|
||||
import { skipToken } from '@tanstack/query-core'
|
||||
import { useQueries } from './useQueries'
|
||||
import { defaultThrowOnError } from './suspense'
|
||||
import type { UseSuspenseQueryOptions, UseSuspenseQueryResult } from './types'
|
||||
import type {
|
||||
DefaultError,
|
||||
QueryClient,
|
||||
QueryFunction,
|
||||
ThrowOnError,
|
||||
} from '@tanstack/query-core'
|
||||
|
||||
// Avoid TS depth-limit error in case of large array literal
|
||||
type MAXIMUM_DEPTH = 20
|
||||
|
||||
// Widen the type of the symbol to enable type inference even if skipToken is not immutable.
|
||||
type SkipTokenForUseQueries = symbol
|
||||
|
||||
type GetUseSuspenseQueryOptions<T> =
|
||||
// Part 1: responsible for applying explicit type parameter to function arguments, if object { queryFnData: TQueryFnData, error: TError, data: TData }
|
||||
T extends {
|
||||
queryFnData: infer TQueryFnData
|
||||
error?: infer TError
|
||||
data: infer TData
|
||||
}
|
||||
? UseSuspenseQueryOptions<TQueryFnData, TError, TData>
|
||||
: T extends { queryFnData: infer TQueryFnData; error?: infer TError }
|
||||
? UseSuspenseQueryOptions<TQueryFnData, TError>
|
||||
: T extends { data: infer TData; error?: infer TError }
|
||||
? UseSuspenseQueryOptions<unknown, TError, TData>
|
||||
: // Part 2: responsible for applying explicit type parameter to function arguments, if tuple [TQueryFnData, TError, TData]
|
||||
T extends [infer TQueryFnData, infer TError, infer TData]
|
||||
? UseSuspenseQueryOptions<TQueryFnData, TError, TData>
|
||||
: T extends [infer TQueryFnData, infer TError]
|
||||
? UseSuspenseQueryOptions<TQueryFnData, TError>
|
||||
: T extends [infer TQueryFnData]
|
||||
? UseSuspenseQueryOptions<TQueryFnData>
|
||||
: // Part 3: responsible for inferring and enforcing type if no explicit parameter was provided
|
||||
T extends {
|
||||
queryFn?:
|
||||
| QueryFunction<infer TQueryFnData, infer TQueryKey>
|
||||
| SkipTokenForUseQueries
|
||||
select?: (data: any) => infer TData
|
||||
throwOnError?: ThrowOnError<any, infer TError, any, any>
|
||||
}
|
||||
? UseSuspenseQueryOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey
|
||||
>
|
||||
: T extends {
|
||||
queryFn?:
|
||||
| QueryFunction<infer TQueryFnData, infer TQueryKey>
|
||||
| SkipTokenForUseQueries
|
||||
throwOnError?: ThrowOnError<any, infer TError, any, any>
|
||||
}
|
||||
? UseSuspenseQueryOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TQueryFnData,
|
||||
TQueryKey
|
||||
>
|
||||
: // Fallback
|
||||
UseSuspenseQueryOptions
|
||||
|
||||
type GetUseSuspenseQueryResult<T> =
|
||||
// Part 1: responsible for mapping explicit type parameter to function result, if object
|
||||
T extends { queryFnData: any; error?: infer TError; data: infer TData }
|
||||
? UseSuspenseQueryResult<TData, TError>
|
||||
: T extends { queryFnData: infer TQueryFnData; error?: infer TError }
|
||||
? UseSuspenseQueryResult<TQueryFnData, TError>
|
||||
: T extends { data: infer TData; error?: infer TError }
|
||||
? UseSuspenseQueryResult<TData, TError>
|
||||
: // Part 2: responsible for mapping explicit type parameter to function result, if tuple
|
||||
T extends [any, infer TError, infer TData]
|
||||
? UseSuspenseQueryResult<TData, TError>
|
||||
: T extends [infer TQueryFnData, infer TError]
|
||||
? UseSuspenseQueryResult<TQueryFnData, TError>
|
||||
: T extends [infer TQueryFnData]
|
||||
? UseSuspenseQueryResult<TQueryFnData>
|
||||
: // Part 3: responsible for mapping inferred type to results, if no explicit parameter was provided
|
||||
T extends {
|
||||
queryFn?:
|
||||
| QueryFunction<infer TQueryFnData, any>
|
||||
| SkipTokenForUseQueries
|
||||
select?: (data: any) => infer TData
|
||||
throwOnError?: ThrowOnError<any, infer TError, any, any>
|
||||
}
|
||||
? UseSuspenseQueryResult<
|
||||
unknown extends TData ? TQueryFnData : TData,
|
||||
unknown extends TError ? DefaultError : TError
|
||||
>
|
||||
: T extends {
|
||||
queryFn?:
|
||||
| QueryFunction<infer TQueryFnData, any>
|
||||
| SkipTokenForUseQueries
|
||||
throwOnError?: ThrowOnError<any, infer TError, any, any>
|
||||
}
|
||||
? UseSuspenseQueryResult<
|
||||
TQueryFnData,
|
||||
unknown extends TError ? DefaultError : TError
|
||||
>
|
||||
: // Fallback
|
||||
UseSuspenseQueryResult
|
||||
|
||||
/**
|
||||
* SuspenseQueriesOptions reducer recursively unwraps function arguments to infer/enforce type param
|
||||
*/
|
||||
export type SuspenseQueriesOptions<
|
||||
T extends Array<any>,
|
||||
TResults extends Array<any> = [],
|
||||
TDepth extends ReadonlyArray<number> = [],
|
||||
> = TDepth['length'] extends MAXIMUM_DEPTH
|
||||
? Array<UseSuspenseQueryOptions>
|
||||
: T extends []
|
||||
? []
|
||||
: T extends [infer Head]
|
||||
? [...TResults, GetUseSuspenseQueryOptions<Head>]
|
||||
: T extends [infer Head, ...infer Tails]
|
||||
? SuspenseQueriesOptions<
|
||||
[...Tails],
|
||||
[...TResults, GetUseSuspenseQueryOptions<Head>],
|
||||
[...TDepth, 1]
|
||||
>
|
||||
: Array<unknown> extends T
|
||||
? T
|
||||
: // If T is *some* array but we couldn't assign unknown[] to it, then it must hold some known/homogenous type!
|
||||
// use this to infer the param types in the case of Array.map() argument
|
||||
T extends Array<
|
||||
UseSuspenseQueryOptions<
|
||||
infer TQueryFnData,
|
||||
infer TError,
|
||||
infer TData,
|
||||
infer TQueryKey
|
||||
>
|
||||
>
|
||||
? Array<
|
||||
UseSuspenseQueryOptions<TQueryFnData, TError, TData, TQueryKey>
|
||||
>
|
||||
: // Fallback
|
||||
Array<UseSuspenseQueryOptions>
|
||||
|
||||
/**
|
||||
* SuspenseQueriesResults reducer recursively maps type param to results
|
||||
*/
|
||||
export type SuspenseQueriesResults<
|
||||
T extends Array<any>,
|
||||
TResults extends Array<any> = [],
|
||||
TDepth extends ReadonlyArray<number> = [],
|
||||
> = TDepth['length'] extends MAXIMUM_DEPTH
|
||||
? Array<UseSuspenseQueryResult>
|
||||
: T extends []
|
||||
? []
|
||||
: T extends [infer Head]
|
||||
? [...TResults, GetUseSuspenseQueryResult<Head>]
|
||||
: T extends [infer Head, ...infer Tails]
|
||||
? SuspenseQueriesResults<
|
||||
[...Tails],
|
||||
[...TResults, GetUseSuspenseQueryResult<Head>],
|
||||
[...TDepth, 1]
|
||||
>
|
||||
: { [K in keyof T]: GetUseSuspenseQueryResult<T[K]> }
|
||||
|
||||
export function useSuspenseQueries<
|
||||
T extends Array<any>,
|
||||
TCombinedResult = SuspenseQueriesResults<T>,
|
||||
>(
|
||||
options: {
|
||||
queries:
|
||||
| readonly [...SuspenseQueriesOptions<T>]
|
||||
| readonly [...{ [K in keyof T]: GetUseSuspenseQueryOptions<T[K]> }]
|
||||
combine?: (result: SuspenseQueriesResults<T>) => TCombinedResult
|
||||
},
|
||||
queryClient?: QueryClient,
|
||||
): TCombinedResult
|
||||
|
||||
export function useSuspenseQueries<
|
||||
T extends Array<any>,
|
||||
TCombinedResult = SuspenseQueriesResults<T>,
|
||||
>(
|
||||
options: {
|
||||
queries: readonly [...SuspenseQueriesOptions<T>]
|
||||
combine?: (result: SuspenseQueriesResults<T>) => TCombinedResult
|
||||
},
|
||||
queryClient?: QueryClient,
|
||||
): TCombinedResult
|
||||
|
||||
export function useSuspenseQueries(options: any, queryClient?: QueryClient) {
|
||||
return useQueries(
|
||||
{
|
||||
...options,
|
||||
queries: options.queries.map((query: any) => {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
if (query.queryFn === skipToken) {
|
||||
console.error('skipToken is not allowed for useSuspenseQueries')
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...query,
|
||||
suspense: true,
|
||||
throwOnError: defaultThrowOnError,
|
||||
enabled: true,
|
||||
placeholderData: undefined,
|
||||
}
|
||||
}),
|
||||
},
|
||||
queryClient,
|
||||
)
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
'use client'
|
||||
import { QueryObserver, skipToken } from '@tanstack/query-core'
|
||||
import { useBaseQuery } from './useBaseQuery'
|
||||
import { defaultThrowOnError } from './suspense'
|
||||
import type { UseSuspenseQueryOptions, UseSuspenseQueryResult } from './types'
|
||||
import type { DefaultError, QueryClient, QueryKey } from '@tanstack/query-core'
|
||||
|
||||
export function useSuspenseQuery<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
>(
|
||||
options: UseSuspenseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
|
||||
queryClient?: QueryClient,
|
||||
): UseSuspenseQueryResult<TData, TError> {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
if ((options.queryFn as any) === skipToken) {
|
||||
console.error('skipToken is not allowed for useSuspenseQuery')
|
||||
}
|
||||
}
|
||||
|
||||
return useBaseQuery(
|
||||
{
|
||||
...options,
|
||||
enabled: true,
|
||||
suspense: true,
|
||||
throwOnError: defaultThrowOnError,
|
||||
placeholderData: undefined,
|
||||
},
|
||||
QueryObserver,
|
||||
queryClient,
|
||||
) as UseSuspenseQueryResult<TData, TError>
|
||||
}
|
||||
Reference in New Issue
Block a user