javascript
exercises
exercises.js⚡javascript
/**
* ========================================
* 9.1 CALLBACKS - EXERCISES
* ========================================
*
* Practice working with callbacks and asynchronous patterns.
*
* Instructions:
* 1. Read each exercise description carefully
* 2. Implement the solution below the exercise
* 3. Check the solution in the comments if stuck
*/
/**
* EXERCISE 1: Basic Callback
*
* Create a function `greetWithDelay` that:
* - Takes a name and a callback function
* - After 1 second, calls the callback with a greeting message
* - The greeting should be "Hello, {name}!"
*/
console.log('--- Exercise 1: Basic Callback ---');
// Your code here:
// Test case:
// greetWithDelay("Alice", (message) => {
// console.log(message); // After 1s: "Hello, Alice!"
// });
/*
* SOLUTION 1:
*
* function greetWithDelay(name, callback) {
* setTimeout(() => {
* callback(`Hello, ${name}!`);
* }, 1000);
* }
*/
/**
* EXERCISE 2: Error-First Callback
*
* Create a function `divide` that:
* - Takes two numbers and a callback
* - Uses error-first callback pattern
* - Returns error if dividing by zero
* - Returns the result otherwise
*/
console.log('\n--- Exercise 2: Error-First Callback ---');
// Your code here:
// Test cases:
// divide(10, 2, (err, result) => {
// if (err) console.error(err.message);
// else console.log("10 / 2 =", result); // 5
// });
//
// divide(10, 0, (err, result) => {
// if (err) console.error(err.message); // "Cannot divide by zero"
// else console.log("Result:", result);
// });
/*
* SOLUTION 2:
*
* function divide(a, b, callback) {
* setTimeout(() => {
* if (b === 0) {
* callback(new Error("Cannot divide by zero"), null);
* } else {
* callback(null, a / b);
* }
* }, 100);
* }
*/
/**
* EXERCISE 3: Array Async Map
*
* Create a function `asyncMap` that:
* - Takes an array and an async transform function
* - Applies the transform to each element
* - Calls the final callback with the transformed array
* - Transform signature: transform(item, callback)
*/
console.log('\n--- Exercise 3: Async Map ---');
// Your code here:
// Test case:
// const numbers = [1, 2, 3];
// const doubleAsync = (n, cb) => setTimeout(() => cb(null, n * 2), 50);
//
// asyncMap(numbers, doubleAsync, (err, results) => {
// console.log(results); // [2, 4, 6]
// });
/*
* SOLUTION 3:
*
* function asyncMap(array, transform, callback) {
* const results = [];
* let completed = 0;
* let hasError = false;
*
* if (array.length === 0) {
* callback(null, []);
* return;
* }
*
* array.forEach((item, index) => {
* transform(item, (error, result) => {
* if (hasError) return;
*
* if (error) {
* hasError = true;
* callback(error, null);
* return;
* }
*
* results[index] = result;
* completed++;
*
* if (completed === array.length) {
* callback(null, results);
* }
* });
* });
* }
*/
/**
* EXERCISE 4: Async Filter
*
* Create a function `asyncFilter` that:
* - Takes an array and an async predicate function
* - Keeps items where predicate returns true
* - Predicate signature: predicate(item, callback)
* - callback(null, boolean)
*/
console.log('\n--- Exercise 4: Async Filter ---');
// Your code here:
// Test case:
// const numbers = [1, 2, 3, 4, 5];
// const isEvenAsync = (n, cb) => setTimeout(() => cb(null, n % 2 === 0), 50);
//
// asyncFilter(numbers, isEvenAsync, (err, results) => {
// console.log(results); // [2, 4]
// });
/*
* SOLUTION 4:
*
* function asyncFilter(array, predicate, callback) {
* const results = [];
* let completed = 0;
* let hasError = false;
*
* if (array.length === 0) {
* callback(null, []);
* return;
* }
*
* array.forEach((item, index) => {
* predicate(item, (error, keep) => {
* if (hasError) return;
*
* if (error) {
* hasError = true;
* callback(error, null);
* return;
* }
*
* if (keep) {
* results.push({ index, item });
* }
* completed++;
*
* if (completed === array.length) {
* // Sort by original index to maintain order
* results.sort((a, b) => a.index - b.index);
* callback(null, results.map(r => r.item));
* }
* });
* });
* }
*/
/**
* EXERCISE 5: Sequential Execution
*
* Create a function `series` that:
* - Takes an array of async tasks
* - Executes them one after another (not in parallel)
* - Each task: task(callback)
* - Collects all results in order
*/
console.log('\n--- Exercise 5: Series Execution ---');
// Your code here:
// Test case:
// const tasks = [
// (cb) => setTimeout(() => cb(null, "First"), 100),
// (cb) => setTimeout(() => cb(null, "Second"), 50),
// (cb) => setTimeout(() => cb(null, "Third"), 75)
// ];
//
// series(tasks, (err, results) => {
// console.log(results); // ["First", "Second", "Third"]
// });
/*
* SOLUTION 5:
*
* function series(tasks, callback) {
* const results = [];
* let index = 0;
*
* function next() {
* if (index >= tasks.length) {
* callback(null, results);
* return;
* }
*
* const task = tasks[index++];
* task((error, result) => {
* if (error) {
* callback(error, null);
* return;
* }
* results.push(result);
* next();
* });
* }
*
* next();
* }
*/
/**
* EXERCISE 6: Parallel Execution
*
* Create a function `parallel` that:
* - Takes an array of async tasks
* - Executes them all at once (in parallel)
* - Returns results in the original order
* - Fails fast if any task errors
*/
console.log('\n--- Exercise 6: Parallel Execution ---');
// Your code here:
// Test case:
// const tasks = [
// (cb) => setTimeout(() => cb(null, "First"), 100),
// (cb) => setTimeout(() => cb(null, "Second"), 50),
// (cb) => setTimeout(() => cb(null, "Third"), 75)
// ];
//
// parallel(tasks, (err, results) => {
// console.log(results); // ["First", "Second", "Third"]
// });
/*
* SOLUTION 6:
*
* function parallel(tasks, callback) {
* const results = [];
* let completed = 0;
* let hasError = false;
*
* if (tasks.length === 0) {
* callback(null, []);
* return;
* }
*
* tasks.forEach((task, index) => {
* task((error, result) => {
* if (hasError) return;
*
* if (error) {
* hasError = true;
* callback(error, null);
* return;
* }
*
* results[index] = result;
* completed++;
*
* if (completed === tasks.length) {
* callback(null, results);
* }
* });
* });
* }
*/
/**
* EXERCISE 7: Waterfall (Chained Callbacks)
*
* Create a function `waterfall` that:
* - Takes an array of async tasks
* - Passes result of each task to the next
* - First task receives no arguments
* - Final callback gets the last result
*/
console.log('\n--- Exercise 7: Waterfall ---');
// Your code here:
// Test case:
// const tasks = [
// (cb) => cb(null, 5),
// (n, cb) => cb(null, n * 2), // 10
// (n, cb) => cb(null, n + 3) // 13
// ];
//
// waterfall(tasks, (err, result) => {
// console.log(result); // 13
// });
/*
* SOLUTION 7:
*
* function waterfall(tasks, callback) {
* let index = 0;
*
* function next(previousResult) {
* if (index >= tasks.length) {
* callback(null, previousResult);
* return;
* }
*
* const task = tasks[index++];
*
* const cb = (error, result) => {
* if (error) {
* callback(error, null);
* return;
* }
* next(result);
* };
*
* if (previousResult === undefined) {
* task(cb);
* } else {
* task(previousResult, cb);
* }
* }
*
* next();
* }
*/
/**
* EXERCISE 8: Retry with Backoff
*
* Create a function `retryWithBackoff` that:
* - Takes an operation, max attempts, and initial delay
* - Retries on failure with exponential backoff
* - Delay doubles after each failure
*/
console.log('\n--- Exercise 8: Retry with Backoff ---');
// Your code here:
// Test case:
// let attempts = 0;
// function flakyOperation(callback) {
// attempts++;
// if (attempts < 3) {
// callback(new Error("Failed"), null);
// } else {
// callback(null, "Success!");
// }
// }
//
// retryWithBackoff(flakyOperation, 5, 100, (err, result) => {
// console.log(result); // "Success!" after delays of 100ms, 200ms
// });
/*
* SOLUTION 8:
*
* function retryWithBackoff(operation, maxAttempts, initialDelay, callback) {
* let attempts = 0;
* let delay = initialDelay;
*
* function attempt() {
* attempts++;
* console.log(`Attempt ${attempts}, delay was ${delay}ms`);
*
* operation((error, result) => {
* if (error) {
* if (attempts >= maxAttempts) {
* callback(new Error(`Failed after ${maxAttempts} attempts`), null);
* } else {
* setTimeout(attempt, delay);
* delay *= 2; // Exponential backoff
* }
* } else {
* callback(null, result);
* }
* });
* }
*
* attempt();
* }
*/
/**
* EXERCISE 9: Debounce with Immediate
*
* Create a `debounce` function that:
* - Delays execution until after wait ms since last call
* - Has an optional `immediate` flag
* - If immediate, runs on leading edge instead of trailing
*/
console.log('\n--- Exercise 9: Debounce ---');
// Your code here:
// Test case:
// const log = debounce((msg) => console.log(msg), 300);
// log("a"); log("b"); log("c");
// // Only logs "c" after 300ms
//
// const logImmediate = debounce((msg) => console.log(msg), 300, true);
// logImmediate("x"); logImmediate("y"); logImmediate("z");
// // Logs "x" immediately, ignores rest until 300ms passes
/*
* SOLUTION 9:
*
* function debounce(func, wait, immediate = false) {
* let timeoutId;
*
* return function(...args) {
* const callNow = immediate && !timeoutId;
*
* clearTimeout(timeoutId);
*
* timeoutId = setTimeout(() => {
* timeoutId = null;
* if (!immediate) {
* func.apply(this, args);
* }
* }, wait);
*
* if (callNow) {
* func.apply(this, args);
* }
* };
* }
*/
/**
* EXERCISE 10: Throttle with Options
*
* Create a `throttle` function that:
* - Limits execution to once per wait ms
* - Options: { leading: true, trailing: true }
* - leading: run on first call
* - trailing: run on last call after wait
*/
console.log('\n--- Exercise 10: Throttle ---');
// Your code here:
// Test case:
// const log = throttle((x) => console.log(x), 100, { leading: true, trailing: true });
// log(1); // Immediate
// log(2); // Ignored
// log(3); // Runs after 100ms
/*
* SOLUTION 10:
*
* function throttle(func, wait, options = {}) {
* const { leading = true, trailing = true } = options;
* let timeoutId = null;
* let lastArgs = null;
* let lastTime = 0;
*
* return function(...args) {
* const now = Date.now();
*
* if (!lastTime && !leading) {
* lastTime = now;
* }
*
* const remaining = wait - (now - lastTime);
*
* if (remaining <= 0) {
* if (timeoutId) {
* clearTimeout(timeoutId);
* timeoutId = null;
* }
* lastTime = now;
* func.apply(this, args);
* } else if (!timeoutId && trailing) {
* lastArgs = args;
* timeoutId = setTimeout(() => {
* lastTime = leading ? Date.now() : 0;
* timeoutId = null;
* func.apply(this, lastArgs);
* }, remaining);
* }
* };
* }
*/
/**
* EXERCISE 11: Timeout Wrapper
*
* Create a function `withTimeout` that:
* - Wraps an async operation with a timeout
* - Returns error if operation takes too long
* - Cancels pending callback if timeout occurs
*/
console.log('\n--- Exercise 11: Timeout Wrapper ---');
// Your code here:
// Test case:
// function slowOp(cb) { setTimeout(() => cb(null, "Done!"), 500); }
// function fastOp(cb) { setTimeout(() => cb(null, "Quick!"), 50); }
//
// withTimeout(slowOp, 200, (err, result) => {
// console.log(err ? err.message : result); // "Operation timed out"
// });
//
// withTimeout(fastOp, 200, (err, result) => {
// console.log(err ? err.message : result); // "Quick!"
// });
/*
* SOLUTION 11:
*
* function withTimeout(operation, timeout, callback) {
* let completed = false;
*
* const timeoutId = setTimeout(() => {
* if (!completed) {
* completed = true;
* callback(new Error("Operation timed out"), null);
* }
* }, timeout);
*
* operation((error, result) => {
* if (!completed) {
* completed = true;
* clearTimeout(timeoutId);
* callback(error, result);
* }
* });
* }
*/
/**
* EXERCISE 12: Rate Limiter
*
* Create a `rateLimiter` that:
* - Limits operations to N per time window
* - Queues excess operations
* - Executes queued operations as slots become available
*/
console.log('\n--- Exercise 12: Rate Limiter ---');
// Your code here:
// Test case:
// const limiter = createRateLimiter(2, 1000); // 2 per second
//
// for (let i = 1; i <= 5; i++) {
// limiter(() => console.log(`Task ${i} executed at`, Date.now()));
// }
// // First 2 run immediately, rest are delayed
/*
* SOLUTION 12:
*
* function createRateLimiter(limit, window) {
* const queue = [];
* let running = 0;
* const timestamps = [];
*
* function tryExecute() {
* const now = Date.now();
*
* // Remove timestamps outside the window
* while (timestamps.length > 0 && timestamps[0] <= now - window) {
* timestamps.shift();
* }
*
* while (queue.length > 0 && timestamps.length < limit) {
* const task = queue.shift();
* timestamps.push(now);
* task();
* }
*
* if (queue.length > 0) {
* const nextSlot = timestamps[0] + window - now;
* setTimeout(tryExecute, nextSlot);
* }
* }
*
* return function(task) {
* queue.push(task);
* tryExecute();
* };
* }
*/
/**
* EXERCISE 13: Callback to Events
*
* Create a class `AsyncOperation` that:
* - Converts callback-based operation to event-based
* - Emits 'start', 'progress', 'complete', 'error' events
*/
console.log('\n--- Exercise 13: Callback to Events ---');
// Your code here:
// Test case:
// const op = new AsyncOperation();
// op.on('start', () => console.log('Started'));
// op.on('progress', (p) => console.log(`Progress: ${p}%`));
// op.on('complete', (result) => console.log('Done:', result));
// op.on('error', (err) => console.log('Error:', err.message));
//
// op.execute((progress) => {
// // Simulate progress
// for (let i = 25; i <= 100; i += 25) {
// progress(i);
// }
// return "Result data";
// });
/*
* SOLUTION 13:
*
* class AsyncOperation {
* #events = {};
*
* on(event, handler) {
* if (!this.#events[event]) {
* this.#events[event] = [];
* }
* this.#events[event].push(handler);
* return this;
* }
*
* emit(event, ...args) {
* if (this.#events[event]) {
* this.#events[event].forEach(handler => handler(...args));
* }
* }
*
* execute(operation) {
* this.emit('start');
*
* const progress = (percent) => {
* this.emit('progress', percent);
* };
*
* setTimeout(() => {
* try {
* const result = operation(progress);
* this.emit('complete', result);
* } catch (error) {
* this.emit('error', error);
* }
* }, 0);
* }
* }
*/
/**
* EXERCISE 14: Promise-like Callbacks
*
* Create a `Deferred` class that:
* - Has resolve(value) and reject(error) methods
* - Has then(onSuccess, onError) method
* - Stores result and calls handlers when available
*/
console.log('\n--- Exercise 14: Deferred Object ---');
// Your code here:
// Test case:
// const deferred = new Deferred();
//
// deferred.then(
// (value) => console.log("Resolved:", value),
// (error) => console.log("Rejected:", error.message)
// );
//
// setTimeout(() => deferred.resolve("Success!"), 100);
/*
* SOLUTION 14:
*
* class Deferred {
* #state = 'pending';
* #value = null;
* #handlers = [];
*
* then(onSuccess, onError) {
* if (this.#state === 'resolved') {
* onSuccess && setTimeout(() => onSuccess(this.#value), 0);
* } else if (this.#state === 'rejected') {
* onError && setTimeout(() => onError(this.#value), 0);
* } else {
* this.#handlers.push({ onSuccess, onError });
* }
* return this;
* }
*
* resolve(value) {
* if (this.#state !== 'pending') return;
* this.#state = 'resolved';
* this.#value = value;
* this.#handlers.forEach(h => h.onSuccess && h.onSuccess(value));
* }
*
* reject(error) {
* if (this.#state !== 'pending') return;
* this.#state = 'rejected';
* this.#value = error;
* this.#handlers.forEach(h => h.onError && h.onError(error));
* }
* }
*/
/**
* EXERCISE 15: File Operations Simulator
*
* Create a file system simulator with callbacks:
* - readFile(path, callback)
* - writeFile(path, content, callback)
* - deleteFile(path, callback)
* - listFiles(callback)
*
* Use in-memory storage.
*/
console.log('\n--- Exercise 15: File System Simulator ---');
// Your code here:
// Test case:
// const fs = new FileSystem();
//
// fs.writeFile('/test.txt', 'Hello World', (err) => {
// if (err) return console.error(err);
//
// fs.readFile('/test.txt', (err, content) => {
// console.log(content); // "Hello World"
//
// fs.listFiles((err, files) => {
// console.log(files); // ["/test.txt"]
// });
// });
// });
/*
* SOLUTION 15:
*
* class FileSystem {
* #files = new Map();
* #delay = 50;
*
* readFile(path, callback) {
* setTimeout(() => {
* if (!this.#files.has(path)) {
* callback(new Error(`File not found: ${path}`), null);
* } else {
* callback(null, this.#files.get(path));
* }
* }, this.#delay);
* }
*
* writeFile(path, content, callback) {
* setTimeout(() => {
* this.#files.set(path, content);
* callback(null);
* }, this.#delay);
* }
*
* deleteFile(path, callback) {
* setTimeout(() => {
* if (!this.#files.has(path)) {
* callback(new Error(`File not found: ${path}`));
* } else {
* this.#files.delete(path);
* callback(null);
* }
* }, this.#delay);
* }
*
* listFiles(callback) {
* setTimeout(() => {
* callback(null, [...this.#files.keys()]);
* }, this.#delay);
* }
* }
*/
console.log('\n========================================');
console.log('End of Callback Exercises');
console.log('========================================');