Ayuda con actualizaciones en tiempo real de MessageBus en plugin de lotería (desarrollado con ayuda de IA)

Hola a todos,

Estoy desarrollando un plugin de lotería para Discourse donde los usuarios pueden participar respondiendo a un tema de lotería. Divulgación completa: No soy programador; estoy construyendo esto completamente con ayuda de IA (Claude Code), así que es posible que me falten conceptos fundamentales. La funcionalidad básica funciona, pero tengo problemas con las actualizaciones en tiempo real usando MessageBus. Agradecería cualquier orientación sobre el enfoque correcto.

Lo que intento lograr

  • Crear una publicación de lotería usando BBCode [lottery]...[/lottery]
  • Mostrar una tarjeta de lotería en la publicación que muestre el recuento de participantes
  • Cuando los usuarios responden para participar, todos los espectadores deberían ver el recuento de participantes actualizarse en tiempo real sin necesidad de actualizar la página.
  • Similar a cómo el plugin discourse-calendar actualiza la información de los eventos.

Implementación actual

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

Los registros del backend confirman que MessageBus.publish se llama correctamente:

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

Frontend (JavaScript)

Decorador (discourse-lottery-decorator.gjs):

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

  if (!post?.lottery_data) return;

  // Comprobar si ya está decorado (la IA sugirió usar el atributo data)
  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;

      // La IA sugirió vincular manualmente la devolución de llamada
      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) {
    // Actualizar todas las propiedades, incluidas las estadísticas
    this.stats = lottery.stats || {};
    // ... otras propiedades
  }
}

El problema

Solo la página del participante se actualiza en tiempo real, pero las páginas de otros espectadores no se actualizan sin refrescar.

Lo que funciona:

  • :white_check_mark: Cuando un usuario responde para participar, su propia página muestra la actualización del recuento de participantes en tiempo real.
  • :white_check_mark: El backend publica mensajes de MessageBus correctamente.
  • :white_check_mark: El mensaje de MessageBus se recibe en la página del participante (los registros muestran “:bell: MessageBus message received!”).
  • :white_check_mark: El modelo se actualiza con el nuevo recuento de participantes (los registros muestran que el recuento aumenta de 0 a 1).

Lo que no funciona:

  • :cross_mark: Otros espectadores que ven el tema de la lotería no ven el recuento actualizarse en tiempo real.
  • :cross_mark: La página del creador de la lotería no se actualiza.
  • :cross_mark: La página de cualquier otro usuario que tenga abierto el tema de la lotería no se actualiza.
  • Todos ellos necesitan actualizar manualmente la página para ver el recuento de participantes actualizado.

Además, veo que el componente se destruye y se recrea varias veces en la consola:

[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

Esto sugiere que decorateCookedElement se está activando varias veces, lo que provoca que los componentes se destruyan y se recrean, lo que podría estar rompiendo la suscripción a MessageBus para otros espectadores.

Preguntas

  1. ¿Es el uso del atributo data-decorated en el elemento cocido la forma correcta de evitar la decoración duplicada? La IA sugirió este enfoque, pero no pude encontrar documentación oficial que lo confirme como el patrón recomendado.

  2. ¿Por qué no se actualiza la interfaz de usuario a pesar de que la propiedad del modelo rastreado cambia? El @tracked _stats debería desencadenar la nueva renderización cuando se actualiza a través de this.stats = lottery.stats, pero el recuento de participantes que se muestra sigue siendo el mismo. ¿Hay algo mal en cómo uso @tracked?

  3. ¿Debería estar usando un enfoque diferente por completo? Le pedí a la IA que siguiera el patrón de discourse-calendar, pero tal vez me falte algo fundamental sobre cómo Discourse maneja las actualizaciones en tiempo real.

  4. ¿Eliminar el lotteryNode original está causando el problema? La IA inicialmente intentó no eliminarlo, pero eso hizo que la tarjeta de lotería se ocultara con CSS (.cooked > .discourse-lottery { display: none; }). ¿Hay un patrón mejor?

Dado que no soy programador, puede que esté haciendo preguntas básicas; ¡cualquier indicación de documentación o ejemplos sería increíblemente útil! ¡Gracias!

2 Me gusta