В один из прекрасных рабочих дней мне прилетела задачка на то, что необходимо исправить очередной баг. Проблема заключалась в следующем: на сайте при заказе товара можно было выбрать временной интервал для доставки, и если клиент задумался или отошел на n-ое количество времени, то он не мог сделать заказ, так как временной интервал на сервере обновился, и стал недоступен, а на клиенте остались старые интервалы.
На первый взгляд, данную задачу решить максимально просто, сделать запрос, получить новые интервалы, обновить состояние (работаем на react) и отрисовать. Вот и мне показалось, что решение займет пару часов.
Итак, я приступил, начал с того, что позвонил backend разработчику, что бы узнать по какому принципу обновляются интервалы на сервере. В итоге выяснилось, что они обновляются каждые 15 минут, и по его словам, в скрипте много бизнес-логики и отрабатывает он довольно долго.
Было принято решение, чтобы не нагружать сервер делать запрос раз в 15 минут, получать актуальные данные и все прекрасно. Спустя несколько минут код был написан и осталось лишь дождаться обновления интервалов на сервере и запроса.
После проверки стало понятно, что необходимо учесть промежуток времени, до истечения 15 минутного интервала. Например, первый интервал, который будет доступен клиенту это с 14:00 до 14:15, интервалы должны обновиться в 13:45 и первым доступным интервалом должно стать время с 14:15 до 14:30. Таким образом, если клиент будет делать заказ в 13:40, то следующий запрос будет в 13:55, соответственно у нас 10 минут будут висеть неактуальные интервалы.
Что с этим делать? Нужно высчитать время до первого интервала от текущего времени, вычесть 15 минут и сделать первый запрос по истечению этого времени, а дальше, как и до этого, каждые 15 минут. В целом неплохая идея, как мне тогда показалось, я приступил к реализации:
const timingUpdate = () => {
if (checkout.intervals) {
let first = null;
for (let key in checkout.intervals) {
if (!first) {
first = checkout.intervals[key][0][0].split(":");
}
}
let currentTime = new Date();
currentTime = (currentTime.getHours() * 3600 + currentTime.getMinutes() * 60 + currentTime.getSeconds());
return (+first[0] * 3600 + +first[1] * 60 - currentTime - 14.9 * 60) * 1000;
}
};
useEffect(() => {
const currentTiming = timingUpdate();
let onInterval = setTimeout(() => {
if (checkout.currentRestaurant) {
dispatch(fetchRestaurantIntervals({ restaurant_id: currentRestaurant.id }));
}
}, currentTiming);
return () => {
clearTimeout(onInterval);
};
}, [intervals]);
Хотелось бы отметить, что пришлось повозиться, так как с сервера приходил объект а не массив и первый интервал вычленять было довольно неприятно. Так же, можно заметить, что запрос я делаю не через 15 минут ровно, а через 15,1 чтобы дать время отработать "тяжелому" скрипту на сервере. Мои скромные мануальные тесты были пройдены и я отправил задачу на тестирование.
Спустя неделю, тестировщик дал сценарий, при котором интервалы не обновлялись и заказ было невозможно сделать, потом последовало несколько созвонов с тестировщиком и backend разработчиком, и как выяснилось, интервалы обновляются на сервере не всегда через 15 минут, а в зависимости от загрузки курьеров. Время обновления присылать с сервера оказалось невозможным.
Я начал терзаться муками, делать вебсокет для такой простой задачи не хотелось, но и слать запросы каждую минуту казалось не логичным. Вообщем, лучшее враг хорошего, поэтому решил, что от запроса раз в минуту сервер не упадет.
const timingUpdate = () => {
let timeUpdate = new Date();
return (65 - timeUpdate.getSeconds()) * 1000;
};
useEffect(() => {
let currentTiming = timingUpdate();
let onInterval = null;
let timer = () => {
currentTiming = timingUpdate();
if (checkout.currentRestaurant) {
dispatch(fetchRestaurantIntervals({ restaurant_id: currentRestaurant.id }, intervals));
}
};
let onTimeout = setTimeout(() => {
timer();
clearInterval(onInterval);
onInterval = setInterval(timer, currentTiming);
}, currentTiming);
return () => {
clearInterval(onInterval);
clearTimeout(onTimeout);
};
}, [intervals]);
Пришлось немного повозиться с setTimeout и setInterval, так как теперь, если у нас не приходил ответ, то состояние не менялось, перерендера не было и соответственно setTimeout не запускался. Поэтому я использовал setTimeout для запуска первого запроса, который срабатывал через несколько секунд, когда наступала новая минута, и далее через setInterval каждую минуту. Так же можно заметить, что я сделал дополнительные 5 секунд на отработку "тяжелого" скрипта на и обновления интервалов на сервере(Как показали эксперименты, 5 секунд было всегда достаточно). Ну и конечно же нужно не забыть очистить setTimeout и setInterval, иначе при изменении состояний различных кнопочек на странице мы получим огромное количество перерендеров.
Резюмируем: Не всегда простая на вид задача, на самом деле простая! Ну, и конечно же, лучше 10 раз переспросить как работает backend, прежде чем пытаться быстрее реализовать frontend, чтобы не делать двойную работу.
P.S.: Первая статья, не судите строго!