Files
skills/docs/api-stack/api/api-logger-worker-proxy/src/batching/batching.md
2026-05-29 08:20:56 +01:00

3.5 KiB

batching

(no submodules)

Purpose

Buffers outbound log records and flushes them in batches over a single MessagePort frame. Reducing one-frame-per-record to one-frame-per-batch materially lowers the wire cost for workers under load, where a sustained plugin can emit several hundred records per second. The batcher is sink-agnostic — it invokes an onFlush callback supplied at construction with a frozen snapshot of buffered records, so it composes equally with the worker-side client's postMessage call and with test sinks.

A second concern handled here is backpressure. Logging is best-effort: a slow main-side sink must never break the application by surfacing as an exception in log.info(...). The batcher caps the number of outstanding-but-not-yet-released batches at maxOutstandingBatches; once the cap is reached new records are dropped silently and an onDrop callback fires so the consumer can increment the LOGGER_PROXY_BACKPRESSURE_DROP counter.

Features

  • Flush triggers: max batch size (default 100), idle interval (default 50 ms), error/fatal level (immediate, but records buffered before the error still ship in the same batch — order is preserved).
  • Bounded outstanding-batch budget (default 8). Dropped records increment a counter; the batcher never throws on enqueue.
  • Injectable TimeoutScheduler for deterministic timer tests.
  • dispose() clears the timer and drops anything still buffered.

Usage: Basic

import { LogBatcher } from "@suaveplan/api-logger-worker-proxy/batching";

const batcher = new LogBatcher((records) => {
    port.postMessage({
        v: "1.0.0",
        kind: "log-batch",
        plugin: { name: "demo", version: "0.1.0" },
        records,
    });
    batcher.releaseBatch();
});

batcher.enqueue({
    level: "info",
    msg: "hello",
    ts: new Date().toISOString(),
});

Usage: Advanced

Wiring the drop counter to an OTel meter and using a deterministic scheduler in tests:

import {
    LogBatcher,
    type TimeoutScheduler,
} from "@suaveplan/api-logger-worker-proxy/batching";

let dropped = 0;
const fakeScheduler: TimeoutScheduler = {
    set: (h) => {
        queueMicrotask(h);
        return null;
    },
    clear: () => {},
};

const batcher = new LogBatcher((records) => sink.write(records), {
    maxBatchSize: 10,
    flushIntervalMs: 5,
    maxOutstandingBatches: 4,
    onDrop: () => {
        dropped++;
    },
    scheduler: fakeScheduler,
});

API Reference

  • new LogBatcher(onFlush, options?) — constructor.
  • enqueue(record): boolean — true accepted, false dropped.
  • flush() — flush whatever is buffered; idempotent.
  • releaseBatch() — return a batch slot to the budget.
  • dispose() — stop accepting; drop everything in the buffer.
  • droppedCount / flushedBatchCount / bufferedCount / outstandingBatches — read-only counters for tests + telemetry.
  • DEFAULT_BATCHER_OPTIONS — the defaults exposed for re-use.

Implementation Notes

error and fatal flush immediately AFTER being appended to the buffer so any records queued before them ship in the same batch and the ordering is preserved. The outstanding counter is decremented by releaseBatch(); the worker-side client invokes that synchronously after postMessage returns since MessagePort has no ack frame for log batches and there is no transport-level confirmation to wait for. Best-effort posture: every code path that could otherwise throw on a log call (full buffer, disposed batcher) routes through recordDrop instead.