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
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
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
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
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
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
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
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
// 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.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.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
);
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!
// 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!
}
@ 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 ();
}
});
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
// 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);
});
});
// 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
// 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
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
// 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\n Caused 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 );
})
);
}
}
// 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