npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@budarin/browser-log-runtime-core

v0.2.1

Published

Reusable journal core: generic buffered queue, batching, and pluggable flush (optional DOM pagehide helper)

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-core
import {
    createBufferedLogger,
    DEFAULT_DEBOUNCE_MS,
} from '@budarin/browser-log-runtime-core';

Значения по умолчанию

Если в опциях число не указано, используются такие значения:

| Параметр | По умолчанию | | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | batchSize | 32 — за один шаг с головы очереди отдаётся не больше стольки элементов | | maxQueueSize | 1000 — столько элементов очередь держит максимум | | debounceMs | DEFAULT_DEBOUNCE_MS (сейчас 50): после серии добавлений в очередь следующая автоматическая попытка передать накопленное наружу ждёт столько миллисекунд тишины |

Чтобы не ждать таймер и передать накопленное почти сразу после текущего синхронного кода, укажите debounceMs: 0: тогда отложенная попытка ставится на следующий microtask (несколько подряд добавленных в одном синхронном участке кода обычно попадут в одну общую передачу наружу).


Как это работает

Очередь

Метод enqueue кладёт элемент в конец. Если длина превысила maxQueueSize, с начала удаляются самые старые элементы, пока длина снова не станет допустимой.

Когда накопленное уходит без отдельной команды с вашей стороны

Пока не идёт уже активная передача наружу:

  1. Если debounceMs > 0 (в том числе значение по умолчанию 50): на каждое добавление в очередь предыдущая отсрочка сбрасывается и заводится новая. Передача наружу запускается только когда с момента последнего добавления прошло debounceMs миллисекунд без новых элементов. Это обычный trailing debounce: при частом потоке событий не дергают приёмник на каждый элемент, а ждут короткой паузы.
  2. Если вы явно указали debounceMs: 0: таймера нет — одна отложенная попытка передачи на следующий microtask. Элементы, добавленные подряд в одном синхронном блоке, чаще всего обрабатываются одним заходом описанного ниже цикла.

Если передача уже выполняется, из enqueue не планируется второй параллельный цикл: хвост очереди подхватывается тем заходом, который уже идёт.

За один заход обработки очереди

Пока очередь не пуста:

  1. С головы берётся не больше batchSize элементов подряд — это одна порция для передачи наружу.
  2. Библиотека ждёт завершения вашей функции из опции flush для этой порции (и при необходимости передаёт второй аргумент options, например { keepalive: true }, если передачу инициировали через flushOnLeave() или flush({ keepalive: true }) у экземпляра логгера).
  3. Если функция завершилась с true: эти элементы снимаются с головы очереди; если очередь ещё не пуста — в том же заходе берётся следующая порция (за один заход можно передать много порций подряд).
  4. Если вернулось 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