2 Realtime
Anton Nesterov edited this page 2026-02-22 22:54:39 +01:00

Realtime

The real-time engine allows you to subscribe to collection changes and receive instant updates via WebSocket connections.

Subscribing to Collections

Basic Subscription

import { VskiClient } from "@vski/sdk";

const client = new VskiClient("http://localhost:3000");

// Subscribe to a collection
const unsubscribe = client.collection("posts").subscribe((event) => {
  console.log("Event received:", event);
});

// Event structure:
{
  action: "create" | "update" | "delete",
  record: { id, title, ... }, // The record that changed
}

Unsubscribing

// Stop receiving events
unsubscribe();

Event Types

VSKI emits three types of events:

Create Events

client.collection("posts").subscribe((event) => {
  if (event.action === "create") {
    console.log("New post created:", event.record);
  }
});

Update Events

client.collection("posts").subscribe((event) => {
  if (event.action === "update") {
    console.log("Post updated:", event.record);
  }
});

Delete Events

client.collection("posts").subscribe((event) => {
  if (event.action === "delete") {
    console.log("Post deleted:", event.record.id);
  }
});

Filtering Subscriptions

Subscribe only to specific records matching a filter:

// Only receive events for published posts
client.collection("posts").subscribe((event) => {
  console.log("Published post changed:", event.record);
}, {
  filter: "published = true",
});

Multiple Subscriptions

You can subscribe to multiple collections simultaneously:

const unsub1 = client.collection("posts").subscribe((event) => {
  console.log("Post event:", event);
});

const unsub2 = client.collection("comments").subscribe((event) => {
  console.log("Comment event:", event);
});

// Unsubscribe from all when done
unsub1();
unsub2();

Real-time Groups

Real-time groups allow you to distribute events among multiple consumers using groups:

Subscribing to Groups

When multiple clients subscribe to the same collection with a group, events are distributed among them:

// Client 1 subscribes to "worker-group"
client.collection("posts").subscribe((event) => {
  console.log("Client 1 received event:", event);
}, {
  group: "worker-group",
});

// Client 2 also subscribes to "worker-group"
client.collection("posts").subscribe((event) => {
  console.log("Client 2 received event:", event);
}, {
  group: "worker-group",
});

// Events are distributed between client 1 and client 2

Default Group (Fan-out)

If no group is specified, all clients receive all events:

// All clients receive every event
client.collection("posts").subscribe((event) => {
  console.log("Received event:", event);
});

Acknowledging Events

When using groups, you can acknowledge events to receive the next one:

// When subscribing with a group, events are sent with an ID
client.collection("posts").subscribe((event) => {
  console.log("Received event with ID:", event.ackId);

  // Process the event

  // Send ACK to receive next event
  // Note: This is handled automatically by the SDK
}, {
  group: "worker-group",
});

WebSocket Connection Management

The SDK manages WebSocket connections automatically. When you subscribe to a collection, the SDK:

  1. Establishes a WebSocket connection if not already connected
  2. Sends subscription messages to the server
  3. Automatically reconnects if the connection is lost
  4. Resends subscriptions after reconnection

Automatic Connection

WebSocket connections are established automatically when you subscribe:

// This automatically establishes WebSocket connection if needed
const unsubscribe = client.collection("posts").subscribe((event) => {
  console.log("Event:", event);
});

Disconnection

Close the client to stop all WebSocket connections:

// Stop all subscriptions and close WebSocket
client.close();

Practical Examples

Chat Application

// Send a message
await client.collection("messages").create({
  roomId: "room-123",
  userId: "user-123",
  text: "Hello!",
});

// Subscribe to new messages in the room
client.collection("messages").subscribe((event) => {
  if (event.action === "create" && event.record.roomId === "room-123") {
    displayMessage(event.record);
  }
}, {
  filter: "roomId = 'room-123'",
});

Live Dashboard

// Subscribe to metrics collection
client.collection("metrics").subscribe((event) => {
  if (event.action === "update") {
    updateDashboard(event.record);
  }
});

// Update metrics
await client.collection("metrics").update("metric-id", {
  value: calculateNewValue(),
});

Collaborative Editing

// Document changes
client.collection("documents").subscribe((event) => {
  if (event.action === "update" && event.record.id === currentDocId) {
    // Update local document with remote changes
    mergeChanges(event.record.content);
  }
});

Notification System

// Subscribe to user notifications
client.collection("notifications").subscribe((event) => {
  if (event.action === "create" && event.record.userId === currentUserId) {
    showNotification(event.record);
  }
}, {
  filter: `userId = '${currentUserId}'`,
});

Performance Considerations

Rate Limiting

Avoid overwhelming the client with rapid updates:

let lastUpdate = 0;
client.collection("metrics").subscribe((event) => {
  const now = Date.now();
  if (now - lastUpdate > 1000) { // Limit to once per second
    updateUI(event.record);
    lastUpdate = now;
  }
});

Batch Processing

Process multiple events in batches:

const eventQueue = [];

client.collection("posts").subscribe((event) => {
  eventQueue.push(event);
});

// Process queue periodically
setInterval(() => {
  if (eventQueue.length > 0) {
    processBatch(eventQueue.splice(0));
  }
}, 1000);

Selective Subscriptions

Only subscribe to collections you need:

// Unsubscribe when done
const unsubscribe = client.collection("posts").subscribe((event) => {
  console.log("Event:", event);
});

// Later, stop receiving events
unsubscribe();

Error Handling

client.collection("posts").subscribe(
  (event) => {
    console.log("Event:", event);
  },
  {
    filter: "published = true",
  },
  (error) => {
    console.error("Subscription error:", error);
    // Handle connection errors, etc.
  },
);

Authentication

WebSocket connections use the same authentication as HTTP requests. The SDK automatically includes your authentication token in the WebSocket connection URL:

// Set authentication token
client.setToken("your-jwt-token");

// WebSocket connection will include token automatically
const unsubscribe = client.collection("posts").subscribe((event) => {
  console.log("Event:", event);
});

Testing Real-time Features

// In tests, you can simulate real-time events
import { VskiClient } from "@vski/sdk";

const client = new VskiClient("http://localhost:3000");

let receivedEvent = null;

client.collection("posts").subscribe((event) => {
  receivedEvent = event;
});

// Create a record
await client.collection("posts").create({
  title: "Test Post",
});

// Wait for event
await new Promise((resolve) => setTimeout(resolve, 500));

assert(receivedEvent !== null);
assert(receivedEvent.action === "create");
assert(receivedEvent.record.title === "Test Post");

Security

Authentication Required

All real-time subscriptions require authentication:

// Unauthenticated connections will be rejected
const client = new VskiClient("http://localhost:3000");
// client.setToken("your-token"); // Must set token before subscribing

client.collection("posts").subscribe((event) => {
  console.log(event);
}); // Will throw error if not authenticated

Authorization

Real-time events respect collection rules:

// If a user doesn't have permission to see a record,
// they won't receive events for that record
await client.settings.rules.create({
  name: "Owner Only",
  collection: "posts",
  rule: "userId = @request.auth.id",
});

WebSocket Protocol

Connection URL

ws://localhost:3000/api/realtime?db=default&auth=your-token

The SDK automatically constructs this URL based on your baseUrl, dbName, and authentication token.

Subscription Messages

The SDK sends subscription messages to the server:

{
  "type": "SUBSCRIBE",
  "collection": "posts",
  "filter": "published = true",
  "group": "worker-group"
}

Event Messages

The server sends event messages to subscribed clients:

{
  "type": "EVENT",
  "collection": "posts",
  "events": [
    {
      "id": "event-id",
      "action": "create",
      "ackId": "ack-id",
      "data": {
        "id": "record-id",
        "title": "Hello",
        ...
      }
    }
  ]
}

ACK Messages

When using groups, clients can acknowledge events:

{
  "type": "ACK",
  "collection": "posts",
  "group": "worker-group",
  "id": "ack-id"
}

Best Practices

  1. Always unsubscribe - Remove subscriptions when components unmount
  2. Use filters - Reduce unnecessary event traffic
  3. Handle errors - Implement proper error handling for connections
  4. Limit subscriptions - Don't subscribe to everything at once
  5. Use presence wisely - Track only what you need
  6. Optimize reconnection - Configure appropriate reconnection intervals
  7. Rate limit updates - Prevent overwhelming clients with rapid changes