finish method

  1. @protected
  2. @mustCallSuper
void finish(
  1. Outcome<T> outcome
)

Ends the job with outcome.

An override calls super.finish, or the job never ends. Ending a running job is not a way to cancel it: this waits for no children and unwinds no cleanup stack; cancel with cancelWith. On a finished job the outcome stays, and a Failed handed in goes to JobObserver.onError and, unless Job.ignoreFailure was called, on to the answer. A job that has accepted a cancellation keeps it: a Done handed in is closed by nobody, and a Failed goes on to the answer unless Job.isCancellationError names it.

A Failed handed in is announced here, once, before finished and JobObserver.onFinish, so an onError that cancels finds the job finished. Do not call notifyObserver for it first: the observer would hear it twice. An error the body threw is not announced again.

A branch of JobContext.runAll handed a Done after its group heard its body fail goes by the failure for the group. The group hears of it right after JobObserver.onError does, or, for a body that is not async and threw while the group was starting the branches, once it has started them all; a value handed in before that is what the group goes by, and the branch answers for the failure itself.

A Cancelled handed in here or to cancelWith belongs to this job alone. The parent finds the child behind a cancellation by the instance: with one for two children, a const one included, it names the child that ended last, and with asserts on the second of them also sends an AssertionError to the zone it was created in.

See Finishing a job by hand on the extending page.

Implementation

@protected
@mustCallSuper
void finish(Outcome<T> outcome) {
  if (_status == JobStatus.finished) {
    // The outcome stays. A failure handed in now has no outcome to carry
    // it — a rule of a domain that ended the job and then threw looks
    // like that — and dropped here it would be heard by nobody.
    if (outcome is Failed && !_wasAnnounced(outcome.error)) {
      _noteAnnounced(outcome.error);
      _reportCovered(outcome, announced: false);
    }
    return;
  }
  // A marked job does not end with a value. `isCancelled`, `check` and
  // `whenCancelled` have been reporting that cancellation since the
  // mark went on, and a `Done` or a `Failed` handed in afterwards would
  // leave the handle contradicting itself for good -- cancelled and
  // `Done(42)` at the same time. So the mark decides those.
  //
  // A [Cancelled] handed in is not that: the handle stays coherent, and an
  // engine ending a job from inside the cascade says how it ended it -- that
  // description is the whole diagnosis it has. The route of the core hands
  // the mark in itself, so nothing here is about it.
  final decided = outcome is Cancelled
      ? outcome
      : (_pendingCancel ?? outcome);
  final replaced = identical(decided, outcome) ? null : outcome;
  if (replaced != null) {
    _debug(
      () =>
          '$this was finished as $replaced and ends $decided: '
          'the cancellation it accepted decides',
    );
  }
  if (_cleanups.isNotEmpty) {
    _debug(() => '$this finished with ${_cleanups.length} cleanups pending');
  }
  _outcome = decided;
  _status = JobStatus.finished;
  // A value a branch of a group is handed by hand, if it is the outcome: a
  // cancellation the branch accepted turns it away. The group goes by what it
  // heard first, and a failure of the body it heard of is what it throws or
  // answers for -- see [_RunAllGroup._takeFailure]. Asked here, among the
  // stores, before any code that is not the core's runs.
  if (decided is Done) {
    _failureWentToGroup = _hold?.takeFailure() ?? false;
  }
  // A section the body walked away from may still hold a cancellation;
  // it would land on a job that is over, which drops it. The trace said
  // it was held until the step ends, so it says it never landed.
  final dropped = _heldCancel;
  if (dropped != null) {
    _debug(
      () =>
          '$this ends with a held cancellation, which is dropped: '
          '$dropped',
    );
  }
  _heldCancel = null;
  _heldBehindTimeout = null;
  // However the job ended — the core's route has lifted the deadline already,
  // an engine ending it by hand has not.
  _timeoutTimer?.cancel();
  _timeoutTimer = null;
  // The list of children is a waiting list, so it shrinks; the link from
  // an outcome to the child that carried it lives on, in the parent's
  // `Expando`. Both happen here, where they are observable: in `finished`
  // and in `onFinish` the parent's list is already without this child.
  _waitedForBy?._stopWaitingFor(this);
  final parent = _parent;
  JobBase<Object?>? endedWithTheSame;
  if (parent != null) {
    if (decided is Cancelled) {
      assert(() {
        endedWithTheSame = parent._outcomeChild[decided];
        return true;
      }(), 'never fails: it reads the entry the next line replaces');
      parent._outcomeChild[decided] = this;
    }
  }
  // A failure an engine hands in has been told to nobody, unless the body
  // threw the same error first. Announced here, once the job is over, so an
  // `onError` that cancels finds a finished job instead of marking one that
  // is about to end `Failed`; and before `finished`, so the observer hears
  // the error before the engine's bookkeeping and `onFinish`. One handed in
  // over the mark goes the way of a failure of the body of a cancelled job,
  // below.
  if (decided is Failed &&
      replaced == null &&
      !_wasAnnounced(decided.error)) {
    _noteAnnounced(decided.error);
    _notifyObserver(
      decided.error,
      decided.stackTrace,
      otherwise: _laterOrNever,
    );
  }
  // Guarded: the hook belongs to an engine, and its error must not stand
  // between the job and its outcome — an unfinished job holds its parent, its
  // waiters and the engine itself forever.
  try {
    finished();
  } on Object catch (error, stackTrace) {
    notifyError(error, stackTrace);
  }
  _notifyFinish();
  // Nothing will call them: `cancelWith` turns around on a finished job.
  // They hold whatever the body gave them — a subscription, a buffer, a
  // token — for as long as anyone holds the handle.
  _onCancel.clear();
  // Nor will anything run the cleanups of a job an engine ended by hand
  // before its unwinding: the core's route turns around on a job that is
  // over. Inside the unwinding the routine that runs the stack is still
  // alive; it reads what is left and lets go of it itself, and the
  // registrations put aside exist only in there.
  if (!_disposing) _Cleanup.dropAll(_cleanups);
  _done.complete(decided);
  // Notify only after the domain has detached this job and delivered its
  // finish hooks: a synchronous subscriber may immediately add more work.
  if (decided is Cancelled) _notifyCancelled(decided);
  _cancelListeners.clear();
  _notifyFinished(decided);
  // Let go of the graph. The handle stays -- it is kept precisely to be read
  // later, by a controller holding the last job, by a widget, by a journal --
  // but nothing it reached through is of any use now: the parent chain leads
  // to jobs that are over, the group of [JobContext.runAll] has taken its
  // verdict and what it goes by, and the outcome of the body has become the
  // outcome, or is the body's route's own to answer for. Held on, these turn
  // one handle in a field into the whole tree it came out of, every closure
  // that tree captured and an error with everything it references.
  _parent = null;
  _hold = null;
  _bodyOutcome = null;
  if (decided is Failed && !_observed) {
    _reportUnobserved(decided);
  }
  // A failure handed in over the mark. The cancellation decides the outcome,
  // but an error is never lost silently: it goes where a failure of the body
  // of a cancelled job goes. It came in here and not through the body, so
  // nobody has announced it yet — unless the body threw this very error, and
  // then the body's route answers for it.
  if (replaced is Failed && !_wasAnnounced(replaced.error)) {
    _noteAnnounced(replaced.error);
    _reportCovered(replaced, announced: false);
  }
  // Said and not thrown, and last of all: the job has ended the way it
  // ends with asserts off. Thrown from here, it would leave through the
  // routine of a job whose engine handed the instance to `cancelWith`,
  // and take the rest of that routine with it.
  if (endedWithTheSame case final other?) {
    _notify(
      () => _toZone(
        AssertionError(
          '$decided ended two children of $parent, $other and $this: a '
          'Cancelled an engine ends a job with belongs to that job, build '
          'one for each',
        ),
        StackTrace.current,
      ),
    );
  }
}