Перейти к содержимому

Задачи и очередь

Job<T> представляет одну операцию и её будущий исход. Он не является Future: вызов profile.load() без await допустим, а что делать с результатом, решает место вызова.

switch (await profile.load().done) {
case Done(:final value):
print('loaded $value');
case Failed(:final error):
print('not loaded: $error');
case Cancelled(:final reason):
print('gave up: $reason');
}

У Outcome<T> эти три варианта: Done содержит значение, Failed — ошибку и стек вызовов, Cancelled — причину отмены.

Член API Назначение
job.value Дождаться T; бросает исключение при ошибке или отмене.
job.done Дождаться Outcome<T> без исключения.
job.outcome Прочитать исход синхронно после завершения.
job.cancel() Запросить отмену и дождаться завершения.
job.ignore() Пометить исход обработанным без ожидания.

Ошибка Job не останавливает очередь. Ошибка передаётся обработчикам, после чего может выполняться следующая Job. Отмена представляет отдельный исход: закрытие контроллера, удаление Job из очереди или нарушение правил состояния могут отменить работу без ошибки приложения.

Каждая ошибка передаётся хуку ошибок контроллера и наблюдателю. Если к исходу провалившейся Job никто не обратился через done, value или ignore(), ошибка также передаётся как необработанная в зону Dart, где Job была создана. Используйте ignore(), когда достаточно обработки ошибок в другом месте; отсутствие await само по себе не помечает ошибку обработанной. Подробнее в разделе Сообщения об ошибках и наблюдение.

// Сначала собрать, поставить потом: два шага, если Job нужно придержать.
final saving = job<Ready, void>(key: _Op.save, (ctx) => ctx.join(store.save));
add(saving, policy: Policy.droppable);
// Либо всё сразу — так обычно и делает метод контроллера.
SoloJob<void> setZoom(double zoom) => run<Ready, void>(
key: _Op.zoom,
// Лениво и только для диагностики: строится, когда спросит лог.
describe: () => 'zoom: $zoom',
(ctx) => ctx.join(() => camera.zoom(zoom)),
);

Все три возвращают SoloJob<T>, который реализует Job<T> и добавляет isQueued. Обычно контроллер предоставляет предметные методы вроде load() или setZoom(), чтобы вызывающему коду не приходилось собирать Job самостоятельно.

key — это то, по чему политики очереди ищут родственную работу, а describe задаёт подпись для логов, наблюдателей и toString(): Job(key: label). Без описания используется представление Job(key).

Job, поставленная в очередь контроллера, является корневой. В каждый момент работает одна. Какая из них переживёт повторный вызов, решает политика:

enum _Op { load, save, zoom, seek, stop }
// sequential, по умолчанию: одна за другой, в порядке обращения.
SoloJob<void> save() =>
run<Ready, void>(key: _Op.save, (ctx) => ctx.join(store.save));
// droppable: повторная загрузка того же профиля вернёт первую Job.
SoloJob<Profile> load(String id) => run<Loaded, Profile>(
key: (_Op.load, id),
policy: Policy.droppable,
(ctx) async => ctx.wait(() => api.load(id)),
);
// replace: ожидающий зум уходит, работающий остаётся нетронутым.
SoloJob<void> setZoom(double zoom) => run<Ready, void>(
key: _Op.zoom,
policy: Policy.replace,
(ctx) => ctx.join(() => camera.zoom(zoom)),
);
// restart: то же самое, и работающую просят остановиться.
SoloJob<void> seek(Duration position) => run<Ready, void>(
key: _Op.seek,
policy: Policy.restart,
(ctx) => ctx.join(() => camera.seek(position)),
);
Политика Когда Job с таким ключом уже существует
Policy.sequential Добавить новую Job в очередь.
Policy.droppable Вернуть существующую Job из очереди или работающую; новую отбросить.
Policy.replace Удалить отменяемые Job с этим ключом из очереди, затем добавить новую.
Policy.restart Сделать то же, что replace, и запросить отмену работающей Job с этим ключом.

Корневая Job удерживает очередь до завершения тела, детей, освобождения ресурсов и итогового обработчика состояния. Дочерние Job могут работать в это время; они разобраны в Дочерние задачи и стримы.

restart запрашивает отмену при добавлении новой Job. Новая Job всё равно ждёт завершения текущей; их тела не пересекаются. Поэтому Job, отклоняющая отмену, может задержать свою замену.

Ключом может быть любой объект со сравнением через ==, поэтому запись (_Op.load, id) выше даёт политике идентичность одного запроса, а не операции вообще: droppable отбрасывает вторую загрузку того же профиля и пропускает загрузку другого, тогда как голый _Op.load отбросил бы обе.

Используйте разные ключи для разных операций и типов результата. Повторное использование ключа для Job<String> и Job<void> заставит droppable бросить ArgumentError до того, как новая Job будет затронута, поэтому её можно добавить снова под собственным ключом. Enum с ключом на метод — удобный способ этого избежать. Политика, отличная от последовательной, с ключом null тоже бросает ArgumentError, и обе проверки работают в release-сборках.

Сама очередь принадлежит контроллеру, и метод может работать с ней напрямую:

SoloJob<void> stop() {
// Что ждёт прямо сейчас.
print(queue.jobs.length);
// Убрать то, что эта команда обесценила...
queue.removeWhere((job) => job.key == _Op.zoom);
// ...и пропустить саму остановку без очереди.
return add(
job<Ready, void>(key: _Op.stop, (ctx) => ctx.join(camera.stop)),
first: true,
);
}

queue предоставляет jobs, remove, removeWhere, clear и lastWhere. Методы удаления касаются только Job в очереди — работающая им не принадлежит — и сохраняют Job с cancellable: false, если не вызваны с force: true. Группы накопителя, ждущие своего окна, могут пропускать готовые Job; см. Накопление событий.