Cursor (Ephemeral State)
Lightweight fire-and-forget broadcasting for transient state - mouse cursors, text selections, drag positions, drawing strokes. Built-in throttle with trailing edge ensures the final position always arrives. Auto-cleanup on disconnect.
Setup
// src/lib/server/cursors.js
import { createCursor } from 'svelte-adapter-uws/plugins/cursor';
export const cursors = createCursor({
throttle: 50, // at most one broadcast per 50ms per user per topic
select: (userData) => ({ id: userData.id, name: userData.name, color: userData.color })
}); Options
| Option | Default | Description |
|---|---|---|
throttle | 50 | Max broadcast interval per user per topic (ms) |
select | identity | (userData) → publicData - extract public fields for the cursor payload |
Server usage
Use the hooks helper for zero-config cursor handling. The message hook handles cursor and cursor-snapshot messages automatically, and close calls remove(). The hooks verify that the sender is subscribed to the __cursor:{topic} channel before processing - clients that haven’t passed the subscribe hook for that topic are silently rejected.
// src/hooks.ws.js
import { cursors } from '$lib/server/cursors';
export function message(ws, ctx) {
if (cursors.hooks.message(ws, ctx)) return;
// handle other messages...
}
export const close = cursors.hooks.close; For custom auth or topic filtering, handle the messages manually:
export function message(ws, { data, platform }) {
const msg = JSON.parse(Buffer.from(data).toString());
if (msg.type === 'cursor') {
cursors.update(ws, msg.topic, { x: msg.x, y: msg.y }, platform);
}
if (msg.type === 'cursor-snapshot') {
cursors.snapshot(ws, msg.topic, platform);
}
}
export function close(ws, { platform }) {
cursors.remove(ws, platform);
} Server API
| Method | Description |
|---|---|
cursors.update(ws, topic, data, platform) | Broadcast position (throttled) |
cursors.remove(ws, platform) | Remove from all topics, broadcast removal |
cursors.snapshot(ws, topic, platform) | Send current positions to one connection (initial sync) |
cursors.list(topic) | Current positions (for SSR) |
cursors.clear() | Reset all state and timers |
Client usage
<script>
import { cursor } from 'svelte-adapter-uws/plugins/cursor/client';
const positions = cursor('canvas');
</script>
{#each [...$positions] as [key, { user, data }] (key)}
<div
class="cursor-dot"
style="left: {data.x}px; top: {data.y}px; background: {user.color}"
>
{user.name}
</div>
{/each} The client store is a Readable<Map<string, { user, data }>>. The Map updates when cursors move or disconnect. The store handles update, remove, snapshot, and bulk events. The snapshot event is authoritative - it replaces all client-side state (used for initial sync and reconnect). The bulk event merges entries additively (used by the extensions repo topicThrottle feature when flushing coalesced updates).
Initial sync and reconnect
The cursor(topic) store sends a { type: 'cursor-snapshot', topic } message every time the WebSocket connection opens - both on first connect and on every reconnect. The server calls cursors.snapshot(ws, topic, platform) in its message handler, which sends a snapshot event back with the current cursor state (or an empty array if nobody is active). The client replaces its entire cursor map with the snapshot contents, clearing any stale entries from before the disconnect. Wire cursors.snapshot() in your message handler as shown in the server example above.
The cursor() function accepts an optional second argument with a maxAge option (in milliseconds). When set, cursor entries that haven’t received an update within that window are automatically removed. This makes clients self-healing when the server fails to broadcast remove events under load:
const positions = cursor('canvas', { maxAge: 30_000 }); How throttle works
The cursor plugin uses leading edge + trailing edge throttle internally:
t=0 update({x:0}) --> broadcasts immediately (leading edge)
t=20 update({x:5}) --> stored (within 50ms window)
t=40 update({x:9}) --> stored (overwrites x:5)
t=50 [timer fires] --> broadcasts {x:9} (trailing edge) The trailing edge ensures you always see where the cursor stopped, even if the user stops moving mid-window.
Limitations
- In-memory. Cursor positions live in the process. In cluster mode, each worker tracks its own connections.
- No persistence. Positions are lost on restart. This is intentional - cursors are ephemeral.
Was this page helpful?