# Add collaboration

Use a transport with a mounted diagram so peers converge on document edits, retain edits made while disconnected, and optionally show presence or anchored comments.

## When to use this

Use the component's `collab` prop when a framework binding mounts the diagram. Use [`render`](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/grafloria-element-core) and [`createSyncSession`](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/grafloria-engine-sync-functions) when you mount the JavaScript surface yourself or need a headless peer. A session synchronizes the document; it does not provide rooms, authentication, or storage.

The transport is broadcast-to-others: `send()` never echoes a message to its sender. Reconnection depends on status notifications, so implement `onStatus()` in a custom transport. The shipped transports are `BroadcastChannelTransport`, `MemoryTransport`, and `WebSocketTransport`; use `BroadcastChannelTransport` for two tabs and `WebSocketTransport` when your server carries the channel.

## Mount two synchronized diagrams

Give both peers the same room name and different actor IDs. Each mounted instance joins at mount and leaves at unmount. The remote instance repaints the merged document after a local edit.

### JavaScript

This sample mounts two real diagrams in one page. Drag a node in either pane: the other pane moves the same node. The containers have height so the diagrams paint.

```ts
const peerA = document.createElement('div');
peerA.id = 'peer-a';
peerA.style.height = '400px';
const peerB = document.createElement('div');
peerB.id = 'peer-b';
peerB.style.height = '400px';
document.body.append(peerA, peerB);

import { render } from '@grafloria/element';
import { BroadcastChannelTransport, createSyncSession } from '@grafloria/engine';

const nodes = [
  { id: 'a', label: 'Ingest', position: { x: 60, y: 80 }, size: { width: 150, height: 66 } },
  { id: 'b', label: 'Transform', position: { x: 300, y: 80 }, size: { width: 150, height: 66 } },
];
const edges = [{ id: 'e1', source: 'a', target: 'b' }];
const room = `grafloria-${Math.random().toString(36).slice(2)}`;

const first = render({ nodes, edges }, '#peer-a');
const second = render({ nodes: structuredClone(nodes), edges: structuredClone(edges) }, '#peer-b');

const firstTransport = new BroadcastChannelTransport({ name: room, actor: 'ana' });
const secondTransport = new BroadcastChannelTransport({ name: room, actor: 'ben' });
const firstSession = createSyncSession(first.getModel(), firstTransport, { actor: 'ana', batch: false });
const secondSession = createSyncSession(second.getModel(), secondTransport, { actor: 'ben', batch: false });
firstSession.join();
secondSession.join();

window.addEventListener('beforeunload', () => {
  firstSession.leave();
  secondSession.leave();
  first.dispose();
  second.dispose();
});
```

`render()` returns a live [`DiagramInstance`](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/grafloria-renderer-instance-diagraminstance). Its `getModel()` supplies the document to the session; the instance remains the object that paints and receives interaction. `batch: false` makes each change cross the demo channel as it occurs rather than waiting for a batch.

### Angular, React, and Vue

The bindings use the same engine seam. Keep each transport and its actor stable for the mounted component's lifetime; changing the room means remounting. These examples show two peers, so each pane receives an independent copy of the data.

:::code-group
```ts title="Angular"
import { Component } from '@angular/core';
import { DiagramCanvasComponent } from '@grafloria/angular';
import { BroadcastChannelTransport } from '@grafloria/engine';

@Component({
  standalone: true,
  imports: [DiagramCanvasComponent],
  template: `
    <grafloria-diagram-canvas [nodes]="nodesA" [edges]="edges" [collab]="collabA" style="display:block;height:400px" />
    <grafloria-diagram-canvas [nodes]="nodesB" [edges]="edges" [collab]="collabB" style="display:block;height:400px" />
  `,
})
export class CollaborationComponent {
  private readonly room = `angular-${Math.random().toString(36).slice(2)}`;
  readonly nodesA = [
    { id: 'a', position: { x: 60, y: 60 }, size: { width: 150, height: 66 }, data: { label: 'Ingest' } },
    { id: 'b', position: { x: 320, y: 60 }, size: { width: 150, height: 66 }, data: { label: 'Publish' } },
  ];
  readonly nodesB = [
    { id: 'a', position: { x: 60, y: 60 }, size: { width: 150, height: 66 }, data: { label: 'Ingest' } },
    { id: 'b', position: { x: 320, y: 60 }, size: { width: 150, height: 66 }, data: { label: 'Publish' } },
  ];
  readonly edges = [{ id: 'e1', source: 'a', target: 'b' }];
  readonly collabA = { transport: new BroadcastChannelTransport({ name: this.room, actor: 'ana' }), actor: 'ana' };
  readonly collabB = { transport: new BroadcastChannelTransport({ name: this.room, actor: 'ben' }), actor: 'ben' };
}
```

```tsx title="Qwik"
import { component$, noSerialize, useSignal } from '@builder.io/qwik';
import { GrafloriaFlow, type GrafloriaCollabOptions } from '@grafloria/qwik';
import { BroadcastChannelTransport } from '@grafloria/engine';

const nodes = [
  { id: 'a', position: { x: 60, y: 60 }, size: { width: 150, height: 66 }, data: { label: 'Ingest' } },
  { id: 'b', position: { x: 320, y: 60 }, size: { width: 150, height: 66 }, data: { label: 'Publish' } },
];
const edges = [{ id: 'e1', source: 'a', target: 'b' }];

export default component$(() => {
  const collab = useSignal(() => {
    const room = `qwik-${Math.random().toString(36).slice(2)}`;
    const a: GrafloriaCollabOptions = { transport: new BroadcastChannelTransport({ name: room, actor: 'ana' }), actor: 'ana' };
    const b: GrafloriaCollabOptions = { transport: new BroadcastChannelTransport({ name: room, actor: 'ben' }), actor: 'ben' };
    return { a: noSerialize(a), b: noSerialize(b) };
  });
  return <div style={{ display: 'flex', height: '400px' }}><GrafloriaFlow defaultNodes={nodes} defaultEdges={edges} collab={collab.value.a} style={{ flex: '1' }} /><GrafloriaFlow defaultNodes={nodes} defaultEdges={edges} collab={collab.value.b} style={{ flex: '1' }} /></div>;
});
```
```tsx title="React"
import { useMemo } from 'react';
import { GrafloriaFlow } from '@grafloria/react';
import { BroadcastChannelTransport } from '@grafloria/engine';

const nodes = [
  { id: 'a', position: { x: 60, y: 60 }, size: { width: 150, height: 66 }, data: { label: 'Ingest' } },
  { id: 'b', position: { x: 320, y: 60 }, size: { width: 150, height: 66 }, data: { label: 'Publish' } },
];
const edges = [{ id: 'e1', source: 'a', target: 'b' }];
const nodesB = [
  { id: 'a', position: { x: 60, y: 60 }, size: { width: 150, height: 66 }, data: { label: 'Ingest' } },
  { id: 'b', position: { x: 320, y: 60 }, size: { width: 150, height: 66 }, data: { label: 'Publish' } },
];

export default function Collaboration() {
  const peers = useMemo(() => {
    const room = `react-${Math.random().toString(36).slice(2)}`;
    return {
      a: { transport: new BroadcastChannelTransport({ name: room, actor: 'ana' }), actor: 'ana' },
      b: { transport: new BroadcastChannelTransport({ name: room, actor: 'ben' }), actor: 'ben' },
    };
  }, []);
  return <div style={{ display: 'flex', height: 400 }}><GrafloriaFlow defaultNodes={nodes} defaultEdges={edges} collab={peers.a} style={{ flex: 1 }} /><GrafloriaFlow defaultNodes={structuredClone(nodes)} defaultEdges={edges} collab={peers.b} style={{ flex: 1 }} /></div>;
}
```
```vue title="Vue"
<script setup lang="ts">
import { BroadcastChannelTransport } from '@grafloria/engine';
import { GrafloriaFlow } from '@grafloria/vue';

const room = `vue-${Math.random().toString(36).slice(2)}`;
const nodes = [
  { id: 'a', position: { x: 60, y: 60 }, size: { width: 150, height: 66 }, data: { label: 'Ingest' } },
  { id: 'b', position: { x: 320, y: 60 }, size: { width: 150, height: 66 }, data: { label: 'Publish' } },
];
const edges = [{ id: 'e1', source: 'a', target: 'b' }];
const collabA = { transport: new BroadcastChannelTransport({ name: room, actor: 'ana' }), actor: 'ana' };
const collabB = { transport: new BroadcastChannelTransport({ name: room, actor: 'ben' }), actor: 'ben' };
const nodesB = [
  { id: 'a', position: { x: 60, y: 60 }, size: { width: 150, height: 66 }, data: { label: 'Ingest' } },
  { id: 'b', position: { x: 320, y: 60 }, size: { width: 150, height: 66 }, data: { label: 'Publish' } },
];
</script>
<template>
  <div style="display:flex;height:400px"><GrafloriaFlow :default-nodes="nodes" :default-edges="edges" :collab="collabA" style="flex:1" /><GrafloriaFlow :default-nodes="nodesB" :default-edges="edges" :collab="collabB" style="flex:1" /></div>
</template>
```
:::

Each framework component joins the session at mount and tears it down at unmount.

## Preserve edits across a disconnect

Use [`MemoryHub`](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/grafloria-engine-sync-classes) to test this flow without a server. Disconnect both transports, edit both models, then reconnect and call the session's sync operation. The local op logs exchange the missed operations; both diagrams contain both peers' edits after the round.

For a production transport, call `connect()` and `disconnect()` without destroying the transport. Its `onStatus()` callback lets the adapter start anti-entropy when the channel reports that it is connected again. A `send()` while disconnected is a no-op, not an error, so do not treat it as delivery.

## Add presence and comments

Presence is ephemeral. Pass `presence: { name: 'Ana' }` beside `transport` and `actor` in the component's `collab` object; the binding paints the other peer's cursor and selection without putting cursor traffic in the document op log. The same option shape works in Angular, React, and Vue.

Anchored comments are document collaboration data. Set `comments: true` on the mounted component, then read the store from the instance with `getCommentStore()`. A non-null [`CommentStore`](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/grafloria-engine-comments-commentstore) can create and reply to threads, resolve or reopen them, list threads for a node or link, and report unread messages. The store exposes `onChange()` for updating your comments UI. Pass a shared store when your host owns its lifetime; otherwise the `comments: true` option creates one for the canvas.

The component creates the store before `onInit` exposes the live instance. `getCommentStore()` is `null` when comments are not enabled. Use the framework's ready-made comment panel when you want the library's threaded UI; your own UI can subscribe to `onChange()` and use the store's thread methods.

## What you get

- Two mounted peers display the same document after edits cross the transport.
- Edits made while disconnected remain in each peer's log and converge after reconnect.
- Presence cursors and selections travel as ephemeral awareness, not document edits.
- Comment threads attach to nodes or links and provide a synchronized, readable store.

## Pitfalls

- Give every peer a distinct actor ID and reuse the same room/channel name.
- Keep a transport instance stable while its diagram is mounted; remount to change rooms.
- Implement `onStatus()` for a custom transport, or reconnect catch-up cannot start.
- A transport does not supply authentication, rooms, or persistence; provide those around the channel.

## Demos

The [Two tabs, live demo](https://grafloria.com/demos/collab/two-tabs-live.html) uses `BroadcastChannelTransport` and shows symmetric live edits with no server.

The [Offline & reconnect demo](https://grafloria.com/demos/collab/offline-and-reconnect.html) disconnects both peers, edits each side, and reconnects them so both offline edits survive.

The [Live cursors demo](https://grafloria.com/demos/collab/live-cursors.html) shows presence on a separate layer while the diagram remains unchanged.

## Related

- [How Grafloria works](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/how-grafloria-works)
- [The `DiagramInstance`](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/grafloria-renderer-instance-diagraminstance)
- [Element core](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/grafloria-element-core)
