Skip to content

board.storage - Shared State

Synchronize component state between all participants in real-time.

Overview

board.storage is the primary API for data synchronization. All operations automatically sync across participants via WebSocket.

Basic Operations

get() - Read Value

typescript
// Get value (returns undefined if not set)
const count = board.storage.get<number>('count');

// Get with default value
const count = board.storage.get<number>('count', 0);

set() - Write Value

typescript
// Set value (automatically syncs to all participants)
await board.storage.set('count', 5);

// Set multiple values
await board.storage.set('user', { name: 'Alice', score: 100 });

update() - Atomic Update

typescript
// Atomic increment (prevents race conditions)
await board.storage.update<number>('count', (prev) => (prev || 0) + 1);

// Update object
await board.storage.update('user', (prev) => ({
  ...prev,
  score: (prev?.score || 0) + 10
}));

delete() - Remove Value

typescript
await board.storage.delete('count');

Subscriptions

subscribe() - Watch for Changes

typescript
// Subscribe to specific key
const unsubscribe = board.storage.subscribe<number>('count', (newValue, oldValue) => {
  console.log(`Count changed: ${oldValue} → ${newValue}`);
  updateUI(newValue);
});

// Unsubscribe when done
unsubscribe();

subscribeAll() - Watch All Changes

typescript
board.storage.subscribeAll((changes) => {
  // changes: Array<{ key: string, newValue: any, oldValue: any }>
  changes.forEach(change => {
    console.log(`${change.key} changed to ${change.newValue}`);
  });
});

Get All State

typescript
const fullState = board.storage.getAll();
// { count: 5, user: { name: 'Alice', score: 100 } }

Examples

Counter Component

typescript
import { BoardAPI } from '@boardapi/sdk';

const board = new BoardAPI();

// Initialize counter
board.on('ready', async () => {
  const count = board.storage.get<number>('count', 0);
  updateDisplay(count);
});

// Increment button
document.querySelector('#increment').addEventListener('click', async () => {
  await board.storage.update<number>('count', (prev) => (prev || 0) + 1);
});

// Listen to changes from other users
board.storage.subscribe<number>('count', (newValue) => {
  updateDisplay(newValue);
});

function updateDisplay(value: number) {
  document.querySelector('#count').textContent = value.toString();
}

Voting Component

typescript
interface Vote {
  userId: string;
  value: number;
  timestamp: string;
}

// Cast vote
async function castVote(value: number) {
  const votes = board.storage.get<Vote[]>('votes', []);

  await board.storage.set('votes', [
    ...votes,
    {
      userId: board.participants.me.id,
      value,
      timestamp: new Date().toISOString()
    }
  ]);
}

// Watch for new votes
board.storage.subscribe<Vote[]>('votes', (votes) => {
  renderVotes(votes);
  updateStats(votes);
});

Form Component

typescript
interface FormData {
  name: string;
  email: string;
  message: string;
}

// Auto-save form
const form = document.querySelector<HTMLFormElement>('#myForm');

form.addEventListener('input', async (e) => {
  const target = e.target as HTMLInputElement;

  await board.storage.update<FormData>('formData', (prev) => ({
    ...prev,
    [target.name]: target.value
  }));
});

// Restore form on load
board.on('ready', () => {
  const formData = board.storage.get<FormData>('formData');
  if (formData) {
    Object.entries(formData).forEach(([key, value]) => {
      const input = form.querySelector<HTMLInputElement>(`[name="${key}"]`);
      if (input) input.value = value;
    });
  }
});

Private Storage (Advanced)

Store data that's not visible to other participants:

typescript
// Private value (only visible to current user)
await board.storage.setPrivate('myVote', 8);

// Private with limited visibility
await board.storage.setPrivate('myAnswer', 'B', {
  visibleTo: ['teacher', 'admin']
});

// Anonymous value (no author tracking)
await board.storage.setAnonymous('feedback', 'Great session!');

// Host reveals all private values
await board.server.reveal('myVote');

// Listen for reveal
board.on('storage:revealed', (key, values) => {
  // values: [{ value: 8, participantId: 'user-1' }, ...]
  showResults(values);
});

Use cases:

  • Planning Poker - Hide votes until reveal
  • Anonymous Surveys - Collect feedback anonymously
  • Exams - Prevent cheating

Transient State (High-Frequency Updates)

For smooth animations and drag-and-drop:

typescript
// Slider - smooth real-time updates
slider.addEventListener('input', (e) => {
  // High-frequency updates (60+ FPS)
  board.storage.setTransient('sliderValue', e.target.value);
});

// Commit to database when done
slider.addEventListener('change', (e) => {
  board.storage.set('sliderValue', e.target.value);
});

// Listen to transient updates
board.storage.onTransient('sliderValue', (value) => {
  animateSlider(value);
});

Transient vs Regular:

  • Transient: RAM only, no DB, high frequency (60/sec)
  • Regular: Persisted to DB, undo/redo, throttled (10/sec)

Rate Limits

  • Regular storage: 30 updates/sec per component
  • Transient storage: 60 updates/sec per key
  • Max payload: 100KB per operation

Exceeding limits returns error:

typescript
{
  success: false,
  error: 'Rate limit exceeded (max 30 updates/sec)'
}

TypeScript Types

typescript
interface MyStorage {
  count: number;
  votes: Array<{ userId: string; value: number }>;
  formData: { name: string; email: string };
}

const board = new BoardAPI<MyStorage>();

// Fully typed!
const count = board.storage.get('count'); // number | undefined
board.storage.set('unknownKey', 123);     // TypeScript Error!

Best Practices

  1. Use update() for counters - Prevents race conditions
  2. Subscribe early - Subscribe before setting initial values
  3. Unsubscribe on cleanup - Prevent memory leaks
  4. Use transient for animations - Keep DB clean
  5. Validate data - Always validate user input

What's Next?