A social knowledge tool for researchers built on ATProto

feat: unify UseCase interface and BaseUseCase abstract class with event publishing support

Co-authored-by: aider (anthropic/claude-sonnet-4-20250514) <aider@aider.chat>

+80 -4
+8 -2
src/modules/feeds/application/eventHandlers/CardAddedToLibraryEventHandler.ts
··· 1 1 import { CardAddedToLibraryEvent } from '../../../cards/domain/events/CardAddedToLibraryEvent'; 2 2 import { IFeedService } from '../ports/IFeedService'; 3 + import { IEventHandler } from '../../../../shared/application/events/IEventSubscriber'; 4 + import { Result, ok, err } from '../../../../shared/core/Result'; 3 5 4 - export class CardAddedToLibraryEventHandler { 6 + export class CardAddedToLibraryEventHandler implements IEventHandler<CardAddedToLibraryEvent> { 5 7 constructor(private feedService: IFeedService) {} 6 8 7 - async handle(event: CardAddedToLibraryEvent): Promise<void> { 9 + async handle(event: CardAddedToLibraryEvent): Promise<Result<void>> { 8 10 try { 9 11 const result = await this.feedService.processCardAddedToLibrary(event); 10 12 ··· 13 15 'Error processing CardAddedToLibraryEvent in feeds:', 14 16 result.error, 15 17 ); 18 + return err(result.error); 16 19 } 20 + 21 + return ok(undefined); 17 22 } catch (error) { 18 23 console.error( 19 24 'Unexpected error handling CardAddedToLibraryEvent in feeds:', 20 25 error, 21 26 ); 27 + return err(error as Error); 22 28 } 23 29 } 24 30 }
+8 -2
src/modules/notifications/application/eventHandlers/CardAddedToLibraryEventHandler.ts
··· 1 1 import { CardAddedToLibraryEvent } from '../../../cards/domain/events/CardAddedToLibraryEvent'; 2 2 import { INotificationService } from '../ports/INotificationService'; 3 + import { IEventHandler } from '../../../../shared/application/events/IEventSubscriber'; 4 + import { Result, ok, err } from '../../../../shared/core/Result'; 3 5 4 - export class CardAddedToLibraryEventHandler { 6 + export class CardAddedToLibraryEventHandler implements IEventHandler<CardAddedToLibraryEvent> { 5 7 constructor(private notificationService: INotificationService) {} 6 8 7 - async handle(event: CardAddedToLibraryEvent): Promise<void> { 9 + async handle(event: CardAddedToLibraryEvent): Promise<Result<void>> { 8 10 try { 9 11 const result = await this.notificationService.processCardAddedToLibrary( 10 12 event, ··· 15 17 'Error processing CardAddedToLibraryEvent in notifications:', 16 18 result.error, 17 19 ); 20 + return err(result.error); 18 21 } 22 + 23 + return ok(undefined); 19 24 } catch (error) { 20 25 console.error( 21 26 'Unexpected error handling CardAddedToLibraryEvent in notifications:', 22 27 error, 23 28 ); 29 + return err(error as Error); 24 30 } 25 31 } 26 32 }
+6
src/shared/application/events/IEventPublisher.ts
··· 1 + import { IDomainEvent } from '../../domain/events/IDomainEvent'; 2 + import { Result } from '../../core/Result'; 3 + 4 + export interface IEventPublisher { 5 + publishEvents(events: IDomainEvent[]): Promise<Result<void>>; 6 + }
+16
src/shared/application/events/IEventSubscriber.ts
··· 1 + import { IDomainEvent } from '../../domain/events/IDomainEvent'; 2 + import { Result } from '../../core/Result'; 3 + 4 + export interface IEventHandler<T extends IDomainEvent> { 5 + handle(event: T): Promise<Result<void>>; 6 + } 7 + 8 + export interface IEventSubscriber { 9 + subscribe<T extends IDomainEvent>( 10 + eventType: string, 11 + handler: IEventHandler<T> 12 + ): Promise<void>; 13 + 14 + start(): Promise<void>; 15 + stop(): Promise<void>; 16 + }
+29
src/shared/core/UseCase.ts
··· 1 + import { IEventPublisher } from '../application/events/IEventPublisher'; 2 + import { DomainEvents } from '../domain/events/DomainEvents'; 3 + import { AggregateRoot } from '../domain/AggregateRoot'; 4 + import { Result, ok } from './Result'; 5 + 1 6 export interface UseCase<IRequest, IResponse> { 2 7 execute(request?: IRequest): Promise<IResponse> | IResponse; 3 8 } 9 + 10 + export abstract class BaseUseCase<IRequest, IResponse> implements UseCase<IRequest, IResponse> { 11 + constructor(protected eventPublisher: IEventPublisher) {} 12 + 13 + abstract execute(request?: IRequest): Promise<IResponse> | IResponse; 14 + 15 + protected async publishEventsForAggregate( 16 + aggregate: AggregateRoot<any> 17 + ): Promise<Result<void>> { 18 + const events = DomainEvents.getEventsForAggregate(aggregate.id); 19 + 20 + if (events.length === 0) { 21 + return ok(undefined); 22 + } 23 + 24 + const publishResult = await this.eventPublisher.publishEvents(events); 25 + 26 + if (publishResult.isOk()) { 27 + DomainEvents.clearEventsForAggregate(aggregate.id); 28 + } 29 + 30 + return publishResult; 31 + } 32 + }
+13
src/shared/domain/events/DomainEvents.ts
··· 66 66 } 67 67 } 68 68 69 + public static getEventsForAggregate(id: UniqueEntityID): IDomainEvent[] { 70 + const aggregate = this.findMarkedAggregateByID(id); 71 + return aggregate ? [...aggregate.domainEvents] : []; 72 + } 73 + 74 + public static clearEventsForAggregate(id: UniqueEntityID): void { 75 + const aggregate = this.findMarkedAggregateByID(id); 76 + if (aggregate) { 77 + aggregate.clearEvents(); 78 + this.removeAggregateFromMarkedDispatchList(aggregate); 79 + } 80 + } 81 + 69 82 public static register( 70 83 callback: (event: IDomainEvent) => void, 71 84 eventClassName: string,