@budarin/browser-log-runtime-core
v0.2.1
Published
Reusable journal core: generic buffered queue, batching, and pluggable flush (optional DOM pagehide helper)
Maintainers
Readme
@budarin/browser-log-runtime-core
Небольшая библиотека для буфера в памяти: вы описываете тип элемента T, задаёте, как именно накопленные элементы уходят к вашему приёмнику (сеть, диск, другой модуль — неважно), и задаёте лимиты очереди и размер одной порции за шаг. Новые элементы попадают в хвост очереди; с головы библиотека по очереди отдаёт в вашу функцию порции фиксированного размера. Считается ли порция успешно переданной, определяется только тем, что ваша функция вернула в виде Promise<boolean>.
Сама библиотека не реализует HTTP, RPC, сериализацию под ваш сервер и не вешает глобальные обработчики ошибок. Ей нужны setTimeout, queueMicrotask и промисы (Node, браузер и т.д.).
Что поставляется
| Entry | Содержимое |
| ------------------------------------------- | --------------------------------------------------------------------- |
| @budarin/browser-log-runtime-core | createBufferedLogger, типы, константа DEFAULT_DEBOUNCE_MS |
| @budarin/browser-log-runtime-core/browser | attachPagehideFlush — только pagehide, без подписок на ошибки |
Установка и импорт
pnpm add @budarin/browser-log-runtime-coreimport {
createBufferedLogger,
DEFAULT_DEBOUNCE_MS,
} from '@budarin/browser-log-runtime-core';Значения по умолчанию
Если в опциях число не указано, используются такие значения:
| Параметр | По умолчанию |
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| batchSize | 32 — за один шаг с головы очереди отдаётся не больше стольки элементов |
| maxQueueSize | 1000 — столько элементов очередь держит максимум |
| debounceMs | DEFAULT_DEBOUNCE_MS (сейчас 50): после серии добавлений в очередь следующая автоматическая попытка передать накопленное наружу ждёт столько миллисекунд тишины |
Чтобы не ждать таймер и передать накопленное почти сразу после текущего синхронного кода, укажите debounceMs: 0: тогда отложенная попытка ставится на следующий microtask (несколько подряд добавленных в одном синхронном участке кода обычно попадут в одну общую передачу наружу).
Как это работает
Очередь
Метод enqueue кладёт элемент в конец. Если длина превысила maxQueueSize, с начала удаляются самые старые элементы, пока длина снова не станет допустимой.
Когда накопленное уходит без отдельной команды с вашей стороны
Пока не идёт уже активная передача наружу:
- Если
debounceMs > 0(в том числе значение по умолчанию 50): на каждое добавление в очередь предыдущая отсрочка сбрасывается и заводится новая. Передача наружу запускается только когда с момента последнего добавления прошлоdebounceMsмиллисекунд без новых элементов. Это обычный trailing debounce: при частом потоке событий не дергают приёмник на каждый элемент, а ждут короткой паузы. - Если вы явно указали
debounceMs: 0: таймера нет — одна отложенная попытка передачи на следующий microtask. Элементы, добавленные подряд в одном синхронном блоке, чаще всего обрабатываются одним заходом описанного ниже цикла.
Если передача уже выполняется, из enqueue не планируется второй параллельный цикл: хвост очереди подхватывается тем заходом, который уже идёт.
За один заход обработки очереди
Пока очередь не пуста:
- С головы берётся не больше
batchSizeэлементов подряд — это одна порция для передачи наружу. - Библиотека ждёт завершения вашей функции из опции
flushдля этой порции (и при необходимости передаёт второй аргументoptions, например{ keepalive: true }, если передачу инициировали черезflushOnLeave()илиflush({ keepalive: true })у экземпляра логгера). - Если функция завершилась с
true: эти элементы снимаются с головы очереди; если очередь ещё не пуста — в том же заходе берётся следующая порция (за один заход можно передать много порций подряд). - Если вернулось
falseили неtrue: заход останавливается, эта порция остаётся в очереди. Библиотека сама не повторяет вызов вашей функции для тех же элементов. Следующая попытка — когда снова сработает отложенная передача (debounce / microtask) или вы сами вызоветеflushу логгера.
Когда вы сами запускаете передачу
flush(options?)у логгера — тот же цикл обработки очереди; в начале отменяется отложенный таймер debounce, чтобы не отправить одно и то же дважды «по таймеру и по руке».flushOnLeave()— то же, чтоflush({ keepalive: true }). Что делать с флагомkeepaliveвнутри вашей функции в опциях, решаете вы (частый случай в браузере — последняя попытка записи при уходе со страницы).
Если flush у логгера вызвали снова, пока ещё шла передача
Параметры вызовов накапливаются: если хоть раз передали keepalive: true, это сохранится. После завершения текущего захода, если очередь ещё не пуста и был отложенный запрос, выполняется ещё один заход с объединёнными опциями.
dispose()
Логгер выключается: новые enqueue игнорируются, очередь очищается, таймер debounce снимается. Уже запущенный у вас асинхронный код внутри вашей функции из опции flush может ещё отработать — это граница вашего кода.
Опции createBufferedLogger<T>(options)
Ниже — поля объекта настроек, который вы один раз передаёте в createBufferedLogger. Это не то же самое, что методы у возвращённого логгера: у экземпляра свой набор действий — см. подзаголовок «Методы логгера» ниже.
| Поле | Обязательное | Кратко |
| -------------- | ------------ | ---------------------------------------------------------------------------------------------- |
| flush | да | ваша функция передачи накопленного наружу; подробности — в следующем подразделе |
| batchSize | нет | максимум элементов с головы очереди за один шаг к вашей функции (по умолчанию 32) |
| maxQueueSize | нет | максимальная длина очереди (по умолчанию 1000) |
| debounceMs | нет | сколько миллисекунд тишины ждать после добавлений перед автоматической передачей; по умолчанию DEFAULT_DEBOUNCE_MS; 0 — вместо таймера следующий microtask |
Поле flush в опциях
Это ваша функция: библиотека только держит очередь и в нужный момент передаёт в неё очередную порцию элементов с головы. Куда эти данные дальше уходят — полностью на вашей стороне.
Сигнатура в типах:
(batch: readonly T[], options?: { keepalive?: boolean }) => Promise<boolean>;batch— неизменяемый массив: срез с головы очереди, не длиннееbatchSize. За один заход обработки очереди библиотека может вызвать вашу функцию несколько раз подряд, пока очередь не опустеет или пока вы не вернёте не-true(см. раздел «Как это работает»).options— необязателен. Если передачу инициировали черезflushOnLeave()илиflush({ keepalive: true })у логгера, сюда может попасть{ keepalive: true }. Как использовать флаг — решаете вы (типичный пример в браузере — передатьkeepaliveвfetchпри уходе со страницы).- Возвращаемое значение — промис, который должен разрешиться в
true, если эту порцию можно убрать из буфера (с вашей точки зрения она надёжно ушла к приёмнику). Любой другой итог — неуспех: порция остаётся в очереди, библиотека сама не повторяет вызов; следующая попытка — при следующей автоматической передаче или когда вы сами вызоветеflushу логгера.
Методы логгера
| Метод | Зачем |
| ---------------- | --------------------------------------------------------------------- |
| enqueue | добавить элемент в конец очереди |
| flush | сейчас запустить цикл передачи накопленного наружу (см. выше) |
| flushOnLeave | то же, что flush({ keepalive: true }) — часто при закрытии вкладки |
| dispose | выключить логгер, очистить очередь, снять таймеры |
…/browser — только уход со страницы
import { attachPagehideFlush } from '@budarin/browser-log-runtime-core/browser';
const detach = attachPagehideFlush(() => {
void logger.flushOnLeave();
});
// …
detach();Подписка на pagehide с { capture: true }. Глобальные error / unhandledrejection не трогаются.
Примеры
Частые события, передавать наружу хочется порциями после короткой паузы
Здесь в качестве T — строка журнала; приёмник — HTTP. То же устройство подойдёт для любых сериализуемых структур.
import { createBufferedLogger, DEFAULT_DEBOUNCE_MS } from '@budarin/browser-log-runtime-core';
type LogRow = { level: string; message: string; at: number };
const logger = createBufferedLogger<LogRow>({
batchSize: 32,
maxQueueSize: 1000,
// debounceMs не указан — будет DEFAULT_DEBOUNCE_MS (сейчас 50)
flush: async (batch, opts) => {
const res = await fetch('/api/logs', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ events: batch }),
keepalive: opts?.keepalive === true,
});
return res.ok;
},
});
logger.enqueue({ level: 'info', message: 'готово', at: Date.now() });
// после ~DEFAULT_DEBOUNCE_MS мс без новых enqueue очередь сама дойдёт до передачи на серверНужна почти немедленная передача после текущего синхронного кода
const logger = createBufferedLogger({
debounceMs: 0,
flush: async (batch) => {
await someSink.write(batch);
return true;
},
});Лицензия
MIT
