Presence and locking ​
The core package depends on nothing network related. It declares a PresenceSource port and owns the presence schema. An adapter for @jolly-pixel/network ships under the ./network subpath.
CollaboratorPresence ​
ts
interface CollaboratorPresence {
clientId: string;
displayName: string;
color: string;
editing?: string; // field path this peer holds
}PresenceSource ​
ts
interface PresenceSource {
readonly clientId: string;
readonly peers: ReadonlyMap<string, CollaboratorPresence>; // local peer included
claim(path: string): LockState;
release(path: string): void;
on(event: "change", listener: () => void): void;
off(event: "change", listener: () => void): void;
}
type LockState = "held" | "denied" | "contended";- A client holds one path at a time:
claimreplaces the previous one. "contended": a remote peer already holds the path. The claim goes through anyway. Locks are advisory, and the data layer resolves writes, last write wins."denied"has no producer in a presence-backed source. It leaves room for a server-granted lease.
Wiring a pane ​
ts
pane.presence = source;ts
html`<jolly-number path="map.width" .value=${width}></jolly-number>`;- Fields find the nearest ancestor's source when they connect, so one property turns on locking for the whole subtree. Without a source, fields never lock.
pathis supplied by the consumer. Do not derive it from a label or DOM position: two clients would only agree while their trees are identical.- A field with
path === null, the default, never claims. - A focused field publishes its path. Other clients show it locked in the holder's colour.
- The local peer never locks its own field. Local focus beats a remote claim: contention shows as a peer chip, not a lock bar.
- A claim releases on blur, on disconnection, and when
pathchanges. A peer's claims disappear when it leaves.
Mapping ​
jolly-presence reads no source: the host pushes snapshots into it.
ts
const repaint = () => presence.update(
toPresencePeers(source.peers.values(), source.clientId)
);
source.on("change", repaint);
repaint();toPresencePeers(peers, selfId) flags the local peer and puts it first, then sorts by clientId, so every client shows the same order.
RoomPresenceSource ​
ts
import { RoomPresenceSource } from "@jolly-pixel/ui/network";
const source = new RoomPresenceSource(room);@jolly-pixel/networkis an optional peer dependency. The subpath imports its./cliententry, which keeps the server andwsout of browser bundles.- Peers are keyed by connection: two tabs of one account are two peers.
clientIdisroom.clientId,""until admitted.- Names and colours come from
toPresencePeer. - Claims travel under the
fieldLockpresence key. dispose()detaches from the room.