# Synchronize diagrams

Connect each diagram instance to a transport and give each peer its own actor id. Connected peers share edits; after a transport reconnects, the session exchanges missed operations so the replicas converge.

## Connect two peers

Use the component's `collab` prop when both diagrams run in a supported framework. Each peer gets its own transport connection and actor id, while both transports connect through the same room. The framework examples use [`MemoryHub`](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/grafloria-engine-sync-classes#memoryhub) to put two visible peers on one page; edits cross between their canvases. The plain JavaScript [`render`](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/grafloria-element-core#render) entry point mounts a diagram, but does not expose that component prop.

For the framework bindings and typed graph data, see [Execute and compute flows](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/execute-and-compute-flows); this page adds the per-peer `collab` configuration that connects mounted diagrams to the same sync room.

:::code-group
```html title="JavaScript"
<!-- The plain JavaScript render() API mounts a diagram but does not expose a collab option. -->
<div id="app" style="height: 460px"></div>
<script type="module">
  import { render } from '@grafloria/element';

  const container = document.getElementById('app');
  const instance = render({
    nodes: [
      { id: 'a', label: 'Alpha', position: { x: 80, y: 90 }, size: { width: 150, height: 66 } },
      { id: 'b', label: 'Beta', position: { x: 320, y: 90 }, size: { width: 150, height: 66 } },
    ],
    edges: [{ id: 'e1', source: 'a', target: 'b' }],
  }, container);

  instance.fitView();
</script>
```
```ts title="Angular"
import { Component } from '@angular/core';
import { DiagramCanvasComponent } from '@grafloria/angular';
import { MemoryHub } from '@grafloria/engine';
import type { EdgeSpec, NodeSpec } from '@grafloria/renderer';

const hub = new MemoryHub();

@Component({
  standalone: true,
  imports: [DiagramCanvasComponent],
  template: `
    <div style="display:flex; height:460px">
      <grafloria-diagram-canvas [(nodes)]="nodesA" [(edges)]="edgesA"
        [collab]="collabA" style="display:block; flex:1" />
      <grafloria-diagram-canvas [(nodes)]="nodesB" [(edges)]="edgesB"
        [collab]="collabB" style="display:block; flex:1" />
    </div>
  `,
})
export class AppComponent {
  nodesA: NodeSpec[] = [
    { id: 'a', label: 'Alpha', position: { x: 80, y: 90 }, size: { width: 150, height: 66 } },
    { id: 'b', label: 'Beta', position: { x: 320, y: 90 }, size: { width: 150, height: 66 } },
  ];
  edgesA: EdgeSpec[] = [{ id: 'e1', source: 'a', target: 'b' }];
  nodesB: NodeSpec[] = [
    { id: 'a', label: 'Alpha', position: { x: 80, y: 90 }, size: { width: 150, height: 66 } },
    { id: 'b', label: 'Beta', position: { x: 320, y: 90 }, size: { width: 150, height: 66 } },
  ];
  edgesB: EdgeSpec[] = [{ id: 'e1', source: 'a', target: 'b' }];
  collabA = { transport: hub.connect('ana'), actor: 'ana' };
  collabB = { transport: hub.connect('bo'), actor: 'bo' };
}
```
```tsx title="Qwik"
import { component$, noSerialize, useSignal, useVisibleTask$, type NoSerialize } from '@builder.io/qwik';
import { GrafloriaFlow, type GrafloriaCollabOptions } from '@grafloria/qwik';
import { MemoryHub } from '@grafloria/engine';
import type { EdgeSpec, NodeSpec } from '@grafloria/renderer';

const nodes: NodeSpec[] = [
  { id: 'a', label: 'Alpha', position: { x: 80, y: 90 }, size: { width: 150, height: 66 } },
  { id: 'b', label: 'Beta', position: { x: 320, y: 90 }, size: { width: 150, height: 66 } },
];
const edges: EdgeSpec[] = [{ id: 'e1', source: 'a', target: 'b' }];

export default component$(() => {
  const collabA = useSignal<NoSerialize<GrafloriaCollabOptions> | undefined>();
  const collabB = useSignal<NoSerialize<GrafloriaCollabOptions> | undefined>();
  useVisibleTask$(() => {
    const hub = new MemoryHub();
    collabA.value = noSerialize({ transport: hub.connect('ana'), actor: 'ana' });
    collabB.value = noSerialize({ transport: hub.connect('bo'), actor: 'bo' });
  });

  const peerA = collabA.value;
  const peerB = collabB.value;
  return (
    <div style={{ display: 'flex', height: '460px' }}>
      {peerA && peerB && (
        <>
          <GrafloriaFlow defaultNodes={nodes} defaultEdges={edges} collab={peerA}
            style={{ display: 'block', flex: '1' }} />
          <GrafloriaFlow defaultNodes={nodes} defaultEdges={edges} collab={peerB}
            style={{ display: 'block', flex: '1' }} />
        </>
      )}
    </div>
  );
});
```
```tsx title="React"
import { useMemo } from 'react';
import { GrafloriaFlow } from '@grafloria/react';
import { MemoryHub } from '@grafloria/engine';
import type { EdgeSpec, NodeSpec } from '@grafloria/renderer';

const nodes: NodeSpec[] = [
  { id: 'a', label: 'Alpha', position: { x: 80, y: 90 }, size: { width: 150, height: 66 } },
  { id: 'b', label: 'Beta', position: { x: 320, y: 90 }, size: { width: 150, height: 66 } },
];
const edges: EdgeSpec[] = [{ id: 'e1', source: 'a', target: 'b' }];

export default function App() {
  const peers = useMemo(() => {
    const hub = new MemoryHub();
    return {
      a: { transport: hub.connect('ana'), actor: 'ana' },
      b: { transport: hub.connect('bo'), actor: 'bo' },
    };
  }, []);

  return (
    <div style={{ display: 'flex', height: 460 }}>
      <GrafloriaFlow defaultNodes={nodes} defaultEdges={edges} collab={peers.a}
        style={{ display: 'block', flex: 1 }} />
      <GrafloriaFlow defaultNodes={nodes} defaultEdges={edges} collab={peers.b}
        style={{ display: 'block', flex: 1 }} />
    </div>
  );
}
```
```vue title="Vue"
<script setup lang="ts">
import { GrafloriaFlow } from '@grafloria/vue';
import { MemoryHub } from '@grafloria/engine';
import type { EdgeSpec, NodeSpec } from '@grafloria/renderer';

const hub = new MemoryHub();
const nodesA: NodeSpec[] = [
  { id: 'a', label: 'Alpha', position: { x: 80, y: 90 }, size: { width: 150, height: 66 } },
  { id: 'b', label: 'Beta', position: { x: 320, y: 90 }, size: { width: 150, height: 66 } },
];
const nodesB: NodeSpec[] = [
  { id: 'a', label: 'Alpha', position: { x: 80, y: 90 }, size: { width: 150, height: 66 } },
  { id: 'b', label: 'Beta', position: { x: 320, y: 90 }, size: { width: 150, height: 66 } },
];
const edgesA: EdgeSpec[] = [{ id: 'e1', source: 'a', target: 'b' }];
const edgesB: EdgeSpec[] = [{ id: 'e1', source: 'a', target: 'b' }];
const collabA = { transport: hub.connect('ana'), actor: 'ana' };
const collabB = { transport: hub.connect('bo'), actor: 'bo' };
</script>

<template>
  <div style="display:flex; height:460px">
    <GrafloriaFlow :default-nodes="nodesA" :default-edges="edgesA" :collab="collabA"
      style="display:block; flex:1" />
    <GrafloriaFlow :default-nodes="nodesB" :default-edges="edgesB" :collab="collabB"
      style="display:block; flex:1" />
  </div>
</template>
```
:::

Each Angular, Qwik, React, and Vue version mounts two canvases with the same two nodes and connecting edge. Move a node in either pane: the other pane receives the operation and renders the edit. The framework canvas joins a CRDT sync session when it mounts and leaves when it unmounts; set up its collaboration options before mounting.

The diagram data is the document peers converge on. CRDT operations merge per property, so concurrent changes to different properties of the same node—for example, one peer moving it while another renames it—both survive. This is not whole-node last-write-wins replacement.

## Options and boundaries

| Option | Type | Default | What it does |
| --- | --- | --- | --- |
| `collab` | Object containing a transport and actor id | Unset | Joins the canvas to a sync session. Give each peer a distinct actor id and connect its transport to the same room. |

The collaboration option is fixed for the lifetime of a mounted instance. If a peer needs a new transport or actor, mount an instance with that configuration rather than changing `collab` in place. In Qwik, create transports in `useVisibleTask$` and retain them with Qwik's non-serializable state handling; a live transport is not serializable application data.

Grafloria starts an anti-entropy round when a transport reports that it has reconnected, exchanging the operations each peer missed. Grafloria supplies convergence and the operation log, not your product's rooms, authentication, or storage; implement those at the transport and application layers.

## See the synchronization states

In the conflict-resolution demo, peer A moves a node while peer B renames it. The status readouts disagree before exchange and show both edits after they converge. The initial view shows both peers with the same `Draft` node.

[Open the live conflict-resolution demo](https://grafloria.com/demos/collab/conflict-resolution.html).

For disconnected edits, open the offline-and-reconnect demo. Disconnect both peers, edit each side, and reconnect; the missed operations are exchanged and both diagrams converge. Its initial view shows two matching diagrams with `Alpha` connected to `Beta`.

[Open the live offline-and-reconnect demo](https://grafloria.com/demos/collab/offline-and-reconnect.html).

For actual cross-tab collaboration without a server, open the two-tabs demo in two browser tabs. It uses `BroadcastChannel` for transport, so edits in either tab reach the other; each tab keeps its own viewport. Its initial view shows the same two-node diagram in both panes.

[Open the live two-tabs demo](https://grafloria.com/demos/collab/two-tabs-live.html).

## Pitfalls

- Use one hub/room for peers that share a document, but a distinct actor id for each peer. The sender does not receive its own echo from [`MemoryHub`](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/grafloria-engine-sync-classes#memoryhub).
- A shared transport has to report status changes if you rely on automatic reconnect catch-up.
- The plain JavaScript [`render`](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/grafloria-element-core#render) API mounts a diagram but does not accept the framework `collab` prop. Its sample above demonstrates mounting only; it does not connect peers. Use a framework binding's collaboration prop for peer synchronization.
- Do not treat the collaboration channel as persistence or authorization. The app still owns rooms, identity, and storage.

## Related

- [Add presence and comments](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/add-presence-and-comments) — add live cursors and anchored threads to a collaboration session.
- [The graph model and document](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/graph-model-and-document) — understand the document peers synchronize.
- [Command history and edits](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/command-history) — understand how local edits become operations.
