Skip to main content

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​

ParameterTypeDescription
invitePick<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.originstring-
base.pathnamestring-

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​

ParameterTypeDescription
search?stringThe query string to read; by default the page's (window.location.search).

Returns​

Type Literal​
{
kind: "invite";
token: string;
}
NameTypeDescription
kind"invite"How the token is honoured.
tokenstringThe token.

null


getSessionShareState()​

function getSessionShareState(drawingId): SessionShareState;

Current sharing state of a drawing (stable object until something changes).

Parameters​

ParameterTypeDescription
drawingIdDrawingIDThe drawing.

Returns​

SessionShareState

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​

ParameterTypeDescription
clientAwvNodeClientThe 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​

ParameterTypeDescription
drawingIdDrawingIDThe drawing.
callback() => voidCalled 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​

PropertyTypeDescription
appstringThe program: 'buerligons', 'classcad-mcp', …
kind?"app" | "agent"'agent' for an AI agent's session, 'app' for a person at an app.
name?stringA name to show for this participant ("Claude Code", a person's name).
version?stringThe program's version.

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​

PropertyTypeDescription
channelstringApplication-defined channel, e.g. 'view' for shared camera viewpoints, 'leave' when a peer disconnects.
dataRecord<string, any>Channel-specific payload, verbatim from the sender.
peerIdstringStable id of the sending connection (matches PeerJoined/PeerLeft peerIds).

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​

PropertyTypeDescription
invitestringThe token guests join with (?invite=<token>).
kindShareKindHow the invite is honoured.
namestringThe name shown for the invite ('' for unnamed ones).
roleSessionRoleWhat 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​

PropertyTypeDescription
invitestringThe invite token the guest joined with.
inviteNamestringDisplay name of that token ('' for unnamed tokens).
peerIdstringStable id of the guest's connection.
roleSessionRoleThe 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​

PropertyTypeDescription
identitiesRecord<string, ClientIdentity>What the other participants said about themselves, by peer id (see ClientIdentity).
inviteNamestringName of the invite this page joined with ('' for the host).
invitesSessionInvite[]The invites this client minted and has not revoked.
isGuestbooleanTrue when this page was opened with a share token (it is a guest, not the host).
kindShareKind | nullHow the client shares, or null when it cannot.
peersSessionPeer[]The guests in the session (host side).
roleSessionRoleThis connection's own role ('edit' for hosts and non-sharing clients).
supportedbooleanA 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​

ParameterTypeDefault valueDescription
drawingIdDrawingIDundefinedThe drawing.
targetsSelectTarget[]undefinedWhat to select; targets that are not in the drawing (any more) are skipped.
replace?booleantruetrue (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​

ParameterTypeDescription
drawingIdDrawingIDThe 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​

PropertyTypeDescription
codenumberThe close code: 4401 the sign-in, 4403 the plan, 4409 another account offers the invite.
reasonstringThe relay's words.

readSelection()​

function readSelection(drawingId): SelectionItem[];

The drawing's selection (interaction.selected), as session items.

Parameters​

ParameterTypeDescription
drawingIdDrawingIDThe drawing.

Returns​

SelectionItem[]

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​

PropertyTypeDescription
done?booleanThe request id is over.
idstringThe request's id; a peer follows each id once.
items?SelectTarget[]What to select.
replace?booleanfalse 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​

PropertyTypeDescription
itemsSelectionItem[]The selected things; the first ones, when the selection does not fit a presence frame.
totalnumberHow 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​

PropertyTypeDescription
containerId?numberGeometry: the body's graphic container. It is replaced whenever the body is rebuilt.
graphicId?numberGeometry: the element in the container. This is the id API calls take for a face, an edge or a vertex.
kind"face" | "edge" | "vertex" | "object"What was selected.
name?stringObjects: their name in the model tree.
objectIdnumberThe model tree object: the picked object itself, or the owner of the picked geometry (a solid).
prodRefId?number | nullThe product or assembly instance the pick was made in.
type?stringGeometry: the element's type (plane, cylinder, line, arc, …). Objects: their class.

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​

PropertyTypeDescription
containerId?numberGeometry: the body's graphic container. A body rebuilt since has another one; the element is looked up then.
graphicId?numberGeometry: the face, edge or vertex.
objectId?numberA model tree object.
prodRefId?number | nullThe product or assembly instance to select the geometry in; by default the part, or its first instance.

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.