前回の記事で、ステータス投稿時に投稿者のプロフィールをキャッシュする実装を行った。しかし、これだと他ユーザーが投稿したステータス(Firehose経由で受信)の投稿者プロフィールがキャッシュされず、display nameが表示されないという課題があった。
今回は、前回の記事の最後で挙げた「ステータス投稿イベント監視時に、その投稿者のプロフィールを取得してキャッシュする」アプローチを実装した。
実装内容はこれ。
分散プロトコルにおけるプロフィール取得
AT Protocolは分散プロトコルであり、各ユーザーのデータは異なるPDS (Personal Data Server) に保存されている。そのため、あるユーザーのプロフィールを取得するには、まず「そのユーザーのデータがどこにあるか」を知る必要がある。
DID と DID Document
AT Protocolでは、ユーザーは DID (Decentralized Identifier)
で識別される。例えば did:plc:z72i7hdynmk6r22z27h6tvur
のような形式だ。
DIDを解決すると、DID Documentが得られる。これにはそのユーザーのPDSのURLが含まれている:
{
"id": "did:plc:z72i7hdynmk6r22z27h6tvur",
"service": [
{
"id": "#atproto_pds",
"type": "AtprotoPersonalDataServer",
"serviceEndpoint": "https://bsky.social"
}
],
...
}プロフィール取得の流れ
Firehoseからステータスイベントを受信した時、イベントには
authorDid
しか含まれていない。プロフィールを取得するには以下の手順が必要になる:
1. DID解決: did:plc:xxx → DID Document
2. PDS特定: DID Document → serviceEndpoint (例: https://bsky.social)
3. プロフィール取得: PDS に getRecord を呼び出し
コードで書くとこんな感じ:
// 1. DID Document を解決
const didDoc = await idResolver.did.resolve(did)
// 2. PDS の URL を取得
const pdsService = didDoc.service?.find(
(s) => s.id === '#atproto_pds'
)
const pdsUrl = pdsService.serviceEndpoint // "https://bsky.social"
// 3. 認証なしの Agent でプロフィールを取得
const agent = new Agent({ service: pdsUrl })
const profile = await agent.com.atproto.repo.getRecord({
repo: did,
collection: 'app.bsky.actor.profile',
rkey: 'self',
})従来のWebアプリケーション開発では「ユーザーデータは自分のDBにある」という前提だったが、分散プロトコルでは「ユーザーデータは世界中に散らばっている」という前提になる。これが根本的な発想の違いだと感じる。
認証なしでプロフィールが取得できる理由
上記のコードでは認証情報を渡していない。これで動くのは、AT Protocolのリポジトリが公開データとして設計されているため。
Repository仕様には以下のように明記されている:
"Public atproto content (records) is stored in per-account repositories... current repository contents are publicly available"
つまり、リポジトリに保存される「レコード」はすべて公開される:
app.bsky.actor.profile(プロフィール)app.bsky.feed.post(投稿)xyz.statusphere.status(ステータス)
一方、app.bsky.actor.preferences
のようなユーザー設定は、リポジトリではなくPDS上の別の領域に保存される。これらは専用のAPI
(app.bsky.actor.getPreferences)
でのみ取得でき、認証が必要。
公開か非公開かを見分けるには、Lexicon定義で `type: "record"` かどうかを確認すればよい。Lexicon仕様では、record型は "Specifies schema of data objects stored in Repositories" と定義されている。
例えば profile.json
を見ると "type": "record"
となっているため、リポジトリに保存され公開される。一方、getPreferences.json
は "type": "query" であり、リポジトリには保存されない。
Firehose処理をブロックしない
今回の実装で気をつけたのは、プロフィール取得がFirehoseのイベント処理をブロックしないようにすること。
Firehoseは大量のイベントをリアルタイムで受信するため、各イベントの処理は高速に完了させる必要がある。プロフィール取得はネットワークI/Oを伴うため、同期的に実行するとFirehose全体の処理が詰まってしまう。
そのため、プロフィール取得は非同期で実行し、完了を待たずに次のイベント処理に進むようにした:
// ステータス保存後
if (!profileCached) {
// await しない = Firehose処理をブロックしない
fetchAndCacheProfile(did, db, idResolver, logger).catch(() => {})
}プロフィール取得に失敗しても、ステータス自体は正常に保存される。display nameは次回のページ読み込み時に表示されればよいので、この程度の遅延は許容範囲とする。
まとめ
今回の実装を通じて、分散プロトコル上でのデータ取得の考え方を学んだ:
- DID解決が必要: ユーザーのデータがどのサーバーにあるか、DID Documentから特定する
- 公開データモデル: プロフィールなどの公開データは認証なしで取得可能
- 非同期処理の重要性: Firehoseのようなリアルタイムストリームでは、ブロッキング処理を避ける
前回の記事で挙げたもう一つのアプローチ「スキーマにdisplay nameを含める」は、AT Protocolの分散性を考えると筋が悪いと判断した。プロフィールは頻繁に更新される可能性があり、ステータスレコードに埋め込むと古いデータが残り続けてしまう。必要な時に最新のプロフィールを取得する今回のアプローチの方が、分散プロトコルの特性に合っていると思う。