コンテンツにスキップ

Presence

Presence は「今この部屋に誰がいて、何をしているか」を表します。 共有ドキュメントのカーソル位置、ヘッダーのアバター、typing インジケーター — リアルタイムコラボレーション的なもの全般です。plasma は presence を 標準搭載しており、WebSocket poke と同じ SyncCoordinator Durable Object に相乗りしています。

2 つの端点があります: クライアントは userInfo payload と onPresence コールバックで subscribe、サーバーは Worker から SyncCoordinator を export します。

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:

worker.ts
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 してください。

userInfo を変更するには (例: カーソル移動)、新しい payload で reconnect します。coordinator は新規 join として扱い、rebroadcast します。低頻度の更新 (名前 / アバター / 色) なら問題ありませんが、 動くカーソルなら broadcast の前に throttle してください — requestAnimationFrame くらいのペースが妥当です。

createWebSocketSubscription は完全に opt-in です。 createPlasmaClientsubscribe を省くと presence は無効化され、 onPresence は fire せず WebSocket も開きません。リアルタイム コラボレーションが不要なアプリではこれが正しいデフォルト: WebSocket コストゼロで poll ループが sync を回します。

WebSocket が切れた場合のフォールバック

Section titled “WebSocket が切れた場合のフォールバック”

クライアントの WebSocket transport は指数バックオフで自動再接続 します。reconnect 中は presence が古いままです: 最後に受け取った onPresence snapshot が「オンラインだった人」を示します。「切断中」 専用のシグナルは無いので、reconnecting…UI を出したい場合は client.onError を watch して network イベントをヒューリスティックに 使うのが実務的です。

Presence と userInfo は payload だけで、coordinator は検証しません。 peer の名前を表示するなら先に sanitize してください。colour / avatar その他 presence UI に見せるもの全部が対象です。