Skip to main content

VISÃO GERAL

Usa a mesma arquitetura da solução com SSE, mas move-se o processo Javascript para um Shared Worker que opera em background no browser. A vantagem é que pode manter uma única conexão ativa com o servidor que serve a várias abas simultaneamente. Nas situações em que o operador provavelmente vai abrir "várias abas na mesma ela", é uma boa solução. É altamente recomendável trabalhar com classes JS nesse caso pra encapsular as coisas e facilitar o controle de sessões (cada aba é uma sessão). A aba do Chrome e o Shared Worker se comunicam através de mensagens internas trocadas por "ports". O worker não tem acesso à DOM, e a aba não tem acesso ao Worker diretamente, então tudo é feito de forma indireta. Deve-se mexer em vários lugares: criar classe Worker, criar classe não-worker (aba), registrar handlers no Accelero.js e atualizar o accelero_worker.js. A arquitetura fica algo assim:

HTML

<table id="tabela">
<tbody><tr class="tableModel noshow">
...
</tbody>
</table>

JS - accelero_worker.js

...
self.importScripts("./worker/MeuWorker.js?r=1234");
...
acceleroWorker.addModule("MeuWorker", new MeuWorker(acceleroWorker));
...

JS - MeuWorker.js

class MeuWorker {
sessions = [];
eSource = null;
acceleroWorker;

constructor(acceleroWorker) {
this.acceleroWorker = acceleroWorker;
acceleroWorker.addHandler({key: "meuworker_start", callback: this.handleMeuWorkerStartRequest});
acceleroWorker.addHandler({key: "meuworker_stop", callback: this.handleMeuWorkerStopRequest});
}
startSharedWorker = () => {
//ENVIA MENSAGEM DE DEBUG PRA TODAS AS ABAS (Ports)
this.acceleroWorker.sendMessageToAllPorts({action: 'debug', message: 'Starting process for MeuWorker'});

//CHECA SE JÁ TEM UM SSE RODANDO, E INTERROMPE SE HOUVER
if (this.eSource) {
this.eSource.close();
this.eSource = null;
}

//SE NINGUÉM SE REGISTROU EM UMA SESSÃO, NÃO TEM NADA A FAZER
if (Object.keys(this.sessions).length === 0) {
console.log('No active sessions, aborting...');
return;
}

//MONTA ARRAY COM OS FILTROS DE CADA SESSION INDIVIDUALMENTE
let filters = {};
for (const session in this.sessions) {
filters[session] = {"filters": this.sessions[session].config};
}

//DEFINIÇÕES GERAIS
const endpoint = 'endpoint/workerSSEShared?sessions=' + encodeURIComponent(btoa(JSON.stringify(filters)));

//ABRE UMA CONEXÃO SSE COM O SERVIDOR
this.eSource = new EventSource(endpoint);

//DEFINE HANDLERS PRA CADA EVENT ESPECÍFICO
this.eSource.addEventListener('open', function(e) {
console.log("Connection was opened.")
});
this.eSource.addEventListener('abort', function(e) {
console.log("Connection was closed.")
this.eSource.close();
});
this.eSource.addEventListener('error', function(e) {
if (e.readyState == EventSource.CLOSED) {
this.acceleroWorker.sendMessageToAllPorts({action: 'meuworker_error'});
}
});
this.eSource.addEventListener('message', function(e) {
try {
//FAZ PARSE DOS DADOS ENVIADOS PELO SERVIDOR E EXTRAI O SESSIONID ESPECIFICADO
const data = JSON.parse(e.data);
const sessionID = data.sessionID;

//ENVIA OS DADOS PRA SESSÃO ESPECÍFICA QUE O SERVER ESPECIFICOU
this.sessions[sessionID].port.postMessage({action: "meuworker_monitor", log: data.row});
} catch (ex) {
}
});
}
handleMeuWorkerStopRequest = (e) => {
//QUANDO ALGUÉM SE DES-REGISTRA NESSE WORKER, É REMOVIDA A SESSÃO E REINICIADO O WORKER PRA AVISAR O SERVER.
const uid = this.acceleroWorker.getPortUID(e.srcElement);
delete (this.sessions[uid]);
this.startSharedWorker();
}
handleMeuWorkerStartRequest = (e) => {
//QUANDO ALGUÉM SE REGISTRA NESSE WORKER, É CRIADA UMA SESSÃO E REINICIADO O WORKER PRA AVISAR O SERVER.
const uid = this.acceleroWorker.getPortUID(e.srcElement);
this.sessions[uid] = {
port: e.srcElement,
config: e.data.filters,
sessionID: e.data.sessionID,
uid: uid,
};
this.startSharedWorker();
}
}

JS - arquivo principal disponível "na aba"

class MeuWorkerAba {
sessionID;
sessionStatus;
watchdog;

static async toggleMonitorMeuWorkerSharedWorker(table, toggleElement) {
//CHECA SE JÁ NÃO ESTÁ RODANDO NESSA ABA, E FAZ UM TOGGLE
if (MeuWorkerAba.sessionStatus) {
MeuWorker.stopMonitoring(toggleElement);
return;
}
MeuWorkerAba.sessionStatus = true;

//GERA UM SESSIONID RANDÔMICO
if (!MeuWorker.sessionID) MeuWorker.sessionID = SNFWHelpers.generateRandomString();

//CRIA UM WATCHDOG PRA VER SE O OPERADOR NÃO SAIU DA TELA
MeuWorkerAba.watchdog = window.setInterval(function () {
if (table.closest('body').length === 0) {
console.log('Not on screen - abort MeuWorker monitoring');
MeuWorkerAba.stopMonitoring(toggleElement);
}
}, 1000);

//POPULA OS DADOS DO FILTRO
const filters = await DataHandler.prepPOST($('.pesquisa_advanced[data-element="#MeuWorkerList"]'));
let urlFilters = {};
for (const filter of Object.keys(filters)) {
const val = filters[filter];
if (val === null || val.length === 0) continue;
const curFilter = filter.split(' ');
for (const f of curFilter) {
urlFilters[f] = val;
}
}

//POSTA UMA MENSAGEM PRO WORKER PEDINDO PRA INICIAR O MONITORAMENTO
window.sharedWorker.port.postMessage({
action: 'meuworker_start',
sessionID: MeuWorkerAba.sessionID,
filters: urlFilters
});
}
static async stopMonitoring(toggleElement) {
//POSTA UMA MENSAGEM PRO WORKER PEDINDO PRA INTERROMPER O MONITORAMENTO
window.sharedWorker.port.postMessage({
action: 'meuworker_stop',
sessionID: MeuWorkerAba.sessionID,
});

//INTERROMPE O WATCHDOG, VOLTA O BOTÃO DE TOGGLE PRO ESTADO ORIGINAL E MARCA QUE REALMENTE ESTÁ INTERROMPIDO O MONITORAMENTO
clearInterval(MeuWorkerAba.watchdog);
toggleElement.addClass('btn-success').removeClass('btn-danger').find('.monitor-status').text(SNFWDictionary.__t('generic.monitor', 'Monitorar'));
MeuWorkerAba.sessionStatus = false;
}
static handleIncomingMessage(e) {
const table = $('#MeuWorkerList');
const newTr = Table.addNewLineFromModel(null, table, true);
data = DataHandler.parseObj(e.data.log);
Table.fillTableRow(newTr, data);
newTr.removeClass('dirty');
table.prepend(newTr);
}
}

Accelero.js

function startSharedWorker() {
window.sharedWorker.port.onmessage = e => {
switch (e.data.action || null) {
...
case 'meuworker_monitor':
return MeuWorkerAba.handleIncomingMessage(e);
}
}
}

PHP

class ABC {
private readonly RequestInterface $request;
private array $sessions;

public function __construct(
private readonly SSEFlusher $sse,
private readonly MQTTClientInterface $mqtt,
) {}
public function handleMQTTMessage(Message $message) : void {
$data = $message->payload;
foreach ($this->sessions as $sessionID => $filters) {
if (!essaMensagemDeveSerEnviadaParaEssaSession($data, $sessionID, $filters)) continue;
$this->sse->send(json_encode($data));
}
}
public function workerSSESharedWorker(ServerRequestInterface $request, array $params) : never {
//GUARDA O REQUEST PARA TRATAR FILTROS
$this->request = $request;

//GUARDA AS SESSÕES
$sessions = json_decode(base64_decode((string)$request->getQueryParams()['sessions']), true);
if (!is_array($sessions)) exit;
$this->sessions = $sessions;

//CONFIGURA O MQTT - TÓPICO, CALLBACK, ETC...
$this->mqtt->onMessage([$this, 'handleMQTTMessage']);
$this->mqtt->subscribe('topic', qos: 0);
$mqtt->connectAndSubscribe();
ignore_user_abort(true);

//ENTRA NUM LOOP INFINITO OUVINDO MQTT E ENVIANDO KEEPALIVE A CADA 30 SEGUNDOS
try {
$keepaliveTimeout = 30;
$nextKeepalive = date('U', strtotime('+' . $keepaliveTimeout . ' seconds'));
while (true) {
if (connection_aborted()) throw new SSEJSONException('Client disconnected.');
$mqtt->loopOnce();
usleep(50_000);
if ($nextKeepalive > date('U')) continue;

$nextKeepalive = date('U', strtotime('+' . $keepaliveTimeout . ' seconds'));
$this->sse->send(data: '');
}
} catch (Exception $e) {
$this->mqtt->disconnect();
throw new SSEJSONException($e->getMessage());
}
}
}

PRÓS

  • funcionamento em tempo real
  • geralmente é bem mais leve para o servidor, especialmente quando múltiplas abas são abertas na mesma página

CONTRAS

  • terrível de debugar
  • complicado pra implementar
  • pode exigir algum tipo de middleware no backend pro worker "saber que há um novo evento" -- uma mensagem MQTT intermediária, por exemplo
  • Chrome mobile não tem suporte a Shared Worker. Se houver necessidade desse suporte, tem que haver uma "versão fallback" em SSE puro, o que significa trabalhar dobrado.