Skip to content

windowToggle - Ventana con control independiente de inicio y fin

El operador windowToggle controla el trigger de inicio y el trigger de fin con Observables separados, emitiendo cada período como un nuevo Observable. Es un operador de ventana avanzado que puede gestionar múltiples períodos de ventana simultáneamente.

🔰 Sintaxis básica y uso

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

const source$ = interval(500); // Emitir valores cada 0.5 segundos

// Trigger de inicio: cada 2 segundos
const opening$ = interval(2000);

// Trigger de fin: 1 segundo después del inicio
const closing = () => interval(1000);

source$.pipe(
  windowToggle(opening$, closing),
  mergeAll()
).subscribe(value => {
  console.log('Valor en ventana:', value);
});

// Inicia en 2s, termina en 3s → Valores: 4, 5
// Inicia en 4s, termina en 5s → Valores: 8, 9
// Inicia en 6s, termina en 7s → Valores: 12, 13

Flujo de operación:

  1. opening$ emite un valor → Inicio de ventana
  2. El Observable devuelto por closing() emite un valor → Fin de ventana
  3. Múltiples períodos de ventana pueden superponerse

🌐 Documentación oficial de RxJS - windowToggle

💡 Patrones de uso típicos

  • Recopilación de datos durante horario comercial
  • Registro de eventos durante presión de botón
  • Seguimiento de acciones durante sesión activa
  • Procesamiento de streams que requiere gestión dinámica de períodos

🔍 Diferencia con bufferToggle

OperadorSalidaCaso de uso
bufferToggleArray (T[])Procesar valores agrupados juntos
windowToggleObservable<T>Procesamiento de stream diferente por grupo
ts
import { interval } from 'rxjs';
import { bufferToggle, windowToggle, mergeAll } from 'rxjs';

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

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

// windowToggle - emite como Observable
source$.pipe(
  windowToggle(opening$, closing)
).subscribe(window$ => {
  console.log('Ventana (Observable):', window$);
  window$.subscribe(value => {
    console.log('  Valor en ventana:', value);
  });
});

🧠 Ejemplo de código práctico 1: Registro de eventos durante presión de botón

Ejemplo de registrar datos desde mousedown hasta mouseup.

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

// Crear botón
const button = document.createElement('button');
button.textContent = 'Mantener';
document.body.appendChild(button);

// Área de salida
const display = document.createElement('div');
display.style.marginTop = '10px';
document.body.appendChild(display);

// Stream de datos (cada 100ms)
const data$ = interval(100);

// Inicio: mousedown
const mouseDown$ = fromEvent(button, 'mousedown');

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

data$.pipe(
  windowToggle(mouseDown$, mouseUp),
  mergeMap(window$ => window$.pipe(toArray()))
).subscribe(events => {
  display.textContent = `Eventos registrados durante mantención: ${events.length} eventos`;
  console.log('Datos registrados:', events);
});

🎯 Ejemplo de código práctico 2: Recopilación de datos durante horario comercial

Ejemplo de recopilar datos de sensores desde la apertura hasta el cierre del negocio.

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

// Datos de sensor (adquisición continua)
const sensorData$ = interval(100).pipe(
  map(() => ({
    timestamp: Date.now(),
    temperature: 20 + Math.random() * 10, // 20-30 grados
    humidity: 40 + Math.random() * 20     // 40-60%
  }))
);

// Apertura del negocio: después de 2s, luego cada 10s
const businessOpen$ = timer(2000, 10000);

// Cierre del negocio: 5 segundos después del inicio
const businessClose = () => timer(5000);

let sessionNumber = 0;

sensorData$.pipe(
  windowToggle(businessOpen$, businessClose),
  mergeMap(window$ => {
    const current = ++sessionNumber;
    console.log(`Inicio de sesión comercial ${current}`);

    // Calcular información estadística de cada ventana
    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(`Sesión ${stats.session}: ${stats.count} muestras`);
  console.log(`  Temperatura promedio: ${stats.avgTemp.toFixed(1)}°C`);
  console.log(`  Humedad promedio: ${stats.avgHumidity.toFixed(1)}%`);
});

🎯 Ejemplo de uso: Gestión de período de descarga

Ejemplo de gestión de período de descarga de datos con botones de inicio y parada.

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

// Crear elementos UI
const startButton = document.createElement('button');
startButton.textContent = 'Iniciar';
document.body.appendChild(startButton);

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

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

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

// Stream de datos (generar datos de descarga cada segundo)
const downloadData$ = interval(1000).pipe(
  map(i => ({
    id: i,
    size: Math.floor(Math.random() * 1000) + 100, // 100-1100KB
    timestamp: new Date()
  }))
);

// Triggers de inicio y fin
const start$ = fromEvent(startButton, 'click');
const stop$ = new Subject<void>();

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

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

// Gestión de ventanas
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>Descarga completa</strong><br>
    Cantidad: ${downloads.length} archivos<br>
    Tamaño total: ${(totalSize / 1024).toFixed(2)} MB<br>
    Tamaño promedio: ${avgSize.toFixed(0)} KB
  `;
});

🎯 Períodos de ventana superpuestos

Como característica de windowToggle, puede gestionar múltiples períodos de ventana simultáneamente.

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

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

// Inicio: cada segundo
const opening$ = interval(1000);

// Fin: 1.5 segundos después del inicio
const closing = () => interval(1500);

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

// Salida:
// Ventana: [4, 5, 6, 7]       (Inicia en 1s → termina en 2.5s)
// Ventana: [9, 10, 11, 12]    (Inicia en 2s → termina en 3.5s)
// Ventana: [14, 15, 16, 17]   (Inicia en 3s → termina en 4.5s)

Timeline:

Fuente:    0--1--2--3--4--5--6--7--8--9--10-11-12-13-14-15-16-17-18-19
Inicio:    ----1s----2s----3s----4s
Período1:  [------1.5s-----]
           └→ Ventana1: [4,5,6,7]
Período2:         [------1.5s-----]
                  └→ Ventana2: [9,10,11,12]
Período3:                [------1.5s-----]
                         └→ Ventana3: [14,15,16,17]

⚠️ Puntos de atención

1. Gestión de suscripciones de ventanas

Como cada ventana es un Observable independiente, es necesario suscribirse explícitamente o aplanar con mergeAll(), etc.

ts
source$.pipe(
  windowToggle(opening$, closing)
).subscribe(window$ => {
  // Si no se suscribe a la ventana misma, no fluyen los valores
  window$.subscribe(value => {
    console.log('Valor:', value);
  });
});

2. Atención a fugas de memoria

Si el trigger de inicio es demasiado frecuente, muchas ventanas existirán simultáneamente, consumiendo memoria.

ts
// ❌ Mal ejemplo: inicio cada 100ms, fin después de 5s
const opening$ = interval(100); // Demasiado frecuente
const closing = () => interval(5000);

source$.pipe(
  windowToggle(opening$, closing)
).subscribe();
// Posibilidad de 50 ventanas simultáneas → Riesgo de memoria

// ✅ Buen ejemplo: configurar intervalo apropiado
const opening$ = interval(2000); // Cada 2 segundos
const closing = () => interval(1000); // 1 segundo

3. Superposición de períodos de ventana

Cuando los períodos de ventana se superponen, el mismo valor se incluye en múltiples ventanas. Confirmar si este es el comportamiento deseado.

ts
// Con superposición
opening$ = interval(1000);    // Inicia cada segundo
closing = () => interval(1500); // 1.5 segundos

// Sin superposición
opening$ = interval(2000);    // Inicia cada 2 segundos
closing = () => interval(1000); // 1 segundo

🆚 Comparación de operadores de la familia window

OperadorControlPeríodo de ventanaCaso de uso
windowEmisión de otro ObservableContinuoDivisión basada en eventos
windowTimeTiempo fijoContinuoDivisión basada en tiempo
windowCountCantidad fijaContinuoDivisión basada en cantidad
windowToggleControl separado inicio/finSuperposición posibleCondiciones inicio/fin complejas
windowWhenSolo control de finContinuoControl periódico simple

🔄 Diferencia con windowWhen

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

const source$ = interval(200);

// windowToggle: controlar inicio y fin por separado
source$.pipe(
  windowToggle(
    interval(1000),          // Trigger de inicio
    () => timer(500)         // Trigger de fin (500ms después del inicio)
  ),
  mergeAll()
).subscribe();

// windowWhen: controlar solo el timing de fin (siguiente inicia inmediatamente después del fin)
source$.pipe(
  windowWhen(() => timer(1000)), // Ventana cada segundo
  mergeAll()
).subscribe();
OperadorControlPeríodo de ventanaCaso de uso
windowToggle(open$, close)Control separado inicio/finSuperposición posibleCondiciones inicio/fin complejas
windowWhen(closing)Solo control de finContinuoVentana periódica simple

📚 Operadores relacionados

  • bufferToggle - Agrupar valores como array (versión de array de windowToggle)
  • window - División de ventana según timing de otro Observable
  • windowTime - División de ventana basada en tiempo
  • windowCount - División de ventana basada en cantidad
  • windowWhen - División de ventana con condición de cierre dinámica

Resumen

El operador windowToggle es una herramienta avanzada que controla inicio y fin de forma independiente y puede procesar cada período como un Observable independiente.

  • ✅ Control separado de inicio y fin posible
  • ✅ Puede gestionar múltiples ventanas simultáneamente
  • ✅ Puede aplicar diferentes procesos a cada ventana
  • ⚠️ Requiere gestión de suscripciones
  • ⚠️ Trigger de inicio frecuente consume memoria
  • ⚠️ Atención a superposición de períodos de ventana

Publicado bajo licencia CC-BY-4.0.