ERP-node/frontend/lib/v2-core/events/EventBus.ts

345 lines
9.1 KiB
TypeScript
Raw Normal View History

/**
* V2 EventBus -
*
* :
* - /
* - ( )
* - /
* -
* - ( )
*/
import {
V2_EVENTS,
V2EventName,
V2EventPayloadMap,
V2EventHandler,
V2Unsubscribe,
} from "./types";
interface SubscriberInfo<T extends V2EventName> {
id: string;
handler: V2EventHandler<T>;
componentId?: string;
once: boolean;
}
interface EmitOptions {
/** 병렬 실행 여부 (기본값: true) */
parallel?: boolean;
/** 타임아웃 (ms, 기본값: 5000) */
timeout?: number;
/** 실패 시 재시도 횟수 (기본값: 0) */
retryCount?: number;
}
interface EmitResult {
success: boolean;
handlerCount: number;
errors: Array<{ subscriberId: string; error: Error }>;
}
class V2EventBus {
private subscribers: Map<V2EventName, Map<string, SubscriberInfo<any>>> =
new Map();
private subscriberIdCounter = 0;
/** 디버그 모드 활성화 시 모든 이벤트 로깅 */
public debug = false;
/** 디버그용 로거 */
private log(message: string, ...args: any[]) {
if (this.debug) {
console.log(`[V2EventBus] ${message}`, ...args);
}
}
/** 에러 로거 */
private logError(message: string, error: any) {
console.error(`[V2EventBus] ${message}`, error);
}
/**
*
*
* @param eventName -
* @param handler -
* @param options -
* @returns
*
* @example
* ```typescript
* const unsubscribe = v2EventBus.subscribe(
* V2_EVENTS.TABLE_REFRESH,
* (payload) => {
* console.log("테이블 새로고침:", payload.tableName);
* },
* { componentId: "my-table" }
* );
*
* // 컴포넌트 언마운트 시
* unsubscribe();
* ```
*/
subscribe<T extends V2EventName>(
eventName: T,
handler: V2EventHandler<T>,
options?: { componentId?: string; once?: boolean }
): V2Unsubscribe {
const subscriberId = `sub_${++this.subscriberIdCounter}`;
if (!this.subscribers.has(eventName)) {
this.subscribers.set(eventName, new Map());
}
const eventSubscribers = this.subscribers.get(eventName)!;
eventSubscribers.set(subscriberId, {
id: subscriberId,
handler,
componentId: options?.componentId,
once: options?.once ?? false,
});
this.log(
`구독 등록: ${eventName} (${subscriberId})`,
options?.componentId ? `컴포넌트: ${options.componentId}` : ""
);
// 구독 해제 함수 반환
return () => {
this.unsubscribe(eventName, subscriberId);
};
}
/**
* ( )
*/
once<T extends V2EventName>(
eventName: T,
handler: V2EventHandler<T>,
options?: { componentId?: string }
): V2Unsubscribe {
return this.subscribe(eventName, handler, { ...options, once: true });
}
/**
*
*/
private unsubscribe(eventName: V2EventName, subscriberId: string): void {
const eventSubscribers = this.subscribers.get(eventName);
if (eventSubscribers) {
eventSubscribers.delete(subscriberId);
this.log(`구독 해제: ${eventName} (${subscriberId})`);
// 구독자가 없으면 Map 정리
if (eventSubscribers.size === 0) {
this.subscribers.delete(eventName);
}
}
}
/**
*
*
* @param componentId - ID
*
* @example
* ```typescript
* // useEffect cleanup에서 사용
* useEffect(() => {
* return () => {
* v2EventBus.unsubscribeByComponent("my-table-component");
* };
* }, []);
* ```
*/
unsubscribeByComponent(componentId: string): void {
let unsubscribedCount = 0;
this.subscribers.forEach((eventSubscribers, eventName) => {
eventSubscribers.forEach((subscriber, subscriberId) => {
if (subscriber.componentId === componentId) {
eventSubscribers.delete(subscriberId);
unsubscribedCount++;
}
});
// 구독자가 없으면 Map 정리
if (eventSubscribers.size === 0) {
this.subscribers.delete(eventName);
}
});
this.log(
`컴포넌트 구독 해제: ${componentId} (${unsubscribedCount}개 해제)`
);
}
/**
*
*
* @param eventName -
* @param payload -
* @param options -
* @returns
*
* @example
* ```typescript
* await v2EventBus.emit(V2_EVENTS.TABLE_REFRESH, {
* tableName: "item_info",
* target: "single",
* });
* ```
*/
async emit<T extends V2EventName>(
eventName: T,
payload: V2EventPayloadMap[T],
options: EmitOptions = {}
): Promise<EmitResult> {
const { parallel = true, timeout = 5000, retryCount = 0 } = options;
const eventSubscribers = this.subscribers.get(eventName);
if (!eventSubscribers || eventSubscribers.size === 0) {
this.log(`이벤트 발행 (구독자 없음): ${eventName}`);
return { success: true, handlerCount: 0, errors: [] };
}
this.log(`이벤트 발행: ${eventName}${eventSubscribers.size}개 구독자`);
const errors: Array<{ subscriberId: string; error: Error }> = [];
const subscribersToRemove: string[] = [];
// 핸들러 실행 함수
const executeHandler = async (
subscriber: SubscriberInfo<T>
): Promise<void> => {
const executeWithRetry = async (retriesLeft: number): Promise<void> => {
try {
// 타임아웃 적용
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(
() => reject(new Error(`Handler timeout after ${timeout}ms`)),
timeout
);
});
const handlerPromise = Promise.resolve(subscriber.handler(payload));
await Promise.race([handlerPromise, timeoutPromise]);
if (subscriber.once) {
subscribersToRemove.push(subscriber.id);
}
} catch (error) {
if (retriesLeft > 0) {
this.log(
`핸들러 재시도: ${subscriber.id} (남은 횟수: ${retriesLeft})`
);
await executeWithRetry(retriesLeft - 1);
} else {
const err =
error instanceof Error ? error : new Error(String(error));
this.logError(`핸들러 실행 실패: ${subscriber.id}`, err);
errors.push({ subscriberId: subscriber.id, error: err });
}
}
};
await executeWithRetry(retryCount);
};
// 병렬 또는 순차 실행
const subscriberArray = Array.from(eventSubscribers.values());
if (parallel) {
// 병렬 실행 (Promise.allSettled로 에러 격리)
await Promise.allSettled(
subscriberArray.map((subscriber) => executeHandler(subscriber))
);
} else {
// 순차 실행
for (const subscriber of subscriberArray) {
await executeHandler(subscriber);
}
}
// 일회성 구독자 정리
subscribersToRemove.forEach((id) => {
eventSubscribers.delete(id);
});
if (eventSubscribers.size === 0) {
this.subscribers.delete(eventName);
}
const success = errors.length === 0;
if (errors.length > 0) {
this.log(
`이벤트 완료: ${eventName} (성공: ${subscriberArray.length - errors.length}, 실패: ${errors.length})`
);
}
return {
success,
handlerCount: subscriberArray.length,
errors,
};
}
/**
* ( )
*
*
*/
emitSync<T extends V2EventName>(
eventName: T,
payload: V2EventPayloadMap[T]
): void {
this.emit(eventName, payload).catch((error) => {
this.logError(`동기 이벤트 발행 실패: ${eventName}`, error);
});
}
/**
*
*/
getSubscriberCount(eventName: V2EventName): number {
return this.subscribers.get(eventName)?.size ?? 0;
}
/**
* ()
*/
clear(): void {
this.subscribers.clear();
this.log("모든 구독 해제됨");
}
/**
* ()
*/
printState(): void {
console.log("=== V2EventBus 상태 ===");
this.subscribers.forEach((subscribers, eventName) => {
console.log(`${eventName}: ${subscribers.size}개 구독자`);
subscribers.forEach((sub) => {
console.log(` - ${sub.id} (컴포넌트: ${sub.componentId ?? "없음"})`);
});
});
console.log("======================");
}
}
// 싱글톤 인스턴스 생성 및 내보내기
export const v2EventBus = new V2EventBus();
// 개발 환경에서 window에 노출 (디버깅용)
if (typeof window !== "undefined" && process.env.NODE_ENV === "development") {
(window as any).__v2EventBus = v2EventBus;
}
// 클래스도 내보내기 (테스트용)
export { V2EventBus };