javascript

exercises

exercises.js
/**
 * 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,
  };
}
Exercises - JavaScript Tutorial | DeepML