Hone logo
Hone
Problems

Custom RxJS Operator for Debounced Input Handling

This challenge focuses on creating a custom RxJS operator to efficiently handle user input in an Angular application, specifically by debouncing rapid keystrokes. This is a common requirement for features like search bars where you want to avoid triggering expensive operations on every single key press.

Problem Description

Your task is to create a custom RxJS operator named debounceInput that will be used to process an observable stream of user input events (e.g., from an input field). This operator should delay the emission of values from the source observable until a specified period of inactivity has passed. In essence, it will only emit the latest value after the user has stopped typing for a certain duration.

Key Requirements:

  • Custom Operator: Implement a reusable RxJS operator function.
  • Debouncing Logic: The operator should accept a wait time (in milliseconds) as an argument.
  • Emit Latest Value: Only the most recent value emitted by the source observable within the wait period should be passed through by the operator.
  • Error Handling: The operator should correctly propagate errors from the source observable.
  • Completion Handling: The operator should correctly propagate completion from the source observable.

Expected Behavior:

When applied to an observable stream of input events, debounceInput(300) should:

  1. Receive a new value from the source.
  2. Start a timer for 300 milliseconds.
  3. If another value arrives before the timer finishes, reset the timer and discard the previous value.
  4. If the timer finishes without any new values arriving, emit the last received value.
  5. If the source observable completes or errors, the debounceInput operator should also complete or error accordingly.

Edge Cases to Consider:

  • Initial Value: What happens if the source emits a value immediately?
  • Rapid Input: How does the operator behave with very fast typing?
  • No Input: If the source never emits a value, the operator should remain idle.
  • Zero Wait Time: Consider how a wait of 0ms should be handled (it should essentially pass through values as quickly as possible, similar to mergeMap or switchMap with no delay).

Examples

Example 1: Basic Debouncing

import { fromEvent, of } from 'rxjs';
import { map, take } from 'rxjs/operators';
import { debounceInput } from './custom-operators'; // Assuming your operator is here

// Simulate an input event stream
const inputElement = document.createElement('input');
const keyupEvents = fromEvent(inputElement, 'keyup').pipe(
    map((event: Event) => (event.target as HTMLInputElement).value),
    take(5) // Limit for demonstration
);

// Apply the custom operator
const debouncedStream = keyupEvents.pipe(
    debounceInput(300) // Wait for 300ms of inactivity
);

// Subscribe to see the output
console.log('Simulating typing: "a", "ab", "abc" with 100ms delay between each');
setTimeout(() => {
    inputElement.value = 'a';
    inputElement.dispatchEvent(new Event('keyup'));
}, 0);
setTimeout(() => {
    inputElement.value = 'ab';
    inputElement.dispatchEvent(new Event('keyup'));
}, 100);
setTimeout(() => {
    inputElement.value = 'abc';
    inputElement.dispatchEvent(new Event('keyup'));
}, 200);

// After the last keyup event (at 200ms), the timer starts.
// If no more events occur for 300ms (i.e., after 500ms), 'abc' should be emitted.

debouncedStream.subscribe(value => {
    console.log('Debounced Value:', value);
    // Expected: 'abc' after about 500ms from start
});

Example 2: Rapid Input and Reset

import { fromEvent, of } from 'rxjs';
import { map, take } from 'rxjs/operators';
import { debounceInput } from './custom-operators';

const inputElement = document.createElement('input');
const keyupEvents = fromEvent(inputElement, 'keyup').pipe(
    map((event: Event) => (event.target as HTMLInputElement).value),
    take(6)
);

const debouncedStream = keyupEvents.pipe(
    debounceInput(500)
);

console.log('Simulating rapid typing: "h", "he", "hel", "hell", "hello" with 150ms delay');
setTimeout(() => {
    inputElement.value = 'h';
    inputElement.dispatchEvent(new Event('keyup'));
}, 0);
setTimeout(() => {
    inputElement.value = 'he';
    inputElement.dispatchEvent(new Event('keyup'));
}, 150);
setTimeout(() => {
    inputElement.value = 'hel';
    inputElement.dispatchEvent(new Event('keyup'));
}, 300);
setTimeout(() => {
    inputElement.value = 'hell';
    inputElement.dispatchEvent(new Event('keyup'));
}, 450);
setTimeout(() => {
    inputElement.value = 'hello';
    inputElement.dispatchEvent(new Event('keyup'));
}, 600);

// The last input event is at 600ms. The timer starts for 500ms.
// If no more events, 'hello' should be emitted around 1100ms.

debouncedStream.subscribe(value => {
    console.log('Debounced Value:', value);
    // Expected: 'hello' after about 1100ms from start
});

Example 3: Edge Case - Zero Wait Time

import { of, timer } from 'rxjs';
import { debounceInput } from './custom-operators';

// Simulate a stream of values
const source = of('first', 'second', 'third');

// Apply with zero wait time
const debouncedStream = source.pipe(
    debounceInput(0)
);

console.log('Testing debounceInput with 0ms wait');
debouncedStream.subscribe(value => {
    console.log('Debounced Value (0ms wait):', value);
    // Expected: 'first', 'second', 'third' (emitted with minimal delay, similar to source)
});

Constraints

  • The debounceInput operator must be implemented as a standalone function that takes wait (number) as an argument and returns an RxJS OperatorFunction.
  • The wait parameter will be a non-negative integer representing milliseconds.
  • The operator should leverage RxJS operators like timer, switchMap, and potentially takeUntil for a robust implementation.
  • The solution should be in TypeScript.
  • Avoid directly using the built-in debounceTime operator for the core logic of your custom operator. You can use it in subscribe callbacks or other parts of your application, but the custom operator itself should implement the debouncing mechanism.

Notes

  • Recall that RxJS operators are essentially higher-order functions that transform observables.
  • Consider using switchMap to cancel previous pending timers when a new value arrives.
  • The timer observable is useful for creating a delay.
  • You'll need to import OperatorFunction from rxjs/internal/types (or rxjs/operators for newer versions where it might be re-exported) and Subscriber from rxjs/internal/operators/Subscriber.
  • Think about how to manage the timer instance and clean it up when the source completes or errors.
Loading editor...
typescript