Совместимость с legacy API

Библиотека js-joda построена на принципах неизменяемости и строгой типизации временных сущностей, тогда как встроенный Date в JavaScript является мутируемым объектом с неявной зоной ответственности за форматирование, парсинг и арифметику. Это фундаментальное различие определяет всю модель совместимости с legacy-кодом.

Основная точка соприкосновения — преобразование между Date и Instant.

import { Instant } from '@js-joda/core';

// Date -> Instant
const instant = Instant.ofEpochMilli(date.getTime());

// Instant -> Date
const date = new Date(instant.toEpochMilli());

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


Проблемы мутируемости и безопасная адаптация

Date изменяем через методы вроде setHours, setDate, что делает его непредсказуемым в асинхронных сценариях и при передаче по ссылке. В отличие от этого, все типы в js-joda (LocalDate, LocalDateTime, Instant, ZonedDateTime) являются неизменяемыми.

При интеграции с legacy-кодом критично изолировать участки преобразования:

function legacyProcess(date) {
  const instant = Instant.ofEpochMilli(date.getTime());

  const processed = instant.plusSeconds(3600);

  return new Date(processed.toEpochMilli());
}

Такая схема предотвращает распространение мутабельных объектов в доменную логику, сохраняя js-joda как слой строгих временных вычислений.


Совместимость с JSON и сериализацией

Legacy-системы часто используют JSON.stringify(Date), что приводит к ISO-строке. js-joda использует явные методы форматирования и парсинга.

const iso = instant.toString(); // ISO-8601

const parsed = Instant.parse(iso);

При интеграции с API, ожидающими стандарт ISO-8601, js-joda демонстрирует более строгую и предсказуемую модель, чем Date, который может вести себя по-разному в зависимости от среды выполнения.

Проблема возникает при обратной совместимости:

const legacy = JSON.stringify({ date: new Date() });
// {"date":"2026-01-24T10:00:00.000Z"}

Такой формат требует явного контроля при десериализации:

const instant = Instant.parse(payload.date);

Интероперабельность с moment.js и dayjs

В старых кодовых базах часто встречается moment.js. Совместимость реализуется через промежуточные преобразования в ISO или epoch.

// moment -> js-joda
const instant = Instant.ofEpochMilli(momentObj.valueOf());

// js-joda -> moment
const m = moment(instant.toEpochMilli());

При переходе важно учитывать, что moment использует мутабельную модель и не разделяет строго Local и Zoned контексты так же явно, как js-joda. Это приводит к потенциальной потере семантики временной зоны при конвертации.


Работа с временными зонами в legacy-контексте

Legacy API часто оперируют строками с неявной зоной ("2024-01-01 10:00"), что приводит к неоднозначности. js-joda требует явного указания зоны через ZoneId.

import { ZonedDateTime, ZoneId } from '@js-joda/core';

const zdt = ZonedDateTime.of(
  LocalDateTime.parse('2024-01-01T10:00'),
  ZoneId.of('Europe/Moscow')
);

При взаимодействии с legacy-данными зона часто отсутствует, и тогда используется стратегия локализации:

const instant = Instant.ofEpochMilli(Date.parse(legacyString));
const zdt = instant.atZone(ZoneId.systemDefault());

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


Конвертация диапазонов и арифметика времени

Legacy-код часто использует числовые диапазоны времени:

  • timestamp ± offset
  • duration в миллисекундах
  • ручные вычисления дат

js-joda заменяет это строгими типами Duration и Period.

import { Duration } from '@js-joda/core';

const updated = instant.plus(Duration.ofHours(2));

Совместимость обеспечивается через перевод:

const duration = Duration.ofMillis(msDiff);
const ms = duration.toMillis();

При миграции важно исключить смешивание арифметики Date.getTime() и js-joda-операций внутри одного слоя бизнес-логики, так как это приводит к дублированию правил округления и ошибкам при DST-переходах.


Интеграция с REST API и DTO слоями

В большинстве legacy API граница обмена представлена DTO-объектами со строковыми датами.

// входящий DTO
{
  createdAt: "2026-05-25T12:30:00Z"
}

Преобразование в доменную модель:

const createdAt = Instant.parse(dto.createdAt);

Обратное преобразование:

const dto = {
  createdAt: instant.toString()
};

Такой подход формирует устойчивый контракт: внешняя система всегда работает со строками, внутренняя — с типизированными временными объектами.


Границы использования native Date

Несмотря на переход на js-joda, Date сохраняет роль транспортного типа:

  • взаимодействие с браузерными API (setTimeout, Date.now())
  • взаимодействие с JSON по умолчанию в старых системах
  • интеграция с библиотеками, не поддерживающими ISO-строки

В таких случаях используется минимальный слой адаптации:

const nowInstant = Instant.ofEpochMilli(Date.now());
const legacyDate = new Date(nowInstant.toEpochMilli());

Изоляция Date в инфраструктурном слое позволяет избежать проникновения его ограничений в бизнес-логику.


Потери информации при конвертации

При переходе между моделями времени возникают типовые потери:

  • LocalDateTimeDate теряет контекст зоны
  • ZonedDateTime → epoch фиксирует момент, но не сохраняет исходное представление
  • DateLocalDate требует явного выбора зоны
const local = instant.atZone(zone).toLocalDate();

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


Стратегии постепенной миграции

В больших legacy-системах применяется поэтапная замена:

  1. Ввод js-joda только в новых модулях
  2. Добавление адаптеров на границах API
  3. Централизация преобразований Date ↔︎ Instant
  4. Перенос бизнес-логики времени внутрь js-joda типов

Пример адаптера:

export const TimeAdapter = {
  toInstant(date) {
    return Instant.ofEpochMilli(date.getTime());
  },

  fromInstant(instant) {
    return new Date(instant.toEpochMilli());
  }
};

Такая структура минимизирует разрастание конвертационного кода по проекту и фиксирует единый контракт взаимодействия с legacy-слоем.