FluxyChat

Core SDK

Presence & typing

Room presence and typing indicators over WebSocket.

Presence and typing flow over the room WebSocket — no separate REST polling required.

Typing with useChat

useChat exposes setTyping(isTyping) which sends a typing frame to the room Durable Object:

const { typingUsers, setTyping } = useChat({ roomId, client });

<input
  onFocus={() => setTyping(true)}
  onBlur={() => setTyping(false)}
/>
<p>
  {Object.entries(typingUsers)
    .filter(([, active]) => active)
    .map(([uid]) => uid)
    .join(", ")}{" "}
  typing…
</p>

Presence members

On connect, the room broadcasts presence events with member lists. The room store tracks presenceMembers:

const snapshot = useFluxyRoomStoreState(store);
snapshot.presenceMembers.map((m) => m.userId);

Pass optional profile data when joining via WebSocket query (presenceInfo JSON) for richer avatars in the dashboard.

Rooms with more than 250 unique users switch to aggregate presence: presenceKind: "aggregate", presenceCount, and a sample of presenceMembers (24). Check presenceKind before rendering a full roster.

Late-joiner derived state

Non-CRDT app state (score, turn, ETA) can live in a JSON bag on the room Durable Object. It is included on subscription_succeeded as derived so a late joiner paints before replaying the log. Replace the bag with setDerivedState from useChat (30/min, 16 KiB). Use Yjs storage when you need conflict-free document edits.

const { derivedState, setDerivedState } = useChat({ roomId, client });
setDerivedState({ score: 3, turn: "ada" });

Live cursors (presence, not broadcast)

Pointers use WebSocket type: "cursor" (up to 600/min, no D1, no webhooks). Do not send pointers as client_event (10/min + webhooks).

const { liveCursors, sendCursor, connected } = useChat({ roomId, replay: "request" });

function onPointerMove(e: React.PointerEvent<HTMLDivElement>) {
  const rect = e.currentTarget.getBoundingClientRect();
  sendCursor({
    x: e.clientX - rect.left,
    y: e.clientY - rect.top,
    label: "Ada",
  });
}

Scaffold: npx @fluxy-chat/create-fluxy-chat@latest my-cursors --example live-cursors. UI: <Cursors /> (damped spring via stepSpring) / <Selections /> / <AvatarStack /> from @fluxy-chat/ui. prefers-reduced-motion snaps without spring.

Selections ride on presence_patch (same throttle as cursors, no D1, no webhooks):

const [, updateMyPresence] = useMyPresence(store);
updateMyPresence({ selection: { x: 10, y: 20, x2: 80, y2: 40 } });

client_event / useBroadcastEvent is for sparse signals (client-ephemeral-* skips webhooks). Not 60 Hz pointers.

Intents

Typing intents support composing, away, viewing_thread — surfaced as typingIntents in the room store for advanced UX (thread focus indicators).

See Connection state for transport details.

On this page