1. Observable Creation Functions and Syntax

1.1 of, from, and fromEvent Creation Operators

Operator Syntax Description Use Case
of of(...values) Creates Observable emitting specified values synchronously, then completes Static value sequences, testing, immediate emission
from from(array | promise | iterable) Converts arrays, promises, iterables, or observables into Observable stream Array iteration, Promise conversion, async/await integration
fromEvent fromEvent(target, eventName) Creates Observable from DOM events or Node.js EventEmitter Click handlers, keyboard input, mouse events, custom events

Example: Basic creation operators

import { of, from, fromEvent } from 'rxjs';

// of - emit static values
of(1, 2, 3).subscribe(val => console.log(val)); // 1, 2, 3

// from - convert array to observable
from([10, 20, 30]).subscribe(val => console.log(val)); // 10, 20, 30

// from - convert promise to observable
from(fetch('/api/data')).subscribe(response => console.log(response));

// fromEvent - DOM event to observable
const clicks$ = fromEvent(document, 'click');
clicks$.subscribe(event => console.log('Clicked at:', event.clientX, event.clientY));

1.2 interval, timer, and range Observable Creators

Operator Syntax Description Emission Pattern
interval interval(period) Emits sequential numbers at specified interval (ms), starting from 0 0, 1, 2, 3... (every period ms)
timer timer(delay, period?) Emits after delay, then optionally repeats at interval. Single emission if no period Initial delay, then interval emissions
range range(start, count?) Emits sequence of numbers synchronously from start to start+count-1 Synchronous number sequence

Example: Time-based and sequence creators

import { interval, timer, range } from 'rxjs';
import { take } from 'rxjs/operators';

// interval - emit every 1000ms
interval(1000).pipe(take(3))
  .subscribe(n => console.log('Interval:', n)); // 0, 1, 2

// timer - emit after 2s, then every 1s
timer(2000, 1000).pipe(take(3))
  .subscribe(n => console.log('Timer:', n)); // 0 (after 2s), 1, 2

// timer - single emission after delay
timer(3000).subscribe(() => console.log('Delayed action'));

// range - synchronous sequence
range(1, 5).subscribe(n => console.log('Range:', n)); // 1, 2, 3, 4, 5

1.3 fromPromise and async Observable Integration

Method Syntax Description Behavior
from(promise) from(promiseObj) Converts Promise to Observable - emits resolved value or error Single emission on resolve, error on reject
defer defer(() => promise) Creates Observable factory - Promise created per subscription (lazy) Fresh Promise instance for each subscriber
async/await await firstValueFrom(obs$) Convert Observable to Promise for async/await syntax Waits for first emission, then resolves

Example: Promise and async integration

import { from, defer } from 'rxjs';
import { firstValueFrom } from 'rxjs';

// Convert Promise to Observable
const promise = fetch('/api/user');
from(promise).subscribe(
  response => console.log('Success:', response),
  error => console.error('Error:', error)
);

// Lazy Promise creation with defer
const lazyPromise$ = defer(() => fetch('/api/data'));
lazyPromise$.subscribe(); // Fetch happens only on subscribe

// Convert Observable to Promise for async/await
async function fetchData() {
  const obs$ = from(fetch('/api/items'));
  const response = await firstValueFrom(obs$);
  return response.json();
}
Note: Use defer when you need fresh Promise instances for each subscription. Direct from(promise) shares the same Promise result across all subscribers.

1.4 ajax and fetch Observable Wrappers

Function Syntax Description Features
ajax ajax(urlOrRequest) RxJS XMLHttpRequest wrapper with built-in operators support Progress events, timeout, retry, cancellation
ajax.get ajax.get(url, headers?) Convenience method for GET requests Automatic JSON parsing, CORS support
ajax.post ajax.post(url, body, headers?) Convenience method for POST requests JSON body serialization, custom headers
from(fetch) from(fetch(url, options)) Modern Fetch API wrapped in Observable Promise-based, streaming, simpler API

Example: HTTP requests with ajax and fetch

import { ajax } from 'rxjs/ajax';
import { from } from 'rxjs';
import { map, catchError } from 'rxjs/operators';

// ajax GET request
ajax.get('https://api.example.com/users')
  .pipe(
    map(response => response.response),
    catchError(error => of({ error: error.message }))
  )
  .subscribe(data => console.log(data));

// ajax POST request
ajax.post('https://api.example.com/users', 
  { name: 'John', email: 'john@example.com' },
  { 'Content-Type': 'application/json' }
).subscribe(response => console.log(response));

// Custom ajax configuration
ajax({
  url: 'https://api.example.com/data',
  method: 'PUT',
  headers: { 'Authorization': 'Bearer token123' },
  body: { value: 42 },
  timeout: 5000
}).subscribe(response => console.log(response));

// Fetch API as Observable
from(fetch('https://api.example.com/items'))
  .pipe(
    switchMap(response => from(response.json()))
  )
  .subscribe(data => console.log(data));

1.5 Custom Observable Creation with new Observable()

Component Syntax Description Purpose
Observable constructor new Observable(subscriber => {...}) Creates custom Observable with full control over emissions Custom data sources, complex logic
subscriber.next() subscriber.next(value) Emit value to observers Push data to subscribers
subscriber.error() subscriber.error(err) Signal error and terminate stream Error handling and propagation
subscriber.complete() subscriber.complete() Signal successful completion Terminate stream normally
Teardown function return () => cleanup() Cleanup logic executed on unsubscribe Resource cleanup, event removal

Example: Custom Observable implementation

import { Observable } from 'rxjs';

// Custom Observable with manual emissions
const custom$ = new Observable(subscriber => {
  subscriber.next(1);
  subscriber.next(2);
  subscriber.next(3);
  subscriber.complete();
  
  // Teardown function for cleanup
  return () => console.log('Unsubscribed');
});

custom$.subscribe({
  next: val => console.log('Value:', val),
  complete: () => console.log('Complete')
});

// Custom event-based Observable
const createEventObservable = (element, eventName) => {
  return new Observable(subscriber => {
    const handler = (event) => subscriber.next(event);
    element.addEventListener(eventName, handler);
    
    // Cleanup on unsubscribe
    return () => element.removeEventListener(eventName, handler);
  });
};

// WebSocket Observable
const websocket$ = new Observable(subscriber => {
  const ws = new WebSocket('wss://example.com/socket');
  
  ws.onmessage = (event) => subscriber.next(event.data);
  ws.onerror = (error) => subscriber.error(error);
  ws.onclose = () => subscriber.complete();
  
  return () => ws.close();
});
Warning: Always provide teardown function to prevent memory leaks. Clean up event listeners, timers, connections in the return statement.

1.6 EMPTY, NEVER, and throwError Constants

Constant Syntax Behavior Use Case
EMPTY EMPTY Completes immediately without emitting any values Placeholder, conditional logic, cancel operations
NEVER NEVER Never emits values and never completes (infinite stream) Testing, keep-alive streams, blocking scenarios
throwError throwError(() => error) Immediately emits error notification, no values Error injection, testing error handling, fallback errors

Example: Special Observable constants

import { EMPTY, NEVER, throwError, of } from 'rxjs';
import { catchError, timeout } from 'rxjs/operators';

// EMPTY - completes immediately
EMPTY.subscribe({
  next: val => console.log('Value:', val),      // Never called
  complete: () => console.log('Complete')        // Immediately called
});

// EMPTY in conditional logic
const shouldProcess = false;
(shouldProcess ? of(1, 2, 3) : EMPTY)
  .subscribe(val => console.log(val)); // No output

// NEVER - infinite stream
NEVER.subscribe({
  next: val => console.log(val),        // Never called
  complete: () => console.log('Done')   // Never called
});

// throwError - immediate error
throwError(() => new Error('Custom error'))
  .subscribe({
    next: val => console.log(val),
    error: err => console.error('Error:', err.message)
  });

// Practical use in error handling
of(1, 2, 3).pipe(
  map(n => {
    if (n === 2) throw new Error('Invalid value');
    return n * 10;
  }),
  catchError(err => throwError(() => new Error('Transformed: ' + err.message)))
).subscribe({
  next: val => console.log(val),
  error: err => console.error(err)
});

Section 1 Summary

  • of() and from() convert static values/arrays/promises to Observables
  • fromEvent() bridges DOM/Node events to reactive streams
  • interval(), timer(), range() create time-based and sequential emissions
  • ajax provides robust HTTP capabilities with cancellation and retry support
  • new Observable() enables full custom control for complex data sources
  • EMPTY, NEVER, throwError handle special control flow scenarios

2. Core Observable Lifecycle and Subscription Management

2.1 subscribe() Method and Observer Pattern

Method Syntax Description Use Case
subscribe(observer) obs$.subscribe({next, error, complete}) Full observer object with all callbacks Complete control over all lifecycle events
subscribe(nextFn) obs$.subscribe(val => {}) Shorthand for next callback only Simple value consumption without error handling
subscribe(next, error) obs$.subscribe(val => {}, err => {}) Next and error callbacks Value handling with error management
subscribe(next, error, complete) obs$.subscribe(val => {}, err => {}, () => {}) All three callbacks as parameters Full lifecycle handling in function form

Example: Different subscribe patterns

import { of, throwError, interval } from 'rxjs';
import { take } from 'rxjs/operators';

// Full observer object pattern
of(1, 2, 3).subscribe({
  next: value => console.log('Next:', value),
  error: err => console.error('Error:', err),
  complete: () => console.log('Complete!')
});

// Shorthand next only
of(10, 20, 30).subscribe(val => console.log(val));

// Next and error callbacks
throwError(() => new Error('Fail')).subscribe(
  val => console.log(val),
  err => console.error('Caught:', err.message)
);

// All three callbacks
interval(1000).pipe(take(3)).subscribe(
  val => console.log('Value:', val),
  err => console.error('Error:', err),
  () => console.log('Stream completed')
);
Note: Observer object pattern {next, error, complete} is preferred for clarity and avoids positional parameter confusion.

2.2 Subscription Object and unsubscribe() Method

Property/Method Syntax Description Behavior
Subscription const sub = obs$.subscribe() Object representing active subscription lifecycle Returned by subscribe(), manages cleanup
unsubscribe() sub.unsubscribe() Cancels subscription and triggers cleanup logic Stops emissions, runs teardown functions
closed sub.closed Boolean indicating if subscription is closed True after unsubscribe or completion

Example: Subscription lifecycle management

import { interval } from 'rxjs';

// Create and store subscription
const subscription = interval(1000).subscribe(
  val => console.log('Tick:', val)
);

// Check subscription state
console.log('Closed?', subscription.closed); // false

// Unsubscribe after 5 seconds
setTimeout(() => {
  subscription.unsubscribe();
  console.log('Closed?', subscription.closed); // true
}, 5000);

// Component lifecycle example
class MyComponent {
  private subscription: Subscription;
  
  onInit() {
    this.subscription = interval(1000).subscribe(
      val => this.updateUI(val)
    );
  }
  
  onDestroy() {
    // Always unsubscribe to prevent memory leaks
    this.subscription.unsubscribe();
  }
}
Warning: Always call unsubscribe() to prevent memory leaks, especially for long-running observables like intervals, timers, and event streams.

2.3 Subscription add() and remove() for Composition

Method Syntax Description Use Case
add() parentSub.add(childSub) Adds child subscription to parent - unsubscribing parent unsubscribes children Group multiple subscriptions for bulk cleanup
add(teardown) sub.add(() => cleanup()) Adds custom teardown function to subscription Custom cleanup logic (close connections, clear timers)
remove() parentSub.remove(childSub) Removes child subscription from parent without unsubscribing it Separate subscription management before parent cleanup

Example: Composing subscriptions

import { interval, fromEvent } from 'rxjs';

// Parent subscription
const parentSub = interval(1000).subscribe(
  val => console.log('Parent:', val)
);

// Add child subscriptions
const childSub1 = interval(500).subscribe(val => console.log('Child1:', val));
const childSub2 = fromEvent(document, 'click').subscribe(() => console.log('Click'));

parentSub.add(childSub1);
parentSub.add(childSub2);

// Add custom teardown logic
parentSub.add(() => console.log('Custom cleanup executed'));

// Unsubscribing parent unsubscribes all children
setTimeout(() => {
  parentSub.unsubscribe(); // Cleans up parent + all children
}, 5000);

// Practical component example
class DataService {
  private subscriptions = new Subscription();
  
  startStreams() {
    this.subscriptions.add(
      interval(1000).subscribe(val => this.process(val))
    );
    this.subscriptions.add(
      fromEvent(window, 'resize').subscribe(() => this.handleResize())
    );
  }
  
  stopStreams() {
    this.subscriptions.unsubscribe(); // Stops all streams
    this.subscriptions = new Subscription(); // Reset for reuse
  }
}

2.4 AutoUnsubscribe Patterns and Lifecycle Management

Pattern Implementation Description Framework
takeUntil() obs$.pipe(takeUntil(destroy$)) Automatically unsubscribe when notifier emits Framework-agnostic, manual control
async pipe {{ obs$ | async }} Template subscription with automatic cleanup Angular built-in
takeUntilDestroyed obs$.pipe(takeUntilDestroyed()) Angular v16+ automatic cleanup tied to component lifecycle Angular v16+
Decorator pattern @AutoUnsubscribe() Class decorator for automatic unsubscription Custom implementation

Example: AutoUnsubscribe patterns

import { interval, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';

// takeUntil pattern (most common)
class Component {
  private destroy$ = new Subject<void>();
  
  ngOnInit() {
    interval(1000).pipe(
      takeUntil(this.destroy$)
    ).subscribe(val => console.log(val));
    
    fromEvent(window, 'resize').pipe(
      takeUntil(this.destroy$)
    ).subscribe(() => this.handleResize());
  }
  
  ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

// Angular async pipe (template)
// component.ts
export class MyComponent {
  data$ = this.http.get('/api/data');
}
// template.html
// <div>{{ data$ | async }}</div>

// Angular v16+ takeUntilDestroyed
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

export class ModernComponent {
  constructor() {
    interval(1000).pipe(
      takeUntilDestroyed() // Automatically tied to component lifecycle
    ).subscribe(val => console.log(val));
  }
}

// Custom decorator pattern
function AutoUnsubscribe() {
  return function(constructor: Function) {
    const original = constructor.prototype.ngOnDestroy;
    constructor.prototype.ngOnDestroy = function() {
      for (let prop in this) {
        const property = this[prop];
        if (property && typeof property.unsubscribe === 'function') {
          property.unsubscribe();
        }
      }
      original?.apply(this);
    };
  };
}

2.5 Memory Leak Prevention and Subscription Cleanup

Issue Symptom Solution Prevention
Infinite observables Interval/timer continues after component destroyed Use takeUntil() or manual unsubscribe Always cleanup long-running streams
Event listeners DOM event handlers accumulate, slow performance Unsubscribe fromEvent subscriptions Use async pipe or takeUntil
HTTP requests Completed requests still held in memory Auto-completes, but use take(1) for clarity Consider using async pipe in templates
Subject retention Subjects hold references to observers Call complete() on subjects in cleanup Complete subjects in ngOnDestroy/cleanup

Example: Memory leak prevention

import { interval, fromEvent, Subject } from 'rxjs';
import { takeUntil, take } from 'rxjs/operators';

// BAD - Memory leak
class LeakyComponent {
  ngOnInit() {
    interval(1000).subscribe(val => console.log(val)); // Never stops!
    fromEvent(window, 'scroll').subscribe(() => {}); // Listener never removed!
  }
}

// GOOD - Proper cleanup
class SafeComponent {
  private destroy$ = new Subject<void>();
  private dataSubject$ = new Subject<any>();
  
  ngOnInit() {
    // Auto-cleanup with takeUntil
    interval(1000).pipe(
      takeUntil(this.destroy$)
    ).subscribe(val => console.log(val));
    
    // Event listener cleanup
    fromEvent(window, 'scroll').pipe(
      takeUntil(this.destroy$)
    ).subscribe(() => this.handleScroll());
    
    // HTTP request - auto-completes but explicit is better
    this.http.get('/api/data').pipe(
      take(1) // Ensure single emission
    ).subscribe(data => this.processData(data));
  }
  
  ngOnDestroy() {
    // Cleanup pattern
    this.destroy$.next();
    this.destroy$.complete();
    
    // Complete subjects to release observers
    this.dataSubject$.complete();
  }
}

// Subscription tracking for debugging
class DebugComponent {
  private subs: Subscription[] = [];
  
  ngOnInit() {
    this.subs.push(interval(1000).subscribe());
    this.subs.push(fromEvent(window, 'click').subscribe());
    console.log('Active subscriptions:', this.subs.length);
  }
  
  ngOnDestroy() {
    this.subs.forEach(sub => sub.unsubscribe());
    console.log('Cleaned up', this.subs.length, 'subscriptions');
  }
}
Warning: Infinite observables (interval, timer, fromEvent) are the primary source of memory leaks. Always use takeUntil() or explicit unsubscribe.

2.6 Subscription Containers and Group Management

Pattern Implementation Description Advantage
Subscription bag subs = new Subscription() Single parent subscription containing all children Single unsubscribe call cleans everything
Array container subs: Subscription[] = [] Array of subscriptions for iteration Easy to track count and iterate
Map container subs = new Map<string, Subscription>() Named subscriptions for selective cleanup Selective unsubscribe by key
SubSink pattern subs.sink = obs$.subscribe() Third-party library for subscription management Simplified API, automatic cleanup

Example: Subscription container patterns

import { Subscription, interval, fromEvent } from 'rxjs';

// Subscription bag pattern (recommended)
class SubscriptionBagComponent {
  private subscriptions = new Subscription();
  
  ngOnInit() {
    this.subscriptions.add(
      interval(1000).subscribe(val => this.tick(val))
    );
    this.subscriptions.add(
      fromEvent(window, 'resize').subscribe(() => this.resize())
    );
  }
  
  ngOnDestroy() {
    this.subscriptions.unsubscribe();
  }
}

// Array container pattern
class ArrayContainerComponent {
  private subscriptions: Subscription[] = [];
  
  ngOnInit() {
    this.subscriptions.push(interval(1000).subscribe());
    this.subscriptions.push(fromEvent(document, 'click').subscribe());
  }
  
  ngOnDestroy() {
    this.subscriptions.forEach(sub => sub.unsubscribe());
    this.subscriptions = [];
  }
}

// Map container for selective cleanup
class MapContainerComponent {
  private subscriptions = new Map<string, Subscription>();
  
  ngOnInit() {
    this.subscriptions.set('timer', 
      interval(1000).subscribe(val => console.log(val))
    );
    this.subscriptions.set('clicks', 
      fromEvent(document, 'click').subscribe(() => console.log('click'))
    );
  }
  
  stopTimer() {
    this.subscriptions.get('timer')?.unsubscribe();
    this.subscriptions.delete('timer');
  }
  
  ngOnDestroy() {
    this.subscriptions.forEach(sub => sub.unsubscribe());
    this.subscriptions.clear();
  }
}

// Reusable subscription manager
class SubscriptionManager {
  private subs = new Subscription();
  
  add(subscription: Subscription): void {
    this.subs.add(subscription);
  }
  
  unsubscribeAll(): void {
    this.subs.unsubscribe();
    this.subs = new Subscription();
  }
  
  get count(): number {
    // Track active subscriptions (custom implementation)
    return Object.keys(this.subs).length;
  }
}

Section 2 Summary

  • subscribe() returns Subscription object for lifecycle management
  • unsubscribe() must be called to prevent memory leaks in long-running streams
  • add() method allows subscription composition for bulk cleanup
  • takeUntil(destroy$) pattern is preferred for automatic cleanup
  • Always cleanup: intervals, timers, event listeners, and complete subjects in destroy hooks
  • Use subscription containers (bag/array/map) for organized group management

3. Transformation Operators Reference

3.1 map and mapTo for Value Transformation

Operator Syntax Description Use Case
map map(val => transformation) Applies projection function to each value, transforms and emits result Data transformation, property extraction, calculations
mapTo mapTo(constantValue) Maps every emission to same constant value, ignoring source Convert events to signals, normalize values

Example: map and mapTo transformations

import { of, fromEvent } from 'rxjs';
import { map, mapTo } from 'rxjs/operators';

// map - transform values
of(1, 2, 3).pipe(
  map(x => x * 10)
).subscribe(val => console.log(val)); // 10, 20, 30

// map - extract object properties
of(
  { name: 'Alice', age: 25 },
  { name: 'Bob', age: 30 }
).pipe(
  map(user => user.name)
).subscribe(name => console.log(name)); // 'Alice', 'Bob'

// map - complex transformation
of(1, 2, 3, 4, 5).pipe(
  map(n => ({ value: n, squared: n * n, isEven: n % 2 === 0 }))
).subscribe(obj => console.log(obj));

// mapTo - convert all values to constant
fromEvent(document, 'click').pipe(
  mapTo('Clicked!')
).subscribe(msg => console.log(msg)); // Always 'Clicked!'

// mapTo - signal transformation
of('a', 'b', 'c').pipe(
  mapTo(1)
).subscribe(val => console.log(val)); // 1, 1, 1

3.2 pluck for Property Extraction DEPRECATED

Operator Syntax Description Modern Alternative
pluck pluck('prop1', 'prop2') Extracts nested property by path - deprecated in RxJS 8 map(obj => obj.prop1.prop2)

Example: pluck deprecated - use map instead

import { of } from 'rxjs';
import { map } from 'rxjs/operators';

const users = of(
  { name: 'Alice', address: { city: 'NYC', zip: '10001' } },
  { name: 'Bob', address: { city: 'LA', zip: '90001' } }
);

// OLD - pluck (deprecated)
// users.pipe(pluck('address', 'city'))

// NEW - use map instead (recommended)
users.pipe(
  map(user => user.address.city)
).subscribe(city => console.log(city)); // 'NYC', 'LA'

// Multiple property extraction with map
users.pipe(
  map(user => ({
    name: user.name,
    city: user.address.city
  }))
).subscribe(data => console.log(data));

// Safe property access with optional chaining
of(
  { name: 'Alice', address: { city: 'NYC' } },
  { name: 'Bob' } // Missing address
).pipe(
  map(user => user.address?.city ?? 'Unknown')
).subscribe(city => console.log(city)); // 'NYC', 'Unknown'
Warning: pluck is deprecated as of RxJS 8. Use map() with property access or optional chaining instead.

3.3 scan and reduce for Accumulation Operations

Operator Syntax Description Emission Pattern
scan scan((acc, val) => acc + val, seed) Accumulates values over time, emits intermediate results (like Array.reduce but emits each step) Emits on every source emission
reduce reduce((acc, val) => acc + val, seed) Accumulates all values, emits only final result when source completes Single emission on completion

Example: scan vs reduce accumulation

import { of } from 'rxjs';
import { scan, reduce } from 'rxjs/operators';

// scan - emits intermediate accumulations
of(1, 2, 3, 4, 5).pipe(
  scan((acc, val) => acc + val, 0)
).subscribe(total => console.log('Scan:', total));
// Output: 1, 3, 6, 10, 15 (running total)

// reduce - emits only final result
of(1, 2, 3, 4, 5).pipe(
  reduce((acc, val) => acc + val, 0)
).subscribe(total => console.log('Reduce:', total));
// Output: 15 (only final total)

// scan - track state over time
of(1, -2, 3, -4, 5).pipe(
  scan((acc, val) => ({
    sum: acc.sum + val,
    count: acc.count + 1,
    min: Math.min(acc.min, val),
    max: Math.max(acc.max, val)
  }), { sum: 0, count: 0, min: Infinity, max: -Infinity })
).subscribe(stats => console.log(stats));

// Practical: running balance
const transactions = of(100, -50, 200, -75, 50);
transactions.pipe(
  scan((balance, amount) => balance + amount, 0)
).subscribe(balance => console.log('Balance:', balance));
// 100, 50, 250, 175, 225

// Practical: collect items into array
of('a', 'b', 'c').pipe(
  scan((arr, item) => [...arr, item], [])
).subscribe(arr => console.log('Array:', arr));
// ['a'], ['a','b'], ['a','b','c']
Note: Use scan for real-time state tracking and reduce when you only need the final accumulated result.

3.4 buffer, bufferTime, and bufferCount Operators

Operator Syntax Description Buffer Trigger
buffer buffer(notifier$) Collects values until notifier emits, then emits buffered array Observable emission
bufferTime bufferTime(timespan) Buffers values for specified time period (ms), emits array Time interval
bufferCount bufferCount(size, startEvery?) Buffers specified number of values, emits array when count reached Value count
bufferToggle bufferToggle(open$, close) Starts buffering on open$ emission, stops on closing observable Start/stop observables
bufferWhen bufferWhen(() => closing$) Buffers values, closing observable factory determines when to emit Dynamic closing observable

Example: Buffering strategies

import { interval, fromEvent } from 'rxjs';
import { buffer, bufferTime, bufferCount, take } from 'rxjs/operators';

// buffer - collect until notifier emits
const clicks$ = fromEvent(document, 'click');
interval(1000).pipe(
  buffer(clicks$),
  take(3)
).subscribe(buffered => console.log('Buffered:', buffered));
// Emits array of interval values when user clicks

// bufferTime - time-based buffering
interval(100).pipe(
  bufferTime(1000),
  take(3)
).subscribe(arr => console.log('Buffer:', arr));
// Emits ~10 values every 1 second: [0,1,2...9], [10,11...19], etc.

// bufferCount - count-based buffering
interval(100).pipe(
  bufferCount(5),
  take(3)
).subscribe(arr => console.log('Count buffer:', arr));
// [0,1,2,3,4], [5,6,7,8,9], [10,11,12,13,14]

// bufferCount with overlap
interval(100).pipe(
  bufferCount(3, 1), // size 3, start every 1
  take(5)
).subscribe(arr => console.log('Sliding:', arr));
// [0,1,2], [1,2,3], [2,3,4], [3,4,5], [4,5,6]

// Practical: batch API calls
const userActions$ = fromEvent(button, 'click').pipe(
  map(() => ({ action: 'click', timestamp: Date.now() }))
);
userActions$.pipe(
  bufferTime(5000), // Collect 5 seconds of actions
  filter(actions => actions.length > 0)
).subscribe(batch => sendToAnalytics(batch));

3.5 concatMap, mergeMap, and switchMap Higher-order Operators

Operator Syntax Concurrency Behavior Use Case
concatMap concatMap(val => obs$) Sequential (1) Waits for inner observable to complete before processing next Order-critical operations, sequential API calls
mergeMap mergeMap(val => obs$, concurrent?) Unlimited (or limit) Subscribes to all inner observables concurrently, emits as they arrive Parallel operations, independent async tasks
switchMap switchMap(val => obs$) Latest only Cancels previous inner observable when new value arrives Search/typeahead, latest-only results, navigation

Example: Higher-order mapping strategies

import { of, interval } from 'rxjs';
import { concatMap, mergeMap, switchMap, take, delay } from 'rxjs/operators';

// concatMap - sequential execution (waits for completion)
of(1, 2, 3).pipe(
  concatMap(n => 
    of(n * 10).pipe(delay(1000)) // Each waits for previous
  )
).subscribe(val => console.log('Concat:', val));
// Output at: 1s: 10, 2s: 20, 3s: 30

// mergeMap - concurrent execution (all run in parallel)
of(1, 2, 3).pipe(
  mergeMap(n => 
    of(n * 10).pipe(delay(1000)) // All start immediately
  )
).subscribe(val => console.log('Merge:', val));
// Output at: 1s: 10, 20, 30 (all at once)

// switchMap - cancel previous (only latest completes)
interval(1000).pipe(
  take(3),
  switchMap(n => 
    interval(700).pipe(
      take(3),
      map(i => `${n}-${i}`)
    )
  )
).subscribe(val => console.log('Switch:', val));
// Cancels previous inner observables when new value arrives

// Practical: Search typeahead with switchMap
const searchInput$ = fromEvent(inputElement, 'input');
searchInput$.pipe(
  debounceTime(300),
  map(e => e.target.value),
  switchMap(query => 
    this.http.get(`/api/search?q=${query}`) // Cancels previous request
  )
).subscribe(results => displayResults(results));

// Practical: Sequential file uploads with concatMap
const files$ = from([file1, file2, file3]);
files$.pipe(
  concatMap(file => uploadFile(file)) // Upload one at a time
).subscribe(response => console.log('Uploaded:', response));

// Practical: Parallel API calls with mergeMap
const userIds$ = of(1, 2, 3, 4, 5);
userIds$.pipe(
  mergeMap(id => 
    this.http.get(`/api/users/${id}`), // All requests in parallel
    3 // Limit to 3 concurrent requests
  )
).subscribe(user => console.log(user));
Note: switchMap for latest-only (search), mergeMap for parallel (independent tasks), concatMap for sequential order (queue processing).

3.6 exhaustMap for Ignore-while-active Pattern

Operator Syntax Behavior Use Case
exhaustMap exhaustMap(val => obs$) Ignores new source values while inner observable is active, prevents overlapping Login/submit buttons, prevent double-click, rate limiting

Example: exhaustMap for preventing concurrent operations

import { fromEvent, of } from 'rxjs';
import { exhaustMap, delay, tap } from 'rxjs/operators';

// exhaustMap - ignore clicks while request is pending
const loginButton = document.getElementById('login-btn');
fromEvent(loginButton, 'click').pipe(
  exhaustMap(() => 
    this.http.post('/api/login', credentials).pipe(
      delay(2000) // Simulates API call
    )
  )
).subscribe(response => console.log('Login response:', response));
// Ignores additional clicks until first request completes

// Compare with other operators
const clicks$ = fromEvent(button, 'click');

// switchMap - cancels previous request
clicks$.pipe(
  switchMap(() => apiCall()) // Latest click cancels previous
);

// mergeMap - allows concurrent requests
clicks$.pipe(
  mergeMap(() => apiCall()) // Every click creates new request
);

// exhaustMap - ignores new clicks while busy
clicks$.pipe(
  exhaustMap(() => apiCall()) // Ignores clicks during request
);

// Practical: Form submission protection
const submitButton$ = fromEvent(submitBtn, 'click');
submitButton$.pipe(
  exhaustMap(() => 
    this.http.post('/api/submit', formData).pipe(
      tap(() => console.log('Submitting...')),
      catchError(err => {
        console.error('Submission failed');
        return EMPTY;
      })
    )
  )
).subscribe(response => {
  console.log('Success:', response);
  resetForm();
});

// Practical: Refresh button with cooldown
fromEvent(refreshBtn, 'click').pipe(
  exhaustMap(() => 
    concat(
      this.http.get('/api/data'),
      timer(5000) // 5s cooldown before accepting new clicks
    )
  )
).subscribe(data => updateUI(data));

// Practical: Game actions (prevent spam)
const attackButton$ = fromEvent(gameButton, 'click');
attackButton$.pipe(
  exhaustMap(() => 
    of('Attack!').pipe(
      tap(() => playAnimation()),
      delay(1000) // Animation duration
    )
  )
).subscribe(action => executeAction(action));

Section 3 Summary

  • map transforms each value, mapTo replaces with constant
  • pluck is deprecated - use map(obj => obj.property) instead
  • scan emits intermediate results, reduce emits only final result
  • buffer variants collect values into arrays based on time, count, or signals
  • switchMap (latest), mergeMap (concurrent), concatMap (sequential)
  • exhaustMap prevents overlapping operations - ideal for button clicks and submissions

4. Filtering Operators Complete Guide

4.1 filter for Conditional Value Filtering

Operator Syntax Description Use Case
filter filter(predicate) Emits only values that satisfy predicate function (returns true) Conditional filtering, validation, data cleanup
filter with index filter((val, idx) => condition) Predicate receives value and index for position-based filtering Skip every nth item, index-based logic

Example: Conditional filtering patterns

import { of, fromEvent } from 'rxjs';
import { filter, map } from 'rxjs/operators';

// Basic filter - even numbers only
of(1, 2, 3, 4, 5, 6).pipe(
  filter(n => n % 2 === 0)
).subscribe(val => console.log(val)); // 2, 4, 6

// Filter objects by property
of(
  { name: 'Alice', age: 25 },
  { name: 'Bob', age: 17 },
  { name: 'Charlie', age: 30 }
).pipe(
  filter(user => user.age >= 18)
).subscribe(user => console.log(user.name)); // Alice, Charlie

// Filter with index
of(10, 20, 30, 40, 50).pipe(
  filter((val, idx) => idx % 2 === 0) // Every other item
).subscribe(val => console.log(val)); // 10, 30, 50

// Filter null/undefined values
of(1, null, 2, undefined, 3, null).pipe(
  filter(val => val != null) // != catches both null and undefined
).subscribe(val => console.log(val)); // 1, 2, 3

// Practical: filter valid keyboard inputs
fromEvent(input, 'keyup').pipe(
  map(e => e.target.value),
  filter(text => text.length >= 3), // Min 3 characters
  filter(text => /^[a-zA-Z0-9]+$/.test(text)) // Alphanumeric only
).subscribe(validInput => search(validInput));

// Practical: filter successful HTTP responses
this.http.get('/api/data').pipe(
  filter(response => response.status === 200),
  map(response => response.body)
).subscribe(data => processData(data));

4.2 take, takeUntil, and takeWhile Operators

Operator Syntax Description Completion
take take(count) Emits first N values, then completes After N emissions
takeUntil takeUntil(notifier$) Emits until notifier observable emits, then completes When notifier emits
takeWhile takeWhile(predicate, inclusive?) Emits while predicate is true, completes on first false When predicate returns false
takeLast takeLast(count) Emits last N values when source completes When source completes

Example: Take operator variants

import { interval, fromEvent, Subject } from 'rxjs';
import { take, takeUntil, takeWhile, takeLast } from 'rxjs/operators';

// take - first N values
interval(1000).pipe(
  take(5)
).subscribe(val => console.log(val)); // 0, 1, 2, 3, 4, then completes

// takeUntil - common cleanup pattern
const destroy$ = new Subject<void>();
interval(500).pipe(
  takeUntil(destroy$)
).subscribe(val => console.log(val));
// Later: destroy$.next() stops the interval

// takeWhile - conditional stopping
interval(500).pipe(
  takeWhile(n => n < 5)
).subscribe(val => console.log(val)); // 0, 1, 2, 3, 4

// takeWhile with inclusive flag
interval(500).pipe(
  takeWhile(n => n < 5, true) // Includes the failing value
).subscribe(val => console.log(val)); // 0, 1, 2, 3, 4, 5

// takeLast - buffer last N
of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10).pipe(
  takeLast(3)
).subscribe(val => console.log(val)); // 8, 9, 10

// Practical: stop on button click
const stop$ = fromEvent(stopButton, 'click');
interval(1000).pipe(
  takeUntil(stop$)
).subscribe(tick => updateTimer(tick));

// Practical: take until condition met
const temps$ = interval(1000).pipe(
  map(() => readTemperature())
);
temps$.pipe(
  takeWhile(temp => temp < 100, true) // Include threshold value
).subscribe(temp => console.log('Temp:', temp));

// Practical: Component lifecycle
class Component {
  private destroy$ = new Subject<void>();
  
  ngOnInit() {
    this.dataService.updates$.pipe(
      takeUntil(this.destroy$)
    ).subscribe(data => this.render(data));
  }
  
  ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

4.3 first, last, and single Value Extraction

Operator Syntax Description Error Condition
first first(predicate?, defaultValue?) Emits first value (or first matching predicate), then completes Errors if empty and no default
last last(predicate?, defaultValue?) Emits last value (or last matching predicate) when source completes Errors if empty and no default
single single(predicate?) Emits single value - errors if zero or multiple emissions Errors if not exactly one emission

Example: Value extraction operators

import { of, EMPTY } from 'rxjs';
import { first, last, single } from 'rxjs/operators';

// first - get first emission
of(1, 2, 3, 4, 5).pipe(
  first()
).subscribe(val => console.log(val)); // 1

// first with predicate
of(1, 2, 3, 4, 5).pipe(
  first(n => n > 3)
).subscribe(val => console.log(val)); // 4

// first with default (prevents error on empty)
EMPTY.pipe(
  first(undefined, 'default')
).subscribe(val => console.log(val)); // 'default'

// last - get last emission
of(1, 2, 3, 4, 5).pipe(
  last()
).subscribe(val => console.log(val)); // 5

// last with predicate
of(1, 2, 3, 4, 5).pipe(
  last(n => n < 4)
).subscribe(val => console.log(val)); // 3

// single - must have exactly one emission
of(42).pipe(
  single()
).subscribe(val => console.log(val)); // 42

// single - errors on multiple values
of(1, 2, 3).pipe(
  single()
).subscribe(
  val => console.log(val),
  err => console.error('Error: Multiple values') // Triggered
);

// single with predicate
of(1, 2, 3, 4, 5).pipe(
  single(n => n === 3)
).subscribe(val => console.log(val)); // 3

// Practical: find first matching user
this.users$.pipe(
  first(user => user.id === targetId)
).subscribe(user => displayUser(user));

// Practical: get final calculation result
calculations$.pipe(
  last()
).subscribe(finalResult => saveResult(finalResult));

// Practical: ensure unique result
this.http.get('/api/config').pipe(
  single() // Validates exactly one response
).subscribe(config => applyConfig(config));
Warning: first() and last() throw errors on empty streams without default values. Use defaults or defaultIfEmpty() for safety.

4.4 skip, skipUntil, and skipWhile Operators

Operator Syntax Description Skip Criteria
skip skip(count) Skips first N values, emits all subsequent values First N emissions
skipUntil skipUntil(notifier$) Skips all values until notifier emits, then emits all Until notifier emits
skipWhile skipWhile(predicate) Skips values while predicate is true, emits rest (opposite of takeWhile) While predicate is true
skipLast skipLast(count) Skips last N values when source completes Last N emissions

Example: Skip operator patterns

import { interval, fromEvent, timer } from 'rxjs';
import { skip, skipUntil, skipWhile, skipLast, take } from 'rxjs/operators';

// skip - ignore first N
of(1, 2, 3, 4, 5).pipe(
  skip(2)
).subscribe(val => console.log(val)); // 3, 4, 5

// skipUntil - start emitting after trigger
const trigger$ = timer(3000); // Emit after 3 seconds
interval(500).pipe(
  skipUntil(trigger$),
  take(5)
).subscribe(val => console.log(val)); // Starts after 3s

// skipWhile - skip until condition false
of(1, 2, 3, 4, 5, 1, 2).pipe(
  skipWhile(n => n < 4)
).subscribe(val => console.log(val)); // 4, 5, 1, 2

// skipLast - omit last N
of(1, 2, 3, 4, 5, 6, 7, 8).pipe(
  skipLast(3)
).subscribe(val => console.log(val)); // 1, 2, 3, 4, 5

// Practical: skip initial loading state
this.dataService.state$.pipe(
  skip(1) // Skip initial/default state
).subscribe(state => updateUI(state));

// Practical: wait for user authentication
const authComplete$ = this.authService.authenticated$;
this.appData$.pipe(
  skipUntil(authComplete$)
).subscribe(data => loadUserData(data));

// Practical: skip warmup values
sensorReadings$.pipe(
  skipWhile(reading => reading.temperature < 20) // Skip until warmed up
).subscribe(reading => processReading(reading));

// Practical: pagination - skip previous pages
const pageSize = 10;
const page = 3;
allItems$.pipe(
  skip(pageSize * (page - 1)),
  take(pageSize)
).subscribe(items => displayPage(items));

4.5 distinct and distinctUntilChanged Deduplication

Operator Syntax Description Memory Impact
distinct distinct(keySelector?, flushes?) Emits only unique values never seen before in entire stream High - stores all seen values
distinctUntilChanged distinctUntilChanged(compare?) Emits only when current value differs from previous (consecutive duplicates) Low - only stores previous value
distinctUntilKeyChanged distinctUntilKeyChanged(key, compare?) Like distinctUntilChanged but compares specific object property Low - stores previous property value

Example: Deduplication strategies

import { of } from 'rxjs';
import { distinct, distinctUntilChanged, distinctUntilKeyChanged } from 'rxjs/operators';

// distinct - all unique values in entire stream
of(1, 2, 2, 3, 1, 4, 3, 5).pipe(
  distinct()
).subscribe(val => console.log(val)); // 1, 2, 3, 4, 5

// distinct with key selector
of(
  { id: 1, name: 'Alice' },
  { id: 2, name: 'Bob' },
  { id: 1, name: 'Alice Again' } // Duplicate id
).pipe(
  distinct(user => user.id)
).subscribe(user => console.log(user.name)); // Alice, Bob

// distinctUntilChanged - consecutive duplicates only
of(1, 1, 2, 2, 2, 3, 1, 1, 4).pipe(
  distinctUntilChanged()
).subscribe(val => console.log(val)); // 1, 2, 3, 1, 4

// distinctUntilChanged with custom comparator
of(
  { value: 1 },
  { value: 1 }, // Same value
  { value: 2 }
).pipe(
  distinctUntilChanged((prev, curr) => prev.value === curr.value)
).subscribe(obj => console.log(obj)); // First and third only

// distinctUntilKeyChanged - compare object property
of(
  { status: 'loading' },
  { status: 'loading' }, // Duplicate
  { status: 'success' },
  { status: 'success' }  // Duplicate
).pipe(
  distinctUntilKeyChanged('status')
).subscribe(state => console.log(state.status)); // loading, success

// Practical: search input deduplication
fromEvent(searchInput, 'input').pipe(
  map(e => e.target.value),
  distinctUntilChanged(), // Only when value actually changes
  debounceTime(300)
).subscribe(query => search(query));

// Practical: state change detection
this.store.select('user').pipe(
  distinctUntilKeyChanged('userId'), // Only when userId changes
  switchMap(user => this.loadUserData(user.userId))
).subscribe(data => updateUI(data));

// Practical: unique IDs from stream
eventStream$.pipe(
  map(event => event.id),
  distinct() // Collect all unique IDs
).subscribe(uniqueId => processUniqueId(uniqueId));

// Performance note: distinct with flush
const flush$ = interval(10000); // Clear cache every 10s
dataStream$.pipe(
  distinct(item => item.id, flush$) // Periodically clear stored values
).subscribe(item => processItem(item));
Note: Use distinctUntilChanged for most cases (low memory). Use distinct only when you need global uniqueness and can manage memory.

4.6 debounceTime and throttleTime Rate Limiting

Operator Syntax Behavior Use Case
debounceTime debounceTime(duration) Emits value after silence period (no new values for duration ms) Search input, resize events, autocomplete
throttleTime throttleTime(duration, config?) Emits first value, ignores subsequent for duration, repeats Scroll events, button clicks, API rate limiting
auditTime auditTime(duration) Emits most recent value after duration (trailing edge) Mouse move tracking, continuous updates
sampleTime sampleTime(period) Periodically emits most recent value at fixed intervals Periodic sampling, monitoring

Example: Rate limiting strategies

import { fromEvent, interval } from 'rxjs';
import { debounceTime, throttleTime, auditTime, sampleTime, map } from 'rxjs/operators';

// debounceTime - wait for pause in emissions
fromEvent(searchInput, 'input').pipe(
  map(e => e.target.value),
  debounceTime(500) // Wait 500ms after user stops typing
).subscribe(query => searchAPI(query));

// throttleTime - first value, then silence
fromEvent(button, 'click').pipe(
  throttleTime(2000) // First click, ignore for 2s, repeat
).subscribe(() => console.log('Throttled click'));

// throttleTime with trailing edge
fromEvent(window, 'scroll').pipe(
  throttleTime(1000, { leading: true, trailing: true })
).subscribe(() => checkScrollPosition());

// auditTime - most recent value after period
fromEvent(document, 'mousemove').pipe(
  auditTime(1000) // Emit latest mouse position every 1s
).subscribe(event => updateTooltip(event));

// sampleTime - periodic sampling
const temperature$ = interval(100).pipe(
  map(() => Math.random() * 100)
);
temperature$.pipe(
  sampleTime(1000) // Sample every 1 second
).subscribe(temp => displayTemperature(temp));

// Practical comparison - resize handling
const resize$ = fromEvent(window, 'resize');

// debounceTime - wait for resize to finish
resize$.pipe(
  debounceTime(300)
).subscribe(() => recalculateLayout()); // After resize complete

// throttleTime - periodic updates during resize
resize$.pipe(
  throttleTime(100)
).subscribe(() => updatePreview()); // Every 100ms while resizing

// Practical: autocomplete with debounce
fromEvent(input, 'input').pipe(
  map(e => e.target.value),
  filter(text => text.length >= 3),
  debounceTime(300),
  distinctUntilChanged(),
  switchMap(query => this.http.get(`/api/search?q=${query}`))
).subscribe(results => displaySuggestions(results));

// Practical: save button with throttle
fromEvent(saveBtn, 'click').pipe(
  throttleTime(3000) // Prevent rapid saves
).subscribe(() => saveDocument());

// Practical: scroll position tracking
fromEvent(window, 'scroll').pipe(
  auditTime(100), // Latest position every 100ms
  map(() => window.scrollY)
).subscribe(scrollY => updateScrollIndicator(scrollY));

Section 4 Summary

  • filter emits only values satisfying predicate condition
  • take/takeUntil/takeWhile limit emissions from start, skip variants limit from end
  • first/last/single extract specific values with optional predicates
  • distinctUntilChanged removes consecutive duplicates (low memory cost)
  • debounceTime for user input (wait for pause), throttleTime for events (periodic limiting)
  • Rate limiting: debounce (trailing), throttle (leading), audit (trailing periodic), sample (fixed interval)

5. Combination Operators and Stream Merging

5.1 merge and mergeAll for Concurrent Combination

Operator Syntax Description Emission Pattern
merge merge(obs1$, obs2$, ...) Combines multiple observables concurrently - emits values as they arrive from any source Interleaved, order by arrival time
obs$.pipe(merge()) obs1$.pipe(merge(obs2$, obs3$)) Pipeable version - merges source with additional observables Same as merge creation operator
mergeAll higherOrder$.pipe(mergeAll(concurrent?)) Flattens higher-order observable by subscribing to all inner observables concurrently Concurrent inner subscriptions

Example: Concurrent stream combination with merge

import { merge, interval, fromEvent, of } from 'rxjs';
import { map, mergeAll, take } from 'rxjs/operators';

// merge - combine multiple sources
const fast$ = interval(500).pipe(map(x => `Fast: ${x}`), take(5));
const slow$ = interval(1000).pipe(map(x => `Slow: ${x}`), take(3));
merge(fast$, slow$).subscribe(val => console.log(val));
// Output: Fast:0, Slow:0, Fast:1, Fast:2, Slow:1, Fast:3, Fast:4, Slow:2

// Pipeable merge
of(1, 2, 3).pipe(
  merge(of(4, 5, 6), of(7, 8, 9))
).subscribe(val => console.log(val)); // 1,2,3,4,5,6,7,8,9

// mergeAll - flatten higher-order observable
const clicks$ = fromEvent(document, 'click');
clicks$.pipe(
  map(() => interval(1000).pipe(take(3))),
  mergeAll() // Subscribe to all inner intervals concurrently
).subscribe(val => console.log(val));

// mergeAll with concurrency limit
const urls = ['url1', 'url2', 'url3', 'url4', 'url5'];
from(urls).pipe(
  map(url => ajax.get(url)),
  mergeAll(2) // Max 2 concurrent HTTP requests
).subscribe(response => console.log(response));

// Practical: combine multiple data sources
const user$ = this.http.get('/api/user');
const settings$ = this.http.get('/api/settings');
const notifications$ = this.http.get('/api/notifications');

merge(user$, settings$, notifications$).subscribe(
  data => console.log('Data arrived:', data)
);

// Practical: multiple event sources
const keyPress$ = fromEvent(document, 'keypress').pipe(mapTo('key'));
const mouseClick$ = fromEvent(document, 'click').pipe(mapTo('click'));
const touchTap$ = fromEvent(document, 'touchstart').pipe(mapTo('touch'));

merge(keyPress$, mouseClick$, touchTap$).pipe(
  scan((count) => count + 1, 0)
).subscribe(count => console.log('Total interactions:', count));

5.2 concat and concatAll for Sequential Combination

Operator Syntax Description Emission Pattern
concat concat(obs1$, obs2$, ...) Combines observables sequentially - waits for each to complete before subscribing to next Sequential, preserves order
obs$.pipe(concat()) obs1$.pipe(concat(obs2$, obs3$)) Pipeable version - concatenates after source completes Source first, then additional streams
concatAll higherOrder$.pipe(concatAll()) Flattens higher-order observable by subscribing to inner observables one at a time Queue-based sequential processing

Example: Sequential stream combination with concat

import { concat, of, interval } from 'rxjs';
import { concatAll, take, delay } from 'rxjs/operators';

// concat - sequential execution
const first$ = of(1, 2, 3);
const second$ = of(4, 5, 6);
const third$ = of(7, 8, 9);
concat(first$, second$, third$).subscribe(val => console.log(val));
// Output: 1,2,3,4,5,6,7,8,9 (in order)

// concat waits for completion
const slow$ = interval(1000).pipe(take(3));
const fast$ = of('a', 'b', 'c');
concat(slow$, fast$).subscribe(val => console.log(val));
// Output: 0,1,2 (over 3 seconds), then immediately a,b,c

// Pipeable concat
of(1, 2, 3).pipe(
  concat(of(4, 5, 6))
).subscribe(val => console.log(val));

// concatAll - sequential flattening
of(
  of(1, 2, 3),
  of(4, 5, 6),
  of(7, 8, 9)
).pipe(
  concatAll()
).subscribe(val => console.log(val)); // 1,2,3,4,5,6,7,8,9

// Practical: sequential API calls
const deleteUser = (id) => this.http.delete(`/api/users/${id}`);
const userIds = [1, 2, 3];
from(userIds).pipe(
  map(id => deleteUser(id)),
  concatAll() // Delete one at a time, in order
).subscribe(
  result => console.log('Deleted:', result),
  err => console.error('Error:', err)
);

// Practical: animation sequence
const animations = [
  animate('fadeIn', 500),
  animate('slideUp', 300),
  animate('bounce', 400)
];
concat(...animations).subscribe(
  animation => console.log('Playing:', animation),
  null,
  () => console.log('Animation sequence complete')
);

// Practical: onboarding flow
const step1$ = showWelcome().pipe(delay(2000));
const step2$ = showFeatures().pipe(delay(3000));
const step3$ = showTutorial().pipe(delay(2000));
concat(step1$, step2$, step3$).subscribe(
  step => displayStep(step),
  null,
  () => completeOnboarding()
);
Note: merge for concurrent (parallel), concat for sequential (queue). Use concat when order matters or operations must not overlap.

5.3 combineLatest and combineLatestWith Multi-stream Coordination

Operator Syntax Description Emission Trigger
combineLatest combineLatest([obs1$, obs2$, ...]) Waits for all sources to emit once, then emits array with latest from each on any change Any source emits (after all emitted once)
combineLatest (dict) combineLatest({key1: obs1$, key2: obs2$}) Object version - emits object with latest values keyed by property names Any source emits (after all emitted once)
combineLatestWith obs1$.pipe(combineLatestWith(obs2$, obs3$)) Pipeable version - combines source with additional observables Same as combineLatest

Example: Multi-stream coordination with combineLatest

import { combineLatest, of, interval } from 'rxjs';
import { combineLatestWith, take, map } from 'rxjs/operators';

// combineLatest - array form
const age$ = of(25, 26, 27);
const name$ = of('Alice', 'Bob');
combineLatest([age$, name$]).subscribe(
  ([age, name]) => console.log(`${name} is ${age}`)
);
// Bob is 25, Bob is 26, Bob is 27

// combineLatest - object form (recommended)
const firstName$ = of('John');
const lastName$ = of('Doe', 'Smith');
const age2$ = of(30, 31);
combineLatest({
  firstName: firstName$,
  lastName: lastName$,
  age: age2$
}).subscribe(user => console.log(user));
// {firstName: 'John', lastName: 'Doe', age: 30}
// {firstName: 'John', lastName: 'Doe', age: 31}
// {firstName: 'John', lastName: 'Smith', age: 31}

// combineLatestWith - pipeable
interval(1000).pipe(
  take(3),
  combineLatestWith(interval(1500).pipe(take(2)))
).subscribe(([a, b]) => console.log(`a: ${a}, b: ${b}`));

// Practical: form validation
const username$ = fromEvent(usernameInput, 'input').pipe(
  map(e => e.target.value)
);
const password$ = fromEvent(passwordInput, 'input').pipe(
  map(e => e.target.value)
);
const terms$ = fromEvent(termsCheckbox, 'change').pipe(
  map(e => e.target.checked)
);

combineLatest({
  username: username$,
  password: password$,
  terms: terms$
}).pipe(
  map(form => ({
    ...form,
    valid: form.username.length > 3 && 
           form.password.length > 8 && 
           form.terms
  }))
).subscribe(form => {
  submitButton.disabled = !form.valid;
});

// Practical: derived data from multiple sources
const temperature$ = sensorData$.pipe(map(d => d.temp));
const humidity$ = sensorData$.pipe(map(d => d.humidity));
const pressure$ = sensorData$.pipe(map(d => d.pressure));

combineLatest({
  temp: temperature$,
  humidity: humidity$,
  pressure: pressure$
}).pipe(
  map(readings => calculateComfortIndex(readings))
).subscribe(index => displayComfortLevel(index));

// Practical: multi-filter search
const nameFilter$ = new BehaviorSubject('');
const categoryFilter$ = new BehaviorSubject('all');
const priceRange$ = new BehaviorSubject({ min: 0, max: 1000 });

combineLatest([nameFilter$, categoryFilter$, priceRange$]).pipe(
  debounceTime(300),
  switchMap(([name, category, price]) => 
    this.http.get('/api/products', { 
      params: { name, category, minPrice: price.min, maxPrice: price.max }
    })
  )
).subscribe(products => displayProducts(products));

5.4 withLatestFrom for Auxiliary Stream Integration

Operator Syntax Description Emission Trigger
withLatestFrom source$.pipe(withLatestFrom(other$)) When source emits, combine with latest value from other observable(s) Only source emissions (not other)
Multiple sources withLatestFrom(obs1$, obs2$, obs3$) Combines with latest from multiple auxiliary streams Only source emissions

Example: Auxiliary stream integration with withLatestFrom

import { fromEvent, interval, BehaviorSubject } from 'rxjs';
import { withLatestFrom, map, take } from 'rxjs/operators';

// withLatestFrom - basic usage
const clicks$ = fromEvent(button, 'click');
const timer$ = interval(1000);
clicks$.pipe(
  withLatestFrom(timer$)
).subscribe(([click, time]) => console.log(`Click at time: ${time}`));
// Only emits when button clicked, includes latest timer value

// Compare with combineLatest
const source$ = interval(1000).pipe(take(3));
const other$ = interval(500).pipe(take(5));

// combineLatest - emits on ANY change
combineLatest([source$, other$]).subscribe(
  ([s, o]) => console.log(`combineLatest: ${s}, ${o}`)
);

// withLatestFrom - emits only on source change
source$.pipe(
  withLatestFrom(other$)
).subscribe(
  ([s, o]) => console.log(`withLatestFrom: ${s}, ${o}`)
);

// withLatestFrom multiple sources
const userAction$ = fromEvent(actionButton, 'click');
const userState$ = new BehaviorSubject({ id: 1, role: 'user' });
const appConfig$ = new BehaviorSubject({ theme: 'dark', locale: 'en' });

userAction$.pipe(
  withLatestFrom(userState$, appConfig$)
).subscribe(([event, user, config]) => {
  console.log('Action by:', user.id);
  console.log('Theme:', config.theme);
  performAction(event, user, config);
});

// Practical: save with current state
const saveButton$ = fromEvent(saveBtn, 'click');
const formState$ = new BehaviorSubject({});
const userId$ = new BehaviorSubject(null);

saveButton$.pipe(
  withLatestFrom(formState$, userId$),
  switchMap(([_, formData, userId]) => 
    this.http.post('/api/save', { userId, data: formData })
  )
).subscribe(result => showSuccessMessage(result));

// Practical: enriched analytics
const clickEvent$ = fromEvent(document, 'click');
const currentPage$ = new BehaviorSubject('/home');
const sessionData$ = new BehaviorSubject({ sessionId: 'abc123' });

clickEvent$.pipe(
  withLatestFrom(currentPage$, sessionData$),
  map(([event, page, session]) => ({
    type: 'click',
    element: event.target.tagName,
    page: page,
    sessionId: session.sessionId,
    timestamp: Date.now()
  }))
).subscribe(analyticsData => sendToAnalytics(analyticsData));

// Practical: game input with state
const keyPress$ = fromEvent(document, 'keydown');
const playerState$ = new BehaviorSubject({ 
  x: 0, y: 0, health: 100, ammo: 50 
});

keyPress$.pipe(
  withLatestFrom(playerState$),
  map(([event, state]) => processInput(event.key, state))
).subscribe(newState => playerState$.next(newState));
Note: combineLatest emits when ANY source changes. withLatestFrom emits ONLY when the source emits, sampling others.

5.5 zip and zipWith for Paired Value Emission

Operator Syntax Description Emission Pattern
zip zip([obs1$, obs2$, ...]) Pairs values by index - emits when all sources have emitted at that position Synchronized by index, slowest determines rate
zipWith obs1$.pipe(zipWith(obs2$, obs3$)) Pipeable version - zips source with additional observables Same as zip

Example: Paired value emission with zip

import { zip, interval, of } from 'rxjs';
import { zipWith, take, map } from 'rxjs/operators';

// zip - pair by index
const numbers$ = of(1, 2, 3, 4, 5);
const letters$ = of('a', 'b', 'c');
zip([numbers$, letters$]).subscribe(
  ([num, letter]) => console.log(`${num}${letter}`)
);
// Output: 1a, 2b, 3c (stops when letters$ completes)

// zip with different speeds
const fast$ = interval(500).pipe(take(5));
const slow$ = interval(1000).pipe(take(3));
zip([fast$, slow$]).subscribe(
  ([f, s]) => console.log(`Fast: ${f}, Slow: ${s}`)
);
// Emits at slow rate: [0,0], [1,1], [2,2]

// zipWith - pipeable form
of(1, 2, 3).pipe(
  zipWith(of('a', 'b', 'c'), of('X', 'Y', 'Z'))
).subscribe(
  ([num, lower, upper]) => console.log(`${num}${lower}${upper}`)
); // 1aX, 2bY, 3cZ

// Practical: parallel API calls with pairing
const userIds$ = of(1, 2, 3);
const users$ = userIds$.pipe(
  mergeMap(id => this.http.get(`/api/users/${id}`))
);
const posts$ = userIds$.pipe(
  mergeMap(id => this.http.get(`/api/posts/${id}`))
);

zip([users$, posts$]).subscribe(
  ([user, posts]) => displayUserWithPosts(user, posts)
);

// Practical: coordinate animations
const element1Anim$ = animate(element1, 'fadeIn', 1000);
const element2Anim$ = animate(element2, 'slideUp', 1500);
const element3Anim$ = animate(element3, 'zoomIn', 800);

zip([element1Anim$, element2Anim$, element3Anim$]).subscribe(
  () => console.log('All animations complete')
);

// Practical: batch processing with paired data
const tasks$ = from(['task1', 'task2', 'task3']);
const resources$ = from([{ cpu: 1 }, { cpu: 2 }, { cpu: 3 }]);

zip([tasks$, resources$]).pipe(
  concatMap(([task, resource]) => processTask(task, resource))
).subscribe(result => console.log('Processed:', result));

// Practical: test data generation
const timestamps$ = interval(100).pipe(take(5));
const values$ = of(10, 20, 30, 40, 50);
const labels$ = of('A', 'B', 'C', 'D', 'E');

zip([timestamps$, values$, labels$]).pipe(
  map(([time, value, label]) => ({ time, value, label }))
).subscribe(dataPoint => chartData.push(dataPoint));
Warning: zip completes when the shortest observable completes. Unused values from longer observables are discarded.

5.6 startWith and endWith for Stream Initialization/Termination

Operator Syntax Description Use Case
startWith startWith(...values) Emits specified values before source observable emissions begin Initial/default values, loading states
endWith endWith(...values) Emits specified values after source observable completes Final/sentinel values, completion markers

Example: Stream initialization and termination

import { of, fromEvent, interval } from 'rxjs';
import { startWith, endWith, map, take, scan } from 'rxjs/operators';

// startWith - provide initial value
of(1, 2, 3).pipe(
  startWith(0)
).subscribe(val => console.log(val)); // 0, 1, 2, 3

// startWith multiple values
of('b', 'c').pipe(
  startWith('a', 'a.5')
).subscribe(val => console.log(val)); // a, a.5, b, c

// endWith - append final values
of(1, 2, 3).pipe(
  endWith(4, 5)
).subscribe(val => console.log(val)); // 1, 2, 3, 4, 5

// Combined startWith and endWith
of('middle').pipe(
  startWith('start'),
  endWith('end')
).subscribe(val => console.log(val)); // start, middle, end

// Practical: loading state management
this.http.get('/api/data').pipe(
  map(data => ({ loading: false, data })),
  startWith({ loading: true, data: null })
).subscribe(state => {
  if (state.loading) {
    showSpinner();
  } else {
    hideSpinner();
    displayData(state.data);
  }
});

// Practical: search with empty initial state
const searchQuery$ = fromEvent(searchInput, 'input').pipe(
  map(e => e.target.value),
  debounceTime(300)
);

searchQuery$.pipe(
  startWith(''), // Empty search on load
  switchMap(query => 
    query ? this.http.get(`/api/search?q=${query}`) : of([])
  )
).subscribe(results => displayResults(results));

// Practical: counter with initial value
fromEvent(incrementBtn, 'click').pipe(
  scan(count => count + 1, 0),
  startWith(0) // Show initial count
).subscribe(count => updateCountDisplay(count));

// Practical: data stream with header/footer
const dataRows$ = from([
  { id: 1, name: 'Alice' },
  { id: 2, name: 'Bob' },
  { id: 3, name: 'Charlie' }
]);

dataRows$.pipe(
  startWith({ type: 'header', columns: ['ID', 'Name'] }),
  endWith({ type: 'footer', total: 3 })
).subscribe(row => {
  if (row.type === 'header') {
    renderHeader(row.columns);
  } else if (row.type === 'footer') {
    renderFooter(row.total);
  } else {
    renderRow(row);
  }
});

// Practical: event stream with markers
interval(1000).pipe(
  take(5),
  map(n => `Event ${n}`),
  startWith('Stream started'),
  endWith('Stream completed')
).subscribe(msg => console.log(msg));

// Practical: form with defaults
const formValue$ = formControl.valueChanges;
formValue$.pipe(
  startWith({ name: '', email: '', age: 0 }) // Default values
).subscribe(value => validateForm(value));

Section 5 Summary

  • merge combines streams concurrently (interleaved), concat combines sequentially (queued)
  • combineLatest emits array/object when ANY source changes (after all emit once)
  • withLatestFrom emits only when source emits, sampling latest from others
  • zip pairs values by index position, rate limited by slowest source
  • startWith provides initial values, endWith appends final values
  • Choose: merge (parallel), concat (sequential), combineLatest (sync all), withLatestFrom (main+aux), zip (paired)

6. Error Handling and Recovery Patterns

6.1 catchError for Exception Handling and Recovery

Operator Syntax Description Return Behavior
catchError catchError((err, caught) => obs$) Catches errors and returns recovery observable - prevents stream termination Must return observable (fallback, retry, or EMPTY)
caught argument catchError((err, caught) => caught) Second parameter is source observable - enables restart/retry patterns Return caught to retry from beginning

Example: Error catching and recovery

import { of, throwError, EMPTY } from 'rxjs';
import { catchError, map } from 'rxjs/operators';

// Basic error catching with fallback
of(1, 2, 3, 4, 5).pipe(
  map(n => {
    if (n === 3) throw new Error('Error at 3');
    return n * 10;
  }),
  catchError(err => {
    console.error('Caught:', err.message);
    return of(999); // Fallback value
  })
).subscribe(val => console.log(val)); // 10, 20, 999, completes

// catchError with EMPTY - suppress error, complete silently
throwError(() => new Error('Fail')).pipe(
  catchError(err => {
    console.error('Error suppressed:', err.message);
    return EMPTY; // Complete without emitting
  })
).subscribe({
  next: val => console.log(val),
  complete: () => console.log('Completed')
});

// catchError in HTTP request
this.http.get('/api/data').pipe(
  catchError(err => {
    console.error('API Error:', err);
    return of({ data: [], fromCache: true }); // Fallback data
  })
).subscribe(response => displayData(response.data));

// Multiple catchError for different error types
this.http.get('/api/users').pipe(
  map(response => response.data),
  catchError(err => {
    if (err.status === 404) {
      return of([]); // Empty array for not found
    } else if (err.status === 401) {
      this.router.navigate(['/login']);
      return EMPTY;
    } else {
      return throwError(() => err); // Re-throw other errors
    }
  })
).subscribe(users => displayUsers(users));

// Practical: graceful degradation
const primaryApi$ = this.http.get('/api/primary');
const fallbackApi$ = this.http.get('/api/fallback');

primaryApi$.pipe(
  catchError(primaryErr => {
    console.warn('Primary failed, trying fallback');
    return fallbackApi$.pipe(
      catchError(fallbackErr => {
        console.error('Both failed');
        return of({ error: 'Service unavailable' });
      })
    );
  })
).subscribe(data => processData(data));

// Practical: error with user notification
this.userService.deleteUser(userId).pipe(
  catchError(err => {
    this.notificationService.showError('Failed to delete user');
    this.logger.error('Delete error:', err);
    return EMPTY; // Suppress error after handling
  })
).subscribe(() => this.notificationService.showSuccess('User deleted'));

// Using caught parameter for retry
let retryCount = 0;
const source$ = throwError(() => new Error('Fail'));
source$.pipe(
  catchError((err, caught) => {
    retryCount++;
    if (retryCount < 3) {
      console.log(`Retry ${retryCount}`);
      return caught; // Retry by returning source
    }
    return of('Failed after retries');
  })
).subscribe(val => console.log(val));
Note: catchError must return an Observable. Use EMPTY to complete silently, of(value) for fallback, or throwError to re-throw.

6.2 retry and retryWhen for Automatic Retry Logic

Operator Syntax Description Strategy
retry retry(count?) Resubscribes to source on error, up to count times (infinite if omitted) Immediate retry, no delay
retry (config) retry({ count, delay, resetOnSuccess }) Configurable retry with delay and reset options Delayed retry with backoff support
retryWhen retryWhen(notifier => notifier.pipe(...)) Custom retry logic based on error notification stream Fully customizable (exponential backoff, conditional)

Example: Retry strategies

import { throwError, timer, of } from 'rxjs';
import { retry, retryWhen, mergeMap, tap, delay } from 'rxjs/operators';

// retry - simple retry N times
let attempt = 0;
const unstable$ = new Observable(subscriber => {
  attempt++;
  if (attempt < 3) {
    subscriber.error(new Error(`Attempt ${attempt} failed`));
  } else {
    subscriber.next('Success!');
    subscriber.complete();
  }
});

unstable$.pipe(
  retry(2) // Retry up to 2 times (3 total attempts)
).subscribe({
  next: val => console.log(val),
  error: err => console.error('Final error:', err)
});

// retry with delay configuration
this.http.get('/api/data').pipe(
  retry({
    count: 3,
    delay: 1000, // Wait 1s between retries
    resetOnSuccess: true
  })
).subscribe(data => processData(data));

// retryWhen - exponential backoff
this.http.get('/api/users').pipe(
  retryWhen(errors => 
    errors.pipe(
      mergeMap((err, index) => {
        const retryAttempt = index + 1;
        if (retryAttempt > 3) {
          return throwError(() => err); // Max retries exceeded
        }
        const delayTime = Math.pow(2, retryAttempt) * 1000; // Exponential backoff
        console.log(`Retry ${retryAttempt} after ${delayTime}ms`);
        return timer(delayTime);
      })
    )
  )
).subscribe(
  users => displayUsers(users),
  err => showError('Failed after retries')
);

// retryWhen - conditional retry based on error type
this.http.post('/api/save', data).pipe(
  retryWhen(errors => 
    errors.pipe(
      mergeMap((err, index) => {
        // Retry only on network errors, not client errors
        if (err.status >= 500 && index < 3) {
          return timer((index + 1) * 2000); // 2s, 4s, 6s
        }
        return throwError(() => err); // Don't retry client errors
      })
    )
  )
).subscribe(
  response => handleSuccess(response),
  err => handleError(err)
);

// Practical: retry with max delay cap
const maxDelay = 10000; // Cap at 10 seconds
apiCall$.pipe(
  retryWhen(errors => 
    errors.pipe(
      mergeMap((err, index) => {
        if (index >= 5) return throwError(() => err);
        const backoff = Math.min(Math.pow(2, index) * 1000, maxDelay);
        return timer(backoff);
      })
    )
  )
).subscribe(data => processData(data));

// Practical: retry with user notification
this.http.get('/api/critical-data').pipe(
  retryWhen(errors => 
    errors.pipe(
      tap(() => this.notificationService.show('Retrying...')),
      delay(2000),
      take(3),
      concat(throwError(() => new Error('Max retries reached')))
    )
  ),
  catchError(err => {
    this.notificationService.showError('Unable to load data');
    return of(null);
  })
).subscribe(data => {
  if (data) {
    this.notificationService.showSuccess('Data loaded');
    processData(data);
  }
});

// Practical: smart retry - wait for connection
const checkOnline$ = interval(5000).pipe(
  filter(() => navigator.onLine),
  take(1)
);

apiCall$.pipe(
  retryWhen(errors => 
    errors.pipe(
      switchMap(err => {
        if (!navigator.onLine) {
          console.log('Waiting for connection...');
          return checkOnline$;
        }
        return timer(2000);
      }),
      take(5)
    )
  )
).subscribe(data => syncData(data));

6.3 throwError for Error Emission

Function Syntax Description Use Case
throwError throwError(() => error) Creates observable that immediately emits error notification Testing, error propagation, fallback errors
Factory function throwError(() => new Error(msg)) Factory ensures fresh error instance per subscription Prevents shared error object mutations

Example: Error emission patterns

import { throwError, of, iif } from 'rxjs';
import { catchError, mergeMap } from 'rxjs/operators';

// Basic throwError
throwError(() => new Error('Something went wrong')).subscribe({
  next: val => console.log(val),
  error: err => console.error('Error:', err.message)
});

// Conditional error throwing
const isValid = false;
iif(
  () => isValid,
  of('Valid data'),
  throwError(() => new Error('Validation failed'))
).subscribe({
  next: val => console.log(val),
  error: err => console.error(err.message)
});

// throwError in catchError chain
this.http.get('/api/data').pipe(
  mergeMap(response => {
    if (!response.data) {
      return throwError(() => new Error('No data in response'));
    }
    return of(response.data);
  }),
  catchError(err => {
    this.logger.error(err);
    return throwError(() => new Error('Data processing failed'));
  })
).subscribe({
  next: data => processData(data),
  error: err => showErrorMessage(err.message)
});

// Practical: validation with throwError
function validateUser(user) {
  if (!user.email) {
    return throwError(() => new Error('Email required'));
  }
  if (!user.name) {
    return throwError(() => new Error('Name required'));
  }
  return of(user);
}

const user = { name: 'Alice' };
validateUser(user).subscribe({
  next: validUser => saveUser(validUser),
  error: err => showValidationError(err.message)
});

// Practical: custom error types
class ValidationError extends Error {
  constructor(public field: string, message: string) {
    super(message);
    this.name = 'ValidationError';
  }
}

class NetworkError extends Error {
  constructor(public statusCode: number, message: string) {
    super(message);
    this.name = 'NetworkError';
  }
}

this.http.get('/api/users').pipe(
  catchError(err => {
    if (err.status === 0) {
      return throwError(() => new NetworkError(0, 'No internet connection'));
    }
    return throwError(() => new NetworkError(err.status, err.message));
  })
).subscribe({
  error: err => {
    if (err instanceof NetworkError) {
      handleNetworkError(err);
    } else {
      handleGenericError(err);
    }
  }
});

// Practical: error transformation
apiCall$.pipe(
  catchError(err => 
    throwError(() => ({
      originalError: err,
      timestamp: Date.now(),
      context: 'API Call Failed',
      userMessage: 'Please try again later'
    }))
  )
).subscribe({
  error: errorInfo => {
    logError(errorInfo);
    showMessage(errorInfo.userMessage);
  }
});

6.4 onErrorResumeNext for Alternative Stream Switching

Operator Syntax Description Behavior
onErrorResumeNext onErrorResumeNext(obs1$, obs2$, ...) Continues with next observable on error OR completion, ignoring errors Errors are swallowed, continues to next stream

Example: Alternative stream switching

import { onErrorResumeNext, of, throwError } from 'rxjs';
import { map } from 'rxjs/operators';

// onErrorResumeNext - continue despite errors
const first$ = throwError(() => new Error('First failed'));
const second$ = of(2, 3, 4);
const third$ = throwError(() => new Error('Third failed'));
const fourth$ = of(5, 6);

onErrorResumeNext(first$, second$, third$, fourth$).subscribe({
  next: val => console.log(val), // 2, 3, 4, 5, 6
  error: err => console.error('Error:', err), // Never called
  complete: () => console.log('Complete')
});

// Compare with catchError
// catchError - handles error and decides next action
of(1, 2, 3).pipe(
  map(n => {
    if (n === 2) throw new Error('Error at 2');
    return n;
  }),
  catchError(err => of(999))
).subscribe(val => console.log(val)); // 1, 999

// onErrorResumeNext - silently moves to next observable
onErrorResumeNext(
  of(1, 2, 3).pipe(
    map(n => {
      if (n === 2) throw new Error('Error');
      return n;
    })
  ),
  of(4, 5, 6)
).subscribe(val => console.log(val)); // 1, 4, 5, 6 (error silently skipped)

// Practical: try multiple data sources
const cache$ = this.cacheService.get('data').pipe(
  map(data => {
    if (!data) throw new Error('No cache');
    return data;
  })
);
const api$ = this.http.get('/api/data');
const fallback$ = of({ data: [], offline: true });

onErrorResumeNext(cache$, api$, fallback$).subscribe(
  data => displayData(data)
);

// Practical: best-effort data collection
const source1$ = this.http.get('/api/source1').pipe(
  catchError(() => EMPTY)
);
const source2$ = this.http.get('/api/source2').pipe(
  catchError(() => EMPTY)
);
const source3$ = this.http.get('/api/source3').pipe(
  catchError(() => EMPTY)
);

onErrorResumeNext(source1$, source2$, source3$).pipe(
  scan((acc, data) => [...acc, data], [])
).subscribe(allData => processCollectedData(allData));

// Note: Prefer catchError for error handling
// onErrorResumeNext silently swallows errors which can hide issues
Warning: onErrorResumeNext silently swallows errors. Prefer catchError for explicit error handling and recovery logic.

6.5 timeout and timeoutWith for Time-based Error Handling

Operator Syntax Description On Timeout
timeout timeout(duration) Errors if no emission within specified time (ms) Emits TimeoutError
timeout (config) timeout({ first, each, with }) Configurable timeout for first emission and each subsequent Error or switch to 'with' observable
timeoutWith timeoutWith(duration, fallback$) Switches to fallback observable on timeout (deprecated, use timeout config) Switch to fallback stream

Example: Timeout handling

import { of, throwError, timer } from 'rxjs';
import { timeout, delay, catchError } from 'rxjs/operators';

// Basic timeout
of('data').pipe(
  delay(3000),
  timeout(2000) // Timeout after 2 seconds
).subscribe({
  next: val => console.log(val),
  error: err => console.error('Timeout:', err.name) // TimeoutError
});

// timeout with different limits
const slow$ = timer(5000);
slow$.pipe(
  timeout({
    first: 3000, // First value must arrive within 3s
    each: 1000   // Subsequent values within 1s of previous
  })
).subscribe({
  error: err => console.error('Timeout error')
});

// timeout with fallback observable
of('slow data').pipe(
  delay(5000),
  timeout({
    first: 2000,
    with: () => of('fallback data') // Switch to this on timeout
  })
).subscribe(val => console.log(val)); // 'fallback data'

// Practical: API call timeout
this.http.get('/api/users').pipe(
  timeout(5000), // 5 second timeout
  catchError(err => {
    if (err.name === 'TimeoutError') {
      this.notificationService.showError('Request timed out');
      return of([]);
    }
    return throwError(() => err);
  })
).subscribe(users => displayUsers(users));

// Practical: timeout with retry
this.http.post('/api/save', data).pipe(
  timeout(10000),
  retry({
    count: 2,
    delay: (error) => {
      if (error.name === 'TimeoutError') {
        return timer(1000); // Retry after 1s on timeout
      }
      return throwError(() => error);
    }
  }),
  catchError(err => {
    if (err.name === 'TimeoutError') {
      return of({ error: 'Operation timed out after retries' });
    }
    return throwError(() => err);
  })
).subscribe(response => handleResponse(response));

// Practical: real-time data with timeout fallback
const liveData$ = this.websocket.messages$.pipe(
  timeout({
    first: 5000,
    each: 3000,
    with: () => this.http.get('/api/fallback-data') // Use REST as fallback
  })
);

liveData$.subscribe(
  data => updateUI(data),
  err => console.error('Stream error:', err)
);

// Practical: user interaction timeout
const userInput$ = fromEvent(input, 'input');
userInput$.pipe(
  timeout({
    first: 30000, // User must start typing within 30s
    with: () => {
      showMessage('Session expired due to inactivity');
      return EMPTY;
    }
  })
).subscribe(event => processInput(event));

// Practical: progressive timeout
const progressiveTimeout$ = this.http.get('/api/large-dataset').pipe(
  timeout({
    first: 10000,  // 10s for first response
    each: 5000,    // 5s between chunks
    with: () => {
      // Try faster endpoint
      return this.http.get('/api/large-dataset/summary').pipe(
        map(summary => ({ partial: true, data: summary }))
      );
    }
  })
);

progressiveTimeout$.subscribe(
  data => {
    if (data.partial) {
      showPartialData(data.data);
    } else {
      showFullData(data);
    }
  }
);

6.6 finalize for Cleanup Operations

Operator Syntax Description Execution
finalize finalize(() => cleanup()) Executes callback when observable completes, errors, or unsubscribes Always runs on stream termination (success/error/cancel)

Example: Cleanup operations with finalize

import { of, throwError, interval } from 'rxjs';
import { finalize, take, delay } from 'rxjs/operators';

// finalize on completion
of(1, 2, 3).pipe(
  finalize(() => console.log('Cleanup on complete'))
).subscribe(val => console.log(val));
// Output: 1, 2, 3, 'Cleanup on complete'

// finalize on error
throwError(() => new Error('Fail')).pipe(
  finalize(() => console.log('Cleanup on error'))
).subscribe({
  error: err => console.error(err.message)
});
// Output: 'Fail', 'Cleanup on error'

// finalize on unsubscribe
const subscription = interval(1000).pipe(
  finalize(() => console.log('Cleanup on unsubscribe'))
).subscribe(val => console.log(val));

setTimeout(() => subscription.unsubscribe(), 3500);
// Output: 0, 1, 2, 3, 'Cleanup on unsubscribe'

// Practical: loading spinner
this.http.get('/api/data').pipe(
  tap(() => this.showSpinner()),
  finalize(() => this.hideSpinner()) // Always hide spinner
).subscribe(
  data => this.displayData(data),
  err => this.showError(err)
);

// Practical: resource cleanup
const connection$ = this.openWebSocket().pipe(
  finalize(() => {
    console.log('Closing WebSocket connection');
    this.closeWebSocket();
  })
);

connection$.subscribe(
  message => handleMessage(message),
  err => handleError(err)
);

// Practical: temporary UI state
fromEvent(button, 'click').pipe(
  tap(() => button.disabled = true),
  switchMap(() => this.saveData()),
  finalize(() => button.disabled = false) // Re-enable button
).subscribe(
  result => showSuccess(result),
  err => showError(err)
);

// Practical: analytics tracking
this.processPayment(paymentData).pipe(
  finalize(() => {
    this.analytics.track('payment_attempt_completed', {
      success: this.paymentSuccess,
      timestamp: Date.now()
    });
  })
).subscribe(
  result => {
    this.paymentSuccess = true;
    showConfirmation(result);
  },
  err => {
    this.paymentSuccess = false;
    showError(err);
  }
);

// Practical: file upload cleanup
this.uploadFile(file).pipe(
  tap(() => this.uploadProgress = 0),
  finalize(() => {
    this.uploadProgress = null;
    this.cleanupTempFiles();
    console.log('Upload process completed');
  })
).subscribe(
  response => handleUploadSuccess(response),
  err => handleUploadError(err)
);

// Multiple finalize operators
of(1, 2, 3).pipe(
  finalize(() => console.log('First cleanup')),
  map(x => x * 10),
  finalize(() => console.log('Second cleanup'))
).subscribe(val => console.log(val));
// Output: 10, 20, 30, 'Second cleanup', 'First cleanup' (reverse order)

// Practical: database transaction
this.database.beginTransaction().pipe(
  switchMap(tx => this.performOperations(tx)),
  finalize(() => this.database.commit()) // Always commit/rollback
).subscribe(
  result => console.log('Transaction completed'),
  err => {
    this.database.rollback();
    console.error('Transaction failed');
  }
);
Note: finalize is guaranteed to run on completion, error, or unsubscribe - perfect for cleanup like hiding loaders, closing connections, or releasing resources.

Section 6 Summary

  • catchError handles errors and returns recovery observable (fallback, EMPTY, or retry)
  • retry resubscribes on error, retryWhen enables custom retry strategies (exponential backoff)
  • throwError creates error-emitting observable for testing and error propagation
  • timeout errors if no emission within time limit, can provide fallback observable
  • finalize executes cleanup on complete/error/unsubscribe - always runs
  • Error handling pattern: try → retry → catchError → finalize for robust pipelines

7. Subjects and Multicasting Patterns

7.1 Subject for Multicast and Imperative Emission

Type Syntax Description Characteristics
Subject new Subject<T>() Both Observable and Observer - multicasts to multiple subscribers Hot observable, no initial value, no replay
next() subject.next(value) Imperative emission to all active subscribers Push values manually
error() subject.error(err) Emit error to all subscribers, terminates subject Cannot emit after error
complete() subject.complete() Notify completion to all subscribers, terminates subject Cannot emit after complete

Example: Subject basic usage and multicasting

import { Subject } from 'rxjs';

// Create Subject
const subject = new Subject<number>();

// Subscribe multiple observers
subject.subscribe(val => console.log('Observer A:', val));
subject.subscribe(val => console.log('Observer B:', val));

// Emit values imperatively
subject.next(1);
subject.next(2);
// Output:
// Observer A: 1
// Observer B: 1
// Observer A: 2
// Observer B: 2

// Late subscriber misses previous emissions
subject.next(3);
subject.subscribe(val => console.log('Observer C:', val));
subject.next(4);
// Output:
// Observer A: 3, Observer B: 3
// Observer A: 4, Observer B: 4, Observer C: 4

// Subject as Observer
const observable$ = interval(1000).pipe(take(3));
observable$.subscribe(subject); // Pipe observable into subject
subject.subscribe(val => console.log('Via subject:', val));

// Practical: Event bus
class EventBus {
  private subject = new Subject<any>();
  
  emit(event: any) {
    this.subject.next(event);
  }
  
  on(eventType: string) {
    return this.subject.pipe(
      filter(event => event.type === eventType)
    );
  }
}

const bus = new EventBus();
bus.on('user:login').subscribe(event => console.log('User logged in:', event.data));
bus.emit({ type: 'user:login', data: { userId: 123 } });

// Practical: Component communication
class DataService {
  private dataUpdated = new Subject<any>();
  
  dataUpdated$ = this.dataUpdated.asObservable(); // Expose as Observable
  
  updateData(data: any) {
    this.processData(data);
    this.dataUpdated.next(data); // Notify subscribers
  }
}

// Practical: Click stream
const clicks = new Subject<MouseEvent>();
clicks.subscribe(event => console.log('Clicked at:', event.clientX, event.clientY));

document.addEventListener('click', event => clicks.next(event));

// Complete subject to stop emissions
subject.complete();
subject.next(5); // This will not emit - subject is completed
Note: Subjects are hot observables - late subscribers don't receive previous emissions. Always call complete() to properly terminate and release resources.

7.2 BehaviorSubject for State Management with Initial Value

Feature Syntax Description Use Case
BehaviorSubject new BehaviorSubject<T>(initialValue) Requires initial value, replays current value to new subscribers State management, current values
getValue() subject.getValue() Synchronously returns current value (not available on regular Subject) Imperative state access
value property subject.value Read-only property for current value (same as getValue()) State inspection

Example: BehaviorSubject for state management

import { BehaviorSubject } from 'rxjs';

// Create with initial value
const count$ = new BehaviorSubject<number>(0);

// New subscriber immediately receives current value
count$.subscribe(val => console.log('Subscriber A:', val)); // 0

count$.next(1);
count$.next(2);
// Output: Subscriber A: 1, Subscriber A: 2

// Late subscriber gets latest value immediately
count$.subscribe(val => console.log('Subscriber B:', val)); // 2
count$.next(3);
// Output: Subscriber A: 3, Subscriber B: 3

// Synchronous value access
console.log('Current value:', count$.getValue()); // 3
console.log('Via property:', count$.value); // 3

// Practical: User authentication state
class AuthService {
  private isAuthenticatedSubject = new BehaviorSubject<boolean>(false);
  isAuthenticated$ = this.isAuthenticatedSubject.asObservable();
  
  login(credentials: any) {
    // Perform login...
    this.isAuthenticatedSubject.next(true);
  }
  
  logout() {
    this.isAuthenticatedSubject.next(false);
  }
  
  get isAuthenticated(): boolean {
    return this.isAuthenticatedSubject.value;
  }
}

const auth = new AuthService();
auth.isAuthenticated$.subscribe(status => 
  console.log('Auth status:', status)
); // Immediately: false

auth.login({ username: 'alice' }); // Emits: true

// Practical: Application state store
class Store<T> {
  private state$: BehaviorSubject<T>;
  
  constructor(initialState: T) {
    this.state$ = new BehaviorSubject<T>(initialState);
  }
  
  select<K extends keyof T>(key: K): Observable<T[K]> {
    return this.state$.pipe(
      map(state => state[key]),
      distinctUntilChanged()
    );
  }
  
  update(partialState: Partial<T>) {
    this.state$.next({ ...this.state$.value, ...partialState });
  }
  
  get snapshot(): T {
    return this.state$.value;
  }
}

const store = new Store({ user: null, loading: false, error: null });
store.select('loading').subscribe(loading => toggleSpinner(loading));
store.update({ loading: true });

// Practical: Form state
class FormState {
  private formValue$ = new BehaviorSubject<any>({
    name: '',
    email: '',
    age: 0
  });
  
  value$ = this.formValue$.asObservable();
  
  updateField(field: string, value: any) {
    const current = this.formValue$.value;
    this.formValue$.next({ ...current, [field]: value });
  }
  
  get currentValue() {
    return this.formValue$.value;
  }
  
  reset() {
    this.formValue$.next({ name: '', email: '', age: 0 });
  }
}

// Practical: Theme management
const theme$ = new BehaviorSubject<'light' | 'dark'>('light');
theme$.subscribe(theme => document.body.className = theme);
theme$.next('dark'); // Switch theme

// Practical: Derived state
const items$ = new BehaviorSubject<any[]>([]);
const itemCount$ = items$.pipe(map(items => items.length));
const hasItems$ = items$.pipe(map(items => items.length > 0));

itemCount$.subscribe(count => console.log('Count:', count)); // 0
items$.next([1, 2, 3]); // Count: 3

7.3 ReplaySubject for Historical Value Replay

Configuration Syntax Description Replay Behavior
Buffer size new ReplaySubject<T>(bufferSize) Replays last N emissions to new subscribers Stores N most recent values
Time window new ReplaySubject<T>(bufferSize, windowTime) Replays values emitted within time window (ms) Age-based filtering
Infinite replay new ReplaySubject<T>() Replays all values to new subscribers (memory warning!) Complete history

Example: ReplaySubject value replay

import { ReplaySubject } from 'rxjs';

// Replay last 3 values
const replay$ = new ReplaySubject<number>(3);

replay$.next(1);
replay$.next(2);
replay$.next(3);
replay$.next(4);

// New subscriber receives last 3 values
replay$.subscribe(val => console.log('Subscriber A:', val));
// Output: 1, 2, 3, 4 (wait, buffer is 3, so: 2, 3, 4)

replay$.next(5);
// Output: Subscriber A: 5

// Another late subscriber
replay$.subscribe(val => console.log('Subscriber B:', val));
// Output: Subscriber B: 3, 4, 5

// ReplaySubject with time window
const replayTime$ = new ReplaySubject<number>(100, 1000); // 100 items, 1 second

replayTime$.next(1);
setTimeout(() => replayTime$.next(2), 500);
setTimeout(() => replayTime$.next(3), 1500);

setTimeout(() => {
  replayTime$.subscribe(val => console.log('Timed:', val));
  // Only receives values within last 1 second
}, 2000);

// Practical: Chat message history
class ChatService {
  private messages$ = new ReplaySubject<Message>(50); // Last 50 messages
  
  messages = this.messages$.asObservable();
  
  sendMessage(message: Message) {
    this.messages$.next(message);
    this.saveToServer(message);
  }
  
  // New users joining see recent chat history
  joinChat() {
    return this.messages$; // Replays last 50 messages
  }
}

// Practical: Activity log
class ActivityLogger {
  private log$ = new ReplaySubject<LogEntry>(100, 5 * 60 * 1000); // Last 5 minutes
  
  logActivity(entry: LogEntry) {
    this.log$.next({ ...entry, timestamp: Date.now() });
  }
  
  getRecentActivity() {
    return this.log$.asObservable();
  }
}

// Practical: Undo/Redo functionality
class UndoRedoService {
  private actions$ = new ReplaySubject<Action>(20); // Last 20 actions
  
  recordAction(action: Action) {
    this.actions$.next(action);
  }
  
  getHistory() {
    const history: Action[] = [];
    this.actions$.subscribe(action => history.push(action));
    return history;
  }
}

// Practical: Real-time sensor data
class SensorMonitor {
  private readings$ = new ReplaySubject<Reading>(10, 60000); // Last 10, max 1 min old
  
  recordReading(reading: Reading) {
    this.readings$.next(reading);
  }
  
  getRecentReadings() {
    return this.readings$.pipe(
      scan((acc, reading) => [...acc, reading], [])
    );
  }
}

// Warning: Infinite replay (memory intensive)
const infiniteReplay$ = new ReplaySubject<number>(); // All values!
for (let i = 0; i < 10000; i++) {
  infiniteReplay$.next(i);
}
// New subscriber receives ALL 10,000 values - memory leak risk!

// Practical: Command history
class CommandHistory {
  private commands$ = new ReplaySubject<Command>(30);
  
  execute(command: Command) {
    command.execute();
    this.commands$.next(command);
  }
  
  getHistory(): Command[] {
    const history: Command[] = [];
    const sub = this.commands$.subscribe(cmd => history.push(cmd));
    sub.unsubscribe();
    return history;
  }
}
Warning: ReplaySubject without buffer size stores ALL values in memory. Always specify buffer size to prevent memory leaks.

7.4 AsyncSubject for Last Value Emission

Feature Syntax Description Emission Pattern
AsyncSubject new AsyncSubject<T>() Emits only the last value when completed Single emission on complete
Behavior Buffers last value until complete() Similar to Promise - emits once when "resolved" Late subscribers get same final value

Example: AsyncSubject final value emission

import { AsyncSubject } from 'rxjs';

// Create AsyncSubject
const async$ = new AsyncSubject<number>();

// Subscribe before emissions
async$.subscribe(val => console.log('Subscriber A:', val));

async$.next(1);
async$.next(2);
async$.next(3);
// No output yet - waiting for complete()

async$.complete(); // Triggers emission of last value
// Output: Subscriber A: 3

// Late subscriber still gets last value
async$.subscribe(val => console.log('Subscriber B:', val));
// Output: Subscriber B: 3

// Practical: One-time calculation result
class Calculator {
  calculate(operation: string): Observable<number> {
    const result$ = new AsyncSubject<number>();
    
    setTimeout(() => {
      const result = this.performCalculation(operation);
      result$.next(result);
      result$.complete(); // Emit final result
    }, 1000);
    
    return result$.asObservable();
  }
}

// Practical: Async initialization
class AppInitializer {
  private initialized$ = new AsyncSubject<boolean>();
  
  init() {
    Promise.all([
      this.loadConfig(),
      this.loadUserData(),
      this.connectServices()
    ]).then(() => {
      this.initialized$.next(true);
      this.initialized$.complete();
    });
    
    return this.initialized$.asObservable();
  }
}

const initializer = new AppInitializer();
initializer.init().subscribe(ready => {
  if (ready) console.log('App ready!');
});

// Practical: Final test result
class TestRunner {
  runTests(): Observable<TestResult> {
    const result$ = new AsyncSubject<TestResult>();
    
    this.executeTests().then(results => {
      const finalResult = this.aggregateResults(results);
      result$.next(finalResult);
      result$.complete();
    });
    
    return result$;
  }
}

// Practical: Migration completion
class DataMigration {
  migrate(): Observable<MigrationStatus> {
    const status$ = new AsyncSubject<MigrationStatus>();
    
    this.performMigration()
      .then(result => {
        status$.next({ 
          success: true, 
          recordsMigrated: result.count 
        });
        status$.complete();
      })
      .catch(err => {
        status$.error(err);
      });
    
    return status$;
  }
}

// Compare with Promise
const promise = new Promise(resolve => {
  setTimeout(() => resolve('Promise result'), 1000);
});

const asyncSubject = new AsyncSubject();
setTimeout(() => {
  asyncSubject.next('AsyncSubject result');
  asyncSubject.complete();
}, 1000);

// Both emit once when resolved/completed
promise.then(val => console.log(val));
asyncSubject.subscribe(val => console.log(val));

// Error handling
const errorAsync$ = new AsyncSubject<number>();
errorAsync$.subscribe({
  next: val => console.log('Value:', val),
  error: err => console.error('Error:', err)
});

errorAsync$.next(1);
errorAsync$.next(2);
errorAsync$.error(new Error('Failed'));
// Output: Error: Failed (no value emission)

7.5 share and shareReplay for Reference Counting

Operator Syntax Description Replay Behavior
share share(config?) Multicasts observable, shares single subscription among all subscribers No replay - late subscribers miss previous values
shareReplay shareReplay(bufferSize?, windowTime?) Like share but replays buffered values to new subscribers Replays last N values or time window
shareReplay (config) shareReplay({ bufferSize, refCount }) Configurable replay with reference counting control refCount: true completes when all unsubscribe

Example: Sharing and replay strategies

import { interval, of } from 'rxjs';
import { share, shareReplay, take, tap } from 'rxjs/operators';

// WITHOUT share - each subscriber creates new subscription
const cold$ = interval(1000).pipe(
  take(3),
  tap(val => console.log('Source emission:', val))
);

cold$.subscribe(val => console.log('Sub A:', val));
cold$.subscribe(val => console.log('Sub B:', val));
// Creates 2 separate interval instances!

// WITH share - single shared subscription
const hot$ = interval(1000).pipe(
  take(3),
  tap(val => console.log('Source emission:', val)),
  share()
);

hot$.subscribe(val => console.log('Sub A:', val));
hot$.subscribe(val => console.log('Sub B:', val));
// Single interval shared between subscribers

// shareReplay - late subscribers get history
const replay$ = interval(1000).pipe(
  take(5),
  tap(val => console.log('Source:', val)),
  shareReplay(2) // Buffer last 2 values
);

replay$.subscribe(val => console.log('Early:', val));

setTimeout(() => {
  replay$.subscribe(val => console.log('Late:', val));
  // Receives last 2 buffered values immediately
}, 3500);

// Practical: Expensive HTTP call sharing
const users$ = this.http.get('/api/users').pipe(
  shareReplay(1) // Cache result, share with all subscribers
);

// Multiple components can subscribe without duplicate requests
users$.subscribe(users => this.displayInComponent1(users));
users$.subscribe(users => this.displayInComponent2(users));
users$.subscribe(users => this.displayInComponent3(users));
// Only ONE HTTP request made!

// shareReplay with refCount
const data$ = this.http.get('/api/data').pipe(
  shareReplay({ bufferSize: 1, refCount: true })
);
// When last subscriber unsubscribes, cache is cleared

// Practical: WebSocket connection sharing
class WebSocketService {
  private connection$ = this.connectWebSocket().pipe(
    share() // Share single WebSocket connection
  );
  
  messages$ = this.connection$.pipe(
    filter(msg => msg.type === 'message')
  );
  
  notifications$ = this.connection$.pipe(
    filter(msg => msg.type === 'notification')
  );
}

// Practical: Current user sharing
class UserService {
  currentUser$ = this.http.get('/api/current-user').pipe(
    shareReplay({ bufferSize: 1, refCount: false })
    // Keeps cached even if all unsubscribe
  );
}

// share vs shareReplay comparison
const source$ = interval(1000).pipe(take(4));

// share - no replay
const shared$ = source$.pipe(share());
shared$.subscribe(val => console.log('A:', val));
setTimeout(() => {
  shared$.subscribe(val => console.log('B:', val)); // Misses early values
}, 2500);

// shareReplay - with replay
const sharedReplay$ = source$.pipe(shareReplay(2));
sharedReplay$.subscribe(val => console.log('A:', val));
setTimeout(() => {
  sharedReplay$.subscribe(val => console.log('B:', val)); // Gets last 2 values
}, 2500);

// Practical: Configuration sharing
const config$ = this.http.get('/api/config').pipe(
  shareReplay(1) // All components get same config
);

// Multiple feature modules
config$.subscribe(config => this.featureA.init(config));
config$.subscribe(config => this.featureB.init(config));
config$.subscribe(config => this.featureC.init(config));
Note: Use share() for hot observables without replay. Use shareReplay(1) for caching expensive operations like HTTP requests.

7.6 multicast and publish Operators for Custom Multicasting

Operator Syntax Description Connection
multicast multicast(subjectFactory) Multicasts using provided Subject/BehaviorSubject/ReplaySubject factory Returns ConnectableObservable - manual connect()
publish publish() Shorthand for multicast(() => new Subject()) Requires connect() to start
publishReplay publishReplay(bufferSize?) Shorthand for multicast(() => new ReplaySubject(bufferSize)) Replays to late subscribers
refCount publish().refCount() Auto-connects on first subscription, disconnects when count reaches 0 Automatic connection management

Example: Custom multicasting patterns

import { interval, Subject, ReplaySubject } from 'rxjs';
import { multicast, publish, publishReplay, refCount, take, tap } from 'rxjs/operators';

// multicast with Subject
const source$ = interval(1000).pipe(
  take(4),
  tap(val => console.log('Source:', val)),
  multicast(new Subject())
);

source$.subscribe(val => console.log('A:', val));
source$.subscribe(val => console.log('B:', val));

// Must call connect() to start
source$.connect();

// publish - simpler syntax
const published$ = interval(1000).pipe(
  take(3),
  publish()
);

const sub1 = published$.subscribe(val => console.log('Sub1:', val));
const sub2 = published$.subscribe(val => console.log('Sub2:', val));

published$.connect(); // Start emissions

// publishReplay - with buffering
const publishedReplay$ = interval(1000).pipe(
  take(5),
  publishReplay(2) // Buffer last 2
);

publishedReplay$.subscribe(val => console.log('Early:', val));
publishedReplay$.connect();

setTimeout(() => {
  publishedReplay$.subscribe(val => console.log('Late:', val));
  // Gets buffered values
}, 3500);

// refCount - automatic connection management
const autoCounted$ = interval(1000).pipe(
  take(5),
  tap(val => console.log('Source:', val)),
  publish(),
  refCount() // Auto-connect on first sub, auto-disconnect on zero subs
);

const subscription1 = autoCounted$.subscribe(val => console.log('A:', val));
// Source starts here (first subscriber)

setTimeout(() => {
  const subscription2 = autoCounted$.subscribe(val => console.log('B:', val));
  // Joins existing stream
}, 2000);

setTimeout(() => {
  subscription1.unsubscribe();
  // Source still running (subscription2 active)
}, 3000);

setTimeout(() => {
  subscription2.unsubscribe();
  // Source stops (zero subscribers)
}, 4000);

// Practical: Shared expensive operation
const expensiveCalc$ = of(null).pipe(
  tap(() => console.log('Expensive calculation...')),
  map(() => performExpensiveCalculation()),
  publishReplay(1),
  refCount()
);

expensiveCalc$.subscribe(result => console.log('User 1:', result));
expensiveCalc$.subscribe(result => console.log('User 2:', result));
// Only one calculation performed

// Practical: Controlled broadcast
class NotificationService {
  private notifications$ = new Subject<Notification>();
  
  private broadcast$ = this.notifications$.pipe(
    multicast(new ReplaySubject<Notification>(5))
  );
  
  constructor() {
    this.broadcast$.connect(); // Start broadcasting
  }
  
  send(notification: Notification) {
    this.notifications$.next(notification);
  }
  
  subscribe() {
    return this.broadcast$; // Subscribers get last 5 notifications
  }
}

// Modern alternative: share() is preferred
// Instead of: publish().refCount()
// Use: share()
const modern$ = interval(1000).pipe(
  take(5),
  share() // Equivalent to publish().refCount()
);

// Instead of: publishReplay(1).refCount()
// Use: shareReplay(1)
const modernReplay$ = interval(1000).pipe(
  take(5),
  shareReplay(1) // Equivalent to publishReplay(1).refCount()
);

Section 7 Summary

  • Subject is both Observable and Observer - multicast without replay
  • BehaviorSubject requires initial value, replays current value to new subscribers
  • ReplaySubject buffers N values, replays to late subscribers (specify buffer to avoid leaks)
  • AsyncSubject emits only last value on completion (Promise-like behavior)
  • share() for hot multicasting, shareReplay(1) for caching expensive operations
  • Modern pattern: prefer share/shareReplay over publish/multicast operators

8. Schedulers and Execution Context Control

8.1 asyncScheduler for Macro-task Scheduling

Feature Syntax Description Use Case
asyncScheduler observeOn(asyncScheduler) Schedules work using setTimeout (macro-task queue) Async operations, preventing blocking
schedule() asyncScheduler.schedule(work, delay) Schedule callback with optional delay (ms) Delayed execution
Default scheduler Used by interval, timer, delay operators Default for time-based operators Standard async timing

Example: asyncScheduler for async execution

import { asyncScheduler, of, range } from 'rxjs';
import { observeOn } from 'rxjs/operators';

// Direct scheduler usage
asyncScheduler.schedule(() => console.log('Executed async'));
console.log('Synchronous');
// Output:
// Synchronous
// Executed async

// With delay
asyncScheduler.schedule(
  () => console.log('Delayed'),
  1000 // 1 second delay
);

// Recursive scheduling
asyncScheduler.schedule(function(state) {
  console.log('Count:', state);
  if (state < 3) {
    this.schedule(state + 1, 500); // Reschedule after 500ms
  }
}, 0, 0); // Initial delay 0, initial state 0

// observeOn with asyncScheduler
range(1, 5).pipe(
  observeOn(asyncScheduler)
).subscribe(val => console.log('Async:', val));

console.log('After subscribe');
// Output:
// After subscribe
// Async: 1
// Async: 2
// Async: 3
// Async: 4
// Async: 5

// Practical: Prevent blocking UI
of(1, 2, 3, 4, 5).pipe(
  observeOn(asyncScheduler),
  map(n => {
    // Heavy computation
    const result = expensiveCalculation(n);
    return result;
  })
).subscribe(result => updateUI(result));

// Practical: Debounced batch processing
const batchProcessor = {
  items: [],
  scheduledWork: null,
  
  addItem(item) {
    this.items.push(item);
    
    if (this.scheduledWork) {
      this.scheduledWork.unsubscribe();
    }
    
    this.scheduledWork = asyncScheduler.schedule(() => {
      this.processBatch(this.items);
      this.items = [];
    }, 100);
  },
  
  processBatch(items) {
    console.log('Processing batch:', items);
  }
};

batchProcessor.addItem('a');
batchProcessor.addItem('b');
batchProcessor.addItem('c');
// Processes all 3 together after 100ms

// Practical: Animation frame alternative for non-visual tasks
const heavyWork$ = range(1, 1000).pipe(
  observeOn(asyncScheduler), // Yield to event loop
  map(n => n * n)
);

// Practical: Testing with scheduler
import { TestScheduler } from 'rxjs/testing';

const testScheduler = new TestScheduler((actual, expected) => {
  expect(actual).toEqual(expected);
});

testScheduler.run(({ cold, expectObservable }) => {
  const source$ = cold('--a--b--c|');
  const expected =     '--a--b--c|';
  
  expectObservable(
    source$.pipe(observeOn(asyncScheduler))
  ).toBe(expected);
});
Note: asyncScheduler uses setTimeout which runs in the macro-task queue after current call stack clears. Default for most time-based RxJS operators.

8.2 queueScheduler for Synchronous Scheduling

Feature Syntax Description Execution Model
queueScheduler observeOn(queueScheduler) Schedules work synchronously in FIFO queue Immediate, blocking execution
Iteration control Prevents recursive stack overflow Trampolines recursive operations Flattens call stack
Default for repeat, retry operators Used for synchronous repetition Sequential operations

Example: queueScheduler for synchronous execution

import { queueScheduler, of, range } from 'rxjs';
import { observeOn } from 'rxjs/operators';

// Synchronous execution
console.log('Before');
queueScheduler.schedule(() => console.log('Queued work'));
console.log('After');
// Output:
// Before
// Queued work
// After

// Compare async vs queue scheduler
console.log('Start');

of(1, 2, 3).pipe(
  observeOn(asyncScheduler)
).subscribe(val => console.log('Async:', val));

of(1, 2, 3).pipe(
  observeOn(queueScheduler)
).subscribe(val => console.log('Queue:', val));

console.log('End');
// Output:
// Start
// Queue: 1
// Queue: 2
// Queue: 3
// End
// Async: 1
// Async: 2
// Async: 3

// Prevent stack overflow with recursive operations
queueScheduler.schedule(function(state) {
  console.log('Count:', state);
  if (state < 10000) {
    this.schedule(state + 1); // No delay, queued synchronously
  }
}, 0, 0);
// Completes without stack overflow

// Without scheduler - stack overflow risk
function recursiveSync(n) {
  console.log(n);
  if (n < 10000) {
    recursiveSync(n + 1); // Stack overflow!
  }
}

// Practical: Synchronous stream processing
const processSync$ = range(1, 100).pipe(
  observeOn(queueScheduler),
  map(n => n * 2)
);

processSync$.subscribe(val => {
  // All values processed before next line
  console.log(val);
});
console.log('Processing complete');

// Practical: State machine transitions
class StateMachine {
  private state = 'idle';
  
  transition(event: string) {
    queueScheduler.schedule(() => {
      this.state = this.getNextState(this.state, event);
      console.log('State:', this.state);
    });
  }
  
  getNextState(current: string, event: string): string {
    // State transition logic
    return `${current}->${event}`;
  }
}

const machine = new StateMachine();
machine.transition('start');
machine.transition('process');
machine.transition('complete');
// All transitions execute synchronously in order

// Practical: Immediate event processing
const events$ = of('click', 'hover', 'focus').pipe(
  observeOn(queueScheduler),
  tap(event => console.log('Processing:', event))
);

console.log('Before subscription');
events$.subscribe();
console.log('After subscription');
// Output:
// Before subscription
// Processing: click
// Processing: hover
// Processing: focus
// After subscription

// Practical: Testing synchronous behavior
const syncTest$ = range(1, 3).pipe(
  observeOn(queueScheduler)
);

const results = [];
syncTest$.subscribe(val => results.push(val));

console.log(results); // [1, 2, 3] immediately available
Warning: queueScheduler executes synchronously and blocks. Use for trampolining recursive operations or when immediate execution is required.

8.3 animationFrameScheduler for Browser Animation

Feature Syntax Description Timing
animationFrameScheduler observeOn(animationFrameScheduler) Schedules work using requestAnimationFrame ~60fps, synced with browser repaint
Frame timing Executes before next browser paint Optimal for DOM animations and visual updates 16.67ms intervals (60Hz)
Performance Pauses when tab inactive Battery-friendly, respects browser optimization Efficient rendering

Example: animationFrameScheduler for smooth animations

import { animationFrameScheduler, interval, fromEvent } from 'rxjs';
import { observeOn, map, takeWhile } from 'rxjs/operators';

// Schedule on animation frame
animationFrameScheduler.schedule(() => {
  console.log('Frame tick');
});

// Smooth animation loop
interval(0, animationFrameScheduler).pipe(
  takeWhile(frame => frame < 60) // 1 second at 60fps
).subscribe(frame => {
  const progress = frame / 60;
  updateAnimation(progress);
});

// Practical: Smooth element movement
const moveElement$ = interval(0, animationFrameScheduler).pipe(
  map(frame => frame * 2), // 2px per frame
  takeWhile(position => position < 500)
);

moveElement$.subscribe(position => {
  element.style.left = position + 'px';
});

// Practical: Easing animation
function animate(element, property, from, to, duration) {
  const start = Date.now();
  
  return interval(0, animationFrameScheduler).pipe(
    map(() => {
      const elapsed = Date.now() - start;
      const progress = Math.min(elapsed / duration, 1);
      return progress;
    }),
    map(progress => {
      // Ease-in-out function
      const eased = progress < 0.5
        ? 2 * progress * progress
        : 1 - Math.pow(-2 * progress + 2, 2) / 2;
      return from + (to - from) * eased;
    }),
    takeWhile((_, index) => Date.now() - start < duration, true)
  ).subscribe(value => {
    element.style[property] = value + 'px';
  });
}

animate(box, 'left', 0, 500, 1000); // Animate left from 0 to 500px in 1s

// Practical: Parallax scrolling
fromEvent(window, 'scroll').pipe(
  observeOn(animationFrameScheduler),
  map(() => window.scrollY)
).subscribe(scrollY => {
  background.style.transform = `translateY(${scrollY * 0.5}px)`;
  foreground.style.transform = `translateY(${scrollY * 0.8}px)`;
});

// Practical: Canvas animation
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');

interval(0, animationFrameScheduler).subscribe(frame => {
  // Clear canvas
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  
  // Draw animated content
  const x = (frame % canvas.width);
  const y = Math.sin(frame * 0.05) * 50 + canvas.height / 2;
  
  ctx.fillStyle = 'blue';
  ctx.fillRect(x, y, 20, 20);
});

// Practical: Drag and drop with smooth updates
const mouseMove$ = fromEvent(document, 'mousemove');

const drag$ = fromEvent(element, 'mousedown').pipe(
  switchMap(() => mouseMove$.pipe(
    observeOn(animationFrameScheduler), // Smooth updates
    takeUntil(fromEvent(document, 'mouseup'))
  ))
);

drag$.subscribe(event => {
  element.style.left = event.clientX + 'px';
  element.style.top = event.clientY + 'px';
});

// Practical: Progress bar animation
function animateProgressBar(target) {
  const startWidth = progressBar.offsetWidth;
  const targetWidth = target;
  const duration = 500;
  const start = Date.now();
  
  interval(0, animationFrameScheduler).pipe(
    map(() => (Date.now() - start) / duration),
    takeWhile(progress => progress <= 1, true),
    map(progress => startWidth + (targetWidth - startWidth) * progress)
  ).subscribe(width => {
    progressBar.style.width = width + '%';
  });
}

// Practical: Game loop
const gameLoop$ = interval(0, animationFrameScheduler);

gameLoop$.subscribe(() => {
  updateGameState();
  detectCollisions();
  renderGame();
});

// Performance monitoring
let frameCount = 0;
let lastTime = Date.now();

interval(0, animationFrameScheduler).subscribe(() => {
  frameCount++;
  const now = Date.now();
  
  if (now - lastTime >= 1000) {
    console.log('FPS:', frameCount);
    frameCount = 0;
    lastTime = now;
  }
});
Note: Use animationFrameScheduler for all DOM animations and visual updates. Automatically pauses when tab is inactive, saving battery and CPU.

8.4 asapScheduler for Micro-task Scheduling

Feature Syntax Description Queue Priority
asapScheduler observeOn(asapScheduler) Schedules work in micro-task queue (like Promise.then) Before macro-tasks, after current stack
Implementation Uses setImmediate (Node.js) or Promise.resolve Platform-specific micro-task scheduling Faster than setTimeout
Use case High-priority async work before next macro-task Critical updates, Promise-like behavior Minimal delay execution

Example: asapScheduler for micro-task execution

import { asapScheduler, asyncScheduler, of } from 'rxjs';
import { observeOn } from 'rxjs/operators';

// Execution order demonstration
console.log('1. Synchronous start');

asapScheduler.schedule(() => console.log('3. ASAP (micro-task)'));

asyncScheduler.schedule(() => console.log('4. Async (macro-task)'));

Promise.resolve().then(() => console.log('2. Promise (micro-task)'));

console.log('1b. Synchronous end');

// Output:
// 1. Synchronous start
// 1b. Synchronous end
// 2. Promise (micro-task)
// 3. ASAP (micro-task)
// 4. Async (macro-task)

// Compare schedulers
of(1, 2, 3).pipe(observeOn(asapScheduler))
  .subscribe(val => console.log('ASAP:', val));

of(1, 2, 3).pipe(observeOn(asyncScheduler))
  .subscribe(val => console.log('Async:', val));

console.log('After subscriptions');
// Output:
// After subscriptions
// ASAP: 1, 2, 3
// Async: 1, 2, 3

// Practical: Critical state updates
class StateManager {
  private state = { count: 0 };
  
  updateState(updates: any) {
    asapScheduler.schedule(() => {
      this.state = { ...this.state, ...updates };
      this.notifySubscribers();
    });
  }
  
  notifySubscribers() {
    // High-priority notification before next event loop
  }
}

// Practical: Promise integration
const promiseValue$ = from(Promise.resolve(42)).pipe(
  observeOn(asapScheduler)
);

// Executes in micro-task queue, similar timing to Promise

// Practical: Immediate DOM updates (before paint)
const criticalUpdate$ = of(newData).pipe(
  observeOn(asapScheduler),
  tap(data => {
    // Update DOM before next paint cycle
    element.textContent = data.value;
  })
);

// Practical: High-priority event processing
fromEvent(button, 'click').pipe(
  observeOn(asapScheduler),
  tap(() => {
    // Process click with minimal delay
    handleCriticalClick();
  })
).subscribe();

// Practical: React-like state batching
class ComponentState {
  private pendingUpdates = [];
  
  setState(update: any) {
    this.pendingUpdates.push(update);
    
    asapScheduler.schedule(() => {
      if (this.pendingUpdates.length > 0) {
        const merged = Object.assign({}, ...this.pendingUpdates);
        this.pendingUpdates = [];
        this.applyState(merged);
      }
    });
  }
  
  applyState(state: any) {
    // Apply batched updates
  }
}

// Practical: Minimal latency data processing
const realtime$ = websocketMessages$.pipe(
  observeOn(asapScheduler), // Process ASAP
  map(msg => processMessage(msg))
);

// Practical: Testing with micro-task timing
const test$ = of('value').pipe(
  observeOn(asapScheduler)
);

let result;
test$.subscribe(val => result = val);

// result is still undefined here (async)
setTimeout(() => {
  console.log(result); // 'value' available in next tick
}, 0);

// Practical: Error boundary with priority
const withErrorHandling$ = source$.pipe(
  observeOn(asapScheduler),
  catchError(err => {
    // Handle error with high priority
    logError(err);
    return of(fallbackValue);
  })
);

8.5 observeOn and subscribeOn for Context Control

Operator Syntax Description Affects
observeOn observeOn(scheduler) Controls execution context for downstream operators and subscription Emission delivery to observers
subscribeOn subscribeOn(scheduler) Controls execution context for subscription (source creation) Subscribe call and source setup
Placement observeOn: affects below, subscribeOn: affects entire chain observeOn can be used multiple times at different points Different scope of influence

Example: observeOn vs subscribeOn control

import { of, asyncScheduler, queueScheduler } from 'rxjs';
import { observeOn, subscribeOn, tap } from 'rxjs/operators';

// observeOn affects downstream
of(1, 2, 3).pipe(
  tap(val => console.log('Before observeOn:', val)), // Sync
  observeOn(asyncScheduler),
  tap(val => console.log('After observeOn:', val)) // Async
).subscribe();

console.log('Subscribe completed');
// Output:
// Before observeOn: 1
// Before observeOn: 2
// Before observeOn: 3
// Subscribe completed
// After observeOn: 1
// After observeOn: 2
// After observeOn: 3

// subscribeOn affects subscription
of(1, 2, 3).pipe(
  tap(val => console.log('Source:', val)),
  subscribeOn(asyncScheduler)
).subscribe(val => console.log('Observer:', val));

console.log('After subscribe call');
// Output:
// After subscribe call
// Source: 1
// Observer: 1
// Source: 2
// Observer: 2
// Source: 3
// Observer: 3

// Multiple observeOn operators
of(1, 2, 3).pipe(
  tap(val => console.log('1. Sync:', val)),
  observeOn(asyncScheduler),
  tap(val => console.log('2. Async:', val)),
  observeOn(queueScheduler),
  tap(val => console.log('3. Queue:', val))
).subscribe();

// Practical: Heavy computation offload
const heavyWork$ = range(1, 1000).pipe(
  subscribeOn(asyncScheduler), // Don't block subscription
  map(n => {
    // Heavy computation
    return complexCalculation(n);
  }),
  observeOn(asyncScheduler), // Don't block observer
);

heavyWork$.subscribe(result => {
  updateUI(result);
});

// Practical: Thread control pattern
const data$ = fetchData().pipe(
  subscribeOn(asyncScheduler), // Fetch on async
  map(processData),
  observeOn(animationFrameScheduler), // Update UI on animation frame
);

data$.subscribe(result => {
  renderToDOM(result);
});

// Practical: Testing with schedulers
import { TestScheduler } from 'rxjs/testing';

const scheduler = new TestScheduler((actual, expected) => {
  expect(actual).toEqual(expected);
});

scheduler.run(({ cold, expectObservable }) => {
  const source$ = cold('--a--b--c|').pipe(
    observeOn(scheduler) // Use test scheduler
  );
  
  expectObservable(source$).toBe('--a--b--c|');
});

// Practical: Event handling with context control
fromEvent(button, 'click').pipe(
  observeOn(asapScheduler), // Handle ASAP
  switchMap(() => httpRequest$.pipe(
    subscribeOn(asyncScheduler) // HTTP on async
  )),
  observeOn(animationFrameScheduler), // Render on RAF
).subscribe(data => {
  updateView(data);
});

// Practical: Web Worker integration
const workerObservable$ = new Observable(observer => {
  const worker = new Worker('worker.js');
  
  worker.onmessage = (event) => observer.next(event.data);
  worker.onerror = (error) => observer.error(error);
  
  return () => worker.terminate();
}).pipe(
  subscribeOn(asyncScheduler),
  observeOn(asyncScheduler)
);

// Practical: Batch processing with timing control
range(1, 10000).pipe(
  subscribeOn(queueScheduler), // Synchronous iteration
  bufferCount(100),
  observeOn(asyncScheduler), // Async batch processing
  concatMap(batch => processBatch(batch))
).subscribe();

// Practical: Form validation
const input$ = fromEvent(inputElement, 'input').pipe(
  map(event => event.target.value),
  observeOn(asyncScheduler), // Don't block typing
  debounceTime(300),
  switchMap(value => validate(value).pipe(
    subscribeOn(asyncScheduler)
  ))
).subscribe(validationResult => {
  displayErrors(validationResult);
});
Note: Use observeOn to control when emissions reach observers. Use subscribeOn to control subscription execution. observeOn is most commonly used.

8.6 VirtualTimeScheduler for Testing and Simulation

Feature Syntax Description Use Case
VirtualTimeScheduler new VirtualTimeScheduler() Simulates passage of time without actual delays Testing, time travel debugging
TestScheduler new TestScheduler(assertDeepEqual) Built on VirtualTimeScheduler for marble testing RxJS testing framework
flush() scheduler.flush() Execute all scheduled work immediately Fast-forward time

Example: VirtualTimeScheduler for testing

import { VirtualTimeScheduler, TestScheduler } from 'rxjs';
import { interval, timer } from 'rxjs';
import { take, map } from 'rxjs/operators';

// Basic VirtualTimeScheduler usage
const virtualScheduler = new VirtualTimeScheduler();

const source$ = interval(1000, virtualScheduler).pipe(take(3));

const results = [];
source$.subscribe(val => results.push(val));

// No actual time has passed yet
console.log('Before flush:', results); // []

// Fast-forward through all scheduled work
virtualScheduler.flush();

console.log('After flush:', results); // [0, 1, 2]

// TestScheduler for marble testing
const testScheduler = new TestScheduler((actual, expected) => {
  expect(actual).toEqual(expected);
});

testScheduler.run(({ cold, hot, expectObservable, expectSubscriptions }) => {
  // Marble syntax: - = 10ms, | = complete, # = error
  const source$ = cold('--a--b--c|');
  const expected =     '--a--b--c|';
  
  expectObservable(source$).toBe(expected);
});

// Testing time-based operators
testScheduler.run(({ cold, expectObservable }) => {
  const source$ = cold('a-b-c|').pipe(
    debounceTime(20) // 2 frames
  );
  const expected =    '----c|';
  
  expectObservable(source$).toBe(expected);
});

// Testing with values
testScheduler.run(({ cold, expectObservable }) => {
  const source$ = cold('--a--b--c|', { a: 1, b: 2, c: 3 }).pipe(
    map(x => x * 10)
  );
  const expected =     '--a--b--c|';
  const values = { a: 10, b: 20, c: 30 };
  
  expectObservable(source$).toBe(expected, values);
});

// Testing subscriptions
testScheduler.run(({ cold, expectObservable, expectSubscriptions }) => {
  const source$ = cold('--a--b--c|');
  const subs =         '^--------!';
  
  expectObservable(source$).toBe('--a--b--c|');
  expectSubscriptions(source$.subscriptions).toBe(subs);
});

// Testing async operations instantly
testScheduler.run(({ cold, expectObservable }) => {
  const source$ = timer(1000, testScheduler).pipe(
    take(3),
    map(x => x * 10)
  );
  
  // 1000ms becomes 100 frames in test
  const expected = '1s a 999ms b 999ms (c|)';
  const values = { a: 0, b: 10, c: 20 };
  
  expectObservable(source$).toBe(expected, values);
});

// Practical: Testing debounced search
testScheduler.run(({ cold, expectObservable }) => {
  const input$ = cold('a-b-c---|');
  const search$ = input$.pipe(
    debounceTime(20, testScheduler),
    switchMap(term => cold('--r|', { r: `result:${term}` }))
  );
  
  const expected = '------r-|';
  const values = { r: 'result:c' };
  
  expectObservable(search$).toBe(expected, values);
});

// Practical: Testing retry logic
testScheduler.run(({ cold, expectObservable }) => {
  const source$ = cold('--#', null, new Error('fail')).pipe(
    retry(2)
  );
  
  const expected = '--#--#--#';
  
  expectObservable(source$).toBe(expected, null, new Error('fail'));
});

// Practical: Testing complex timing
testScheduler.run(({ cold, hot, expectObservable }) => {
  const source1$ = hot('-a--b--c|');
  const source2$ = hot('--1--2--3|');
  
  const result$ = combineLatest([source1$, source2$]);
  
  const expected = '--x-yz-w-|';
  const values = {
    x: ['a', '1'],
    y: ['b', '1'],
    z: ['b', '2'],
    w: ['c', '3']
  };
  
  expectObservable(result$).toBe(expected, values);
});

// Practical: Testing HTTP with delays
testScheduler.run(({ cold, expectObservable }) => {
  const mockHttp = (url: string) => {
    return cold('---r|', { r: { data: url } });
  };
  
  const request$ = of('/api/users').pipe(
    delay(10, testScheduler),
    switchMap(url => mockHttp(url))
  );
  
  const expected = '----------(r|)';
  const values = { r: { data: '/api/users' } };
  
  expectObservable(request$).toBe(expected, values);
});

// Marble diagram reference:
// '-' = 10ms time frame
// '|' = completion
// '#' = error
// 'a' = emission with value 'a'
// '()' = grouping (simultaneous emissions)
// '^' = subscription start
// '!' = unsubscription
Note: TestScheduler enables marble testing - visual representation of observable streams over time. Essential for testing async RxJS code without actual delays.

Section 8 Summary

  • asyncScheduler uses setTimeout (macro-task) - default for time-based operators
  • queueScheduler executes synchronously in FIFO queue - prevents stack overflow
  • animationFrameScheduler uses requestAnimationFrame - optimal for DOM animations at 60fps
  • asapScheduler uses micro-task queue (Promise-like) - high priority async execution
  • observeOn controls emission delivery, subscribeOn controls subscription execution
  • TestScheduler enables marble testing with virtual time - test async code instantly

9. Advanced Operators and Higher-Order Observables

9.1 switchMap for Latest Value Switching

Feature Syntax Description Behavior
switchMap switchMap(project: (value, index) => ObservableInput) Maps to inner observable, cancels previous inner observable on new emission Only latest inner observable emits
Cancellation Automatically unsubscribes from previous inner observable Prevents memory leaks and race conditions Cancel-on-new pattern
Use case HTTP requests, type-ahead search, latest value only When only most recent result matters Discard outdated responses

Example: switchMap for request cancellation

import { fromEvent, interval } from 'rxjs';
import { switchMap, map, ajax } from 'rxjs';

// Basic switchMap - cancels previous
interval(1000).pipe(
  switchMap(n => interval(300).pipe(
    map(i => `Outer: ${n}, Inner: ${i}`)
  ))
).subscribe(console.log);
// Each outer emission cancels previous inner observable

// Practical: Type-ahead search with cancellation
const searchBox = document.getElementById('search');

fromEvent(searchBox, 'input').pipe(
  map(event => event.target.value),
  debounceTime(300),
  distinctUntilChanged(),
  switchMap(term => {
    if (term.length < 2) return of([]);
    
    return ajax.getJSON(`/api/search?q=${term}`);
    // Previous search cancelled if user types again
  })
).subscribe(results => {
  displayResults(results);
});

// Practical: Auto-save with latest value only
const input$ = fromEvent(inputElement, 'input').pipe(
  map(e => e.target.value),
  debounceTime(500),
  switchMap(value => 
    ajax.post('/api/save', { value }).pipe(
      catchError(err => {
        console.error('Save failed:', err);
        return of({ error: true });
      })
    )
  )
);

input$.subscribe(response => {
  if (!response.error) {
    showSaveSuccess();
  }
});

// Practical: Dynamic data refresh
const refreshButton$ = fromEvent(refreshBtn, 'click');

refreshButton$.pipe(
  switchMap(() => ajax.getJSON('/api/data').pipe(
    catchError(err => of({ error: err }))
  ))
).subscribe(data => updateView(data));

// Practical: Tab switching
const tabClick$ = fromEvent(document, 'click').pipe(
  filter(e => e.target.classList.contains('tab')),
  map(e => e.target.dataset.tabId)
);

tabClick$.pipe(
  switchMap(tabId => ajax.getJSON(`/api/tabs/${tabId}/content`))
).subscribe(content => renderTabContent(content));

// Practical: Cascading dropdowns
const countrySelect$ = fromEvent(countryDropdown, 'change').pipe(
  map(e => e.target.value)
);

countrySelect$.pipe(
  switchMap(countryId => 
    ajax.getJSON(`/api/countries/${countryId}/states`)
  )
).subscribe(states => {
  populateStateDropdown(states);
});

// Avoid race conditions
// BAD: without switchMap
searchInput$.pipe(
  debounceTime(300),
  mergeMap(term => ajax.getJSON(`/api/search?q=${term}`))
  // Old requests can return after new ones!
).subscribe(results => display(results));

// GOOD: with switchMap
searchInput$.pipe(
  debounceTime(300),
  switchMap(term => ajax.getJSON(`/api/search?q=${term}`))
  // Old requests cancelled, only latest matters
).subscribe(results => display(results));

// Practical: Polling with cancellation
const startPolling$ = fromEvent(startBtn, 'click');
const stopPolling$ = fromEvent(stopBtn, 'click');

startPolling$.pipe(
  switchMap(() => 
    interval(5000).pipe(
      switchMap(() => ajax.getJSON('/api/status')),
      takeUntil(stopPolling$)
    )
  )
).subscribe(status => updateStatus(status));

// Practical: Real-time validation
fromEvent(emailInput, 'input').pipe(
  map(e => e.target.value),
  debounceTime(500),
  distinctUntilChanged(),
  switchMap(email => 
    ajax.post('/api/validate-email', { email }).pipe(
      map(response => ({ valid: response.valid, email })),
      catchError(() => of({ valid: false, email }))
    )
  )
).subscribe(result => {
  if (result.valid) {
    showValidIcon();
  } else {
    showInvalidIcon();
  }
});
Note: switchMap is perfect for search/autocomplete - automatically cancels outdated requests. Use when only the latest result matters.

9.2 mergeMap for Concurrent Inner Observable Processing

Feature Syntax Description Concurrency
mergeMap mergeMap(project, concurrent?) Maps to inner observable, subscribes to all concurrently All inner observables run in parallel
Concurrent limit mergeMap(project, 3) Limit number of concurrent inner subscriptions Control resource usage
Alias flatMap (deprecated name) Same as mergeMap Use mergeMap instead

Example: mergeMap for parallel processing

import { of, from, interval } from 'rxjs';
import { mergeMap, map, delay } from 'rxjs';

// Basic mergeMap - all run concurrently
of(1, 2, 3).pipe(
  mergeMap(n => 
    interval(1000).pipe(
      map(i => `n=${n}, i=${i}`),
      take(3)
    )
  )
).subscribe(console.log);
// All 3 inner observables run simultaneously

// Practical: Parallel HTTP requests
const userIds = [1, 2, 3, 4, 5];

from(userIds).pipe(
  mergeMap(id => ajax.getJSON(`/api/users/${id}`))
).subscribe(user => {
  console.log('User loaded:', user);
  // All 5 requests made in parallel
});

// Practical: Concurrent limit for resource control
from(userIds).pipe(
  mergeMap(
    id => ajax.getJSON(`/api/users/${id}`),
    3 // Max 3 concurrent requests
  )
).subscribe(user => processUser(user));

// Practical: File upload queue
const fileList = [file1, file2, file3, file4, file5];

from(fileList).pipe(
  mergeMap(
    file => uploadFile(file).pipe(
      map(response => ({ file, response })),
      catchError(err => of({ file, error: err }))
    ),
    2 // Upload 2 files at a time
  )
).subscribe(result => {
  if (result.error) {
    console.error('Upload failed:', result.file.name);
  } else {
    console.log('Uploaded:', result.file.name);
  }
});

// Practical: Batch processing
const items = Array.from({ length: 100 }, (_, i) => i);

from(items).pipe(
  mergeMap(
    item => processItem(item).pipe(
      delay(Math.random() * 1000) // Simulate variable processing time
    ),
    10 // Process 10 items concurrently
  )
).subscribe(result => console.log('Processed:', result));

// Practical: Multi-source data aggregation
const dataSources = [
  '/api/source1',
  '/api/source2',
  '/api/source3'
];

from(dataSources).pipe(
  mergeMap(url => ajax.getJSON(url).pipe(
    catchError(err => of({ error: err, url }))
  ))
).subscribe(data => {
  if (!data.error) {
    aggregateData(data);
  }
});

// Practical: Click handler with async operation
fromEvent(buttons, 'click').pipe(
  mergeMap(event => {
    const button = event.target;
    button.disabled = true;
    
    return ajax.post('/api/action', { id: button.dataset.id }).pipe(
      finalize(() => button.disabled = false)
    );
  })
).subscribe(response => {
  showNotification('Action completed');
});

// Practical: WebSocket message handling
const messages$ = new WebSocketSubject('ws://api.example.com');

messages$.pipe(
  mergeMap(message => {
    // Process each message independently
    return processMessage(message).pipe(
      catchError(err => {
        console.error('Message processing failed:', err);
        return of(null);
      })
    );
  })
).subscribe();

// Practical: Rate-limited API calls
const requests = Array.from({ length: 50 }, (_, i) => i);

from(requests).pipe(
  mergeMap(
    id => ajax.getJSON(`/api/items/${id}`).pipe(
      delay(100) // Add delay to respect rate limits
    ),
    5 // Max 5 concurrent
  )
).subscribe(item => storeItem(item));

// mergeMap vs switchMap comparison
// mergeMap: All searches complete
searchInput$.pipe(
  mergeMap(term => search(term))
).subscribe(results => {
  // Might show old results after new ones
});

// switchMap: Only latest search completes
searchInput$.pipe(
  switchMap(term => search(term))
).subscribe(results => {
  // Always shows latest results
});
Note: Use mergeMap when all operations should complete. Specify concurrent limit to prevent overwhelming server/client resources.

9.3 concatMap for Sequential Inner Observable Processing

Feature Syntax Description Ordering
concatMap concatMap(project) Maps to inner observable, waits for each to complete before starting next Sequential, FIFO order preserved
Queuing Buffers outer emissions while inner observable active Processes all emissions in order No cancellation, no concurrency
Use case Ordered operations, sequential API calls, queue processing When order matters Guaranteed sequence

Example: concatMap for sequential processing

import { of, from, interval } from 'rxjs';
import { concatMap, map, delay } from 'rxjs';

// Basic concatMap - sequential execution
of(1, 2, 3).pipe(
  concatMap(n => 
    of(n).pipe(
      delay(1000),
      map(val => val * 10)
    )
  )
).subscribe(console.log);
// Output (1s apart): 10, 20, 30

// Practical: Sequential API updates
const updates = [
  { id: 1, value: 'first' },
  { id: 2, value: 'second' },
  { id: 3, value: 'third' }
];

from(updates).pipe(
  concatMap(update => 
    ajax.post('/api/update', update).pipe(
      tap(() => console.log('Updated:', update.id))
    )
  )
).subscribe(
  response => console.log('Response:', response),
  err => console.error('Error:', err),
  () => console.log('All updates complete')
);

// Practical: Animation sequence
const animationSteps = [
  { element: box1, property: 'left', to: 200 },
  { element: box2, property: 'top', to: 100 },
  { element: box3, property: 'opacity', to: 0 }
];

from(animationSteps).pipe(
  concatMap(step => 
    animate(step.element, step.property, step.to, 500)
  )
).subscribe({
  complete: () => console.log('Animation sequence complete')
});

// Practical: Queue processor
class TaskQueue {
  private tasks$ = new Subject<Task>();
  
  constructor() {
    this.tasks$.pipe(
      concatMap(task => this.executeTask(task))
    ).subscribe(result => {
      console.log('Task completed:', result);
    });
  }
  
  enqueue(task: Task) {
    this.tasks$.next(task);
  }
  
  private executeTask(task: Task): Observable<any> {
    return of(task).pipe(
      delay(task.duration),
      map(() => ({ id: task.id, status: 'completed' }))
    );
  }
}

const queue = new TaskQueue();
queue.enqueue({ id: 1, duration: 1000 });
queue.enqueue({ id: 2, duration: 500 });
queue.enqueue({ id: 3, duration: 800 });
// Executes in order: 1, then 2, then 3

// Practical: Form submission queue
const submitButton$ = fromEvent(submitBtn, 'click');

submitButton$.pipe(
  concatMap(() => {
    const formData = getFormData();
    return ajax.post('/api/submit', formData).pipe(
      tap(() => showSuccess()),
      catchError(err => {
        showError(err);
        return of({ error: err });
      })
    );
  })
).subscribe();
// Multiple rapid clicks processed sequentially

// Practical: Database migration steps
const migrationSteps = [
  () => createTables(),
  () => insertSeedData(),
  () => createIndexes(),
  () => updateConstraints()
];

from(migrationSteps).pipe(
  concatMap(step => from(step()))
).subscribe({
  next: result => console.log('Step completed:', result),
  error: err => console.error('Migration failed:', err),
  complete: () => console.log('Migration complete')
});

// Practical: Log file writing
const logEntries$ = new Subject<string>();

logEntries$.pipe(
  concatMap(entry => 
    ajax.post('/api/log', { entry, timestamp: Date.now() })
  )
).subscribe();

// Ensures logs written in order
logEntries$.next('User logged in');
logEntries$.next('User viewed page');
logEntries$.next('User clicked button');

// Practical: Sequential file processing
const files = ['file1.txt', 'file2.txt', 'file3.txt'];

from(files).pipe(
  concatMap(filename => 
    readFile(filename).pipe(
      concatMap(content => processContent(content)),
      concatMap(processed => writeFile(filename + '.out', processed))
    )
  )
).subscribe({
  complete: () => console.log('All files processed')
});

// Order guarantee comparison
// concatMap: Guaranteed order
clicks$.pipe(
  concatMap(() => saveData())
).subscribe(); // Saves execute in click order

// mergeMap: No order guarantee
clicks$.pipe(
  mergeMap(() => saveData())
).subscribe(); // Faster save might complete first

// switchMap: Only last click
clicks$.pipe(
  switchMap(() => saveData())
).subscribe(); // Only last click's save completes
Warning: concatMap queues all emissions - can cause memory issues if inner observables are slow and outer emits rapidly. Consider mergeMap with concurrency limit instead.

9.4 exhaustMap for Ignore-while-busy Pattern

Feature Syntax Description Behavior
exhaustMap exhaustMap(project) Maps to inner observable, ignores new emissions while inner is active Drop new emissions while busy
Ignore pattern Only accepts new outer emission after inner completes Prevents overlapping operations First-wins strategy
Use case Login buttons, form submission, prevent double-click When operation shouldn't overlap Debounce for async ops

Example: exhaustMap to prevent duplicate operations

import { fromEvent, of } from 'rxjs';
import { exhaustMap, delay, tap } from 'rxjs';

// Basic exhaustMap - ignores while busy
const clicks$ = fromEvent(button, 'click');

clicks$.pipe(
  exhaustMap(() => 
    of('Processing...').pipe(
      delay(2000),
      tap(() => console.log('Done'))
    )
  )
).subscribe(console.log);
// Rapid clicks ignored while processing

// Practical: Login button (prevent double submission)
const loginButton$ = fromEvent(loginBtn, 'click');

loginButton$.pipe(
  exhaustMap(() => {
    loginBtn.disabled = true;
    showSpinner();
    
    return ajax.post('/api/login', {
      username: usernameInput.value,
      password: passwordInput.value
    }).pipe(
      tap(response => {
        if (response.success) {
          redirectToDashboard();
        } else {
          showError('Invalid credentials');
        }
      }),
      catchError(err => {
        showError('Login failed');
        return of({ error: err });
      }),
      finalize(() => {
        loginBtn.disabled = false;
        hideSpinner();
      })
    );
  })
).subscribe();
// Multiple clicks ignored while login in progress

// Practical: Save button (prevent concurrent saves)
const saveButton$ = fromEvent(saveBtn, 'click');

saveButton$.pipe(
  exhaustMap(() => {
    const data = getCurrentFormData();
    
    return ajax.post('/api/save', data).pipe(
      tap(() => showSuccessMessage()),
      catchError(err => {
        showErrorMessage(err);
        return of(null);
      })
    );
  })
).subscribe();
// Ignores save attempts while save in progress

// Practical: Refresh button
const refreshButton$ = fromEvent(refreshBtn, 'click');

refreshButton$.pipe(
  exhaustMap(() => {
    showRefreshSpinner();
    
    return ajax.getJSON('/api/data').pipe(
      tap(data => updateView(data)),
      finalize(() => hideRefreshSpinner())
    );
  })
).subscribe();
// Ignores refresh clicks while refresh in progress

// Practical: Infinite scroll (prevent overlapping loads)
const scroll$ = fromEvent(window, 'scroll').pipe(
  filter(() => isNearBottom()),
  debounceTime(200)
);

scroll$.pipe(
  exhaustMap(() => 
    ajax.getJSON(`/api/items?page=${currentPage++}`).pipe(
      tap(items => appendItems(items))
    )
  )
).subscribe();
// Won't load next page until current page loaded

// Practical: File download (prevent duplicate downloads)
const downloadLinks$ = fromEvent(document, 'click').pipe(
  filter(e => e.target.classList.contains('download-link')),
  map(e => e.target.href)
);

downloadLinks$.pipe(
  exhaustMap(url => {
    showDownloadProgress();
    
    return ajax({
      url,
      method: 'GET',
      responseType: 'blob'
    }).pipe(
      tap(response => saveFile(response.response)),
      finalize(() => hideDownloadProgress())
    );
  })
).subscribe();

// Practical: Modal open (prevent opening multiple)
const openModalButton$ = fromEvent(openBtn, 'click');

openModalButton$.pipe(
  exhaustMap(() => {
    return showModal().pipe(
      // Modal returns observable that completes when closed
      tap(() => console.log('Modal opened')),
      switchMap(() => modalClosed$)
    );
  })
).subscribe(() => console.log('Modal closed'));
// Can't open new modal while one is open

// Practical: Payment processing
const payButton$ = fromEvent(payBtn, 'click');

payButton$.pipe(
  exhaustMap(() => {
    const paymentData = getPaymentData();
    payBtn.disabled = true;
    
    return ajax.post('/api/payment', paymentData).pipe(
      tap(response => {
        if (response.success) {
          showPaymentSuccess();
        } else {
          showPaymentError();
        }
      }),
      catchError(err => {
        showPaymentError(err);
        return of({ error: err });
      }),
      finalize(() => payBtn.disabled = false)
    );
  })
).subscribe();
// Critical: prevents double payment

// Comparison with other operators
// exhaustMap: Ignore new while busy
loginBtn$.pipe(
  exhaustMap(() => login())
); // Clicks during login ignored

// switchMap: Cancel old, start new
searchInput$.pipe(
  switchMap(term => search(term))
); // Old search cancelled

// concatMap: Queue all
submitBtn$.pipe(
  concatMap(() => submit())
); // All clicks queued and processed

// mergeMap: Run all concurrently
uploadBtn$.pipe(
  mergeMap(() => upload())
); // All clicks trigger concurrent uploads
Note: exhaustMap is perfect for login/submit buttons - ignores rapid clicks while operation in progress. Prevents duplicate submissions naturally.

9.5 expand for Recursive Observable Generation

Feature Syntax Description Pattern
expand expand(project, concurrent?, scheduler?) Recursively projects each emission to observable, subscribes to results Breadth-first expansion
Recursion Each emission fed back as input to projection function Continues until EMPTY returned Tree traversal, pagination
Termination Return EMPTY to stop recursion Must have base case to prevent infinite loop Conditional completion

Example: expand for recursive operations

import { of, EMPTY } from 'rxjs';
import { expand, map, delay, take } from 'rxjs';

// Basic expand - countdown
of(5).pipe(
  expand(n => n > 0 ? of(n - 1).pipe(delay(1000)) : EMPTY)
).subscribe(console.log);
// Output (1s apart): 5, 4, 3, 2, 1, 0

// Practical: Paginated API loading
function loadAllPages(page = 1) {
  return ajax.getJSON(`/api/items?page=${page}`).pipe(
    expand(response => 
      response.hasMore 
        ? ajax.getJSON(`/api/items?page=${response.nextPage}`)
        : EMPTY
    )
  );
}

loadAllPages().subscribe(
  response => {
    appendItems(response.items);
    console.log('Loaded page:', response.page);
  },
  err => console.error('Error:', err),
  () => console.log('All pages loaded')
);

// Practical: Exponential backoff retry
function retryWithBackoff(source$, maxRetries = 5) {
  return of({ attempt: 0 }).pipe(
    expand(state => 
      state.attempt < maxRetries
        ? source$.pipe(
            catchError(err => {
              const delay = Math.pow(2, state.attempt) * 1000;
              console.log(`Retry ${state.attempt + 1} in ${delay}ms`);
              return of({ attempt: state.attempt + 1 }).pipe(
                delay(delay)
              );
            })
          )
        : EMPTY
    ),
    filter(result => !result.attempt) // Only emit actual results
  );
}

retryWithBackoff(
  ajax.getJSON('/api/unreliable')
).subscribe(data => console.log('Success:', data));

// Practical: Tree traversal (file system)
interface FileNode {
  name: string;
  type: 'file' | 'folder';
  children?: FileNode[];
}

function traverseFileTree(node: FileNode): Observable<FileNode> {
  return of(node).pipe(
    expand(n => 
      n.type === 'folder' && n.children
        ? from(n.children)
        : EMPTY
    )
  );
}

const root = {
  name: 'root',
  type: 'folder',
  children: [
    { name: 'file1.txt', type: 'file' },
    {
      name: 'subfolder',
      type: 'folder',
      children: [
        { name: 'file2.txt', type: 'file' }
      ]
    }
  ]
};

traverseFileTree(root).subscribe(node => {
  console.log('Found:', node.name);
});

// Practical: Polling with increasing intervals
of(1000).pipe(
  expand(interval => {
    return ajax.getJSON('/api/status').pipe(
      delay(interval),
      map(() => Math.min(interval * 1.5, 30000)) // Max 30s
    );
  }),
  take(10) // Limit total polls
).subscribe();

// Practical: Fibonacci sequence generator
of([0, 1]).pipe(
  expand(([prev, curr]) => 
    curr < 100
      ? of([curr, prev + curr]).pipe(delay(100))
      : EMPTY
  ),
  map(([_, curr]) => curr)
).subscribe(n => console.log('Fibonacci:', n));

// Practical: Recursive API data collection
function fetchUserWithFriends(userId: number): Observable<User> {
  return ajax.getJSON(`/api/users/${userId}`).pipe(
    expand(user => 
      user.friends && user.friends.length > 0
        ? from(user.friends).pipe(
            mergeMap(friendId => 
              ajax.getJSON(`/api/users/${friendId}`)
            )
          )
        : EMPTY
    ),
    take(50) // Prevent infinite expansion
  );
}

// Practical: Breadth-first search
interface Node {
  id: number;
  neighbors: number[];
}

function bfs(startId: number, graph: Map<number, Node>): Observable<Node> {
  const visited = new Set<number>();
  
  return of(graph.get(startId)).pipe(
    expand(node => {
      visited.add(node.id);
      return from(node.neighbors).pipe(
        filter(id => !visited.has(id)),
        map(id => graph.get(id)),
        filter(n => n !== undefined)
      );
    })
  );
}

// Practical: Dynamic form field generation
of({ level: 1, maxLevel: 5 }).pipe(
  expand(state => 
    state.level < state.maxLevel
      ? ajax.getJSON(`/api/form-fields?level=${state.level}`).pipe(
          tap(fields => renderFormFields(fields)),
          map(() => ({ level: state.level + 1, maxLevel: state.maxLevel }))
        )
      : EMPTY
  )
).subscribe({
  complete: () => console.log('All form fields loaded')
});

// Practical: Power calculation with logging
of(2).pipe(
  expand(n => n < 1024 ? of(n * 2) : EMPTY),
  tap(n => console.log('Power of 2:', n))
).subscribe({
  complete: () => console.log('Complete')
});
// Output: 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024
Warning: Always include a termination condition (return EMPTY) in expand to prevent infinite loops. Use take() or similar as a safety limit.

9.6 window and windowTime for Grouped Emission Windows

Operator Syntax Description Output
window window(windowBoundaries$) Groups emissions into nested observables based on boundary emissions Observable of Observables
windowTime windowTime(windowTimeSpan, windowCreationInterval?) Groups emissions into time-based windows Observable of Observables
windowCount windowCount(windowSize, startWindowEvery?) Groups emissions into count-based windows Observable of Observables

Example: window operators for grouped processing

import { interval, fromEvent } from 'rxjs';
import { window, windowTime, windowCount, mergeAll, take, map, reduce } from 'rxjs';

// Basic window with boundary observable
const source$ = interval(1000);
const boundary$ = interval(3000);

source$.pipe(
  take(10),
  window(boundary$),
  mergeAll() // Flatten windows
).subscribe(console.log);

// Process each window
source$.pipe(
  take(10),
  window(boundary$),
  map(window$ => window$.pipe(
    reduce((acc, val) => acc + val, 0)
  )),
  mergeAll()
).subscribe(sum => console.log('Window sum:', sum));

// windowTime - time-based grouping
interval(500).pipe(
  take(10),
  windowTime(2000), // 2-second windows
  map((window$, index) => {
    console.log('Window', index, 'opened');
    return window$.pipe(
      reduce((acc, val) => [...acc, val], [])
    );
  }),
  mergeAll()
).subscribe(values => {
  console.log('Window values:', values);
});

// Practical: Batch click processing
const clicks$ = fromEvent(document, 'click');

clicks$.pipe(
  windowTime(1000), // 1-second windows
  map(window$ => window$.pipe(
    reduce((count) => count + 1, 0)
  )),
  mergeAll()
).subscribe(count => {
  if (count > 0) {
    console.log(`${count} clicks in last second`);
  }
});

// windowCount - count-based grouping
interval(1000).pipe(
  take(10),
  windowCount(3), // Groups of 3
  map((window$, index) => {
    console.log('Batch', index);
    return window$.pipe(
      reduce((acc, val) => [...acc, val], [])
    );
  }),
  mergeAll()
).subscribe(batch => {
  console.log('Processing batch:', batch);
});

// Practical: Real-time metrics aggregation
const metrics$ = interval(100).pipe(
  map(() => ({
    cpu: Math.random() * 100,
    memory: Math.random() * 100,
    timestamp: Date.now()
  }))
);

metrics$.pipe(
  windowTime(5000), // 5-second windows
  map(window$ => window$.pipe(
    reduce((acc, metric) => ({
      avgCpu: (acc.avgCpu + metric.cpu) / 2,
      avgMemory: (acc.avgMemory + metric.memory) / 2,
      count: acc.count + 1
    }), { avgCpu: 0, avgMemory: 0, count: 0 })
  )),
  mergeAll()
).subscribe(stats => {
  console.log('5s stats:', stats);
  sendToMonitoring(stats);
});

// Practical: Message batching for bulk API
const messages$ = new Subject<Message>();

messages$.pipe(
  windowTime(2000), // Batch every 2 seconds
  mergeMap(window$ => window$.pipe(
    reduce((acc, msg) => [...acc, msg], [])
  )),
  filter(batch => batch.length > 0),
  mergeMap(batch => 
    ajax.post('/api/messages/bulk', { messages: batch })
  )
).subscribe(() => {
  console.log('Batch sent');
});

// Send individual messages
messages$.next({ text: 'Hello' });
messages$.next({ text: 'World' });
// Automatically batched and sent every 2s

// Practical: Analytics event batching
const analyticsEvents$ = new Subject<AnalyticsEvent>();

analyticsEvents$.pipe(
  windowTime(10000), // 10-second windows
  mergeMap(window$ => window$.pipe(
    reduce((events, event) => [...events, event], [])
  )),
  filter(events => events.length > 0)
).subscribe(events => {
  sendAnalyticsBatch(events);
  console.log(`Sent ${events.length} analytics events`);
});

// Practical: Sliding window for moving average
interval(1000).pipe(
  take(20),
  map(() => Math.random() * 100),
  windowTime(5000, 1000), // 5s window, new window every 1s
  mergeMap(window$ => window$.pipe(
    reduce((acc, val) => [...acc, val], []),
    map(values => {
      const sum = values.reduce((a, b) => a + b, 0);
      return sum / values.length;
    })
  ))
).subscribe(avg => {
  console.log('5-second moving average:', avg.toFixed(2));
});

// Practical: Request rate limiting
const requests$ = new Subject<Request>();

requests$.pipe(
  windowTime(1000), // 1-second windows
  mergeMap(window$ => window$.pipe(
    take(10) // Max 10 requests per second
  ))
).subscribe(request => {
  processRequest(request);
});

// Practical: Grouped error tracking
const errors$ = new Subject<Error>();

errors$.pipe(
  windowTime(60000), // 1-minute windows
  mergeMap(window$ => window$.pipe(
    reduce((acc, error) => {
      acc[error.type] = (acc[error.type] || 0) + 1;
      return acc;
    }, {})
  ))
).subscribe(errorCounts => {
  console.log('Error counts (last minute):', errorCounts);
  if (Object.values(errorCounts).some(count => count > 10)) {
    triggerAlert('High error rate detected');
  }
});
Note: window operators return Observable of Observables (higher-order). Use mergeAll, switchAll, or map+mergeAll to process windows. Similar to buffer but with nested observables.

Section 9 Summary

  • switchMap cancels previous inner observable - perfect for search/autocomplete (only latest matters)
  • mergeMap runs all inner observables concurrently - use concurrent parameter to limit parallelism
  • concatMap queues and processes sequentially in order - when sequence matters
  • exhaustMap ignores new emissions while busy - ideal for login/submit buttons
  • expand recursively applies projection - pagination, tree traversal, always return EMPTY to terminate
  • window/windowTime groups emissions into nested observables - batch processing, metrics aggregation

10. Utility Operators and Stream Manipulation

10.1 tap for Side Effects without Affecting Stream

Feature Syntax Description Use Case
tap tap(observer | nextFn) Performs side effects without modifying stream values Logging, debugging, analytics
Full observer tap({ next, error, complete }) Handle all notification types Comprehensive side effects
Transparent Does not transform values or affect stream Pure observation without interference Non-invasive monitoring

Example: tap for debugging and side effects

import { of, interval } from 'rxjs';
import { tap, map, filter, take } from 'rxjs/operators';

// Basic tap - logging without affecting stream
of(1, 2, 3, 4, 5).pipe(
  tap(val => console.log('Before filter:', val)),
  filter(val => val % 2 === 0),
  tap(val => console.log('After filter:', val)),
  map(val => val * 10),
  tap(val => console.log('After map:', val))
).subscribe(val => console.log('Final:', val));

// Full observer syntax
interval(1000).pipe(
  take(3),
  tap({
    next: val => console.log('Next:', val),
    error: err => console.error('Error:', err),
    complete: () => console.log('Complete!')
  })
).subscribe();

// Practical: Analytics tracking
fromEvent(button, 'click').pipe(
  tap(event => {
    analytics.track('button_click', {
      buttonId: event.target.id,
      timestamp: Date.now()
    });
  }),
  map(event => event.target.value)
).subscribe(value => processValue(value));

// Practical: Debug pipeline
ajax.getJSON('/api/users').pipe(
  tap(response => console.log('Raw response:', response)),
  map(response => response.data),
  tap(data => console.log('Extracted data:', data)),
  filter(data => data.active),
  tap(data => console.log('Filtered data:', data)),
  map(data => transformData(data)),
  tap(transformed => console.log('Transformed:', transformed))
).subscribe(result => displayResult(result));

// Practical: Loading state management
this.http.get('/api/data').pipe(
  tap(() => this.isLoading = true),
  tap(() => this.loadingStartTime = Date.now()),
  tap({
    next: data => {
      this.isLoading = false;
      const duration = Date.now() - this.loadingStartTime;
      console.log(`Loaded in ${duration}ms`);
    },
    error: err => {
      this.isLoading = false;
      this.showError(err);
    }
  })
).subscribe(data => this.data = data);

// Practical: Caching side effect
const cache = new Map();

ajax.getJSON('/api/expensive').pipe(
  tap(data => {
    cache.set('expensive-data', data);
    cache.set('cached-at', Date.now());
  })
).subscribe();

// Practical: Progress tracking
const files = [file1, file2, file3, file4, file5];
let completed = 0;

from(files).pipe(
  mergeMap(file => uploadFile(file).pipe(
    tap(() => {
      completed++;
      const progress = (completed / files.length) * 100;
      updateProgressBar(progress);
      console.log(`Progress: ${progress.toFixed(0)}%`);
    })
  ), 2)
).subscribe({
  complete: () => console.log('All uploads complete')
});

// Practical: Request/response logging
const logRequest$ = (url: string) => ajax.getJSON(url).pipe(
  tap({
    subscribe: () => console.log(`[REQUEST] ${url}`),
    next: data => console.log(`[RESPONSE] ${url}:`, data),
    error: err => console.error(`[ERROR] ${url}:`, err),
    complete: () => console.log(`[COMPLETE] ${url}`),
    finalize: () => console.log(`[FINALIZE] ${url}`)
  })
);

// Practical: Form validation feedback
formValue$.pipe(
  tap(() => clearValidationErrors()),
  switchMap(value => validateForm(value)),
  tap(validation => {
    if (validation.errors.length > 0) {
      displayErrors(validation.errors);
    }
  })
).subscribe();

// Practical: Debugging slow operations
source$.pipe(
  tap(() => console.time('operation')),
  map(heavyComputation),
  tap(() => console.timeEnd('operation'))
).subscribe();

// Practical: State synchronization
dataStream$.pipe(
  tap(data => {
    localStorage.setItem('lastData', JSON.stringify(data));
    this.store.dispatch(updateAction(data));
  })
).subscribe(data => this.currentData = data);

// Practical: Monitoring and metrics
api$.pipe(
  tap({
    next: () => metrics.increment('api.success'),
    error: () => metrics.increment('api.error')
  })
).subscribe();
Note: tap is transparent - never modifies stream values. Perfect for logging, debugging, analytics, and side effects without affecting data flow.

10.2 delay and delayWhen for Time-based Stream Delays

Operator Syntax Description Delay Strategy
delay delay(dueTime, scheduler?) Delays all emissions by fixed duration (ms) Constant delay for all values
delay (Date) delay(new Date('2024-01-01')) Delays emissions until specific date/time Absolute time delay
delayWhen delayWhen(delayDurationSelector) Delays each emission by duration from selector observable Dynamic per-value delay

Example: delay operators for timing control

import { of, interval, timer } from 'rxjs';
import { delay, delayWhen, map, take } from 'rxjs/operators';

// Basic delay - constant delay
of(1, 2, 3).pipe(
  delay(1000) // Delay entire stream by 1s
).subscribe(val => console.log('Delayed:', val));

// Delay each emission
interval(500).pipe(
  take(5),
  delay(2000) // Each value delayed by 2s from original time
).subscribe(val => console.log('Value:', val));

// Delay until specific time
const futureTime = new Date(Date.now() + 5000);
of('Message').pipe(
  delay(futureTime)
).subscribe(msg => console.log('Delivered at scheduled time:', msg));

// delayWhen - dynamic delay per value
of(1, 2, 3).pipe(
  delayWhen(val => timer(val * 1000))
).subscribe(val => console.log('Dynamic delay:', val));
// 1 after 1s, 2 after 2s, 3 after 3s

// Practical: Retry with exponential backoff
function retryWithBackoff(source$, maxRetries = 3) {
  return source$.pipe(
    retryWhen(errors => errors.pipe(
      mergeMap((error, index) => {
        if (index >= maxRetries) {
          return throwError(() => error);
        }
        const delayMs = Math.pow(2, index) * 1000;
        console.log(`Retry ${index + 1} after ${delayMs}ms`);
        return of(error).pipe(delay(delayMs));
      })
    ))
  );
}

retryWithBackoff(ajax.getJSON('/api/flaky')).subscribe(
  data => console.log('Success:', data),
  err => console.error('Failed after retries:', err)
);

// Practical: Staggered animation
const elements = [box1, box2, box3, box4];

from(elements).pipe(
  delayWhen((element, index) => timer(index * 200))
).subscribe(element => {
  element.classList.add('animate-in');
});

// Practical: Rate limiting with delay
const requests = [req1, req2, req3, req4, req5];

from(requests).pipe(
  concatMap(req => ajax(req).pipe(
    delay(200) // 200ms between requests
  ))
).subscribe(response => processResponse(response));

// Practical: Delayed notification
of('Session expiring soon').pipe(
  delay(25 * 60 * 1000) // 25 minutes
).subscribe(message => showWarning(message));

// Practical: Simulated network latency (testing)
const mockAPI = (data) => of(data).pipe(
  delay(Math.random() * 1000 + 500) // 500-1500ms random delay
);

mockAPI({ users: [...] }).subscribe(data => {
  console.log('Mock response:', data);
});

// Practical: Tooltip delay
fromEvent(element, 'mouseenter').pipe(
  switchMap(() => of(true).pipe(
    delay(500), // Show tooltip after 500ms hover
    takeUntil(fromEvent(element, 'mouseleave'))
  ))
).subscribe(() => showTooltip());

// Practical: Debounced save with confirmation delay
formValue$.pipe(
  debounceTime(1000),
  switchMap(value => saveData(value).pipe(
    switchMap(response => of(response).pipe(
      tap(() => showSaveSuccess()),
      delay(2000), // Hide success message after 2s
      tap(() => hideSaveSuccess())
    ))
  ))
).subscribe();

// Practical: Message queue with processing delay
const messageQueue$ = new Subject<Message>();

messageQueue$.pipe(
  concatMap(message => of(message).pipe(
    delay(100), // Process at max 10 msgs/second
    tap(msg => processMessage(msg))
  ))
).subscribe();

// Practical: Scheduled task execution
const tasks = [task1, task2, task3];
const startTime = new Date(Date.now() + 10000); // Start in 10s

from(tasks).pipe(
  delay(startTime),
  concatMap(task => executeTask(task))
).subscribe();

// delayWhen with conditional delay
source$.pipe(
  delayWhen(value => 
    value.priority === 'high' 
      ? timer(0) // No delay for high priority
      : timer(5000) // 5s delay for normal
  )
).subscribe();

10.3 repeat and repeatWhen for Stream Repetition

Operator Syntax Description Repetition Control
repeat repeat(count?) Re-subscribes to source when it completes Fixed count or infinite
repeatWhen repeatWhen(notifier) Re-subscribes based on notifier observable emissions Dynamic, conditional repetition
Behavior Only repeats on completion, not on error For errors, use retry/retryWhen Success repetition only

Example: repeat operators for stream repetition

import { of, interval, fromEvent } from 'rxjs';
import { repeat, repeatWhen, take, delay, tap } from 'rxjs/operators';

// Basic repeat - repeat 3 times
of(1, 2, 3).pipe(
  tap(val => console.log('Value:', val)),
  repeat(3)
).subscribe({
  complete: () => console.log('All repetitions complete')
});
// Output: 1,2,3, 1,2,3, 1,2,3

// Infinite repeat
interval(1000).pipe(
  take(3),
  tap(val => console.log('Interval:', val)),
  repeat() // Infinite - careful!
).subscribe();
// Output: 0,1,2, 0,1,2, 0,1,2... forever

// repeatWhen - conditional repetition
of('Attempt').pipe(
  tap(val => console.log(val)),
  repeatWhen(notifications => notifications.pipe(
    delay(1000), // Wait 1s between repetitions
    take(3) // Repeat 3 times
  ))
).subscribe({
  complete: () => console.log('Done repeating')
});

// Practical: Polling
function pollAPI(url: string, intervalMs: number) {
  return ajax.getJSON(url).pipe(
    repeatWhen(notifications => notifications.pipe(
      delay(intervalMs)
    ))
  );
}

pollAPI('/api/status', 5000).pipe(
  takeUntil(stopPolling$)
).subscribe(status => updateStatus(status));

// Practical: Heartbeat
of('ping').pipe(
  tap(() => console.log('Heartbeat sent')),
  switchMap(() => ajax.post('/api/heartbeat')),
  repeatWhen(notifications => notifications.pipe(
    delay(30000) // Every 30 seconds
  )),
  takeUntil(disconnect$)
).subscribe();

// Practical: Game loop
function gameLoop() {
  return of(null).pipe(
    tap(() => {
      updateGameState();
      renderFrame();
    }),
    repeatWhen(notifications => notifications.pipe(
      delay(16) // ~60 FPS
    ))
  );
}

gameLoop().pipe(
  takeUntil(fromEvent(stopButton, 'click'))
).subscribe();

// Practical: Retry with increasing delay
let attemptCount = 0;

ajax.getJSON('/api/data').pipe(
  tap(() => attemptCount++),
  repeatWhen(notifications => notifications.pipe(
    delayWhen(() => timer(Math.min(attemptCount * 1000, 10000))),
    take(5) // Max 5 attempts
  ))
).subscribe(
  data => console.log('Data:', data),
  err => console.error('Failed:', err)
);

// Practical: Scheduled tasks
of('Task execution').pipe(
  tap(() => executeScheduledTask()),
  repeatWhen(notifications => notifications.pipe(
    delay(60 * 60 * 1000) // Every hour
  )),
  takeUntil(shutdownSignal$)
).subscribe();

// Practical: Animation loop with repeat
function animateElement(element, duration) {
  return of(element).pipe(
    tap(el => el.classList.add('animate')),
    delay(duration),
    tap(el => el.classList.remove('animate')),
    delay(500), // Pause between animations
    repeat(3) // Animate 3 times
  );
}

animateElement(box, 1000).subscribe({
  complete: () => console.log('Animation complete')
});

// Practical: Data sync with conditional repeat
function syncData() {
  return ajax.post('/api/sync', localChanges).pipe(
    tap(response => {
      if (response.hasMore) {
        console.log('More data to sync');
      }
    }),
    repeatWhen(notifications => notifications.pipe(
      delayWhen(() => timer(1000)),
      takeWhile(() => hasLocalChanges())
    ))
  );
}

// Practical: Queue processor with repeat
const queue = [];

of(null).pipe(
  tap(() => {
    if (queue.length > 0) {
      const item = queue.shift();
      processItem(item);
    }
  }),
  repeatWhen(notifications => notifications.pipe(
    delay(100)
  )),
  takeUntil(stopProcessing$)
).subscribe();

// Practical: Metrics collection
of(null).pipe(
  switchMap(() => collectMetrics()),
  tap(metrics => sendToAnalytics(metrics)),
  repeatWhen(notifications => notifications.pipe(
    delay(10000) // Collect every 10 seconds
  ))
).subscribe();

// Practical: Keep-alive connection
of('keep-alive').pipe(
  switchMap(() => ajax.post('/api/keep-alive')),
  repeatWhen(notifications => notifications.pipe(
    delay(45000) // Every 45 seconds
  )),
  takeUntil(connectionClosed$)
).subscribe(
  response => console.log('Keep-alive sent'),
  err => console.error('Keep-alive failed:', err)
);
Note: repeat only triggers on completion, not errors. Use with takeUntil to stop infinite repetition. For errors, use retry/retryWhen instead.

10.4 sample and sampleTime for Periodic Sampling

Operator Syntax Description Sampling Strategy
sample sample(notifier$) Emits most recent value when notifier emits Event-based sampling
sampleTime sampleTime(period) Emits most recent value periodically (ms) Time-based sampling
Behavior Only emits if source emitted since last sample Ignores periods with no source emissions Leading edge sampling

Example: sample operators for value sampling

import { interval, fromEvent } from 'rxjs';
import { sample, sampleTime, map, take } from 'rxjs/operators';

// Basic sample - sample on click
const source$ = interval(500).pipe(take(20));
const clicks$ = fromEvent(document, 'click');

source$.pipe(
  sample(clicks$)
).subscribe(val => console.log('Sampled on click:', val));
// Emits latest interval value when user clicks

// sampleTime - periodic sampling
interval(100).pipe(
  sampleTime(1000)
).subscribe(val => console.log('Sample:', val));
// Samples every 1 second (emits latest value from 100ms intervals)

// Practical: Mouse position sampling
const mouseMove$ = fromEvent(document, 'mousemove').pipe(
  map(e => ({ x: e.clientX, y: e.clientY }))
);

mouseMove$.pipe(
  sampleTime(200) // Sample every 200ms
).subscribe(pos => {
  updatePositionDisplay(pos);
  // Reduces updates from potentially thousands to 5/second
});

// Practical: Sensor data throttling
const sensorData$ = interval(10).pipe(
  map(() => ({
    temperature: readTemperature(),
    humidity: readHumidity(),
    timestamp: Date.now()
  }))
);

sensorData$.pipe(
  sampleTime(5000) // Sample every 5 seconds
).subscribe(data => {
  logSensorData(data);
  // Reduces logging from 100/sec to 1/5sec
});

// Practical: Form value sampling on button click
const formValue$ = formControl.valueChanges;
const submitClick$ = fromEvent(submitBtn, 'click');

formValue$.pipe(
  sample(submitClick$)
).subscribe(value => {
  console.log('Form value on submit:', value);
  submitForm(value);
});

// Practical: Window resize sampling
fromEvent(window, 'resize').pipe(
  sampleTime(500) // Sample every 500ms
).subscribe(() => {
  recalculateLayout();
  // Prevents excessive recalculations during resize
});

// Practical: Scroll position sampling
fromEvent(window, 'scroll').pipe(
  map(() => window.scrollY),
  sampleTime(100)
).subscribe(scrollY => {
  updateScrollIndicator(scrollY);
  checkLazyLoadImages(scrollY);
});

// Practical: Real-time chart updates
const dataStream$ = websocket.pipe(
  map(msg => msg.value)
);

dataStream$.pipe(
  sampleTime(1000) // Update chart every second
).subscribe(value => {
  addDataPointToChart(value);
  // Smooth chart updates instead of jerky real-time
});

// Practical: Network bandwidth sampling
const bytesTransferred$ = new Subject<number>();

bytesTransferred$.pipe(
  scan((acc, bytes) => acc + bytes, 0),
  sampleTime(1000),
  map((total, index) => {
    const previous = this.previousTotal || 0;
    const bandwidth = (total - previous) / 1024; // KB/s
    this.previousTotal = total;
    return bandwidth;
  })
).subscribe(bandwidth => {
  displayBandwidth(`${bandwidth.toFixed(2)} KB/s`);
});

// Practical: Game state sampling
const gameState$ = new BehaviorSubject(initialState);

gameState$.pipe(
  sampleTime(16) // Sample at ~60 FPS
).subscribe(state => {
  renderGame(state);
});

// Practical: Audio level meter
const audioLevel$ = new Subject<number>();

audioLevel$.pipe(
  sampleTime(50) // Update 20 times per second
).subscribe(level => {
  updateAudioMeter(level);
});

// Practical: Sample on interval
const rapidSource$ = interval(50);
const sampler$ = interval(1000);

rapidSource$.pipe(
  sample(sampler$)
).subscribe(val => console.log('Sampled:', val));
// Samples every 1 second instead of every 50ms

// Practical: Price ticker sampling
const priceUpdates$ = websocket.pipe(
  filter(msg => msg.type === 'price'),
  map(msg => msg.price)
);

priceUpdates$.pipe(
  sampleTime(500) // Update UI twice per second
).subscribe(price => {
  updatePriceDisplay(price);
});

10.5 audit and auditTime for Trailing Edge Throttling

Operator Syntax Description Edge
audit audit(durationSelector) Emits most recent value after duration selector completes Trailing edge
auditTime auditTime(duration) Emits most recent value after fixed duration Trailing edge
vs throttle audit emits trailing (last), throttle emits leading (first) Opposite edge sampling Different use cases

Example: audit operators for trailing edge throttling

import { fromEvent, interval, timer } from 'rxjs';
import { audit, auditTime, map, throttleTime } from 'rxjs/operators';

// Basic auditTime
fromEvent(button, 'click').pipe(
  auditTime(1000)
).subscribe(() => console.log('Click processed (trailing)'));
// Ignores clicks for 1s after each processed click

// audit with dynamic duration
fromEvent(button, 'click').pipe(
  audit(() => timer(1000))
).subscribe(() => console.log('Audited click'));

// Practical: Search input (trailing edge)
fromEvent(searchInput, 'input').pipe(
  map(e => e.target.value),
  auditTime(500)
).subscribe(searchTerm => {
  performSearch(searchTerm);
  // Searches with final value after user stops typing for 500ms
});

// Practical: Window resize (final dimensions)
fromEvent(window, 'resize').pipe(
  auditTime(300)
).subscribe(() => {
  const width = window.innerWidth;
  const height = window.innerHeight;
  recalculateLayout(width, height);
  // Uses final dimensions after resize settles
});

// Practical: Scroll position (final position)
fromEvent(window, 'scroll').pipe(
  map(() => window.scrollY),
  auditTime(200)
).subscribe(finalScrollY => {
  updateScrollBasedContent(finalScrollY);
  // Updates based on final scroll position
});

// Practical: Button click flood prevention
fromEvent(actionBtn, 'click').pipe(
  auditTime(2000)
).subscribe(() => {
  performExpensiveAction();
  // Executes with last click in 2-second window
});

// Practical: Form field validation (final value)
fromEvent(emailInput, 'input').pipe(
  map(e => e.target.value),
  auditTime(800)
).subscribe(email => {
  validateEmail(email);
  // Validates final value after user stops typing
});

// Practical: Drag end position
fromEvent(element, 'drag').pipe(
  map(e => ({ x: e.clientX, y: e.clientY })),
  auditTime(100)
).subscribe(finalPosition => {
  updateElementPosition(finalPosition);
});

// Compare: throttleTime (leading) vs auditTime (trailing)
const clicks$ = fromEvent(button, 'click');

// throttleTime - emits first, ignores rest
clicks$.pipe(
  throttleTime(1000)
).subscribe(() => console.log('Throttle: First click'));

// auditTime - emits last after period
clicks$.pipe(
  auditTime(1000)
).subscribe(() => console.log('Audit: Last click'));

// Practical: Auto-save (save final changes)
formValue$.pipe(
  auditTime(3000) // Wait 3s of no changes
).subscribe(value => {
  autoSave(value);
  showSaveIndicator();
});

// Practical: Live preview update (final content)
fromEvent(textarea, 'input').pipe(
  map(e => e.target.value),
  auditTime(500)
).subscribe(content => {
  updatePreview(content);
  // Preview shows final content after typing pauses
});

// Practical: API rate limiting (trailing requests)
const apiRequests$ = new Subject<Request>();

apiRequests$.pipe(
  auditTime(1000) // Max 1 request per second (last one wins)
).subscribe(request => {
  sendRequest(request);
});

// Practical: Mouse hover end detection
fromEvent(element, 'mousemove').pipe(
  auditTime(1000)
).subscribe(() => {
  console.log('Mouse stopped moving');
  showContextMenu();
});

// audit with custom duration selector
clicks$.pipe(
  audit(click => {
    // Longer delay for rapid clicks
    return timer(rapidClickDetected ? 2000 : 500);
  })
).subscribe();

// Practical: Zoom level adjustment (final zoom)
fromEvent(canvas, 'wheel').pipe(
  map(e => e.deltaY),
  auditTime(150)
).subscribe(delta => {
  adjustZoom(delta);
  renderAtNewZoom();
});
Note: auditTime emits the trailing (last) value, unlike throttleTime which emits leading (first). Use audit for capturing final state after activity settles.

10.6 materialize and dematerialize for Notification Objects

Operator Syntax Description Transformation
materialize materialize() Converts emissions/errors/completion to Notification objects Observable<T> → Observable<Notification<T>>
dematerialize dematerialize() Converts Notification objects back to normal emissions Observable<Notification<T>> → Observable<T>
Notification types next, error, complete Metadata about stream events Reify stream semantics

Example: materialize and dematerialize for metadata handling

import { of, throwError, EMPTY } from 'rxjs';
import { materialize, dematerialize, map, delay } from 'rxjs/operators';

// Basic materialize - convert to notifications
of(1, 2, 3).pipe(
  materialize()
).subscribe(notification => {
  console.log('Kind:', notification.kind);
  console.log('Value:', notification.value);
  console.log('Has value:', notification.hasValue);
});
// Output:
// { kind: 'N', value: 1, hasValue: true }
// { kind: 'N', value: 2, hasValue: true }
// { kind: 'N', value: 3, hasValue: true }
// { kind: 'C', hasValue: false }

// Error notification
throwError(() => new Error('Oops')).pipe(
  materialize()
).subscribe(notification => {
  console.log('Kind:', notification.kind); // 'E'
  console.log('Error:', notification.error);
});

// dematerialize - convert back
of(
  { kind: 'N', value: 1 },
  { kind: 'N', value: 2 },
  { kind: 'C' }
).pipe(
  dematerialize()
).subscribe({
  next: val => console.log('Value:', val),
  complete: () => console.log('Complete')
});

// Practical: Error logging without stopping stream
ajax.getJSON('/api/data').pipe(
  materialize(),
  tap(notification => {
    if (notification.kind === 'E') {
      console.error('API Error:', notification.error);
      logError(notification.error);
    }
  }),
  dematerialize()
).subscribe();

// Practical: Delay errors
source$.pipe(
  materialize(),
  delay(1000), // Delay all notifications including errors
  dematerialize()
).subscribe();

// Practical: Convert errors to values
source$.pipe(
  materialize(),
  map(notification => {
    if (notification.kind === 'E') {
      return { kind: 'N', value: { error: notification.error } };
    }
    return notification;
  }),
  dematerialize()
).subscribe(result => {
  if (result.error) {
    handleError(result.error);
  } else {
    handleSuccess(result);
  }
});

// Practical: Stream replay with timing
const notifications = [];

source$.pipe(
  materialize(),
  tap(n => notifications.push({
    notification: n,
    timestamp: Date.now()
  }))
).subscribe();

// Later, replay with original timing
function replayStream() {
  const startTime = Date.now();
  
  from(notifications).pipe(
    concatMap(item => {
      const delay = item.timestamp - startTime;
      return of(item.notification).pipe(delay(Math.max(0, delay)));
    }),
    dematerialize()
  ).subscribe();
}

// Practical: Error recovery with metadata
source$.pipe(
  materialize(),
  scan((acc, notification) => {
    if (notification.kind === 'E') {
      acc.errors.push(notification.error);
      return { ...acc, lastError: notification.error };
    }
    if (notification.kind === 'N') {
      acc.values.push(notification.value);
    }
    return acc;
  }, { values: [], errors: [], lastError: null }),
  map(state => {
    // Convert back to notification
    if (state.lastError) {
      return { kind: 'N', value: { error: state.lastError } };
    }
    return { kind: 'N', value: state.values[state.values.length - 1] };
  }),
  dematerialize()
).subscribe();

// Practical: Notification filtering
source$.pipe(
  materialize(),
  filter(notification => {
    // Filter out specific errors
    if (notification.kind === 'E') {
      return !isIgnorableError(notification.error);
    }
    return true;
  }),
  dematerialize()
).subscribe();

// Practical: Testing observable behavior
testScheduler.run(({ cold, expectObservable }) => {
  const source$ = cold('--a--b--#', { a: 1, b: 2 }, new Error('fail'));
  
  const materialized$ = source$.pipe(materialize());
  
  expectObservable(materialized$).toBe('--a--b--(c|)', {
    a: { kind: 'N', value: 1, hasValue: true },
    b: { kind: 'N', value: 2, hasValue: true },
    c: { kind: 'E', error: new Error('fail'), hasValue: false }
  });
});

// Practical: Stream introspection
source$.pipe(
  materialize(),
  tap(notification => {
    metrics.increment(`stream.${notification.kind}`);
    
    if (notification.kind === 'N') {
      console.log('Value emitted:', notification.value);
    } else if (notification.kind === 'E') {
      console.error('Error occurred:', notification.error);
    } else if (notification.kind === 'C') {
      console.log('Stream completed');
    }
  }),
  dematerialize()
).subscribe();

// Practical: Conditional retry based on error type
source$.pipe(
  materialize(),
  expand(notification => {
    if (notification.kind === 'E' && isRetryable(notification.error)) {
      return source$.pipe(
        delay(1000),
        materialize()
      );
    }
    return EMPTY;
  }),
  dematerialize()
).subscribe();
Note: materialize/dematerialize convert stream events to/from Notification objects. Useful for delaying errors, logging stream behavior, or manipulating stream semantics.

Section 10 Summary

  • tap performs side effects without modifying stream - perfect for logging, analytics, debugging
  • delay delays all emissions by fixed duration, delayWhen applies dynamic per-value delays
  • repeat re-subscribes on completion, repeatWhen enables conditional repetition with delays
  • sample/sampleTime emits most recent value periodically - reduces high-frequency streams
  • audit/auditTime emits trailing edge (last value after silence) - opposite of throttle
  • materialize/dematerialize converts stream events to Notification objects for metadata manipulation

11. Conditional and Boolean Logic Operators

11.1 iif for Conditional Observable Selection

Feature Syntax Description Use Case
iif iif(condition, trueResult$, falseResult$?) Subscribes to one observable based on condition evaluated at subscription time Conditional stream selection
Lazy evaluation Condition checked when subscribed, not when created Deferred decision making Runtime branching
Default If falseResult$ omitted, emits EMPTY Optional false branch Conditional execution

Example: iif for conditional observable selection

import { iif, of, EMPTY, interval } from 'rxjs';
import { mergeMap, map } from 'rxjs/operators';

// Basic iif - simple conditional
const condition = Math.random() > 0.5;

iif(
  () => condition,
  of('Condition is true'),
  of('Condition is false')
).subscribe(console.log);

// Without false branch - emits EMPTY
iif(
  () => false,
  of('True branch')
  // No false branch - completes immediately
).subscribe({
  next: val => console.log(val),
  complete: () => console.log('Completed (empty)')
});

// Practical: Conditional data source
function getData(useCache: boolean) {
  return iif(
    () => useCache,
    of(cachedData),
    ajax.getJSON('/api/data')
  );
}

getData(hasCachedData).subscribe(data => displayData(data));

// Practical: Authentication check
const isAuthenticated$ = iif(
  () => hasAuthToken(),
  ajax.getJSON('/api/user/profile'),
  of(null)
);

isAuthenticated$.subscribe(profile => {
  if (profile) {
    displayProfile(profile);
  } else {
    redirectToLogin();
  }
});

// Practical: Environment-based configuration
const config$ = iif(
  () => process.env.NODE_ENV === 'production',
  ajax.getJSON('/api/config/production'),
  of(developmentConfig)
);

config$.subscribe(config => initializeApp(config));

// Practical: Feature flag
function getFeatureData(featureEnabled: boolean) {
  return iif(
    () => featureEnabled,
    ajax.getJSON('/api/feature/data'),
    of({ message: 'Feature disabled' })
  );
}

// Practical: Conditional retry
function fetchWithRetry(url: string, shouldRetry: boolean) {
  return ajax.getJSON(url).pipe(
    catchError(err => iif(
      () => shouldRetry,
      ajax.getJSON(url).pipe(delay(1000)), // Retry
      throwError(() => err) // Don't retry
    ))
  );
}

// Practical: User preference based loading
const theme$ = iif(
  () => userPreferences.theme === 'dark',
  of('dark-theme.css'),
  of('light-theme.css')
).pipe(
  mergeMap(themePath => loadStylesheet(themePath))
);

// Practical: A/B testing
const variant$ = iif(
  () => Math.random() < 0.5,
  of('variant-A'),
  of('variant-B')
);

variant$.subscribe(variant => {
  trackVariant(variant);
  showVariant(variant);
});

// Practical: Conditional polling
const pollInterval$ = iif(
  () => isHighPriority,
  interval(1000), // Poll every 1s
  interval(5000)  // Poll every 5s
);

pollInterval$.pipe(
  switchMap(() => ajax.getJSON('/api/status'))
).subscribe(status => updateStatus(status));

// Practical: Device-based content loading
const content$ = iif(
  () => isMobileDevice(),
  ajax.getJSON('/api/content/mobile'),
  ajax.getJSON('/api/content/desktop')
);

// Practical: Offline/online handling
const data$ = iif(
  () => navigator.onLine,
  ajax.getJSON('/api/data'),
  of(offlineData)
);

data$.subscribe(data => renderData(data));

// With mergeMap for dynamic conditions
clicks$.pipe(
  mergeMap(() => iif(
    () => currentUser.isPremium,
    getPremiumContent(),
    getFreeContent()
  ))
).subscribe(content => displayContent(content));
Note: iif evaluates condition at subscription time, not creation time. For in-stream conditionals, use filter or other operators.

11.2 every for Universal Condition Checking

Feature Syntax Description Result
every every(predicate, thisArg?) Returns true if all values satisfy predicate, false otherwise Single boolean emission
Short-circuit Completes with false on first failing value Doesn't wait for all values Early termination
Empty stream Returns true for empty observables Vacuous truth Logical consistency

Example: every for universal condition checking

import { of, from, range } from 'rxjs';
import { every, map } from 'rxjs/operators';

// Basic every - all values satisfy condition
of(2, 4, 6, 8).pipe(
  every(val => val % 2 === 0)
).subscribe(result => console.log('All even?', result)); // true

// Fails on first non-matching value
of(2, 4, 5, 8).pipe(
  every(val => val % 2 === 0)
).subscribe(result => console.log('All even?', result)); // false

// Empty stream returns true
of().pipe(
  every(val => val > 100)
).subscribe(result => console.log('Empty stream:', result)); // true

// Practical: Form validation - all fields valid
const formFields$ = from([
  { name: 'email', value: 'user@example.com', valid: true },
  { name: 'password', value: 'secret123', valid: true },
  { name: 'age', value: '25', valid: true }
]);

formFields$.pipe(
  every(field => field.valid)
).subscribe(allValid => {
  if (allValid) {
    enableSubmitButton();
  } else {
    showValidationErrors();
  }
});

// Practical: Permissions check
const requiredPermissions = ['read', 'write', 'delete'];
const userPermissions$ = from(requiredPermissions);

userPermissions$.pipe(
  every(permission => hasPermission(permission))
).subscribe(hasAllPermissions => {
  if (hasAllPermissions) {
    showAdminPanel();
  } else {
    showAccessDenied();
  }
});

// Practical: Data quality check
ajax.getJSON('/api/records').pipe(
  mergeMap(response => from(response.records)),
  every(record => {
    return record.id && 
           record.name && 
           record.timestamp &&
           typeof record.value === 'number';
  })
).subscribe(dataValid => {
  if (dataValid) {
    console.log('All records valid');
    processRecords();
  } else {
    console.error('Invalid data detected');
    rejectBatch();
  }
});

// Practical: Age verification
const users$ = ajax.getJSON('/api/users');

users$.pipe(
  mergeMap(response => from(response.users)),
  every(user => user.age >= 18)
).subscribe(allAdults => {
  if (allAdults) {
    proceedWithContent();
  } else {
    showAgeRestrictionWarning();
  }
});

// Practical: Configuration validation
const configEntries$ = from(Object.entries(config));

configEntries$.pipe(
  every(([key, value]) => value !== null && value !== undefined)
).subscribe(configComplete => {
  if (configComplete) {
    initializeApp();
  } else {
    showConfigurationError();
  }
});

// Practical: File upload validation
const files$ = from(selectedFiles);

files$.pipe(
  every(file => {
    const validType = ['image/jpeg', 'image/png'].includes(file.type);
    const validSize = file.size < 5 * 1024 * 1024; // 5MB
    return validType && validSize;
  })
).subscribe(allValid => {
  if (allValid) {
    uploadFiles(selectedFiles);
  } else {
    showFileValidationError();
  }
});

// Practical: Network connectivity check
const endpoints$ = from([
  '/api/health',
  '/api/status',
  '/api/ping'
]);

endpoints$.pipe(
  mergeMap(endpoint => ajax.getJSON(endpoint).pipe(
    map(() => true),
    catchError(() => of(false))
  )),
  every(success => success)
).subscribe(allReachable => {
  if (allReachable) {
    setOnlineStatus(true);
  } else {
    setOnlineStatus(false);
    showOfflineWarning();
  }
});

// Practical: Dependencies loaded check
const dependencies$ = from([
  loadScript('jquery.js'),
  loadScript('bootstrap.js'),
  loadScript('app.js')
]);

dependencies$.pipe(
  every(loaded => loaded === true)
).subscribe(allLoaded => {
  if (allLoaded) {
    initializeApplication();
  } else {
    showLoadingError();
  }
});

// Practical: Inventory check
const products$ = from(shoppingCart.items);

products$.pipe(
  mergeMap(item => checkStock(item.productId)),
  every(inStock => inStock)
).subscribe(allAvailable => {
  if (allAvailable) {
    proceedToCheckout();
  } else {
    showOutOfStockMessage();
  }
});
Note: every short-circuits on first false - efficient for large streams. Returns single boolean value then completes.

11.3 find and findIndex for First Match Operations

Operator Syntax Description Returns
find find(predicate, thisArg?) Emits first value that satisfies predicate, then completes First matching value or undefined
findIndex findIndex(predicate, thisArg?) Emits index of first value that satisfies predicate Index number or -1
Behavior Completes after first match or when source completes Short-circuits on match Early termination

Example: find and findIndex for searching

import { from, range } from 'rxjs';
import { find, findIndex, delay } from 'rxjs/operators';

// Basic find - first match
from([1, 2, 3, 4, 5]).pipe(
  find(val => val > 3)
).subscribe(result => console.log('Found:', result)); // 4

// No match returns undefined
from([1, 2, 3]).pipe(
  find(val => val > 10)
).subscribe(result => console.log('Found:', result)); // undefined

// findIndex - get position
from(['a', 'b', 'c', 'd']).pipe(
  findIndex(val => val === 'c')
).subscribe(index => console.log('Index:', index)); // 2

// No match returns -1
from(['a', 'b', 'c']).pipe(
  findIndex(val => val === 'z')
).subscribe(index => console.log('Index:', index)); // -1

// Practical: Find user by ID
ajax.getJSON('/api/users').pipe(
  mergeMap(response => from(response.users)),
  find(user => user.id === targetUserId)
).subscribe(user => {
  if (user) {
    displayUserProfile(user);
  } else {
    showUserNotFound();
  }
});

// Practical: Find first available slot
const timeSlots$ = from([
  { time: '09:00', available: false },
  { time: '10:00', available: false },
  { time: '11:00', available: true },
  { time: '12:00', available: true }
]);

timeSlots$.pipe(
  find(slot => slot.available)
).subscribe(slot => {
  if (slot) {
    console.log('First available:', slot.time);
    bookAppointment(slot);
  }
});

// Practical: Find error in logs
const logs$ = ajax.getJSON('/api/logs').pipe(
  mergeMap(response => from(response.logs))
);

logs$.pipe(
  find(log => log.level === 'ERROR')
).subscribe(errorLog => {
  if (errorLog) {
    highlightError(errorLog);
    console.error('First error:', errorLog);
  }
});

// Practical: Find product by name
const products$ = from(productCatalog);

products$.pipe(
  find(product => product.name.toLowerCase().includes(searchTerm))
).subscribe(product => {
  if (product) {
    navigateToProduct(product.id);
  } else {
    showNoResultsMessage();
  }
});

// Practical: Find first invalid field
const formFields$ = from([
  { name: 'email', valid: true },
  { name: 'password', valid: false },
  { name: 'age', valid: true }
]);

formFields$.pipe(
  find(field => !field.valid)
).subscribe(invalidField => {
  if (invalidField) {
    focusField(invalidField.name);
    showFieldError(invalidField.name);
  }
});

// findIndex for position-based logic
const items$ = from(['apple', 'banana', 'cherry', 'date']);

items$.pipe(
  findIndex(item => item.startsWith('c'))
).subscribe(index => {
  console.log('Found at position:', index); // 2
  highlightItemAtIndex(index);
});

// Practical: Find overdue task
const tasks$ = ajax.getJSON('/api/tasks').pipe(
  mergeMap(response => from(response.tasks))
);

tasks$.pipe(
  find(task => {
    const dueDate = new Date(task.dueDate);
    return dueDate < new Date() && task.status !== 'completed';
  })
).subscribe(overdueTask => {
  if (overdueTask) {
    showOverdueNotification(overdueTask);
  }
});

// Practical: Find matching route
const routes$ = from([
  { path: '/home', component: 'Home' },
  { path: '/about', component: 'About' },
  { path: '/contact', component: 'Contact' }
]);

routes$.pipe(
  find(route => route.path === currentPath)
).subscribe(route => {
  if (route) {
    loadComponent(route.component);
  } else {
    show404Page();
  }
});

// Practical: Find threshold breach
const sensorReadings$ = interval(1000).pipe(
  map(() => Math.random() * 100),
  take(100)
);

sensorReadings$.pipe(
  find(reading => reading > 95)
).subscribe(highReading => {
  if (highReading !== undefined) {
    console.warn('Threshold breached:', highReading);
    triggerAlert();
  }
});

// Practical: Find first cached item
const cacheKeys$ = from(['key1', 'key2', 'key3', 'key4']);

cacheKeys$.pipe(
  find(key => cache.has(key))
).subscribe(cachedKey => {
  if (cachedKey) {
    const data = cache.get(cachedKey);
    useCachedData(data);
  } else {
    fetchFreshData();
  }
});

// Combined with other operators
ajax.getJSON('/api/items').pipe(
  mergeMap(response => from(response.items)),
  find(item => item.featured === true)
).subscribe(featuredItem => {
  if (featuredItem) {
    displayFeaturedItem(featuredItem);
  }
});

11.4 isEmpty for Empty Stream Detection

Feature Syntax Description Result
isEmpty isEmpty() Emits true if source completes without emitting, false otherwise Single boolean emission
Timing Waits for source to complete before emitting Must complete to know if empty Deferred evaluation

Example: isEmpty for empty stream detection

import { EMPTY, of, from } from 'rxjs';
import { isEmpty, filter } from 'rxjs/operators';

// Basic isEmpty - empty stream
EMPTY.pipe(
  isEmpty()
).subscribe(result => console.log('Is empty?', result)); // true

// Non-empty stream
of(1, 2, 3).pipe(
  isEmpty()
).subscribe(result => console.log('Is empty?', result)); // false

// Practical: Check if search has results
ajax.getJSON(`/api/search?q=${query}`).pipe(
  mergeMap(response => from(response.results)),
  isEmpty()
).subscribe(noResults => {
  if (noResults) {
    showNoResultsMessage();
  } else {
    showResultsFound();
  }
});

// Practical: Validate shopping cart
const cartItems$ = from(shoppingCart.items);

cartItems$.pipe(
  isEmpty()
).subscribe(cartEmpty => {
  if (cartEmpty) {
    showEmptyCartMessage();
    disableCheckoutButton();
  } else {
    enableCheckoutButton();
  }
});

// Practical: Check for unread messages
ajax.getJSON('/api/messages/unread').pipe(
  mergeMap(response => from(response.messages)),
  isEmpty()
).subscribe(noUnread => {
  if (noUnread) {
    hideBadge();
  } else {
    showNotificationBadge();
  }
});

// Practical: Validate filtered results
const data$ = ajax.getJSON('/api/data');

data$.pipe(
  mergeMap(response => from(response.items)),
  filter(item => item.active && item.inStock),
  isEmpty()
).subscribe(noneAvailable => {
  if (noneAvailable) {
    showOutOfStockMessage();
  } else {
    displayAvailableItems();
  }
});

// Practical: Check pending tasks
ajax.getJSON('/api/tasks/pending').pipe(
  mergeMap(response => from(response.tasks)),
  isEmpty()
).subscribe(noPending => {
  if (noPending) {
    showAllCaughtUpMessage();
    hideTaskList();
  } else {
    showTaskList();
  }
});

// Practical: Validate form errors
const validationErrors$ = validateForm(formData);

validationErrors$.pipe(
  isEmpty()
).subscribe(noErrors => {
  if (noErrors) {
    submitForm();
  } else {
    showValidationSummary();
  }
});

// Practical: Check notification queue
const notifications$ = from(notificationQueue);

notifications$.pipe(
  filter(n => !n.dismissed),
  isEmpty()
).subscribe(allDismissed => {
  if (allDismissed) {
    hideNotificationPanel();
  }
});

// Practical: Detect empty file
const fileContent$ = readFileAsObservable(file);

fileContent$.pipe(
  isEmpty()
).subscribe(fileEmpty => {
  if (fileEmpty) {
    showError('File is empty');
  } else {
    processFile();
  }
});

// Practical: Check cached data
const cachedItems$ = from(Array.from(cache.values()));

cachedItems$.pipe(
  filter(item => !isExpired(item)),
  isEmpty()
).subscribe(cacheEmpty => {
  if (cacheEmpty) {
    fetchFreshData();
  } else {
    useCachedData();
  }
});

// Practical: Verify authentication tokens
const validTokens$ = from(authTokens).pipe(
  filter(token => !isExpired(token))
);

validTokens$.pipe(
  isEmpty()
).subscribe(noValidTokens => {
  if (noValidTokens) {
    redirectToLogin();
  } else {
    proceedToApp();
  }
});
Note: isEmpty must wait for source to complete. Use with finite streams or apply take/takeUntil to prevent infinite waiting.

11.5 defaultIfEmpty for Fallback Value Provision

Feature Syntax Description Behavior
defaultIfEmpty defaultIfEmpty(defaultValue) Emits default value if source completes without emitting Fallback for empty streams
Pass-through If source emits, default is ignored Only activates on empty completion Conditional fallback

Example: defaultIfEmpty for fallback values

import { EMPTY, of, from } from 'rxjs';
import { defaultIfEmpty, filter } from 'rxjs/operators';

// Basic defaultIfEmpty - provides fallback
EMPTY.pipe(
  defaultIfEmpty('No data')
).subscribe(val => console.log(val)); // 'No data'

// Non-empty stream - default ignored
of(1, 2, 3).pipe(
  defaultIfEmpty(999)
).subscribe(val => console.log(val)); // 1, 2, 3

// Practical: Search with fallback
ajax.getJSON(`/api/search?q=${query}`).pipe(
  mergeMap(response => from(response.results)),
  defaultIfEmpty({ message: 'No results found' })
).subscribe(result => displayResult(result));

// Practical: User preferences with defaults
ajax.getJSON('/api/user/preferences').pipe(
  mergeMap(response => from(response.preferences)),
  filter(pref => pref.enabled),
  defaultIfEmpty({
    theme: 'light',
    language: 'en',
    notifications: true
  })
).subscribe(preferences => applyPreferences(preferences));

// Practical: Shopping cart with default message
const cartItems$ = from(cart.items);

cartItems$.pipe(
  defaultIfEmpty({ type: 'empty-cart', message: 'Your cart is empty' })
).subscribe(item => {
  if (item.type === 'empty-cart') {
    showEmptyCartUI(item.message);
  } else {
    displayCartItem(item);
  }
});

// Practical: Configuration with fallbacks
ajax.getJSON('/api/config').pipe(
  map(config => config.feature),
  filter(feature => feature.enabled),
  defaultIfEmpty({
    enabled: false,
    settings: defaultSettings
  })
).subscribe(featureConfig => initializeFeature(featureConfig));

// Practical: Filtered list with placeholder
const activeUsers$ = ajax.getJSON('/api/users').pipe(
  mergeMap(response => from(response.users)),
  filter(user => user.active)
);

activeUsers$.pipe(
  defaultIfEmpty({ 
    id: null, 
    name: 'No active users',
    placeholder: true 
  })
).subscribe(user => {
  if (user.placeholder) {
    showPlaceholder(user.name);
  } else {
    displayUser(user);
  }
});

// Practical: Latest notification with default
const notifications$ = ajax.getJSON('/api/notifications').pipe(
  mergeMap(response => from(response.notifications)),
  filter(n => !n.read),
  take(1)
);

notifications$.pipe(
  defaultIfEmpty({
    title: 'All caught up!',
    message: 'No new notifications',
    type: 'placeholder'
  })
).subscribe(notification => displayNotification(notification));

// Practical: Validation errors with success message
const errors$ = validateForm(formData);

errors$.pipe(
  defaultIfEmpty({ 
    type: 'success', 
    message: 'Form is valid' 
  })
).subscribe(result => {
  if (result.type === 'success') {
    showSuccess(result.message);
    submitForm();
  } else {
    showError(result);
  }
});

// Practical: Product recommendations
ajax.getJSON('/api/recommendations').pipe(
  mergeMap(response => from(response.products)),
  filter(product => product.inStock),
  defaultIfEmpty({
    id: null,
    name: 'Check back later for recommendations',
    placeholder: true
  })
).subscribe(product => displayProduct(product));

// Practical: Chat messages with welcome
const messages$ = ajax.getJSON(`/api/chat/${roomId}`).pipe(
  mergeMap(response => from(response.messages))
);

messages$.pipe(
  defaultIfEmpty({
    author: 'System',
    text: 'No messages yet. Start the conversation!',
    timestamp: Date.now()
  })
).subscribe(message => displayMessage(message));

// Practical: Dashboard widgets with default
const widgets$ = from(userDashboard.widgets).pipe(
  filter(w => w.visible)
);

widgets$.pipe(
  defaultIfEmpty({
    type: 'welcome',
    content: 'Add widgets to customize your dashboard'
  })
).subscribe(widget => renderWidget(widget));

// Multiple defaults example
const data$ = ajax.getJSON('/api/data').pipe(
  mergeMap(response => from(response.items)),
  filter(item => item.priority === 'high'),
  defaultIfEmpty(null)
);

data$.subscribe(item => {
  if (item === null) {
    console.log('No high priority items');
  } else {
    processItem(item);
  }
});

11.6 sequenceEqual for Stream Comparison

Feature Syntax Description Comparison
sequenceEqual sequenceEqual(compareTo$, comparator?) Compares two observables emit same values in same order Element-wise equality
Custom comparator sequenceEqual(other$, (a, b) => a.id === b.id) Define custom equality logic Flexible comparison
Result Emits single boolean then completes true if sequences equal, false otherwise Complete comparison needed

Example: sequenceEqual for stream comparison

import { of, from } from 'rxjs';
import { sequenceEqual, delay } from 'rxjs/operators';

// Basic sequenceEqual - identical sequences
const a$ = of(1, 2, 3);
const b$ = of(1, 2, 3);

a$.pipe(
  sequenceEqual(b$)
).subscribe(equal => console.log('Equal?', equal)); // true

// Different sequences
const c$ = of(1, 2, 3);
const d$ = of(1, 2, 4);

c$.pipe(
  sequenceEqual(d$)
).subscribe(equal => console.log('Equal?', equal)); // false

// Different lengths
const e$ = of(1, 2, 3);
const f$ = of(1, 2);

e$.pipe(
  sequenceEqual(f$)
).subscribe(equal => console.log('Equal?', equal)); // false

// Timing doesn't matter
const slow$ = of(1, 2, 3).pipe(delay(1000));
const fast$ = of(1, 2, 3);

slow$.pipe(
  sequenceEqual(fast$)
).subscribe(equal => console.log('Equal?', equal)); // true

// Custom comparator
const users1$ = of(
  { id: 1, name: 'Alice' },
  { id: 2, name: 'Bob' }
);

const users2$ = of(
  { id: 1, name: 'Alice Updated' },
  { id: 2, name: 'Bob Updated' }
);

users1$.pipe(
  sequenceEqual(users2$, (a, b) => a.id === b.id)
).subscribe(equal => console.log('IDs equal?', equal)); // true

// Practical: Verify data integrity
const original$ = ajax.getJSON('/api/data/original');
const backup$ = ajax.getJSON('/api/data/backup');

original$.pipe(
  mergeMap(r => from(r.items)),
  sequenceEqual(
    backup$.pipe(mergeMap(r => from(r.items)))
  )
).subscribe(dataIntact => {
  if (dataIntact) {
    console.log('Backup verified');
  } else {
    console.error('Data mismatch detected');
    triggerBackupAlert();
  }
});

// Practical: Compare form states
const initialFormState$ = of(initialFormData);
const currentFormState$ = of(getCurrentFormData());

initialFormState$.pipe(
  sequenceEqual(currentFormState$, deepEqual)
).subscribe(unchanged => {
  if (unchanged) {
    disableSaveButton();
  } else {
    enableSaveButton();
  }
});

// Practical: Validate sequential processing
const expectedOrder$ = from(['step1', 'step2', 'step3']);
const actualOrder$ = processSteps();

expectedOrder$.pipe(
  sequenceEqual(actualOrder$)
).subscribe(correctOrder => {
  if (correctOrder) {
    console.log('Steps executed in correct order');
    markProcessComplete();
  } else {
    console.error('Step order violation');
    abortProcess();
  }
});

// Practical: Test API response consistency
const firstCall$ = ajax.getJSON('/api/data');
const secondCall$ = ajax.getJSON('/api/data');

firstCall$.pipe(
  mergeMap(r => from(r.items)),
  sequenceEqual(
    secondCall$.pipe(mergeMap(r => from(r.items)))
  )
).subscribe(consistent => {
  if (!consistent) {
    console.warn('API returned inconsistent data');
  }
});

// Practical: Verify replay accuracy
const live$ = dataStream$;
const replayed$ = replayStream$;

live$.pipe(
  sequenceEqual(replayed$)
).subscribe(accurate => {
  if (accurate) {
    console.log('Replay accurate');
  } else {
    console.error('Replay discrepancy');
  }
});

// Practical: Compare user action sequences
const expectedActions$ = from(['login', 'navigate', 'edit', 'save']);
const actualActions$ = from(recordedUserActions);

expectedActions$.pipe(
  sequenceEqual(actualActions$)
).subscribe(matchesPattern => {
  if (matchesPattern) {
    console.log('User followed expected workflow');
  } else {
    console.log('Workflow deviation detected');
  }
});

// Practical: Configuration comparison
const defaultConfig$ = of(defaultConfiguration);
const userConfig$ = of(userConfiguration);

defaultConfig$.pipe(
  sequenceEqual(userConfig$, (a, b) => JSON.stringify(a) === JSON.stringify(b))
).subscribe(isDefault => {
  if (isDefault) {
    hideResetButton();
  } else {
    showResetButton();
  }
});

// Practical: Cache validation
const serverData$ = ajax.getJSON('/api/data').pipe(
  mergeMap(r => from(r.items))
);
const cachedData$ = from(cache.getAll());

serverData$.pipe(
  sequenceEqual(cachedData$)
).subscribe(cacheValid => {
  if (cacheValid) {
    useCachedData();
  } else {
    invalidateCache();
    fetchFreshData();
  }
});

// Practical: A/B test variant verification
const variantA$ = from(variantAResults);
const variantB$ = from(variantBResults);

variantA$.pipe(
  sequenceEqual(variantB$)
).subscribe(identical => {
  if (identical) {
    console.log('Variants produce identical results');
  } else {
    console.log('Variants differ - analyze results');
  }
});
Note: sequenceEqual requires both observables to complete. Compares values in order - timing differences ignored.

Section 11 Summary

  • iif selects observable at subscription time based on condition - lazy conditional branching
  • every returns true if all values satisfy predicate, false on first failure - short-circuits
  • find/findIndex emit first matching value/index then complete - efficient searching
  • isEmpty emits true if source completes without values - requires completion
  • defaultIfEmpty provides fallback value for empty streams - graceful empty handling
  • sequenceEqual compares two observables element-wise - supports custom comparators

12. Time-based Operators and Temporal Logic

12.1 debounceTime for Input Stabilization

Feature Syntax Description Timing
debounceTime debounceTime(dueTime, scheduler?) Emits value only after dueTime silence period (no new emissions) Trailing edge - emits after quiet period
Reset on emission Each new emission resets the timer Only emits when source stops emitting Waits for stabilization
Use case Search input, resize events, auto-save Wait for user to stop typing/acting Reduce API calls

Example: debounceTime for input stabilization

import { fromEvent, interval } from 'rxjs';
import { debounceTime, map, distinctUntilChanged } from 'rxjs/operators';

// Basic debounceTime - wait for quiet period
const input$ = fromEvent(searchInput, 'input');

input$.pipe(
  map(e => e.target.value),
  debounceTime(300), // Wait 300ms after typing stops
  distinctUntilChanged()
).subscribe(searchTerm => {
  console.log('Search for:', searchTerm);
  performSearch(searchTerm);
});

// Rapid emissions - only last emitted
interval(100).pipe(
  take(10),
  debounceTime(500)
).subscribe(val => console.log('Debounced:', val));
// Only emits value 9 (after 500ms silence)

// Practical: Type-ahead search
fromEvent(searchBox, 'input').pipe(
  map(e => e.target.value),
  debounceTime(400), // Wait 400ms after user stops typing
  distinctUntilChanged(),
  filter(term => term.length >= 2),
  switchMap(term => ajax.getJSON(`/api/search?q=${term}`))
).subscribe(results => {
  displaySearchResults(results);
  // Reduces API calls dramatically
});

// Practical: Auto-save form
const formChanges$ = merge(
  fromEvent(input1, 'input'),
  fromEvent(input2, 'input'),
  fromEvent(textarea, 'input')
);

formChanges$.pipe(
  debounceTime(2000), // Save 2s after user stops editing
  map(() => getFormData()),
  distinctUntilChanged((prev, curr) => 
    JSON.stringify(prev) === JSON.stringify(curr)
  ),
  switchMap(data => ajax.post('/api/save', data))
).subscribe({
  next: () => showSavedIndicator(),
  error: err => showSaveError(err)
});

// Practical: Window resize handling
fromEvent(window, 'resize').pipe(
  debounceTime(250), // Wait 250ms after resize stops
  map(() => ({
    width: window.innerWidth,
    height: window.innerHeight
  }))
).subscribe(dimensions => {
  recalculateLayout(dimensions);
  repositionElements(dimensions);
  // Prevents excessive layout calculations
});

// Practical: Scroll end detection
fromEvent(window, 'scroll').pipe(
  debounceTime(150), // User stopped scrolling
  map(() => window.scrollY)
).subscribe(scrollPosition => {
  saveScrollPosition(scrollPosition);
  checkIfAtBottom(scrollPosition);
});

// Practical: Live validation with delay
fromEvent(emailInput, 'input').pipe(
  map(e => e.target.value),
  debounceTime(600),
  distinctUntilChanged(),
  switchMap(email => validateEmail(email))
).subscribe(validation => {
  if (validation.valid) {
    showValidIcon();
  } else {
    showInvalidIcon(validation.errors);
  }
});

// Practical: Infinite scroll trigger
fromEvent(window, 'scroll').pipe(
  debounceTime(100),
  filter(() => isNearBottom()),
  switchMap(() => loadMoreItems())
).subscribe(items => appendItems(items));

// Practical: Text editor auto-complete
fromEvent(editor, 'input').pipe(
  map(e => ({
    text: e.target.value,
    cursorPosition: e.target.selectionStart
  })),
  debounceTime(300),
  filter(state => shouldShowAutoComplete(state)),
  switchMap(state => getSuggestions(state.text))
).subscribe(suggestions => displayAutoComplete(suggestions));

// Practical: Real-time collaboration presence
const userActivity$ = merge(
  fromEvent(document, 'mousemove'),
  fromEvent(document, 'keypress'),
  fromEvent(document, 'click')
);

userActivity$.pipe(
  debounceTime(5000), // 5s of inactivity
  mapTo('idle')
).subscribe(status => {
  updatePresenceStatus(status);
  sendStatusToServer(status);
});

// Practical: Filter panel updates
const filterChanges$ = merge(
  fromEvent(priceRange, 'input'),
  fromEvent(categorySelect, 'change'),
  fromEvent(ratingFilter, 'change')
);

filterChanges$.pipe(
  debounceTime(500),
  map(() => getFilterValues()),
  distinctUntilChanged((a, b) => deepEqual(a, b)),
  switchMap(filters => ajax.post('/api/filter', filters))
).subscribe(results => updateProductList(results));

// Practical: Chat typing indicator
fromEvent(chatInput, 'input').pipe(
  debounceTime(1000),
  mapTo(false) // Stopped typing
).subscribe(isTyping => {
  sendTypingStatus(isTyping);
});

// With immediate emission on first input
fromEvent(chatInput, 'input').pipe(
  tap(() => sendTypingStatus(true)), // Immediate
  debounceTime(1000),
  tap(() => sendTypingStatus(false)) // After stop
).subscribe();

// Comparison: debounce vs throttle
// debounceTime: Waits for silence, emits last
fromEvent(button, 'click').pipe(
  debounceTime(1000)
).subscribe(() => console.log('Debounced click'));

// throttleTime: Emits first, ignores until period ends
fromEvent(button, 'click').pipe(
  throttleTime(1000)
).subscribe(() => console.log('Throttled click'));
Note: debounceTime emits after silence period - perfect for waiting until user finishes action. Drastically reduces API calls for search/autocomplete.

12.2 throttleTime for Rate Limiting

Feature Syntax Description Timing
throttleTime throttleTime(duration, scheduler?, config?) Emits first value, then ignores for duration Leading edge by default
Configuration { leading: true, trailing: false } Control leading/trailing edge behavior Flexible timing control
Use case Button clicks, scroll events, mouse moves Limit event frequency Performance optimization

Example: throttleTime for rate limiting

import { fromEvent, interval } from 'rxjs';
import { throttleTime, map, scan } from 'rxjs/operators';

// Basic throttleTime - emit first, ignore rest
fromEvent(button, 'click').pipe(
  throttleTime(1000)
).subscribe(() => console.log('Throttled click'));
// First click processed, others ignored for 1s

// Rapid emissions throttled
interval(100).pipe(
  take(20),
  throttleTime(1000)
).subscribe(val => console.log('Throttled:', val));
// Emits: 0, 10 (approx)

// Trailing edge configuration
fromEvent(button, 'click').pipe(
  throttleTime(1000, asyncScheduler, { 
    leading: false, 
    trailing: true 
  })
).subscribe(() => console.log('Trailing throttle'));

// Practical: Scroll event throttling
fromEvent(window, 'scroll').pipe(
  throttleTime(200), // Max 5 times per second
  map(() => window.scrollY)
).subscribe(scrollY => {
  updateScrollProgress(scrollY);
  toggleBackToTop(scrollY);
  // Prevents excessive calculations
});

// Practical: Button click protection
fromEvent(submitButton, 'click').pipe(
  throttleTime(2000) // Prevent double-click
).subscribe(() => {
  console.log('Processing submission...');
  submitForm();
});

// Practical: Mouse move tracking (performance)
fromEvent(canvas, 'mousemove').pipe(
  throttleTime(50), // ~20 updates per second
  map(e => ({ x: e.clientX, y: e.clientY }))
).subscribe(position => {
  updateCursor(position);
  drawAtPosition(position);
});

// Practical: Window resize (immediate response)
fromEvent(window, 'resize').pipe(
  throttleTime(100) // Leading edge - immediate first response
).subscribe(() => {
  updateResponsiveLayout();
  // First resize handled immediately, then throttled
});

// Practical: API rate limiting
const apiCalls$ = new Subject<Request>();

apiCalls$.pipe(
  throttleTime(1000), // Max 1 call per second
  switchMap(request => ajax(request))
).subscribe(response => handleResponse(response));

// Queue multiple calls
apiCalls$.next(request1);
apiCalls$.next(request2); // Ignored
apiCalls$.next(request3); // Ignored
// Only request1 processed

// Practical: Save button rate limiting
fromEvent(saveBtn, 'click').pipe(
  throttleTime(3000),
  switchMap(() => saveData())
).subscribe({
  next: () => showSaveSuccess(),
  error: err => showSaveError(err)
});

// Practical: Infinite scroll throttling
fromEvent(window, 'scroll').pipe(
  throttleTime(300),
  filter(() => isNearBottom()),
  exhaustMap(() => loadNextPage())
).subscribe(items => appendItems(items));

// Practical: Keyboard event throttling
fromEvent(document, 'keydown').pipe(
  throttleTime(100),
  map(e => e.key)
).subscribe(key => {
  handleKeyPress(key);
  // Prevents key repeat spam
});

// Practical: Analytics event throttling
const trackEvent = (category: string, action: string) => {
  return of({ category, action }).pipe(
    throttleTime(5000) // Max once per 5 seconds
  ).subscribe(event => {
    analytics.track(event.category, event.action);
  });
};

// Practical: Drag and drop throttling
fromEvent(element, 'drag').pipe(
  throttleTime(16), // ~60fps
  map(e => ({ x: e.clientX, y: e.clientY }))
).subscribe(position => {
  updateDragPreview(position);
});

// Both leading and trailing
fromEvent(input, 'input').pipe(
  throttleTime(1000, asyncScheduler, {
    leading: true,  // Immediate first
    trailing: true  // Last after period
  }),
  map(e => e.target.value)
).subscribe(value => processValue(value));

// Practical: Network request throttling
const requests$ = new Subject<string>();

requests$.pipe(
  throttleTime(200),
  switchMap(url => ajax.getJSON(url))
).subscribe(data => updateUI(data));

// Practical: Game input throttling
fromEvent(document, 'keypress').pipe(
  filter(e => e.key === ' '), // Spacebar
  throttleTime(500) // Limit firing rate
).subscribe(() => {
  fireWeapon();
});

// Practical: Audio visualization throttling
audioData$.pipe(
  throttleTime(33) // ~30fps for visualization
).subscribe(data => {
  updateVisualization(data);
});
Note: throttleTime emits immediately (leading edge), then ignores emissions for duration. Use for performance optimization of high-frequency events.

12.3 auditTime for Trailing Edge Sampling

Feature Syntax Description Edge
auditTime auditTime(duration, scheduler?) Ignores values for duration, then emits most recent Trailing edge
vs throttleTime audit = trailing, throttle = leading Opposite timing behavior Different use cases
Use case Final state after activity, settling values Capture result after action completes Post-action processing

Example: auditTime for trailing edge sampling

import { fromEvent, interval } from 'rxjs';
import { auditTime, map } from 'rxjs/operators';

// Basic auditTime - emit last value after period
fromEvent(button, 'click').pipe(
  auditTime(1000)
).subscribe(() => console.log('Audit: last click after 1s'));

// Rapid emissions - emits last
interval(100).pipe(
  take(15),
  auditTime(1000)
).subscribe(val => console.log('Audited:', val));
// Emits values at trailing edge

// Practical: Capture final scroll position
fromEvent(window, 'scroll').pipe(
  auditTime(200),
  map(() => window.scrollY)
).subscribe(finalScrollY => {
  saveScrollPosition(finalScrollY);
  updateSectionHighlight(finalScrollY);
  // Uses final position after scroll settles
});

// Practical: Final resize dimensions
fromEvent(window, 'resize').pipe(
  auditTime(300),
  map(() => ({
    width: window.innerWidth,
    height: window.innerHeight
  }))
).subscribe(finalDimensions => {
  recalculateLayout(finalDimensions);
  // Uses final window size
});

// Practical: Input field final value
fromEvent(input, 'input').pipe(
  map(e => e.target.value),
  auditTime(500)
).subscribe(finalValue => {
  console.log('Final value:', finalValue);
  performValidation(finalValue);
});

// Practical: Mouse position after movement stops
fromEvent(canvas, 'mousemove').pipe(
  auditTime(200),
  map(e => ({ x: e.clientX, y: e.clientY }))
).subscribe(finalPosition => {
  showContextMenuAt(finalPosition);
  // Shows menu at final mouse position
});

// Practical: Slider final value
fromEvent(slider, 'input').pipe(
  map(e => e.target.value),
  auditTime(300)
).subscribe(finalValue => {
  applyFilterWithValue(finalValue);
  // Applies filter after user stops adjusting
});

// Practical: Final drag position
fromEvent(draggable, 'drag').pipe(
  auditTime(100),
  map(e => ({ x: e.clientX, y: e.clientY }))
).subscribe(finalPosition => {
  snapToGrid(finalPosition);
  savePosition(finalPosition);
});

// Compare timing operators
const clicks$ = fromEvent(button, 'click');

// throttleTime: First click immediately
clicks$.pipe(throttleTime(1000))
  .subscribe(() => console.log('Throttle: first'));

// auditTime: Last click after period
clicks$.pipe(auditTime(1000))
  .subscribe(() => console.log('Audit: last'));

// debounceTime: After clicking stops
clicks$.pipe(debounceTime(1000))
  .subscribe(() => console.log('Debounce: after stop'));

// Practical: Color picker final selection
fromEvent(colorPicker, 'input').pipe(
  map(e => e.target.value),
  auditTime(400)
).subscribe(finalColor => {
  applyColor(finalColor);
  addToRecentColors(finalColor);
});

// Practical: Volume slider final value
fromEvent(volumeSlider, 'input').pipe(
  map(e => e.target.value),
  auditTime(200)
).subscribe(finalVolume => {
  setVolume(finalVolume);
  saveVolumePreference(finalVolume);
});

// Practical: Zoom level final value
fromEvent(canvas, 'wheel').pipe(
  map(e => e.deltaY),
  scan((zoom, delta) => {
    const newZoom = zoom + (delta > 0 ? -0.1 : 0.1);
    return Math.max(0.1, Math.min(5, newZoom));
  }, 1),
  auditTime(150)
).subscribe(finalZoom => {
  applyZoom(finalZoom);
  renderAtZoomLevel(finalZoom);
});

12.4 sampleTime for Periodic Value Sampling

Feature Syntax Description Sampling
sampleTime sampleTime(period, scheduler?) Emits most recent value at periodic intervals Fixed time intervals
Behavior Only emits if source emitted since last sample Silent periods produce no emissions Conditional periodic
Use case Periodic snapshots, data sampling, UI updates Regular polling of changing values Time-based reduction

Example: sampleTime for periodic sampling

import { fromEvent, interval } from 'rxjs';
import { sampleTime, map, scan } from 'rxjs/operators';

// Basic sampleTime - periodic sampling
interval(100).pipe(
  sampleTime(1000)
).subscribe(val => console.log('Sampled:', val));
// Samples every 1 second

// Practical: Mouse position sampling
fromEvent(document, 'mousemove').pipe(
  map(e => ({ x: e.clientX, y: e.clientY })),
  sampleTime(500) // Sample every 500ms
).subscribe(position => {
  logMousePosition(position);
  // Reduces logging from potentially 1000s to 2/second
});

// Practical: Real-time metrics sampling
const cpuUsage$ = interval(50).pipe(
  map(() => getCPUUsage())
);

cpuUsage$.pipe(
  sampleTime(5000) // Sample every 5 seconds
).subscribe(usage => {
  displayMetric('CPU', usage);
  sendToMonitoring(usage);
});

// Practical: Sensor data sampling
const temperatureSensor$ = interval(100).pipe(
  map(() => readTemperature())
);

temperatureSensor$.pipe(
  sampleTime(10000) // Sample every 10 seconds
).subscribe(temp => {
  logTemperature(temp);
  checkThresholds(temp);
});

// Practical: Network bandwidth monitoring
const bytesTransferred$ = new Subject<number>();

bytesTransferred$.pipe(
  scan((total, bytes) => total + bytes, 0),
  sampleTime(1000) // Sample every second
).subscribe(totalBytes => {
  const bandwidth = calculateBandwidth(totalBytes);
  updateBandwidthDisplay(bandwidth);
});

// Practical: Stock price ticker
const priceUpdates$ = websocket$.pipe(
  filter(msg => msg.type === 'price'),
  map(msg => msg.price)
);

priceUpdates$.pipe(
  sampleTime(1000) // Update display every second
).subscribe(price => {
  updatePriceTicker(price);
  // Prevents excessive UI updates
});

// Practical: Audio level meter
const audioLevel$ = new Subject<number>();

audioLevel$.pipe(
  sampleTime(100) // Sample 10 times per second
).subscribe(level => {
  updateAudioMeter(level);
  checkForClipping(level);
});

// Practical: GPS location sampling
const gpsUpdates$ = new Subject<Location>();

gpsUpdates$.pipe(
  sampleTime(30000) // Sample every 30 seconds
).subscribe(location => {
  saveLocationHistory(location);
  updateMapPosition(location);
});

// Practical: Form field activity monitoring
const formActivity$ = merge(
  fromEvent(input1, 'input'),
  fromEvent(input2, 'input'),
  fromEvent(select1, 'change')
).pipe(
  mapTo(Date.now())
);

formActivity$.pipe(
  sampleTime(60000) // Check every minute
).subscribe(lastActivity => {
  updateSessionTimeout(lastActivity);
});

// Practical: Chart data point collection
const dataStream$ = new Subject<number>();

dataStream$.pipe(
  sampleTime(2000) // Add point every 2 seconds
).subscribe(value => {
  addDataPointToChart(value);
  // Prevents chart from becoming too dense
});

// Practical: Scroll progress indicator
fromEvent(window, 'scroll').pipe(
  map(() => {
    const scrolled = window.scrollY;
    const height = document.documentElement.scrollHeight - window.innerHeight;
    return (scrolled / height) * 100;
  }),
  sampleTime(200)
).subscribe(progress => {
  updateProgressBar(progress);
});

// Practical: Performance monitoring
const performanceMetrics$ = interval(100).pipe(
  map(() => ({
    fps: measureFPS(),
    memory: performance.memory?.usedJSHeapSize,
    timestamp: Date.now()
  }))
);

performanceMetrics$.pipe(
  sampleTime(5000)
).subscribe(metrics => {
  logPerformanceMetrics(metrics);
});

12.5 timeInterval for Emission Time Measurement

Feature Syntax Description Output
timeInterval timeInterval(scheduler?) Wraps each emission with time elapsed since previous emission { value, interval }
Interval measurement Time in milliseconds between emissions Measures spacing between values Temporal analytics
Use case Performance monitoring, rate detection, timing analysis Understanding emission patterns Debugging and metrics

Example: timeInterval for timing measurement

import { fromEvent, interval } from 'rxjs';
import { timeInterval, map, tap, take } from 'rxjs/operators';

// Basic timeInterval - measure spacing
interval(1000).pipe(
  take(5),
  timeInterval()
).subscribe(({ value, interval }) => {
  console.log(`Value: ${value}, Interval: ${interval}ms`);
});
// Output shows ~1000ms intervals

// Practical: Click rate detection
fromEvent(button, 'click').pipe(
  timeInterval(),
  tap(({ interval }) => {
    if (interval < 200) {
      console.log('Rapid clicking detected!');
    }
  })
).subscribe(({ value, interval }) => {
  console.log(`Clicked (${interval}ms since last)`);
});

// Practical: API response time monitoring
ajax.getJSON('/api/data').pipe(
  timeInterval()
).subscribe(({ value, interval }) => {
  console.log(`Response time: ${interval}ms`);
  metrics.recordResponseTime(interval);
  
  if (interval > 2000) {
    console.warn('Slow API response');
  }
});

// Practical: User typing speed analysis
fromEvent(input, 'input').pipe(
  timeInterval(),
  map(({ interval }) => interval),
  scan((acc, interval) => {
    acc.intervals.push(interval);
    acc.average = acc.intervals.reduce((a, b) => a + b) / acc.intervals.length;
    return acc;
  }, { intervals: [], average: 0 })
).subscribe(stats => {
  console.log(`Avg typing interval: ${stats.average.toFixed(0)}ms`);
  displayTypingSpeed(stats);
});

// Practical: Network event spacing
const networkEvents$ = merge(
  fromEvent(connection, 'message').pipe(mapTo('message')),
  fromEvent(connection, 'error').pipe(mapTo('error')),
  fromEvent(connection, 'close').pipe(mapTo('close'))
);

networkEvents$.pipe(
  timeInterval()
).subscribe(({ value, interval }) => {
  console.log(`${value} after ${interval}ms`);
  
  if (value === 'error' && interval < 1000) {
    console.error('Rapid errors - connection unstable');
  }
});

// Practical: Frame rate measurement
const frames$ = interval(0, animationFrameScheduler).pipe(
  take(120)
);

frames$.pipe(
  timeInterval(),
  scan((acc, { interval }) => {
    acc.frameTime = interval;
    acc.fps = 1000 / interval;
    return acc;
  }, { frameTime: 0, fps: 0 })
).subscribe(stats => {
  displayFPS(stats.fps);
  
  if (stats.fps < 30) {
    console.warn('Low frame rate detected');
  }
});

// Practical: Message rate limiting detection
const messages$ = new Subject<Message>();

messages$.pipe(
  timeInterval(),
  filter(({ interval }) => interval < 100),
  scan((count) => count + 1, 0)
).subscribe(rapidCount => {
  if (rapidCount > 10) {
    console.warn('Message spam detected');
    throttleMessages();
  }
});

// Practical: Keyboard event analysis
fromEvent(document, 'keydown').pipe(
  timeInterval(),
  map(({ value, interval }) => ({
    key: value.key,
    interval
  }))
).subscribe(({ key, interval }) => {
  console.log(`Key '${key}' pressed ${interval}ms after previous`);
  
  if (interval < 50) {
    console.log('Rapid key press (possible key repeat)');
  }
});

// Practical: Stream health monitoring
dataStream$.pipe(
  timeInterval(),
  tap(({ interval }) => {
    if (interval > 5000) {
      console.warn('Stream stalled - no data for 5s');
      checkStreamHealth();
    }
  })
).subscribe();

// Practical: Request pacing analysis
requestStream$.pipe(
  timeInterval(),
  scan((stats, { interval }) => {
    stats.count++;
    stats.totalTime += interval;
    stats.avgInterval = stats.totalTime / stats.count;
    stats.minInterval = Math.min(stats.minInterval, interval);
    stats.maxInterval = Math.max(stats.maxInterval, interval);
    return stats;
  }, {
    count: 0,
    totalTime: 0,
    avgInterval: 0,
    minInterval: Infinity,
    maxInterval: 0
  })
).subscribe(stats => {
  displayRequestStats(stats);
});

// Practical: Heartbeat monitoring
heartbeat$.pipe(
  timeInterval(),
  tap(({ interval }) => {
    if (interval > 35000) { // Expected every 30s
      console.error('Missed heartbeat');
      attemptReconnect();
    }
  })
).subscribe();
Note: timeInterval wraps values with timing metadata. Useful for performance monitoring, rate detection, and temporal pattern analysis.

12.6 timestamp for Emission Timestamping

Feature Syntax Description Output
timestamp timestamp(scheduler?) Wraps each emission with absolute timestamp { value, timestamp }
Timestamp format Milliseconds since Unix epoch Absolute time, not relative Date.now() equivalent
Use case Event logging, audit trails, temporal ordering Record when events occurred Historical tracking

Example: timestamp for emission timestamping

import { fromEvent, interval } from 'rxjs';
import { timestamp, map, scan, take } from 'rxjs/operators';

// Basic timestamp - add absolute timestamp
interval(1000).pipe(
  take(3),
  timestamp()
).subscribe(({ value, timestamp }) => {
  const date = new Date(timestamp);
  console.log(`Value: ${value} at ${date.toISOString()}`);
});

// Practical: Click event logging
fromEvent(button, 'click').pipe(
  timestamp(),
  map(({ value, timestamp }) => ({
    event: 'click',
    element: value.target.id,
    timestamp,
    date: new Date(timestamp).toISOString()
  }))
).subscribe(log => {
  console.log('Click log:', log);
  sendToAnalytics(log);
});

// Practical: Error logging with timestamps
errors$.pipe(
  timestamp(),
  tap(({ value, timestamp }) => {
    const errorLog = {
      error: value,
      timestamp,
      datetime: new Date(timestamp).toISOString(),
      stackTrace: value.stack
    };
    logError(errorLog);
    sendToErrorTracking(errorLog);
  })
).subscribe();

// Practical: User activity timeline
const userActions$ = merge(
  fromEvent(document, 'click').pipe(mapTo('click')),
  fromEvent(document, 'keypress').pipe(mapTo('keypress')),
  fromEvent(document, 'scroll').pipe(mapTo('scroll'))
);

userActions$.pipe(
  timestamp(),
  scan((timeline, { value, timestamp }) => {
    timeline.push({
      action: value,
      timestamp,
      time: new Date(timestamp).toLocaleTimeString()
    });
    return timeline.slice(-50); // Keep last 50 actions
  }, [])
).subscribe(timeline => {
  updateActivityTimeline(timeline);
});

// Practical: Message timestamps for chat
const messages$ = new Subject<string>();

messages$.pipe(
  timestamp(),
  map(({ value, timestamp }) => ({
    text: value,
    timestamp,
    formattedTime: new Date(timestamp).toLocaleTimeString(),
    sender: currentUser.id
  }))
).subscribe(message => {
  displayMessage(message);
  saveToHistory(message);
});

// Practical: Performance event tracking
const performanceEvents$ = merge(
  fromEvent(window, 'load').pipe(mapTo('page_load')),
  ajaxComplete$.pipe(mapTo('ajax_complete')),
  renderComplete$.pipe(mapTo('render_complete'))
);

performanceEvents$.pipe(
  timestamp(),
  scan((metrics, { value, timestamp }) => {
    metrics[value] = timestamp;
    
    if (metrics.page_load && metrics.render_complete) {
      metrics.timeToInteractive = 
        metrics.render_complete - metrics.page_load;
    }
    
    return metrics;
  }, {})
).subscribe(metrics => {
  reportPerformanceMetrics(metrics);
});

// Practical: Sensor data logging
const sensorData$ = interval(1000).pipe(
  map(() => ({
    temperature: readTemperature(),
    humidity: readHumidity(),
    pressure: readPressure()
  }))
);

sensorData$.pipe(
  timestamp(),
  map(({ value, timestamp }) => ({
    ...value,
    timestamp,
    datetime: new Date(timestamp).toISOString()
  }))
).subscribe(reading => {
  saveSensorReading(reading);
  if (reading.temperature > 30) {
    logAlert({
      type: 'high_temp',
      value: reading.temperature,
      timestamp: reading.timestamp
    });
  }
});

// Practical: API request/response logging
ajax.getJSON('/api/data').pipe(
  timestamp(),
  tap(({ timestamp }) => {
    console.log(`Request completed at: ${new Date(timestamp).toISOString()}`);
  })
).subscribe();

// Practical: Form submission audit trail
fromEvent(form, 'submit').pipe(
  timestamp(),
  map(({ value, timestamp }) => ({
    formData: new FormData(value.target),
    submittedAt: timestamp,
    submittedBy: currentUser.id,
    formattedDate: new Date(timestamp).toISOString()
  }))
).subscribe(submission => {
  auditLog.record(submission);
  processSubmission(submission);
});

// Practical: State change history
stateChanges$.pipe(
  timestamp(),
  scan((history, { value, timestamp }) => {
    history.push({
      state: value,
      timestamp,
      date: new Date(timestamp).toISOString()
    });
    return history.slice(-100); // Keep last 100 changes
  }, [])
).subscribe(history => {
  saveStateHistory(history);
  enableUndoRedo(history);
});

// Practical: WebSocket message timestamps
websocket$.pipe(
  timestamp(),
  map(({ value, timestamp }) => ({
    ...value,
    receivedAt: timestamp,
    latency: timestamp - value.sentAt
  }))
).subscribe(message => {
  console.log(`Message latency: ${message.latency}ms`);
  processMessage(message);
});

// Practical: Download progress timestamps
downloadProgress$.pipe(
  timestamp(),
  map(({ value, timestamp }) => ({
    bytesDownloaded: value,
    timestamp,
    elapsedTime: timestamp - downloadStartTime,
    speed: value / ((timestamp - downloadStartTime) / 1000) // bytes/sec
  }))
).subscribe(progress => {
  updateProgressBar(progress);
  displayDownloadSpeed(progress.speed);
});

// Compare timeInterval vs timestamp
const source$ = interval(1000).pipe(take(3));

// timeInterval: relative time between emissions
source$.pipe(timeInterval())
  .subscribe(x => console.log('Interval:', x));
// { value: 0, interval: 1000 }, { value: 1, interval: 1000 }

// timestamp: absolute time of emission
source$.pipe(timestamp())
  .subscribe(x => console.log('Timestamp:', x));
// { value: 0, timestamp: 1703347200000 }, ...
Note: timestamp adds absolute time (Date.now()), while timeInterval measures relative time between emissions.

Section 12 Summary

  • debounceTime waits for silence period before emitting - perfect for search/autocomplete
  • throttleTime emits first value immediately, ignores rest for duration - rate limiting
  • auditTime emits last value after period (trailing edge) - final state capture
  • sampleTime periodically emits most recent value - regular snapshots
  • timeInterval measures time between emissions - performance monitoring
  • timestamp adds absolute timestamp to emissions - event logging and audit trails

13. Custom Operators and Operator Development

13.1 Creating Custom Pipeable Operators

Concept Pattern Description Use Case
Pipeable Operator function<T, R>(Observable<T>): Observable<R> Function that transforms observables, chainable with pipe() Creating reusable transformation logic
Operator Factory function(...args) { return (source$) => ... } Higher-order function that returns an operator Parameterized custom operators
lift() Method DEPRECATED source.lift(operator) Legacy operator creation method Use pipeable operators instead
Observable Constructor new Observable(subscriber => ...) Manual observable creation within operator Full control over emission logic
pipe() Composition pipe(operator1, operator2, ...) Combine multiple operators into one Building complex operators from simple ones

Example: Basic custom pipeable operator

// Simple operator without parameters
function doubleValue<T extends number>() {
  return (source$: Observable<T>): Observable<T> => {
    return new Observable(subscriber => {
      return source$.subscribe({
        next: value => subscriber.next((value * 2) as T),
        error: err => subscriber.error(err),
        complete: () => subscriber.complete()
      });
    });
  };
}

// Usage
of(1, 2, 3).pipe(
  doubleValue()
).subscribe(console.log); // 2, 4, 6

Example: Parameterized custom operator

// Operator factory pattern
function multiplyBy<T extends number>(multiplier: number) {
  return (source$: Observable<T>): Observable<T> => {
    return new Observable(subscriber => {
      return source$.subscribe({
        next: value => subscriber.next((value * multiplier) as T),
        error: err => subscriber.error(err),
        complete: () => subscriber.complete()
      });
    });
  };
}

// Usage
of(1, 2, 3).pipe(
  multiplyBy(10)
).subscribe(console.log); // 10, 20, 30

// More concise using existing operators
function multiplyBy<T extends number>(multiplier: number) {
  return (source$: Observable<T>) => source$.pipe(
    map(value => (value * multiplier) as T)
  );
}

// Even more concise using direct pipe return
const multiplyBy = <T extends number>(multiplier: number) =>
  map<T, T>(value => (value * multiplier) as T);

Example: Operator with state management

// Custom operator with internal state
function rateLimit<T>(count: number, timeWindow: number) {
  return (source$: Observable<T>): Observable<T> => {
    return new Observable(subscriber => {
      const timestamps: number[] = [];
      
      return source$.subscribe({
        next: value => {
          const now = Date.now();
          // Remove timestamps outside window
          while (timestamps.length > 0 && 
                 timestamps[0] < now - timeWindow) {
            timestamps.shift();
          }
          
          if (timestamps.length < count) {
            timestamps.push(now);
            subscriber.next(value);
          } else {
            console.warn('Rate limit exceeded');
          }
        },
        error: err => subscriber.error(err),
        complete: () => subscriber.complete()
      });
    });
  };
}

// Usage: Max 5 emissions per 1000ms
clicks$.pipe(
  rateLimit(5, 1000)
).subscribe(event => processClick(event));

13.2 Operator Function Composition Patterns

Pattern Syntax Description Benefit
Pipe Composition pipe(op1, op2, op3) Chain operators sequentially Readable, maintainable operator chains
Operator Wrapping source => source.pipe(...ops) Create operator from operator chain Encapsulate complex logic
Higher-Order Composition (...args) => source => pipe(...) Parameterized operator composition Flexible, reusable operators
Conditional Operators condition ? operator1 : operator2 Dynamic operator selection Runtime operator switching
Operator Arrays pipe(...operatorArray) Dynamic operator chains from arrays Programmatic pipeline construction

Example: Composing operators into reusable functions

// Create reusable operator combinations
const debugOperator = <T>(label: string) => (source$: Observable<T>) =>
  source$.pipe(
    tap(value => console.log(`[${label}] Next:`, value)),
    tap({ error: err => console.error(`[${label}] Error:`, err) }),
    tap({ complete: () => console.log(`[${label}] Complete`) })
  );

// Compose multiple operators
const retryWithBackoff = (maxRetries: number) => <T>(source$: Observable<T>) =>
  source$.pipe(
    retryWhen(errors => errors.pipe(
      scan((retryCount, err) => {
        if (retryCount >= maxRetries) throw err;
        return retryCount + 1;
      }, 0),
      delayWhen(retryCount => timer(retryCount * 1000))
    ))
  );

// Usage
http.get('/api/data').pipe(
  debugOperator('API Call'),
  retryWithBackoff(3)
).subscribe(data => console.log(data));

Example: Dynamic operator composition

// Build operator chain dynamically
function createFilterPipeline<T>(config: {
  debounce?: number;
  distinct?: boolean;
  minLength?: number;
}) {
  const operators: any[] = [];
  
  if (config.debounce) {
    operators.push(debounceTime(config.debounce));
  }
  
  if (config.distinct) {
    operators.push(distinctUntilChanged());
  }
  
  if (config.minLength) {
    operators.push(
      filter((value: string) => value.length >= config.minLength)
    );
  }
  
  return (source$: Observable<T>) => source$.pipe(...operators);
}

// Usage
searchInput$.pipe(
  createFilterPipeline({
    debounce: 300,
    distinct: true,
    minLength: 3
  }),
  switchMap(term => searchAPI(term))
).subscribe(results => displayResults(results));

13.3 Reusable Operator Libraries

Library Type Example Organization Best Practice
Utility Operators filterNullish, tapOnce, delayedRetry Group by functionality Single responsibility per operator
Domain Operators validateEmail, parseJSON, sanitizeHTML Group by domain logic Business logic encapsulation
Integration Operators fromWebSocket, toLocalStorage, withAuth Group by integration type External system interactions
Performance Operators memoize, shareWithExpiry, bufferOptimal Group by optimization type Performance enhancement

Example: Building a custom operator library

// operators/filtering.ts
export const filterNullish = <T>() => (source$: Observable<T | null | undefined>): Observable<T> =>
  source$.pipe(filter((value): value is T => value != null));

export const filterByType = <T, K extends keyof T>(
  key: K, 
  type: string
) => filter<T>(item => typeof item[key] === type);

// operators/transformation.ts
export const parseJSON = <T>() => (source$: Observable<string>): Observable<T> =>
  source$.pipe(
    map(str => JSON.parse(str) as T),
    catchError(err => {
      console.error('JSON Parse Error:', err);
      return EMPTY;
    })
  );

export const toArray = <T>() => (source$: Observable<T>): Observable<T[]> =>
  source$.pipe(
    scan((acc: T[], value) => [...acc, value], [])
  );

// operators/timing.ts
export const debounceAfterFirst = <T>(duration: number) => 
  (source$: Observable<T>): Observable<T> => {
    return new Observable(subscriber => {
      let isFirst = true;
      
      return source$.pipe(
        mergeMap(value => 
          isFirst 
            ? (isFirst = false, of(value))
            : of(value).pipe(debounceTime(duration))
        )
      ).subscribe(subscriber);
    });
  };

// Usage
import { filterNullish, parseJSON, debounceAfterFirst } from './operators';

apiResponse$.pipe(
  filterNullish(),
  parseJSON<UserData>(),
  debounceAfterFirst(300)
).subscribe(data => console.log(data));

Example: Domain-specific operator library

// operators/http.ts
export const withRetry = (maxRetries = 3, delayMs = 1000) => <T>(source$: Observable<T>) =>
  source$.pipe(
    retry({
      count: maxRetries,
      delay: delayMs
    })
  );

export const withTimeout = (ms: number, fallback?: any) => <T>(source$: Observable<T>) =>
  source$.pipe(
    timeout(ms),
    catchError(err => 
      fallback !== undefined ? of(fallback) : throwError(() => err)
    )
  );

export const withCache = <T>(cacheTime = 60000) => (source$: Observable<T>) =>
  source$.pipe(
    shareReplay({
      bufferSize: 1,
      refCount: true,
      windowTime: cacheTime
    })
  );

// Combine operators
export const httpRequest = <T>(config?: {
  retries?: number;
  timeout?: number;
  cache?: number;
}) => (source$: Observable<T>) => {
  let pipeline = source$;
  
  if (config?.retries) {
    pipeline = pipeline.pipe(withRetry(config.retries));
  }
  if (config?.timeout) {
    pipeline = pipeline.pipe(withTimeout(config.timeout));
  }
  if (config?.cache) {
    pipeline = pipeline.pipe(withCache(config.cache));
  }
  
  return pipeline;
};

// Usage
http.get<User[]>('/api/users').pipe(
  httpRequest({ retries: 3, timeout: 5000, cache: 60000 })
).subscribe(users => console.log(users));

13.4 Higher-Order Operator Development

Type Pattern Description Example
Flattening Operators source => inner$ => result$ Transform and flatten inner observables Custom switchMap variant
Buffering Operators source => buffer$[] => emit Accumulate and emit batches Smart buffering logic
Windowing Operators source => window$ => Observable<Observable> Group emissions into observable windows Custom window logic
Joining Operators source1 + source2 => combined$ Combine multiple observables with custom logic Special combination patterns

Example: Custom flattening operator

// switchMapWithPriority: Cancel low-priority, keep high-priority
function switchMapWithPriority<T, R>(
  project: (value: T) => Observable<R>,
  getPriority: (value: T) => number
) {
  return (source$: Observable<T>): Observable<R> => {
    return new Observable(subscriber => {
      let currentPriority = -Infinity;
      let innerSubscription: Subscription | null = null;
      
      const subscription = source$.subscribe({
        next: value => {
          const priority = getPriority(value);
          
          // Only switch if new priority is higher or equal
          if (priority >= currentPriority) {
            currentPriority = priority;
            innerSubscription?.unsubscribe();
            
            innerSubscription = project(value).subscribe({
              next: result => subscriber.next(result),
              error: err => subscriber.error(err),
              complete: () => {
                currentPriority = -Infinity;
                innerSubscription = null;
              }
            });
          }
        },
        error: err => subscriber.error(err),
        complete: () => {
          if (!innerSubscription || innerSubscription.closed) {
            subscriber.complete();
          }
        }
      });
      
      return () => {
        subscription.unsubscribe();
        innerSubscription?.unsubscribe();
      };
    });
  };
}

// Usage
interface SearchRequest {
  query: string;
  priority: number; // 1=normal, 2=high, 3=critical
}

searchRequests$.pipe(
  switchMapWithPriority(
    req => searchAPI(req.query),
    req => req.priority
  )
).subscribe(results => displayResults(results));

Example: Custom buffering operator

// bufferUntilSizeOrTime: Buffer until size OR time limit
function bufferUntilSizeOrTime<T>(size: number, timeMs: number) {
  return (source$: Observable<T>): Observable<T[]> => {
    return new Observable(subscriber => {
      let buffer: T[] = [];
      let timer: any = null;
      
      const emit = () => {
        if (buffer.length > 0) {
          subscriber.next([...buffer]);
          buffer = [];
        }
        if (timer) {
          clearTimeout(timer);
          timer = null;
        }
      };
      
      const resetTimer = () => {
        if (timer) clearTimeout(timer);
        timer = setTimeout(emit, timeMs);
      };
      
      const subscription = source$.subscribe({
        next: value => {
          buffer.push(value);
          
          if (buffer.length === 1) {
            resetTimer(); // Start timer on first item
          }
          
          if (buffer.length >= size) {
            emit(); // Emit when size reached
          }
        },
        error: err => {
          emit(); // Emit remaining buffer on error
          subscriber.error(err);
        },
        complete: () => {
          emit(); // Emit remaining buffer on complete
          subscriber.complete();
        }
      });
      
      return () => {
        if (timer) clearTimeout(timer);
        subscription.unsubscribe();
      };
    });
  };
}

// Usage: Batch API calls (max 10 items OR every 1 second)
userActions$.pipe(
  bufferUntilSizeOrTime(10, 1000),
  filter(batch => batch.length > 0),
  mergeMap(batch => sendBatchToAPI(batch))
).subscribe(response => console.log('Batch sent:', response));

13.5 Operator Testing and Validation

Testing Type Tool Description Focus
Marble Testing TestScheduler Visual testing with marble diagrams Timing and sequence verification
Unit Testing Jest/Mocha Test operator logic in isolation Correctness and edge cases
Integration Testing TestBed Test operators in real scenarios Integration with other operators
Performance Testing Benchmarking Measure execution time and memory Efficiency and optimization
Type Testing TypeScript Verify type safety and inference Type correctness and generics

Example: Marble testing custom operators

import { TestScheduler } from 'rxjs/testing';

describe('Custom multiplyBy operator', () => {
  let scheduler: TestScheduler;
  
  beforeEach(() => {
    scheduler = new TestScheduler((actual, expected) => {
      expect(actual).toEqual(expected);
    });
  });
  
  it('should multiply values by multiplier', () => {
    scheduler.run(({ cold, expectObservable }) => {
      const source$ = cold('a-b-c|', { a: 1, b: 2, c: 3 });
      const expected =     'a-b-c|';
      const values = { a: 10, b: 20, c: 30 };
      
      const result$ = source$.pipe(multiplyBy(10));
      
      expectObservable(result$).toBe(expected, values);
    });
  });
  
  it('should handle errors correctly', () => {
    scheduler.run(({ cold, expectObservable }) => {
      const source$ = cold('a-b-#', { a: 1, b: 2 });
      const expected =     'a-b-#';
      const values = { a: 5, b: 10 };
      
      const result$ = source$.pipe(multiplyBy(5));
      
      expectObservable(result$).toBe(expected, values);
    });
  });
  
  it('should complete when source completes', () => {
    scheduler.run(({ cold, expectObservable }) => {
      const source$ = cold('a-b-c-|', { a: 2, b: 4, c: 6 });
      const expected =     'a-b-c-|';
      const values = { a: 4, b: 8, c: 12 };
      
      const result$ = source$.pipe(multiplyBy(2));
      
      expectObservable(result$).toBe(expected, values);
    });
  });
});

Example: Testing operator with state

describe('rateLimit operator', () => {
  let scheduler: TestScheduler;
  
  beforeEach(() => {
    scheduler = new TestScheduler((actual, expected) => {
      expect(actual).toEqual(expected);
    });
  });
  
  it('should limit emissions within time window', () => {
    scheduler.run(({ cold, expectObservable, time }) => {
      const windowTime = time('---|'); // 3 time units
      
      // Emit 5 values quickly
      const source$ = cold('(abcde)', { a: 1, b: 2, c: 3, d: 4, e: 5 });
      // Only first 3 should pass (limit: 3 per window)
      const expected =     '(abc)';
      const values = { a: 1, b: 2, c: 3 };
      
      const result$ = source$.pipe(rateLimit(3, 3));
      
      expectObservable(result$).toBe(expected, values);
    });
  });
  
  it('should reset limit after time window', (done) => {
    const emissions: number[] = [];
    
    // Use real time for this test
    interval(100).pipe(
      take(10),
      rateLimit(3, 500) // 3 per 500ms
    ).subscribe({
      next: value => emissions.push(value),
      complete: () => {
        // Should get ~6 values (3 in first 500ms, 3 in next 500ms)
        expect(emissions.length).toBeGreaterThanOrEqual(6);
        expect(emissions.length).toBeLessThanOrEqual(7);
        done();
      }
    });
  });
});

Example: Type testing

// Type testing with TypeScript
import { Observable } from 'rxjs';
import { expectType } from 'tsd';

// Test type inference
const numberObs$ = of(1, 2, 3);
const multiplied$ = numberObs$.pipe(multiplyBy(10));
expectType<Observable<number>>(multiplied$);

// Test generic constraints
interface User { id: number; name: string; }
const users$ = of<User>({ id: 1, name: 'Alice' });

// This should work
const filtered$ = users$.pipe(filterByType('name', 'string'));
expectType<Observable<User>>(filtered$);

// Test error cases (should not compile)
// const invalid$ = of('string').pipe(multiplyBy(10)); // Error: string not assignable to number

// Test operator signature
type MultiplyByOperator = <T extends number>(
  multiplier: number
) => (source$: Observable<T>) => Observable<T>;

expectType<MultiplyByOperator>(multiplyBy);

13.6 Community Operator Patterns and Libraries

Library/Pattern Category Key Operators Use Case
rxjs-etc Utilities tapIf, filterMap, pluckFirst Common utility operators
rxjs-spy Debugging tag, snapshot, deck Observable debugging and inspection
ngx-operators Angular filterNil, mapArray, ofType Angular-specific operators
Custom Retry Error Handling exponentialBackoff, retryStrategy Advanced retry patterns
Caching Performance cacheable, stale-while-revalidate Response caching strategies
State Management State scan-with-reducer, snapshot Observable state patterns

Example: Common community operator patterns

// filterNil: Remove null and undefined
const filterNil = <T>() => filter<T | null | undefined, T>(
  (value): value is T => value != null
);

// tapIf: Conditional side effect
const tapIf = <T>(
  predicate: (value: T) => boolean,
  fn: (value: T) => void
) => tap<T>(value => {
  if (predicate(value)) fn(value);
});

// filterMap: Combine filter and map
const filterMap = <T, R>(
  fn: (value: T) => R | null | undefined
) => (source$: Observable<T>): Observable<R> =>
  source$.pipe(
    map(fn),
    filter((value): value is R => value != null)
  );

// Usage
apiResponse$.pipe(
  filterNil(),
  tapIf(
    data => data.isImportant,
    data => logImportantData(data)
  ),
  filterMap(data => data.value > 0 ? data : null)
).subscribe(result => console.log(result));

Example: Advanced retry with exponential backoff

// Exponential backoff retry strategy
const retryWithExponentialBackoff = (config: {
  maxRetries?: number;
  initialDelay?: number;
  maxDelay?: number;
  backoffMultiplier?: number;
  shouldRetry?: (error: any) => boolean;
} = {}) => <T>(source$: Observable<T>): Observable<T> => {
  const {
    maxRetries = 3,
    initialDelay = 1000,
    maxDelay = 30000,
    backoffMultiplier = 2,
    shouldRetry = () => true
  } = config;
  
  return source$.pipe(
    retryWhen(errors => errors.pipe(
      mergeMap((error, index) => {
        const retryAttempt = index + 1;
        
        // Don't retry if max retries reached or shouldn't retry
        if (retryAttempt > maxRetries || !shouldRetry(error)) {
          return throwError(() => error);
        }
        
        // Calculate delay with exponential backoff
        const delay = Math.min(
          initialDelay * Math.pow(backoffMultiplier, index),
          maxDelay
        );
        
        console.log(`Retry ${retryAttempt}/${maxRetries} after ${delay}ms`);
        
        return timer(delay);
      })
    ))
  );
};

// Usage
http.get('/api/data').pipe(
  retryWithExponentialBackoff({
    maxRetries: 5,
    initialDelay: 500,
    maxDelay: 10000,
    backoffMultiplier: 2,
    shouldRetry: (error) => error.status !== 404
  })
).subscribe({
  next: data => console.log('Success:', data),
  error: err => console.error('Failed after retries:', err)
});

Example: Caching with stale-while-revalidate

// Stale-while-revalidate caching pattern
const staleWhileRevalidate = <T>(config: {
  cacheTime?: number;
  staleTime?: number;
}) => {
  const { cacheTime = 60000, staleTime = 5000 } = config;
  let cached: { value: T; timestamp: number } | null = null;
  
  return (source$: Observable<T>): Observable<T> => {
    return new Observable(subscriber => {
      const now = Date.now();
      
      // Return cached value if fresh
      if (cached && now - cached.timestamp < staleTime) {
        subscriber.next(cached.value);
        subscriber.complete();
        return;
      }
      
      // Return stale cache while revalidating
      if (cached && now - cached.timestamp < cacheTime) {
        subscriber.next(cached.value);
        
        // Fetch fresh data in background
        source$.subscribe({
          next: value => {
            cached = { value, timestamp: Date.now() };
          },
          error: err => console.warn('Revalidation failed:', err)
        });
        
        subscriber.complete();
        return;
      }
      
      // No cache or cache expired, fetch fresh
      return source$.subscribe({
        next: value => {
          cached = { value, timestamp: Date.now() };
          subscriber.next(value);
        },
        error: err => subscriber.error(err),
        complete: () => subscriber.complete()
      });
    });
  };
};

// Usage
function getUser(id: number): Observable<User> {
  return http.get<User>(`/api/users/${id}`).pipe(
    staleWhileRevalidate({
      staleTime: 5000,    // Fresh for 5 seconds
      cacheTime: 60000    // Valid for 60 seconds
    })
  );
}

// First call: fetches from API
getUser(1).subscribe(user => console.log('Fresh:', user));

// Within 5s: returns cached instantly
setTimeout(() => {
  getUser(1).subscribe(user => console.log('Cached:', user));
}, 2000);

// After 5s but before 60s: returns stale, revalidates in background
setTimeout(() => {
  getUser(1).subscribe(user => console.log('Stale:', user));
}, 10000);
Best Practices:
  • Keep operators pure - no side effects except in tap-like operators
  • Use TypeScript generics for type safety and inference
  • Document operators with JSDoc comments including marble diagrams
  • Handle unsubscription properly to prevent memory leaks
  • Write tests for all custom operators, especially edge cases
  • Follow naming conventions - verb-based names (filterBy, mapTo, etc.)
Common Pitfalls:
  • Not handling errors properly in custom operators
  • Creating memory leaks by not cleaning up subscriptions
  • Using stateful logic without considering multiple subscriptions
  • Ignoring backpressure in buffering operators
  • Over-engineering - use existing operators when possible

Section 13 Summary

  • Pipeable operators return functions that transform observables - highly composable
  • Operator factories allow parameterization using higher-order functions
  • Compose operators from existing ones for reusability and clarity
  • Build libraries organized by functionality, domain, or integration type
  • Test operators thoroughly using marble diagrams and unit tests
  • Community patterns provide proven solutions for common use cases

14. RxJS Testing Strategies and Tools

14.1 TestScheduler for Virtual Time Testing

Feature Syntax Description Use Case
TestScheduler new TestScheduler(assertDeepEqual) Virtual time scheduler for synchronous testing Testing time-based operators without waiting
run() scheduler.run(callback) Execute tests in virtual time context Running marble tests synchronously
flush() scheduler.flush() Execute all scheduled actions immediately Force completion of async operations
maxFrames scheduler.maxFrames = 1000 Set maximum virtual time frames Preventing infinite loops in tests
Virtual Time Unit 1 frame = 1ms (virtual) Time advances only when scheduled Deterministic timing in tests

Example: Basic TestScheduler setup

import { TestScheduler } from 'rxjs/testing';

describe('Observable Tests', () => {
  let scheduler: TestScheduler;
  
  beforeEach(() => {
    // Create scheduler with assertion function
    scheduler = new TestScheduler((actual, expected) => {
      expect(actual).toEqual(expected);
    });
  });
  
  it('should test time-based operators synchronously', () => {
    scheduler.run(({ cold, expectObservable }) => {
      // Test debounceTime without waiting
      const source$ = cold('a-b-c---|');
      const expected =     '----c---|';
      
      const result$ = source$.pipe(
        debounceTime(30) // 30 virtual frames
      );
      
      expectObservable(result$).toBe(expected);
    });
  });
});

Example: Testing asynchronous operations synchronously

it('should test interval and delay synchronously', () => {
  scheduler.run(({ expectObservable }) => {
    // interval emits every 10 frames
    const source$ = interval(10).pipe(
      take(5),
      delay(5) // Add 5 frame delay
    );
    
    // Expected: delay 5, then emit at 10, 20, 30, 40, 50
    const expected = '-----a-b-c-d-(e|)';
    const values = { a: 0, b: 1, c: 2, d: 3, e: 4 };
    
    expectObservable(source$).toBe(expected, values);
  });
});

it('should test timer and timeout', () => {
  scheduler.run(({ expectObservable, cold }) => {
    const source$ = cold('-------a--|').pipe(
      timeout(50)
    );
    
    const expected =     '-------a--|';
    
    expectObservable(source$).toBe(expected);
  });
  
  scheduler.run(({ expectObservable, cold }) => {
    // This should timeout
    const source$ = cold('---------------a--|').pipe(
      timeout(50)
    );
    
    const expected =     '-----#'; // Error at frame 50
    
    expectObservable(source$).toBe(expected, undefined, 
      new Error('Timeout has occurred'));
  });
});

14.2 Marble Testing Syntax and Patterns

Symbol Meaning Example Description
- Time frame '---a' Each dash = 1 virtual time frame (1ms)
a-z, 0-9 Emission 'a-b-c' Emit values at specific frames
| Completion 'a-b-c|' Observable completes
# Error 'a-b-#' Observable errors at this frame
( ) Grouping '(ab)-c' Synchronous emissions in same frame
^ Subscription start '^--!' Subscription point marker
! Unsubscription '^--!' Unsubscription point marker
' ' Space (ignored) 'a b c' For readability, no effect on timing

Example: Marble diagram patterns

// Basic emission pattern
'a-b-c|'        // Emit a, b, c then complete
// Frame: 0-1-2-3

// Synchronous emissions
'(abc)|'        // Emit a, b, c synchronously then complete
// All at frame 0

// Multiple values with grouping
'a-(bc)-d|'     // Emit a, then b+c together, then d, complete
// a at 0, b+c at 2, d at 4, complete at 5

// Error handling
'a-b-#'         // Emit a, b, then error
'a-b-#', null, new Error('oops')  // With specific error

// Long delays
'a 99ms b|'     // Emit a, wait 99ms, emit b, complete
'a 1s b|'       // Emit a, wait 1 second, emit b

// Never completes
'a-b-c-'        // Emit a, b, c, never completes

// Immediate completion
'|'             // Complete immediately
'(a|)'          // Emit a and complete synchronously

// Empty observable
''              // Never emits, never completes

// Subscription tracking
'^---!'         // Subscribe at 0, unsubscribe at 4
'---^--!'       // Subscribe at 3, unsubscribe at 6

Example: Complex marble test

it('should test switchMap with marble diagrams', () => {
  scheduler.run(({ cold, hot, expectObservable }) => {
    // Source observable (hot)
    const source$ = hot('  -a-----b-----c----|');
    
    // Inner observables (cold)
    const a$ =      cold('   --1--2--3|       ');
    const b$ =      cold('         --4--5--6|');
    const c$ =      cold('               --7|');
    
    const expected =    '  ---1--2---4--5---7|';
    const values = { 
      1: 'a1', 2: 'a2', 
      4: 'b4', 5: 'b5', 6: 'b6',
      7: 'c7'
    };
    
    const result$ = source$.pipe(
      switchMap(val => {
        if (val === 'a') return a$.pipe(map(n => val + n));
        if (val === 'b') return b$.pipe(map(n => val + n));
        if (val === 'c') return c$.pipe(map(n => val + n));
        return EMPTY;
      })
    );
    
    expectObservable(result$).toBe(expected, values);
  });
});

14.3 hot() and cold() Observable Testing Helpers

Helper Type Behavior Use Case
cold() Cold Observable Starts emitting when subscribed Testing operators that create new observables
hot() Hot Observable Emits regardless of subscriptions Testing shared observables, subjects
Subscription Point ^ Default subscription at frame 0 Controlling when subscription occurs
Values Object { a: value } Map marble symbols to actual values Testing with complex objects
Error Object { # : error } Define error for # symbol Testing error handling

Example: cold() vs hot() observables

it('should understand cold observable behavior', () => {
  scheduler.run(({ cold, expectObservable }) => {
    // Cold: each subscription gets full sequence
    const cold$ = cold('--a--b--c|');
    
    // First subscription at frame 0
    expectObservable(cold$).toBe('--a--b--c|');
    
    // Second subscription also starts from beginning
    expectObservable(cold$).toBe('--a--b--c|');
  });
});

it('should understand hot observable behavior', () => {
  scheduler.run(({ hot, expectObservable }) => {
    // Hot: emissions happen regardless of subscriptions
    const hot$ = hot('--a--b--c--d--e|');
    
    // Subscribe at frame 0, see all emissions
    expectObservable(
      hot$,
      '^--------------!'
    ).toBe('--a--b--c--d--e|');
    
    // Subscribe at frame 6 (after 'b'), miss early values
    expectObservable(
      hot$,
      '------^--------!'
    ).toBe('------c--d--e|');
  });
});

it('should test hot observable with subscription points', () => {
  scheduler.run(({ hot, expectObservable }) => {
    // Subscription point marked with ^
    const hot$ = hot('---^-a--b--c|');
    
    // Will receive a, b, c (subscribed at ^)
    expectObservable(hot$).toBe('--a--b--c|');
  });
});

Example: Using values object for complex data

it('should test with complex objects', () => {
  scheduler.run(({ cold, expectObservable }) => {
    const values = {
      a: { id: 1, name: 'Alice' },
      b: { id: 2, name: 'Bob' },
      c: { id: 3, name: 'Charlie' }
    };
    
    const source$ = cold('--a--b--c|', values);
    const expected =     '--a--b--c|';
    
    const result$ = source$.pipe(
      map(user => ({ ...user, processed: true }))
    );
    
    expectObservable(result$).toBe(expected, {
      a: { id: 1, name: 'Alice', processed: true },
      b: { id: 2, name: 'Bob', processed: true },
      c: { id: 3, name: 'Charlie', processed: true }
    });
  });
});

it('should test error emissions', () => {
  scheduler.run(({ cold, expectObservable }) => {
    const error = new Error('Test error');
    
    const source$ = cold('--a--b--#', 
      { a: 1, b: 2 }, 
      error
    );
    
    const result$ = source$.pipe(
      catchError(err => of('recovered'))
    );
    
    const expected = '--a--b--(r|)';
    
    expectObservable(result$).toBe(expected, { 
      a: 1, b: 2, r: 'recovered' 
    });
  });
});

14.4 expectObservable() and expectSubscriptions() Assertions

Assertion Syntax Description Use Case
expectObservable() .toBe(marble, values?, error?) Assert observable emissions match pattern Verifying emission timing and values
expectSubscriptions() .toBe(subscriptionMarbles) Assert subscription/unsubscription timing Verifying subscription lifecycle
toEqual() .toEqual(expected) Deep equality check Comparing complex objects
Subscription Marble '^---!' ^ = subscribe, ! = unsubscribe Tracking subscription timing
Multiple Subscriptions ['^--!', '--^--!'] Array of subscription patterns Testing multicasting, sharing

Example: Using expectObservable()

it('should verify emission values and timing', () => {
  scheduler.run(({ cold, expectObservable }) => {
    const source$ = cold('--a--b--c|');
    const expected =     '--a--b--c|';
    
    expectObservable(source$).toBe(expected);
  });
});

it('should verify with custom values', () => {
  scheduler.run(({ cold, expectObservable }) => {
    const source$ = cold('--a--b--c|', { a: 1, b: 2, c: 3 });
    
    const result$ = source$.pipe(
      map(x => x * 10)
    );
    
    const expected = '--a--b--c|';
    const values = { a: 10, b: 20, c: 30 };
    
    expectObservable(result$).toBe(expected, values);
  });
});

it('should verify error emissions', () => {
  scheduler.run(({ cold, expectObservable }) => {
    const error = new Error('Failed');
    const source$ = cold('--a--#', { a: 1 }, error);
    
    expectObservable(source$).toBe('--a--#', { a: 1 }, error);
  });
});

Example: Using expectSubscriptions()

it('should verify subscription timing', () => {
  scheduler.run(({ cold, expectObservable, expectSubscriptions }) => {
    const source$ = cold('--a--b--c--d--e|');
    const subs =         '^--------------!';
    
    expectObservable(source$).toBe('--a--b--c--d--e|');
    expectSubscriptions(source$.subscriptions).toBe(subs);
  });
});

it('should verify unsubscription with takeUntil', () => {
  scheduler.run(({ cold, hot, expectObservable, expectSubscriptions }) => {
    const source$ = cold('--a--b--c--d--e|');
    const notifier$ = hot('-------x');
    const expected =      '--a--b-|';
    const subs =          '^------!';
    
    const result$ = source$.pipe(
      takeUntil(notifier$)
    );
    
    expectObservable(result$).toBe(expected);
    expectSubscriptions(source$.subscriptions).toBe(subs);
  });
});

it('should verify multiple subscriptions with share', () => {
  scheduler.run(({ cold, expectObservable, expectSubscriptions }) => {
    const source$ = cold('--a--b--c|');
    const shared$ = source$.pipe(share());
    
    // Two subscriptions
    expectObservable(shared$).toBe('--a--b--c|');
    expectObservable(shared$).toBe('--a--b--c|');
    
    // But only one subscription to source
    expectSubscriptions(source$.subscriptions).toBe('^--------!');
  });
});

14.5 flush() and getMessages() for Test Execution Control

Method Purpose Description Use Case
flush() Execute scheduled Force all scheduled virtual time actions Manual test execution control
getMessages() Inspect emissions Retrieve array of notification messages Custom assertions on messages
frame Virtual time Current virtual time frame number Timing verification
Notification Message object { frame, notification: { kind, value/error } } Detailed emission inspection

Example: Using flush() for manual control

import { TestScheduler } from 'rxjs/testing';

it('should use flush for manual execution', () => {
  const scheduler = new TestScheduler((actual, expected) => {
    expect(actual).toEqual(expected);
  });
  
  const emissions: number[] = [];
  
  // Create observable on the scheduler
  interval(10, scheduler).pipe(
    take(5)
  ).subscribe(x => emissions.push(x));
  
  // Nothing emitted yet
  expect(emissions).toEqual([]);
  
  // Manually flush - executes all scheduled actions
  scheduler.flush();
  
  // Now all emissions have occurred
  expect(emissions).toEqual([0, 1, 2, 3, 4]);
});

it('should control partial execution', () => {
  const scheduler = new TestScheduler((actual, expected) => {
    expect(actual).toEqual(expected);
  });
  
  const emissions: number[] = [];
  
  timer(0, 10, scheduler).pipe(
    take(10)
  ).subscribe(x => emissions.push(x));
  
  // Advance time partially
  scheduler.flush();
  
  expect(emissions.length).toBe(10);
});

Example: Using getMessages() for custom assertions

it('should inspect messages directly', () => {
  scheduler.run(({ cold, expectObservable }) => {
    const source$ = cold('--a--b--c|', { a: 1, b: 2, c: 3 });
    
    const result$ = source$.pipe(
      map(x => x * 10)
    );
    
    // Subscribe and get messages
    const messages = scheduler.run(({ expectObservable }) => {
      expectObservable(result$);
      return result$;
    });
  });
});

// More advanced: accessing raw messages
it('should verify notification details', (done) => {
  const scheduler = new TestScheduler((actual, expected) => {
    expect(actual).toEqual(expected);
  });
  
  scheduler.run(({ cold }) => {
    const source$ = cold('--a--b--c|', { a: 1, b: 2, c: 3 });
    
    // Create a tracked observable
    const messages: any[] = [];
    
    source$.subscribe({
      next: val => messages.push({ 
        frame: scheduler.frame, 
        kind: 'N', 
        value: val 
      }),
      complete: () => messages.push({ 
        frame: scheduler.frame, 
        kind: 'C' 
      })
    });
    
    scheduler.flush();
    
    // Custom assertions
    expect(messages).toEqual([
      { frame: 20, kind: 'N', value: 1 },  // 'a' at frame 20
      { frame: 50, kind: 'N', value: 2 },  // 'b' at frame 50
      { frame: 80, kind: 'N', value: 3 },  // 'c' at frame 80
      { frame: 90, kind: 'C' }             // Complete at frame 90
    ]);
  });
  
  done();
});

14.6 Mock Observers and Subscription Testing

Technique Implementation Purpose Use Case
Mock Observer Object with next/error/complete spies Track observer method calls Verifying observer behavior
Spy Functions jest.fn() or jasmine.createSpy() Mock and track function calls Assertion on call count and arguments
Subscription Tracking Track add() and unsubscribe() calls Verify subscription lifecycle Memory leak prevention testing
Subject Mocking Use real Subjects for testing Control emission timing manually Integration testing

Example: Mock observer testing

describe('Mock Observer Tests', () => {
  it('should track observer method calls', (done) => {
    const nextSpy = jest.fn();
    const errorSpy = jest.fn();
    const completeSpy = jest.fn();
    
    const observer = {
      next: nextSpy,
      error: errorSpy,
      complete: completeSpy
    };
    
    of(1, 2, 3).subscribe(observer);
    
    // Verify calls
    expect(nextSpy).toHaveBeenCalledTimes(3);
    expect(nextSpy).toHaveBeenNthCalledWith(1, 1);
    expect(nextSpy).toHaveBeenNthCalledWith(2, 2);
    expect(nextSpy).toHaveBeenNthCalledWith(3, 3);
    expect(errorSpy).not.toHaveBeenCalled();
    expect(completeSpy).toHaveBeenCalledTimes(1);
    
    done();
  });
  
  it('should track error handling', (done) => {
    const nextSpy = jest.fn();
    const errorSpy = jest.fn();
    const completeSpy = jest.fn();
    
    throwError(() => new Error('Test error')).subscribe({
      next: nextSpy,
      error: errorSpy,
      complete: completeSpy
    });
    
    expect(nextSpy).not.toHaveBeenCalled();
    expect(errorSpy).toHaveBeenCalledTimes(1);
    expect(errorSpy.mock.calls[0][0]).toBeInstanceOf(Error);
    expect(completeSpy).not.toHaveBeenCalled();
    
    done();
  });
});

Example: Subscription lifecycle testing

describe('Subscription Testing', () => {
  it('should verify subscription cleanup', () => {
    const teardownSpy = jest.fn();
    
    const source$ = new Observable(subscriber => {
      subscriber.next(1);
      subscriber.next(2);
      
      // Return teardown function
      return () => {
        teardownSpy();
      };
    });
    
    const subscription = source$.subscribe();
    
    expect(teardownSpy).not.toHaveBeenCalled();
    
    subscription.unsubscribe();
    
    expect(teardownSpy).toHaveBeenCalledTimes(1);
  });
  
  it('should test subscription composition', () => {
    const teardown1 = jest.fn();
    const teardown2 = jest.fn();
    const teardown3 = jest.fn();
    
    const sub1 = new Subscription(teardown1);
    const sub2 = new Subscription(teardown2);
    const sub3 = new Subscription(teardown3);
    
    sub1.add(sub2);
    sub1.add(sub3);
    
    sub1.unsubscribe();
    
    // All should be called when parent unsubscribes
    expect(teardown1).toHaveBeenCalledTimes(1);
    expect(teardown2).toHaveBeenCalledTimes(1);
    expect(teardown3).toHaveBeenCalledTimes(1);
  });
  
  it('should verify takeUntil unsubscription', () => {
    scheduler.run(({ cold, hot }) => {
      const sourceTeardown = jest.fn();
      
      const source$ = cold('--a--b--c--d--e|').pipe(
        finalize(() => sourceTeardown())
      );
      
      const notifier$ = hot('-------x');
      
      source$.pipe(
        takeUntil(notifier$)
      ).subscribe();
      
      scheduler.flush();
      
      expect(sourceTeardown).toHaveBeenCalledTimes(1);
    });
  });
});

Example: Testing with Subject mocks

describe('Subject Mock Testing', () => {
  it('should test component with Subject input', () => {
    const clicks$ = new Subject<MouseEvent>();
    const emissions: number[] = [];
    
    clicks$.pipe(
      scan((count, _) => count + 1, 0)
    ).subscribe(count => emissions.push(count));
    
    // Simulate clicks
    clicks$.next({} as MouseEvent);
    clicks$.next({} as MouseEvent);
    clicks$.next({} as MouseEvent);
    
    expect(emissions).toEqual([1, 2, 3]);
  });
  
  it('should test with BehaviorSubject state', () => {
    const state$ = new BehaviorSubject({ count: 0 });
    const emissions: any[] = [];
    
    state$.subscribe(state => emissions.push(state));
    
    // Initial value received immediately
    expect(emissions).toEqual([{ count: 0 }]);
    
    state$.next({ count: 1 });
    state$.next({ count: 2 });
    
    expect(emissions).toEqual([
      { count: 0 },
      { count: 1 },
      { count: 2 }
    ]);
  });
  
  it('should test service with injected observables', () => {
    // Mock HTTP service
    const mockHttp = {
      get: jest.fn().mockReturnValue(of({ data: 'test' }))
    };
    
    // Service under test
    class DataService {
      getData() {
        return mockHttp.get('/api/data').pipe(
          map(response => response.data)
        );
      }
    }
    
    const service = new DataService();
    
    service.getData().subscribe(data => {
      expect(data).toBe('test');
    });
    
    expect(mockHttp.get).toHaveBeenCalledWith('/api/data');
    expect(mockHttp.get).toHaveBeenCalledTimes(1);
  });
});
Testing Best Practices:
  • Use marble diagrams for visual clarity in time-based tests
  • Prefer scheduler.run() over manual flush() for consistency
  • Test error paths and edge cases explicitly
  • Verify subscription cleanup to prevent memory leaks
  • Use mock observers to verify exact call sequences
  • Keep tests deterministic - avoid real timers
Common Testing Pitfalls:
  • Mixing real time with virtual time - leads to flaky tests
  • Not handling asynchronous completion properly (use done() or async/await)
  • Forgetting to unsubscribe in tests - causes test pollution
  • Over-mocking - test too far from real behavior
  • Not testing unsubscription and cleanup logic

Section 14 Summary

  • TestScheduler provides virtual time for synchronous testing of async operations
  • Marble diagrams offer visual, declarative syntax for test expectations
  • cold() creates new emissions per subscription; hot() shares emissions
  • expectObservable() verifies emissions; expectSubscriptions() verifies lifecycle
  • flush() executes all scheduled virtual time; getMessages() inspects raw notifications
  • Mock observers and subjects enable precise behavior verification

15. Performance Optimization and Memory Management

15.1 Subscription Management and Automatic Cleanup

Pattern Implementation Description Use Case
Manual Unsubscribe subscription.unsubscribe() Explicitly clean up single subscription Simple component cleanup
Subscription Container sub.add(sub1); sub.add(sub2) Compose multiple subscriptions Managing multiple subscriptions together
takeUntil Pattern takeUntil(destroy$) Auto-unsubscribe based on notifier Component lifecycle management
async Pipe (Angular) {{ obs$ | async }} Framework handles subscription lifecycle Template-driven subscriptions
SubSink Pattern Custom subscription manager class Centralized subscription management Complex components with many subscriptions
finalize Operator finalize(() => cleanup()) Execute cleanup on unsubscribe or complete Resource cleanup (timers, connections)

Example: Subscription container pattern

class MyComponent {
  private subscriptions = new Subscription();
  
  ngOnInit() {
    // Add multiple subscriptions to container
    this.subscriptions.add(
      interval(1000).subscribe(n => console.log(n))
    );
    
    this.subscriptions.add(
      fromEvent(button, 'click').subscribe(e => this.handleClick(e))
    );
    
    this.subscriptions.add(
      this.dataService.getData().subscribe(data => this.data = data)
    );
  }
  
  ngOnDestroy() {
    // Unsubscribe all at once
    this.subscriptions.unsubscribe();
  }
}

Example: takeUntil pattern for automatic cleanup

class MyComponent implements OnDestroy {
  private destroy$ = new Subject<void>();
  
  ngOnInit() {
    // All subscriptions auto-complete when destroy$ emits
    interval(1000).pipe(
      takeUntil(this.destroy$)
    ).subscribe(n => console.log(n));
    
    this.userService.getUser().pipe(
      takeUntil(this.destroy$)
    ).subscribe(user => this.user = user);
    
    fromEvent(document, 'click').pipe(
      takeUntil(this.destroy$)
    ).subscribe(e => this.handleClick(e));
  }
  
  ngOnDestroy() {
    // Single emission completes all subscriptions
    this.destroy$.next();
    this.destroy$.complete();
  }
}

Example: SubSink pattern for advanced subscription management

// Custom SubSink class
class SubSink {
  private subscriptions: Subscription[] = [];
  
  add(...subs: Subscription[]) {
    this.subscriptions.push(...subs);
  }
  
  unsubscribe() {
    this.subscriptions.forEach(sub => sub.unsubscribe());
    this.subscriptions = [];
  }
  
  get count() {
    return this.subscriptions.length;
  }
}

// Usage in component
class MyComponent {
  private subs = new SubSink();
  
  ngOnInit() {
    this.subs.add(
      timer(1000).subscribe(() => console.log('tick')),
      this.api.getData().subscribe(data => this.data = data),
      this.route.params.subscribe(params => this.id = params.id)
    );
    
    console.log(`Active subscriptions: ${this.subs.count}`);
  }
  
  ngOnDestroy() {
    this.subs.unsubscribe();
  }
}

15.2 Operator Chain Optimization and Pipeline Efficiency

Optimization Inefficient Efficient Benefit
Operator Order map().filter() filter().map() Reduce unnecessary transformations
Early Termination map().map().take(1) take(1).map().map() Stop processing early
Shared Computation Multiple subscriptions share() or shareReplay() Avoid duplicate work
Flattening Strategy Wrong operator choice switchMap vs mergeMap vs concatMap Match concurrency to use case
Avoid Nested Subscribes subscribe(x => obs.subscribe()) switchMap/mergeMap/concatMap Cleaner code, better memory management
Memoization Recalculate every time Cache expensive operations Reduce computation overhead

Example: Operator ordering optimization

// INEFFICIENT - processes all 1000 items before filtering
range(1, 1000).pipe(
  map(n => expensiveTransform(n)),  // 1000 operations
  filter(n => n > 500)               // Then filters
).subscribe();

// EFFICIENT - filters first, reducing transformations
range(1, 1000).pipe(
  filter(n => n > 500),              // Filters to ~500 items
  map(n => expensiveTransform(n))   // Only 500 operations
).subscribe();

// EVEN BETTER - early termination
range(1, 1000).pipe(
  filter(n => n > 500),
  take(10),                          // Stop after 10
  map(n => expensiveTransform(n))   // Only 10 operations
).subscribe();

Example: Avoiding nested subscriptions

// ANTI-PATTERN - nested subscriptions (hard to manage, memory leaks)
this.userService.getUser(userId).subscribe(user => {
  this.orderService.getOrders(user.id).subscribe(orders => {
    this.productService.getProducts(orders[0].id).subscribe(products => {
      console.log(products);
    });
  });
});

// BETTER - flattening operators
this.userService.getUser(userId).pipe(
  switchMap(user => this.orderService.getOrders(user.id)),
  switchMap(orders => this.productService.getProducts(orders[0].id))
).subscribe(products => {
  console.log(products);
});

// BEST - with error handling and cleanup
this.userService.getUser(userId).pipe(
  switchMap(user => this.orderService.getOrders(user.id)),
  switchMap(orders => this.productService.getProducts(orders[0].id)),
  catchError(err => {
    console.error('Error:', err);
    return of([]);
  }),
  takeUntil(this.destroy$)
).subscribe(products => {
  console.log(products);
});

Example: Choosing the right flattening operator

// switchMap - cancels previous, keeps latest (search/autocomplete)
searchInput$.pipe(
  debounceTime(300),
  switchMap(term => this.api.search(term))  // Cancel old searches
).subscribe(results => displayResults(results));

// mergeMap - concurrent processing (independent operations)
userIds$.pipe(
  mergeMap(id => this.api.getUser(id), 5)   // Max 5 concurrent
).subscribe(user => processUser(user));

// concatMap - sequential, ordered (must complete in order)
queue$.pipe(
  concatMap(task => this.processTask(task))  // One at a time
).subscribe(result => console.log(result));

// exhaustMap - ignore while busy (prevent duplicate submissions)
saveButton$.pipe(
  exhaustMap(() => this.api.save(data))      // Ignore clicks while saving
).subscribe(response => console.log('Saved:', response));

15.3 Memory Leak Detection and Prevention

Leak Source Symptom Prevention Detection Method
Unsubscribed Observables Growing memory usage Always unsubscribe or use takeUntil Chrome DevTools memory profiler
Long-lived Subjects Retained observers Complete subjects, unsubscribe Check observer count
Event Listeners DOM nodes not GC'd Use fromEvent with takeUntil Event listener count in DevTools
Timers/Intervals Background activity Unsubscribe from interval/timer Network/console activity when idle
shareReplay Misuse Cached subscriptions never release Use refCount: true and windowTime Memory snapshots over time
Closure Captures Large objects retained Limit closure scope, use finalize Heap snapshot analysis

Example: Common memory leak scenarios and fixes

// MEMORY LEAK - interval never stops
class LeakyComponent {
  ngOnInit() {
    interval(1000).subscribe(n => console.log(n));
    // Leak: subscription never cleaned up
  }
}

// FIXED - properly managed subscription
class FixedComponent implements OnDestroy {
  private destroy$ = new Subject<void>();
  
  ngOnInit() {
    interval(1000).pipe(
      takeUntil(this.destroy$)
    ).subscribe(n => console.log(n));
  }
  
  ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

// MEMORY LEAK - Subject with many subscribers never completes
class DataService {
  private data$ = new Subject<any>();
  
  subscribe() {
    return this.data$.subscribe();  // Subscribers accumulate
  }
  
  // Missing: complete() or cleanup mechanism
}

// FIXED - proper Subject lifecycle
class DataService implements OnDestroy {
  private data$ = new Subject<any>();
  
  subscribe() {
    return this.data$.asObservable();
  }
  
  ngOnDestroy() {
    this.data$.complete();  // Release all subscribers
  }
}

Example: Detecting memory leaks

// Add leak detection helper
class LeakDetector {
  private subscriptions = new Map<string, Subscription>();
  
  track(name: string, subscription: Subscription) {
    if (this.subscriptions.has(name)) {
      console.warn(`Subscription '${name}' already exists`);
    }
    this.subscriptions.set(name, subscription);
  }
  
  check() {
    const active = Array.from(this.subscriptions.entries())
      .filter(([_, sub]) => !sub.closed);
    
    if (active.length > 0) {
      console.warn(`${active.length} active subscriptions:`, 
        active.map(([name]) => name));
    }
    return active.length;
  }
  
  cleanup() {
    this.subscriptions.forEach(sub => sub.unsubscribe());
    this.subscriptions.clear();
  }
}

// Usage
const detector = new LeakDetector();

detector.track('interval', interval(1000).subscribe());
detector.track('http', http.get('/api').subscribe());

// Check for leaks before component destruction
detector.check();  // Warns if subscriptions still active
detector.cleanup();  // Clean up all

Example: shareReplay memory leak prevention

// MEMORY LEAK - shareReplay without refCount
const data$ = this.http.get('/api/data').pipe(
  shareReplay(1)  // Keeps subscription alive forever
);

// Multiple components subscribe
data$.subscribe(d1 => console.log(d1));
data$.subscribe(d2 => console.log(d2));
// Even after components destroy, HTTP subscription remains

// FIXED - use refCount to allow cleanup
const data$ = this.http.get('/api/data').pipe(
  shareReplay({ bufferSize: 1, refCount: true })
  // Unsubscribes from source when no subscribers
);

// BETTER - add time limit for cache
const data$ = this.http.get('/api/data').pipe(
  shareReplay({ 
    bufferSize: 1, 
    refCount: true,
    windowTime: 60000  // Cache for 60 seconds
  })
);

// BEST - complete observable pattern
const data$ = this.http.get('/api/data').pipe(
  shareReplay({ bufferSize: 1, refCount: true }),
  takeUntil(this.serviceDestroy$)  // Complete on service destroy
);

15.4 Cold vs Hot Observable Performance Characteristics

Characteristic Cold Observable Hot Observable Performance Impact
Execution Per subscriber (unicast) Shared (multicast) Hot avoids duplicate work
Memory N × subscription overhead 1 × shared overhead Hot uses less memory
Side Effects Repeated per subscription Executed once Hot prevents duplicate API calls
Late Subscribers Get full sequence Miss past emissions Consider with shareReplay
When to Use Independent data per subscriber Shared data across subscribers Match pattern to use case

Example: Cold observable behavior

// Cold observable - separate execution per subscriber
const cold$ = new Observable(subscriber => {
  console.log('Observable execution started');
  
  // Expensive operation
  const data = fetchExpensiveData();
  subscriber.next(data);
  subscriber.complete();
});

// Each subscription triggers new execution
cold$.subscribe(data => console.log('Sub 1:', data));
// Console: "Observable execution started"
// Console: "Sub 1: data"

cold$.subscribe(data => console.log('Sub 2:', data));
// Console: "Observable execution started" (again!)
// Console: "Sub 2: data"

// Performance issue: fetchExpensiveData() called twice!

Example: Converting cold to hot for better performance

// Convert cold to hot with share()
const hot$ = new Observable(subscriber => {
  console.log('Observable execution started');
  const data = fetchExpensiveData();
  subscriber.next(data);
  subscriber.complete();
}).pipe(
  share()  // Multicast to all subscribers
);

// First subscription triggers execution
hot$.subscribe(data => console.log('Sub 1:', data));
// Console: "Observable execution started"
// Console: "Sub 1: data"

// Second subscription reuses same execution
hot$.subscribe(data => console.log('Sub 2:', data));
// Console: "Sub 2: data" (no duplicate execution!)

// Performance benefit: fetchExpensiveData() called only once

// Use shareReplay to cache for late subscribers
const cached$ = expensive$.pipe(
  shareReplay({ bufferSize: 1, refCount: true })
);

// Even late subscribers get cached result
setTimeout(() => {
  cached$.subscribe(data => console.log('Late sub:', data));
  // Gets cached data without re-execution
}, 5000);

Example: Real-world HTTP optimization

// Service with cold HTTP observable
class UserService {
  getUsers() {
    return this.http.get<User[]>('/api/users');
    // Cold - each subscription = new HTTP request
  }
}

// Component using service
class UserListComponent {
  users$ = this.userService.getUsers();
  
  ngOnInit() {
    // Multiple subscriptions
    this.users$.subscribe(u => this.userCount = u.length);
    this.users$.subscribe(u => this.firstUser = u[0]);
    this.users$.subscribe(u => this.lastUser = u[u.length - 1]);
    // Problem: 3 HTTP requests for same data!
  }
}

// OPTIMIZED - share the observable
class UserService {
  getUsers() {
    return this.http.get<User[]>('/api/users').pipe(
      shareReplay({ bufferSize: 1, refCount: true })
    );
    // Hot - single HTTP request, shared result
  }
}

// Now component makes only 1 HTTP request
// All 3 subscriptions share the same response

15.5 Share Strategy Selection (share, shareReplay, refCount)

Operator Behavior Cache Best For
share() Multicast while subscribers exist No replay Real-time streams, live data
shareReplay(1) Cache last value forever Infinite Config data, rarely changes (with refCount!)
shareReplay({refCount: true}) Cache while subscribers, cleanup when none Temporary API responses, user data
shareReplay({windowTime}) Cache for time duration Time-limited Polling data, time-sensitive cache
publish() + refCount() Manual multicast control Custom Advanced scenarios, custom logic

Example: Choosing the right sharing strategy

// share() - for real-time data (WebSocket, events)
const clicks$ = fromEvent(button, 'click').pipe(
  share()  // Share emissions while any subscriber exists
);

clicks$.subscribe(e => console.log('Handler 1'));
clicks$.subscribe(e => console.log('Handler 2'));
// Both handlers receive same click events

// shareReplay(1) - for static/config data
const config$ = this.http.get('/api/config').pipe(
  shareReplay({ bufferSize: 1, refCount: true })
  // IMPORTANT: use refCount to prevent memory leaks
);

// Late subscribers get cached config
config$.subscribe(c => this.applyConfig(c));
setTimeout(() => {
  config$.subscribe(c => console.log('Cached:', c));
}, 5000);

// shareReplay with windowTime - for time-sensitive data
const stockPrice$ = this.api.getStockPrice('AAPL').pipe(
  shareReplay({ 
    bufferSize: 1, 
    refCount: true,
    windowTime: 10000  // Cache for 10 seconds
  })
);

// Subscribers within 10s get cached price
// After 10s, new request is made

Example: Share strategy comparison

// Test observable that logs executions
const test$ = defer(() => {
  console.log('Execution!');
  return of(Math.random());
});

// 1. No sharing - multiple executions
console.log('--- No sharing ---');
test$.subscribe(v => console.log('A:', v));  // Execution! A: 0.123
test$.subscribe(v => console.log('B:', v));  // Execution! B: 0.456

// 2. share() - single execution, no replay
console.log('--- share() ---');
const shared$ = test$.pipe(share());
shared$.subscribe(v => console.log('A:', v));  // Execution! A: 0.789
shared$.subscribe(v => console.log('B:', v));  // B: 0.789 (same value)
setTimeout(() => {
  shared$.subscribe(v => console.log('C:', v));  // Execution! C: 0.321 (new)
}, 100);

// 3. shareReplay() - single execution, replay to late subscribers
console.log('--- shareReplay ---');
const replayed$ = test$.pipe(shareReplay(1));
replayed$.subscribe(v => console.log('A:', v));  // Execution! A: 0.555
replayed$.subscribe(v => console.log('B:', v));  // B: 0.555 (replayed)
setTimeout(() => {
  replayed$.subscribe(v => console.log('C:', v));  // C: 0.555 (replayed!)
}, 100);

// 4. shareReplay with refCount - cleanup when no subscribers
console.log('--- shareReplay with refCount ---');
const refCounted$ = test$.pipe(
  shareReplay({ bufferSize: 1, refCount: true })
);
const sub1 = refCounted$.subscribe(v => console.log('A:', v));  // Execution!
const sub2 = refCounted$.subscribe(v => console.log('B:', v));  // Replayed
sub1.unsubscribe();
sub2.unsubscribe();  // All unsubscribed - source cleaned up
setTimeout(() => {
  refCounted$.subscribe(v => console.log('C:', v));  // Execution! (new)
}, 100);

15.6 Backpressure Handling and Buffer Management

Strategy Operator Behavior Use Case
Sampling sample(), sampleTime() Periodically emit most recent value Fast producers, slow consumers
Throttling throttleTime(), throttle() Emit first, ignore rest for duration Rate limiting, preventing spam
Debouncing debounceTime(), debounce() Emit after silence period Search input, form validation
Buffering buffer(), bufferTime() Collect values, emit batches Batch API requests, analytics
Windowing window(), windowTime() Group into observable windows Complex batching logic
Dropping take(), first(), filter() Selectively drop emissions Limiting data volume

Example: Handling fast producers with sampling

// Problem: Mouse moves emit 100+ events per second
const mouseMoves$ = fromEvent<MouseEvent>(document, 'mousemove');

// BAD - processes every event (performance issue)
mouseMoves$.subscribe(e => {
  updateUI(e.clientX, e.clientY);  // Called 100+ times/sec
});

// GOOD - sample at reasonable rate
mouseMoves$.pipe(
  sampleTime(50)  // Sample every 50ms (20 updates/sec)
).subscribe(e => {
  updateUI(e.clientX, e.clientY);  // Called 20 times/sec
});

// ALTERNATIVE - throttle (emit first, ignore rest)
mouseMoves$.pipe(
  throttleTime(50, undefined, { leading: true, trailing: false })
).subscribe(e => {
  updateUI(e.clientX, e.clientY);
});

Example: Buffering for batch processing

// Collect analytics events and send in batches
const analyticsEvent$ = new Subject<AnalyticsEvent>();

// Buffer events and send every 10 seconds or 100 events
analyticsEvent$.pipe(
  bufferTime(10000, null, 100),  // Time OR count
  filter(batch => batch.length > 0),
  mergeMap(batch => this.api.sendAnalytics(batch))
).subscribe(
  response => console.log('Batch sent:', response),
  error => console.error('Batch failed:', error)
);

// Track events
analyticsEvent$.next({ type: 'click', target: 'button1' });
analyticsEvent$.next({ type: 'view', page: '/home' });
// ... events accumulate and sent in batch

// Alternative: buffer with custom trigger
const flushTrigger$ = new Subject<void>();

analyticsEvent$.pipe(
  buffer(flushTrigger$),  // Buffer until trigger emits
  filter(batch => batch.length > 0)
).subscribe(batch => this.api.sendAnalytics(batch));

// Manually trigger flush
flushTrigger$.next();  // Sends accumulated events

Example: Backpressure with concurrency control

// Problem: Processing 1000 files simultaneously
const files$ = from(largeFileArray);  // 1000+ files

// BAD - processes all concurrently (memory issue)
files$.pipe(
  mergeMap(file => this.processFile(file))
).subscribe();  // 1000+ concurrent operations!

// GOOD - limit concurrency
files$.pipe(
  mergeMap(file => this.processFile(file), 5)  // Max 5 concurrent
).subscribe(
  result => console.log('Processed:', result),
  error => console.error('Error:', error)
);

// ALTERNATIVE - use concatMap for sequential (slow but safe)
files$.pipe(
  concatMap(file => this.processFile(file))  // One at a time
).subscribe();

// ADVANCED - adaptive concurrency based on system load
let currentConcurrency = 5;

files$.pipe(
  mergeMap(file => 
    this.processFile(file).pipe(
      tap(() => {
        // Adjust based on performance
        if (performance.now() % 1000 < 500) {
          currentConcurrency = Math.min(10, currentConcurrency + 1);
        } else {
          currentConcurrency = Math.max(1, currentConcurrency - 1);
        }
      })
    ),
    currentConcurrency
  )
).subscribe();

Example: Preventing queue overflow

// Queue with overflow protection
class BoundedQueue<T> {
  private queue$ = new Subject<T>();
  private queueSize = 0;
  private maxSize = 100;
  
  enqueue(item: T): boolean {
    if (this.queueSize >= this.maxSize) {
      console.warn('Queue full, dropping item');
      return false;  // Drop item
    }
    
    this.queueSize++;
    this.queue$.next(item);
    return true;
  }
  
  process(processor: (item: T) => Observable<any>, concurrency = 1) {
    return this.queue$.pipe(
      mergeMap(item => 
        processor(item).pipe(
          finalize(() => this.queueSize--)
        ),
        concurrency
      )
    );
  }
}

// Usage
const queue = new BoundedQueue<Task>();

queue.process(task => this.processTask(task), 3)
  .subscribe(result => console.log('Processed:', result));

// Add tasks
tasks.forEach(task => {
  if (!queue.enqueue(task)) {
    console.error('Task dropped:', task);
  }
});
Performance Best Practices:
  • Always unsubscribe from long-lived observables
  • Use takeUntil for automatic cleanup in components
  • Choose correct flattening operator for your use case
  • Share expensive computations with share/shareReplay
  • Use refCount: true with shareReplay to prevent leaks
  • Apply backpressure strategies for high-volume streams
  • Limit concurrency in mergeMap for resource-intensive operations
  • Monitor memory with Chrome DevTools heap snapshots
Common Performance Issues:
  • Missing unsubscribe - number one cause of memory leaks
  • shareReplay without refCount - subscriptions never cleanup
  • Nested subscriptions - hard to manage, causes leaks
  • Wrong flattening operator - unnecessary work or race conditions
  • No concurrency limits - resource exhaustion
  • Unbounded buffers - memory overflow
  • Hot observables without cleanup - event listeners pile up

Section 15 Summary

  • Subscription management via takeUntil, containers, or finalize prevents leaks
  • Operator ordering and early termination significantly improve performance
  • Memory leaks from unsubscribed observables detectable via DevTools profiling
  • Cold observables repeat work; hot observables share - choose wisely
  • Sharing strategies - share() for real-time, shareReplay() for caching (with refCount!)
  • Backpressure managed via sampling, throttling, buffering, or concurrency limits

16. Framework Integration Patterns

16.1 Angular RxJS Integration and async Pipe

Feature Syntax Description Benefit
async Pipe {{ observable$ | async }} Subscribe/unsubscribe automatically in template No manual subscription management
HttpClient http.get<T>(url) Returns observables for HTTP requests Built-in RxJS integration
Reactive Forms formControl.valueChanges Observable streams for form values Reactive form validation
Router Events router.events Observable stream of navigation events React to route changes
Event Emitters @Output() event = new EventEmitter() Component communication with observables Type-safe event handling
Service State BehaviorSubject in services Centralized state management Share state across components

Example: async pipe for automatic subscription management

// Component
@Component({
  selector: 'app-user-list',
  template: `
    <div *ngIf="users$ | async as users; else loading">
      <div *ngFor="let user of users">
        {{ user.name }}
      </div>
    </div>
    
    <ng-template #loading>
      <div>Loading...</div>
    </ng-template>
    
    <!-- Error handling -->
    <div *ngIf="error$ | async as error" class="error">
      {{ error }}
    </div>
  `
})
export class UserListComponent {
  users$: Observable<User[]>;
  error$ = new Subject<string>();
  
  constructor(private userService: UserService) {
    this.users$ = this.userService.getUsers().pipe(
      catchError(err => {
        this.error$.next(err.message);
        return of([]);
      })
    );
  }
  // No ngOnDestroy needed - async pipe handles cleanup!
}

Example: Reactive forms with RxJS

@Component({
  selector: 'app-search',
  template: `
    <input [formControl]="searchControl" placeholder="Search...">
    <div *ngFor="let result of results$ | async">
      {{ result.title }}
    </div>
  `
})
export class SearchComponent implements OnInit {
  searchControl = new FormControl('');
  results$: Observable<SearchResult[]>;
  
  constructor(private searchService: SearchService) {}
  
  ngOnInit() {
    this.results$ = this.searchControl.valueChanges.pipe(
      debounceTime(300),           // Wait for user to stop typing
      distinctUntilChanged(),      // Only if value changed
      filter(term => term.length >= 3),  // Minimum 3 characters
      switchMap(term => 
        this.searchService.search(term).pipe(
          catchError(() => of([]))  // Handle errors gracefully
        )
      ),
      shareReplay({ bufferSize: 1, refCount: true })  // Cache results
    );
  }
}

Example: Angular service with state management

// User service with BehaviorSubject
@Injectable({ providedIn: 'root' })
export class UserService {
  private currentUserSubject = new BehaviorSubject<User | null>(null);
  public currentUser$ = this.currentUserSubject.asObservable();
  
  private loadingSubject = new BehaviorSubject<boolean>(false);
  public loading$ = this.loadingSubject.asObservable();
  
  constructor(private http: HttpClient) {}
  
  loadCurrentUser() {
    this.loadingSubject.next(true);
    
    return this.http.get<User>('/api/user/current').pipe(
      tap(user => {
        this.currentUserSubject.next(user);
        this.loadingSubject.next(false);
      }),
      catchError(err => {
        this.loadingSubject.next(false);
        return throwError(() => err);
      })
    );
  }
  
  updateUser(user: User) {
    return this.http.put<User>(`/api/users/${user.id}`, user).pipe(
      tap(updatedUser => this.currentUserSubject.next(updatedUser))
    );
  }
  
  logout() {
    this.currentUserSubject.next(null);
  }
}

// Component using the service
@Component({
  template: `
    <div *ngIf="user$ | async as user">
      Welcome, {{ user.name }}!
    </div>
    <div *ngIf="loading$ | async">Loading...</div>
  `
})
export class HeaderComponent {
  user$ = this.userService.currentUser$;
  loading$ = this.userService.loading$;
  
  constructor(private userService: UserService) {}
}

16.2 React Hooks Integration with RxJS

Hook Pattern Purpose Implementation Use Case
useObservable Subscribe to observable useState + useEffect with observable Render observable values
useSubject Create managed subject useRef for subject + cleanup Event streams in components
useEventObservable Convert events to observable fromEvent with cleanup DOM event handling
useObservableState Observable-driven state BehaviorSubject + useState Complex state management
useRxEffect Side effects with observables useEffect with observable pipeline API calls, subscriptions

Example: Custom useObservable hook

// useObservable.ts
function useObservable<T>(
  observable$: Observable<T>, 
  initialValue: T
): T {
  const [value, setValue] = useState<T>(initialValue);
  
  useEffect(() => {
    const subscription = observable$.subscribe({
      next: setValue,
      error: err => console.error('Observable error:', err)
    });
    
    return () => subscription.unsubscribe();
  }, [observable$]);
  
  return value;
}

// Usage in component
function UserProfile({ userId }: { userId: string }) {
  const user$ = useMemo(
    () => userService.getUser(userId),
    [userId]
  );
  
  const user = useObservable(user$, null);
  
  if (!user) return <div>Loading...</div>;
  
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

Example: useSubject for event handling

// useSubject.ts
function useSubject<T>(): [Observable<T>, (value: T) => void] {
  const subjectRef = useRef<Subject<T>>();
  
  if (!subjectRef.current) {
    subjectRef.current = new Subject<T>();
  }
  
  useEffect(() => {
    return () => {
      subjectRef.current?.complete();
    };
  }, []);
  
  const emit = useCallback((value: T) => {
    subjectRef.current?.next(value);
  }, []);
  
  return [subjectRef.current.asObservable(), emit];
}

// Usage: Search with debounce
function SearchComponent() {
  const [search$, emitSearch] = useSubject<string>();
  
  const results = useObservable(
    useMemo(() => search$.pipe(
      debounceTime(300),
      distinctUntilChanged(),
      switchMap(term => searchAPI(term))
    ), [search$]),
    []
  );
  
  return (
    <>
      <input 
        onChange={e => emitSearch(e.target.value)} 
        placeholder="Search..."
      />
      {results.map(r => <div key={r.id}>{r.title}</div>)}
    </>
  );
}

Example: Advanced observable state management

// useObservableState.ts
function useObservableState<T>(
  initialState: T
): [T, (value: T | ((prev: T) => T)) => void, Observable<T>] {
  const [state$] = useState(() => new BehaviorSubject<T>(initialState));
  const [state, setState] = useState<T>(initialState);
  
  useEffect(() => {
    const sub = state$.subscribe(setState);
    return () => {
      sub.unsubscribe();
      state$.complete();
    };
  }, [state$]);
  
  const updateState = useCallback((value: T | ((prev: T) => T)) => {
    const newValue = typeof value === 'function' 
      ? (value as (prev: T) => T)(state$.value)
      : value;
    state$.next(newValue);
  }, [state$]);
  
  return [state, updateState, state$.asObservable()];
}

// Usage: Complex state with derived values
function TodoList() {
  const [todos, setTodos, todos$] = useObservableState<Todo[]>([]);
  
  const stats = useObservable(
    useMemo(() => todos$.pipe(
      map(todos => ({
        total: todos.length,
        completed: todos.filter(t => t.done).length,
        pending: todos.filter(t => !t.done).length
      }))
    ), [todos$]),
    { total: 0, completed: 0, pending: 0 }
  );
  
  const addTodo = (text: string) => {
    setTodos(prev => [...prev, { id: Date.now(), text, done: false }]);
  };
  
  return (
    <div>
      <div>Total: {stats.total}, Completed: {stats.completed}</div>
      {todos.map(todo => <TodoItem key={todo.id} todo={todo} />)}
    </div>
  );
}

16.3 Vue.js Composition API and RxJS

Composable Purpose Implementation Use Case
useObservable Observable to ref ref + watchEffect with subscription Reactive observable values
useSubscription Manage subscription lifecycle onUnmounted cleanup Side effect subscriptions
fromRef Ref to observable Observable from watch Convert reactive refs
useRxState BehaviorSubject state Subject + ref sync Observable state management

Example: Vue composables for RxJS

// useObservable.ts
import { ref, onUnmounted, Ref } from 'vue';
import { Observable } from 'rxjs';

export function useObservable<T>(
  observable$: Observable<T>,
  initialValue?: T
): Ref<T | undefined> {
  const value = ref<T | undefined>(initialValue);
  
  const subscription = observable$.subscribe({
    next: (v) => { value.value = v; },
    error: (err) => console.error('Observable error:', err)
  });
  
  onUnmounted(() => {
    subscription.unsubscribe();
  });
  
  return value;
}

// useSubscription.ts
export function useSubscription() {
  const subscriptions: Subscription[] = [];
  
  const add = (subscription: Subscription) => {
    subscriptions.push(subscription);
  };
  
  onUnmounted(() => {
    subscriptions.forEach(sub => sub.unsubscribe());
  });
  
  return { add };
}

// Usage in Vue component
export default {
  setup() {
    const userService = inject('userService');
    
    // Convert observable to ref
    const users = useObservable(
      userService.getUsers(),
      []
    );
    
    const searchTerm = ref('');
    
    // Create observable from ref
    const searchResults = useObservable(
      computed(() => searchTerm.value).pipe(
        debounceTime(300),
        switchMap(term => searchAPI(term))
      ),
      []
    );
    
    return {
      users,
      searchTerm,
      searchResults
    };
  }
};

Example: Vue RxJS state management

// composables/useRxState.ts
export function useRxState<T>(initialValue: T) {
  const subject = new BehaviorSubject<T>(initialValue);
  const state = ref<T>(initialValue);
  
  const subscription = subject.subscribe(value => {
    state.value = value;
  });
  
  onUnmounted(() => {
    subscription.unsubscribe();
    subject.complete();
  });
  
  const setState = (value: T | ((prev: T) => T)) => {
    const newValue = typeof value === 'function'
      ? value(subject.value)
      : value;
    subject.next(newValue);
  };
  
  return {
    state: readonly(state),
    setState,
    state$: subject.asObservable()
  };
}

// Usage
export default {
  setup() {
    const { state: todos, setState: setTodos, state$ } = useRxState<Todo[]>([]);
    
    // Derived state
    const completedCount = useObservable(
      state$.pipe(
        map(todos => todos.filter(t => t.completed).length)
      ),
      0
    );
    
    const addTodo = (text: string) => {
      setTodos(prev => [...prev, { 
        id: Date.now(), 
        text, 
        completed: false 
      }]);
    };
    
    return {
      todos,
      completedCount,
      addTodo
    };
  }
};

16.4 Node.js Streams Integration

Integration Method Description Use Case
Readable to Observable fromEvent(stream, 'data') Convert Node.js readable stream File reading, HTTP response
Observable to Writable Subscribe and write to stream Pipe observable to writable stream File writing, HTTP request
Transform Stream Observable operators as Transform Use RxJS operators on streams Data processing pipelines
EventEmitter fromEvent(emitter, 'event') Convert EventEmitter to observable Event-driven architectures

Example: Node.js stream to observable

import { fromEvent, merge } from 'rxjs';
import { map, takeUntil } from 'rxjs/operators';
import * as fs from 'fs';
import * as readline from 'readline';

// Read file line by line with RxJS
function readFileLines(filePath: string): Observable<string> {
  return new Observable(subscriber => {
    const readStream = fs.createReadStream(filePath);
    const rl = readline.createInterface({
      input: readStream,
      crlfDelay: Infinity
    });
    
    rl.on('line', line => subscriber.next(line));
    rl.on('close', () => subscriber.complete());
    rl.on('error', err => subscriber.error(err));
    
    return () => {
      rl.close();
      readStream.destroy();
    };
  });
}

// Usage: Process large files
readFileLines('./large-file.txt').pipe(
  map(line => line.toUpperCase()),
  filter(line => line.includes('ERROR')),
  take(100)
).subscribe({
  next: line => console.log(line),
  complete: () => console.log('Done processing')
});

// Convert stream events to observable
const readStream = fs.createReadStream('./data.txt');

const data$ = fromEvent(readStream, 'data');
const end$ = fromEvent(readStream, 'end');
const error$ = fromEvent(readStream, 'error');

data$.pipe(
  map((chunk: Buffer) => chunk.toString()),
  takeUntil(merge(end$, error$))
).subscribe({
  next: chunk => processChunk(chunk),
  error: err => console.error('Stream error:', err),
  complete: () => console.log('Stream ended')
});

Example: Observable to Node.js stream

import { interval } from 'rxjs';
import { map } from 'rxjs/operators';
import * as fs from 'fs';

// Write observable data to file
function observableToStream<T>(
  observable$: Observable<T>,
  writeStream: fs.WriteStream
): Promise<void> {
  return new Promise((resolve, reject) => {
    const subscription = observable$.subscribe({
      next: value => {
        const canWrite = writeStream.write(JSON.stringify(value) + '\n');
        if (!canWrite) {
          // Backpressure handling
          subscription.unsubscribe();
          writeStream.once('drain', () => {
            // Resume when buffer drained
          });
        }
      },
      error: err => {
        writeStream.end();
        reject(err);
      },
      complete: () => {
        writeStream.end();
        resolve();
      }
    });
    
    writeStream.on('error', err => {
      subscription.unsubscribe();
      reject(err);
    });
  });
}

// Usage: Stream data to file
const writeStream = fs.createWriteStream('./output.jsonl');

const data$ = interval(1000).pipe(
  take(100),
  map(n => ({ id: n, timestamp: Date.now() }))
);

observableToStream(data$, writeStream)
  .then(() => console.log('Write complete'))
  .catch(err => console.error('Write error:', err));

16.5 WebSocket Integration Patterns

Pattern Implementation Description Feature
webSocket() Function webSocket(url) RxJS built-in WebSocket subject Bidirectional communication
Reconnection retryWhen() with delay Auto-reconnect on disconnect Connection resilience
Message Queue Buffer messages during disconnect Queue messages until reconnected Guaranteed delivery
Heartbeat Periodic ping/pong Keep connection alive Connection monitoring
multiplexing Share single WebSocket Multiple logical channels Resource efficiency

Example: Basic WebSocket observable

import { webSocket, WebSocketSubject } from 'rxjs/webSocket';

// Create WebSocket observable
const ws$: WebSocketSubject<any> = webSocket({
  url: 'ws://localhost:8080',
  openObserver: {
    next: () => console.log('WebSocket connected')
  },
  closeObserver: {
    next: () => console.log('WebSocket disconnected')
  }
});

// Subscribe to messages
ws$.subscribe({
  next: msg => console.log('Message:', msg),
  error: err => console.error('WebSocket error:', err),
  complete: () => console.log('WebSocket completed')
});

// Send messages
ws$.next({ type: 'subscribe', channel: 'updates' });
ws$.next({ type: 'message', data: 'Hello Server!' });

// Unsubscribe closes connection
// ws$.complete();

Example: WebSocket with auto-reconnect

import { webSocket } from 'rxjs/webSocket';
import { retryWhen, delay, tap } from 'rxjs/operators';

function createWebSocketWithReconnect(url: string) {
  return webSocket({
    url,
    openObserver: {
      next: () => console.log('Connected to WebSocket')
    },
    closeObserver: {
      next: () => console.log('Disconnected from WebSocket')
    }
  }).pipe(
    retryWhen(errors => errors.pipe(
      tap(err => console.log('Connection lost, reconnecting...')),
      delay(5000)  // Wait 5s before reconnect
    ))
  );
}

// Usage with automatic reconnection
const ws$ = createWebSocketWithReconnect('ws://localhost:8080');

ws$.subscribe({
  next: msg => handleMessage(msg),
  error: err => console.error('Fatal error:', err)
});

// Advanced: Exponential backoff
function createResilientWebSocket(url: string) {
  return webSocket(url).pipe(
    retryWhen(errors => errors.pipe(
      scan((retryCount, error) => {
        if (retryCount >= 10) throw error;
        console.log(`Retry ${retryCount + 1}/10`);
        return retryCount + 1;
      }, 0),
      delayWhen(retryCount => 
        timer(Math.min(1000 * Math.pow(2, retryCount), 30000))
      )
    ))
  );
}

Example: WebSocket with message queue and multiplexing

// WebSocket service with message queue
class WebSocketService {
  private ws$: WebSocketSubject<any>;
  private connectionState$ = new BehaviorSubject<'connected' | 'disconnected'>('disconnected');
  private messageQueue: any[] = [];
  
  constructor(url: string) {
    this.ws$ = webSocket({
      url,
      openObserver: {
        next: () => {
          this.connectionState$.next('connected');
          this.flushQueue();
        }
      },
      closeObserver: {
        next: () => this.connectionState$.next('disconnected')
      }
    });
    
    // Auto-reconnect
    this.ws$.pipe(
      retryWhen(errors => errors.pipe(delay(5000)))
    ).subscribe();
  }
  
  // Send message (queue if disconnected)
  send(message: any) {
    if (this.connectionState$.value === 'connected') {
      this.ws$.next(message);
    } else {
      this.messageQueue.push(message);
    }
  }
  
  // Flush queued messages
  private flushQueue() {
    while (this.messageQueue.length > 0) {
      const msg = this.messageQueue.shift();
      this.ws$.next(msg);
    }
  }
  
  // Subscribe to specific message type
  on<T>(messageType: string): Observable<T> {
    return this.ws$.pipe(
      filter((msg: any) => msg.type === messageType),
      map((msg: any) => msg.data as T)
    );
  }
  
  // Multiplexing - multiple logical channels
  channel<T>(channelName: string): Observable<T> {
    return this.ws$.pipe(
      filter((msg: any) => msg.channel === channelName),
      map((msg: any) => msg.data as T)
    );
  }
}

// Usage
const wsService = new WebSocketService('ws://localhost:8080');

// Subscribe to user updates
wsService.on<User>('user-update').subscribe(
  user => console.log('User updated:', user)
);

// Subscribe to notifications
wsService.on<Notification>('notification').subscribe(
  notification => showNotification(notification)
);

// Send messages
wsService.send({ type: 'subscribe', channel: 'users' });
wsService.send({ type: 'message', data: 'Hello' });

16.6 Service Worker and RxJS Integration

Pattern Implementation Description Use Case
Message Channel fromEvent on message port Observable messaging with SW Background sync, push notifications
Cache Strategy Observable cache operators Implement cache-first, network-first Offline-first apps
Background Sync Queue with retry logic Sync data when online Offline data persistence
Push Notifications fromEvent on push events Handle push notifications Real-time updates

Example: Service Worker messaging with RxJS

// In main app
class ServiceWorkerService {
  private messageSubject = new Subject<any>();
  public messages$ = this.messageSubject.asObservable();
  
  constructor() {
    if ('serviceWorker' in navigator) {
      navigator.serviceWorker.ready.then(registration => {
        // Listen for messages from SW
        fromEvent<MessageEvent>(navigator.serviceWorker, 'message')
          .pipe(
            map(event => event.data)
          )
          .subscribe(message => this.messageSubject.next(message));
      });
    }
  }
  
  // Send message to SW
  async postMessage(message: any) {
    const registration = await navigator.serviceWorker.ready;
    registration.active?.postMessage(message);
  }
  
  // Observable for specific message types
  on<T>(type: string): Observable<T> {
    return this.messages$.pipe(
      filter(msg => msg.type === type),
      map(msg => msg.data as T)
    );
  }
}

// Usage
const swService = new ServiceWorkerService();

// Listen for sync complete
swService.on<{ count: number }>('sync-complete').subscribe(
  data => console.log(`Synced ${data.count} items`)
);

// Request background sync
swService.postMessage({ 
  type: 'background-sync', 
  data: { items: [...] } 
});

Example: Cache strategies with RxJS

// Offline-first HTTP service
class OfflineHttpService {
  constructor(private http: HttpClient) {}
  
  // Cache-first strategy
  cacheFirst<T>(url: string): Observable<T> {
    return from(caches.match(url)).pipe(
      mergeMap(cached => {
        if (cached) {
          return from(cached.json() as Promise<T>);
        }
        // Not in cache, fetch from network
        return this.http.get<T>(url).pipe(
          tap(data => {
            // Store in cache
            caches.open('api-cache').then(cache => {
              cache.put(url, new Response(JSON.stringify(data)));
            });
          })
        );
      })
    );
  }
  
  // Network-first with cache fallback
  networkFirst<T>(url: string): Observable<T> {
    return this.http.get<T>(url).pipe(
      timeout(5000),
      tap(data => {
        // Update cache
        caches.open('api-cache').then(cache => {
          cache.put(url, new Response(JSON.stringify(data)));
        });
      }),
      catchError(() => 
        // Network failed, try cache
        from(caches.match(url)).pipe(
          mergeMap(cached => {
            if (cached) {
              return from(cached.json() as Promise<T>);
            }
            return throwError(() => new Error('No cached data'));
          })
        )
      )
    );
  }
  
  // Stale-while-revalidate
  staleWhileRevalidate<T>(url: string): Observable<T> {
    const cache$ = from(caches.match(url)).pipe(
      filter(response => !!response),
      mergeMap(response => from(response!.json() as Promise<T>))
    );
    
    const network$ = this.http.get<T>(url).pipe(
      tap(data => {
        caches.open('api-cache').then(cache => {
          cache.put(url, new Response(JSON.stringify(data)));
        });
      })
    );
    
    // Return cache immediately, then network
    return concat(cache$, network$).pipe(
      distinctUntilChanged()
    );
  }
}

// Usage
const offlineHttp = new OfflineHttpService(http);

// Get data with cache-first
offlineHttp.cacheFirst<User[]>('/api/users')
  .subscribe(users => displayUsers(users));
Framework Integration Best Practices:
  • Angular: Use async pipe for automatic subscription management
  • React: Create custom hooks for observable integration and cleanup
  • Vue: Use composables with onUnmounted for proper cleanup
  • Node.js: Handle backpressure when converting streams
  • WebSocket: Implement reconnection logic and message queuing
  • Service Workers: Use RxJS for offline-first patterns

Section 16 Summary

  • Angular async pipe provides automatic subscription lifecycle management
  • React hooks require custom implementation for observable integration and cleanup
  • Vue composables with onUnmounted enable reactive observable patterns
  • Node.js streams convert to/from observables for unified data processing
  • WebSocket subjects enable bidirectional communication with retry logic
  • Service Workers with RxJS implement sophisticated offline-first strategies

17. Real-world Application Patterns

17.1 HTTP Request Management and Cancellation

Pattern Operator Description Use Case
Auto-Cancel Previous switchMap() Cancel previous request when new one starts Search, autocomplete, type-ahead
Prevent Duplicate exhaustMap() Ignore new requests while one is pending Save buttons, form submissions
Concurrent Requests mergeMap() Allow multiple simultaneous requests Batch operations, parallel loading
Sequential Requests concatMap() Wait for previous request to complete Ordered operations, dependencies
Retry with Backoff retryWhen() Retry failed requests with delay Network resilience
Polling interval() + switchMap() Periodic data refresh Real-time updates, status checks

Example: Search with auto-cancel and debounce

// Autocomplete search
const searchInput$ = fromEvent<Event>(searchInput, 'input').pipe(
  map(e => (e.target as HTMLInputElement).value),
  debounceTime(300),           // Wait for user to stop typing
  distinctUntilChanged(),      // Only if value changed
  filter(term => term.length >= 2),  // Min 2 characters
  tap(() => showLoadingSpinner()),
  switchMap(term =>            // Cancel previous searches
    this.http.get<Result[]>(`/api/search?q=${term}`).pipe(
      catchError(err => {
        console.error('Search failed:', err);
        return of([]);         // Return empty on error
      }),
      finalize(() => hideLoadingSpinner())
    )
  ),
  shareReplay({ bufferSize: 1, refCount: true })
);

searchInput$.subscribe(results => displayResults(results));

Example: Prevent duplicate submissions

// Save button that prevents duplicate clicks
const saveButton$ = fromEvent(saveBtn, 'click');

saveButton$.pipe(
  tap(() => saveBtn.disabled = true),
  exhaustMap(() =>               // Ignore clicks while saving
    this.http.post('/api/save', formData).pipe(
      timeout(10000),            // 10 second timeout
      retry(2),                  // Retry twice
      catchError(err => {
        showError('Save failed');
        return throwError(() => err);
      }),
      finalize(() => saveBtn.disabled = false)
    )
  )
).subscribe({
  next: response => showSuccess('Saved successfully'),
  error: err => console.error('Save error:', err)
});

Example: Parallel requests with concurrency limit

// Load user details for multiple IDs with max 5 concurrent
const userIds = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

from(userIds).pipe(
  mergeMap(
    id => this.http.get<User>(`/api/users/${id}`).pipe(
      catchError(err => {
        console.error(`Failed to load user ${id}:`, err);
        return of(null);         // Continue with others
      })
    ),
    5                            // Max 5 concurrent requests
  ),
  filter(user => user !== null), // Remove failed requests
  toArray()                      // Collect all results
).subscribe(users => {
  console.log('Loaded users:', users);
  displayUsers(users);
});

Example: Polling with start/stop control

// Polling service with start/stop
class PollingService {
  private polling$ = new Subject<boolean>();
  
  startPolling(url: string, intervalMs: number = 5000): Observable<any> {
    return this.polling$.pipe(
      startWith(true),
      switchMap(shouldPoll => 
        shouldPoll 
          ? interval(intervalMs).pipe(
              startWith(0),      // Immediate first request
              switchMap(() => this.http.get(url).pipe(
                catchError(err => {
                  console.error('Poll failed:', err);
                  return of(null);
                })
              ))
            )
          : EMPTY                // Stop polling
      )
    );
  }
  
  stop() {
    this.polling$.next(false);
  }
  
  start() {
    this.polling$.next(true);
  }
}

// Usage
const poller = new PollingService();
const data$ = poller.startPolling('/api/status', 3000);

data$.subscribe(status => updateStatus(status));

// Stop polling when tab not visible
document.addEventListener('visibilitychange', () => {
  if (document.hidden) {
    poller.stop();
  } else {
    poller.start();
  }
});

17.2 User Input Handling and Form Validation

Pattern Implementation Description Benefit
Debounced Validation debounceTime() + switchMap() Validate after user stops typing Reduce API calls
Cross-field Validation combineLatest() Validate multiple fields together Password confirmation, date ranges
Async Validators switchMap() to API Server-side validation (uniqueness) Username availability
Real-time Feedback map() + scan() Show validation as user types Password strength, character count
Form State BehaviorSubject Centralized form state management Complex multi-step forms

Example: Real-time form validation

// Email input with async validation
const emailInput$ = fromEvent<Event>(emailField, 'input').pipe(
  map(e => (e.target as HTMLInputElement).value),
  shareReplay({ bufferSize: 1, refCount: true })
);

// Synchronous validation
const emailValid$ = emailInput$.pipe(
  map(email => ({
    valid: /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email),
    error: 'Invalid email format'
  }))
);

// Asynchronous validation (check availability)
const emailAvailable$ = emailInput$.pipe(
  debounceTime(500),
  distinctUntilChanged(),
  filter(email => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)),
  switchMap(email => 
    this.http.get<{available: boolean}>(`/api/check-email?email=${email}`).pipe(
      map(result => ({
        valid: result.available,
        error: result.available ? null : 'Email already taken'
      })),
      catchError(() => of({ valid: true, error: null }))
    )
  ),
  startWith({ valid: true, error: null })
);

// Combine validations
combineLatest([emailValid$, emailAvailable$]).pipe(
  map(([format, available]) => {
    if (!format.valid) return format;
    if (!available.valid) return available;
    return { valid: true, error: null };
  })
).subscribe(validation => {
  if (validation.valid) {
    emailField.classList.remove('error');
    errorMsg.textContent = '';
  } else {
    emailField.classList.add('error');
    errorMsg.textContent = validation.error;
  }
});

Example: Password strength meter

// Password strength indicator
const passwordInput$ = fromEvent<Event>(passwordField, 'input').pipe(
  map(e => (e.target as HTMLInputElement).value),
  shareReplay({ bufferSize: 1, refCount: true })
);

interface PasswordStrength {
  score: number;      // 0-4
  label: string;      // weak, fair, good, strong
  requirements: {
    length: boolean;
    uppercase: boolean;
    lowercase: boolean;
    number: boolean;
    special: boolean;
  };
}

const passwordStrength$ = passwordInput$.pipe(
  map(password => {
    const requirements = {
      length: password.length >= 8,
      uppercase: /[A-Z]/.test(password),
      lowercase: /[a-z]/.test(password),
      number: /[0-9]/.test(password),
      special: /[^A-Za-z0-9]/.test(password)
    };
    
    const metCount = Object.values(requirements).filter(Boolean).length;
    
    let score = 0;
    let label = 'weak';
    
    if (metCount === 5) {
      score = 4;
      label = 'strong';
    } else if (metCount === 4) {
      score = 3;
      label = 'good';
    } else if (metCount === 3) {
      score = 2;
      label = 'fair';
    } else if (metCount >= 1) {
      score = 1;
      label = 'weak';
    }
    
    return { score, label, requirements };
  })
);

passwordStrength$.subscribe(strength => {
  updateStrengthMeter(strength.score);
  strengthLabel.textContent = strength.label;
  
  // Update requirement checklist
  Object.entries(strength.requirements).forEach(([req, met]) => {
    const element = document.getElementById(`req-${req}`);
    element.classList.toggle('met', met);
  });
});

Example: Multi-step form with state management

// Multi-step form manager
class FormWizard {
  private currentStep$ = new BehaviorSubject<number>(1);
  private formData$ = new BehaviorSubject<Partial<FormData>>({});
  
  public step$ = this.currentStep$.asObservable();
  public data$ = this.formData$.asObservable();
  
  // Validation status for each step
  public stepValid$ = (step: number): Observable<boolean> => {
    return this.formData$.pipe(
      map(data => this.validateStep(step, data))
    );
  };
  
  // Can navigate to next step
  public canGoNext$ = combineLatest([
    this.currentStep$,
    this.formData$
  ]).pipe(
    map(([step, data]) => this.validateStep(step, data))
  );
  
  updateData(stepData: Partial<FormData>) {
    this.formData$.next({
      ...this.formData$.value,
      ...stepData
    });
  }
  
  nextStep() {
    const current = this.currentStep$.value;
    if (this.validateStep(current, this.formData$.value)) {
      this.currentStep$.next(current + 1);
    }
  }
  
  previousStep() {
    const current = this.currentStep$.value;
    if (current > 1) {
      this.currentStep$.next(current - 1);
    }
  }
  
  submit(): Observable<any> {
    return this.http.post('/api/submit', this.formData$.value);
  }
  
  private validateStep(step: number, data: Partial<FormData>): boolean {
    // Validation logic per step
    switch(step) {
      case 1:
        return !!data.name && !!data.email;
      case 2:
        return !!data.address && !!data.city;
      case 3:
        return !!data.payment;
      default:
        return false;
    }
  }
}

// Usage
const wizard = new FormWizard();

wizard.canGoNext$.subscribe(canGoNext => {
  nextBtn.disabled = !canGoNext;
});

wizard.step$.subscribe(step => {
  showStep(step);
});

17.3 Real-time Data Synchronization

Pattern Implementation Description Use Case
Optimistic Updates Update UI, then sync Immediate feedback, rollback on error Todo apps, social media
Conflict Resolution Merge strategies Handle concurrent edits Collaborative editing
Delta Sync Send only changes Minimize bandwidth Large datasets
Offline Queue Queue operations, sync when online Offline-first apps Mobile apps, PWAs
Version Control Track versions, detect conflicts Consistency guarantees Documents, configuration

Example: Optimistic update pattern

// Todo service with optimistic updates
class TodoService {
  private todos$ = new BehaviorSubject<Todo[]>([]);
  
  getTodos(): Observable<Todo[]> {
    return this.todos$.asObservable();
  }
  
  addTodo(text: string): Observable<Todo> {
    // Create optimistic todo
    const optimisticTodo: Todo = {
      id: `temp-${Date.now()}`,
      text,
      completed: false,
      synced: false
    };
    
    // Update UI immediately
    this.todos$.next([...this.todos$.value, optimisticTodo]);
    
    // Sync to server
    return this.http.post<Todo>('/api/todos', { text }).pipe(
      tap(serverTodo => {
        // Replace optimistic with server response
        const todos = this.todos$.value.map(t => 
          t.id === optimisticTodo.id 
            ? { ...serverTodo, synced: true }
            : t
        );
        this.todos$.next(todos);
      }),
      catchError(err => {
        // Rollback on error
        const todos = this.todos$.value.filter(t => t.id !== optimisticTodo.id);
        this.todos$.next(todos);
        return throwError(() => err);
      })
    );
  }
  
  updateTodo(id: string, updates: Partial<Todo>): Observable<Todo> {
    // Optimistic update
    const todos = this.todos$.value.map(t => 
      t.id === id ? { ...t, ...updates, synced: false } : t
    );
    this.todos$.next(todos);
    
    // Sync to server
    return this.http.put<Todo>(`/api/todos/${id}`, updates).pipe(
      tap(serverTodo => {
        const updated = this.todos$.value.map(t => 
          t.id === id ? { ...serverTodo, synced: true } : t
        );
        this.todos$.next(updated);
      }),
      catchError(err => {
        // Revert on error
        this.loadTodos().subscribe();
        return throwError(() => err);
      })
    );
  }
  
  private loadTodos(): Observable<Todo[]> {
    return this.http.get<Todo[]>('/api/todos').pipe(
      tap(todos => this.todos$.next(todos.map(t => ({ ...t, synced: true }))))
    );
  }
}

Example: Offline-first with sync queue

// Offline sync manager
class OfflineSyncService {
  private syncQueue$ = new BehaviorSubject<SyncOperation[]>([]);
  private online$ = merge(
    fromEvent(window, 'online').pipe(mapTo(true)),
    fromEvent(window, 'offline').pipe(mapTo(false))
  ).pipe(
    startWith(navigator.onLine),
    shareReplay({ bufferSize: 1, refCount: true })
  );
  
  // Sync when online
  constructor() {
    this.online$.pipe(
      filter(online => online),
      switchMap(() => this.processSyncQueue())
    ).subscribe();
  }
  
  queueOperation(operation: SyncOperation) {
    const queue = [...this.syncQueue$.value, operation];
    this.syncQueue$.next(queue);
    
    // Save to localStorage for persistence
    localStorage.setItem('syncQueue', JSON.stringify(queue));
    
    // Try to sync if online
    if (navigator.onLine) {
      this.processSyncQueue().subscribe();
    }
  }
  
  private processSyncQueue(): Observable<void> {
    const queue = this.syncQueue$.value;
    
    if (queue.length === 0) {
      return of(undefined);
    }
    
    return from(queue).pipe(
      concatMap(operation => 
        this.executeOperation(operation).pipe(
          tap(() => this.removeFromQueue(operation)),
          catchError(err => {
            console.error('Sync failed:', err);
            // Keep in queue, try again later
            return of(undefined);
          })
        )
      ),
      toArray(),
      map(() => undefined)
    );
  }
  
  private executeOperation(op: SyncOperation): Observable<any> {
    switch (op.type) {
      case 'CREATE':
        return this.http.post(op.url, op.data);
      case 'UPDATE':
        return this.http.put(op.url, op.data);
      case 'DELETE':
        return this.http.delete(op.url);
      default:
        return throwError(() => new Error('Unknown operation'));
    }
  }
  
  private removeFromQueue(operation: SyncOperation) {
    const queue = this.syncQueue$.value.filter(op => op.id !== operation.id);
    this.syncQueue$.next(queue);
    localStorage.setItem('syncQueue', JSON.stringify(queue));
  }
  
  getQueueStatus(): Observable<{ pending: number; online: boolean }> {
    return combineLatest([
      this.syncQueue$,
      this.online$
    ]).pipe(
      map(([queue, online]) => ({
        pending: queue.length,
        online
      }))
    );
  }
}

// Usage
const syncService = new OfflineSyncService();

// Queue operation
syncService.queueOperation({
  id: Date.now().toString(),
  type: 'CREATE',
  url: '/api/todos',
  data: { text: 'New todo' }
});

// Monitor sync status
syncService.getQueueStatus().subscribe(status => {
  if (status.pending > 0) {
    showSyncIndicator(`${status.pending} items pending`);
  }
});

17.4 WebSocket Message Processing

Pattern Implementation Description Use Case
Message Routing filter() by message type Route messages to handlers Chat, notifications, updates
Request-Response correlationId matching Match responses to requests RPC over WebSocket
Message Ordering concatMap() or sequence numbers Ensure message order Critical sequences
Heartbeat interval() + merge() Keep connection alive Connection monitoring
Reconnect Buffer Buffer during disconnect Queue messages while offline Guaranteed delivery

Example: WebSocket message router

// Advanced WebSocket message router
class WebSocketRouter {
  private ws$: WebSocketSubject<any>;
  private messageRoutes = new Map<string, Subject<any>>();
  
  constructor(url: string) {
    this.ws$ = webSocket({
      url,
      openObserver: { next: () => console.log('WS Connected') },
      closeObserver: { next: () => console.log('WS Disconnected') }
    });
    
    // Route incoming messages
    this.ws$.pipe(
      retryWhen(errors => errors.pipe(delay(5000)))
    ).subscribe(message => {
      const route = this.messageRoutes.get(message.type);
      if (route) {
        route.next(message.data);
      } else {
        console.warn('Unhandled message type:', message.type);
      }
    });
  }
  
  // Subscribe to specific message type
  on<T>(messageType: string): Observable<T> {
    if (!this.messageRoutes.has(messageType)) {
      this.messageRoutes.set(messageType, new Subject<T>());
    }
    return this.messageRoutes.get(messageType)!.asObservable();
  }
  
  // Send message
  send(type: string, data: any) {
    this.ws$.next({ type, data, timestamp: Date.now() });
  }
  
  // Request-response pattern
  request<T>(type: string, data: any, timeout = 5000): Observable<T> {
    const correlationId = `req-${Date.now()}-${Math.random()}`;
    
    // Wait for response with matching correlationId
    const response$ = this.ws$.pipe(
      filter((msg: any) => 
        msg.type === `${type}-response` && 
        msg.correlationId === correlationId
      ),
      map((msg: any) => msg.data as T),
      take(1),
      timeout(timeout)
    );
    
    // Send request
    this.ws$.next({ 
      type, 
      data, 
      correlationId,
      timestamp: Date.now() 
    });
    
    return response$;
  }
}

// Usage
const wsRouter = new WebSocketRouter('ws://localhost:8080');

// Subscribe to chat messages
wsRouter.on<ChatMessage>('chat').subscribe(
  msg => displayChatMessage(msg)
);

// Subscribe to notifications
wsRouter.on<Notification>('notification').subscribe(
  notif => showNotification(notif)
);

// Request-response
wsRouter.request<UserProfile>('get-profile', { userId: 123 })
  .subscribe({
    next: profile => console.log('Profile:', profile),
    error: err => console.error('Request timeout or error:', err)
  });

17.5 Animation and UI State Management

Pattern Implementation Description Use Case
RAF Animation animationFrameScheduler Smooth 60fps animations Scroll effects, transitions
Tween Animation interval() + map() Value interpolation over time Number counters, progress bars
Gesture Handling merge() mouse/touch events Unified gesture processing Drag, swipe, pinch
State Machine scan() + BehaviorSubject Manage complex UI states Modal flows, wizards
Loading States startWith() + catchError() Track async operation status Spinners, skeletons

Example: Smooth scroll animation

// Smooth scroll to element
function smoothScrollTo(element: HTMLElement, duration = 1000): Observable<number> {
  const start = window.pageYOffset;
  const target = element.offsetTop;
  const distance = target - start;
  const startTime = performance.now();
  
  return interval(0, animationFrameScheduler).pipe(
    map(() => (performance.now() - startTime) / duration),
    takeWhile(progress => progress < 1),
    map(progress => {
      // Easing function (ease-in-out)
      const eased = progress < 0.5
        ? 2 * progress * progress
        : -1 + (4 - 2 * progress) * progress;
      return start + (distance * eased);
    }),
    tap(position => window.scrollTo(0, position)),
    endWith(target)
  );
}

// Usage
const scrollBtn = document.getElementById('scroll-btn');
const targetSection = document.getElementById('target');

fromEvent(scrollBtn, 'click').pipe(
  switchMap(() => smoothScrollTo(targetSection, 800))
).subscribe({
  complete: () => console.log('Scroll complete')
});

Example: Drag and drop with RxJS

// Draggable element
function makeDraggable(element: HTMLElement): Observable<{x: number; y: number}> {
  const mouseDown$ = fromEvent<MouseEvent>(element, 'mousedown');
  const mouseMove$ = fromEvent<MouseEvent>(document, 'mousemove');
  const mouseUp$ = fromEvent<MouseEvent>(document, 'mouseup');
  
  return mouseDown$.pipe(
    switchMap(start => {
      const startX = start.clientX - element.offsetLeft;
      const startY = start.clientY - element.offsetTop;
      
      return mouseMove$.pipe(
        map(move => ({
          x: move.clientX - startX,
          y: move.clientY - startY
        })),
        takeUntil(mouseUp$)
      );
    })
  );
}

// Usage
const draggable = document.getElementById('draggable');

makeDraggable(draggable).subscribe(pos => {
  draggable.style.left = `${pos.x}px`;
  draggable.style.top = `${pos.y}px`;
});

Example: UI state machine

// Modal state machine
type ModalState = 'closed' | 'opening' | 'open' | 'closing';
type ModalAction = 'OPEN' | 'CLOSE' | 'TRANSITION_COMPLETE';

class ModalStateMachine {
  private actions$ = new Subject<ModalAction>();
  
  public state$ = this.actions$.pipe(
    scan((state: ModalState, action: ModalAction) => {
      switch (state) {
        case 'closed':
          return action === 'OPEN' ? 'opening' : state;
        case 'opening':
          return action === 'TRANSITION_COMPLETE' ? 'open' : state;
        case 'open':
          return action === 'CLOSE' ? 'closing' : state;
        case 'closing':
          return action === 'TRANSITION_COMPLETE' ? 'closed' : state;
        default:
          return state;
      }
    }, 'closed' as ModalState),
    startWith('closed' as ModalState),
    shareReplay({ bufferSize: 1, refCount: true })
  );
  
  open() {
    this.actions$.next('OPEN');
    
    // Auto-complete transition after animation
    timer(300).subscribe(() => {
      this.actions$.next('TRANSITION_COMPLETE');
    });
  }
  
  close() {
    this.actions$.next('CLOSE');
    
    timer(300).subscribe(() => {
      this.actions$.next('TRANSITION_COMPLETE');
    });
  }
}

// Usage
const modal = new ModalStateMachine();

modal.state$.subscribe(state => {
  const element = document.getElementById('modal');
  
  element.className = `modal modal-${state}`;
  
  if (state === 'open') {
    element.setAttribute('aria-hidden', 'false');
  } else if (state === 'closed') {
    element.setAttribute('aria-hidden', 'true');
  }
});

openBtn.addEventListener('click', () => modal.open());
closeBtn.addEventListener('click', () => modal.close());

17.6 Complex Workflow Orchestration

Pattern Implementation Description Use Case
Sequential Workflow concatMap() Execute steps in order Onboarding, checkout
Parallel with Merge forkJoin() Wait for all parallel tasks Data aggregation
Conditional Branching iif() or switchMap Dynamic workflow paths A/B testing, permissions
Saga Pattern Compensating transactions Rollback on failure Distributed transactions
Circuit Breaker Error tracking + fallback Prevent cascade failures Microservices

Example: Multi-step checkout workflow

// Complex checkout workflow
class CheckoutWorkflow {
  executeCheckout(order: Order): Observable<CheckoutResult> {
    return of(order).pipe(
      // Step 1: Validate cart
      concatMap(order => this.validateCart(order).pipe(
        tap(() => this.updateProgress('Validating cart...'))
      )),
      
      // Step 2: Check inventory
      concatMap(order => this.checkInventory(order).pipe(
        tap(() => this.updateProgress('Checking inventory...'))
      )),
      
      // Step 3: Calculate totals
      concatMap(order => this.calculateTotals(order).pipe(
        tap(() => this.updateProgress('Calculating totals...'))
      )),
      
      // Step 4: Process payment
      concatMap(order => this.processPayment(order).pipe(
        tap(() => this.updateProgress('Processing payment...'))
      )),
      
      // Step 5: Reserve inventory
      concatMap(order => this.reserveInventory(order).pipe(
        tap(() => this.updateProgress('Reserving items...'))
      )),
      
      // Step 6: Create order
      concatMap(order => this.createOrder(order).pipe(
        tap(() => this.updateProgress('Creating order...'))
      )),
      
      // Step 7: Send confirmation
      concatMap(result => this.sendConfirmation(result).pipe(
        map(() => result),
        tap(() => this.updateProgress('Complete!'))
      )),
      
      // Error handling with rollback
      catchError(err => {
        console.error('Checkout failed:', err);
        return this.rollbackCheckout(order).pipe(
          concatMap(() => throwError(() => err))
        );
      })
    );
  }
  
  private rollbackCheckout(order: Order): Observable<void> {
    // Compensating transactions
    return forkJoin([
      this.releaseInventory(order).pipe(catchError(() => of(null))),
      this.refundPayment(order).pipe(catchError(() => of(null))),
      this.cancelOrder(order).pipe(catchError(() => of(null)))
    ]).pipe(
      map(() => undefined),
      tap(() => console.log('Rollback complete'))
    );
  }
}

// Usage
const checkout = new CheckoutWorkflow();

checkout.executeCheckout(order).subscribe({
  next: result => {
    showSuccess(`Order #${result.orderId} confirmed!`);
    redirectToConfirmation(result.orderId);
  },
  error: err => {
    showError('Checkout failed. Please try again.');
    console.error(err);
  }
});

Example: Circuit breaker pattern

// Circuit breaker for resilient API calls
class CircuitBreaker {
  private failureCount = 0;
  private state: 'CLOSED' | 'OPEN' | 'HALF_OPEN' = 'CLOSED';
  private failureThreshold = 5;
  private resetTimeout = 60000; // 1 minute
  
  execute<T>(request: Observable<T>): Observable<T> {
    if (this.state === 'OPEN') {
      return throwError(() => new Error('Circuit breaker is OPEN'));
    }
    
    return request.pipe(
      tap(() => {
        // Success - reset failure count
        this.failureCount = 0;
        if (this.state === 'HALF_OPEN') {
          this.state = 'CLOSED';
          console.log('Circuit breaker CLOSED');
        }
      }),
      catchError(err => {
        this.failureCount++;
        
        if (this.failureCount >= this.failureThreshold) {
          this.state = 'OPEN';
          console.log('Circuit breaker OPEN');
          
          // Auto-retry after timeout
          timer(this.resetTimeout).subscribe(() => {
            this.state = 'HALF_OPEN';
            this.failureCount = 0;
            console.log('Circuit breaker HALF_OPEN');
          });
        }
        
        return throwError(() => err);
      })
    );
  }
  
  getState(): string {
    return this.state;
  }
}

// Usage
const breaker = new CircuitBreaker();

function makeApiCall(): Observable<any> {
  return breaker.execute(
    this.http.get('/api/unstable-endpoint').pipe(
      timeout(5000)
    )
  ).pipe(
    catchError(err => {
      if (err.message === 'Circuit breaker is OPEN') {
        // Use cached data or show user-friendly message
        return of({ cached: true, data: getCachedData() });
      }
      return throwError(() => err);
    })
  );
}

// Monitor circuit breaker state
interval(1000).subscribe(() => {
  const state = breaker.getState();
  updateStatusIndicator(state);
});
Real-world Pattern Best Practices:
  • Request management: Use switchMap for searches, exhaustMap for submissions
  • Form validation: Combine sync and async validators with debouncing
  • Data sync: Implement optimistic updates with rollback on error
  • WebSocket: Add message routing, correlation IDs, and reconnection logic
  • Animations: Use animationFrameScheduler for smooth 60fps performance
  • Workflows: Design compensating transactions for failure recovery

Section 17 Summary

  • HTTP management - switchMap cancels, exhaustMap prevents duplicates, retryWhen adds resilience
  • Form validation - combine debouncing, async validation, and cross-field checks
  • Data sync - optimistic updates with rollback, offline queues for resilience
  • WebSocket processing - message routing, request-response patterns, reconnection
  • UI animations - animationFrameScheduler for smooth effects, state machines for flows
  • Workflow orchestration - sequential steps, parallel execution, circuit breakers

18. Debugging and Development Tools

18.1 RxJS DevTools Browser Extension

Feature Description Use Case Browser Support
Observable Graph Visual dependency graph of observables Understand stream relationships Chrome, Firefox
Timeline View Chronological emission timeline Track emission sequences Chrome, Firefox
Value Inspector Inspect emitted values in detail Examine complex objects Chrome, Firefox
Subscription Tracking Monitor active subscriptions Detect memory leaks Chrome, Firefox
Performance Metrics Measure operator execution time Identify bottlenecks Chrome
Stream Recording Record and replay streams Reproduce bugs Chrome

Example: Enable RxJS DevTools instrumentation

// Install: npm install @rxjs-debugging/rxjs-debugging-for-vscode

// Enable in development
import { instrumentObservables } from '@rxjs-debugging/rxjs-debugging-for-vscode';

if (environment.development) {
  instrumentObservables({
    // Tag observables for easier identification
    tag: true,
    // Capture stack traces
    stackTrace: true,
    // Track subscriptions
    trackSubscriptions: true
  });
}

// Tag observables for DevTools
const user$ = this.http.get<User>('/api/user').pipe(
  tag('user-request'),  // Shows as "user-request" in DevTools
  shareReplay(1)
);

// DevTools will show:
// - Observable name: "user-request"
// - Subscriber count
// - Emission timeline
// - Values emitted
DevTools Installation:
  • Chrome: Install "RxJS DevTools" from Chrome Web Store
  • Firefox: Install "RxJS DevTools" from Firefox Add-ons
  • Setup: Instrument observables in development mode only
  • Performance: DevTools adds overhead, disable in production

18.2 tap Operator for Stream Debugging

Pattern Syntax Description Best Practice
Value Logging tap(val => console.log(val)) Log emitted values Use descriptive labels
Side Effect Debugging tap(() => doSomething()) Execute without modifying stream Keep side effects pure
Observer Debugging tap({ next, error, complete }) Log all notification types Track stream lifecycle
Conditional Logging tap(val => val && log(val)) Log based on conditions Reduce noise in logs
Performance Tracking tap(() => performance.mark()) Measure execution time Use Performance API

Example: Comprehensive tap debugging

// Create reusable debug tap operators
const debug = (tag: string) => <T>(source: Observable<T>) => {
  return source.pipe(
    tap({
      next: value => console.log(`[${tag}] Next:`, value),
      error: error => console.error(`[${tag}] Error:`, error),
      complete: () => console.log(`[${tag}] Complete`)
    })
  );
};

const debugTime = (tag: string) => <T>(source: Observable<T>) => {
  const startTime = performance.now();
  let emissionCount = 0;
  
  return source.pipe(
    tap({
      next: () => {
        emissionCount++;
        const elapsed = performance.now() - startTime;
        console.log(`[${tag}] Emission #${emissionCount} at ${elapsed.toFixed(2)}ms`);
      },
      complete: () => {
        const totalTime = performance.now() - startTime;
        console.log(`[${tag}] Completed: ${emissionCount} emissions in ${totalTime.toFixed(2)}ms`);
      }
    })
  );
};

// Usage in stream
this.http.get<User[]>('/api/users').pipe(
  debug('API Response'),
  map(users => users.filter(u => u.active)),
  debug('After Filter'),
  debugTime('Processing'),
  map(users => users.map(u => u.name)),
  debug('Final Result')
).subscribe();

Example: Stack trace capture with tap

// Capture stack trace at subscription point
const captureStack = (label: string) => <T>(source: Observable<T>) => {
  const stack = new Error().stack;
  
  return source.pipe(
    tap({
      error: (err) => {
        console.error(`[${label}] Error occurred`);
        console.error('Original stack:', stack);
        console.error('Error:', err);
      }
    })
  );
};

// Use to track where errors originate
const data$ = this.service.getData().pipe(
  captureStack('getData call'),
  map(data => data.value),
  captureStack('after map')
);

// When error occurs, you'll see:
// - Where the observable was created
// - Which operator failed
// - Full error context

18.3 Observable Inspector and Stream Visualization

Tool Description Feature Platform
RxViz Interactive marble diagram visualizer Real-time stream visualization Web (rxviz.com)
RxJS Marbles Live operator testing playground Test operator combinations Web (rxmarbles.com)
Console Utilities Custom console formatters Structured logging Browser DevTools
Performance Monitor Execution time tracking Identify slow operators Chrome DevTools
Memory Profiler Detect subscription leaks Track object retention Chrome DevTools

Example: Custom observable inspector

// Observable inspector utility
class ObservableInspector {
  private observations = new Map<string, ObservationData>();
  
  inspect<T>(name: string) {
    return (source: Observable<T>): Observable<T> => {
      const data: ObservationData = {
        name,
        emissions: [],
        subscriptions: 0,
        activeSubscriptions: 0,
        errors: [],
        completions: 0,
        startTime: Date.now()
      };
      
      this.observations.set(name, data);
      
      return new Observable<T>(subscriber => {
        data.subscriptions++;
        data.activeSubscriptions++;
        
        const subscription = source.subscribe({
          next: (value) => {
            data.emissions.push({
              value,
              timestamp: Date.now() - data.startTime
            });
            subscriber.next(value);
          },
          error: (err) => {
            data.errors.push({
              error: err,
              timestamp: Date.now() - data.startTime
            });
            data.activeSubscriptions--;
            subscriber.error(err);
          },
          complete: () => {
            data.completions++;
            data.activeSubscriptions--;
            subscriber.complete();
          }
        });
        
        return () => {
          data.activeSubscriptions--;
          subscription.unsubscribe();
        };
      });
    };
  }
  
  getReport(name?: string): string {
    if (name) {
      return this.formatReport(this.observations.get(name));
    }
    
    let report = 'Observable Inspector Report\n';
    report += '='.repeat(50) + '\n\n';
    
    this.observations.forEach((data, name) => {
      report += this.formatReport(data) + '\n\n';
    });
    
    return report;
  }
  
  private formatReport(data?: ObservationData): string {
    if (!data) return 'No data available';
    
    return `
Observable: ${data.name}
Subscriptions: ${data.subscriptions} (${data.activeSubscriptions} active)
Emissions: ${data.emissions.length}
Errors: ${data.errors.length}
Completions: ${data.completions}
Duration: ${Date.now() - data.startTime}ms

Emission Timeline:
${data.emissions.map(e => 
  `  ${e.timestamp}ms: ${JSON.stringify(e.value)}`
).join('\n')}

${data.errors.length > 0 ? 'Errors:\n' + data.errors.map(e => 
  `  ${e.timestamp}ms: ${e.error}`
).join('\n') : ''}
    `.trim();
  }
}

// Usage
const inspector = new ObservableInspector();

const data$ = interval(1000).pipe(
  take(5),
  inspector.inspect('interval-stream'),
  map(x => x * 2),
  inspector.inspect('after-map')
);

data$.subscribe();

// After completion, view report
setTimeout(() => {
  console.log(inspector.getReport());
}, 6000);

18.4 Console Logging Patterns for Observable Streams

Pattern Implementation Use Case Advantage
Grouped Logs console.group() Organize related emissions Hierarchical visualization
Colored Output %c CSS styling Differentiate streams Visual clarity
Table Format console.table() Display arrays/objects Structured view
Time Tracking console.time() Measure durations Performance insight
Trace Logs console.trace() Call stack visualization Track execution flow
Count Emissions console.count() Track emission frequency Detect duplicates

Example: Advanced logging patterns

// Styled console logger for observables
class ObservableLogger {
  private colors = {
    next: '#4CAF50',
    error: '#F44336',
    complete: '#2196F3',
    subscribe: '#FF9800',
    unsubscribe: '#9E9E9E'
  };
  
  log<T>(name: string, options: LogOptions = {}) {
    return (source: Observable<T>): Observable<T> => {
      const groupLabel = `🔵 Observable: ${name}`;
      
      return new Observable<T>(subscriber => {
        if (options.logSubscription) {
          console.log(
            `%c[${name}] Subscribe`,
            `color: ${this.colors.subscribe}; font-weight: bold`
          );
        }
        
        if (options.groupLogs) {
          console.group(groupLabel);
        }
        
        const startTime = performance.now();
        let emissionCount = 0;
        
        const subscription = source.subscribe({
          next: (value) => {
            emissionCount++;
            const elapsed = performance.now() - startTime;
            
            console.log(
              `%c▶ Next #${emissionCount}`,
              `color: ${this.colors.next}; font-weight: bold`,
              `(${elapsed.toFixed(2)}ms)`,
              value
            );
            
            if (options.tableFormat && typeof value === 'object') {
              console.table(value);
            }
            
            subscriber.next(value);
          },
          error: (err) => {
            const elapsed = performance.now() - startTime;
            
            console.log(
              `%c✖ Error`,
              `color: ${this.colors.error}; font-weight: bold`,
              `(${elapsed.toFixed(2)}ms)`
            );
            console.error(err);
            
            if (options.stackTrace) {
              console.trace('Error stack trace:');
            }
            
            if (options.groupLogs) {
              console.groupEnd();
            }
            
            subscriber.error(err);
          },
          complete: () => {
            const elapsed = performance.now() - startTime;
            
            console.log(
              `%c✓ Complete`,
              `color: ${this.colors.complete}; font-weight: bold`,
              `(${elapsed.toFixed(2)}ms, ${emissionCount} emissions)`
            );
            
            if (options.groupLogs) {
              console.groupEnd();
            }
            
            subscriber.complete();
          }
        });
        
        return () => {
          if (options.logUnsubscribe) {
            console.log(
              `%c[${name}] Unsubscribe`,
              `color: ${this.colors.unsubscribe}; font-weight: bold`
            );
          }
          subscription.unsubscribe();
        };
      });
    };
  }
}

// Usage
const logger = new ObservableLogger();

const users$ = this.http.get<User[]>('/api/users').pipe(
  logger.log('API Request', {
    groupLogs: true,
    logSubscription: true,
    logUnsubscribe: true,
    tableFormat: true,
    stackTrace: true
  }),
  map(users => users.filter(u => u.active)),
  logger.log('After Filter', { groupLogs: true })
);

users$.subscribe();

Example: Production-safe conditional logging

// Environment-aware logging
const logInDev = <T>(label: string) => {
  return (source: Observable<T>): Observable<T> => {
    // Only log in development
    if (!environment.production) {
      return source.pipe(
        tap({
          next: (val) => console.log(`[${label}] Next:`, val),
          error: (err) => console.error(`[${label}] Error:`, err),
          complete: () => console.log(`[${label}] Complete`)
        })
      );
    }
    
    return source;
  };
};

// Conditional logging based on criteria
const logIf = <T>(
  predicate: (value: T) => boolean,
  label: string
) => {
  return tap<T>(value => {
    if (predicate(value)) {
      console.log(`[${label}]`, value);
    }
  });
};

// Usage
const data$ = getData().pipe(
  logInDev('getData'),
  map(processData),
  logIf(data => data.length === 0, 'Empty Data Warning'),
  logIf(data => data.length > 100, 'Large Dataset Warning')
);

18.5 Stack Trace Preservation and Error Context

Technique Implementation Benefit Overhead
Error Enhancement Wrap errors with context Additional debugging info Low
Stack Capture new Error().stack at creation Original call site Medium
Error Chain Preserve original error Full error history Low
Source Maps TypeScript/bundler config Original source mapping None (build time)
Async Stack Zone.js integration Full async call stack High

Example: Enhanced error context

// Custom error class with context
class ObservableError extends Error {
  constructor(
    message: string,
    public context: {
      operatorName?: string;
      streamName?: string;
      subscriptionStack?: string;
      timestamp?: number;
      additionalData?: any;
    },
    public originalError?: Error
  ) {
    super(message);
    this.name = 'ObservableError';
    
    // Preserve original stack if available
    if (originalError?.stack) {
      this.stack = `${this.stack}\n\nCaused by:\n${originalError.stack}`;
    }
  }
}

// Operator to add error context
const withErrorContext = <T>(
  streamName: string,
  operatorName: string
) => {
  // Capture subscription stack
  const subscriptionStack = new Error().stack;
  
  return (source: Observable<T>): Observable<T> => {
    return source.pipe(
      catchError(err => {
        const enhancedError = new ObservableError(
          `Error in ${streamName}`,
          {
            operatorName,
            streamName,
            subscriptionStack,
            timestamp: Date.now()
          },
          err
        );
        
        return throwError(() => enhancedError);
      })
    );
  };
};

// Usage
const data$ = this.http.get('/api/data').pipe(
  withErrorContext('data-fetch', 'http.get'),
  map(data => data.value),
  withErrorContext('data-fetch', 'map'),
  filter(value => value !== null),
  withErrorContext('data-fetch', 'filter')
);

data$.subscribe({
  error: (err: ObservableError) => {
    console.error('Stream:', err.context.streamName);
    console.error('Operator:', err.context.operatorName);
    console.error('Subscription Stack:', err.context.subscriptionStack);
    console.error('Original Error:', err.originalError);
  }
});

Example: Zone.js async stack traces (Angular)

// Enable long stack trace in Angular development
// zone-flags.ts (loaded before zone.js)

// Development configuration
if (!environment.production) {
  // Enable long stack trace
  (window as any).__Zone_enable_long_stack_trace = true;
  
  // More detailed error messages
  (window as any).__zone_symbol__UNPATCHED_EVENTS = [];
}

// main.ts
import './zone-flags';
import 'zone.js';

// Now errors will show full async stack
const data$ = this.http.get('/api/data').pipe(
  switchMap(data => this.processData(data)),
  map(result => result.value)
);

// Error will show:
// 1. Where observable was created
// 2. All async boundaries crossed
// 3. Original error location
// 4. Full chain of operators

18.6 Production Debugging and Monitoring Strategies

Strategy Implementation Use Case Tools
Error Tracking Sentry, Rollbar integration Capture production errors Sentry, Rollbar, Bugsnag
Performance Monitoring Custom metrics, APM tools Track operator performance New Relic, DataDog
Analytics Events Track observable completions User behavior insights Google Analytics, Mixpanel
Health Checks Monitor critical streams Detect stream failures Custom health endpoints
Feature Flags Toggle debugging at runtime Debug specific users LaunchDarkly, Split
Replay Sessions Record user sessions Reproduce issues LogRocket, FullStory

Example: Production error tracking

// Sentry integration for RxJS
import * as Sentry from '@sentry/angular';

// Global error handler for observables
const captureObservableError = <T>(
  context: string
) => {
  return (source: Observable<T>): Observable<T> => {
    return source.pipe(
      catchError(error => {
        // Send to Sentry with context
        Sentry.captureException(error, {
          tags: {
            errorType: 'observable-error',
            context
          },
          extra: {
            timestamp: new Date().toISOString(),
            userAgent: navigator.userAgent
          }
        });
        
        // Re-throw or return fallback
        return throwError(() => error);
      })
    );
  };
};

// Usage in services
@Injectable()
export class DataService {
  getData(): Observable<Data> {
    return this.http.get<Data>('/api/data').pipe(
      captureObservableError('DataService.getData'),
      retry(2),
      catchError(err => {
        // Log additional context
        console.error('Data fetch failed after retries');
        return of({ fallback: true } as Data);
      })
    );
  }
}

Example: Performance monitoring

// Custom performance monitoring
class PerformanceMonitor {
  private metrics = new Map<string, Metric>();
  
  track<T>(operationName: string) {
    return (source: Observable<T>): Observable<T> => {
      return new Observable<T>(subscriber => {
        const startTime = performance.now();
        let emissionCount = 0;
        
        const subscription = source.subscribe({
          next: (value) => {
            emissionCount++;
            subscriber.next(value);
          },
          error: (err) => {
            this.recordMetric(operationName, {
              duration: performance.now() - startTime,
              emissionCount,
              status: 'error'
            });
            subscriber.error(err);
          },
          complete: () => {
            this.recordMetric(operationName, {
              duration: performance.now() - startTime,
              emissionCount,
              status: 'complete'
            });
            subscriber.complete();
          }
        });
        
        return () => subscription.unsubscribe();
      });
    };
  }
  
  private recordMetric(name: string, data: MetricData) {
    // Send to analytics service
    if (window.gtag) {
      window.gtag('event', 'observable_performance', {
        event_category: 'RxJS',
        event_label: name,
        value: Math.round(data.duration),
        custom_dimensions: {
          emissions: data.emissionCount,
          status: data.status
        }
      });
    }
    
    // Store locally for aggregation
    this.metrics.set(name, data);
  }
  
  getMetrics(): Map<string, Metric> {
    return this.metrics;
  }
  
  // Send metrics batch to server
  flush(): Observable<void> {
    const metrics = Array.from(this.metrics.entries());
    
    return this.http.post('/api/metrics', { metrics }).pipe(
      tap(() => this.metrics.clear()),
      map(() => undefined)
    );
  }
}

// Usage
const monitor = new PerformanceMonitor();

const data$ = this.http.get('/api/data').pipe(
  monitor.track('api-fetch'),
  map(transform),
  monitor.track('data-transform')
);

// Periodically flush metrics
interval(60000).pipe(
  switchMap(() => monitor.flush())
).subscribe();

Example: Feature-flag controlled debugging

// Dynamic debug toggle with feature flags
class DebugService {
  private debugEnabled$ = new BehaviorSubject<boolean>(false);
  
  constructor(
    private featureFlags: FeatureFlagService
  ) {
    // Check feature flag
    this.featureFlags.isEnabled('rxjs-debug').subscribe(enabled => {
      this.debugEnabled$.next(enabled);
    });
  }
  
  // Conditional debug operator
  debug<T>(label: string) {
    return (source: Observable<T>): Observable<T> => {
      return this.debugEnabled$.pipe(
        take(1),
        switchMap(enabled => {
          if (enabled) {
            return source.pipe(
              tap({
                next: val => console.log(`[${label}]`, val),
                error: err => console.error(`[${label}] Error:`, err),
                complete: () => console.log(`[${label}] Complete`)
              })
            );
          }
          return source;
        })
      );
    };
  }
  
  // Enable debug for specific user
  enableForUser(userId: string) {
    const storedDebugUsers = localStorage.getItem('debug-users') || '[]';
    const debugUsers = JSON.parse(storedDebugUsers);
    
    if (!debugUsers.includes(userId)) {
      debugUsers.push(userId);
      localStorage.setItem('debug-users', JSON.stringify(debugUsers));
      this.debugEnabled$.next(true);
    }
  }
}

// Usage
@Injectable()
export class UserService {
  constructor(private debug: DebugService) {}
  
  getUsers(): Observable<User[]> {
    return this.http.get<User[]>('/api/users').pipe(
      this.debug.debug('fetch-users'),
      map(users => users.filter(u => u.active)),
      this.debug.debug('filter-users')
    );
  }
}

// Enable debugging from console
// window.enableRxjsDebug('user-123');
Production Debugging Warnings:
  • ⚠️ Remove or disable DevTools instrumentation in production builds
  • ⚠️ Use conditional logging to avoid console spam
  • ⚠️ Implement error tracking to capture production issues
  • ⚠️ Monitor performance metrics to detect slow operators
  • ⚠️ Consider privacy when logging user data
Debugging Best Practices:
  • Development: Use DevTools, verbose logging, stack traces
  • Staging: Enable selective debugging, performance monitoring
  • Production: Error tracking, health checks, feature-flag controlled logs
  • Testing: Use marble diagrams, TestScheduler for deterministic tests
  • Monitoring: Track subscription counts, emission rates, error rates

Section 18 Summary

  • DevTools - browser extensions provide visual debugging, timeline views, subscription tracking
  • tap operator - essential for non-intrusive stream debugging and side-effect logging
  • Stream visualization - tools like RxViz and custom inspectors reveal observable behavior
  • Console patterns - structured logging with colors, groups, tables improves clarity
  • Stack traces - preserve error context and async call stacks for better debugging
  • Production monitoring - integrate Sentry, performance tracking, feature flags for live debugging