Skip to content

Shared Data Structures

P2P provides three CRDT-backed data structures that sync automatically between all peers in a room. They're created via the P2PManager instance:

typescript
const p2p = await client.p2p;
const map = p2p.sharedMap('my-map');
const list = p2p.sharedList('my-list');
const counter = p2p.sharedCounter('my-counter');

Each call with the same name returns the same instance. Names are scoped to the room.

Limits

The number of shared data structures per room is limited by the environment config (maxSharedObjects, default 20). Exceeding this throws an error.

SharedMap

A key-value store that syncs across all peers. Conflict resolution: last-writer-wins per key.

API

typescript
const board = p2p.sharedMap<{ title: string; column: string }>('tasks');

board.set('task-1', { title: 'Design homepage', column: 'doing' });
board.get('task-1');        // { title: 'Design homepage', column: 'doing' }
board.has('task-1');        // true
board.delete('task-1');     // true
board.size;                 // 0
board.keys();               // string[]
board.values();             // T[]
board.entries();            // [string, T][]
board.forEach((val, key) => { ... });
board.toJSON();             // { 'task-1': { title: '...', column: '...' } }
MethodReturnsDescription
get(key)T | undefinedGet value by key
set(key, value)voidSet a key-value pair
delete(key)booleanDelete a key, returns whether it existed
has(key)booleanCheck if key exists
keys()string[]All keys
values()T[]All values
entries()[string, T][]All key-value pairs
forEach(fn)voidIterate all entries
sizenumberNumber of entries
toJSON()Record<string, T>Serialize to plain object
on('change', handler)() => voidSubscribe to changes (returns unsubscribe)
destroy()voidClean up observers

Change Events

typescript
const unsub = board.on('change', (changes) => {
  for (const change of changes) {
    console.log(change.action, change.key, change.value, change.oldValue);
  }
});

// Stop listening
unsub();

Each change has:

FieldTypeDescription
action'add' | 'update' | 'delete'What happened
keystringThe affected key
valueT | undefinedNew value (for add/update)
oldValueT | undefinedPrevious value (for update/delete)

Use Cases

  • Kanban board state (taskId → { column, title, assignee })
  • User settings / preferences
  • Feature flags / toggles
  • Form state across collaborators

SharedList

An ordered array that syncs across all peers. Supports insert, delete, and move operations. Conflict resolution: concurrent inserts get deterministic positions.

API

typescript
const tasks = p2p.sharedList<string>('task-order');

tasks.push('task-1', 'task-2');       // Append items
tasks.insert(0, 'task-0');            // Insert at index
tasks.get(0);                         // 'task-0'
tasks.delete(1);                      // Remove at index
tasks.delete(0, 2);                   // Remove 2 items starting at index 0
tasks.move(2, 0);                     // Move item from index 2 to index 0
tasks.indexOf('task-1');              // Find index of item
tasks.toArray();                      // ['task-0', 'task-1', 'task-2']
tasks.length;                         // 3
MethodReturnsDescription
get(index)T | undefinedGet item at index
push(...items)voidAppend items to end
insert(index, item)voidInsert at specific index
delete(index, count?)voidDelete items (default count: 1)
move(from, to)voidMove item from one index to another
indexOf(item)numberFind index (-1 if not found)
toArray()T[]Snapshot as plain array
lengthnumberNumber of items
on('change', handler)() => voidSubscribe to changes (returns unsubscribe)
destroy()voidClean up observers

Change Events

typescript
tasks.on('change', (changes) => {
  for (const change of changes) {
    if (change.action === 'insert') {
      console.log('Inserted at', change.index, change.values);
    } else {
      console.log('Deleted at', change.index);
    }
  }
});
FieldTypeDescription
action'insert' | 'delete'What happened
indexnumberPosition of the change
valuesT[] | undefinedInserted items (for insert only)

Use Cases

  • Task ordering within kanban columns
  • Layer stacks in a drawing tool
  • Playlist / queue ordering
  • Chat message ordering

SharedCounter

A distributed counter that always converges to the correct value, even with concurrent increments from multiple peers.

How It Works

Each peer tracks its own increment total. The counter value is the sum of all peers' contributions. This avoids conflicts — two peers incrementing simultaneously both succeed.

API

typescript
const votes = p2p.sharedCounter('vote-count');

votes.increment();        // +1
votes.increment(5);       // +5
votes.decrement();        // -1
votes.decrement(3);       // -3
votes.value;              // Current total (sum of all peers)
MethodReturnsDescription
valuenumberCurrent counter value
increment(amount?)voidAdd to counter (default 1)
decrement(amount?)voidSubtract from counter (default 1)
on('change', handler)() => voidSubscribe to changes (returns unsubscribe)
destroy()voidClean up observers

Change Events

typescript
votes.on('change', (value) => {
  document.getElementById('count').textContent = value;
});

The handler receives the new total value as a number.

Use Cases

  • Like / vote counters
  • View counts
  • Score tracking
  • Online user counts

Combining Data Structures

You can use multiple data structures together for complex collaborative UIs:

typescript
const p2p = await client.p2p;

// Kanban board with ordered columns
const tasks = p2p.sharedMap('tasks');         // task data
const columns = p2p.sharedMap('columns');     // column metadata
const todoOrder = p2p.sharedList('col-todo'); // task order per column
const doingOrder = p2p.sharedList('col-doing');

// Add a task
tasks.set('task-1', { title: 'Fix bug', assignee: 'Alice' });
todoOrder.push('task-1');

// Move task to "doing"
todoOrder.delete(todoOrder.indexOf('task-1'));
doingOrder.push('task-1');
tasks.set('task-1', { ...tasks.get('task-1'), column: 'doing' });

Persistence

All shared data auto-persists to the server. You don't need to call save manually.

BehaviorDetails
Auto-save intervalEvery 5 seconds (configurable: p2pPersistIntervalMs)
Late-joiner bootstrapNew peers load persisted state, then sync live updates
TTLState expires after 30 days of inactivity (configurable: p2pStateTTLDays)
Size limit5 MB per room by default (configurable: p2pMaxStateBytes)

See Environment Config for all P2P settings.

Pulse Collaboration SDK