This repository has no description
0

Configure Feed

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

semble / src / modules / atproto / infrastructure / services / DrizzleFirehoseEventDuplicationService.ts
4.1 kB 119 lines
1import { eq, and } from 'drizzle-orm'; 2import { PostgresJsDatabase } from 'drizzle-orm/postgres-js'; 3import { 4 IFirehoseEventDuplicationService, 5 FirehoseEventType, 6} from '../../domain/services/IFirehoseEventDuplicationService'; 7import { IAtUriResolutionService } from '../../../cards/domain/services/IAtUriResolutionService'; 8import { ATUri } from '../../domain/ATUri'; 9import { publishedRecords } from '../../../cards/infrastructure/repositories/schema/publishedRecord.sql'; 10import { Result, ok, err } from 'src/shared/core/Result'; 11import { EnvironmentConfigService } from 'src/shared/infrastructure/config/EnvironmentConfigService'; 12 13export class DrizzleFirehoseEventDuplicationService 14 implements IFirehoseEventDuplicationService 15{ 16 constructor( 17 private db: PostgresJsDatabase, 18 private atUriResolver: IAtUriResolutionService, 19 private configService: EnvironmentConfigService, 20 ) {} 21 22 async hasEventBeenProcessed( 23 atUri: string, 24 cid: string | null, 25 operation: FirehoseEventType, 26 ): Promise<Result<boolean>> { 27 try { 28 // For CREATE/UPDATE: check if (uri, cid) exists 29 if (operation === 'create' || operation === 'update') { 30 if (!cid) return ok(false); 31 32 const result = await this.db 33 .select({ id: publishedRecords.id }) 34 .from(publishedRecords) 35 .where( 36 and(eq(publishedRecords.uri, atUri), eq(publishedRecords.cid, cid)), 37 ) 38 .limit(1); 39 40 return ok(result.length > 0); 41 } 42 43 // For DELETE: use more complex logic 44 return this.hasBeenDeleted(atUri); 45 } catch (error) { 46 return err(error as Error); 47 } 48 } 49 50 async hasBeenDeleted(atUri: string): Promise<Result<boolean>> { 51 try { 52 // 1. Find all publishedRecords with matching URI 53 const records = await this.db 54 .select() 55 .from(publishedRecords) 56 .where(eq(publishedRecords.uri, atUri)); 57 58 if (records.length === 0) { 59 return ok(true); // No records = was deleted 60 } 61 62 // 2. Parse AT URI to get collection 63 const atUriResult = ATUri.create(atUri); 64 if (atUriResult.isErr()) { 65 return err(atUriResult.error); 66 } 67 68 const collection = atUriResult.value.collection; 69 const collections = this.configService.getAtProtoCollections(); 70 71 // 3. Check if entity still exists based on collection type 72 switch (collection) { 73 case collections.collection: 74 case collections.marginCollection: { 75 const collectionIdResult = 76 await this.atUriResolver.resolveCollectionId(atUri); 77 if (collectionIdResult.isErr()) { 78 return err(collectionIdResult.error); 79 } 80 return ok(collectionIdResult.value === null); 81 } 82 case collections.card: 83 case collections.marginBookmark: { 84 const cardIdResult = await this.atUriResolver.resolveCardId(atUri); 85 if (cardIdResult.isErr()) { 86 return err(cardIdResult.error); 87 } 88 return ok(cardIdResult.value === null); 89 } 90 case collections.collectionLink: 91 case collections.marginCollectionItem: { 92 const linkInfoResult = 93 await this.atUriResolver.resolveCollectionLinkId(atUri); 94 if (linkInfoResult.isErr()) { 95 return err(linkInfoResult.error); 96 } 97 return ok(linkInfoResult.value === null); 98 } 99 case collections.collectionLinkRemoval: { 100 // For removal records, we track them in published_records table 101 // If a record exists, it hasn't been deleted 102 return ok(records.length === 0); 103 } 104 case collections.follow: { 105 const followInfoResult = 106 await this.atUriResolver.resolveFollowId(atUri); 107 if (followInfoResult.isErr()) { 108 return err(followInfoResult.error); 109 } 110 return ok(followInfoResult.value === null); 111 } 112 default: 113 return err(new Error(`Unknown collection type: ${collection}`)); 114 } 115 } catch (error) { 116 return err(error as Error); 117 } 118 } 119}