Skip to main content
O zapo é desenhado para que um único processo conduza muitas contas a partir de uma store compartilhada. Cada conta vive atrás de um sessionId estável; tudo que é seguro compartilhar (o pool de conexão do backend, a factory do WebSocket, o logger) é compartilhado, e tudo que é específico da conta (sessões Signal, identidades, app-state, mailbox) é particionado por sessionId.

O padrão

sessionId é a chave durável de uma conta — o mesmo id entre restarts retoma o mesmo device pareado. Mudá-lo orfana as credenciais anteriores.

O que é por sessão vs compartilhado

Migrar para multi-tenant é (1) instanciar N WaClients na mesma store, e (2) dimensionar o pool de backend + o orçamento de memória para N sessões concorrentes.

Ciclo de vida da sessão

store.session(sessionId) é memoizado. A primeira chamada materializa o bundle por domínio (locks por sessão, wrappers de cache opcionais, …) e o cacheia dentro da store; chamadas posteriores com o mesmo id retornam o mesmo bundle.
O WaClient chama store.session(sessionId) sob demanda; você normalmente não o invoca.

Adicionando tenants em runtime

Não há etapa de pré-registro — basta construir um novo WaClient com um novo sessionId:

Removendo tenants

Para processos multi-tenant longos, três opções — cada uma com escopo diferente:
  • await client.logout() — remoção lógica. Apaga o estado persistente daquele sessionId (sujeito a logoutStoreClear) e desvincula o device no servidor. O bundle continua no map interno da store até você destruí-lo ou o processo terminar; um WaClient({ store, sessionId }) posterior com o mesmo id reutiliza o mesmo bundle.
  • await storeSession.destroy() — reclaim mid-process. Derruba as stores por domínio da sessão e evicta o bundle do map interno (o sessionId é liberado assim que a destruição começa), então um store.session(id) concorrente ou posterior constrói um bundle fresco. O teardown é idempotente — chamadas repetidas aguardam o mesmo promise in-flight — e falhas de teardown são logadas, nunca lançadas.
  • await store.destroy() — shutdown do processo. Derruba toda sessão viva e os backends registrados. store.session() lança erro depois; a store é single-shot.
Para um reset limitado que mantém a sessão viva, await storeSession.destroyCaches() troca os domínios de cache (retry / groupMetadata / deviceList / messageSecret) por instâncias frescas em vez de fechá-los — útil quando você quer dropar entries stale de um backend de cache persistente sem perder a sessão. Resets concorrentes são serializados e referências de cache capturadas antes da chamada rejeitam depois, então recrie o client para pegar os novos cache stores.
session(id) retorna a mesma instância de bundle enquanto ele está vivo — então um WaClient segurando uma referência stale antes de você destruir a sessão continua batendo no bundle fechado. Na prática: destrua a conexão do client primeiro (client.disconnect()), depois session.destroy(), depois construa um novo client se você quer o mesmo sessionId de volta.

Propriedade entre processos

Em deploys multi-processo, decida como os sessionIds mapeiam pra processos:
  • Um processo por sessionId via hash consistente / roteamento sticky no load balancer ou queue (mais simples).
  • Eleição de líder antes de abrir o client (advisory lock do Postgres, Redis SET NX, lease etcd) — útil pra failover HA.
A opção cacheLayer aperta isso: seu L1 não tem canal de invalidação entre processos, então as linhas de backend de um sessionId devem ser donas de um único processo durante todo o lifecycle. O L1 de um processo que assume começa frio e pode servir leituras stale até pegar as escritas que o dono anterior fez.

Compartilhando um media processor

WaMediaProcessor é um wrapper stateless sobre seus binários de mídia (sharp, ffmpeg/ffprobe, file-type). A mesma instância pode servir todos os WaClients — não há estado por sessão dentro do processor, então reutilizá-lo evita pagar o custo de lookup / lazy-import dos binários N vezes.
Cada método do processor recebe um argumento opcional ctx: WaMediaProcessorCallContext carregando o Logger daquela chamada. O runtime preenche com o logger da sessão chamadora, então warnings (binário ausente, detectMimetype que falhou, …) caem nos bindings corretos por sessão automaticamente — sem setup. Processors customizados devem consumir ctx.logger por chamada e não cacheá-lo, já que a mesma instância é compartilhada entre sessões.

Orçamento de memória

Os caps em WaCreateStoreOptions.memory.limits valem por sessão. Com N sessões concorrentes, a RAM in-process no pior caso escala linearmente: Ajuste os caps por sessão para baixo conforme N cresce, ou mova o mailbox / domínios de alta cardinalidade para um backend persistente (o provider in-memory existe para testes e contas pequenas). Os TTLs em memory.cacheTtlMs são independentes de N — eles só limitam por quanto tempo uma entry sobrevive em cada cache.

Estratégias de sharding

@zapo-js/store-sqlite é single-host e o arquivo SQLite é segurado por um processo — escolha um dos backends de rede para qualquer layout com mais de um processo.

Shutdown gracioso

client.disconnect() faz flush da fila de write-behind por sessão e fecha o socket sem desvincular o device, então o próximo boot retoma a partir da store. store.destroy() então libera o backend compartilhado (pool, file handle, …). Chamar disconnect() em todos os clients antes de store.destroy() garante que as escritas pendentes de cada sessão sejam flushed; store.destroy() não faz isso por você.
Não substitua logout() por disconnect() aqui — logout() desvincula o device server-side e limpa o estado armazenado. Use-o só quando você intencionalmente quer remover a conta.

Veja também

  • Stores — o modelo de persistência por sessionId e a camada opcional de read-through cache.
  • Produção & deploy — checklist operacional mais amplo (logging, timeouts, segurança).
  • Reconnection — a política de reconexão é por sessão; não existe loop de reconexão compartilhado.
Última modificação em 27 de julho de 2026