Skip to content

windowToggle - Window with Independent Start and End Control ​

The windowToggle operator controls start trigger and end trigger with separate Observables, issuing each period as a new Observable. This is an advanced window operator that can manage multiple window periods simultaneously.

🔰 Basic Syntax and Usage ​

ts
import { interval } from 'rxjs';
import { windowToggle, mergeAll } from 'rxjs';

const source$ = interval(500); // Emit values every 0.5 seconds

// Start trigger: every 2 seconds
const opening$ = interval(2000);

// End trigger: 1 second after start
const closing = () => interval(1000);

source$.pipe(
  windowToggle(opening$, closing),
  mergeAll()
).subscribe(value => {
  console.log('Value in window:', value);
});

// Starts at 2 sec, ends at 3 sec → Values: 4, 5
// Starts at 4 sec, ends at 5 sec → Values: 8, 9
// Starts at 6 sec, ends at 7 sec → Values: 12, 13

Flow of operation:

  1. opening$ emits a value → Window starts
  2. Observable returned by closing() emits a value → Window ends
  3. Multiple window periods can overlap

🌐 RxJS Official Documentation - windowToggle

💡 Typical Usage Patterns ​

  • Data collection during business hours
  • Event recording during button presses
  • Action tracking during active sessions
  • Stream processing requiring dynamic period management

🔍 Difference from bufferToggle ​

OperatorOutputUse Case
bufferToggleArray (T[])Process grouped values together
windowToggleObservable<T>Different stream processing for each group
ts
import { interval } from 'rxjs';
import { bufferToggle, windowToggle, mergeAll } from 'rxjs';

const source$ = interval(500);
const opening$ = interval(2000);
const closing = () => interval(1000);

// bufferToggle - Output as array
source$.pipe(
  bufferToggle(opening$, closing)
).subscribe(values => {
  console.log('Buffer (array):', values);
  // Output: Buffer (array): [4, 5]
});

// windowToggle - Output as Observable
source$.pipe(
  windowToggle(opening$, closing)
).subscribe(window$ => {
  console.log('Window (Observable):', window$);
  window$.subscribe(value => {
    console.log('  Value in window:', value);
  });
});

🧠 Practical Code Example 1: Recording Events During Button Press ​

This is an example of recording data between mouse down and mouse up.

ts
import { fromEvent, interval } from 'rxjs';
import { windowToggle, mergeMap, toArray, take } from 'rxjs';

// Create button
const button = document.createElement('button');
button.textContent = 'Hold';
document.body.appendChild(button);

// Output area
const display = document.createElement('div');
display.style.marginTop = '10px';
document.body.appendChild(display);

// Data stream (every 100ms)
const data$ = interval(100);

// Start: Mouse down
const mouseDown$ = fromEvent(button, 'mousedown');

// End: Mouse up
const mouseUp = () => fromEvent(document, 'mouseup').pipe(take(1));

data$.pipe(
  windowToggle(mouseDown$, mouseUp),
  mergeMap(window$ => window$.pipe(toArray()))
).subscribe(events => {
  display.textContent = `Events recorded during hold: ${events.length} items`;
  console.log('Recorded data:', events);
});

🎯 Practical Code Example 2: Data Collection During Business Hours ​

This is an example of collecting sensor data from the start of business hours to the end of business hours.

ts
import { interval, timer } from 'rxjs';
import { windowToggle, mergeMap, scan, map } from 'rxjs';

// Sensor data (always acquiring)
const sensorData$ = interval(100).pipe(
  map(() => ({
    timestamp: Date.now(),
    temperature: 20 + Math.random() * 10, // 20-30 degrees
    humidity: 40 + Math.random() * 20     // 40-60%
  }))
);

// Business open: after 2 seconds, then every 10 seconds
const businessOpen$ = timer(2000, 10000);

// Business close: 5 seconds after start
const businessClose = () => timer(5000);

let sessionNumber = 0;

sensorData$.pipe(
  windowToggle(businessOpen$, businessClose),
  mergeMap(window$ => {
    const current = ++sessionNumber;
    console.log(`Business session ${current} started`);

    // Calculate statistics for each window
    return window$.pipe(
      scan((stats, data) => ({
        count: stats.count + 1,
        totalTemp: stats.totalTemp + data.temperature,
        totalHumidity: stats.totalHumidity + data.humidity
      }), { count: 0, totalTemp: 0, totalHumidity: 0 }),
      map(stats => ({
        session: current,
        count: stats.count,
        avgTemp: stats.totalTemp / stats.count,
        avgHumidity: stats.totalHumidity / stats.count
      }))
    );
  })
).subscribe(stats => {
  console.log(`Session ${stats.session}: ${stats.count} samples`);
  console.log(`  Average temperature: ${stats.avgTemp.toFixed(1)}°C`);
  console.log(`  Average humidity: ${stats.avgHumidity.toFixed(1)}%`);
});

🎯 Practical Example: Download Period Management ​

This is an example of managing data download periods with start and stop buttons.

ts
import { interval, fromEvent, Subject } from 'rxjs';
import { windowToggle, mergeMap, toArray, map } from 'rxjs';

// Create UI elements
const startButton = document.createElement('button');
startButton.textContent = 'Start';
document.body.appendChild(startButton);

const stopButton = document.createElement('button');
stopButton.textContent = 'Stop';
stopButton.disabled = true;
stopButton.style.marginLeft = '10px';
document.body.appendChild(stopButton);

const status = document.createElement('div');
status.style.marginTop = '10px';
status.textContent = 'Waiting...';
document.body.appendChild(status);

const result = document.createElement('div');
result.style.marginTop = '10px';
document.body.appendChild(result);

// Data stream (generate download data every 1 second)
const downloadData$ = interval(1000).pipe(
  map(i => ({
    id: i,
    size: Math.floor(Math.random() * 1000) + 100, // 100-1100KB
    timestamp: new Date()
  }))
);

// Start and stop triggers
const start$ = fromEvent(startButton, 'click');
const stop$ = new Subject<void>();

fromEvent(stopButton, 'click').subscribe(() => {
  stop$.next();
  status.textContent = 'Stopped';
  startButton.disabled = false;
  stopButton.disabled = true;
});

start$.subscribe(() => {
  status.textContent = 'Downloading...';
  startButton.disabled = true;
  stopButton.disabled = false;
});

// Window management
downloadData$.pipe(
  windowToggle(start$, () => stop$),
  mergeMap(window$ => window$.pipe(toArray()))
).subscribe(downloads => {
  const totalSize = downloads.reduce((sum, d) => sum + d.size, 0);
  const avgSize = downloads.length > 0 ? totalSize / downloads.length : 0;

  result.innerHTML = `
    <strong>Download Complete</strong><br>
    Count: ${downloads.length} items<br>
    Total size: ${(totalSize / 1024).toFixed(2)} MB<br>
    Average size: ${avgSize.toFixed(0)} KB
  `;
});

🎯 Overlapping Window Periods ​

One feature of windowToggle is that it can manage multiple window periods simultaneously.

ts
import { interval } from 'rxjs';
import { windowToggle, mergeMap, toArray, take } from 'rxjs';

const source$ = interval(200).pipe(take(20)); // 0-19

// Start: every 1 second
const opening$ = interval(1000);

// End: 1.5 seconds after start
const closing = () => interval(1500);

source$.pipe(
  windowToggle(opening$, closing),
  mergeMap(window$ => window$.pipe(toArray()))
).subscribe(values => {
  console.log('Window:', values);
});

// Output:
// Window: [4, 5, 6, 7]       (Starts at 1 sec → Ends at 2.5 sec)
// Window: [9, 10, 11, 12]    (Starts at 2 sec → Ends at 3.5 sec)
// Window: [14, 15, 16, 17]   (Starts at 3 sec → Ends at 4.5 sec)

Timeline:

Source:    0--1--2--3--4--5--6--7--8--9--10-11-12-13-14-15-16-17-18-19
Start:     ----1sec----2sec----3sec----4sec
Period 1:  [------1.5sec-----]
            └→ Window 1: [4,5,6,7]
Period 2:         [------1.5sec-----]
                   └→ Window 2: [9,10,11,12]
Period 3:                [------1.5sec-----]
                          └→ Window 3: [14,15,16,17]

⚠️ Notes ​

1. Window Subscription Management ​

Each window is an independent Observable, so you must either subscribe to it explicitly or flatten it with mergeAll() or similar.

ts
source$.pipe(
  windowToggle(opening$, closing)
).subscribe(window$ => {
  // Values won't flow unless you subscribe to the window itself
  window$.subscribe(value => {
    console.log('Value:', value);
  });
});

2. Watch Out for Memory Leaks ​

If start triggers are too frequent, many windows will exist at the same time, consuming memory.

ts
// ❌ Bad example: Start every 100ms, end after 5 seconds
const opening$ = interval(100); // Too frequent
const closing = () => interval(5000);

source$.pipe(
  windowToggle(opening$, closing)
).subscribe();
// Up to 50 windows can exist simultaneously → Memory risk

// ✅ Good example: Set appropriate interval
const opening$ = interval(2000); // Every 2 seconds
const closing = () => interval(1000); // For 1 second

3. Overlapping Window Periods ​

Overlapping window periods result in the same value being contained in multiple windows. Check if this is the intended behavior.

ts
// With overlap
opening$ = interval(1000);    // Start every 1 second
closing = () => interval(1500); // For 1.5 seconds

// Without overlap
opening$ = interval(2000);    // Start every 2 seconds
closing = () => interval(1000); // For 1 second

🆚 Comparison of window Operators ​

OperatorControlWindow PeriodUse Case
windowAnother Observable emitsContinuousEvent-driven partitioning
windowTimeFixed time intervalContinuousTime-based partitioning
windowCountFixed countContinuousCount-based partitioning
windowToggleSeparate start/end controlCan overlapComplex start/end conditions
windowWhenEnd only controlContinuousSimple periodic control

🔄 Difference from windowWhen ​

ts
import { interval, timer } from 'rxjs';
import { windowToggle, windowWhen, mergeAll } from 'rxjs';

const source$ = interval(200);

// windowToggle: Separate control of start and end
source$.pipe(
  windowToggle(
    interval(1000),          // Start trigger
    () => timer(500)         // End trigger (500ms after start)
  ),
  mergeAll()
).subscribe();

// windowWhen: Control only end timing (next starts immediately after end)
source$.pipe(
  windowWhen(() => timer(1000)), // Window every 1 second
  mergeAll()
).subscribe();
OperatorControlWindow PeriodUse Case
windowToggle(open$, close)Separate start/end controlCan overlapComplex start/end conditions
windowWhen(closing)End only controlContinuousSimple periodic window
  • bufferToggle - Collect values as array (array version of windowToggle)
  • window - Split window at different Observable timings
  • windowTime - Time-based window splitting
  • windowCount - Count-based window splitting
  • windowWhen - Window splitting with dynamic closing conditions

Summary ​

The windowToggle operator is an advanced tool that allows you to control the start and end independently and treat each period as an independent Observable.

  • ✅ Start and end can be controlled separately
  • ✅ Multiple windows can be managed simultaneously
  • ✅ Different processing can be applied to each window
  • ⚠️ Subscription management required
  • ⚠️ Frequent start triggers consume memory
  • ⚠️ Be aware of overlapping window periods

Lançado sob a licença CC-BY-4.0.