docs: document cursor telemetry buffer API and surface drop events

Add JSDoc to every public export in cursorTelemetryBuffer so the module
meets the 80% docstring-coverage threshold, and make two silent-drop
paths observable:

- endSession() now returns the number of pending batches evicted by the
  maxPendingBatches cap and emits console.warn when any are dropped.
- prependBatch() defensively trims and warns if an unusual retry pattern
  would push the queue past the cap (normal retry after takeNextBatch()
  stays a no-op).

Tests cover both drop paths.
This commit is contained in:
shaun0927
2026-04-21 17:07:19 +09:00
parent fac0b405d3
commit adc610544c
2 changed files with 148 additions and 4 deletions
+97 -3
View File
@@ -1,17 +1,89 @@
/**
* A single cursor telemetry sample captured during a recording session.
*
* Coordinates (`cx`, `cy`) are device-pixel positions relative to the
* captured surface; `timeMs` is the offset from the recording's start.
*/
export interface CursorTelemetryPoint {
timeMs: number;
cx: number;
cy: number;
}
/**
* Per-session cursor telemetry buffer with bounded memory.
*
* Flow: `startSession()` → `push(point)` N times → `endSession()` enqueues
* the collected samples as a completed batch. The main process later
* drains batches in FIFO order via `takeNextBatch()` to persist them to
* disk, and can `prependBatch()` on write failure to retry without losing
* order.
*
* Memory is bounded by `maxActiveSamples` (ring buffer on the in-progress
* batch) and `maxPendingBatches` (FIFO cap across completed batches).
*/
export interface CursorTelemetryBuffer {
/**
* Begin a new recording session. Clears any in-progress active samples
* (without touching already-completed pending batches). Safe to call
* repeatedly — e.g. a rapid Stop → Record sequence.
*/
startSession(): void;
/**
* Append a telemetry sample to the current active session. When the
* active buffer exceeds `maxActiveSamples`, the oldest sample is
* dropped (ring behaviour).
*/
push(point: CursorTelemetryPoint): void;
endSession(): void;
/**
* Finalize the active session, moving its samples into the pending
* queue as a single batch. Empty sessions are dropped (no empty batch
* is enqueued).
*
* If the pending queue would exceed `maxPendingBatches`, the oldest
* batches are evicted to bound memory. A `console.warn` is emitted
* whenever at least one batch is dropped so that pathological rapid-
* restart scenarios are observable.
*
* @returns the number of pending batches dropped by this call (0 under
* normal operation).
*/
endSession(): number;
/**
* Remove and return the oldest pending batch, or an empty array if
* the queue is empty.
*/
takeNextBatch(): CursorTelemetryPoint[];
/**
* Re-insert a batch at the front of the queue, preserving FIFO order
* on retry paths (e.g. when persisting the batch failed and the
* caller wants the next `takeNextBatch()` to yield it again).
*
* Empty batches are ignored. The pending cap is enforced defensively
* — if prepending would push the queue past `maxPendingBatches`, the
* oldest entries are evicted and a `console.warn` is emitted. In
* normal retry usage this trim is a no-op because the caller has just
* removed the batch via `takeNextBatch()`.
*/
prependBatch(batch: CursorTelemetryPoint[]): void;
/**
* Drop the most recently enqueued pending batch. Used when a recording
* is discarded after `endSession()` but before it has been persisted.
* No-op on an empty queue.
*/
discardLatestPending(): void;
/**
* Clear both the active and pending state. Intended for tests and
* full teardown paths.
*/
reset(): void;
readonly activeCount: number;
readonly pendingCount: number;
}
@@ -23,6 +95,11 @@ export interface CursorTelemetryBufferOptions {
const DEFAULT_MAX_PENDING_BATCHES = 8;
/**
* Create a cursor telemetry buffer.
*
* @see CursorTelemetryBuffer for the full lifecycle contract.
*/
export function createCursorTelemetryBuffer(
options: CursorTelemetryBufferOptions,
): CursorTelemetryBuffer {
@@ -43,20 +120,37 @@ export function createCursorTelemetryBuffer(
}
},
endSession() {
let dropped = 0;
if (active.length > 0) {
pending.push(active);
while (pending.length > maxPending) {
pending.shift();
dropped++;
}
}
active = [];
if (dropped > 0) {
console.warn(
`[cursorTelemetryBuffer] dropped ${dropped} pending batch(es) to stay within maxPendingBatches=${maxPending}`,
);
}
return dropped;
},
takeNextBatch() {
return pending.shift() ?? [];
},
prependBatch(batch) {
if (batch.length > 0) {
pending.unshift(batch);
if (batch.length === 0) return;
pending.unshift(batch);
let dropped = 0;
while (pending.length > maxPending) {
pending.pop();
dropped++;
}
if (dropped > 0) {
console.warn(
`[cursorTelemetryBuffer] prependBatch trimmed ${dropped} trailing batch(es) to stay within maxPendingBatches=${maxPending}`,
);
}
},
discardLatestPending() {