This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

semble / src / modules / user / application / useCases / commands / UnfollowTargetUseCase.ts
4.8 kB 141 lines
1import { Result, ok, err } from '../../../../../shared/core/Result'; 2import { BaseUseCase } from '../../../../../shared/core/UseCase'; 3import { UseCaseError } from '../../../../../shared/core/UseCaseError'; 4import { AppError } from '../../../../../shared/core/AppError'; 5import { IEventPublisher } from '../../../../../shared/application/events/IEventPublisher'; 6import { IFollowsRepository } from '../../../domain/repositories/IFollowsRepository'; 7import { IFollowPublisher } from '../../ports/IFollowPublisher'; 8import { DID } from '../../../domain/value-objects/DID'; 9import { FollowTargetType } from '../../../domain/value-objects/FollowTargetType'; 10import { IProfileService } from '../../../../cards/domain/services/IProfileService'; 11 12export interface UnfollowTargetDTO { 13 followerId: string; // DID 14 targetId: string; // DID or Collection UUID 15 targetType: 'USER' | 'COLLECTION'; 16 skipUnpublish?: boolean; // For firehose events - skip unpublishing if true 17} 18 19export class ValidationError extends UseCaseError { 20 constructor(message: string) { 21 super(message); 22 } 23} 24 25export class UnfollowTargetUseCase extends BaseUseCase< 26 UnfollowTargetDTO, 27 Result<void, ValidationError | AppError.UnexpectedError> 28> { 29 constructor( 30 private followsRepository: IFollowsRepository, 31 private followPublisher: IFollowPublisher, 32 private profileService: IProfileService, 33 eventPublisher: IEventPublisher, 34 ) { 35 super(eventPublisher); 36 } 37 38 async execute( 39 request: UnfollowTargetDTO, 40 ): Promise<Result<void, ValidationError | AppError.UnexpectedError>> { 41 try { 42 // 1. Validate followerId (create DID value object) 43 const followerDidResult = DID.create(request.followerId); 44 if (followerDidResult.isErr()) { 45 return err( 46 new ValidationError( 47 `Invalid follower ID: ${followerDidResult.error.message}`, 48 ), 49 ); 50 } 51 const followerDid = followerDidResult.value; 52 53 // 2. Validate targetType 54 const targetTypeResult = FollowTargetType.create( 55 request.targetType as any, 56 ); 57 if (targetTypeResult.isErr()) { 58 return err( 59 new ValidationError( 60 `Invalid target type: ${targetTypeResult.error.message}`, 61 ), 62 ); 63 } 64 const targetType = targetTypeResult.value; 65 66 // 3. Find existing follow record 67 const existingFollowResult = 68 await this.followsRepository.findByFollowerAndTarget( 69 request.followerId, 70 request.targetId, 71 targetType, 72 ); 73 74 if (existingFollowResult.isErr()) { 75 return err(AppError.UnexpectedError.create(existingFollowResult.error)); 76 } 77 78 // 4. If not found, return success (idempotent) 79 if (!existingFollowResult.value) { 80 return ok(undefined); 81 } 82 83 const follow = existingFollowResult.value; 84 85 // 5. Unpublish from AT Protocol (skip for firehose events) 86 if (!request.skipUnpublish && follow.publishedRecordId) { 87 const unpublishResult = 88 await this.followPublisher.unpublishFollow(follow); 89 if (unpublishResult.isErr()) { 90 // Log but don't fail - we still want to delete locally 91 console.error( 92 'Failed to unpublish follow from AT Protocol:', 93 unpublishResult.error, 94 ); 95 } 96 } 97 98 // 6. Call markForRemoval() (raises UserUnfollowedTargetEvent) 99 const markResult = follow.markForRemoval(); 100 if (markResult.isErr()) { 101 return err(new ValidationError(markResult.error.message)); 102 } 103 104 // 7. Delete from repository 105 const deleteResult = await this.followsRepository.delete( 106 request.followerId, 107 request.targetId, 108 targetType, 109 ); 110 111 if (deleteResult.isErr()) { 112 return err(AppError.UnexpectedError.create(deleteResult.error)); 113 } 114 115 // 8. Publish domain events 116 const publishEventsResult = await this.publishEventsForAggregate(follow); 117 if (publishEventsResult.isErr()) { 118 console.error( 119 'Failed to publish domain events:', 120 publishEventsResult.error, 121 ); 122 // Don't fail the operation 123 } 124 125 // 9. Invalidate profile counts cache 126 // Invalidate follower's counts (followingCount decreased) 127 if (this.profileService.invalidateCounts) { 128 await this.profileService.invalidateCounts(request.followerId); 129 } 130 // Invalidate target's counts (followerCount decreased, only for USER targets) 131 if (targetType.value === 'USER' && this.profileService.invalidateCounts) { 132 await this.profileService.invalidateCounts(request.targetId); 133 } 134 135 // 10. Return success 136 return ok(undefined); 137 } catch (error) { 138 return err(AppError.UnexpectedError.create(error)); 139 } 140 } 141}