API Reference

Mutation API

Complete API reference for MutationController, MutationState, MutationOptions, MutationUpdate, and mutation observability on QoraClient.

MutationController

The core lifecycle controller for a single mutation.

Type parameterRole
TDataThe value returned by the mutator on success
TVariablesThe value passed to mutate()
TContextThe snapshot returned by onMutate (used for rollback)

Constructor

MutationController({
  required MutatorFunction<TData, TVariables> mutator,
  MutationOptions<TData, TVariables, TContext>? options,
  MutationTracker? tracker,
  Map<String, Object?>? metadata,
})
post_repository.dart
final controller = MutationController<Post, String, List<Post>?>(
  mutator: (title) => api.createPost(title),
  options: MutationOptions(
    onMutate: (title) async => client.getQueryData(['posts']),
    onError: (err, _, prev) async => client.restoreQueryData(['posts'], prev),
    onSuccess: (_, __, ___) async => client.invalidate(['posts']),
  ),
  tracker: client,
  metadata: {'category': 'content', 'resource': 'post'},
);

Properties

id

final String id

Unique identifier for this controller instance. Format: mutation_N (monotonically increasing). Used as the key in QoraClient.activeMutations and MutationUpdate.mutatorId.

mutator

final MutatorFunction<TData, TVariables> mutator

The async function that performs the write. Named mutator to mirror fetcher in fetchQuery.

options

final MutationOptions<TData, TVariables, TContext>? options

Lifecycle callbacks and retry configuration. See MutationOptions below.

tracker

final MutationTracker? tracker

Optional tracker (typically QoraClient) that receives state-change notifications for DevTools observability. When null, the controller operates in standalone mode.

metadata

final Map<String, Object?>? metadata

Arbitrary key-value pairs forwarded verbatim to every MutationUpdate emitted by this controller. Use for DevTools labelling without modifying the core schema.

state

MutationState<TData, TVariables> get state

The current synchronous state snapshot.

stream

Stream<MutationState<TData, TVariables>> get stream

A per-subscriber stream that immediately emits the current state on listen (synchronous capture), then forwards every subsequent transition.

Supports multiple concurrent subscribers. Each subscription gets its own stream backed by the shared broadcast controller: no events are lost between listen() and the first microtask.

Methods

mutate

Future<TData?> mutate(TVariables variables)

Execute the mutation. State sequence:

  1. Calls onMutate(variables) → result becomes TContext
  2. → MutationPending
  3. Calls mutator(variables) (with optional retry)
  4. → MutationSuccess + onSuccess + onSettled, or → MutationFailure + onError + onSettled

Returns TData on success, null on failure. Errors are never re-thrown to the caller: they are captured in MutationFailure state.

Throws StateError if the controller has been disposed.

reset

void reset()

Transition back to MutationIdle. Also removes this controller from tracker.activeMutations.

Throws StateError if the controller has been disposed.

dispose

void dispose()

Release all resources and close the internal stream. Removes the controller from the tracker silently (no event emitted). Must be called when the controller is no longer needed.

After dispose, any call to mutate or reset throws StateError.


MutationOptions

Per-mutation lifecycle callbacks and retry configuration.

Constructor

const MutationOptions({
  Future<TContext?> Function(TVariables variables)? onMutate,
  Future<void> Function(TData data, TVariables variables, TContext? context)? onSuccess,
  Future<void> Function(Object error, TVariables variables, TContext? context)? onError,
  Future<void> Function(TData? data, Object? error, TVariables variables, TContext? context)? onSettled,
  int retryCount = 0,
  Duration retryDelay = const Duration(seconds: 1),
  List<Object>? invalidates,
})

Properties

onMutate

Future<TContext?> Function(TVariables variables)? onMutate

Called before the mutator. Returns TContext: the optimistic snapshot passed to onError/onSettled for rollback. If this callback throws, the mutator is not called and the controller transitions directly to MutationFailure.

onSuccess

Future<void> Function(TData data, TVariables variables, TContext? context)? onSuccess

Called after a successful mutator, before onSettled.

onError

Future<void> Function(Object error, TVariables variables, TContext? context)? onError

Called after the mutator throws (after all retries), before onSettled. Use context (the snapshot from onMutate) to roll back via client.restoreQueryData.

onSettled

Future<void> Function(TData? data, Object? error, TVariables variables, TContext? context)? onSettled

Called after every outcome: success or failure. data is non-null on success; error is non-null on failure.

retryCount

final int retryCount

Number of retry attempts after the initial failure. Default: 0 (no retry). Each retry uses exponential backoff: retryDelay * 2^attempt.

retryDelay

final Duration retryDelay

Base delay for the first retry. Default: Duration(seconds: 1). Subsequent retries double: 1s, 2s, 4s, …

invalidates

final List<Object>? invalidates

List of query keys to automatically invalidate after a successful mutation. Each key is passed to QoraClient.invalidate after onSuccess completes, triggering a refetch of any active queries whose key matches.

Replaces the common manual pattern:

// Before: manual invalidation inside onSuccess
onSuccess: (data, vars, _) async { client.invalidate(['posts']); }

// After: automatic via options
invalidates: [['posts']],

Works with offline queue replay too: queries are invalidated on successful replay, not on the initial optimistic emit.

Requires a QoraClient or an invalidateQuery callback wired via MutationController. Both QoraMutationBuilder and useMutation provide this automatically when a QoraScope ancestor is present.

invalidatesTags

final List<QueryTag>? invalidatesTags

Default: null

Tags to invalidate after a successful mutation. Every query that provides a matching tag is automatically refetched. Works alongside invalidates (which targets query keys directly) and is processed after it.

Wildcard matching applies: invalidating QueryTag('post') refetches ALL queries that provide any tag of type 'post', regardless of id.

// Refetch ALL queries providing a 'post' tag (any id).
MutationOptions(
  invalidatesTags: [QueryTag('post')],
)

// Refetch only queries providing the specific 'post:123' tag.
MutationOptions(
  invalidatesTags: [QueryTag('post', '123')],
)

Requires a QoraClient or an invalidateTags callback wired via MutationController. Both QoraMutationBuilder and useMutation provide this automatically when a QoraScope ancestor is present.


MutationState

Sealed class hierarchy representing all possible states of a mutation.

sealed MutationState
  ├── MutationIdle
  ├── MutationPending   { variables }
  ├── MutationSuccess   { data, variables }
  └── MutationFailure   { error, stackTrace?, variables }

Common Getters

Available on every variant:

GetterTypeDescription
isIdlebooltrue in MutationIdle
isPendingbooltrue in MutationPending
isSuccessbooltrue in MutationSuccess
isErrorbooltrue in MutationFailure
dataOrNullTData?Non-null only in MutationSuccess
errorOrNullObject?Non-null only in MutationFailure
variablesOrNullTVariables?null only in MutationIdle

when

void when({
  void Function()? onIdle,
  void Function(TVariables variables)? onPending,
  void Function(TData data, TVariables variables)? onSuccess,
  void Function(Object error, StackTrace? stackTrace, TVariables variables)? onError,
})

Call a callback for the matching variant. Unhandled variants are silently ignored.

maybeWhen

R maybeWhen<R>({
  required R Function() orElse,
  R Function()? onIdle,
  R Function(TVariables)? onPending,
  R Function(TData, TVariables)? onSuccess,
  R Function(Object, StackTrace?, TVariables)? onError,
})

Returns the value from the matched callback, or orElse() if none matched.

fold (extension)

R fold<R>({
  required R Function() onIdle,
  required R Function(TVariables) onPending,
  required R Function(TData, TVariables) onSuccess,
  required R Function(Object, StackTrace?, TVariables) onError,
})

Exhaustive mapper: the compiler enforces that every variant is handled.

create_post_screen.dart
final label = state.fold(
  onIdle: () => 'Create',
  onPending: (_) => 'Creating…',
  onSuccess: (post, _) => 'Created: ${post.title}',
  onError: (err, _, __) => 'Error: $err',
);

status (extension)

MutationStatus get status

Returns the coarse-grained MutationStatus enum value: idle | pending | success | error.


MutationStatus

enum MutationStatus { idle, pending, success, error }

Coarse-grained status used by MutationUpdate to avoid generic type erasure issues at the event bus level.


MutationUpdate

Type-erased snapshot of a mutation state transition. Emitted by QoraClient.mutationEvents on every MutationController state change.

Properties

PropertyTypeDescription
mutatorIdStringUnique controller identifier (mutation_N)
statusMutationStatusCoarse-grained status at emission time
dataObject?Success data (type-erased). Non-null on success.
errorObject?Error. Non-null on failure.
variablesObject?Variables passed to mutate(). null when idle.
timestampDateTimeWhen this event was emitted
metadataMap<String, Object?>?Forwarded from MutationController.metadata

Getters

GetterTypeDescription
isIdleboolstatus == MutationStatus.idle
isPendingboolstatus == MutationStatus.pending
isSuccessboolstatus == MutationStatus.success
isErrorboolstatus == MutationStatus.error
isFinishedboolisSuccess || isError: used by QoraClient to auto-purge activeMutations

MutationTracker

Abstract interface implemented by QoraClient. Decouples MutationController from the client to avoid circular imports.

abstract interface class MutationTracker {
  void trackMutation<TData, TVariables>(
    String id,
    MutationState<TData, TVariables> state, {
    Map<String, Object?>? metadata,
  });

  void untrackMutation(String id);
}

Implement MutationTracker to build custom DevTools, logging middleware, or test spies:

mutation_logger.dart
class MutationLogger implements MutationTracker {
  @override
  void trackMutation<TData, TVariables>(
    String id,
    MutationState<TData, TVariables> state, {
    Map<String, Object?>? metadata,
  }) {
    print('[$id] → ${state.runtimeType} ${metadata ?? ''}');
  }

  @override
  void untrackMutation(String id) => print('[$id] disposed');
}

QoraClient: Mutation Observability

QoraClient implements MutationTracker and exposes two surfaces for observing tracked mutations.

mutationEvents

Stream<MutationUpdate> get mutationEvents

Real-time broadcast stream of every MutationUpdate from all tracked controllers. Emits on every state transition: including MutationIdle (reset) and MutationFailure.

app_error_handler.dart
client.mutationEvents.listen((event) {
  if (event.isError) {
    logger.error(
      '[${event.mutatorId}] Mutation failed',
      event.error,
      event.metadata,
    );
  }
});
mutationEvents is a broadcast stream: events fired before your subscription are lost. To catch mutations that started before you subscribed, read activeMutations first.

activeMutations

Map<String, MutationUpdate> get activeMutations

Unmodifiable snapshot of all currently running (pending) mutations, keyed by MutationController.id.

  • Entry added when a controller transitions to MutationPending
  • Entry auto-purged when the mutation finishes (MutationSuccess or MutationFailure)
  • Entry removed when the controller resets to MutationIdle or is disposed
devtools_panel.dart
// DevTools late-connect pattern:
// 1. Read snapshot to see what is currently in-flight
final pending = client.activeMutations;

// 2. Subscribe to stream for real-time updates (including completions)
client.mutationEvents.listen((event) {
  devtools.push(event);
});

activeMutations never accumulates "ghost" entries for completed mutations: it is always a reliable "currently running" indicator.

debugInfo

QoraClient.debugInfo() includes mutation state in its output:

main.dart
print(client.debugInfo());
// {
//   total_queries: 5,
//   active_queries: 2,
//   inactive_queries: 3,
//   pending_requests: 0,
//   max_size: 100,
//   active_mutations: 1,   // ← currently pending mutations
//   keys: [...]
// }

MutatorFunction

typedef MutatorFunction<TData, TVariables> =
    Future<TData> Function(TVariables variables);

Type alias for the mutation function. Mirrors QueryFunction<T> for consistency with the fetcher/mutator naming convention throughout the API.


Next Steps

Copyright © 2026