Eventos enviados pelo servidor


1Apresentação

Esta página documenta a funcionalidade de server-sent events do Temma, que possibilita uma comunicação unidirecional (do servidor para o navegador) em tempo real.

Atenção: se você está procurando a documentação sobre o gerenciamento de eventos assíncronos, veja as fontes de dados ZeroMQ, SQS e Beanstalk.

Quando um código Javascript se conecta a uma URL para receber SSEs (server-sent events), a conexão permanece aberta, de forma parecida com um websocket. Se a conexão for interrompida, o cliente se reconectará automaticamente.

O Temma introduziu um novo tipo de controlador, o controlador de eventos, derivado do objeto básico \Temma\Web\EventController. Esses controladores só podem ser usados para processar SSEs, e têm algumas diferenças em relação aos controladores convencionais (que derivam de \Temma\Web\Controller):

  • Eles são projetados para funcionar pelo tempo que for necessário.
  • Eles não executam uma visão ao final da execução, pois enviam mensagens de evento durante a execução.
  • Sem visão = sem template = sem variáveis de template.
  • Quando um valor é definido (da mesma forma como normalmente se definem variáveis de template), ele é imediatamente enviado ao cliente, que o recebe como um evento.

Observe que um grande número de conexões pode permanecer aberto (tantas quantos forem os clientes conectados ao site). Verifique se o seu servidor aceita um número suficiente de conexões simultâneas.


2Exemplo

Vamos imaginar uma página web bem simples. Ela inclui um código Javascript que vai se conectar à URL /message/fetch, e exibir os eventos do canal "demo" em uma lista.

Aqui está o código HTML da página:

<html>
<head>
    <!-- carregamento do código Javascript -->
    <script src="event-manager.js"></script>
</head>
<body>
    <!-- lista que vai conter as mensagens de evento -->
    <ul id="liste">
    </ul>
</body>
</html>

E o código Javascript (arquivo event-manager.js):

// conexão ao fluxo de eventos
const evtSource = new EventSource("/message/fetch");

// processa os eventos recebidos no canal "demo"
evtSource.addEventListener("demo", function(event) {
    // recupera o texto (serializado em JSON no evento)
    const str = JSON.parse(event.data);

    // cria um novo elemento de lista
    const newElement = document.createElement("li");
    // adiciona o texto do evento como conteúdo do elemento de lista
    newElement.textContent = str;

    // adiciona o elemento à lista (na página HTML)
    document.getElementById("liste").appendChild(newElement);
});

Aqui está o código do controlador que vai enviar os eventos (ele envia uma mensagem de texto a cada 2 segundos):

// controlador Message
class Message extends \Temma\Web\EventController {
    // ação fetch
    public function fetch() {
        $i = 1;
        // laço infinito
        while (true) {
            // envia um evento no canal "demo"
            $this['demo'] = "Mensagem n°$i";
            // incrementa o contador
            $i++;
            // espera 2 segundos antes de enviar a próxima mensagem
            sleep(2);
        }
    }
}

A cada dois segundos, o controlador enviará um evento, e o cliente adicionará a mensagem do evento à lista na página web.


3Princípios do controlador de eventos

Além das características específicas listadas acima (sem template ou visão), os controladores de eventos funcionam de forma bastante semelhante aos controladores convencionais. Aliás, o objeto \Temma\Web\EventController herda de \Temma\Web\Controller.

Assim, os conceitos de ação raiz, ação proxy e ação padrão permanecem idênticos, assim como a inicialização e a finalização dos controladores (veja a documentação dos controladores).

Os plugins também funcionam da mesma forma. Tenha em mente, no entanto, que os pós-plugins não serão processados a cada envio de evento, mas somente ao final da execução (sabendo que a execução pode ser interrompida pelo servidor devido a um tempo de execução excessivamente longo).


4Envio de eventos

Como visto no exemplo acima, para enviar um evento, basta definir um valor da mesma forma como normalmente se define uma variável de template, usando a escrita de array associativo. A chave é o nome do canal de evento, e o valor pode conter qualquer dado PHP (que será serializado em JSON).

Exemplos:

// envia duas mensagens de texto no canal "msg"
$this['msg'] = "Primeira mensagem";
$this['msg'] = "Segunda mensagem";

// envia uma lista de itens no canal "update"
$this['update'] = [
    123 => [
        'id'     => 123,
        'name'   => "Projeto Netuno",
        'author' => "Fulana de Tal",
    ],
    456 => [
        'id'     => 456,
        'name'   => "Projeto Plutão",
        'author' => "Fulano de Tal",
    ],
];

A cada envio de dados, o controlador primeiro verifica automaticamente se a conexão com o cliente ainda está aberta. Se não estiver, uma exceção \Temma\Exceptions\FlowQuit é lançada; se ela não for interceptada, isso levará à interrupção do processamento (veja a documentação do fluxo de execução).

Se a mensagem pôde ser enviada, o tempo máximo de execução do script é estendido. O valor usado é aquele configurado para a diretiva max_execution_time no arquivo php.ini. Se esse valor não estiver definido, o tempo máximo de execução é definido como 30 segundos. Se esse valor estiver definido como zero (tempo ilimitado), ele não é modificado.


5Gerenciamento de canais de eventos

É possível saber se mensagens já foram enviadas em um canal, e, em caso afirmativo, quantas.

Exemplos:

// um canal já foi usado?
if (isset($this['myChannel']))
    print("O canal 'myChannel' foi usado.");

// recupera o número de eventos enviados em um canal
$cnt = $this['myChannel'];

// reinicializa um canal (como se nunca tivesse sido usado)
unset($this['myChannel']);

6Métodos disponíveis nos controladores de eventos

Os controladores de eventos oferecem métodos específicos que podem ser usados no código das ações.

$this->_checkConnection()
Verifica se a conexão com o cliente ainda está aberta. Caso contrário, uma exceção \Temma\Exceptions\FlowQuit é lançada (veja a documentação do fluxo de execução).

$this->_renewTimeLimit()
Este método primeiro chama o método _checkConnection(), depois estende o tempo máximo de execução do script. O valor usado é aquele configurado para a diretiva max_execution_time no arquivo php.ini. Se esse valor não estiver definido, o tempo máximo de execução é definido como 30 segundos. Se esse valor estiver definido como zero (tempo ilimitado), ele não é modificado.

$this->_ping()
Envia um evento no canal ping, que contém um array associativo cuja chave time contém a data e a hora atuais no formato ISO 8601.