Session Sharing
Shares a drawing's session with other apps and AI agents: invites and share links, who is in the session, what they publish, and what they have selected.
Functions
buildShareUrl()
function buildShareUrl(invite, base?): string;
Builds the link to hand to a guest: the current page with the invite token as query parameter.
A guest is another app window, or an MCP's use_session. base defaults to the page's origin + path.
Parameters
| Parameter | Type | Description |
|---|---|---|
invite | Pick<SessionInvite, "invite" | "kind"> | The invite, as createInvite returns it. |
base? | { origin: string; pathname: string; } | The page the link opens; by default the current one. |
base.origin | string | - |
base.pathname | string | - |
Returns
string
Example
import { BuerliCadFacade, buildShareUrl, DEFAULT_SESSION_URL, getShareTokenFromUrl } from '@buerli.io/classcad'
import { init, WASMClient, WSClient } from '@buerli.io/classcad'
// One page, two roles: opened with a share link it joins as a guest, else it hosts the engine itself
const shared = getShareTokenFromUrl()
let client!: WASMClient | WSClient
init(drawingId =>
shared
? (client = new WSClient(DEFAULT_SESSION_URL, drawingId, { invite: shared.token }))
: (client = new WASMClient(drawingId, { token: 'ccpk_…' })),
)
await new BuerliCadFacade().connect()
// The host's button: a link to this page for a colleague
shareButton.hidden = Boolean(shared)
shareButton.onclick = async () => {
const invite = await client.createInvite('edit')
await navigator.clipboard.writeText(buildShareUrl(invite)) // https://my.app/editor?invite=…
}
getShareTokenFromUrl()
function getShareTokenFromUrl(search?):
| {
kind: "invite";
token: string;
}
| null;
Reads a share token from a page URL (?invite=…).
Returns null when the page is a plain host. buildShareUrl has an example of both sides.
Parameters
| Parameter | Type | Description |
|---|---|---|
search? | string | The query string to read; by default the page's (window.location.search). |
Returns
Type Literal
{
kind: "invite";
token: string;
}
| Name | Type | Description |
|---|---|---|
kind | "invite" | How the token is honoured. |
token | string | The token. |
null
getSessionShareState()
function getSessionShareState(drawingId): SessionShareState;
Current sharing state of a drawing (stable object until something changes).
Parameters
| Parameter | Type | Description |
|---|---|---|
drawingId | DrawingID | The drawing. |
Returns
Example
import { getSessionShareState, subscribeSessionShare } from '@buerli.io/classcad'
// A badge without a framework: who is in the session, kept up to date
const render = () => {
const share = getSessionShareState(drawingId)
const names = share.peers.map(peer => share.identities[peer.peerId]?.name ?? (peer.inviteName || 'Guest'))
badge.hidden = !share.supported
badge.textContent = share.isGuest ? `Guest (${share.role})` : `With you: ${names.join(', ') || 'nobody'}`
}
const unsubscribe = subscribeSessionShare(drawingId, render)
syncSelection()
function syncSelection(client): () => void;
Shares the selection of a client's drawing with its session: publishes it on change, selects what a peer asks for.
Call it when the client is created; it returns the function that stops it. On a client that is in no shared session it does nothing.
Parameters
| Parameter | Type | Description |
|---|---|---|
client | AwvNodeClient | The drawing's client, e.g. from onClient. |
Returns
A function that stops sharing the selection.
() => void
Example
import { onClient, SELECTION_CHANNEL, syncSelection, WASMClient, WSClient } from '@buerli.io/classcad'
import type { PresenceMessage, SelectionFrame } from '@buerli.io/classcad'
// Every shared drawing shows its selection to the others, and selects what they ask for
let stop: (() => void) | undefined
onClient(client => {
if (!(client instanceof WSClient || client instanceof WASMClient)) return
stop?.()
stop = syncSelection(client)
// What the others have selected
client.on('presence', (_client, msg: PresenceMessage) => {
if (msg.channel === SELECTION_CHANNEL) highlightForPeer(msg.peerId, msg.data as SelectionFrame)
})
})
subscribeSessionShare()
function subscribeSessionShare(drawingId, callback): () => void;
Subscribes to sharing changes of a drawing: its client, connection, role, peers, invites and identities.
It fires when the client is (re)created, connects or loses its connection, joins a session (session_joined),
and when peers, invites or identities change. Returns the unsubscribe function.
With getSessionShareState it fits React's useSyncExternalStore.
Parameters
| Parameter | Type | Description |
|---|---|---|
drawingId | DrawingID | The drawing. |
callback | () => void | Called on every change; read the new state with getSessionShareState. |
Returns
() => void
Example
import { useSyncExternalStore } from 'react'
import { getSessionShareState, subscribeSessionShare } from '@buerli.io/classcad'
// A React hook for a drawing's sharing state
export function useSessionShare(drawingId: string) {
return useSyncExternalStore(
onChange => subscribeSessionShare(drawingId, onChange),
() => getSessionShareState(drawingId),
)
}
export function Guests({ drawingId }: { drawingId: string }) {
const { peers, invites } = useSessionShare(drawingId)
return (
<span>
{peers.length} guests, {invites.length} invites
</span>
)
}
Types
ClientIdentity
type ClientIdentity = {
app: string;
kind?: "app" | "agent";
name?: string;
version?: string;
};
What a participant says about itself.
Give it to a WSClient or WASMClient as identity; the others in the session find it in the
client's identities, by peer id.
Example
import { init, WASMClient } from '@buerli.io/classcad'
import type { ClientIdentity } from '@buerli.io/classcad'
// Says who this page is, and lists who else is in the session
const identity: ClientIdentity = { app: 'configurator', version: '2.1.0', name: 'Ada', kind: 'app' }
init(drawingId => {
const client = new WASMClient(drawingId, { token: 'ccpk_…', identity })
client.on('identities_changed', (_client, identities: Record<string, ClientIdentity>) =>
showParticipants(Object.values(identities).map(i => `${i.name ?? i.app}${i.kind === 'agent' ? ' (AI)' : ''}`)),
)
return client
})
Properties
PresenceMessage
type PresenceMessage = {
channel: string;
data: Record<string, any>;
peerId: string;
};
An ephemeral presence message from a session sibling (e.g. its camera viewpoint).
Clients emit it as presence (client, message); the guest example of WSClient follows a camera with it.
Properties
SessionInvite
type SessionInvite = {
invite: string;
kind: ShareKind;
name: string;
role: SessionRole;
};
An invite this client minted (the server keeps no queryable list, so the client tracks it).
buildShareUrl makes the link a guest opens.
Properties
| Property | Type | Description |
|---|---|---|
invite | string | The token guests join with (?invite=<token>). |
kind | ShareKind | How the invite is honoured. |
name | string | The name shown for the invite ('' for unnamed ones). |
role | SessionRole | What guests with the invite may do. |
SessionPeer
type SessionPeer = {
invite: string;
inviteName: string;
peerId: string;
role: SessionRole;
};
A guest currently sharing the session, keyed by peerId.
Clients list them in peers and emit peers_changed; see the host example of WSClient.
Properties
| Property | Type | Description |
|---|---|---|
invite | string | The invite token the guest joined with. |
inviteName | string | Display name of that token ('' for unnamed tokens). |
peerId | string | Stable id of the guest's connection. |
role | SessionRole | The role of that token. |
SessionRole
type SessionRole = "edit" | "view";
Role attached to an invite. Advisory: enforced by the client, not the server (yet).
'edit' may change the model, 'view' only look; an app suppresses changes for a 'view' guest.
SessionShareState
type SessionShareState = {
identities: Record<string, ClientIdentity>;
inviteName: string;
invites: SessionInvite[];
isGuest: boolean;
kind: ShareKind | null;
peers: SessionPeer[];
role: SessionRole;
supported: boolean;
};
Snapshot of a drawing's sharing state, as served to UI.
getSessionShareState returns it, subscribeSessionShare says when it changed.
Properties
| Property | Type | Description |
|---|---|---|
identities | Record<string, ClientIdentity> | What the other participants said about themselves, by peer id (see ClientIdentity). |
inviteName | string | Name of the invite this page joined with ('' for the host). |
invites | SessionInvite[] | The invites this client minted and has not revoked. |
isGuest | boolean | True when this page was opened with a share token (it is a guest, not the host). |
kind | ShareKind | null | How the client shares, or null when it cannot. |
peers | SessionPeer[] | The guests in the session (host side). |
role | SessionRole | This connection's own role ('edit' for hosts and non-sharing clients). |
supported | boolean | A client exists for the drawing and can mint invites. |
Constants
DEFAULT_SESSION_URL
const DEFAULT_SESSION_URL: "ws://127.0.0.1:9098/session" = 'ws://127.0.0.1:9098/session';
Where a page that hosts its own engine is joined: the session address of a ClassCAD MCP on the same machine.
The default of the WASMClient option sessionUrl. 127.0.0.1 on
purpose: localhost may be IPv6 in the browser and IPv4 in the MCP.
Advanced
Most apps do not need these directly: types they meet through the exports above, and what is there for special cases or for building on buerli itself.
applySelection()
function applySelection(
drawingId,
targets,
replace?
): number;
Selects targets in the drawing — what a peer asked for.
Returns how many of them were found. replace: false adds to the selection.
Parameters
| Parameter | Type | Default value | Description |
|---|---|---|---|
drawingId | DrawingID | undefined | The drawing. |
targets | SelectTarget[] | undefined | What to select; targets that are not in the drawing (any more) are skipped. |
replace? | boolean | true | true (default) replaces the selection, false adds to it. |
Returns
number
Example
import { applySelection } from '@buerli.io/classcad'
// Selects the faces a script found, adding them to what is selected
const found = applySelection(drawingId, faceIds.map(graphicId => ({ graphicId })), false)
if (found < faceIds.length) console.warn(`${faceIds.length - found} faces are not in the model any more`)
CLIENT_CHANNEL
const CLIENT_CHANNEL: "client" = 'client';
The presence channel identities are published on.
getSessionShareClient()
function getSessionShareClient(drawingId): AwvNodeClient | null;
The client that serves drawingId, or null before the drawing is connected.
Parameters
| Parameter | Type | Description |
|---|---|---|
drawingId | DrawingID | The drawing. |
Returns
AwvNodeClient | null
Example
import { getSessionShareClient } from '@buerli.io/classcad'
// Ends the sharing of a drawing: every invite taken back, every guest cut off
const client = getSessionShareClient(drawingId)
for (const invite of client?.invites ?? []) await client?.revokeInvite(invite.invite)
OfferRefusal
type OfferRefusal = {
code: number;
reason: string;
};
Why the share relay would not take a page's invite, for good: the code it closed the offer with, and its words.
The code is 4000 + the HTTP status: 4401 the sign-in, 4403 the plan, 4409 another account offers the invite. A WASMClient emits it as 'invite_refused' (client, invite, refusal); WASMClient.createInvite has an example.
Properties
| Property | Type | Description |
|---|---|---|
code | number | The close code: 4401 the sign-in, 4403 the plan, 4409 another account offers the invite. |
reason | string | The relay's words. |
readSelection()
function readSelection(drawingId): SelectionItem[];
The drawing's selection (interaction.selected), as session items.
Parameters
| Parameter | Type | Description |
|---|---|---|
drawingId | DrawingID | The drawing. |
Returns
Example
import { readSelection } from '@buerli.io/classcad'
// Tells an assistant what "this" means: the selection, by the engine's ids
const faces = readSelection(drawingId).filter(item => item.kind === 'face')
const prompt = `Selected faces: ${faces.map(face => `${face.graphicId} (${face.type})`).join(', ')}`
SELECT_CHANNEL
const SELECT_CHANNEL: "select" = 'select';
The presence channel requests to select something are sent on.
SelectFrame
type SelectFrame = {
done?: boolean;
id: string;
items?: SelectTarget[];
replace?: boolean;
};
The 'select' channel's payload: a request (items), or the note that the request id is over (done).
Example
import { SELECT_CHANNEL } from '@buerli.io/classcad'
import type { SelectFrame, WSClient } from '@buerli.io/classcad'
// Asks the others in the session to select an edge (they run syncSelection)
const id = crypto.randomUUID()
const request: SelectFrame = { id, items: [{ graphicId: edgeId }], replace: true }
client.sendPresence(SELECT_CHANNEL, request)
// Right after: the request is over, so the frame the server keeps asks for nothing
client.sendPresence(SELECT_CHANNEL, { id, done: true } satisfies SelectFrame)
Properties
| Property | Type | Description |
|---|---|---|
done? | boolean | The request id is over. |
id | string | The request's id; a peer follows each id once. |
items? | SelectTarget[] | What to select. |
replace? | boolean | false adds to the selection; by default it is replaced. |
SELECTION_CHANNEL
const SELECTION_CHANNEL: "selection" = 'selection';
The presence channel a participant's selection is published on.
SelectionFrame
type SelectionFrame = {
items: SelectionItem[];
total: number;
};
The 'selection' channel's payload: what a participant has selected.
total counts the whole selection; items may be cut to fit a presence frame. The example of
syncSelection reads the frames of the others.
Properties
| Property | Type | Description |
|---|---|---|
items | SelectionItem[] | The selected things; the first ones, when the selection does not fit a presence frame. |
total | number | How many things are selected. |
SelectionItem
type SelectionItem = {
containerId?: number;
graphicId?: number;
kind: "face" | "edge" | "vertex" | "object";
name?: string;
objectId: number;
prodRefId?: number | null;
type?: string;
};
One selected thing: a face, edge or vertex of a body, or an object of the model tree (a sketch entity, a dimension, a feature). readSelection has an example.
Properties
SelectTarget
type SelectTarget = {
containerId?: number;
graphicId?: number;
objectId?: number;
prodRefId?: number | null;
};
What a request names: a tree object by its id, or geometry by its element id (and container and product, when known).
applySelection and SelectFrame have examples.
Properties
SESSION_SHARING_UNSUPPORTED
const SESSION_SHARING_UNSUPPORTED: "SessionSharingUnsupported" = 'SessionSharingUnsupported';
Error.name of the rejection when a client cannot share (no shareKind).
SHARE_PARAM
const SHARE_PARAM: Record<ShareKind, string>;
Query parameter carrying a share token, by ShareKind: invite.
ShareKind
type ShareKind = "invite";
How a client's sessions are shared: by invite. null (on a client) = it cannot share.