javascript

exercises

exercises.js
/**
 * ========================================
 * 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('========================================');
Exercises - JavaScript Tutorial | DeepML