Skip to content

Collaborative Signals ​

AkashJS ships with built-in real-time collaboration powered by CRDTs (Conflict-free Replicated Data Types). With createSync(), multiple users can edit the same state simultaneously — conflicts resolve automatically, no server logic required.

What Are CRDTs? ​

Imagine two people editing a shared document at the same time, even while offline. When they reconnect, their changes merge without overwriting each other. That is what CRDTs do — they are data structures designed so that any order of operations produces the same result. No central authority needed.

AkashJS wraps this complexity in a familiar signals API. You read and write state the same way you always do — the syncing happens behind the scenes.

Basic Setup ​

ts
import { createSync, createWebSocketTransport } from '@akashjs/runtime';

const sync = createSync('my-room', {
  title: 'Untitled',
  content: '',
  votes: 0,
}, {
  transport: createWebSocketTransport('wss://sync.example.com'),
});

createSync() takes three arguments:

ArgumentDescription
roomIdA string identifying the shared room. All clients with the same room ID share state.
initialStateThe default state shape. Each property becomes a synced signal.
optionsConfiguration — at minimum, a transport.

State Signals ​

Every property on the initial state becomes a reactive signal that auto-syncs to all connected peers.

ts
// Read state (same as a regular signal)
sync.state.title();       // 'Untitled'
sync.state.content();     // ''

// Write state — change propagates to all peers
sync.state.title.set('Meeting Notes');
sync.state.content.set('Agenda item 1...');

// Update from previous value
sync.state.votes.update((v) => v + 1);

Use these in components exactly like regular signals:

ts
import { defineComponent } from '@akashjs/runtime';

const Editor = defineComponent((ctx) => {
  const sync = createSync('doc-123', { content: '' }, {
    transport: createWebSocketTransport('wss://sync.example.com'),
  });

  return () => (
    <div>
      <textarea
        value={sync.state.content()}
        on:input={(e) => sync.state.content.set(e.target.value)}
      />
      <p>Connected peers: {sync.peers().length}</p>
    </div>
  );
});

Transports ​

Transports define how state travels between clients.

WebSocket (production) ​

ts
import { createWebSocketTransport } from '@akashjs/runtime';

const transport = createWebSocketTransport('wss://sync.example.com', {
  reconnect: true,        // auto-reconnect on disconnect
  reconnectDelay: 1000,   // ms between retries
  maxRetries: 10,
});

Local (testing) ​

For unit tests and local development, use a local transport that syncs in-memory:

ts
import { createLocalTransport } from '@akashjs/runtime';

const transport = createLocalTransport();

// Both sync instances share state in memory
const syncA = createSync('room', { count: 0 }, { transport });
const syncB = createSync('room', { count: 0 }, { transport });

syncA.state.count.set(5);
syncB.state.count();  // 5

Presence Tracking ​

Track ephemeral per-user data like cursor positions, selections, or online status. Presence data is not persisted — it exists only while a peer is connected.

ts
const sync = createSync('room', { content: '' }, {
  transport: createWebSocketTransport('wss://sync.example.com'),
  presence: {
    cursor: { x: 0, y: 0 },
    name: 'Alice',
  },
});

// Update your own presence
sync.presence.set({ cursor: { x: 100, y: 200 } });

// Read all peers' presence (returns Map<peerId, presenceData>)
sync.peerPresence();
// Map { 'peer-xyz' => { cursor: { x: 50, y: 80 }, name: 'Bob' } }

Render cursors in a component:

ts
const Cursors = defineComponent((ctx) => {
  return () => (
    <div>
      <For each={[...sync.peerPresence().entries()]} key={([id]) => id}>
        {([id, peer]) => (
          <div
            class="cursor"
            style={`left: ${peer.cursor.x}px; top: ${peer.cursor.y}px`}
          >
            {peer.name}
          </div>
        )}
      </For>
    </div>
  );
});

Conflict Resolution (LWW Register) ​

AkashJS uses Last-Writer-Wins (LWW) Registers for conflict resolution. When two peers write to the same property at the same time, the write with the highest timestamp wins. This is deterministic — every client converges to the same value.

Peer A sets title = "Hello"   (timestamp: 1001)
Peer B sets title = "World"   (timestamp: 1002)

Result on all peers: title = "World"  (1002 > 1001)

This strategy works well for most real-time use cases. For text editing where you need character-level merging, combine collaborative signals with a text CRDT library.

Peers List ​

Track who is currently connected to the room:

ts
// Reactive list of connected peer IDs
sync.peers();  // ['peer-abc', 'peer-xyz']

// Your own peer ID
sync.peerId;   // 'peer-123'

// Connection state
sync.connected();  // true | false

Peers that disconnect are automatically removed from peerPresence() — their presence data is cleaned up.

Presence Helpers ​

AkashJS provides high-level helpers that build on top of the presence system, so you don't have to wire up common patterns yourself.

useCursor ​

Automatically tracks mouse movement and broadcasts cursor position to peers.

ts
import { useCursor } from '@akashjs/runtime';

const cursor = useCursor(sync, { throttle: 50 }); // ms

// Reactive accessors
cursor.x();  // current local x
cursor.y();  // current local y

// Stop tracking when done
cursor.dispose();

useCursor listens to mousemove on the document, throttles updates to the given interval (default 100ms), and sets presence automatically. In components, it disposes when the component is destroyed.

useTypingIndicator ​

Manages typing state with automatic timeout.

ts
import { useTypingIndicator } from '@akashjs/runtime';

const typing = useTypingIndicator(sync, { timeout: 2000 }); // ms

// Call on keystrokes
typing.start();

// Manual stop (also stops automatically after timeout)
typing.stop();

// Read state
typing.isTyping();       // true/false for local user
typing.othersTyping();   // ['peer-abc', 'peer-xyz']

Wire it into an input:

ts
<textarea
  on:input={() => typing.start()}
  on:blur={() => typing.stop()}
/>
{typing.othersTyping().length > 0 && (
  <span>{typing.othersTyping().length} typing...</span>
)}

Conflict Resolution ​

By default, AkashJS uses Last-Writer-Wins (LWW). For finer control, pass an onConflict callback in your sync options.

onConflict Callback ​

ts
const sync = createSync({ score: 0 }, {
  transport,
  onConflict({ key, localValue, remoteValue }) {
    // Return a value to auto-resolve
    if (key === 'score') return Math.max(localValue, remoteValue);

    // Return undefined to queue the conflict for manual resolution
    return undefined;
  },
});

If onConflict throws, AkashJS falls back to LWW so the document always converges.

Reactive Conflicts ​

Unresolved conflicts (where onConflict returned undefined) are exposed as a reactive signal:

ts
sync.conflicts();
// [{ key: 'title', localValue: 'Hello', remoteValue: 'World', localTimestamp: ..., remoteTimestamp: ..., remotePeerId: '...' }]

Resolve them manually:

ts
sync.resolveConflict('title', 'Hello World');

Once resolved, the entry is removed from sync.conflicts() and the chosen value propagates to all peers.

Synced Stores ​

For larger apps, you can enable sync directly on a store created with defineStore.

ts
import { defineStore, createWebSocketTransport } from '@akashjs/runtime';

const useTodos = defineStore('todos', {
  state: () => ({
    items: [],
    filter: 'all',
  }),
  sync: {
    transport: createWebSocketTransport('wss://sync.example.com'),
    room: 'todos-room',
    presence: true,
  },
});

When sync is provided, the store exposes a $sync object:

ts
const store = useTodos();

store.$sync.connected();      // true/false
store.$sync.peers();          // ['peer-abc']
store.$sync.presence;         // local presence (set/get)
store.$sync.peerPresence();   // all peers' presence data

Offline + Sync ​

Synced stores work alongside persist. Writes are saved locally first, then synced when a connection is available:

ts
const useTodos = defineStore('todos', {
  state: () => ({ items: [] }),
  persist: true,
  sync: {
    transport: createWebSocketTransport('wss://sync.example.com'),
    room: 'todos-room',
  },
});

Example: Collaborative Text Editor ​

A complete collaborative editor in under 40 lines:

ts
import { defineComponent } from '@akashjs/runtime';
import { createSync, createWebSocketTransport } from '@akashjs/runtime';

const CollaborativeEditor = defineComponent((ctx) => {
  const sync = createSync('editor-room', {
    title: 'Untitled Document',
    content: '',
  }, {
    transport: createWebSocketTransport('wss://sync.example.com'),
    presence: { cursor: 0, name: 'Anonymous' },
  });

  const handleInput = (e: InputEvent) => {
    sync.state.content.set((e.target as HTMLTextAreaElement).value);
  };

  const handleCursor = (e: Event) => {
    const pos = (e.target as HTMLTextAreaElement).selectionStart;
    sync.presence.set({ cursor: pos });
  };

  return () => (
    <div class="editor">
      <input
        value={sync.state.title()}
        on:input={(e) => sync.state.title.set(e.target.value)}
        placeholder="Document title"
      />
      <textarea
        value={sync.state.content()}
        on:input={handleInput}
        on:select={handleCursor}
        rows={20}
      />
      <aside class="peers">
        <h4>Online ({sync.peers().length})</h4>
        <ul>
          <For each={[...sync.peerPresence().entries()]} key={([id]) => id}>
            {([id, p]) => <li>{p.name} — position {p.cursor}</li>}
          </For>
        </ul>
      </aside>
    </div>
  );
});

Cleanup ​

Disconnect from the room when done:

ts
sync.disconnect();

In components, AkashJS automatically disconnects when the component is destroyed.

Released under the MIT License.