皆さん、こんにちは。
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 || {};
// ... その他のプロパティ
}
}
問題点
参加者のページのみがリアルタイムで更新され、他の視聴者のページはリフレッシュせずに更新されません。
動作すること:
ユーザーが参加するために返信すると、自身のページで参加者数のリアルタイム更新が表示される
バックエンドは MessageBus メッセージを正常に発行する
MessageBus メッセージが参加者のページで受信される (ログに「
MessageBus message received!」と表示される)
モデルは新しい参加者数で更新される (ログにカウントが 0 から 1 に増加すると表示される)
動作しないこと:
宝くじトピックを視聴している他の視聴者は、カウントがリアルタイムで更新されるのを見ない
宝くじ作成者のページは更新されない
宝くじトピックが開いている他のユーザーのページは更新されない- すべて手動でページをリフレッシュして、更新された参加者数を確認する必要がある
さらに、コンソールでコンポーネントが複数回破棄され、再作成されているのがわかります:
[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 サブスクリプションが壊れている可能性があります。
質問
-
重複装飾を防ぐために、cooked 要素に
data-decorated属性を使用するのは正しい方法ですか? AI はこのアプローチを提案しましたが、公式ドキュメントで推奨されるパターンであることを確認できませんでした。 -
追跡されたモデルプロパティが変更されているにもかかわらず、UI が更新されないのはなぜですか?
@tracked _statsはthis.stats = lottery.statsを介して更新されるときに再レンダリングをトリガーするはずですが、表示される参加者数は変わりません。@trackedの使い方が間違っていますか? -
まったく異なるアプローチを使用すべきですか? AI に discourse-calendar パターンに従うように依頼しましたが、Discourse がリアルタイム更新を処理する方法について、基本的な何かを見落としているのかもしれません。
-
元の
lotteryNodeを削除することが問題を引き起こしていますか? AI は当初、削除しないように試みましたが、その結果、宝くじカードが CSS によって非表示になりました (.cooked > .discourse-lottery { display: none; })。より良いパターンはありますか?
プログラマーではないため、基本的な質問をしている可能性があります。ドキュメントや例へのポインタがあれば、非常に役立ちます!ありがとうございます!