# Cancellations. Отмена асинхронных операций

Использование Promise позволяет организовать обработку результатов работы
асинхронных фукнций. Ключевые слова async/await позволяют работать с
асинхронными вызовами в стиле процедурного программирования, хотя по сути это
событиный подход. При всей своей красоте даннго подхода в нем умышленно
отсутсвует механизм отмены асинхронной операции, т.е. ее можно начать, но нельзя
отказаться от результатов ее выполнения, даже если это уже не требуется.

Примером того, когда может потребоваться отмена является загрузка большого
файла, при которой пользователю отображается окно хода операции с возможностью
ее отмены.

```ts
// имеется некоторый HTTP клиент
let client = new HttpClient();

// загружается большой файл, с использованием медленного канала
let data = await client.getJsonAsync('http://host/large-file.json');

```

Как поступить в данной ситуации, прежде всего нужно, чтобы сама операция
поддерживала возможность отмены, предположим, что для этого есть метод
`client.abort()`.

```ts

// имеется некоторый HTTP клиент
let client = new HttpClient();

// отображаем окно с информацией о ходе операции
let progressView = showProgress("Downloading, please wait...");

// код оборачивается в try/finally поскольку созданную форму нужно закрыть
try {
    // загружается большой файл, с использованием медленного канала
    // здесь, в отличии от предыдущего примера, мы не дожидаемся результата,
    // а запоминаем обещание в переменную downloadTask
    let downloadTask = client.getJsonAsync('http://host/large-file.json');

    // связываем событие нажатия кнопки с отменой загрузки
    progressView.once('cancel', () => client.abort());

    // ожидаем окончания загрузки данных

    let data = await downloadTask;
} finally {
    // независимот от результата закрываем форму
    // при этом также происходит ануллирование подписок на события
    progressView.close();
}

```

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

```ts
// обновление информации о человеке на форме
async function updatePersonInfo(info) {
    let client = new RestApiClient();

    // выплнение нескольких асинхронных операций
    let org = await client.getOrgAsync(info.orgId);
    let city = await client.getCityAsync(info.cityId);

    // обновление содержимого представления
    renderContent({
        person: info,
        org: org,
        city: city
    });
}

```

Чтобы реализовать возможность отмены такой операции требуется, чтобы в логике
самой операции была реализована поддержка отмены. Для реализации этого
потребуется чтобы у операции была информация о запросе отмены, причем данная
информация относится именно к текущей операции.

Информацию о состоянии запроса на отмену назовём **маркер отмены (cancellation
token)**. Поскольку маркер отмены тесно связан с операцией, его удобно
передавать в виде параметра, тогда код операции будет выглядеть так:

```ts
// обновление информации о человеке на форме
// ct - маркер отмены
async function updatePersonInfo(info, ct) {
    let client = new RestApiClient();

    // выплнение нескольких асинхронных операций
    // маркер отмены просто передается далее по цепочке вызовов, без
    // дополнительных действий
    let org = await client.getOrg(info.orgId, ct);
    let city = await client.getCity(info.cityId, ct);

    // обновление содержимого представления
    renderContent({
        person: info,
        org: org,
        city: city
    });
}

///////////////////////////////////////////////////////////////////////////////
// ... где-то в другом месте
///////////////////////////////////////////////////////////////////////////////

// отображаем окно с информацией о хоже операции
let progressView = showProgress("Loading, please wait...");

// создаем маркер отмены для операции на основе события 'cancel'.
let ct = new Cancellation(cancel => progressView.on('cancel', cancel));

// код оборачивается в try/finally поскольку созданную форму нужно закрыть
try {
    // асинхронно получаем информацию о человеке
    let data = await getPersonInfo(personId, ct);

    // асинхронно обновляем представление
    await updatePersonInfo(data, ct);
} finally {
    // независимот от результата закрываем форму
    // при этом также происходит ануллирование подписок на события
    progressView.close();
}

```

Таким образом тот, кто начинает асинхронную операцию заранее определяет как эта
опреция будет отменена.

Важно понимать, что для реализации отмены операции
могут выделаться ресурсы требующие явного освобождения (DOM, таймеры, события),
об их освобождении по окончанию операции (успешном или нет) должен позаботиться
инициатор этой операции. `Cancellation` выступает только в роли посредника для
доставки события отмены операции до конечного получателя, он не отслеживает и
не освобождает ресурсы, кроме того, асинхронная операция может его попросту
проигнорировать.

## `ICancellation` Маркер отмены операции

Интерфейс маркера отмены операции. Используется асинхронными операциями, чтобы
получить оповещение о требуемой отмене.

### `isSupported(): boolean`

Определяет, может ли быть запрошена отмена операции через данный маркер.

### `isRequested(): boolean`

Возвращает текущее состояние запроса на отмену.

### `throwIfRequested(): void`

Если отмена была запрошена, бросает в качестве исключения причину отмены.

### `register(cb: (e:any) => void): IDestroyable`

Метод, зарегистрировать обработчик на запрос отмены. Если отмена была запрошена
зарегистрированный обработчик будет вызван ровно один раз, независимо от того,
был ли он зарегистрирован до или после запроса отмены.

Если отмена уже была запрошена, обработчик будет вызван сразу при регистрации,
при этом исключения, которые могу возникнуть в обработчике не будут обработаны,
а передадуться наверх.

Вызов данного метода приводит к выделению ресурсов, поэтому операция,
зарегистрировавшая обработчик должна освободить подписку, которую вернет метод.

```ts
async function getAsync(url: string, ct: ICancellation = Cancellation.none) {
    // переменная в которой будет запомнена подписка на запрос отмены
    let reg;
    try {
        // оборачиваем операцию загрузки в Promise
        return await new Promise<string>((resolve, reject) => {
            // объект Xhr
            const xhr = new XMLHttpRequest();
            xhr.open("GET", url);

            // регистрируем обработчики Promise
            xhr.onload = () => resolve(xhr.responseText);
            xhr.onerror = () => reject(xhr.statusText);

            // отправляем запрос
            xhr.send();

            // подписываемся на запрос отмены
            reg = ct.register((e) => {
                reject(e);
                xhr.abort();
            });
        });
    } finally {
        if (reg)
            reg.destroy();
    }
}

```

Использование метода `register()` предполагается для организации отмены операций
не поддерживающих маркеры отмены.

## `Cancellation` Источник отмены

Класс используется для создания маркеров отмены. Позволяет создать маркер при
начале асинхронной операции и связать его, например, с событием DOM.

Также маркер можно создавать, когда требуется сложное условие отмены текущей и
всех нижележещих операций.

Как правило в большинстве операций достаточно маркера переданного в параметрах,
этот же маркер может передаваться ниже.

### `constructor(exec: (cancel: (reason:any) => void ) => void )`

Создает новый маркер, при помощи параметра и инициализирует его при помощи
фукнции, переданной в параметре `exec`.

```ts

let htimer;
let ct = new Cancellation(cancel => {
    htimer = setTimeout(() => cancel("The request is timed out."), 1000);
});

try {
    let text = await getAsync(url, ct);
} finally {
    // инициатор должен освобождать ресурсы
    // передача недействительного htimer не приводит ни к каким последствиям
    clearTimeout(htimer);
}

```

## `Cancellation.none: ICancellation`

Статическое свойство только для чтения, в котором находится специальный токен
запроса отмены. Этот токен означает, что отмена никогда не может произойти.

Данный токен рекомендуется использовать как значение по-умолчанию для
параметров, принимающих токен отмены.

```ts

async function load(url: string, ct: ICancellation = Cancellation.none) {
    ct.throwIfRequested();

    // ... the rest of method
}

```