# `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 ```ts 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: ```ts 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.