245 lines
7.1 KiB
TypeScript
245 lines
7.1 KiB
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,
|
|
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
|
|
}
|