mirror of
https://github.com/backnotprop/plannotator.git
synced 2026-09-14 14:17:26 +08:00
472e17be3d
* feat: expandable diff context in code review (closes #243) Switch from PatchDiff to FileDiff component from @pierre/diffs to enable GitHub-style "show more lines" buttons between hunks. The library handles all expansion UI when provided full file contents via oldLines/newLines. - Add /api/file-content endpoint to serve old/new file content per diff type - Add getFileContentsForDiff() helper with ref mapping for all diff types - Parse patch client-side via getSingularPatch(), augment with file contents - Update @pierre/diffs from 1.0.4 to 1.0.11 (scroll sync fix, no breaking changes) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: upgrade @pierre/diffs to beta, re-parse patch with full file contents The previous approach spread file content arrays onto a partial-mode FileDiffMetadata, causing hunk index mismatches and Shiki decoration errors. Now uses processFile() to re-parse the patch with oldFile/newFile so hunk indices are computed against the full file (isPartial: false), which is required for expansion to work correctly. Also fixes a flash error on file switch by tagging fileContents with the filePath they were fetched for, preventing stale contents from being paired with the wrong patch during the render before useEffect fires. - Upgrade @pierre/diffs from ^1.0.x to ^1.1.0-beta.19 in all workspaces - Add test fixtures for disjoint hunks, deleted/renamed/new files Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: abort stale file-content fetches, validate path params - Add AbortController to file-content fetch effect to cancel in-flight requests when switching files - Reject path traversal (.. or absolute paths) on /api/file-content - Document /api/file-content endpoint in CLAUDE.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
77 lines
2.0 KiB
TypeScript
77 lines
2.0 KiB
TypeScript
/**
|
|
* Typed Event Emitter
|
|
*
|
|
* A minimal, type-safe event system for internal pub/sub communication.
|
|
* Used by services to broadcast lifecycle and domain events.
|
|
*/
|
|
|
|
export type EventHandler<T = unknown> = (data: T) => void | Promise<void>;
|
|
|
|
export interface EventSubscription {
|
|
unsubscribe(): void;
|
|
}
|
|
|
|
export class EventEmitter<Events extends Record<string, unknown>> {
|
|
private handlers = new Map<keyof Events, Set<EventHandler<any>>>();
|
|
|
|
/**
|
|
* Subscribe to an event. Returns a subscription object with unsubscribe().
|
|
*/
|
|
on<K extends keyof Events>(event: K, handler: EventHandler<Events[K]>): EventSubscription {
|
|
if (!this.handlers.has(event)) {
|
|
this.handlers.set(event, new Set());
|
|
}
|
|
this.handlers.get(event)!.add(handler);
|
|
|
|
return {
|
|
unsubscribe: () => {
|
|
this.handlers.get(event)?.delete(handler);
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Subscribe to an event for a single emission only.
|
|
*/
|
|
once<K extends keyof Events>(event: K, handler: EventHandler<Events[K]>): EventSubscription {
|
|
const wrapper: EventHandler<Events[K]> = (data) => {
|
|
sub.unsubscribe();
|
|
return handler(data);
|
|
};
|
|
const sub = this.on(event, wrapper);
|
|
return sub;
|
|
}
|
|
|
|
/**
|
|
* Emit an event, calling all registered handlers.
|
|
* Handlers are called in registration order.
|
|
* Async handlers are awaited sequentially.
|
|
*/
|
|
async emit<K extends keyof Events>(event: K, data: Events[K]): Promise<void> {
|
|
const handlers = this.handlers.get(event);
|
|
if (!handlers) return;
|
|
|
|
for (const handler of handlers) {
|
|
await handler(data);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove all handlers for a specific event, or all events if no event specified.
|
|
*/
|
|
removeAllListeners(event?: keyof Events): void {
|
|
if (event) {
|
|
this.handlers.delete(event);
|
|
} else {
|
|
this.handlers.clear();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the number of handlers registered for a specific event.
|
|
*/
|
|
listenerCount(event: keyof Events): number {
|
|
return this.handlers.get(event)?.size ?? 0;
|
|
}
|
|
}
|