Appearance
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: '...' } }| Method | Returns | Description |
|---|---|---|
get(key) | T | undefined | Get value by key |
set(key, value) | void | Set a key-value pair |
delete(key) | boolean | Delete a key, returns whether it existed |
has(key) | boolean | Check if key exists |
keys() | string[] | All keys |
values() | T[] | All values |
entries() | [string, T][] | All key-value pairs |
forEach(fn) | void | Iterate all entries |
size | number | Number of entries |
toJSON() | Record<string, T> | Serialize to plain object |
on('change', handler) | () => void | Subscribe to changes (returns unsubscribe) |
destroy() | void | Clean 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:
| Field | Type | Description |
|---|---|---|
action | 'add' | 'update' | 'delete' | What happened |
key | string | The affected key |
value | T | undefined | New value (for add/update) |
oldValue | T | undefined | Previous 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| Method | Returns | Description |
|---|---|---|
get(index) | T | undefined | Get item at index |
push(...items) | void | Append items to end |
insert(index, item) | void | Insert at specific index |
delete(index, count?) | void | Delete items (default count: 1) |
move(from, to) | void | Move item from one index to another |
indexOf(item) | number | Find index (-1 if not found) |
toArray() | T[] | Snapshot as plain array |
length | number | Number of items |
on('change', handler) | () => void | Subscribe to changes (returns unsubscribe) |
destroy() | void | Clean 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);
}
}
});| Field | Type | Description |
|---|---|---|
action | 'insert' | 'delete' | What happened |
index | number | Position of the change |
values | T[] | undefined | Inserted 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)| Method | Returns | Description |
|---|---|---|
value | number | Current counter value |
increment(amount?) | void | Add to counter (default 1) |
decrement(amount?) | void | Subtract from counter (default 1) |
on('change', handler) | () => void | Subscribe to changes (returns unsubscribe) |
destroy() | void | Clean 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.
| Behavior | Details |
|---|---|
| Auto-save interval | Every 5 seconds (configurable: p2pPersistIntervalMs) |
| Late-joiner bootstrap | New peers load persisted state, then sync live updates |
| TTL | State expires after 30 days of inactivity (configurable: p2pStateTTLDays) |
| Size limit | 5 MB per room by default (configurable: p2pMaxStateBytes) |
See Environment Config for all P2P settings.