/** * Keyboard-wedge scanners type like superhuman typists: a burst of characters * with tiny inter-key gaps, terminated by Enter. This detector distinguishes a * scan from human typing purely from timing + length, with injected timestamps * so it is testable without a DOM or a clock. */ export interface WedgeOptions { /** Max gap between scanner keystrokes; humans are slower. */ maxInterKeyMs: number /** Bursts shorter than this are treated as typed input. */ minLength: number } export const DEFAULT_WEDGE: WedgeOptions = { maxInterKeyMs: 35, minLength: 6 } export type WedgeResult = { type: 'scan'; code: string } | { type: 'typed'; text: string } export class WedgeDetector { private chars: string[] = [] private lastAtMs: number | undefined private maxGapMs = 0 constructor(private readonly opts: WedgeOptions = DEFAULT_WEDGE) {} feed(char: string, atMs: number): void { if (char.length !== 1) throw new Error('feed() takes single characters') if (this.lastAtMs !== undefined) { this.maxGapMs = Math.max(this.maxGapMs, atMs - this.lastAtMs) } this.lastAtMs = atMs this.chars.push(char) } /** Call on Enter. Decides scan vs typed and resets for the next burst. */ terminate(): WedgeResult { const text = this.chars.join('') const isScan = this.chars.length >= this.opts.minLength && this.maxGapMs <= this.opts.maxInterKeyMs this.reset() return isScan ? { type: 'scan', code: text } : { type: 'typed', text } } reset(): void { this.chars = [] this.lastAtMs = undefined this.maxGapMs = 0 } }