Need help with MessageBus real-time updates in lottery plugin (developed with AI assistance)

Hi everyone,

I’m developing a lottery plugin for Discourse where users can participate by replying to a lottery topic. Full disclosure: I’m not a programmer - I’m building this entirely with AI assistance (Claude Code), so I may be missing fundamental concepts. The basic functionality works, but I’m having issues with real-time updates using MessageBus. I’d appreciate any guidance on the correct approach.

What I’m trying to achieve

  • Create a lottery post using BBCode [lottery]...[/lottery]
  • Display a lottery card in the post showing participant count
  • When users reply to participate, all viewers should see the participant count update in real-time without page refresh
  • Similar to how the discourse-calendar plugin updates event information

Current implementation

Backend (Ruby)

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

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

  MessageBus.publish(channel, message)
end

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

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

private

def publish_lottery_update
  self.lottery.publish_update!
end

Backend logs confirm MessageBus.publish is called successfully:

[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;

  // Check if already decorated (AI suggested using data attribute)
  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" });

Component (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;

      // AI suggested manually binding the 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);
  }
}

Model (models/lottery.js):

export default class Lottery {
  @tracked _stats;

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

  updateFromLottery(lottery) {
    // Update all properties including stats
    this.stats = lottery.stats || {};
    // ... other properties
  }
}

The problem

Only the participant’s page updates in real-time, but other viewers’ pages don’t update without refreshing.

What works:

  • :white_check_mark: When a user replies to participate, their own page shows the participant count update in real-time
  • :white_check_mark: Backend publishes MessageBus messages successfully
  • :white_check_mark: MessageBus message is received on the participant’s page (logs show “:bell: MessageBus message received!”)
  • :white_check_mark: Model updates with new participant count (logs show count increases from 0 to 1)

What doesn’t work:

  • :cross_mark: Other viewers watching the lottery topic don’t see the count update in real-time
  • :cross_mark: The lottery creator’s page doesn’t update
  • :cross_mark: Any other user’s page that has the lottery topic open doesn’t update
  • They all need to manually refresh the page to see the updated participant count

Additionally, I see the component being destroyed and recreated multiple times in the 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

This suggests decorateCookedElement is being triggered multiple times, causing components to be destroyed and recreated, which might be breaking the MessageBus subscription for other viewers.

Questions

  1. Is using data-decorated attribute on the cooked element the correct way to prevent duplicate decoration? The AI suggested this approach, but I couldn’t find official documentation confirming it’s the recommended pattern.

  2. Why is the UI not updating even though the tracked model property changes? The @tracked _stats should trigger re-render when updated via this.stats = lottery.stats, but the displayed participant count stays the same. Is there something wrong with how I’m using @tracked?

  3. Should I be using a different approach entirely? I asked the AI to follow the discourse-calendar pattern, but maybe I’m missing something fundamental about how Discourse handles real-time updates.

  4. Is removing the original lotteryNode causing the issue? The AI initially tried not removing it, but that caused the lottery card to be hidden by CSS (.cooked > .discourse-lottery { display: none; }). Is there a better pattern?

Since I’m not a programmer, I may be asking basic questions - any pointers to documentation or examples would be incredibly helpful! Thank you!