A third party ATProto appview

follow post backfill fix

Changed files
+59
server
+59
server/services/auto-backfill-follows.ts
··· 20 20 // Track ongoing backfills to prevent duplicates 21 21 const ongoingBackfills = new Set<string>(); 22 22 23 + // Track ongoing new follow backfills to prevent duplicate cascading 24 + const ongoingNewFollowBackfills = new Set<string>(); 25 + 26 + // Track recently backfilled users to prevent spam (DID -> timestamp) 27 + const recentlyBackfilledUsers = new Map<string, number>(); 28 + const NEW_FOLLOW_BACKFILL_COOLDOWN_MS = 60 * 60 * 1000; // 1 hour cooldown 29 + 23 30 export class AutoBackfillFollowsService { 31 + constructor() { 32 + // Periodically clean up old entries from recentlyBackfilledUsers to prevent memory leaks 33 + setInterval(() => { 34 + const now = Date.now(); 35 + let cleaned = 0; 36 + const entries = Array.from(recentlyBackfilledUsers.entries()); 37 + for (const [did, timestamp] of entries) { 38 + if (now - timestamp > NEW_FOLLOW_BACKFILL_COOLDOWN_MS) { 39 + recentlyBackfilledUsers.delete(did); 40 + cleaned++; 41 + } 42 + } 43 + if (cleaned > 0) { 44 + console.log( 45 + `[AUTO_BACKFILL_FOLLOWS] Cleaned ${cleaned} expired cooldown entries` 46 + ); 47 + } 48 + }, 60 * 60 * 1000); // Run every hour 49 + } 50 + 24 51 /** 25 52 * Check if a user needs follows backfilled and trigger it if needed 26 53 * Called automatically on login ··· 768 795 * Backfill posts from a single user (called when following someone new) 769 796 */ 770 797 async backfillNewFollowPosts(followedDid: string): Promise<void> { 798 + // Check if already backfilling this user 799 + if (ongoingNewFollowBackfills.has(followedDid)) { 800 + console.log( 801 + `[AUTO_BACKFILL_FOLLOWS] Already backfilling posts for ${followedDid}, skipping duplicate request` 802 + ); 803 + return; 804 + } 805 + 806 + // Check if recently backfilled (within cooldown period) 807 + const lastBackfill = recentlyBackfilledUsers.get(followedDid); 808 + if (lastBackfill) { 809 + const timeSinceBackfill = Date.now() - lastBackfill; 810 + if (timeSinceBackfill < NEW_FOLLOW_BACKFILL_COOLDOWN_MS) { 811 + const minutesRemaining = Math.ceil( 812 + (NEW_FOLLOW_BACKFILL_COOLDOWN_MS - timeSinceBackfill) / (60 * 1000) 813 + ); 814 + console.log( 815 + `[AUTO_BACKFILL_FOLLOWS] User ${followedDid} was backfilled ${Math.floor(timeSinceBackfill / 60000)} minutes ago, skipping (${minutesRemaining}m cooldown remaining)` 816 + ); 817 + return; 818 + } 819 + } 820 + 771 821 console.log( 772 822 `[AUTO_BACKFILL_FOLLOWS] Backfilling posts from newly followed user: ${followedDid}` 773 823 ); 824 + 825 + // Mark as ongoing 826 + ongoingNewFollowBackfills.add(followedDid); 774 827 775 828 try { 776 829 const eventProcessor = new EventProcessor(storage); ··· 874 927 console.log( 875 928 `[AUTO_BACKFILL_FOLLOWS] Fetched ${postsFetched} posts from newly followed user ${followedDid}` 876 929 ); 930 + 931 + // Record timestamp of successful backfill 932 + recentlyBackfilledUsers.set(followedDid, Date.now()); 877 933 } catch (error) { 878 934 console.error( 879 935 `[AUTO_BACKFILL_FOLLOWS] Error backfilling new follow posts:`, 880 936 error 881 937 ); 938 + } finally { 939 + // Always remove from ongoing set 940 + ongoingNewFollowBackfills.delete(followedDid); 882 941 } 883 942 } 884 943 }