Skip to content

Real-Time Updates

Problem

You need real-time features: WebSocket messaging, collaborative state, presence (who's online), optimistic updates with rollback, and reconnection handling.

Solution

Use createSocket for WebSocket connections and createSync for CRDT-based collaborative state.

1. Chat Application with WebSocket

ts
// src/chat.ts
import { signal } from '@akashjs/runtime';
import { createSocket } from '@akashjs/http';

interface Message {
  id: string;
  user: string;
  text: string;
  timestamp: number;
}

const messages = signal<Message[]>([]);
const input = signal('');

const ws = createSocket('wss://api.example.com/chat', {
  autoReconnect: true,
  maxRetries: 10,
  reconnectDelay: 1000,
  maxReconnectDelay: 30000,
});

// Listen for incoming messages
ws.on('message', (data) => {
  const msg = data as Message;
  messages.update((prev) => [...prev, msg]);
});

function sendMessage() {
  const text = input().trim();
  if (!text) return;

  const msg: Message = {
    id: crypto.randomUUID(),
    user: currentUser.name,
    text,
    timestamp: Date.now(),
  };

  // Optimistic: show immediately
  messages.update((prev) => [...prev, msg]);
  ws.send({ type: 'chat', payload: msg });
  input.set('');
}

2. Template

html
<div class="chat">
  <!-- Connection status banner -->
  <div :if={ws.status() !== 'open'} class="status-banner">
    <span :if={ws.status() === 'connecting'}>Connecting...</span>
    <span :if={ws.status() === 'closed'}>
      Disconnected.
      <button @click={ws.reconnect}>Reconnect</button>
    </span>
    <span :if={ws.status() === 'error'}>Connection error. Retrying...</span>
  </div>

  <!-- Messages -->
  <div class="messages" :ref={scrollToBottom}>
    <div :for={msg of messages()} :key={msg.id} class="message">
      <strong>{msg.user}</strong>
      <span class="time">{formatTime(msg.timestamp)}</span>
      <p>{msg.text}</p>
    </div>
  </div>

  <!-- Input -->
  <form class="chat-input" @submit|preventDefault={sendMessage}>
    <input :value={input}
           @input={e => input.set(e.target.value)}
           placeholder="Type a message..."
           :disabled={ws.status() !== 'open'} />
    <button type="submit" :disabled={ws.status() !== 'open'}>Send</button>
  </form>
</div>

TIP

Disable the input when the socket is not open. Queuing messages during disconnection sounds nice but leads to confusing UX when messages arrive out of order after reconnect.

3. Collaborative State with createSync

ts
// src/collab-editor.ts
import { createSync, createWebSocketTransport } from '@akashjs/runtime';

const doc = createSync('document-abc', {
  title: 'Untitled',
  content: '',
  lastEditedBy: '',
}, {
  transport: createWebSocketTransport({
    url: 'wss://sync.example.com',
    room: 'document-abc',
  }),
});

// Connect on mount
doc.connect();

// Setting state auto-syncs to all peers
function updateTitle(newTitle: string) {
  doc.state.title.set(newTitle);
  doc.state.lastEditedBy.set(currentUser.name);
}
html
<input :value={doc.state.title}
       @input={e => updateTitle(e.target.value)}
       placeholder="Document title" />
<p class="muted">Last edited by: {doc.state.lastEditedBy()}</p>

4. Presence (Who's Online)

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

// Broadcast cursor position as presence
effect(() => {
  doc.presence.set({
    name: currentUser.name,
    color: currentUser.color,
    cursor: cursorPosition(),
  });
});
html
<!-- Show other users' cursors -->
<div :for={[peerId, data] of doc.peerPresence()} :key={peerId}>
  <div class="remote-cursor"
       :style="{ left: data.cursor.x + 'px', top: data.cursor.y + 'px',
                 borderColor: data.color }">
    <span class="cursor-label">{data.name}</span>
  </div>
</div>

<!-- Online users list -->
<div class="online-users">
  <span>{doc.peers().length + 1} online</span>
  <div :for={peer of doc.peers()} :key={peer.id} class="avatar-dot"
       :title={peer.id}></div>
</div>

5. Optimistic Updates with Rollback

ts
const items = signal<Item[]>([]);

function deleteItem(id: string) {
  const backup = [...items()];

  // Optimistic: remove immediately
  items.update((list) => list.filter((i) => i.id !== id));

  ws.send({ type: 'delete', payload: { id } });

  // Listen for server rejection
  const unsub = ws.on('message', (data) => {
    if (data.type === 'delete_failed' && data.payload.id === id) {
      items.set(backup); // Rollback
      unsub();
    }
    if (data.type === 'delete_confirmed' && data.payload.id === id) {
      unsub(); // Success, cleanup
    }
  });
}

WARNING

Always keep a backup of the previous state before optimistic updates. CRDT-based sync via createSync handles conflicts automatically, but raw WebSocket messages need manual rollback logic.

6. Reconnection Handling

ts
// createSocket handles reconnection automatically, but you may want
// to re-sync state after reconnecting
ws.on('open', () => {
  // Re-subscribe to channels after reconnect
  ws.send({ type: 'subscribe', channel: 'updates' });

  // Fetch missed messages
  const lastId = messages().at(-1)?.id;
  if (lastId) {
    ws.send({ type: 'sync', after: lastId });
  }
});

ws.on('close', () => {
  console.log('Connection lost, will retry automatically');
});

7. Styles

css
.status-banner {
  background: #fef3cd;
  color: #856404;
  padding: 0.5rem 1rem;
  text-align: center;
  font-size: 0.875rem;
}

.messages {
  flex: 1;
  overflow-y: auto;
  padding: 1rem;
}

.remote-cursor {
  position: absolute;
  width: 2px;
  height: 20px;
  border-left: 2px solid;
  pointer-events: none;
}

.cursor-label {
  position: absolute;
  top: -1.25rem;
  left: 0;
  font-size: 0.7rem;
  white-space: nowrap;
  background: inherit;
  padding: 0 4px;
  border-radius: 3px;
}

Result

A real-time system with WebSocket chat, CRDT-based collaborative editing, presence indicators, optimistic updates with rollback, and automatic reconnection. The reactive signals mean the UI updates instantly whether data comes from local actions or remote peers.

Released under the MIT License.