javascript

exercises

exercises.js
// ============================================
// 18.1 Promise Combinators - Exercises
// ============================================

// Helper function
function delay(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

// Exercise 1: Basic Promise.all
// Fetch three users in parallel and return their names

async function fetchUser(id) {
  await delay(100);
  return { id, name: `User ${id}` };
}

// Your code here:
// async function getAllUserNames(ids) {
//     ...
// }

// Test: getAllUserNames([1, 2, 3]).then(console.log);
// Expected: ['User 1', 'User 2', 'User 3']

/*
Solution:
async function getAllUserNames(ids) {
    const users = await Promise.all(ids.map(id => fetchUser(id)));
    return users.map(user => user.name);
}

getAllUserNames([1, 2, 3]).then(console.log);
*/

// --------------------------------------------

// Exercise 2: Promise.all with Error Handling
// Fetch users but catch errors and return null for failed fetches

async function fetchUserMayFail(id) {
  await delay(50);
  if (id === 2) throw new Error('User not found');
  return { id, name: `User ${id}` };
}

// Your code here:
// async function getAllUsersSafe(ids) {
//     // Should return [user1, null, user3] if user 2 fails
// }

/*
Solution:
async function getAllUsersSafe(ids) {
    const promises = ids.map(async id => {
        try {
            return await fetchUserMayFail(id);
        } catch {
            return null;
        }
    });
    return Promise.all(promises);
}

getAllUsersSafe([1, 2, 3]).then(console.log);
// [{ id: 1, name: 'User 1' }, null, { id: 3, name: 'User 3' }]
*/

// --------------------------------------------

// Exercise 3: Using Promise.allSettled
// Fetch multiple URLs and return { successful: [...], failed: [...] }

async function fetchUrl(url) {
  await delay(50);
  if (url.includes('bad')) throw new Error(`Failed: ${url}`);
  return { url, content: 'data' };
}

// Your code here:
// async function fetchAllUrls(urls) {
//     // Return { successful: [...results], failed: [...errors] }
// }

// Test:
// fetchAllUrls(['/good1', '/bad', '/good2']).then(console.log);

/*
Solution:
async function fetchAllUrls(urls) {
    const results = await Promise.allSettled(urls.map(url => fetchUrl(url)));
    
    return {
        successful: results
            .filter(r => r.status === 'fulfilled')
            .map(r => r.value),
        failed: results
            .filter(r => r.status === 'rejected')
            .map(r => r.reason.message)
    };
}

fetchAllUrls(['/good1', '/bad', '/good2']).then(console.log);
*/

// --------------------------------------------

// Exercise 4: Implement Timeout using Promise.race
// Create a function that rejects if operation takes too long

// Your code here:
// function withTimeout(promise, ms) {
//     ...
// }

// Test:
// withTimeout(delay(100).then(() => 'fast'), 500).then(console.log);  // 'fast'
// withTimeout(delay(1000).then(() => 'slow'), 500).catch(console.log); // Error: Timeout

/*
Solution:
function withTimeout(promise, ms) {
    const timeout = new Promise((_, reject) => {
        setTimeout(() => reject(new Error('Timeout')), ms);
    });
    return Promise.race([promise, timeout]);
}

// Tests:
withTimeout(delay(100).then(() => 'fast'), 500)
    .then(console.log)  // 'fast'
    .catch(console.log);

withTimeout(delay(1000).then(() => 'slow'), 500)
    .then(console.log)
    .catch(e => console.log(e.message));  // 'Timeout'
*/

// --------------------------------------------

// Exercise 5: First Successful with Promise.any
// Try multiple servers and return first successful response

async function fetchFromServer(server) {
  await delay(Math.random() * 200);
  if (server === 'server1') throw new Error('Server 1 down');
  return { server, data: 'content' };
}

// Your code here:
// async function fetchFromAnyServer(servers) {
//     ...
// }

/*
Solution:
async function fetchFromAnyServer(servers) {
    try {
        return await Promise.any(servers.map(s => fetchFromServer(s)));
    } catch (error) {
        throw new Error('All servers failed');
    }
}

fetchFromAnyServer(['server1', 'server2', 'server3']).then(console.log);
*/

// --------------------------------------------

// Exercise 6: Parallel with Concurrency Limit
// Execute promises in parallel but limit concurrent executions

// Your code here:
// async function parallelLimit(tasks, limit) {
//     // tasks is array of functions that return promises
//     // limit is max concurrent executions
// }

// Test:
// const tasks = [
//     () => delay(100).then(() => 1),
//     () => delay(100).then(() => 2),
//     () => delay(100).then(() => 3),
//     () => delay(100).then(() => 4),
// ];
// parallelLimit(tasks, 2).then(console.log);  // [1, 2, 3, 4]

/*
Solution:
async function parallelLimit(tasks, limit) {
    const results = [];
    const executing = [];
    
    for (const [index, task] of tasks.entries()) {
        const promise = task().then(result => {
            results[index] = result;
            executing.splice(executing.indexOf(promise), 1);
        });
        
        executing.push(promise);
        
        if (executing.length >= limit) {
            await Promise.race(executing);
        }
    }
    
    await Promise.all(executing);
    return results;
}

const tasks = [
    () => delay(100).then(() => 1),
    () => delay(100).then(() => 2),
    () => delay(100).then(() => 3),
    () => delay(100).then(() => 4),
];
parallelLimit(tasks, 2).then(console.log);  // [1, 2, 3, 4]
*/

// --------------------------------------------

// Exercise 7: Promise.all with Progress
// Execute promises and report progress

// Your code here:
// async function allWithProgress(promises, onProgress) {
//     // onProgress(completed, total) called after each completion
// }

/*
Solution:
async function allWithProgress(promises, onProgress) {
    let completed = 0;
    const total = promises.length;
    
    const wrapped = promises.map(p =>
        p.then(result => {
            completed++;
            onProgress(completed, total);
            return result;
        })
    );
    
    return Promise.all(wrapped);
}

const promises = [
    delay(100).then(() => 'a'),
    delay(200).then(() => 'b'),
    delay(150).then(() => 'c')
];

allWithProgress(promises, (done, total) => {
    console.log(`Progress: ${done}/${total}`);
}).then(console.log);
*/

// --------------------------------------------

// Exercise 8: Racing with Cleanup
// Race promises but cancel/cleanup losers

// Your code here:
// async function raceWithCleanup(entries) {
//     // entries: [{ promise, cleanup: () => void }, ...]
// }

/*
Solution:
async function raceWithCleanup(entries) {
    const promises = entries.map((entry, index) =>
        entry.promise.then(value => ({ value, index }))
    );
    
    const winner = await Promise.race(promises);
    
    // Cleanup losers
    entries.forEach((entry, index) => {
        if (index !== winner.index && entry.cleanup) {
            entry.cleanup();
        }
    });
    
    return winner.value;
}

// Usage example:
// raceWithCleanup([
//     { promise: delay(100).then(() => 'A'), cleanup: () => console.log('Cleanup A') },
//     { promise: delay(50).then(() => 'B'), cleanup: () => console.log('Cleanup B') }
// ]).then(console.log);  // 'B', then 'Cleanup A'
*/

// --------------------------------------------

// Exercise 9: Retry with Promise.any
// Try operation multiple times in parallel

// Your code here:
// function retryParallel(fn, attempts) {
//     // Run fn() 'attempts' times in parallel, return first success
// }

/*
Solution:
function retryParallel(fn, attempts) {
    const promises = Array.from({ length: attempts }, (_, i) =>
        fn(i).catch(error => {
            throw new Error(`Attempt ${i + 1}: ${error.message}`);
        })
    );
    
    return Promise.any(promises);
}

// Example:
let attemptCount = 0;
const unreliable = () => {
    attemptCount++;
    return delay(50).then(() => {
        if (attemptCount < 3) throw new Error('Failed');
        return 'Success!';
    });
};

// retryParallel(unreliable, 5).then(console.log);  // 'Success!'
*/

// --------------------------------------------

// Exercise 10: Batch Processing with allSettled
// Process items in batches, collect all results

// Your code here:
// async function processBatches(items, batchSize, processFn) {
//     // Process in batches, return all results (success and failures)
// }

/*
Solution:
async function processBatches(items, batchSize, processFn) {
    const results = [];
    
    for (let i = 0; i < items.length; i += batchSize) {
        const batch = items.slice(i, i + batchSize);
        const batchResults = await Promise.allSettled(
            batch.map(item => processFn(item))
        );
        results.push(...batchResults);
    }
    
    return {
        successful: results.filter(r => r.status === 'fulfilled').map(r => r.value),
        failed: results.filter(r => r.status === 'rejected').map(r => r.reason)
    };
}

const items = [1, 2, 3, 4, 5, 6, 7, 8];
const process = async (n) => {
    await delay(50);
    if (n % 3 === 0) throw new Error(`Failed on ${n}`);
    return n * 2;
};

processBatches(items, 3, process).then(console.log);
*/

// --------------------------------------------

// Exercise 11: Sequential Fallback with any
// Try sources in order of preference, fall back to next

// Your code here:
// async function fetchWithFallbacks(sources) {
//     // Try each source, use Promise.any but prefer earlier sources
// }

/*
Solution:
async function fetchWithFallbacks(sources) {
    // Add delay to give preference to earlier sources
    const promises = sources.map((source, index) =>
        delay(index * 50).then(() => source.fetch())
    );
    
    return Promise.any(promises);
}

const sources = [
    { name: 'primary', fetch: async () => { throw new Error('Down'); } },
    { name: 'secondary', fetch: async () => ({ from: 'secondary', data: 'content' }) },
    { name: 'tertiary', fetch: async () => ({ from: 'tertiary', data: 'content' }) }
];

// fetchWithFallbacks(sources).then(console.log);
*/

// --------------------------------------------

// Exercise 12: Implement Promise.allSettled Polyfill
// Create your own version of Promise.allSettled

// Your code here:
// function myAllSettled(promises) {
//     ...
// }

/*
Solution:
function myAllSettled(promises) {
    return Promise.all(
        promises.map(p =>
            Promise.resolve(p)
                .then(value => ({ status: 'fulfilled', value }))
                .catch(reason => ({ status: 'rejected', reason }))
        )
    );
}

myAllSettled([
    Promise.resolve(1),
    Promise.reject(new Error('fail')),
    Promise.resolve(3)
]).then(console.log);
*/

// --------------------------------------------

// Exercise 13: Implement Promise.any Polyfill
// Create your own version of Promise.any

// Your code here:
// function myAny(promises) {
//     ...
// }

/*
Solution:
function myAny(promises) {
    return new Promise((resolve, reject) => {
        const errors = [];
        let pending = promises.length;
        
        if (pending === 0) {
            reject(new AggregateError([], 'All promises were rejected'));
            return;
        }
        
        promises.forEach((p, index) => {
            Promise.resolve(p)
                .then(resolve)
                .catch(error => {
                    errors[index] = error;
                    pending--;
                    if (pending === 0) {
                        reject(new AggregateError(errors, 'All promises were rejected'));
                    }
                });
        });
    });
}

myAny([
    Promise.reject(1),
    delay(100).then(() => 'success'),
    Promise.reject(3)
]).then(console.log);  // 'success'
*/

// --------------------------------------------

// Exercise 14: Load Balancer
// Distribute requests across servers based on response time

// Your code here:
// class LoadBalancer {
//     constructor(servers) { ... }
//     async request(data) { ... }
// }

/*
Solution:
class LoadBalancer {
    constructor(servers) {
        this.servers = servers.map(s => ({
            url: s,
            avgLatency: 0,
            requests: 0
        }));
    }
    
    async request(data) {
        // Race all servers, track latency
        const start = Date.now();
        
        const result = await Promise.any(
            this.servers.map(async server => {
                const response = await this.fetchFromServer(server.url, data);
                const latency = Date.now() - start;
                
                // Update stats
                server.avgLatency = (server.avgLatency * server.requests + latency) / (server.requests + 1);
                server.requests++;
                
                return { response, server: server.url, latency };
            })
        );
        
        return result;
    }
    
    async fetchFromServer(url, data) {
        await delay(Math.random() * 200);
        return { url, data: 'response' };
    }
    
    getStats() {
        return this.servers.map(s => ({
            url: s.url,
            avgLatency: s.avgLatency.toFixed(2),
            requests: s.requests
        }));
    }
}

// const lb = new LoadBalancer(['server1', 'server2', 'server3']);
// lb.request({}).then(console.log);
*/

// --------------------------------------------

// Exercise 15: Coordinated Async Operations
// Wait for setup tasks before running main tasks

// Your code here:
// async function coordinatedExecution(setupTasks, mainTasks) {
//     // Run all setup tasks first (all must succeed)
//     // Then run main tasks in parallel
//     // Return { setupResults, mainResults }
// }

/*
Solution:
async function coordinatedExecution(setupTasks, mainTasks) {
    // Setup phase - all must succeed
    const setupResults = await Promise.all(
        setupTasks.map(task => task())
    );
    
    console.log('Setup complete, running main tasks...');
    
    // Main phase - collect all results
    const mainResults = await Promise.allSettled(
        mainTasks.map(task => task())
    );
    
    return {
        setupResults,
        mainResults: {
            successful: mainResults.filter(r => r.status === 'fulfilled').map(r => r.value),
            failed: mainResults.filter(r => r.status === 'rejected').map(r => r.reason.message)
        }
    };
}

const setupTasks = [
    () => delay(50).then(() => 'DB Connected'),
    () => delay(30).then(() => 'Cache Ready')
];

const mainTasks = [
    () => delay(100).then(() => 'Task 1 done'),
    () => delay(50).then(() => { throw new Error('Task 2 failed'); }),
    () => delay(80).then(() => 'Task 3 done')
];

coordinatedExecution(setupTasks, mainTasks).then(console.log);
*/
Exercises - JavaScript Tutorial | DeepML