Периодические события

Работа с периодическими событиями в JavaScript почти всегда требует сочетания двух механизмов: вычисления времени с помощью библиотеки работы с датами и планирования выполнения через таймеры среды выполнения. Day.js предоставляет компактный и предсказуемый инструментарий для расчёта дат, а логика повторяемости строится поверх этих вычислений.

Основная идея периодических событий заключается не в «запуске по расписанию» как таковом, а в постоянном пересчёте следующего момента выполнения на основе текущего времени и заданных правил повторения.


Базовая модель повторяемости

Любое периодическое событие можно выразить через три компонента:

  • стартовая точка
  • интервал повторения
  • функция вычисления следующего запуска

Day.js используется для работы со стартовой точкой и интервалами.

import dayjs from "dayjs";

const start = dayjs();
const next = start.add(10, "minute");

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


Использование setInterval и вычисление времени через Day.js

Типичная реализация периодического события не полагается исключительно на setInterval, так как он не учитывает дрейф времени. Вместо этого Day.js используется для контроля фактических временных меток.

import dayjs from "dayjs";

let nextRun = dayjs().add(5, "second");

setInterval(() => {
  const now = dayjs();

  if (now.isAfter(nextRun) || now.isSame(nextRun)) {
    // выполнение задачи
    console.log("Событие выполнено:", now.format());

    // пересчёт следующего запуска
    nextRun = nextRun.add(5, "second");
  }
}, 200);

Такой подход обеспечивает устойчивость к задержкам и неточностям таймеров JavaScript.


Регулярные интервалы фиксированной длины

Повторение каждые N секунд

const interval = 30; // секунд
let next = dayjs().add(interval, "second");

setInterval(() => {
  const now = dayjs();

  if (now.isAfter(next)) {
    console.log("Триггер:", now.toISOString());
    next = next.add(interval, "second");
  }
}, 250);

Подобная модель подходит для:

  • опроса API
  • обновления UI
  • фоновых задач

Повторение каждые N минут

const stepMinutes = 15;
let nextRun = dayjs().add(stepMinutes, "minute");

function tick() {
  const now = dayjs();

  if (now.isAfter(nextRun)) {
    console.log("Запуск задачи");

    nextRun = nextRun.add(stepMinutes, "minute");
  }
}

setInterval(tick, 1000);

Суточные периодические события

Суточные задачи требуют фиксации времени суток, а не только интервала.

Пример: выполнение каждый день в 09:00

import dayjs from "dayjs";

function getNextMorningRun() {
  const now = dayjs();
  let next = now.hour(9).minute(0).second(0).millisecond(0);

  if (now.isAfter(next)) {
    next = next.add(1, "day");
  }

  return next;
}

let nextRun = getNextMorningRun();

setInterval(() => {
  const now = dayjs();

  if (now.isAfter(nextRun)) {
    console.log("Ежедневное событие");

    nextRun = getNextMorningRun().add(1, "day");
  }
}, 1000);

Ключевая особенность: время фиксируется внутри суток, а не просто прибавляется интервал.


Недельные периодические события

Для недельных событий важны день недели и время.

function nextWeeklyRun(targetDay = 1, hour = 10) {
  // 0 - воскресенье, 1 - понедельник ...
  const now = dayjs();

  let next = now.day(targetDay).hour(hour).minute(0).second(0).millisecond(0);

  if (now.isAfter(next)) {
    next = next.add(1, "week");
  }

  return next;
}

let nextRun = nextWeeklyRun(5, 18); // пятница 18:00

setInterval(() => {
  const now = dayjs();

  if (now.isAfter(nextRun)) {
    console.log("Недельное событие");

    nextRun = nextWeeklyRun(5, 18).add(1, "week");
  }
}, 1000);

Месячные события и проблемы календаря

Месячные интервалы сложнее из-за разной длины месяцев.

Пример: событие 10 числа каждого месяца

function nextMonthlyRun(dayOfMonth = 10, hour = 12) {
  const now = dayjs();

  let next = now.date(dayOfMonth).hour(hour).minute(0).second(0).millisecond(0);

  if (now.isAfter(next)) {
    next = next.add(1, "month");
  }

  return next;
}

let nextRun = nextMonthlyRun(10, 12);

Проблема переполнения даты

При установке даты, которой нет в следующем месяце, Day.js автоматически нормализует значение.

Например:

  • 31 января + 1 месяц → 3 марта (в зависимости от года)

Это требует дополнительной защиты:

function safeMonthlyRun(day, hour) {
  const now = dayjs();

  const daysInMonth = now.daysInMonth();
  const validDay = Math.min(day, daysInMonth);

  let next = now.date(validDay).hour(hour).minute(0).second(0).millisecond(0);

  if (now.isAfter(next)) {
    const nextMonth = now.add(1, "month");
    const daysNext = nextMonth.daysInMonth();

    next = nextMonth.date(Math.min(day, daysNext))
      .hour(hour)
      .minute(0)
      .second(0)
      .millisecond(0);
  }

  return next;
}

Периодические события с накоплением дрейфа

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

const step = 10; // секунд
let next = dayjs();

function loop() {
  const now = dayjs();

  while (now.isAfter(next)) {
    console.log("Срабатывание:", next.toISOString());

    next = next.add(step, "second");
  }
}

setInterval(loop, 500);

Использование while вместо if позволяет компенсировать пропущенные циклы.


Таймзоны и периодические задачи

При работе с глобальными системами требуется учитывать смещение времени.

Подключение UTC-плагина:

import utc from "dayjs/plugin/utc";
import timezone from "dayjs/plugin/timezone";
import dayjs from "dayjs";

dayjs.extend(utc);
dayjs.extend(timezone);

Пример фиксированного времени в таймзоне

const tz = "Europe/Moscow";

function nextRunTz(hour) {
  const now = dayjs().tz(tz);

  let next = now.hour(hour).minute(0).second(0).millisecond(0);

  if (now.isAfter(next)) {
    next = next.add(1, "day");
  }

  return next;
}

Сравнение фиксированных интервалов и календарных событий

Интервальные события

  • основаны на add(n, unit)
  • простые
  • не зависят от календаря
  • подходят для технических задач

Календарные события

  • фиксируют конкретное время суток
  • зависят от даты, месяца, недели
  • требуют проверки переполнений

Моделирование cron-подобного поведения

Day.js не содержит встроенного cron-движка, но его можно эмулировать.

function matchSchedule(now, config) {
  return (
    now.minute() === config.minute &&
    now.hour() === config.hour &&
    now.day() === config.weekday
  );
}

Использование:

setInterval(() => {
  const now = dayjs();

  if (matchSchedule(now, { minute: 0, hour: 12, weekday: 1 })) {
    console.log("Понедельник 12:00");
  }
}, 1000);

Долгоживущие процессы и восстановление расписания

При перезапуске приложения необходимо восстанавливать следующую точку выполнения.

const stored = localStorage.getItem("nextRun");
let nextRun = stored ? dayjs(stored) : dayjs().add(1, "minute");

function persist() {
  localStorage.setItem("nextRun", nextRun.toISOString());
}

Такой подход позволяет:

  • сохранять состояние между сессиями
  • восстанавливать расписание после рестарта
  • избегать повторных запусков

Комбинированные сценарии повторения

Реальные системы часто объединяют несколько типов расписаний:

  • ежедневные задачи + интервальный контроль
  • недельные события + компенсация дрейфа
  • таймзонные события + локальные настройки пользователя
function scheduler() {
  const now = dayjs();

  if (now.isAfter(nextRun)) {
    executeTask();

    nextRun = computeNext();
  }
}

Функция computeNext становится центральным элементом логики, определяя тип периодичности.