Oi
Meu nome é Sasha e trabalho como arquiteto na Tinkoff Business.
Neste artigo, quero falar sobre como superar a restrição de navegadores no número de conexões HTTP de longa duração abertas no mesmo domínio usando o service worker.
Se desejar, pule o plano de fundo, a descrição do problema, procure uma solução e prossiga imediatamente para o resultado.

Antecedentes
Era uma vez no Tinkoff Business, houve um bate-papo que funcionava no
Websocket .
Depois de algum tempo, ele deixou de se encaixar no design de sua conta pessoal e, em geral, estava pedindo uma reescrita de angular 1.6 para angular 2+. Decidi que era hora de começar a atualizá-lo. Um colega-colaborador descobriu que o front-end do bate-papo mudará e sugeriu ao mesmo tempo refazer a API, em particular - alterar o transporte de websocket para
SSE (eventos enviados pelo servidor) . Ele sugeriu isso porque, ao atualizar a configuração do NGINX, todas as conexões foram interrompidas, o que foi difícil de restaurar.
Discutimos a arquitetura da nova solução e chegamos à conclusão de que receberemos e enviaremos dados usando solicitações HTTP regulares. Por exemplo, envie uma
mensagem POST: / api / send-message , obtenha uma lista das caixas de diálogo
GET: / api / conversations-list e assim por diante. E eventos assíncronos como "uma nova mensagem do interlocutor" serão enviados via SSE. Portanto, aumentaremos a tolerância a falhas do aplicativo: se a conexão SSE cair, o bate-papo continuará funcionando, mas não receberá notificações em tempo real.
Além do bate-papo no websocket, estávamos perseguindo eventos para o componente "notificações finas". Este componente permite enviar várias notificações para a conta pessoal do usuário, por exemplo, que a importação de contas, que pode levar alguns minutos, foi concluída com êxito. Para abandonar completamente o websocket, movemos esse componente para uma conexão SSE separada.
O problema
Quando você abre uma guia do navegador, duas conexões SSE são criadas: uma para bate-papo e outra para notificações sutis. Bem, que eles sejam criados. Desculpe ou o que? Não sentimos muito, mas os navegadores sentem muito! Eles têm um
limite no número de conexões persistentes simultâneas para um domínio . Adivinha quanto está no Chrome? Certo, seis! Abri três guias - marquei todo o conjunto de conexões e você não pode mais fazer solicitações HTTP. Isso é verdade para o protocolo HTTP / 1.x. No HTTP / 2, não existe esse problema devido à multiplexação.
Existem algumas maneiras de resolver esse problema no nível da infraestrutura:
- Fragmento de domínio.
- HTTP / 2.
Ambos os métodos pareciam caros, já que muita infraestrutura teria que ser afetada.
Portanto, para iniciantes, tentamos resolver o problema no lado do navegador. A primeira idéia foi fazer algum tipo de transporte entre as guias, por exemplo, por meio da
API LocalStorage ou
Broadcast Channel .
O significado é o seguinte: abrimos as conexões SSE em apenas uma guia e enviamos os dados para o restante. Essa solução também não parecia ótima, pois exigiria o lançamento de todos os 50 SPA, que compõem a conta pessoal do Tinkoff Business. A liberação de 50 aplicativos também é cara, então continuei procurando outras maneiras.
Solução
Recentemente, trabalhei com trabalhadores de serviços e pensei: é possível aplicá-los nessa situação?
Para responder a essa pergunta, primeiro você precisa entender o que os funcionários de serviço geralmente fazem? Eles podem fazer pedidos de proxy, é algo como isto:
self.addEventListener('fetch', event => { const response = self.caches.open('example') .then(caches => caches.match(event.request)) .then(response => response || fetch(event.request)); event.respondWith(response); });
Ouvimos eventos para solicitações HTTP e respondemos como desejamos. Nesse caso, estamos tentando responder a partir do cache e, se não der certo, fazemos uma solicitação ao servidor.
Ok, vamos tentar interceptar a conexão SSE e responder:
self.addEventListener('fetch', event => { const {headers} = event.request; const isSSERequest = headers.get('Accept') === 'text/event-stream'; if (!isSSERequest) { return; } event.respondWith(new Response('Hello!')); });
Nas solicitações de rede, vemos a seguinte imagem:

E no console, isso:

Já não é ruim. A solicitação foi interceptada, mas o SSE não deseja uma resposta na forma de
texto / sem formatação , mas deseja
texto / fluxo de eventos . Como criar um fluxo agora? Mas posso responder com um fluxo de um trabalhador de serviço? Bem, vamos ver:

Ótimo! A classe
Response leva como um corpo
ReadableStream . Depois de ler a
documentação , você pode descobrir que o
ReadableStream possui um controlador que possui um método
enqueue () - com sua ajuda, você pode transmitir dados. Adequado, pegue!
self.addEventListener('fetch', event => { const {headers} = event.request; const isSSERequest = headers.get('Accept') === 'text/event-stream'; if (!isSSERequest) { return; } const responseText = 'Hello!'; const responseData = Uint8Array.from(responseText, x => x.charCodeAt(0)); const stream = new ReadableStream({start: controller => controller.enqueue(responseData)}); const response = new Response(stream); event.respondWith(response); });

Não há erro, a conexão trava no status pendente e nenhum dado chega no lado do cliente. Comparando minha solicitação com uma solicitação real do servidor, percebi que a resposta estava nos cabeçalhos. Para solicitações SSE, os seguintes cabeçalhos devem ser especificados:
const sseHeaders = { 'content-type': 'text/event-stream', 'Transfer-Encoding': 'chunked', 'Connection': 'keep-alive', };
Quando você adiciona esses cabeçalhos, a conexão é aberta com êxito, mas os dados não serão recebidos no lado do cliente. Isso é óbvio, já que você não pode enviar texto aleatório - deve haver algum formato.
No javascript.info,
o formato de dados no qual você deseja enviar dados do servidor está
bem descrito . Pode ser facilmente descrito com uma função:
const sseChunkData = (data: string, event?: string, retry?: number, id?: number): string => Object.entries({event, id, data, retry}) .filter(([, value]) => ![undefined, null].includes(value)) .map(([key, value]) => `${key}: ${value}`) .join('\n') + '\n\n';
Para estar em conformidade com o formato SSE, o servidor deve enviar mensagens separadas por uma quebra de linha dupla
\ n \ n .
A mensagem consiste nos seguintes campos:
- data - corpo da mensagem, vários dados seguidos são interpretados como uma mensagem, separados por quebras de linha \ n;
- id - atualiza a propriedade lastEventId enviada no cabeçalho Last-Event-ID ao reconectar;
- tente novamente - o atraso recomendado antes de reconectar em milissegundos, não pode ser definido usando JavaScript;
- event - o nome do evento do usuário, indicado antes dos dados.
Adicione os cabeçalhos necessários, altere a resposta para o formato desejado e veja o que acontece:
self.addEventListener('fetch', event => { const {headers} = event.request; const isSSERequest = headers.get('Accept') === 'text/event-stream'; if (!isSSERequest) { return; } const sseChunkData = (data, event, retry, id) => Object.entries({event, id, data, retry}) .filter(([, value]) => ![undefined, null].includes(value)) .map(([key, value]) => `${key}: ${value}`) .join('\n') + '\n\n'; const sseHeaders = { 'content-type': 'text/event-stream', 'Transfer-Encoding': 'chunked', 'Connection': 'keep-alive', }; const responseText = sseChunkData('Hello!'); const responseData = Uint8Array.from(responseText, x => x.charCodeAt(0)); const stream = new ReadableStream({start: controller => controller.enqueue(responseData)}); const response = new Response(stream, {headers: sseHeaders}); event.respondWith(response); });

Oh meu globo! Sim, fiz uma conexão SSE sem um servidor!
Resultado
Agora podemos interceptar com êxito a solicitação SSE e responder a ela sem ir além do navegador.
Inicialmente, a idéia era estabelecer uma conexão com o servidor, mas apenas uma coisa - e dele enviar dados para as guias. Vamos fazer isso!
self.addEventListener('fetch', event => { const {headers, url} = event.request; const isSSERequest = headers.get('Accept') === 'text/event-stream';
O mesmo código no github.Eu tenho uma solução bastante simples para uma tarefa não tão trivial. Mas, é claro, ainda existem muitas nuances. Por exemplo, você precisa fechar a conexão com o servidor ao fechar todas as guias, oferecer suporte total ao protocolo SSE e assim por diante.
Decidimos com sucesso tudo isso. Tenho certeza de que não será difícil para você!