Защищаем состояние от устаревших асинхронных результатов!
Порядок запуска асинхронных операций не определяет порядок их завершения. Если несколько вызовов обновляют одно состояние, более ранний запрос может завершиться позже и перезаписать актуальные данные:
let state;
async function loadUser(id) {
const user = await fetchUser(id);
state = user;
}
loadUser(1);
loadUser(2);
Если fetchUser(1) завершится после fetchUser(2), итоговым значением state станет пользователь 1, хотя последним был запущен loadUser(2).
Когда отмена предыдущей операции невозможна или не требуется, актуальность результата можно контролировать с помощью монотонно увеличивающейся версии:
let state;
let version = 0;
async function loadUser(id) {
const currentVersion = ++version;
const user = await fetchUser(id);
if (currentVersion !== version) {
return;
}
state = user;
}
Каждый запуск получает собственную версию, а version хранит версию последнего запуска. Если после await значения различаются, результат уже устарел и не должен изменять состояние.
При нескольких асинхронных этапах проверку следует выполнять после каждой границы, после которой выполнение может продолжиться с неактуальными данными:
async function loadUser(id) {
const currentVersion = ++version;
const user = await fetchUser(id);
if (currentVersion !== version) {
return;
}
const permissions = await fetchPermissions(user.id);
if (currentVersion !== version) {
return;
}
state = { user, permissions };
}
Проверка версии не отменяет уже выполняющуюся работу. Она определяет только то, сохраняет ли результат право влиять на текущее состояние:
loadUser(1); // version = 1
loadUser(2); // version = 2
// version = 1 игнорируется
// version = 2 может обновить state
🔥 Такой version token реализует семантику latest invocation wins: завершение асинхронной операции отделяется от актуальности её результата и предотвращает запись устаревших данных.
📣 JS Ready | #практика
Порядок запуска асинхронных операций не определяет порядок их завершения. Если несколько вызовов обновляют одно состояние, более ранний запрос может завершиться позже и перезаписать актуальные данные:
let state;
async function loadUser(id) {
const user = await fetchUser(id);
state = user;
}
loadUser(1);
loadUser(2);
Если fetchUser(1) завершится после fetchUser(2), итоговым значением state станет пользователь 1, хотя последним был запущен loadUser(2).
Когда отмена предыдущей операции невозможна или не требуется, актуальность результата можно контролировать с помощью монотонно увеличивающейся версии:
let state;
let version = 0;
async function loadUser(id) {
const currentVersion = ++version;
const user = await fetchUser(id);
if (currentVersion !== version) {
return;
}
state = user;
}
Каждый запуск получает собственную версию, а version хранит версию последнего запуска. Если после await значения различаются, результат уже устарел и не должен изменять состояние.
При нескольких асинхронных этапах проверку следует выполнять после каждой границы, после которой выполнение может продолжиться с неактуальными данными:
async function loadUser(id) {
const currentVersion = ++version;
const user = await fetchUser(id);
if (currentVersion !== version) {
return;
}
const permissions = await fetchPermissions(user.id);
if (currentVersion !== version) {
return;
}
state = { user, permissions };
}
Проверка версии не отменяет уже выполняющуюся работу. Она определяет только то, сохраняет ли результат право влиять на текущее состояние:
loadUser(1); // version = 1
loadUser(2); // version = 2
// version = 1 игнорируется
// version = 2 может обновить state
🔥 Такой version token реализует семантику latest invocation wins: завершение асинхронной операции отделяется от актуальности её результата и предотвращает запись устаревших данных.
📣 JS Ready | #практика