hacker news alerts in slack (incessant pings if you make front page)
at space-optimization 254 lines 6.8 kB view raw
1export interface User { 2 id: string; 3 created: number; 4 karma: number; 5 about?: string; 6 submitted?: number[]; 7} 8 9export interface Story { 10 id: number; 11 by: string; 12 title: string; 13 url?: string; 14 text?: string; 15 time: number; 16 score: number; 17 descendants: number; 18 type: "story" | "job" | "comment" | "poll" | "pollopt"; 19 kids?: number[]; 20} 21 22export interface Comment { 23 id: number; 24 by: string; 25 text: string; 26 time: number; 27 parent: number; 28 type: "comment"; 29 kids?: number[]; 30} 31 32export type HNItem = Story | Comment; 33 34/** 35 * Fetches user data by user ID from the Hacker News API. 36 * Only users with public activity (comments or story submissions) are available. 37 * 38 * @param userId - The user's unique username (case-sensitive) 39 * @returns Promise resolving to the user data 40 * @throws Error if the user cannot be found or if there's a network error 41 */ 42export async function getUser(userId: string): Promise<User> { 43 if (!userId) { 44 throw new Error("User ID is required"); 45 } 46 47 try { 48 const response = await fetch( 49 `https://hacker-news.firebaseio.com/v0/user/${userId}.json`, 50 ); 51 52 if (!response.ok) { 53 throw new Error( 54 `Failed to fetch user with ID ${userId}: ${response.statusText}`, 55 ); 56 } 57 58 const userData = await response.json(); 59 60 if (!userData) { 61 throw new Error(`User with ID ${userId} not found`); 62 } 63 64 return userData as User; 65 } catch (error) { 66 if (error instanceof Error) { 67 throw error; 68 } 69 throw new Error(`Failed to fetch user with ID ${userId}: ${String(error)}`); 70 } 71} 72 73/** 74 * Fetches the list of newest story IDs from Hacker News. 75 * 76 * @returns Promise resolving to an array of story IDs 77 */ 78export async function getNewStories(): Promise<number[]> { 79 try { 80 console.log("getNewStories: Fetching new story IDs from HackerNews API..."); 81 82 const response = await fetch( 83 "https://hacker-news.firebaseio.com/v0/newstories.json", 84 ); 85 86 if (!response.ok) { 87 throw new Error(`Failed to fetch new stories: ${response.statusText}`); 88 } 89 90 const stories = (await response.json()) as number[]; 91 console.log( 92 `getNewStories: Successfully fetched ${stories.length} story IDs`, 93 ); 94 95 return stories; 96 } catch (error) { 97 if (error instanceof Error) { 98 throw error; 99 } 100 throw new Error(`Failed to fetch new stories: ${String(error)}`); 101 } 102} 103 104/** 105 * Fetches the list of top story IDs from Hacker News. 106 * 107 * @returns Promise resolving to an array of story IDs 108 */ 109export async function getTopStories(): Promise<number[]> { 110 try { 111 const response = await fetch( 112 "https://hacker-news.firebaseio.com/v0/topstories.json", 113 ); 114 115 if (!response.ok) { 116 throw new Error(`Failed to fetch top stories: ${response.statusText}`); 117 } 118 119 return (await response.json()) as number[]; 120 } catch (error) { 121 if (error instanceof Error) { 122 throw error; 123 } 124 throw new Error(`Failed to fetch top stories: ${String(error)}`); 125 } 126} 127 128/** 129 * Fetches an item (story, comment, etc) from Hacker News by its ID. 130 * 131 * @param itemId - The unique ID of the item to fetch 132 * @returns Promise resolving to the item data or null if not found 133 */ 134export async function getItem<T extends HNItem>( 135 itemId: number, 136): Promise<T | null> { 137 try { 138 // Uncomment for detailed debugging of individual item fetches 139 // console.log(`getItem: Fetching item ${itemId}...`); 140 141 const response = await fetch( 142 `https://hacker-news.firebaseio.com/v0/item/${itemId}.json`, 143 ); 144 145 if (!response.ok) { 146 throw new Error(`Failed to fetch item ${itemId}: ${response.statusText}`); 147 } 148 149 const item = await response.json(); 150 151 // Uncomment for detailed debugging of individual item results 152 // if (item) { 153 // console.log(`getItem: Successfully fetched item ${itemId} (type: ${item.type})`); 154 // } else { 155 // console.log(`getItem: Item ${itemId} not found (null response)`); 156 // } 157 158 return item ? (item as T) : null; 159 } catch (error) { 160 if (error instanceof Error) { 161 throw error; 162 } 163 throw new Error(`Failed to fetch item ${itemId}: ${String(error)}`); 164 } 165} 166 167/** 168 * Fetches multiple items (stories, comments, etc.) from Hacker News by their IDs. 169 * Uses parallel requests for better performance. 170 * 171 * @param itemIds - Array of item IDs to fetch 172 * @param limit - Optional limit on number of items to fetch 173 * @param batchSize - Optional batch size for parallel requests (default: 20) 174 * @returns Promise resolving to an array of successfully fetched items 175 */ 176export async function getItems<T extends HNItem>( 177 itemIds: number[], 178 limit?: number, 179 batchSize = 20, 180): Promise<T[]> { 181 const ids = limit ? itemIds.slice(0, limit) : itemIds; 182 183 console.log( 184 `getItems: Fetching ${ids.length} items from HackerNews API in parallel...`, 185 ); 186 let successCount = 0; 187 let errorCount = 0; 188 189 const items: T[] = []; 190 191 // Process in batches to avoid overwhelming the API 192 for (let i = 0; i < ids.length; i += batchSize) { 193 const batchIds = ids.slice(i, i + batchSize); 194 console.log( 195 `getItems: Processing batch ${Math.floor(i / batchSize) + 1}/${Math.ceil(ids.length / batchSize)} (${batchIds.length} items)`, 196 ); 197 198 // Create an array of promises for this batch 199 const batchPromises = batchIds.map((id) => 200 getItem<T>(id) 201 .then((item) => { 202 if (item !== null) { 203 successCount++; 204 return item; 205 } 206 return null; 207 }) 208 .catch((error) => { 209 console.error(`Failed to fetch item ${id}:`, error); 210 errorCount++; 211 return null; 212 }), 213 ); 214 215 // Wait for all promises in this batch to resolve 216 const batchResults = await Promise.all(batchPromises); 217 218 // Add non-null results to our items array 219 items.push( 220 ...batchResults.filter( 221 (item): item is NonNullable<typeof item> => item !== null, 222 ), 223 ); 224 225 console.log( 226 `getItems: Batch complete - ${successCount} successful, ${errorCount} failed so far`, 227 ); 228 } 229 230 console.log( 231 `getItems: Completed fetching items - ${successCount} successful, ${errorCount} failed, ${items.length} total items returned`, 232 ); 233 return items; 234} 235 236/** 237 * Generates a URL to a Hacker News item. 238 * 239 * @param itemId - The ID of the item 240 * @returns The URL to the item on Hacker News 241 */ 242export function getItemUrl(itemId: number): string { 243 return `https://news.ycombinator.com/item?id=${itemId}`; 244} 245 246/** 247 * Generates a URL to a Hacker News user profile. 248 * 249 * @param username - The username of the user 250 * @returns The URL to the user's profile on Hacker News 251 */ 252export function getUserProfileUrl(username: string): string { 253 return `https://news.ycombinator.com/user?id=${username}`; 254}