WebSocketStream: integra transmisiones con la API de WebSocket

Aplica la contrapresión para evitar que tu app se ahogue en los mensajes de WebSocket o que sature un servidor de WebSocket de mensajes.

Segundo plano

La API de WebSocket proporciona una interfaz de JavaScript para el protocolo de WebSocket, lo que permite abrir una sesión de comunicación interactiva bidireccional entre el navegador del usuario y un servidor. Con esta API, puedes enviar mensajes a un servidor y recibir respuestas controladas por eventos sin sondear el servidor para obtener una respuesta.

La API de Streams

La API de Streams permite que JavaScript acceda de manera programática a flujos de fragmentos de datos recibidos a través de la red y los procese como desees. Un concepto importante en el contexto de transmisiones es la contrapresión. Este es el proceso mediante el cual una sola transmisión o cadena de tuberías regula la velocidad de lectura o escritura. Cuando la transmisión en sí o una transmisión posterior en la cadena de canalizaciones sigue ocupada y aún no está lista para aceptar más fragmentos, envía una señal hacia atrás a través de la cadena para ralentizar la entrega según corresponda.

El problema con la API de WebSocket actual

Es imposible aplicar la contrapresión a los mensajes recibidos.

Con la API de WebSocket actual, la reacción a un mensaje se produce en WebSocket.onmessage, un EventHandler al que se llama cuando se recibe un mensaje del servidor.

Supongamos que tienes una aplicación que necesita realizar operaciones de procesamiento de datos pesadas cada vez que se recibe un mensaje nuevo. Es probable que debas configurar el flujo de manera similar al siguiente código y, como await el resultado de la llamada a process(), deberías estar bien.

// A heavy data crunching operation.
const process = async (data) => {
  return new Promise((resolve) => {
    window.setTimeout(() => {
      console.log('WebSocket message processed:', data);
      return resolve('done');
    }, 1000);
  });
};

webSocket.onmessage = async (event) => {
  const data = event.data;
  // Await the result of the processing step in the message handler.
  await process(data);
};

Incorrecto. El problema con la API de WebSocket actual es que no hay forma de aplicar la contrapresión. Cuando los mensajes llegan más rápido de lo que el método process() puede manejarlos, el proceso de renderización llenará la memoria almacenando en búfer esos mensajes, dejará de responder debido al uso del 100% de la CPU o ambos.

Aplicar la contrapresión a los mensajes enviados no es ergonómico.

Es posible aplicar la contrapresión a los mensajes enviados, pero implica sondear la propiedad WebSocket.bufferedAmount, lo que es ineficiente y no ergonómico. Esta propiedad de solo lectura muestra la cantidad de bytes de datos que se pusieron en cola con llamadas a WebSocket.send(), pero que aún no se transmitieron a la red. Este valor se restablece a cero una vez que se envían todos los datos en cola, pero si sigues llamando a WebSocket.send(), seguirá aumentando.

¿Qué es la API de WebSocketStream?

La API de WebSocketStream soluciona el problema de la contrapresión inexistente o no ergonómico mediante la integración de transmisiones con la API de WebSocket. Esto significa que la contrapresión se puede aplicar "de forma gratuita" sin ningún costo adicional.

Casos de uso sugeridos para la API de WebSocketStream

Estos son algunos ejemplos de sitios que pueden usar esta API:

  • Aplicaciones de WebSocket de ancho de banda alto que necesitan conservar la interactividad, en particular el video y el uso compartido de pantalla.
  • Del mismo modo, la captura de video y otras aplicaciones que generan muchos datos en el navegador que se deben subir al servidor. Con la contrapresión, el cliente puede dejar de producir datos en lugar de acumularlos en la memoria.

Estado actual

Paso Estado
1. Crear explicación Completar
2. Crea el borrador inicial de la especificación En curso
3. Recopilar comentarios e iterar el diseño En curso
4. Prueba de origen Completar
5. Lanzamiento Sin iniciar

Cómo usar la API de WebSocketStream

La API de WebSocketStream se basa en promesas, lo que hace que trabajar con ella sea algo natural en un mundo moderno de JavaScript. Para comenzar, debes crear un WebSocketStream nuevo y pasarle la URL del servidor WebSocket. A continuación, esperas a que la conexión sea opened, lo que genera un ReadableStream o un WritableStream.

Cuando llamas al método ReadableStream.getReader(), obtienes un ReadableStreamDefaultReader, del cual puedes obtener datos read() hasta que finalice la transmisión, es decir, hasta que se muestre un objeto con el formato {value: undefined, done: true}.

En consecuencia, cuando llamas al método WritableStream.getWriter(), finalmente obtienes un WritableStreamDefaultWriter, del que puedes luego write() obtener datos.

  const wss = new WebSocketStream(WSS_URL);
  const {readable, writable} = await wss.opened;
  const reader = readable.getReader();
  const writer = writable.getWriter();

  while (true) {
    const {value, done} = await reader.read();
    if (done) {
      break;
    }
    const result = await process(value);
    await writer.write(result);
  }

Contrapresión

¿Qué pasa con la función de contrapresión prometida? Lo obtiene "gratis", sin necesidad de realizar pasos adicionales. Si process() lleva más tiempo, el siguiente mensaje solo se consume una vez que la canalización está lista. Del mismo modo, el paso WritableStreamDefaultWriter.write() solo continúa si es seguro hacerlo.

Ejemplos avanzados

El segundo argumento de WebSocketStream es un paquete de opciones para permitir una extensión futura. La única opción es protocols, que se comporta de la misma manera que el segundo argumento para el constructor de WebSocket:

const chatWSS = new WebSocketStream(CHAT_URL, {protocols: ['chat', 'chatv2']});
const {protocol} = await chatWSS.opened;

El protocol seleccionado y los extensions potenciales son parte del diccionario disponible a través de la promesa WebSocketStream.opened. Esta promesa proporciona toda la información sobre la conexión en vivo, ya que no es relevante si falla la conexión.

const {readable, writable, protocol, extensions} = await chatWSS.opened;

Información sobre la conexión cerrada de WebSocketStream

La información que estaba disponible a partir de los eventos WebSocket.onclose y WebSocket.onerror en la API de WebSocket ahora está disponible a través de la promesa WebSocketStream.closed. La promesa se rechaza en caso de un cierre incorrecto. De lo contrario, se resuelve en el código y el motivo que envió el servidor.

Todos los códigos de estado posibles y su significado se explican en la lista de códigos de estado de CloseEvent.

const {code, reason} = await chatWSS.closed;

Cómo cerrar una conexión de WebSocketStream

Se puede cerrar un WebSocketStream con un AbortController. Por lo tanto, pasa un AbortSignal al constructor WebSocketStream.

const controller = new AbortController();
const wss = new WebSocketStream(URL, {signal: controller.signal});
setTimeout(() => controller.abort(), 1000);

Como alternativa, también puedes usar el método WebSocketStream.close(), pero su objetivo principal es permitir que se especifique el código y el motivo que se envía al servidor.

wss.close({code: 4000, reason: 'Game over'});

Interoperabilidad y mejora progresiva

Actualmente, Chrome es el único navegador que implementa la API de WebSocketStream. Para la interoperabilidad con la API de WebSocket clásica, no es posible aplicar la contrapresión a los mensajes recibidos. Es posible aplicar la contrapresión a los mensajes enviados, pero implica sondear la propiedad WebSocket.bufferedAmount, que es ineficiente y no ergonómico.

Detección de funciones

Para verificar si la API de WebSocketStream es compatible, usa lo siguiente:

if ('WebSocketStream' in window) {
  // `WebSocketStream` is supported!
}

Demostración

En los navegadores compatibles, puedes ver la API de WebSocketStream en acción en el iframe incorporado o directamente en Glitch.

Comentarios

El equipo de Chrome quiere conocer tus experiencias con la API de WebSocketStream.

Cuéntanos sobre el diseño de la API

¿Existe algún aspecto de la API que no funcione como esperabas? ¿O faltan métodos o propiedades que necesitas para implementar tu idea? ¿Tienes alguna pregunta o comentario sobre el modelo de seguridad? Informa un problema de especificaciones en el repositorio de GitHub correspondiente o agrega tus ideas sobre un problema existente.

Informar un problema con la implementación

¿Encontraste un error en la implementación de Chrome? ¿O la implementación es diferente de la especificación? Informa un error en new.crbug.com. Asegúrate de incluir la mayor cantidad de detalles posible, así como instrucciones simples para la reproducción, y, luego, ingresa Blink>Network>WebSockets en el cuadro Componentes. Glitch funciona muy bien para compartir casos de reproducción de forma rápida y fácil.

Cómo mostrar compatibilidad con la API

¿Piensas usar la API de WebSocketStream? Tu apoyo público ayuda al equipo de Chrome a priorizar las funciones y les muestra a otros proveedores de navegadores lo importante que es admitirlas.

Envía un tweet a @ChromiumDev con el hashtag #WebSocketStream y cuéntanos dónde y cómo lo usas.

Vínculos útiles

Agradecimientos

Adam Rice y Yutaka Hirano implementaron la API de WebSocketStream.