|
| 1 | +/** |
| 2 | + * Professional Example: Concurrent Data Fetching in TypeScript |
| 3 | + * |
| 4 | + * This module demonstrates best practices for fetching multiple independent |
| 5 | + * resources concurrently using `Promise.all` and `Promise.allSettled`. |
| 6 | + * Concurrent fetching significantly improves performance compared to |
| 7 | + * sequential fetching when requests do not depend on each other. |
| 8 | + */ |
| 9 | + |
| 10 | +import { Semaphore } from './Semaphore' |
| 11 | + |
| 12 | +// --- Type Definitions --- |
| 13 | + |
| 14 | +export interface User { |
| 15 | + id: number |
| 16 | + name: string |
| 17 | + email: string |
| 18 | +} |
| 19 | + |
| 20 | +export interface Post { |
| 21 | + id: number |
| 22 | + userId: number |
| 23 | + title: string |
| 24 | + body: string |
| 25 | +} |
| 26 | + |
| 27 | +export interface Comment { |
| 28 | + id: number |
| 29 | + postId: number |
| 30 | + name: string |
| 31 | + body: string |
| 32 | +} |
| 33 | + |
| 34 | +// --- Mock API Service --- |
| 35 | +// In a real application, these would be actual fetch calls (e.g., using native fetch or axios). |
| 36 | + |
| 37 | +const api = { |
| 38 | + async fetchUsers(): Promise<User[]> { |
| 39 | + // Simulating network latency |
| 40 | + await new Promise((resolve) => setTimeout(resolve, 500)) |
| 41 | + return [ |
| 42 | + { id: 1, name: 'Alice Smith', email: 'alice@example.com' }, |
| 43 | + { id: 2, name: 'Bob Jones', email: 'bob@example.com' } |
| 44 | + ] |
| 45 | + }, |
| 46 | + |
| 47 | + async fetchPosts(): Promise<Post[]> { |
| 48 | + await new Promise((resolve) => setTimeout(resolve, 800)) |
| 49 | + return [ |
| 50 | + { |
| 51 | + id: 101, |
| 52 | + userId: 1, |
| 53 | + title: 'TypeScript Tips', |
| 54 | + body: 'Use strict mode.' |
| 55 | + }, |
| 56 | + { |
| 57 | + id: 102, |
| 58 | + userId: 2, |
| 59 | + title: 'Async/Await', |
| 60 | + body: 'Makes promises easier to read.' |
| 61 | + } |
| 62 | + ] |
| 63 | + }, |
| 64 | + |
| 65 | + async fetchComments(): Promise<Comment[]> { |
| 66 | + await new Promise((resolve) => setTimeout(resolve, 300)) |
| 67 | + // Simulating a potential network error for demonstration purposes |
| 68 | + if (Math.random() < 0.1) { |
| 69 | + throw new Error('Network Error') |
| 70 | + } |
| 71 | + return [{ id: 1001, postId: 101, name: 'Charlie', body: 'Great tips!' }] |
| 72 | + } |
| 73 | +} |
| 74 | + |
| 75 | +// --- Concurrent Fetching Implementations --- |
| 76 | + |
| 77 | +/** |
| 78 | + * Example 1: Using Promise.all |
| 79 | + * |
| 80 | + * Best when you need ALL requests to succeed. If any single promise rejects, |
| 81 | + * the entire Promise.all rejects immediately (fail-fast behavior). |
| 82 | + */ |
| 83 | +export async function fetchAllDataStrict(): Promise<{ |
| 84 | + users: User[] |
| 85 | + posts: Post[] |
| 86 | + comments: Comment[] |
| 87 | +}> { |
| 88 | + try { |
| 89 | + console.log('Starting concurrent fetch (Strict)...') |
| 90 | + const startTime = Date.now() |
| 91 | + |
| 92 | + // The requests are initiated concurrently. |
| 93 | + // We await the resolution of all promises together. |
| 94 | + const [users, posts, comments] = await Promise.all([ |
| 95 | + api.fetchUsers(), |
| 96 | + api.fetchPosts(), |
| 97 | + api.fetchComments() |
| 98 | + ]) |
| 99 | + |
| 100 | + const duration = Date.now() - startTime |
| 101 | + console.log(`Successfully fetched all data in ${duration}ms`) |
| 102 | + |
| 103 | + return { users, posts, comments } |
| 104 | + } catch (error) { |
| 105 | + // If ANY of the fetches fail, we catch the error here. |
| 106 | + console.error('Critical failure during concurrent data fetch:', error) |
| 107 | + throw new Error('Failed to load application data. Please try again later.') |
| 108 | + } |
| 109 | +} |
| 110 | + |
| 111 | +/** |
| 112 | + * Example 2: Using Promise.allSettled |
| 113 | + * |
| 114 | + * Best when requests are independent and you want to handle successes and |
| 115 | + * failures individually without failing the entire operation if one request fails. |
| 116 | + */ |
| 117 | +export async function fetchDataResiliently(): Promise<void> { |
| 118 | + console.log('\nStarting concurrent fetch (Resilient)...') |
| 119 | + const startTime = Date.now() |
| 120 | + |
| 121 | + const results = await Promise.allSettled([ |
| 122 | + api.fetchUsers(), |
| 123 | + api.fetchPosts(), |
| 124 | + api.fetchComments() |
| 125 | + ]) |
| 126 | + |
| 127 | + const duration = Date.now() - startTime |
| 128 | + console.log(`Finished resilient fetch in ${duration}ms`) |
| 129 | + |
| 130 | + // Process results safely using type narrowing |
| 131 | + const [usersResult, postsResult, commentsResult] = results |
| 132 | + |
| 133 | + if (usersResult.status === 'fulfilled') { |
| 134 | + console.log(`✅ Loaded ${usersResult.value.length} users.`) |
| 135 | + } else { |
| 136 | + console.error(`❌ Failed to load users:`, usersResult.reason) |
| 137 | + } |
| 138 | + |
| 139 | + if (postsResult.status === 'fulfilled') { |
| 140 | + console.log(`✅ Loaded ${postsResult.value.length} posts.`) |
| 141 | + } else { |
| 142 | + console.error(`❌ Failed to load posts:`, postsResult.reason) |
| 143 | + } |
| 144 | + |
| 145 | + if (commentsResult.status === 'fulfilled') { |
| 146 | + console.log(`✅ Loaded ${commentsResult.value.length} comments.`) |
| 147 | + } else { |
| 148 | + console.warn( |
| 149 | + `⚠️ Comments could not be loaded, continuing without them:`, |
| 150 | + commentsResult.reason |
| 151 | + ) |
| 152 | + } |
| 153 | +} |
| 154 | + |
| 155 | +/** |
| 156 | + * Example 3: Rate-Limited Concurrent Fetching (Using Semaphore) |
| 157 | + * |
| 158 | + * Executes multiple async tasks with a limit on concurrency. |
| 159 | + * This is CRITICAL for bulk fetching to avoid rate-limiting, network congestion, |
| 160 | + * or overwhelming the server/database. |
| 161 | + * |
| 162 | + * @param tasks Array of functions that return Promises. |
| 163 | + * @param limit The maximum number of concurrent executions. |
| 164 | + */ |
| 165 | +export async function concurrentFetch<T>( |
| 166 | + tasks: (() => Promise<T>)[], |
| 167 | + limit: number |
| 168 | +): Promise<T[]> { |
| 169 | + const semaphore = new Semaphore(limit) |
| 170 | + return Promise.all(tasks.map((task) => semaphore.run(task))) |
| 171 | +} |
| 172 | + |
| 173 | +/** |
| 174 | + * Demonstrates bulk fetching with concurrency limits. |
| 175 | + */ |
| 176 | +export async function fetchBulkDataWithLimits(): Promise<void> { |
| 177 | + console.log('\nStarting rate-limited bulk fetch (Max 3 concurrent)...') |
| 178 | + const startTime = Date.now() |
| 179 | + |
| 180 | + // Create 10 dummy tasks that take 200ms each |
| 181 | + const tasks = Array.from({ length: 10 }, (_, i) => async () => { |
| 182 | + await new Promise((resolve) => setTimeout(resolve, 200)) |
| 183 | + console.log(`Task ${i + 1} completed.`) |
| 184 | + return i + 1 |
| 185 | + }) |
| 186 | + |
| 187 | + // Limit to 3 concurrent requests at any given time |
| 188 | + await concurrentFetch(tasks, 3) |
| 189 | + |
| 190 | + const duration = Date.now() - startTime |
| 191 | + console.log(`Finished rate-limited fetch in ${duration}ms.`) |
| 192 | +} |
| 193 | + |
| 194 | +// --- Execution --- |
| 195 | +// If you want to test this file, you can uncomment the lines below: |
| 196 | +// async function runExamples() { |
| 197 | +// try { |
| 198 | +// await fetchAllDataStrict(); |
| 199 | +// await fetchDataResiliently(); |
| 200 | +// await fetchBulkDataWithLimits(); |
| 201 | +// } catch (err) { |
| 202 | +// console.error("Application encountered a top-level error.", err); |
| 203 | +// } |
| 204 | +// } |
| 205 | +// runExamples(); |
0 commit comments