Preciso de ajuda com atualizações em tempo real do MessageBus no plugin de loteria (desenvolvido com auxílio de IA)

Olá a todos,

Estou desenvolvendo um plugin de loteria para o Discourse, onde os usuários podem participar respondendo a um tópico de loteria. Divulgação completa: não sou programador - estou construindo isso inteiramente com assistência de IA (Claude Code), então posso estar perdendo conceitos fundamentais. A funcionalidade básica funciona, mas estou tendo problemas com atualizações em tempo real usando o MessageBus. Agradeceria qualquer orientação sobre a abordagem correta.

O que estou tentando alcançar

  • Criar uma postagem de loteria usando BBCode [lottery]...[/lottery]
  • Exibir um cartão de loteria na postagem mostrando a contagem de participantes
  • Quando os usuários responderem para participar, todos os espectadores devem ver a contagem de participantes atualizar em tempo real sem a necessidade de atualizar a página
  • Semelhante a como o plugin discourse-calendar atualiza as informações do evento

Implementação atual

Backend (Ruby)

Modelo Lottery (app/models/lottery.rb):

def publish_update!
  channel = "/lottery/#{self.post.topic_id}"
  message = { id: self.id }

  MessageBus.publish(channel, message)
end

Modelo LotteryParticipant (app/models/lottery_participant.rb):

after_commit :publish_lottery_update, on: [:create, :destroy]

private

def publish_lottery_update
  self.lottery.publish_update!
end

Os logs do backend confirmam que o MessageBus.publish é chamado com sucesso:

[Lottery] 📡 Publishing MessageBus update:
  Channel: /lottery/27
  Message: {:id=>52}
[Lottery] ✅ MessageBus.publish completed

Frontend (JavaScript)

Decorator (discourse-lottery-decorator.gjs):

api.decorateCookedElement((cooked, helper) => {
  const post = helper.getModel();

  if (!post?.lottery_data) return;

  // Verifica se já foi decorado (IA sugeriu usar atributo de dados)
  if (cooked.dataset.lotteryDecorated === "true") return;

  const lotteryNode = cooked.querySelector(".discourse-lottery");
  if (!lotteryNode) return;

  const wrapper = document.createElement("div");
  lotteryNode.before(wrapper);

  const lottery = Lottery.create(post.lottery_data);

  helper.renderGlimmer(
    wrapper,
    `<template><DiscourseLottery @lotteryData={{lottery}} /></template>`
  );

  lotteryNode.remove();
  cooked.dataset.lotteryDecorated = "true";
}, { id: "discourse-lottery" });

Componente (discourse-lottery/index.gjs):

export default class DiscourseLottery extends Component {
  @service messageBus;
  @service lotteryApi;

  constructor() {
    super(...arguments);

    const { lotteryData } = this.args;
    if (lotteryData?.topicId) {
      this.lotteryPath = `/lottery/${lotteryData.topicId}`;
      this.lotteryData = lotteryData;

      // IA sugeriu vincular manualmente o callback
      this._boundOnLotteryUpdate = this._onLotteryUpdate.bind(this);
      this.messageBus.subscribe(this.lotteryPath, this._boundOnLotteryUpdate);

      registerDestructor(this, () => {
        this.messageBus.unsubscribe(this.lotteryPath, this._boundOnLotteryUpdate);
      });
    }
  }

  async _onLotteryUpdate(msg) {
    console.log('[Lottery Component] 🔔 MessageBus message received!');

    const updatedData = await this.lotteryApi.lottery(this.lotteryData.topicId);
    this.lotteryData.updateFromLottery(updatedData);
  }
}

Modelo (models/lottery.js):

export default class Lottery {
  @tracked _stats;

  set stats(stats) {
    this._stats = LotteryStats.create(stats || {});
  }

  updateFromLottery(lottery) {
    // Atualiza todas as propriedades, incluindo estatísticas
    this.stats = lottery.stats || {};
    // ... outras propriedades
  }
}

O problema

Apenas a página do participante é atualizada em tempo real, mas as páginas de outros espectadores não são atualizadas sem recarregar.

O que funciona:

  • :white_check_mark: Quando um usuário responde para participar, a própria página dele mostra a contagem de participantes atualizando em tempo real
  • :white_check_mark: O backend publica mensagens do MessageBus com sucesso
  • :white_check_mark: A mensagem do MessageBus é recebida na página do participante (os logs mostram “:bell: MessageBus message received!”)
  • :white_check_mark: O modelo é atualizado com a nova contagem de participantes (os logs mostram a contagem aumentando de 0 para 1)

O que não funciona:

  • :cross_mark: Outros espectadores que estão visualizando o tópico da loteria não veem a contagem atualizar em tempo real
  • :cross_mark: A página do criador da loteria não atualiza
  • :cross_mark: A página de qualquer outro usuário que tenha o tópico da loteria aberto não atualiza
  • Todos eles precisam atualizar a página manualmente para ver a contagem de participantes atualizada

Além disso, vejo o componente sendo destruído e recriado várias vezes no console:

[Lottery Component] ✅ MessageBus subscription established
[Lottery Component] 🧹 Cleaning up MessageBus for topic: 27
[Lottery Component] ✅ MessageBus subscription established
[Lottery Component] 🧹 Cleaning up MessageBus for topic: 27

Isso sugere que decorateCookedElement está sendo acionado várias vezes, fazendo com que os componentes sejam destruídos e recriados, o que pode estar quebrando a assinatura do MessageBus para outros espectadores.

Perguntas

  1. Usar o atributo data-decorated no elemento cozido é a maneira correta de evitar decoração duplicada? A IA sugeriu essa abordagem, mas não consegui encontrar documentação oficial confirmando que é o padrão recomendado.

  2. Por que a interface do usuário não está atualizando, mesmo que a propriedade do modelo rastreada mude? O @tracked _stats deve acionar a renderização quando atualizado via this.stats = lottery.stats, mas a contagem de participantes exibida permanece a mesma. Há algo errado na forma como estou usando @tracked?

  3. Devo usar uma abordagem completamente diferente? Pedi à IA para seguir o padrão do discourse-calendar, mas talvez eu esteja perdendo algo fundamental sobre como o Discourse lida com atualizações em tempo real.

  4. Remover o lotteryNode original está causando o problema? A IA inicialmente tentou não removê-lo, mas isso fez com que o cartão da loteria ficasse oculto pelo CSS (.cooked > .discourse-lottery { display: none; }). Existe um padrão melhor?

Como não sou programador, posso estar fazendo perguntas básicas - qualquer indicação de documentação ou exemplos seria incrivelmente útil! Obrigado!

2 curtidas