AIアシスタントで開発した宝くじプラグインのMessageBusリアルタイムアップデートに関するヘルプ

皆さん、こんにちは。

Discourse の宝くじプラグインを開発しています。ユーザーは宝くじトピックに返信することで参加できます。完全開示: 私はプログラマーではなく、AI (Claude Code) の支援を受けて完全に構築しているため、基本的な概念を見落としている可能性があります。 基本的な機能は動作していますが、MessageBus を使用したリアルタイム更新に問題があります。正しいアプローチについて、ご指導いただけると幸いです。

実現したいこと

  • BBCode [lottery]...[/lottery] を使用して宝くじ投稿を作成する
  • 投稿内に参加者数を表示する宝くじカードを表示する
  • ユーザーが参加するために返信したときに、すべての視聴者がページをリロードせずに参加者数をリアルタイムで更新する
  • discourse-calendar プラグインがイベント情報を更新する方法と同様

現在の実装

バックエンド (Ruby)

Lottery モデル (app/models/lottery.rb):

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

  MessageBus.publish(channel, message)
end

LotteryParticipant モデル (app/models/lottery_participant.rb):

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

private

def publish_lottery_update
  self.lottery.publish_update!
end

バックエンドログは MessageBus.publish が正常に呼び出されていることを確認しています:

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

フロントエンド (JavaScript)

デコレーター (discourse-lottery-decorator.gjs):

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

  if (!post?.lottery_data) return;

  // すでに装飾されているか確認 (AI が 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" });

コンポーネント (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 がコールバックの手動バインディングを提案
      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);
  }
}

モデル (models/lottery.js):

export default class Lottery {
  @tracked _stats;

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

  updateFromLottery(lottery) {
    // stats を含むすべてのプロパティを更新
    this.stats = lottery.stats || {};
    // ... その他のプロパティ
  }
}

問題点

参加者のページのみがリアルタイムで更新され、他の視聴者のページはリフレッシュせずに更新されません。

動作すること:

  • :white_check_mark: ユーザーが参加するために返信すると、自身のページで参加者数のリアルタイム更新が表示される
  • :white_check_mark: バックエンドは MessageBus メッセージを正常に発行する
  • :white_check_mark: MessageBus メッセージが参加者のページで受信される (ログに「:bell: MessageBus message received!」と表示される)
  • :white_check_mark: モデルは新しい参加者数で更新される (ログにカウントが 0 から 1 に増加すると表示される)

動作しないこと:

  • :cross_mark: 宝くじトピックを視聴している他の視聴者は、カウントがリアルタイムで更新されるのを見ない
  • :cross_mark: 宝くじ作成者のページは更新されない
  • :cross_mark: 宝くじトピックが開いている他のユーザーのページは更新されない
  • すべて手動でページをリフレッシュして、更新された参加者数を確認する必要がある

さらに、コンソールでコンポーネントが複数回破棄され、再作成されているのがわかります:

[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

これは、decorateCookedElement が複数回トリガーされ、コンポーネントが破棄および再作成されていることを示唆しており、他の視聴者の MessageBus サブスクリプションが壊れている可能性があります。

質問

  1. 重複装飾を防ぐために、cooked 要素に data-decorated 属性を使用するのは正しい方法ですか? AI はこのアプローチを提案しましたが、公式ドキュメントで推奨されるパターンであることを確認できませんでした。

  2. 追跡されたモデルプロパティが変更されているにもかかわらず、UI が更新されないのはなぜですか? @tracked _statsthis.stats = lottery.stats を介して更新されるときに再レンダリングをトリガーするはずですが、表示される参加者数は変わりません。@tracked の使い方が間違っていますか?

  3. まったく異なるアプローチを使用すべきですか? AI に discourse-calendar パターンに従うように依頼しましたが、Discourse がリアルタイム更新を処理する方法について、基本的な何かを見落としているのかもしれません。

  4. 元の lotteryNode を削除することが問題を引き起こしていますか? AI は当初、削除しないように試みましたが、その結果、宝くじカードが CSS によって非表示になりました (.cooked > .discourse-lottery { display: none; })。より良いパターンはありますか?

プログラマーではないため、基本的な質問をしている可能性があります。ドキュメントや例へのポインタがあれば、非常に役立ちます!ありがとうございます!

「いいね!」 2