javascript
exercises
exercises.js⚡javascript
/**
* AbortController & Cancellation - Exercises
*
* Practice implementing cancellation patterns
*/
// ============================================
// EXERCISE 1: Basic Fetch with Cancel Button
// ============================================
/**
* Exercise: Create a function that fetches data and can be cancelled
*
* Requirements:
* 1. Return an object with { promise, cancel }
* 2. promise resolves with the JSON data
* 3. cancel() aborts the request
* 4. Handle AbortError gracefully (resolve with null)
*
* @param {string} url - The URL to fetch
* @returns {{ promise: Promise, cancel: Function }}
*/
function createCancellableFetch(url) {
// TODO: Create an AbortController
// TODO: Start the fetch with the signal
// TODO: Return promise and cancel function
return {
promise: Promise.resolve(null), // Replace with actual implementation
cancel: () => {},
};
}
// Test
async function testExercise1() {
const { promise, cancel } = createCancellableFetch(
'https://jsonplaceholder.typicode.com/posts'
);
// Cancel after 50ms
setTimeout(cancel, 50);
const result = await promise;
console.log(
'Exercise 1 - Result:',
result === null ? 'Cancelled (correct)' : 'Got data'
);
}
// ============================================
// EXERCISE 2: Fetch with Timeout
// ============================================
/**
* Exercise: Create a fetch function with automatic timeout
*
* Requirements:
* 1. Abort the request if it takes longer than timeout
* 2. Throw a TimeoutError if timeout occurs
* 3. Clean up the timeout if request succeeds
* 4. Allow passing additional fetch options
*
* @param {string} url - The URL to fetch
* @param {number} timeout - Timeout in milliseconds
* @param {object} options - Additional fetch options
* @returns {Promise<Response>}
*/
async function fetchWithTimeout(url, timeout = 5000, options = {}) {
// TODO: Create AbortController
// TODO: Set up timeout to abort
// TODO: Make fetch request
// TODO: Clear timeout on success/failure
// TODO: Return response
throw new Error('Not implemented');
}
// Test
async function testExercise2() {
try {
// This should timeout
const response = await fetchWithTimeout(
'https://httpbin.org/delay/10',
1000
);
console.log('Exercise 2 - Got response (unexpected)');
} catch (error) {
console.log('Exercise 2 - Error:', error.name);
}
}
// ============================================
// EXERCISE 3: Auto-Cancelling Search
// ============================================
/**
* Exercise: Create a search function that auto-cancels previous searches
*
* Requirements:
* 1. Only the latest search should complete
* 2. Previous searches should be cancelled
* 3. Include debouncing (300ms default)
* 4. Return null for cancelled searches
*/
class AutoCancelSearch {
constructor(searchUrl, debounceMs = 300) {
this.searchUrl = searchUrl;
this.debounceMs = debounceMs;
// TODO: Initialize controller and timer
}
async search(query) {
// TODO: Cancel previous search
// TODO: Clear previous debounce timer
// TODO: Set up new debounce timer
// TODO: Make search request
// TODO: Return results or null if cancelled
return null;
}
cancel() {
// TODO: Cancel current search
}
}
// Test
async function testExercise3() {
const search = new AutoCancelSearch('https://api.example.com/search');
// Rapid searches - only last should complete
search.search('a');
search.search('ab');
search.search('abc');
const result = await search.search('abcd');
console.log('Exercise 3 - Final search term: abcd');
}
// ============================================
// EXERCISE 4: Combine Abort Signals
// ============================================
/**
* Exercise: Create a function to combine multiple abort signals
*
* Requirements:
* 1. Return a new signal that aborts when ANY input signal aborts
* 2. Include the reason from the signal that aborted first
* 3. Handle already-aborted signals
* 4. Clean up event listeners properly
*
* @param {...AbortSignal} signals - Signals to combine
* @returns {AbortSignal}
*/
function anySignal(...signals) {
// TODO: Create new AbortController
// TODO: Check for already-aborted signals
// TODO: Add abort listeners to all signals
// TODO: Abort when any signal aborts
// TODO: Return combined signal
return new AbortController().signal;
}
// Test
async function testExercise4() {
const controller1 = new AbortController();
const controller2 = new AbortController();
const combined = anySignal(controller1.signal, controller2.signal);
combined.addEventListener('abort', () => {
console.log('Exercise 4 - Combined signal aborted:', combined.reason);
});
// Abort one of them
controller2.abort('Second controller aborted');
}
// ============================================
// EXERCISE 5: Cancellable Delay
// ============================================
/**
* Exercise: Create a cancellable delay/sleep function
*
* Requirements:
* 1. Return a promise that resolves after the delay
* 2. Accept an AbortSignal to cancel the delay
* 3. Reject with AbortError if cancelled
* 4. Clean up the timer if cancelled
*
* @param {number} ms - Delay in milliseconds
* @param {AbortSignal} signal - Optional abort signal
* @returns {Promise<void>}
*/
function delay(ms, signal) {
// TODO: Return a promise
// TODO: Set up timeout to resolve
// TODO: Listen for abort signal
// TODO: Clean up on abort or completion
return new Promise((resolve) => setTimeout(resolve, ms));
}
// Test
async function testExercise5() {
const controller = new AbortController();
setTimeout(() => controller.abort('Cancelled early'), 100);
try {
await delay(5000, controller.signal);
console.log('Exercise 5 - Delay completed (unexpected)');
} catch (error) {
console.log('Exercise 5 - Delay cancelled:', error.name);
}
}
// ============================================
// EXERCISE 6: Request Queue with Cancellation
// ============================================
/**
* Exercise: Create a request queue that processes one at a time
*
* Requirements:
* 1. Queue requests and process sequentially
* 2. Allow cancelling individual requests by ID
* 3. Allow cancelling all pending requests
* 4. Return results for completed requests
*/
class RequestQueue {
constructor() {
this.queue = [];
this.processing = false;
// TODO: Initialize other needed properties
}
/**
* Add request to queue
* @returns {{ id: string, promise: Promise }}
*/
add(url, options = {}) {
// TODO: Generate unique ID
// TODO: Create AbortController for this request
// TODO: Add to queue
// TODO: Start processing if not already
// TODO: Return id and promise
return { id: '', promise: Promise.resolve(null) };
}
/**
* Cancel a specific request by ID
*/
cancel(id) {
// TODO: Find request in queue
// TODO: Abort if found
}
/**
* Cancel all pending requests
*/
cancelAll() {
// TODO: Abort all requests in queue
}
async _processQueue() {
// TODO: Process queue items sequentially
}
}
// Test
async function testExercise6() {
const queue = new RequestQueue();
const req1 = queue.add('https://jsonplaceholder.typicode.com/posts/1');
const req2 = queue.add('https://jsonplaceholder.typicode.com/posts/2');
const req3 = queue.add('https://jsonplaceholder.typicode.com/posts/3');
// Cancel second request
queue.cancel(req2.id);
console.log('Exercise 6 - Queue with cancellation');
}
// ============================================
// EXERCISE 7: Polling with Stop
// ============================================
/**
* Exercise: Create a polling function that can be stopped
*
* Requirements:
* 1. Poll an endpoint at specified intervals
* 2. Return a stop function to cancel polling
* 3. Call a callback with each result
* 4. Handle errors gracefully (continue polling)
* 5. Cancel pending request on stop
*/
function startPolling(url, intervalMs, callback) {
// TODO: Create AbortController
// TODO: Set up polling interval
// TODO: Make fetch requests
// TODO: Call callback with results
// TODO: Return stop function
return {
stop: () => {},
};
}
// Test
async function testExercise7() {
let pollCount = 0;
const { stop } = startPolling(
'https://jsonplaceholder.typicode.com/posts/1',
1000,
(data) => {
pollCount++;
console.log('Exercise 7 - Poll', pollCount);
if (pollCount >= 3) {
stop();
console.log('Exercise 7 - Polling stopped');
}
}
);
}
// ============================================
// EXERCISE 8: Parallel Fetch with First Success
// ============================================
/**
* Exercise: Fetch from multiple URLs, return first success, cancel rest
*
* Requirements:
* 1. Start all fetches in parallel
* 2. Return the first successful response
* 3. Cancel all other pending requests
* 4. If all fail, throw an error
*
* @param {string[]} urls - Array of URLs to try
* @returns {Promise<any>} - First successful response data
*/
async function fetchFirstSuccess(urls) {
// TODO: Create AbortController
// TODO: Start all fetches
// TODO: Return first success
// TODO: Cancel remaining on success
// TODO: Handle all failures
throw new Error('Not implemented');
}
// Test
async function testExercise8() {
try {
const result = await fetchFirstSuccess([
'https://jsonplaceholder.typicode.com/posts/1',
'https://jsonplaceholder.typicode.com/posts/2',
'https://jsonplaceholder.typicode.com/posts/3',
]);
console.log('Exercise 8 - First success:', result.id);
} catch (error) {
console.log('Exercise 8 - All failed:', error.message);
}
}
// ============================================
// EXERCISE 9: Event Cleanup Component
// ============================================
/**
* Exercise: Create a component that cleans up events on destroy
*
* Requirements:
* 1. Use AbortController for event listener cleanup
* 2. Mount should add all event listeners
* 3. Destroy should remove all listeners with one call
* 4. Track if component is mounted
*/
class EventComponent {
constructor(element) {
this.element = element;
this.mounted = false;
// TODO: Initialize AbortController
}
mount() {
// TODO: Add event listeners with signal
// TODO: Set mounted to true
}
destroy() {
// TODO: Abort to remove all listeners
// TODO: Set mounted to false
}
// Event handlers
handleClick(e) {
console.log('Click!');
}
handleKeydown(e) {
console.log('Key:', e.key);
}
handleResize() {
console.log('Resize');
}
}
// Test
function testExercise9() {
const div = document.createElement('div');
const component = new EventComponent(div);
component.mount();
console.log('Exercise 9 - Component mounted:', component.mounted);
component.destroy();
console.log('Exercise 9 - Component destroyed:', !component.mounted);
}
// ============================================
// EXERCISE 10: Cancellable Async Generator
// ============================================
/**
* Exercise: Create a cancellable async generator for pagination
*
* Requirements:
* 1. Yield pages of data one at a time
* 2. Accept an AbortSignal for cancellation
* 3. Stop iteration when signal is aborted
* 4. Clean up on cancellation
*
* @param {string} baseUrl - Base URL for pagination
* @param {AbortSignal} signal - Abort signal
* @yields {Promise<any[]>} - Page of items
*/
async function* paginatedFetch(baseUrl, signal) {
// TODO: Initialize page counter
// TODO: Loop while not aborted
// TODO: Fetch page data
// TODO: Yield page items
// TODO: Handle abort
yield [];
}
// Test
async function testExercise10() {
const controller = new AbortController();
let pageCount = 0;
try {
for await (const items of paginatedFetch(
'https://jsonplaceholder.typicode.com/posts',
controller.signal
)) {
pageCount++;
console.log('Exercise 10 - Page', pageCount);
if (pageCount >= 2) {
controller.abort('Enough pages');
}
}
} catch (error) {
if (error.name === 'AbortError') {
console.log(
'Exercise 10 - Pagination cancelled after',
pageCount,
'pages'
);
}
}
}
// ============================================
// SOLUTIONS VERIFICATION
// ============================================
async function verifySolutions() {
console.log('Verifying AbortController exercise solutions...\n');
const tests = [
{ name: 'Exercise 1: Cancellable Fetch', fn: testExercise1 },
{ name: 'Exercise 2: Fetch with Timeout', fn: testExercise2 },
{ name: 'Exercise 3: Auto-Cancel Search', fn: testExercise3 },
{ name: 'Exercise 4: Combine Signals', fn: testExercise4 },
{ name: 'Exercise 5: Cancellable Delay', fn: testExercise5 },
{ name: 'Exercise 6: Request Queue', fn: testExercise6 },
{ name: 'Exercise 7: Polling with Stop', fn: testExercise7 },
{ name: 'Exercise 8: First Success', fn: testExercise8 },
{ name: 'Exercise 9: Event Cleanup', fn: testExercise9 },
{ name: 'Exercise 10: Async Generator', fn: testExercise10 },
];
for (const test of tests) {
console.log(`\n--- ${test.name} ---`);
try {
await test.fn();
} catch (error) {
console.log('Error:', error.message);
}
}
console.log('\n--- Verification Complete ---');
console.log('Review console output to verify each exercise works correctly.');
}
// Solution hints
function showHints() {
console.log('=== Exercise Hints ===\n');
console.log('Exercise 1: Use new AbortController() and pass signal to fetch');
console.log('Exercise 2: Use setTimeout with controller.abort()');
console.log(
'Exercise 3: Store controller as instance property, abort before new search'
);
console.log(
'Exercise 4: Add abort listener to each signal, abort combined on any'
);
console.log(
'Exercise 5: Use signal.addEventListener("abort", ...) with reject'
);
console.log(
'Exercise 6: Map of ID -> controller, check aborted before processing'
);
console.log(
'Exercise 7: Use setInterval, clear on stop, abort pending fetch'
);
console.log(
'Exercise 8: Use Promise.race or manual tracking, abort on first resolve'
);
console.log('Exercise 9: Pass { signal } as third arg to addEventListener');
console.log(
'Exercise 10: Check signal.aborted before each fetch, throw if aborted'
);
}
// Export for module usage
if (typeof module !== 'undefined' && module.exports) {
module.exports = {
createCancellableFetch,
fetchWithTimeout,
AutoCancelSearch,
anySignal,
delay,
RequestQueue,
startPolling,
fetchFirstSuccess,
EventComponent,
paginatedFetch,
verifySolutions,
showHints,
};
}