mutate method
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;
}
}
}
}