Event sourcing
Принцип: каждое изменение — событие, текущее состояние — результат их суммы. Объясняет, как event sourcing применён к транзакциям сейчас и куда расширяется в целевой архитектуре.
В системе event sourcing нет колонки «текущий баланс». Есть лог событий
(append-only), а баланс вычисляется как SUM() всех транзакций до «сейчас».
Снапшоты — кэш для производительности, не источник правды. Удалить событие
нельзя — только пометить отменённым (soft delete, deleted_at).
Транзакции в проде: event sourcing в узком смысле
Текущая реализация — таблица transactions (миграция 034). Это append-only
лог денежных операций и движения предметов. Никакой хранимой колонки
balance — баланс PC и сташа считается через SUM() при каждом запросе.
Виды транзакций (kind)
kind | Смысл |
|---|---|
money | денежное движение: cp/sp/gp/pp (знаковые int) |
item | предмет вошёл/вышел из инвентаря; item_qty ≥ 1 |
transfer | перевод между PC или в общак; обе ноги связаны transfer_group_id |
Парные переводы через transfer_group_id
Перевод — всегда две строки с одним transfer_group_id: одна нога
с отрицательным amount_* (уходит) и вторая с положительным (приходит).
Никаких обновлений existing rows. Если нужно откатить — добавляется
обратная пара.
Статусы (status)
pending → заявка игрока ожидает одобрения DM.
approved → применено (дефолт для DM-записей).
rejected → отклонено; в баланс не включается.
Статус изменяется server action'ом; сама строка не обновляется по
event-sourcing-принципу — но status — единственное поле, которое
меняется после создания (это компромисс между строгим event sourcing
и практичностью).
autogen-поля
Некоторые транзакции создаются автоматически (стартовый набор, авто-лут).
Поля autogen_label и autogen_reconcile_key маркируют их как
machine-generated и позволяют реконсайлу обнаружить дубликаты при
повторной генерации.
Почему SUM() вместо хранимого баланса
Хранимый баланс — источник рассинхронизации: если запись пропала или была
откорректирована, баланс врёт без видимых следов. SUM() по append-only
логу всегда даёт консистентный результат. Цена — чуть медленнее на
больших лентах (индекс по actor_pc_id + фильтр по status='approved'
это компенсирует).
Целевая архитектура: универсальный events лог
Таблица transactions — частный случай паттерна. Цель engine pivot —
расширить до универсального events:
events (
id, event_type, at_tick, actor_id, location_id,
payload jsonb, visibility jsonb,
persistence_scope text,
deleted_at timestamptz
)
Все значимые изменения мира — событие: DM-операция, движение NPC, изменение
погоды, добыча лута. transactions становятся одним из event_type.
→ roadmap/generic-events-table.md
Ключевые гарантии
- Append-only: insert-only, no delete (кроме
deleted_at). - Полный аудит: любое состояние восстановимо по логу.
- Безопасный откат: отмена = добавление обратного события, не удаление.
- Visibility: каждое событие несёт
visibility— кто его видит. →visibility.md
→
persistence-scope.md— тегиloop/character/metaна событиях для управления сбросом петли. →features/accounting/README.md— текущая реализация бухгалтерии.