WebSocketStream: integra transmisiones con la API de WebSocket

Evita que tu aplicación se ahogue en los mensajes de WebSocket o sobrepase un servidor de WebSocket con mensajes mediante la aplicación de contrapresión.

Información general

La API de WebSocket

La API de WebSocket proporciona una interfaz de JavaScript para el protocolo WebSocket, 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 en la red y los procese según lo desee. Un concepto importante en el contexto de las transmisiones es la contrapresión. Este es el proceso mediante el cual una sola transmisión o una cadena de canalización regula la velocidad de lectura o escritura. Cuando la transmisión en sí o una transmisión posterior en la cadena de canalización 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

Aplicar contrapresión a los mensajes recibidos es imposible

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

Supongamos que tienes una aplicación que necesita realizar operaciones de procesamiento intensivo de datos cada vez que se recibe un mensaje nuevo. Es probable que configures el flujo de manera similar al siguiente código y, como await el resultado de la llamada a process(), deberías 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 manera de aplicar la contrapresión. Cuando los mensajes llegan más rápido de lo que el método process() puede controlarlos, el proceso de procesamiento llenará la memoria almacenando esos mensajes en búfer, dejará de responder debido al uso del 100% de la CPU, o ambos.

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

Es posible aplicar contrapresión a los mensajes enviados, pero implica sondear la propiedad WebSocket.bufferedAmount, que es ineficiente y no ergonómica. 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á subiendo.

¿Qué es la API de WebSocket?

La API de WebSocketStream resuelve el problema de la contrapresión inexistente o no ergonómica 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 WebSocket de ancho de banda alto que necesitan retener la interactividad, en particular, el uso compartido de video y pantalla.
  • Del mismo modo, las capturas de video y otras aplicaciones que generan una gran cantidad de 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. Crea la explicación | [Completo][explainer] | | 2. Crea el borrador inicial de la especificación | [En curso][spec] | | 3. Recopila comentarios e itera sobre el diseño | [En curso](#feedback) | | 4. Prueba de origen | [Completo][ot] | | 5. Lanzamiento | Sin iniciar |

Cómo usar la API de WebSocketStream

Ejemplo introductorio

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

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

En consecuencia, si llamas al método WritableStream.getWriter(), finalmente obtienes un WritableStreamDefaultWriter, en el que 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é pasa con la función de contrapresión prometida? Como escribí anteriormente, lo obtienes "de forma gratuita", sin pasos adicionales. Si process() tarda más, el siguiente mensaje solo se consumirá una vez que la canalización esté lista. Del mismo modo, el paso WritableStreamDefaultWriter.write() solo continuará si es seguro hacerlo.

Ejemplos avanzados

El segundo argumento de WebSocketStream es una bolsa de opciones para permitir extensiones futuras. Por el momento, la única opción es protocols, que se comporta igual 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 y el posible 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 cerrada de WebSocketStream

La información que estaba disponible en los eventos WebSocket.onclose y WebSocket.onerror en la API de WebSocket ahora está disponible a través de la promesa WebSocketStream.closed. En caso de un cierre no limpio, la promesa se rechaza. 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 CloseEvent.

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

Cierra una conexión de WebSocketStream

Un WebSocketStream se puede cerrar 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 progresivas

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

Detección de funciones

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

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 acerca de 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 sobre un problema de especificaciones en el repositorio de GitHub correspondiente o agrega tus ideas a un problema existente.

Informar un problema con la implementación

¿Encontraste un error en la implementación de Chrome? ¿O es diferente la implementación de las especificaciones? Informa un error en new.crbug.com. Asegúrate de incluir todos los detalles que puedas, así como instrucciones simples para reproducir el problema y, luego, ingresa Blink>Network>WebSockets en el cuadro Componentes. Glitch funciona muy bien para compartir casos de reproducción rápidos y fáciles.

Demuestra compatibilidad con la API

¿Planeas usar la API de WebSocket? Tu asistencia pública ayuda al equipo de Chrome a priorizar funciones y muestra a otros proveedores de navegadores lo fundamental que es admitirlas.

Envía un tuit a @ChromiumDev con el hashtag #WebSocketStream y cuéntanos dónde y cómo lo estás usando.

Vínculos útiles

Agradecimientos

Adam Rice y Yutaka Hirano implementaron la API de WebSocketStream. Hero image de Daan Mooij en Unsplash.