mutate method

Future<TData?> mutate(
  1. TVariables variables, {
  2. void onSuccess(
    1. TData data,
    2. TVariables variables
    )?,
  3. void onError(
    1. Object error,
    2. TVariables variables
    )?,
  4. void onSettled(
    1. TData? data,
    2. Object? error,
    3. TVariables variables
    )?,
})

Execute the mutation.

You can provide call-time callbacks onSuccess, onError, and onSettled which will run after the mutation-level callbacks defined in the constructor.

Implementation

Future<TData?> mutate(
  TVariables variables, {
  void Function(TData data, TVariables variables)? onSuccess,
  void Function(Object error, TVariables variables)? onError,
  void Function(TData? data, Object? error, TVariables variables)? onSettled,
}) async {
  if (isDisposed) {
    throw StateError('Mutation has been disposed');
  }
  // Clear any pending reset request from a prior call to reset() so this
  // fresh mutate() runs to completion rather than exiting immediately.
  _resetRequested = false;

  // Reset state
  status.value = ZenMutationStatus.loading;
  _isLoadingNotifier?.value = true;
  error.value = null;
  update();

  activeMutations.value++;
  if (mutationKey != null) {
    _activeByKey[mutationKey!] = (_activeByKey[mutationKey!] ?? 0) + 1;
  }
  Object? context;

  try {
    // 1. Lifecycle: onMutate
    // Run immediately (optimistic updates depend on this)
    if (onMutate != null) {
      context = await onMutate!(variables);
    }

    // 2. Check Offline / Queueing
    if (mutationKey != null && !ZenQueryCache.instance.isOnline) {
      return await _queueOfflineMutation(variables);
    }

    int retryAttempt = 0;
    late TData result;

    while (true) {
      try {
        result = await mutationFn(variables);
        break;
      } catch (e) {
        if (isDisposed) rethrow;

        // If we went offline mid-execution and this mutation is keyed,
        // bubble out so the outer catch can queue it.
        if (mutationKey != null && !ZenQueryCache.instance.isOnline) {
          rethrow;
        }

        if (retryAttempt >= retryCount) rethrow;

        retryAttempt++;
        final keyStr = mutationKey ?? 'unnamed';
        ZenLogger.logDebug(
            'Mutation $keyStr failed, retrying ($retryAttempt/$retryCount)');
        await _cancellableDelay(_calculateRetryDelay(retryAttempt, e));
        if (isDisposed) rethrow;
        // reset() was called during the backoff window — exit the retry loop
        // cleanly without rethrowing. The mutation remains usable.
        if (_resetRequested) return null;
      }
    }

    if (isDisposed) return null;

    // Update state: Success
    data.value = result;
    status.value = ZenMutationStatus.success;
    _isLoadingNotifier?.value = false;
    update();

    // Lifecycle callbacks...
    this.onSuccess?.call(result, variables, context);
    onSuccess?.call(result, variables);

    this.onSettled?.call(result, null, variables, context);
    onSettled?.call(result, null, variables);

    return result;
  } catch (e) {
    if (isDisposed) return null;

    // Check if we should queue due to network error during execution
    // (Simple check for now, can be improved to check Exception type)
    if (mutationKey != null && !ZenQueryCache.instance.isOnline) {
      return await _queueOfflineMutation(variables); // coverage:ignore-line
    }

    // Update state: Error
    error.value = e;
    status.value = ZenMutationStatus.error;
    _isLoadingNotifier?.value = false;
    update();

    // Lifecycle callbacks...
    this.onError?.call(e, variables, context);
    onError?.call(e, variables); // coverage:ignore-line

    this.onSettled?.call(null, e, variables, context);
    onSettled?.call(null, e, variables); // coverage:ignore-line

    return null;
  } finally {
    activeMutations.value--;
    if (mutationKey != null) {
      final current = (_activeByKey[mutationKey!] ?? 1) - 1;
      if (current <= 0) {
        _activeByKey.remove(mutationKey!);
      } else {
        _activeByKey[mutationKey!] = current;
      }
    }
  }
}