Версия 0.1.0. Правообладатель — ООО «Айти Дев Линк». Журнал действий и очередь доставки событий для приложений и ботов на Node.js. Библиотека не привязана к какому-либо мессенджеру.
1. Что нужно
- Node.js 20 или новее (Windows, Linux, macOS). На российских ОС Node.js ставится из репозитория системы, например
sudo apt install nodejs npm(Astra Linux, ALT) илиsudo dnf install nodejs(РЕД ОС). - Доступ к интернету не требуется. Docker, виртуальные машины, база данных и сервер не нужны. Зависимостей у пакета нет.
2. Установка
Скачайте архив event-journal-0.1.0.tgz и выполните в каталоге проекта одну команду:
npm install --offline ./event-journal-0.1.0.tgzКоманда ставит библиотеку из локального файла и не обращается к сети. Скомпилированный код уже внутри архива, TypeScript для использования не нужен.
3. Проверка установки
npx event-journal-selfcheckЕсли команда не найдена:
node node_modules/event-journal/dist/src/selfcheck.jsСамопроверка проверяет создание события, дедупликацию, повтор доставки, вызов фильтра персональных данных, файловое хранилище и выгрузку в файл. Успех — «Самопроверка пройдена (5 из 5)» и код выхода 0; при ошибке код 1.
4. Быстрый старт
import { DeliveryQueue, FileSink, FileStore, Journal } from 'event-journal';
const queue = new DeliveryQueue({
store: new FileStore('./queue.jsonl'), // состояние переживает перезапуск
sink: new FileSink('./events.jsonl'), // куда выгружать
});
const journal = new Journal({ queue });
await journal.record({
type: 'appointment.created',
actor: 'user-42',
source: 'bot',
payload: { slotId: 'slot-17' },
dedupKey: 'appointment.created:slot-17:user-42',
});
queue.start(5000); // доставка каждые 5 секунд
// ... при завершении работы:
await queue.stop();Доставить немедленно и получить итог: await queue.flush() возвращает { delivered, retried, dead }.
Запуск примера из файла example.mjs: node example.mjs. При первом запуске record вернёт duplicate: false, в events.jsonl появится одна строка; повторный запуск в той же папке вернёт duplicate: true.
5. Схема события
| Поле | Описание |
|---|---|
schemaVersion | версия схемы, сейчас 1 |
id | UUID записи |
type | тип события, до 128 символов |
occurredAt | время события, ISO 8601, UTC |
actor, subject, source | кто, над чем, откуда (необязательные) |
payload | данные события, JSON-совместимый объект |
dedupKey | ключ дедупликации |
6. Дедупликация
События с одинаковым dedupKey попадают в очередь один раз; record возвращает duplicate: true. Рекомендуется задавать ключ явно по смыслу события. Если ключ не задан, он считается как SHA-256 от содержимого без id.
Ключ считается после работы фильтра персональных данных, поэтому не хранит производных от них. Следствие: два события, которые отличались только удалёнными данными, получат один ключ. Запись о доставленном событии хранится, пока вы её не удалите: queue.purgeDelivered(мс). После удаления такое же событие будет принято снова.
7. Повторы с паузой
new DeliveryQueue({
store, sink,
retry: { baseDelayMs: 1000, factor: 2, maxDelayMs: 300000, maxAttempts: 8, jitter: 0.2 },
onDead: (event, error) => console.error('не доставлено', event.id, error),
});Пауза перед повтором: baseDelayMs * factor^(n-1), не больше maxDelayMs, с разбросом jitter. Исчерпавшая попытки запись получает статус dead и остаётся в хранилище. Вернуть такие записи в очередь: queue.requeueDead().
Приёмник может отклонить отдельные события, не проваливая всю пачку:
return { rejected: [{ dedupKey: 'k', error: 'неверный формат', permanent: true }] };permanent: true сразу переводит запись в dead. Гарантия доставки — «минимум один раз»: приёмник должен терпеть повторы; для БД это обеспечивает dedup_key.
8. Хранилища и приёмники
MemoryStore— в памяти, для тестов.FileStore(путь)— журнал операций в JSONL. Рассчитан на один процесс-владельца файла. Оборванная последняя строка после сбоя пропускается.compact()сокращает файл.- Своё хранилище: реализуйте интерфейс
QueueStore. FileSink(путь)— дописывает события в JSONL.CallbackSink(fn)— любая ваша функция.SqlSink({ execute, table, dialect })— запись в таблицу через ваш драйвер БД (postgresилиsqlite). Драйвер библиотека не подключает.
import { SqlSink, createTableSql } from 'event-journal';
await pool.query(createTableSql('event_journal', 'postgres'));
const sink = new SqlSink({ execute: (sql, params) => pool.query(sql, params) });Вставка использует ON CONFLICT (dedup_key) DO NOTHING. Имя таблицы проверяется (латиница, цифры, подчёркивание).
9. Подключение фильтра персональных данных
Параметр sanitize получает событие и возвращает очищенное. Фильтр вызывается до записи в очередь, поэтому исходные данные в хранилище и приёмник не попадают. Если фильтр бросил исключение, событие не записывается, а record завершается ошибкой. Подойдёт любая функция вида (event) => event, в том числе фильтр из библиотеки pdn-filter.
const journal = new Journal({
queue,
sanitize: (event) => ({ ...event, payload: maskPersonalData(event.payload) }),
});10. Ограничения
- Одна очередь рассчитана на один процесс. Одновременный запуск двух процессов на одном файле не поддерживается.
- Порядок доставки примерно по времени, строгий порядок не гарантируется.
- Размер хранилища нужно ограничивать самостоятельно:
purgeDelivered,compact.
11. Типичные проблемы
| Симптом | Причина | Что сделать |
|---|---|---|
node: команда не найдена | не установлен Node.js | установить пакет из репозитория ОС |
npm ERR! code ENOENT | неверный путь к архиву | запустить команду в каталоге с архивом |
| версия Node.js старее 20 | устаревшая ОС | обновить пакет Node.js из репозитория ОС |
12. Удаление
npm uninstall event-journalБиблиотека ставится в каталог node_modules/event-journal проекта. Данные хранятся в файлах, пути которых вы задали сами. Сетевых портов и фоновых служб нет.
Вопросы по установке и использованию принимаются через форму обратной связи.