Event Sourcing меняет привычный подход к хранению данных: вместо перезаписи текущего состояния система сохраняет последовательность событий, из которых это состояние можно восстановить.В этом посте:
• Разберём, почему в Event Sourcing состояние становится производным;
• Отделим команды от событий и поймём их разные роли;
• Реализуем восстановление состояния через последовательный replay событий;
• Разберём, почему события становятся источником истины, а не просто журналом изменений.
Такой подход сохраняет не только результат изменений, но и их причинную историю, позволяя строить новые представления данных и воспроизводить состояние системы на основе событий.
Please open Telegram to view this post
VIEW IN TELEGRAM
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥11👍6❤3
Получаем только изменённые поля объекта!
При изменении состояния приложения можно сравнить предыдущую и текущую версию объекта, чтобы определить только обновлённые значения. Такой
Для получения изменённых полей можно преобразовать объект в массив пар через
В результате получится объект, содержащий только изменённые поля:
Такой подход применяется при обновлении состояния во frontend-приложениях, работе с формами и подготовке частичных изменений для API через
Вместо отправки всей структуры можно передать только те значения, которые действительно были изменены.
Проверка через количество ключей позволяет определить, были ли изменения вообще. Если объект
Важно учитывать, что данный способ выполняет только поверхностное сравнение (
Вложенные объекты сравниваются по ссылке, поэтому одинаковая структура внутри разных объектов не делает их равными. Для таких случаев требуется использовать глубокое сравнение (
🔥 Также при реализации реальных обновлений через
📣 JS Ready | #практика
При изменении состояния приложения можно сравнить предыдущую и текущую версию объекта, чтобы определить только обновлённые значения. Такой
diff позволяет отделить изменившиеся поля от тех, которые остались без изменений, и использовать только актуальные данные для дальнейшей обработки.const oldUser = {
name: "Alex",
age: 30,
role: "developer"
};
const newUser = {
name: "Alex",
age: 31,
role: "developer"
};Для получения изменённых полей можно преобразовать объект в массив пар через
Object.entries(), отфильтровать только отличающиеся значения и собрать новый объект обратно с помощью Object.fromEntries().const changes = Object.fromEntries(
Object.entries(newUser)
.filter(([key, value]) =>
!Object.is(value, oldUser[key])
)
);
console.log(changes);
В результате получится объект, содержащий только изменённые поля:
{
age: 31
}Такой подход применяется при обновлении состояния во frontend-приложениях, работе с формами и подготовке частичных изменений для API через
PATCH запросы. Вместо отправки всей структуры можно передать только те значения, которые действительно были изменены.
const hasChanges =
Object.keys(changes).length > 0;
console.log(hasChanges);
Проверка через количество ключей позволяет определить, были ли изменения вообще. Если объект
changes пустой, значит исходное и новое состояние полностью совпадают.Важно учитывать, что данный способ выполняет только поверхностное сравнение (
shallow diff) и проверяет значения первого уровня объекта.const first = {
profile: {
name: "Alex"
}
};
const second = {
profile: {
name: "Alex"
}
};
console.log(first.profile === second.profile);
// falseВложенные объекты сравниваются по ссылке, поэтому одинаковая структура внутри разных объектов не делает их равными. Для таких случаев требуется использовать глубокое сравнение (
deep comparison) или отдельную логику построения diff.PATCH нужно отдельно учитывать удаление полей, так как простой shallow diff определяет только добавленные и изменённые значения.Please open Telegram to view this post
VIEW IN TELEGRAM
🔥11❤7👍4🤝1
Например,
await приостанавливает выполнение текущей async-функции до завершения Promise, а try...catch позволяет удобно обрабатывать возникающие ошибки.На картинке — основной синтаксис
async/await, принцип его работы, сравнение с цепочками .then() и другие полезные моменты.Сохрани, чтобы не потерять!
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥9❤8👍7
Склеивайте множество синхронных обновлений в одно!
Иногда функция может вызываться десятки раз за один проход синхронного кода.
Можно накопить изменения и запланировать обработку один раз:
Теперь даже серия вызовов обработается одной пачкой:
🔥 Такой подход объединяет множество быстрых обновлений в одну обработку, уменьшая лишние вычисления и нагрузку. Что крайне полезно для событий и часто обновляемых данных.
📣 JS Ready | #совет
Иногда функция может вызываться десятки раз за один проход синхронного кода.
Можно накопить изменения и запланировать обработку один раз:
const pending = new Set();
function update(id) {
pending.add(id);
scheduleFlush();
}
queueMicrotask() выполнит flush после текущего синхронного кода, но до перехода к следующей macrotask:function flush() {
scheduled = false;
process([...pending]);
pending.clear();
}Теперь даже серия вызовов обработается одной пачкой:
update(1);
update(2);
update(3);
// process([1, 2, 3]) — один раз
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥10❤6👍5
Например,
const [a, b] = arr позволяет получить значения из массива, а const { name, age } = user — извлечь нужные свойства из объекта.На картинке — основные способы деструктуризации массивов и объектов, которые пригодятся в повседневной разработке.
Сохрани, чтобы не потерять!
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥12👍8❤3🤝2
Почему Array.sort() может создать баг далеко от места сортировки!
С
Особенно неприятно это становится на границе функций. По названию
В результате функция имеет побочный эффект, который не очевиден из места её вызова. Это уже может затронуть состояние, кэш, мемоизацию или другой код, использующий тот же массив:
Классический способ избежать этого — сначала создать новый массив и сортировать уже его. Такой код работает, но копирование приходится выражать отдельной операцией:
Важно, что копируется именно массив. Объекты внутри него не клонируются, поэтому обе коллекции по-прежнему содержат ссылки на одни и те же объекты:
Поэтому изменение самого объекта будет видно через обе коллекции:
Похожая логика есть у
🔥 Поэтому выбор между
📣 JS Ready | #практика
С
sort() проблема не в самой сортировке, а в скрытой мутации. Вызов выглядит как обычное получение отсортированного результата, хотя одновременно изменяет сам исходный массив:const scores = [30, 10, 20];
const sorted = scores.sort(
(a, b) => a - b
);
console.log(scores);
// [10, 20, 30]
sort() сортирует массив непосредственно и возвращает ту же ссылку. Поэтому изменение увидит не только код, работающий с sorted, но и любой код, который уже хранит ссылку на исходный массив:const scores = [30, 10, 20];
const cached = scores;
scores.sort((a, b) => a - b);
console.log(cached);
// [10, 20, 30]
Особенно неприятно это становится на границе функций. По названию
getSortedUsers() можно ожидать получение нового результата, но реализация незаметно меняет массив вызывающего кода:function getSortedUsers(users) {
return users.sort(
(a, b) => a.age - b.age
);
}В результате функция имеет побочный эффект, который не очевиден из места её вызова. Это уже может затронуть состояние, кэш, мемоизацию или другой код, использующий тот же массив:
const users = getUsers();
const original = users;
getSortedUsers(users);
console.log(original === users);
// true
Классический способ избежать этого — сначала создать новый массив и сортировать уже его. Такой код работает, но копирование приходится выражать отдельной операцией:
const sorted = [...scores].sort(
(a, b) => a - b
);
console.log(scores);
// [30, 10, 20]
toSorted() делает намерение явным: получить новый массив с другим порядком элементов, не изменяя исходный. При этом возвращается уже другая ссылка:const sorted = scores.toSorted(
(a, b) => a - b
);
console.log(scores);
// [30, 10, 20]
console.log(scores === sorted);
// false
Важно, что копируется именно массив. Объекты внутри него не клонируются, поэтому обе коллекции по-прежнему содержат ссылки на одни и те же объекты:
const users = [
{ name: "Alex", age: 30 },
{ name: "Bob", age: 20 },
];
const sorted = users.toSorted(
(a, b) => a.age - b.age
);
console.log(
sorted[0] === users[1]
);
// true
Поэтому изменение самого объекта будет видно через обе коллекции:
sorted[0].age = 25;
console.log(users[1].age);
// 25
Похожая логика есть у
toReversed() и toSpliced(): они позволяют выполнить соответствующую операцию без изменения исходного массива, в отличие от мутирующих reverse() и splice().sort() и toSorted() — это не просто вопрос синтаксиса, это выбор контракта операции: изменить существующий массив или получить новый, сохранив исходный. Чем больше у массива потребителей, тем важнее эта разница.Please open Telegram to view this post
VIEW IN TELEGRAM
🔥9👍6🤝4❤1
Метод
filter() используется для отбора элементов массива по заданному условию. Результатом выполнения является новый массив, при этом исходный массив не изменяется.На изображении — синтаксис метода, аргументы callback-функции и примеры фильтрации чисел и объектов.
Сохрани, чтобы не потерять!
Please open Telegram to view this post
VIEW IN TELEGRAM
👍8🔥5🤝3
Почему JavaScript может изменить большой ID из API
Числовой идентификатор может быть корректно сформирован на сервере, но изменить значение после преобразования в js
Причина заключается в модели представления чисел:
Граница гарантированно точного представления целых чисел определяется константой
Безопасный диапазон составляет от
Для точных операций с большими целыми в js существует
С API возникает дополнительная граница — десериализация JSON. Сам JSON может содержать последовательность цифр
Если значение находится за пределами безопасного целочисленного диапазона, точность теряется уже во время этого преобразования:
После такого парсинга восстановить исходный ID по полученному
Поэтому идентификаторы, над которыми не предполагается арифметика, надёжнее передавать в JSON как строки. Это сохраняет исходную последовательность цифр независимо от числовых ограничений JavaScript-клиента:
Если значение всё же требуется использовать в точных целочисленных вычислениях, строку можно явно преобразовать в
🔥 Числовая природа ID не означает, что его следует представлять типом
📣 JS Ready | #практика
Числовой идентификатор может быть корректно сформирован на сервере, но изменить значение после преобразования в js
Number. Причина заключается в модели представления чисел:
Number соответствует IEEE 754 binary64 и не способен точно представить все целые числа произвольного размера:const id = 9007199254740993;
console.log(id);
// 9007199254740992
Граница гарантированно точного представления целых чисел определяется константой
Number.MAX_SAFE_INTEGER:console.log(Number.MAX_SAFE_INTEGER);
// 9007199254740991
console.log(
Number.isSafeInteger(9007199254740991)
);
// true
Безопасный диапазон составляет от
-(2^53 - 1) до 2^53 - 1. За его пределами проблема заключается не просто в том, что число становится «слишком большим»: соседние математические целые уже не обязаны иметь разные представления в Number. Поэтому два разных исходных значения могут оказаться одним и тем же значением javascript:const a = 9007199254740992;
const b = 9007199254740993;
console.log(a === b);
// true
Для точных операций с большими целыми в js существует
BigInt. Суффикс n создаёт значение непосредственно как BigInt, без промежуточного преобразования в Number:const id = 9007199254740993n;
console.log(id + 1n);
// 9007199254740994n
С API возникает дополнительная граница — десериализация JSON. Сам JSON может содержать последовательность цифр
9007199254740993 как число, но стандартный JSON.parse() преобразует JSON-число в JavaScript Number. Если значение находится за пределами безопасного целочисленного диапазона, точность теряется уже во время этого преобразования:
const data = JSON.parse(
'{"id":9007199254740993}'
);
console.log(data.id);
// 9007199254740992
После такого парсинга восстановить исходный ID по полученному
Number в общем случае невозможно: информация о первоначальном целом уже потеряна.Поэтому идентификаторы, над которыми не предполагается арифметика, надёжнее передавать в JSON как строки. Это сохраняет исходную последовательность цифр независимо от числовых ограничений JavaScript-клиента:
const data = JSON.parse(
'{"id":"9007199254740993"}'
);
console.log(data.id);
// "9007199254740993"
Если значение всё же требуется использовать в точных целочисленных вычислениях, строку можно явно преобразовать в
BigInt уже после получения ответа API:const data = JSON.parse(
'{"id":"9007199254740993"}'
);
const id = BigInt(data.id);
console.log(id + 1n);
// 9007199254740994n
Number. Если идентификатор потенциально превышает Number.MAX_SAFE_INTEGER, его представление становится частью корректности API-контракта: строка сохраняет ID без потерь при передаче, а BigInt подходит там, где действительно требуется точная целочисленная арифметика.Please open Telegram to view this post
VIEW IN TELEGRAM
❤12👍8🔥6
Разбираем Web Workers, с помощью которых можно выполнять ресурсоёмкие задачи вне основного UI-потока: запускать фоновые вычисления, передавать данные между main thread и Worker, получать результаты, обрабатывать ошибки и управлять завершением Worker. Используется для тяжёлых вычислений и обработки больших объёмов данных без блокировки интерфейса.Please open Telegram to view this post
VIEW IN TELEGRAM
Please open Telegram to view this post
VIEW IN TELEGRAM
❤12👍9🔥5
Выполняйте независимые операции без эффекта домино!
Если несколько операций не зависят друг от друга,
Для независимых задач используйте
Что полезно для пакетной отправки, фоновых задач и загрузки независимых данных: ошибка одной операции не скрывает успешные результаты остальных.
🔥 Если операции независимы, не позволяйте одной ошибке скрывать результат остальных — обрабатывайте каждую отдельно.
📣 JS Ready | #совет
Если несколько операций не зависят друг от друга,
Promise.all() может создать лишнюю связанность: одна ошибка отклоняет общий Promise, хотя остальные задачи продолжат выполняться и могут успешно завершиться.const users = await Promise.all(
ids.map(id => loadUser(id))
);
Для независимых задач используйте
Promise.allSettled() — он дождётся каждой и сохранит результат отдельно:const results = await Promise.allSettled(
ids.map(id => loadUser(id))
);
const successful = results
.filter(r => r.status === 'fulfilled')
.map(r => r.value);
Что полезно для пакетной отправки, фоновых задач и загрузки независимых данных: ошибка одной операции не скрывает успешные результаты остальных.
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥11👍5🤝3❤1
В этой статье:
• Показывается, как с нуля собрать браузерного тамагочи на HTML, CSS и JavaScript — без фреймворков, npm и сборщиков;• Разбираются игровое состояние, жизненный цикл питомца, случайные события, мини-игра, коллекция и сохранение прогресса через localStorage;• Объясняется, как рисовать пиксельных персонажей программно через Canvas и создавать звуки через Web Audio API без готовых изображений и аудиофайлов.🔊 Продолжайте читать на Habr!
Please open Telegram to view this post
VIEW IN TELEGRAM
❤9🔥7👍5🤝2
Например,
REST используют для построения API, WebSocket — для двустороннего обмена данными, а gRPC — для взаимодействия между сервисами.На картинке — основные технологии и подходы, с которыми регулярно сталкиваются разработчики: REST, Webhooks, GraphQL, SOAP, WebSocket, gRPC, MQTT, AMQP, SSE, EDI и EDA. Кратко и наглядно показано, чем они отличаются, как работают и для каких задач применяются.
Сохрани, чтобы не потерять!
Please open Telegram to view this post
VIEW IN TELEGRAM
👍8❤5🤝5🔥2
HTTP передаёт данные между клиентом и сервером без шифрования, а HTTPS добавляет TLS — он аутентифицирует сервер, согласовывает ключи и защищает дальнейший обмен данными шифрованием.
На картинке наглядно показано, чем отличается обычное HTTP-соединение от HTTPS: от TCP-handshake до установки защищённого соединения и передачи данных.
Сохрани, чтобы не потерять!
Please open Telegram to view this post
VIEW IN TELEGRAM
👍9❤5🔥4🤝3
Используйте возвращаемое значение delete() вместо лишней проверки!
У
Поэтому вместо двойного поиска:
можно сразу использовать результат удаления:
То же самое работает с
Вы не выполняете сначала
🔥 Не проверяйте коллекцию перед удалением, если проверка нужна только ради условия —
📣 JS Ready | #совет
У
Map.delete() и Set.delete() есть полезное свойство: метод возвращает true, только если элемент действительно существовал и был удалён.Поэтому вместо двойного поиска:
if (cache.has(key)) {
cache.delete(key);
onRemoved(key);
}можно сразу использовать результат удаления:
if (cache.delete(key)) {
onRemoved(key);
}То же самое работает с
Set:if (activeUsers.delete(userId)) {
updateCounter();
}Вы не выполняете сначала
has(), а затем отдельный delete(). Сама операция удаления одновременно отвечает на вопрос, был ли элемент.delete() уже возвращает нужный результат.Please open Telegram to view this post
VIEW IN TELEGRAM
👍8🔥8❤3🤝2
Подписывайтесь на событие только один раз через once!
У
Особенно полезно для одноразовых действий: завершения анимации, первого взаимодействия, загрузки ресурса или ожидания ответа.
Можно удобно превращать одноразовые события в
🔥 Для одноразового события используйте
📣 JS Ready | #совет
У
addEventListener() есть опция once: обработчик автоматически удаляется после первого срабатывания. Без ручного removeEventListener():button.addEventListener('click', handleClick, {
once: true
});Особенно полезно для одноразовых действий: завершения анимации, первого взаимодействия, загрузки ресурса или ожидания ответа.
element.addEventListener('transitionend', () => {
element.remove();
}, { once: true });Можно удобно превращать одноразовые события в
Promise:const once = (target, event) =>
new Promise(resolve =>
target.addEventListener(event, resolve, { once: true })
);
await once(video, 'ended');
console.log('Видео закончилось');
{ once: true }. Если ждёте одно из нескольких — снимайте оставшиеся обработчики через AbortController.Please open Telegram to view this post
VIEW IN TELEGRAM
👍11❤6🔥4
Как оплачивать зарубежные сервисы в 2026 году?
Можно бегать между посредниками и бояться блокировок после оплаты, а можно выпустить международную карту Lumio Pay и пользоваться любимыми сервисами без рисков.
— выпуск карты за 2 минуты
— лучший курс пополнения на рынке (у конкурентов на 20% выше)
— пополнение рублями или криптой
— чистые BIN карт, оплата без риска блокировок
Пока все ищут идеальное решение, оно у тебя перед глазами: @LumioPay
Можно бегать между посредниками и бояться блокировок после оплаты, а можно выпустить международную карту Lumio Pay и пользоваться любимыми сервисами без рисков.
— выпуск карты за 2 минуты
— лучший курс пополнения на рынке (у конкурентов на 20% выше)
— пополнение рублями или криптой
— чистые BIN карт, оплата без риска блокировок
Пока все ищут идеальное решение, оно у тебя перед глазами: @LumioPay
Почему forEach() не ждёт async-функцию?
Если операции должны выполняться последовательно, ожидание необходимо сделать частью самого цикла. Для этого подходит
Следующая итерация начнётся только после завершения предыдущей. Такой вариант нужен, когда важен порядок выполнения или операции зависят друг от друга.
Для независимых операций можно сформировать массив
Есть и важное следствие для обработки ошибок: внешний
Для управляемой обработки ошибки необходимо ожидать
🔥 Проблема не в
📣 JS Ready | #практика
forEach() не учитывает возвращаемое значение callback и не содержит механизма ожидания Promise. Поэтому передача async-callback не меняет модель выполнения метода: forEach() синхронно запускает callback для каждого элемента, не ожидая завершения созданных им асинхронных операций:const users = [1, 2, 3];
users.forEach(async (id) => {
await saveUser(id);
});
console.log("Готово");
"Готово" будет выведено до завершения saveUser(). Каждый вызов async-callback возвращает Promise, но forEach() эти Promise не ожидает и не собирает.Если операции должны выполняться последовательно, ожидание необходимо сделать частью самого цикла. Для этого подходит
for...of с await:for (const id of users) {
await saveUser(id);
}
console.log("Готово");Следующая итерация начнётся только после завершения предыдущей. Такой вариант нужен, когда важен порядок выполнения или операции зависят друг от друга.
Для независимых операций можно сформировать массив
Promise и дождаться их через Promise.all():await Promise.all(
users.map((id) => saveUser(id))
);
console.log("Готово");
Promise.all() успешно выполняется после выполнения всех переданных Promise и отклоняется, если хотя бы один из них завершается с ошибкой. При этом остальные уже запущенные операции автоматически не отменяются.Есть и важное следствие для обработки ошибок: внешний
try...catch вокруг forEach() не перехватит последующее отклонение Promise из async-callback:try {
users.forEach(async (id) => {
await saveUser(id);
});
} catch (error) {
console.error(error);
}Для управляемой обработки ошибки необходимо ожидать
Promise, представляющий выполнение асинхронных операций:try {
await Promise.all(
users.map((id) => saveUser(id))
);
} catch (error) {
console.error(error);
}async внутри forEach(), а в контракте самого метода: forEach() игнорирует возвращаемое значение callback и не ожидает Promise. Для последовательного выполнения используйте for...of с await, для конкурентного выполнения независимых операций — Promise.all().Please open Telegram to view this post
VIEW IN TELEGRAM
👍9❤6🔥6