Presence
Presence は「今この部屋に誰がいて、何をしているか」を表します。
共有ドキュメントのカーソル位置、ヘッダーのアバター、typing インジケーター
— リアルタイムコラボレーション的なもの全般です。plasma は presence を
標準搭載しており、WebSocket poke と同じ SyncCoordinator Durable Object
に相乗りしています。
2 つの端点があります: クライアントは userInfo payload と
onPresence コールバックで subscribe、サーバーは Worker から
SyncCoordinator を export します。
クライアント
Section titled “クライアント”createWebSocketSubscription に presence 用のオプションを渡します:
import { createPlasmaClient, createWebSocketSubscription } from "@sh1n4ps/plasma-client"
const plasma = createPlasmaClient({ schema, mutators, endpoint: "/sync", clientGroupID: user.id, schemaVersion: SCHEMA_VERSION, getContext: async () => ({ userId: user.id }),
subscribe: createWebSocketSubscription({ url: "wss://api.example.com/sync/coordinator", userInfo: { name: user.name, colour: user.colour, cursor: null, // 後で room の broadcast で更新 }, onPresence: (entries) => { // 誰かの join / leave / userInfo 更新のたびに呼ばれる。 // entries には自分自身も含まれる — 自分を UI に表示したくなければ // filter する。 setPeers(entries.filter((e) => e.clientID !== plasma.clientID)) }, }),})PresenceEntry の shape:
{ clientID: string, userInfo: unknown }userInfo は JSON として coordinator に渡り、全 subscriber にそのまま
forward されます。JSON にシリアライズできるものならなんでも OK
(null / プリミティブ / object / array)。
SyncCoordinator は poke と presence 両方を扱う Durable Object です。
Worker から export し wrangler.jsonc で bind:
export { SyncCoordinator } from "@sh1n4ps/plasma-server"{ "durable_objects": { "bindings": [ { "name": "COORDINATOR", "class_name": "SyncCoordinator" } ] }, "migrations": [ { "tag": "v1", "new_sqlite_classes": ["SyncCoordinator"] } ]}サーバー側のセットアップはこれで全部です。plasma が /sync/coordinator
での WebSocket upgrade をこの DO にルーティングします。
Presence は room 単位です。デフォルトの room は "global" —
同じ coordinator DO に繋がった全 client が互いに見えます。小規模
アプリなら OK、ドキュメント / チャンネル / 部屋を分けたい規模に
なると誤りです。
Room をドキュメント単位 (or チャンネル単位) に scope するには
client 側で room を渡します:
createWebSocketSubscription({ url: "wss://api.example.com/sync/coordinator", room: `doc-${docId}`, userInfo: { name: user.name, colour: user.colour }, onPresence: (entries) => setPeers(entries),})WebSocket upgrade 時に ?room=doc-{docId} として送出され、
coordinator の onPresence broadcast はその room に限定されます。
同じドキュメントを編集している 2 ユーザーは互いに見え、別ドキュ
メントの 2 ユーザーは見えません。
Presence が更新されるタイミング
Section titled “Presence が更新されるタイミング”onPresence コールバックが呼ばれるのは:
- Room に誰かが join — coordinator は hibernate も面倒みるので、 新規 WebSocket subscribe は join として扱われる。
- Room から誰かが leave — WebSocket close イベント、または hibernation タイムアウト。
- Subscriber が
userInfoを変更 — 新しい payload で reconnect、 または follow-up メッセージで push (下記「更新のブロードキャスト」)。
コールバックは毎回 完全な presence リストを受け取ります。誰が join したか / leave したかを区別したい場合、アプリ側で diff してください。
更新のブロードキャスト
Section titled “更新のブロードキャスト”userInfo を変更するには (例: カーソル移動)、新しい payload で
reconnect します。coordinator は新規 join として扱い、rebroadcast
します。低頻度の更新 (名前 / アバター / 色) なら問題ありませんが、
動くカーソルなら broadcast の前に throttle してください —
requestAnimationFrame くらいのペースが妥当です。
WebSocket を配線しないとき
Section titled “WebSocket を配線しないとき”createWebSocketSubscription は完全に opt-in です。
createPlasmaClient の subscribe を省くと presence は無効化され、
onPresence は fire せず WebSocket も開きません。リアルタイム
コラボレーションが不要なアプリではこれが正しいデフォルト: WebSocket
コストゼロで poll ループが sync を回します。
WebSocket が切れた場合のフォールバック
Section titled “WebSocket が切れた場合のフォールバック”クライアントの WebSocket transport は指数バックオフで自動再接続
します。reconnect 中は presence が古いままです: 最後に受け取った
onPresence snapshot が「オンラインだった人」を示します。「切断中」
専用のシグナルは無いので、reconnecting…UI を出したい場合は
client.onError を watch して network イベントをヒューリスティックに
使うのが実務的です。
匿名ユーザー
Section titled “匿名ユーザー”Presence と userInfo は payload だけで、coordinator は検証しません。
peer の名前を表示するなら先に sanitize してください。colour / avatar
その他 presence UI に見せるもの全部が対象です。
次に読むページ
Section titled “次に読むページ”- Devtools — panel で runtime の presence snapshot を確認
- Multi-region — presence room は region ローカル、region 横断 presence は v1.1 候補
- Concepts / clientID vs clientGroupID — room を scope する identity モデル