UNPKG

@tanstack/query-core

Version:

The framework agnostic core that powers TanStack Query

316 lines (315 loc) • 10.1 kB
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); const require_notifyManager = require("./notifyManager.cjs"); const require_retryer = require("./retryer.cjs"); const require_removable = require("./removable.cjs"); //#region src/mutation.ts /** * Represents a single mutation attempt. A `Mutation` holds the mutation's * options, state (data/error/status), and the `MutationObserver`s currently * subscribed to it. * * Instances are created and managed internally by `MutationCache`; application * code typically interacts with mutations indirectly through `QueryClient` or * a framework hook like `useMutation`. Direct access to a `Mutation` instance * is possible via `mutationCache.find()`/`getAll()` for inspecting cache state. * @example * ```ts * const mutationCache = queryClient.getMutationCache() * * const mutation = mutationCache.find({ mutationKey: ['addPost'] }) * ``` */ var Mutation = class extends require_removable.Removable { #client; #observers; #mutationCache; #retryer; constructor(config) { 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(); } /** * Replaces the mutation's options and applies their `gcTime`. * @internal * @param options - The new mutation options. */ setOptions(options) { this.options = options; this.updateGcTime(this.options.gcTime); } /** * The `meta` object passed in the mutation's options, if any. * @returns The mutation's `meta`, or `undefined` if none was set. */ get meta() { return this.options.meta; } /** * Subscribes an observer to the mutation and stops its garbage collection. * @internal * @param observer - The observer to add. Adding an observer that is already subscribed does * nothing. */ addObserver(observer) { if (!this.#observers.includes(observer)) { this.#observers.push(observer); this.clearGcTimeout(); this.#mutationCache.notify({ type: "observerAdded", mutation: this, observer }); } } /** * Unsubscribes an observer from the mutation. * @internal * @param observer - The observer to remove. Garbage collection is scheduled afterwards. */ removeObserver(observer) { this.#observers = this.#observers.filter((x) => x !== observer); this.scheduleGc(); this.#mutationCache.notify({ type: "observerRemoved", mutation: this, observer }); } optionalRemove() { if (!this.#observers.length) { if (this.state.status === "pending") this.scheduleGc(); else this.#mutationCache.remove(this); } } /** * Resumes a mutation that is currently paused or was restored from a * dehydrated, still-`pending` state. * * - If this mutation has an active retryer (it paused mid-attempt, e.g. due * to the network mode or scope-based queuing), its retryer is resumed. * - Otherwise, if the mutation's status is still `pending` (e.g. it was * dehydrated while an attempt was in flight and never got a retryer in * this instance), `execute` is called again with the last known variables. * - Otherwise the mutation has already settled and this resolves immediately * without running anything again. * @returns A promise that settles with the resumed mutation: it rejects if the mutation fails. * It resolves immediately if the mutation has already settled. * @see {@link Mutation#execute} * @example * ```ts * // typically driven by reconnect handling, e.g. queryClient.resumePausedMutations() * const mutation = mutationCache.find({ mutationKey: ['addPost'] }) * await mutation?.continue() * ``` */ continue() { return this.#retryer?.continue() ?? (this.state.status === "pending" ? this.execute(this.state.variables) : Promise.resolve()); } /** * Runs the mutation function for the given variables through a retryer, and * drives the mutation's state and lifecycle callbacks through to settlement. * * If this mutation's state is already `pending` when `execute` is called * (i.e. it was restored, still in-flight, from a dehydrated state), the * `onMutate` step is skipped and a `continue` action is dispatched to * unpause it; otherwise a `pending` action is dispatched first, then the * mutation cache's `onMutate` and the mutation's own `onMutate` option are * awaited in that order, and the resulting context is stored. * * The mutation function is then run (subject to `retry`/`retryDelay`/ * `networkMode`, and to the mutation cache's scope-based serialization). * On success, the cache's `onSuccess`/`onSettled` callbacks run before the * mutation's own `onSuccess`/`onSettled` options, a `success` action is * dispatched, and the resolved data is returned. On failure, the same * cache-then-option ordering is used for `onError`/`onSettled`, but each of * those four callbacks is individually caught so that a throwing callback * cannot mask the original error; an `error` action is then dispatched and * the original error is re-thrown. * @param variables - The variables passed to the `mutationFn`. * @returns A promise that resolves with the mutation's data, or rejects with its error. * @see {@link Mutation#continue} * @example * ```ts * // Called internally by `MutationObserver.mutate` and `Mutation.continue` — * // applications normally trigger mutations through those, not this method. * const data = await mutation.execute(variables) * ``` */ async execute(variables) { const onContinue = () => { this.#dispatch({ type: "continue" }); }; const mutationFnContext = { client: this.#client, meta: this.options.meta, mutationKey: this.options.mutationKey }; const retryer = this.#retryer = require_retryer.createRetryer({ fn: () => { if (!this.options.mutationFn) return Promise.reject(/* @__PURE__ */ 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 = !retryer.canStart(); try { if (restored) onContinue(); else { this.#dispatch({ type: "pending", variables, isPaused }); if (this.#mutationCache.config.onMutate) await this.#mutationCache.config.onMutate(variables, this, mutationFnContext); const context = await this.options.onMutate?.(variables, mutationFnContext); if (context !== this.state.context) this.#dispatch({ type: "pending", context, variables, isPaused }); } const data = await retryer.start(); await this.#mutationCache.config.onSuccess?.(data, variables, this.state.context, this, mutationFnContext); await this.options.onSuccess?.(data, variables, this.state.context, mutationFnContext); await this.#mutationCache.config.onSettled?.(data, null, this.state.variables, this.state.context, this, mutationFnContext); await this.options.onSettled?.(data, null, variables, this.state.context, mutationFnContext); this.#dispatch({ type: "success", data }); return data; } catch (error) { try { await this.#mutationCache.config.onError?.(error, variables, this.state.context, this, mutationFnContext); } catch (e) { Promise.reject(e); } try { await this.options.onError?.(error, variables, this.state.context, mutationFnContext); } catch (e) { Promise.reject(e); } try { await this.#mutationCache.config.onSettled?.(void 0, error, this.state.variables, this.state.context, this, mutationFnContext); } catch (e) { Promise.reject(e); } try { await this.options.onSettled?.(void 0, error, variables, this.state.context, mutationFnContext); } catch (e) { Promise.reject(e); } this.#dispatch({ type: "error", error }); throw error; } finally { if (this.#retryer === retryer) this.#retryer = void 0; this.#mutationCache.runNext(this); } } #dispatch(action) { const reducer = (state) => { 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: void 0, 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: void 0, error: action.error, failureCount: state.failureCount + 1, failureReason: action.error, isPaused: false, status: "error" }; } }; this.state = reducer(this.state); require_notifyManager.notifyManager.batch(() => { this.#observers.forEach((observer) => { observer.onMutationUpdate(action); }); this.#mutationCache.notify({ mutation: this, type: "updated", action }); }); } }; /** * Returns the initial state of a mutation: `'idle'`, with no data, error, or variables. * @returns The initial mutation state. */ function getDefaultState() { return { context: void 0, data: void 0, error: null, failureCount: 0, failureReason: null, isPaused: false, status: "idle", variables: void 0, submittedAt: 0 }; } //#endregion exports.Mutation = Mutation; exports.getDefaultState = getDefaultState; //# sourceMappingURL=mutation.cjs.map