# Работа кусками — `$.task`

Долгая синхронная работа вешает кадр: генерация карты, тёплый проход по сотне
ассетов, сборка prefab'ов. `$.task` режет такую работу **по времени**: колбэк
вызывается столько раз, сколько помещается в бюджет кадра, между вызовами кадр
рисуется, ввод работает.

```js
$.ready(() => {
    // 4000 тайлов, не больше 4 мс на кадр, с экраном загрузки.
    $.task.chunked({
        total: 4000,
        budget: 4,
        label: 'Генерация леса',
        step: (i) => placeTree(i),
        done: () => $.log('лес готов'),
    });

    // Цикл-генератор: сам решает, когда закончил.
    $.task.each(function* () {
        for (const node of prefabs) { add(node); yield; }
    });
});
```

---

## 1. Способы запустить

| Вызов | Что делает |
|---|---|
| `$.task.chunked({ total, step, budget?, label?, done? })` | вызвать `step(i, n)` `total` раз, тратя не больше `budget` мс за кадр |
| `$.task.chunked(step, total)` | то же коротко |
| `$.task.each(generator, opts?)` | генератор: каждый `yield` — конец порции, `return` — конец работы |
| `$.task.list(items, each, opts?)` | пройти список по кадрам |

`label` показывает экран загрузки и обновляет его прогресс (см.
[loading.md](highlevel/loading)); `done(cancelled)` вызывается в конце — с `true`, если
задачу отменили или работа бросила исключение.

## 2. Задача

Возвращается объект:

| Поле | Смысл |
|---|---|
| `progress` | 0..1 — сколько сделано |
| `finished` | закончила ли |
| `abort()` | отменить: работа не докрутится |

`$.task.running()` — сколько задач идёт сейчас, `$.task.abortAll()` — отменить
все.

## 3. Бюджет

`budget` — **миллисекунды на кадр** (по умолчанию 4). Время проверяется не на
каждой итерации, а раз в несколько: вызов часов сам стоит времени, и на мелких
шагах он съел бы весь бюджет. Как следствие, за кадр может уйти чуть больше
бюджета — это нормально, важен порядок.

Бюджет 0 или отрицательный поднимается до минимума (0.05 мс), чтобы цикл не
зависал.

Исключение внутри шага **отменяет задачу**, а не роняет кадр: игра продолжает
работать, в `done` придёт `cancelled = true`. Планировщик хранит ошибку в
`task.error` (у объекта-планировщика из `createScheduler`).

## 4. Переход сцены с загрузкой

`$.scene.loadAsync(name, opts)` показывает экран загрузки, выполняет шаги по
кадрам и только потом уходит в сцену:

```js
$.scene.loadAsync('level2', {
    label: 'Уровень 2',
    steps: [
        { label: 'лес',  work: (i) => plant(i), total: 900 },
        { label: 'враги', work: () => spawnHorde() },
    ],
});
```

У шага либо `work` c `total` (кусками), либо просто функция (одна порция).

## 5. Ограничения

* **оценка времени, а не гарантия**: кадр может уйти за бюджет на один шаг —
  длинный шаг (например, загрузка текстуры из файла) не режется;
* **нет приоритетов и пауз**: задачи идут в порядке постановки; приостановить и
  продолжить задачу нельзя, только отменить;
* **нет фоновых потоков**: всё выполняется в игровом потоке, поэтому
  CPU-тяжёлая работа всё равно замедляет кадр, просто не замораживает его;
* **генератор закрывается при отмене** (`return()`), но `abort()` у планировщика
  не откатывает уже сделанную работу.

## 6. Проверка

```bash
# планировщик: бюджет, продолжение с места, отмена, исключение, генератор
build/_deps/quickjs-build/qjs tests/js/task_test.mjs
```
