WebSocketStream: Integra transmisiones con la API de WebSocket

Aplica contrapresión para evitar que tu app se sature con mensajes de WebSocket o que un servidor de WebSocket se inunde con mensajes.

Fondo

La API de WebSocket proporciona una interfaz de JavaScript para el protocolo 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 basadas en eventos sin sondear el servidor para obtener una respuesta.

La API de Streams

La API de Streams permite que JavaScript acceda de forma programática a transmisiones de fragmentos de datos recibidos a través de la red y los procese según sea necesario. Un concepto importante en el contexto de los flujos es la contrapresión. Este es el proceso por el cual una sola transmisión o una cadena de tuberías regulan la velocidad de lectura o escritura. Cuando la transmisión en sí o una transmisión posterior en la cadena de canalización aún está ocupada y 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 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 que se llama cuando se recibe un mensaje del servidor.

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

// 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 contrapresión. Cuando los mensajes llegan más rápido de lo que el método process() puede controlarlos, 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 ambas cosas.

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

Es posible aplicar contrapresión a los mensajes enviados, pero esto implica sondear la propiedad WebSocket.bufferedAmount, lo que resulta ineficiente y poco ergonómico. Esta propiedad de solo lectura devuelve 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 aborda el problema de la contrapresión inexistente o no ergonómica integrando 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 WebSocket de alto ancho de banda que necesitan conservar la interactividad, en particular, las de video y uso compartido de pantalla.
  • De manera similar, la captura de video y otras aplicaciones que generan muchos datos en el navegador que deben subirse 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. Crea una explicación Completar
2. Crea el borrador inicial de la especificación En curso
3. Recopila comentarios y realiza iteraciones en el diseño En curso
4. Prueba de origen Completar
5. Lanzamiento No se inició

Cómo usar la API de WebSocketStream

La API de WebSocketStream se basa en promesas, lo que hace que trabajar con ella sea natural en el mundo moderno de JavaScript. Para comenzar, construye un nuevo WebSocketStream y pásale la URL del servidor WebSocket. Luego, esperas a que la conexión sea opened, lo que genera un ReadableStream o un WritableStream.

Si llamas al método ReadableStream.getReader(), finalmente obtendrás un ReadableStreamDefaultReader, del que podrás read() datos hasta que finalice el flujo, es decir, hasta que muestre un objeto de la forma {value: undefined, done: true}.

En consecuencia, al llamar al método WritableStream.getWriter(), finalmente obtienes un WritableStreamDefaultWriter, al que luego puedes write() 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é sucede con la función de contrapresión prometida? Lo obtienes "gratis", sin necesidad de realizar pasos adicionales. Si process() tarda más tiempo, el siguiente mensaje solo se consumirá cuando 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 una bolsa de opciones que permite la extensión futura. La única opción es protocols, que se comporta de la misma manera que el segundo argumento del constructor de WebSocket:

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

El protocol seleccionado, así como los posibles extensions, forman 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 la conexión falla.

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

Información sobre la conexión de WebSocketStream cerrada

La información que estaba disponible en los eventos WebSocket.onclose y WebSocket.onerror de 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

Un WebSocketStream se puede cerrar con un AbortController. Por lo tanto, pasa un AbortSignal al constructor WebSocketStream. AbortController.abort() solo funciona antes del handshake, no después.

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 propósito principal es permitir especificar el código y el motivo que se envían al servidor.

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

Mejora progresiva e interoperabilidad

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 contrapresión a los mensajes recibidos. Es posible aplicar contrapresión a los mensajes enviados, pero esto implica sondear la propiedad WebSocket.bufferedAmount, lo que resulta ineficiente y poco ergonómico.

Detección de características

Para verificar si se admite la API de WebSocketStream, 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 tu experiencia con la API de WebSocketStream.

Cuéntanos sobre el diseño de la API

¿Hay algo sobre la API que no funciona 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 especificación en el repositorio de GitHub correspondiente o agrega tus comentarios a un problema existente.

Informa 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? Presenta un error en new.crbug.com. Asegúrate de incluir tantos detalles como sea posible, instrucciones sencillas para reproducir el error y, luego, ingresa Blink>Network>WebSockets en el cuadro Components.

Cómo mostrar compatibilidad con la API

¿Planeas usar la API de WebSocketStream? Tu apoyo público ayuda al equipo de Chrome a priorizar funciones y 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

La API de WebSocketStream fue implementada por Adam Rice y Yutaka Hirano.