Project: typed event delivery
Request
The dispatch product team needs a typed event service for shipment panels.
Data
Commands describe subscriptions and event deliveries. Identifiers are unique within a topic; limit counts are positive integers.
Deliverable
A reusable typed emitter with removal and cleanup, demonstrated by the command runner. Each step extends the same implementation.
A dispatch screen needs to notify several panels when a shipment changes. Polling each panel creates duplicated work, so the application uses an event emitter. Each topic has a known payload type, and each subscriber has an identifier that can be removed later.
Build the emitter through eight small releases. Start with registration and delivery, then add removal, one-time subscribers, counts, cleanup, captured prefixes, batched delivery, and limited subscriptions. The wrapper represents actions as pipe-separated commands so every behavior can be tested without a browser page. Your code carries forward between steps. Keep delivery order stable and isolate topics: removing a subscriber from one topic must not remove a subscriber with the same identifier elsewhere. Compiler checks protect both event names and payloads.
type Listener<T>={id:string;fn:(value:T)=>void;remaining:number};
class Emitter<E extends Record<string,unknown>> {
private entries=new Map<keyof E,Listener<unknown>[]>();
on<K extends keyof E>(event:K,id:string,fn:(value:E[K])=>void,remaining=Infinity):void { const rows=this.entries.get(event) ?? []; rows.push({id,fn:fn as (value:unknown)=>void,remaining}); this.entries.set(event,rows); }
off<K extends keyof E>(event:K,id:string):void { this.entries.set(event,(this.entries.get(event) ?? []).filter(row=>row.id!==id)); }
count<K extends keyof E>(event:K):number { return (this.entries.get(event) ?? []).length; }
clear<K extends keyof E>(event:K):void { this.entries.delete(event); }
emit<K extends keyof E>(event:K,value:E[K]):void { for(const row of [...(this.entries.get(event) ?? [])]) { row.remaining-=1; if(row.remaining===0) this.off(event,row.id); row.fn(value); } }
}
function solve(commands:string[]):string[] { const out:string[]=[]; const bus=new Emitter<Record<string,string>>(); for(const command of commands){ const [op,topic,id,extra]=command.split("|"); if(op==="on") bus.on(topic,id,v=>out.push(id+":"+v));
else if(op==="emit") bus.emit(topic,id); } return out; }
A complete implementation for the first scenario; trace its input before editing.