Async и промисы
Плагин ждёт так же, как JavaScript: async-функции, await, Promise.
Пока одна функция ждёт — ответа на веб-запрос, пару секунд, — сервер и
остальной плагин продолжают работать.
server.addCommand("/weather", async ({ player }) => {
print(player, "спрашиваю...");
const response = await fetch("https://wttr.in/?format=3");
if (!response.ok) return;
print(player, await response.text());
});
async function countdown(player: Player) {
for (let i = 3; i > 0; i--) {
print(player, `${i}`);
await sleep(1000);
}
print(player, "Вперёд!");
}
async и await
async-функция сразу возвращаетPromiseи выполняется до первогоawait. То, что она возвращает, становится значением промиса:async function count() { return 3; }даётPromise<number>.- Вызов без
awaitне ждёт:countdown(player)идёт сам по себе, а строка после вызова выполняется сразу. await promiseотдаёт значение промиса, когда оно появится. Остаток функции выполнится позже — на том кадре, когда пришло значение, — а сервер тем временем занят всем остальным. Даже уже выполненный промис сначала даёт закончиться текущему коду, как в JavaScript.awaitработает вasync-функциях, стрелках и методах.- Async-стрелка видит переменные вокруг себя и до, и после
await, как любое замыкание.
sleep
await sleep(1000); // одна секунда
await sleep(5000, { signal: AbortSignal.timeout(2000) }); // отклонится через 2 с
Промис, который выполнится через столько-то миллисекунд, — setTimeout для
async-функции. Как любой таймер AMX Mod X, он ждёт не меньше 0,1 секунды и
срабатывает на кадре сервера: sleep(10) длится около 100 мс.
Async-обработчики и команды
async-функция подходит везде, где нужен обработчик: server.addCommand,
server.addEventListener, game.addEventListener, Forward.subscribe и
"change" у квара. Но то, что она сообщает игре, учитывается только до
первого await: ответ, который возвращает обработчик события игры,
event.preventDefault(). Как только функция начала ждать, игра пошла
дальше; сама функция просто продолжится позже.
game.addEventListener("fallDamage", async (event) => {
if (event.player.isBot) return 0; // учитывается: ботам без урона от падения
await sleep(100);
return 0; // поздно - урон уже нанесён
});
Promise
Глобальный Promise работает как в JavaScript: then, catch, finally,
Promise.resolve, Promise.reject, Promise.all, Promise.allSettled,
Promise.race и Promise.any.
async function compare(a: string, b: string) {
const [first, second] = await Promise.all([fetch(a), fetch(b)]);
console.log(`${first.status} ${second.status}`);
}
fetch("https://example.com/")
.then((response) => console.log(`${response.status}`))
.catch((error) => console.error(error.message));
Колбэк .then возвращает обычное значение. Чтобы выполнить один запрос
вслед за другим, дождитесь их по очереди через await.
Свой промис
new Promise((resolve, reject) => ...) превращает в промис то, что работает
через колбэк. Переданная функция выполняется сразу, а то, что она
подготовила, вызывает resolve или reject позже:
function delay(ms: number) {
return new Promise<void>((resolve) => {
setTimeout(() => resolve(), ms);
});
}
Если ответ придёт откуда-то ещё — из меню, где игрок выбирает пункт, из
строки в чате, — сохраните resolve до тех пор:
let answer: ((value: string) => void) | null = null;
function askName(player: Player) {
print(player, "Напиши в чат название клана");
return new Promise<string>((resolve) => {
answer = resolve;
});
}
function onChatLine(text: string) {
const resolve = answer;
if (resolve == null) return;
answer = null;
resolve(text);
}
Promise.all и allSettled
Промисы разных типов дают кортеж — каждое значение со своим типом, — как в TypeScript:
const url = "https://example.com/stats";
const [response, _] = await Promise.all([fetch(url), sleep(1000)]); // Response, void
const [count, name] = await Promise.all([countAsync(), nameAsync()]); // number, string
const [page, rank] = await Promise.allSettled([fetch(a), rankAsync()]);
if (page.status == "fulfilled") print(player, await page.value.text());
if (rank.status == "rejected") console.error(rank.reason.message);
- Промисы одного типа или список в переменной дают массив:
(await Promise.all(list)).length. - Кортеж читается деструктуризацией или по индексу, записанному числом:
results[0]. Индекс за его концом — ошибка, как в TypeScript.for...of,.mapи.length— для массивов. - Один вызов принимает до 8 промисов разных типов.
- Обычное значение берётся как промис, выполненный с ним:
Promise.all([load(), 5]), иPromise.all(numbers)изnumber[]. allSettledвыполняется, когда все промисы завершились, как бы ни завершились. У каждого результата естьstatus:"fulfilled"соvalueили"rejected"сreason— объектомError.
Promise.race и any
race завершается так же, как первый завершившийся промис. any берёт
первое значение и пропускает отказы; если отклонены все промисы, он
отклоняется с AggregateError, в errors которого — причина каждого.
const url = "https://example.com/stats";
const response = await Promise.race([fetch(url), sleep(3000)]); // Response | null
if (!response) return; // первым закончился sleep
print(player, await response.text());
const fastest = await Promise.any([fetch(mirrorA), fetch(mirrorB)]);
Тип у результата один, поэтому он нужен и промисам:
- один и тот же тип — результат этого типа;
Promise<void>(sleep, async-функция без возвращаемого значения) рядом с объектом или строкой — результат этого типа илиnull: проверяйте черезif (!response);- классы с общим базовым классом — результат этого класса
(
Promise.race([cat(), dog()])— этоAnimal).
Всё остальное — число и строка, число и sleep — не собирается:
Promise.race of 'number' and 'string': their values share no type. Тогда
дожидайтесь их по одному.
Ошибки
Ошибка в async-функции — throw или ошибка из того, что она вызывает,
включая выход за границы массива, — отклоняет её промис этой ошибкой, как в
JavaScript. Ошибку несёт отклонённый промис:
try/catchвокругawaitеё получает:consturl= "https://example.com/stats"; try { constresponse= awaitfetch(url);print(player, awaitresponse.text()); } catch (error) {console.log(`запрос не удался: ${error.message}`); }.catchеё получает.awaitотклонённого промиса внеtryзавершает ожидающую функцию на этом месте, и её собственный промис отклоняется с той же ошибкой — и так до того, кто её поймает.- Если ошибку никто не обработал, в консоли сервера появится одна строка:
Unhandled promise rejection: <сообщение>. Отмена — ошибка с именемAbortErrorилиTimeoutError— ожидаема, о ней не пишется.
Вне async-функции отклоняйте через reject(new Error("сообщение")) в
new Promise или Promise.reject(new Error("сообщение")). error.message
и error.name читаются обратно.
Падение, которое не ошибка, — переполнение стека — завершает async-функцию:
консоль называет плагин и падение, а плагин и сервер работают дальше. Тот,
кто её ждал, получает ошибку с именем AbortError и сообщением
the async function crashed.
Отмена: AbortController и AbortSignal
Как в браузере:
const url = "https://example.com/stats";
const controller = new AbortController();
fetch(url, { signal: controller.signal }).catch((error) => console.log(error.name)); // "AbortError"
controller.abort();
controller.signal.aborted; // true
controller.signal.reason; // Error, с которым отменили
controller.signal.addEventListener("abort", () => console.log("отменено"));
await fetch(url, { signal: AbortSignal.timeout(3000) }); // "TimeoutError" через 3 с
fetch(url, { signal })отменяет запрос и отклоняется с причиной сигнала — ошибкой с именемAbortError, если вabort()не передали другую.sleep(ms, { signal })перестаёт ждать и отклоняется так же.AbortSignal.timeout(ms)отменяется сам, с ошибкойTimeoutError;AbortSignal.abort()уже отменён;AbortSignal.any([a, b])отменяется вместе с тем, кто отменится первым.
Когда игрок уходит
player.signal отменяется, когда игрок уходит с сервера; следующий игрок в
этом слоте получает новый:
const url = "https://example.com/stats";
const stats = await fetch(url, { signal: player.signal });
Async-обработчику команды или события про игрока (putInServer, connect,
authorized и другие, у которых есть event.player) не нужно и этого: он
и так выполняется под сигналом этого игрока. Каждый await в нём — запрос,
sleep, любой промис — тихо завершается, когда игрок уходит: ничего в
консоли и ничего не нужно прибирать:
server.addCommand("/rank", async ({ player }) => {
const rank = await fetch(`https://example.com/rank/${player.steamId}`); // отменится, если он уйдёт
print(player, await rank.text()); // тогда не выполнится
});
Async-функции, вызванные из такого обработчика, работают под тем же
сигналом, а signal, переданный вручную, действует вместе с ним: кто
отменится первым. Обработчики событий игры и событий ухода игрока сигнала не
получают.
После await
Функция, которая ждала, продолжается на одном из следующих кадров, а за это
время многое могло измениться: раунд закончился, игрок умер — или ушёл. Под
сигналом игрока его уход сам завершает функцию; в остальных местах после
await проверяйте player.isConnected, прежде чем снова обращаться к
игроку.
Ограничения
- Async-генераторы (
async function*) не собираются;for awaitобходит список промисов или значений. - Пока async-функция ждёт, она хранит до 4 КБ своего состояния; функция, которой нужно больше, останавливается с сообщением в консоли, где назван плагин.
- Код, из которого можно дойти до
await, в плотном цикле работает несколько медленнее. Плагин безasync-функций за это не платит.
Чего ещё amxts пока не умеет — в Ограничениях.
Форварды
Форвард — событие, которое один плагин поднимает, а другие слышат, и на TypeScript, и на Pawn. Так плагин сообщает остальному серверу, что начался раунд его режима, что игрок что-то купил, что изменилась настройка.
Производительность
Плагин компилируется в машинный код до того, как сервер его загрузит, поэтому работа, которую он делает сам, — циклы, арифметика, массивы, текст — идёт быстро. Время уходит на переходы между плагином и игрой — вызов в AMX Mod X, событие по пути к обработчику — и на память, которую нагруженная функция просит снова и снова. На этой странице — куда уходит время и привычки, которые из этого следуют.