event-journal: установка и использование

Версия 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
idUUID записи
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 проекта. Данные хранятся в файлах, пути которых вы задали сами. Сетевых портов и фоновых служб нет.

Вопросы по установке и использованию принимаются через форму обратной связи.