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