JSPM

  • Created
  • Published
  • Downloads 1249
  • Score
    100M100P100Q89894F
  • License ISC

XHub Chat Core SDK - Real-time messaging and chat functionality

Package Exports

  • @xhub-chat/core
  • @xhub-chat/core/lib/index.js

This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (@xhub-chat/core) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.

Readme

πŸš€ XHubChat SDK Documentation

Build the next generation of chat applications with XHubChat SDK - A powerful, feature-rich JavaScript/TypeScript SDK for building real-time messaging applications.

npm version TypeScript License


🌟 Why Choose XHubChat SDK?

XHubChat SDK is more than just a messaging libraryβ€”it's your gateway to building sophisticated, secure, and scalable chat applications. Whether you're developing a customer support platform, team collaboration tool, or social messaging app, XHubChat provides everything you need out of the box.

✨ Key Highlights

  • πŸ” Enterprise-Grade Security - End-to-end encryption with advanced crypto storage
  • ⚑ Real-time Messaging - Lightning-fast message delivery with sync capabilities
  • 🧩 Modular Architecture - Use what you need, when you need it
  • πŸ“± Cross-Platform - Works seamlessly across web, mobile, and desktop
  • 🎯 Developer-Friendly - Intuitive APIs with comprehensive TypeScript support
  • πŸ”„ Offline Support - Robust offline capabilities with automatic sync
  • πŸ“Š Rich Media - Support for files, images, reactions, and custom content

πŸ“š Table of Contents

  1. Quick Start
  2. Installation
  3. Core Concepts
  4. API Reference
  5. Examples
  6. Advanced Usage
  7. Best Practices
  8. Troubleshooting

πŸš€ Quick Start

Get up and running with XHubChat SDK in just a few minutes!

πŸ“¦ Installation

# Using npm
npm install @xhub-chat/core

# Using yarn
yarn add @xhub-chat/core

# Using pnpm
pnpm add @xhub-chat/core

⚑ Your First Chat Application

import { createClient } from '@xhub-chat/core';

// 🎯 Step 1: Initialize the client
const client = createClient({
  baseUrl: 'https://your-chat-server.com',
  accessToken: 'your_access_token',
  userId: '@alice:example.com',
});

// πŸš€ Step 2: Start the client
await client.startClient();

// πŸ’¬ Step 3: Listen for messages
client.on('Room.timeline', (event, room, toStartOfTimeline) => {
  if (event.getType() === 'm.room.message' && !toStartOfTimeline) {
    const sender = event.getSender();
    const content = event.getContent();
    console.log(`${sender}: ${content.body}`);
  }
});

// πŸ“ Step 4: Send your first message
const roomId = '!example:your-server.com';
await client.sendTextMessage(roomId, 'Hello, XHubChat! πŸŽ‰');

πŸŽ‰ Congratulations! You've just built your first chat application with XHubChat SDK!


πŸ’‘ Core Concepts

Understanding these core concepts will help you leverage the full power of XHubChat SDK:

πŸ—οΈ Client Architecture

The XHubChatClient is the heart of your application. It manages:

  • Connection management - Handles network connectivity and reconnection
  • Event processing - Processes incoming messages and events
  • State synchronization - Keeps your local state in sync with the server
  • Crypto operations - Manages end-to-end encryption

🏠 Rooms

Rooms are where conversations happen. They can be:

  • Direct messages - One-on-one conversations
  • Group chats - Multi-user conversations
  • Channels - Public or private discussion spaces
  • Spaces - Organizational containers for related rooms

πŸ“¨ Events

Everything in XHubChat is an event:

  • Messages - Text, media, or custom content
  • Reactions - Emoji responses to messages
  • Receipts - Read confirmations
  • State changes - Room updates, member changes, etc.

πŸ”„ Synchronization

XHubChat automatically synchronizes:

  • Message history - Backfills messages when you come online
  • Room state - Keeps room metadata up to date
  • User presence - Shows who's online and available
  • Device state - Manages encryption keys across devices

πŸ› οΈ API Reference

🎯 Client Creation & Configuration

createClient(options: ICreateClientOpts): XHubChatClient

Creates a new XHubChat client instance.

import { createClient, MemoryStore, XHubChatScheduler } from '@xhub-chat/core';

const client = createClient({
  // πŸ”— Connection settings
  baseUrl: 'https://your-server.com',
  accessToken: 'your_access_token',
  userId: '@user:example.com',

  // πŸ—„οΈ Storage configuration
  store: new MemoryStore(), // or IndexedDBStore for persistence

  // ⏰ Task scheduling
  scheduler: new XHubChatScheduler(),

  // πŸ” Crypto settings
  cryptoStore: new MemoryCryptoStore(),

  // 🌐 Network options
  request: fetch, // Custom HTTP implementation

  // πŸŽ›οΈ Feature toggles
  useAuthorizationHeader: true,
  timelineSupport: true,
});

Configuration Options:

Option Type Description
baseUrl string Your XHubChat server URL
accessToken string User authentication token
userId string Full user ID (e.g., @user:domain.com)
store IStore Data persistence layer
scheduler IScheduler Task scheduling system
cryptoStore CryptoStore Encryption key storage

πŸ”„ Client Lifecycle

Starting the Client

// πŸš€ Start with default sync
await client.startClient();

// 🎯 Start with custom options
await client.startClient({
  initialSyncLimit: 20, // Number of recent messages to sync
  includeArchivedRooms: false, // Skip archived rooms
  resolveInvitesFromPhoneBook: true, // Auto-resolve contacts
  pollTimeout: 30000, // Long-polling timeout
  disablePresence: false, // Enable presence updates
  lazyLoadMembers: true, // Load room members on demand
});

Stopping the Client

// πŸ›‘ Graceful shutdown
client.stopClient();

// πŸ’₯ Force stop (not recommended)
client.stopClient({ force: true });

πŸ’¬ Messaging

Send Text Messages

// ✍️ Simple text message
await client.sendTextMessage(roomId, 'Hello World!');

// 🎨 Formatted message with HTML
await client.sendHtmlMessage(
  roomId,
  'Check out this <strong>bold</strong> text!',
  'Check out this **bold** text!' // Fallback plain text
);

// 🧡 Reply to a message
await client.sendTextMessage(roomId, 'Great point!', {
  'm.relates_to': {
    'm.in_reply_to': {
      event_id: '$original_message_id',
    },
  },
});

Send Rich Content

// πŸ“ Send a file
const fileContent = {
  msgtype: 'm.file',
  body: 'document.pdf',
  filename: 'document.pdf',
  info: {
    size: 1024,
    mimetype: 'application/pdf',
  },
  url: 'mxc://server.com/file_id',
};
await client.sendMessage(roomId, fileContent);

// πŸ–ΌοΈ Send an image
const imageContent = {
  msgtype: 'm.image',
  body: 'screenshot.png',
  info: {
    w: 800,
    h: 600,
    size: 2048,
    mimetype: 'image/png',
  },
  url: 'mxc://server.com/image_id',
};
await client.sendMessage(roomId, imageContent);

Reactions & Interactions

// πŸ‘ Add a reaction
await client.sendEvent(roomId, 'm.reaction', {
  'm.relates_to': {
    rel_type: 'm.annotation',
    event_id: '$message_id',
    key: 'πŸ‘',
  },
});

// ✏️ Edit a message
await client.sendEvent(roomId, 'm.room.message', {
  msgtype: 'm.text',
  body: '* Updated message text',
  'm.new_content': {
    msgtype: 'm.text',
    body: 'Updated message text',
  },
  'm.relates_to': {
    rel_type: 'm.replace',
    event_id: '$original_message_id',
  },
});

// πŸ—‘οΈ Delete a message (redaction)
await client.redactEvent(roomId, '$message_id', 'Spam content');

🏠 Room Management

Creating Rooms

// πŸ’¬ Create a direct message
const dmRoom = await client.createRoom({
  is_direct: true,
  invite: ['@friend:example.com'],
  preset: 'trusted_private_chat',
});

// 🏒 Create a group chat
const groupRoom = await client.createRoom({
  name: 'Project Alpha Team',
  topic: 'Discussion about Project Alpha',
  visibility: 'private',
  preset: 'private_chat',
  invite: ['@alice:example.com', '@bob:example.com'],
  room_alias_name: 'project-alpha',
  power_level_content_override: {
    users_default: 0,
    events_default: 0,
    redact: 50,
    ban: 50,
    kick: 50,
  },
});

// πŸ“’ Create a public channel
const publicRoom = await client.createRoom({
  name: 'General Discussion',
  topic: 'Open chat for everyone',
  visibility: 'public',
  preset: 'public_chat',
  room_alias_name: 'general',
});

Joining & Leaving Rooms

// πŸšͺ Join a room
await client.joinRoom('#general:example.com');

// πŸƒ Leave a room
await client.leave(roomId);

// 🚫 Kick a user
await client.kick(roomId, '@user:example.com', 'Violation of rules');

// πŸ”¨ Ban a user
await client.ban(roomId, '@user:example.com', 'Repeated violations');

// 🎭 Invite users
await client.invite(roomId, '@newuser:example.com');

πŸ‘‚ Event Handling

XHubChat uses an event-driven architecture. Here are the most important events:

Message Events

// πŸ’¬ New messages
client.on('Room.timeline', (event, room, toStartOfTimeline) => {
  if (event.getType() === 'm.room.message' && !toStartOfTimeline) {
    const message = event.getContent();
    const sender = event.getSender();
    const timestamp = event.getTs();

    console.log(`[${new Date(timestamp)}] ${sender}: ${message.body}`);
  }
});

// ✏️ Message edits
client.on('Room.timeline', event => {
  if (event.getType() === 'm.room.message' && event.getContent()['m.relates_to']?.rel_type === 'm.replace') {
    const newContent = event.getContent()['m.new_content'];
    console.log('Message edited:', newContent.body);
  }
});

// πŸ‘ Reactions
client.on('Room.timeline', event => {
  if (event.getType() === 'm.reaction') {
    const reaction = event.getContent()['m.relates_to'];
    console.log(`Reaction ${reaction.key} added to ${reaction.event_id}`);
  }
});

Room Events

// 🏠 Room updates
client.on('Room', room => {
  console.log('Room created or updated:', room.getRoomId());
});

// πŸ‘₯ Member changes
client.on('RoomMember.membership', (event, member) => {
  const action = member.membership;
  const userId = member.userId;
  console.log(`${userId} ${action} the room`);
});

---

## ⚑ Realtime (Centrifuge) Integration

XHubChat Core provides an optional realtime layer backed by a WebSocket connection (via the [`centrifuge`](https://github.com/centrifugal/centrifugo-js) client). It emits high-level typed events for new messages, room updates, and presence signals.

### βœ… Quick Enable

You only need to supply the realtime config with a `url` and (optionally) an initial auth token:

```ts
import { XHubChatClient } from '@xhub-chat/core';

const client = new XHubChatClient({
  baseUrl: 'https://api.example.com',
  accessToken: 'rest_token',
  userId: '@alice:example.com',
  realtime: {
    url: 'wss://realtime.example.com/connection/websocket',
    initialToken: 'jwt_initial_optional', // optional
    defaultChannels: ['user.@alice', 'presence.global'], // optional
  },
});

await client.startClient();

// Explicitly connect (unless you wire autoStart externally)
await client.getRealtime()!.connect();

client.on('client.realtime.connected', info => {
  console.log('Realtime connected', info.session);
});
client.on('client.realtime.message.new', m => {
  console.log('New realtime message', m.id, m.content);
});

πŸ§ͺ Testing / Custom Factory

For tests you can inject a fake implementation:

const fake = { connect(){}, disconnect(){}, on(){}, subscribe(){ return { unsubscribe(){} }; } };
const client = new XHubChatClient({
  baseUrl, accessToken, userId,
  realtime: { centrifugeFactory: () => fake },
});

πŸ”„ Token Refresh

Provide tokenProvider: () => Promise<string> to fetch/rotate an auth token before each connect(). Call refreshToken() manually if the server invalidates the current one.

πŸ” Reconnect Strategy

Simple linear backoff: attempt * (reconnectBaseDelayMs || 1000) up to maxReconnectAttempts (default 5). Listen for client.realtime.reconnecting to display UI hints.

πŸ”” Event Names (bridged)

Client Event Description
client.realtime.connected WebSocket session established
client.realtime.disconnected Connection closed (will attempt reconnect)
client.realtime.reconnecting Reconnect attempt number emitted
client.realtime.error Error surfaced in realtime layer
client.realtime.message.new New chat message payload
client.realtime.room.updated Room metadata / state change
client.realtime.presence Presence join/leave/update
client.realtime.channel.subscribed Channel subscription created
client.realtime.channel.unsubscribed Channel unsubscribed

🧩 Channel Classification

By naming convention:

  • user.* β†’ message events
  • room.* β†’ room update events
  • presence.* β†’ presence events

You can override the payload mapping via mapMessage, mapRoomUpdate, mapPresence in realtime options.

πŸ›‘ Graceful Shutdown

client.getRealtime()?.disconnect();
client.stopClient();

🧭 When To Use Realtime vs Polling

The core SDK still functions without realtimeβ€”fallback sync/poll logic delivers messages. Enable realtime for lower latency delivery and richer presence responsiveness.


// πŸ“› Room name changes client.on('RoomState.events', event => { if (event.getType() === 'm.room.name') { const newName = event.getContent().name; console.log('Room renamed to:', newName); } });


#### Connection Events

```typescript
// πŸ”Œ Connection status
client.on('sync', (state, prevState, data) => {
  switch (state) {
    case 'PREPARED':
      console.log('βœ… Client is ready!');
      break;
    case 'SYNCING':
      console.log('πŸ”„ Syncing...');
      break;
    case 'ERROR':
      console.error('❌ Sync error:', data.error);
      break;
    case 'STOPPED':
      console.log('πŸ›‘ Sync stopped');
      break;
  }
});

// 🌐 Network status
client.on('http-api:error', error => {
  console.error('Network error:', error);
});

πŸ” Security & Encryption

Setting Up End-to-End Encryption

import { MemoryCryptoStore } from '@xhub-chat/core';

// πŸ”’ Initialize with crypto support
const client = createClient({
  baseUrl: 'https://your-server.com',
  accessToken: 'your_token',
  userId: '@user:example.com',
  cryptoStore: new MemoryCryptoStore(),
  cryptoCallbacks: {
    // πŸ”‘ Device verification callback
    verifyDevice: async (userId, deviceId, deviceInfo) => {
      // Implement your device verification UI
      const isVerified = await showDeviceVerificationDialog(deviceInfo);
      return isVerified;
    },

    // πŸ” Key backup callback
    getSecretStorageKey: async keyInfo => {
      // Implement your key recovery UI
      return await promptForRecoveryKey();
    },
  },
});

// πŸš€ Start crypto after client initialization
await client.initCrypto();

Managing Device Trust

// πŸ“± Get user devices
const devices = await client.getStoredDevicesForUser('@user:example.com');

// βœ… Verify a device
await client.setDeviceVerified('@user:example.com', 'DEVICE_ID', true);

// πŸ” Check if room is encrypted
const room = client.getRoom(roomId);
const isEncrypted = client.isRoomEncrypted(roomId);

// πŸ”’ Enable encryption for a room
await client.sendStateEvent(roomId, 'm.room.encryption', {
  algorithm: 'm.megolm.v1.aes-sha2',
});

πŸ‘₯ User Management

User Profiles

// πŸ‘€ Get user profile
const profile = await client.getProfileInfo('@user:example.com');
console.log('Display name:', profile.displayname);
console.log('Avatar URL:', profile.avatar_url);

// ✏️ Update your profile
await client.setDisplayName('Alice Johnson');
await client.setAvatarUrl('mxc://server.com/avatar_id');

// πŸ” Search users
const results = await client.searchUserDirectory({
  search_term: 'alice',
  limit: 10,
});

Presence & Status

// 🟒 Set your presence
await client.setPresence({
  presence: 'online',
  status_msg: 'Working on the new feature',
});

// πŸ‘€ Get user presence
const presence = await client.getPresence('@user:example.com');
console.log(`${user} is ${presence.presence}: ${presence.status_msg}`);

// πŸ”” Typing indicators
await client.sendTyping(roomId, true, 5000); // Typing for 5 seconds
await client.sendTyping(roomId, false); // Stop typing

🎯 Examples

πŸ“± Building a Simple Chat Interface

import { createClient } from '@xhub-chat/core';

class SimpleChatApp {
  private client: XHubChatClient;
  private messageContainer: HTMLElement;
  private inputField: HTMLInputElement;

  constructor() {
    this.messageContainer = document.getElementById('messages')!;
    this.inputField = document.getElementById('messageInput') as HTMLInputElement;
    this.setupClient();
    this.setupEventHandlers();
  }

  private setupClient() {
    this.client = createClient({
      baseUrl: 'https://your-server.com',
      accessToken: localStorage.getItem('accessToken')!,
      userId: localStorage.getItem('userId')!,
    });

    // πŸ“¨ Listen for new messages
    this.client.on('Room.timeline', (event, room, toStartOfTimeline) => {
      if (event.getType() === 'm.room.message' && !toStartOfTimeline) {
        this.displayMessage(event);
      }
    });

    // πŸ”„ Handle sync states
    this.client.on('sync', state => {
      this.updateConnectionStatus(state);
    });
  }

  private setupEventHandlers() {
    this.inputField.addEventListener('keypress', e => {
      if (e.key === 'Enter' && this.inputField.value.trim()) {
        this.sendMessage();
      }
    });
  }

  private displayMessage(event: XHubChatEvent) {
    const messageDiv = document.createElement('div');
    messageDiv.className = 'message';

    const sender = event.getSender();
    const content = event.getContent();
    const timestamp = new Date(event.getTs()).toLocaleTimeString();

    messageDiv.innerHTML = `
      <div class="message-header">
        <span class="sender">${sender}</span>
        <span class="timestamp">${timestamp}</span>
      </div>
      <div class="message-content">${content.body}</div>
    `;

    this.messageContainer.appendChild(messageDiv);
    this.messageContainer.scrollTop = this.messageContainer.scrollHeight;
  }

  private async sendMessage() {
    const roomId = this.getCurrentRoomId();
    const message = this.inputField.value.trim();

    try {
      await this.client.sendTextMessage(roomId, message);
      this.inputField.value = '';
    } catch (error) {
      console.error('Failed to send message:', error);
      this.showError('Failed to send message');
    }
  }

  private updateConnectionStatus(state: SyncState) {
    const statusElement = document.getElementById('connectionStatus')!;

    switch (state) {
      case 'PREPARED':
        statusElement.textContent = 'βœ… Connected';
        statusElement.className = 'status-connected';
        break;
      case 'SYNCING':
        statusElement.textContent = 'πŸ”„ Syncing...';
        statusElement.className = 'status-syncing';
        break;
      case 'ERROR':
        statusElement.textContent = '❌ Connection Error';
        statusElement.className = 'status-error';
        break;
    }
  }

  async start() {
    try {
      await this.client.startClient({ initialSyncLimit: 50 });
    } catch (error) {
      console.error('Failed to start client:', error);
      this.showError('Failed to connect to chat server');
    }
  }

  private getCurrentRoomId(): string {
    // Implementation depends on your UI
    return document.querySelector('.room-item.active')?.dataset.roomId || '';
  }

  private showError(message: string) {
    // Implementation for error display
    console.error(message);
  }
}

// πŸš€ Initialize the app
const app = new SimpleChatApp();
app.start();

πŸ€– Creating a Chat Bot

import { createClient, XHubChatEvent } from '@xhub-chat/core';

class ChatBot {
  private client: XHubChatClient;
  private commands = new Map<string, (event: XHubChatEvent, args: string[]) => Promise<void>>();

  constructor(accessToken: string, userId: string, baseUrl: string) {
    this.client = createClient({
      baseUrl,
      accessToken,
      userId,
    });

    this.setupCommands();
    this.setupEventHandlers();
  }

  private setupCommands() {
    // πŸ‘‹ Hello command
    this.commands.set('hello', async event => {
      const roomId = event.getRoomId()!;
      const sender = event.getSender()!;
      await this.client.sendTextMessage(roomId, `Hello ${sender}! πŸ‘‹`);
    });

    // 🎲 Random number command
    this.commands.set('random', async (event, args) => {
      const roomId = event.getRoomId()!;
      const max = parseInt(args[0]) || 100;
      const randomNum = Math.floor(Math.random() * max) + 1;
      await this.client.sendTextMessage(roomId, `🎲 Random number: ${randomNum}`);
    });

    // πŸ“Š Room stats command
    this.commands.set('stats', async event => {
      const roomId = event.getRoomId()!;
      const room = this.client.getRoom(roomId)!;
      const memberCount = room.getJoinedMemberCount();
      const roomName = room.name || 'Unnamed Room';

      await this.client.sendHtmlMessage(
        roomId,
        `πŸ“Š <strong>${roomName}</strong> Statistics:<br/>` +
          `πŸ‘₯ Members: ${memberCount}<br/>` +
          `πŸ†” Room ID: <code>${roomId}</code>`,
        `πŸ“Š ${roomName} Statistics:\nπŸ‘₯ Members: ${memberCount}\nπŸ†” Room ID: ${roomId}`
      );
    });

    // ❓ Help command
    this.commands.set('help', async event => {
      const roomId = event.getRoomId()!;
      const helpText = `πŸ€– **Available Commands:**
      
β€’ \`!hello\` - Say hello
β€’ \`!random [max]\` - Generate random number
β€’ \`!stats\` - Show room statistics
β€’ \`!help\` - Show this help message`;

      await this.client.sendHtmlMessage(roomId, helpText.replace(/\n/g, '<br/>'), helpText);
    });
  }

  private setupEventHandlers() {
    this.client.on('Room.timeline', (event, room, toStartOfTimeline) => {
      if (this.shouldProcessEvent(event, toStartOfTimeline)) {
        this.processMessage(event);
      }
    });

    this.client.on('sync', state => {
      console.log(`Bot sync state: ${state}`);
    });

    // πŸŽ‰ Welcome new members
    this.client.on('RoomMember.membership', (event, member) => {
      if (member.membership === 'join' && member.previousMembership !== 'join') {
        const roomId = member.roomId;
        const userId = member.userId;

        // Don't welcome ourselves
        if (userId !== this.client.getUserId()) {
          this.client.sendTextMessage(roomId, `Welcome to the room, ${userId}! πŸŽ‰`);
        }
      }
    });
  }

  private shouldProcessEvent(event: XHubChatEvent, toStartOfTimeline: boolean): boolean {
    return (
      event.getType() === 'm.room.message' &&
      !toStartOfTimeline &&
      event.getSender() !== this.client.getUserId() &&
      !event.isRedacted()
    );
  }

  private async processMessage(event: XHubChatEvent) {
    const content = event.getContent();

    if (content.msgtype !== 'm.text') return;

    const message = content.body.trim();

    // Check for commands (starting with !)
    if (message.startsWith('!')) {
      const parts = message.slice(1).split(' ');
      const command = parts[0].toLowerCase();
      const args = parts.slice(1);

      const handler = this.commands.get(command);
      if (handler) {
        try {
          await handler(event, args);
        } catch (error) {
          console.error(`Error executing command ${command}:`, error);
          const roomId = event.getRoomId()!;
          await this.client.sendTextMessage(roomId, `❌ Error executing command: ${error.message}`);
        }
      }
    }

    // Auto-responses for certain keywords
    if (message.toLowerCase().includes('good morning')) {
      const roomId = event.getRoomId()!;
      await this.client.sendTextMessage(roomId, 'πŸŒ… Good morning! Have a great day!');
    }
  }

  async start() {
    console.log('πŸ€– Starting chat bot...');
    await this.client.startClient();
    console.log('βœ… Chat bot is online!');
  }

  async stop() {
    console.log('πŸ›‘ Stopping chat bot...');
    this.client.stopClient();
    console.log('βœ… Chat bot stopped');
  }
}

// πŸš€ Usage
const bot = new ChatBot('your_bot_access_token', '@bot:example.com', 'https://your-server.com');

bot.start().catch(console.error);

// Graceful shutdown
process.on('SIGTERM', () => bot.stop());
process.on('SIGINT', () => bot.stop());

πŸ“ File Upload & Media Handling

class MediaHandler {
  constructor(private client: XHubChatClient) {}

  // πŸ“ Upload and send any file
  async sendFile(roomId: string, file: File, progressCallback?: (progress: number) => void) {
    try {
      // πŸ“€ Upload file to server
      const upload = await this.client.uploadContent(file, {
        name: file.name,
        progressHandler: progressCallback,
      });

      // πŸ“‹ Create message content based on file type
      const content = this.createFileContent(file, upload.content_uri);

      // πŸ“¨ Send the message
      await this.client.sendMessage(roomId, content);
    } catch (error) {
      console.error('File upload failed:', error);
      throw new Error(`Failed to upload ${file.name}: ${error.message}`);
    }
  }

  private createFileContent(file: File, contentUri: string) {
    const baseContent = {
      body: file.name,
      filename: file.name,
      info: {
        size: file.size,
        mimetype: file.type,
      },
      url: contentUri,
    };

    // πŸ–ΌοΈ Image content
    if (file.type.startsWith('image/')) {
      return {
        msgtype: 'm.image',
        ...baseContent,
        info: {
          ...baseContent.info,
          w: 0, // Will be set by server or client
          h: 0, // Will be set by server or client
        },
      };
    }

    // 🎡 Audio content
    if (file.type.startsWith('audio/')) {
      return {
        msgtype: 'm.audio',
        ...baseContent,
      };
    }

    // 🎬 Video content
    if (file.type.startsWith('video/')) {
      return {
        msgtype: 'm.video',
        ...baseContent,
        info: {
          ...baseContent.info,
          w: 0,
          h: 0,
          duration: 0,
        },
      };
    }

    // πŸ“„ Generic file
    return {
      msgtype: 'm.file',
      ...baseContent,
    };
  }

  // πŸ–ΌοΈ Send image with thumbnail
  async sendImageWithThumbnail(roomId: string, imageFile: File) {
    // Create thumbnail
    const thumbnailBlob = await this.createThumbnail(imageFile, 200, 200);

    // Upload both image and thumbnail
    const [imageUpload, thumbUpload] = await Promise.all([
      this.client.uploadContent(imageFile),
      this.client.uploadContent(thumbnailBlob, { name: 'thumbnail.jpg' }),
    ]);

    // Get image dimensions
    const dimensions = await this.getImageDimensions(imageFile);

    const content = {
      msgtype: 'm.image',
      body: imageFile.name,
      info: {
        size: imageFile.size,
        mimetype: imageFile.type,
        w: dimensions.width,
        h: dimensions.height,
        thumbnail_url: thumbUpload.content_uri,
        thumbnail_info: {
          size: thumbnailBlob.size,
          mimetype: 'image/jpeg',
          w: 200,
          h: 200,
        },
      },
      url: imageUpload.content_uri,
    };

    await this.client.sendMessage(roomId, content);
  }

  private async createThumbnail(file: File, maxWidth: number, maxHeight: number): Promise<Blob> {
    return new Promise((resolve, reject) => {
      const img = new Image();
      const canvas = document.createElement('canvas');
      const ctx = canvas.getContext('2d')!;

      img.onload = () => {
        // Calculate thumbnail dimensions
        const { width, height } = this.calculateThumbnailSize(img.width, img.height, maxWidth, maxHeight);

        canvas.width = width;
        canvas.height = height;

        // Draw and convert to blob
        ctx.drawImage(img, 0, 0, width, height);
        canvas.toBlob(resolve, 'image/jpeg', 0.8);
      };

      img.onerror = reject;
      img.src = URL.createObjectURL(file);
    });
  }

  private calculateThumbnailSize(width: number, height: number, maxWidth: number, maxHeight: number) {
    const ratio = Math.min(maxWidth / width, maxHeight / height);
    return {
      width: Math.round(width * ratio),
      height: Math.round(height * ratio),
    };
  }

  private async getImageDimensions(file: File): Promise<{ width: number; height: number }> {
    return new Promise((resolve, reject) => {
      const img = new Image();
      img.onload = () => resolve({ width: img.width, height: img.height });
      img.onerror = reject;
      img.src = URL.createObjectURL(file);
    });
  }
}

// 🎯 Usage example
const mediaHandler = new MediaHandler(client);

// Handle file input
document.getElementById('fileInput')?.addEventListener('change', async e => {
  const files = (e.target as HTMLInputElement).files;
  if (!files || files.length === 0) return;

  const roomId = getCurrentRoomId();

  for (const file of files) {
    try {
      await mediaHandler.sendFile(roomId, file, progress => {
        console.log(`Upload progress: ${Math.round(progress * 100)}%`);
      });
    } catch (error) {
      console.error('Failed to send file:', error);
    }
  }
});

πŸš€ Advanced Usage

πŸ”§ Custom Storage Implementations

For production applications, you'll want persistent storage:

IndexedDB Storage

import { IndexedDBStore, IndexedDBCryptoStore } from '@xhub-chat/core';

// πŸ—„οΈ Persistent client storage
const client = createClient({
  baseUrl: 'https://your-server.com',
  accessToken: 'your_token',
  userId: '@user:example.com',
  store: new IndexedDBStore({
    indexedDB: window.indexedDB,
    dbName: 'xhubchat-store',
    workerScript: '/store-worker.js', // Optional web worker
  }),
  cryptoStore: new IndexedDBCryptoStore(window.indexedDB, 'xhubchat-crypto'),
});

Custom Storage Implementation

import { IStore, ISavedSync } from '@xhub-chat/core';

class CustomStore implements IStore {
  private rooms = new Map();
  private users = new Map();

  // πŸ’Ύ Store sync data
  async setSyncData(syncData: ISavedSync): Promise<void> {
    await this.saveToDatabase('sync', syncData);
  }

  // πŸ“– Retrieve sync data
  async getSyncData(): Promise<ISavedSync | null> {
    return await this.loadFromDatabase('sync');
  }

  // 🏠 Store room data
  async storeRoom(room: Room): Promise<void> {
    await this.saveToDatabase(`room:${room.getRoomId()}`, room.toJSON());
  }

  // ... implement other required methods

  private async saveToDatabase(key: string, data: any): Promise<void> {
    // Your custom storage implementation
  }

  private async loadFromDatabase(key: string): Promise<any> {
    // Your custom storage implementation
  }
}

πŸ”„ Handling Offline/Online Scenarios

class OfflineHandler {
  private client: XHubChatClient;
  private isOnline = navigator.onLine;
  private messageQueue: Array<{ roomId: string; content: any }> = [];

  constructor(client: XHubChatClient) {
    this.client = client;
    this.setupOfflineHandling();
  }

  private setupOfflineHandling() {
    // πŸ“‘ Listen for network changes
    window.addEventListener('online', () => {
      console.log('🟒 Back online - processing queued messages');
      this.isOnline = true;
      this.processMessageQueue();
    });

    window.addEventListener('offline', () => {
      console.log('πŸ”΄ Gone offline - queueing messages');
      this.isOnline = false;
    });

    // πŸ”„ Handle sync errors
    this.client.on('sync', (state, prevState, data) => {
      if (state === 'ERROR' && data.error.name === 'ConnectionError') {
        console.log('πŸ“‘ Connection lost - entering offline mode');
        this.isOnline = false;
      }
    });
  }

  // πŸ“€ Send message with offline queueing
  async sendMessage(roomId: string, content: any): Promise<void> {
    if (this.isOnline) {
      try {
        await this.client.sendMessage(roomId, content);
      } catch (error) {
        if (this.isNetworkError(error)) {
          console.log('πŸ“₯ Network error - queueing message');
          this.messageQueue.push({ roomId, content });
        } else {
          throw error;
        }
      }
    } else {
      console.log('πŸ“₯ Offline - queueing message');
      this.messageQueue.push({ roomId, content });
    }
  }

  private async processMessageQueue(): Promise<void> {
    const queue = [...this.messageQueue];
    this.messageQueue = [];

    for (const { roomId, content } of queue) {
      try {
        await this.client.sendMessage(roomId, content);
        console.log('βœ… Queued message sent successfully');
      } catch (error) {
        console.error('❌ Failed to send queued message:', error);
        // Re-queue if it was a network error
        if (this.isNetworkError(error)) {
          this.messageQueue.push({ roomId, content });
        }
      }
    }
  }

  private isNetworkError(error: any): boolean {
    return error.name === 'ConnectionError' || error.code === 'NETWORK_ERROR' || !navigator.onLine;
  }
}

// Usage
const offlineHandler = new OfflineHandler(client);
await offlineHandler.sendMessage(roomId, { msgtype: 'm.text', body: 'Hello!' });

🧡 Thread Support

// 🧡 Send a threaded message
await client.sendMessage(roomId, {
  msgtype: 'm.text',
  body: 'This is a reply in the thread',
  'm.relates_to': {
    rel_type: 'm.thread',
    event_id: '$root_message_id', // The message that started the thread
    is_falling_back: true,
    'm.in_reply_to': {
      event_id: '$previous_message_in_thread_id',
    },
  },
});

// πŸ“– Get thread messages
const thread = room.findThreadForEvent(rootEvent);
if (thread) {
  const threadEvents = thread.events;
  console.log(`Thread has ${threadEvents.length} messages`);

  // πŸ“₯ Load more thread history
  await thread.fetchEvents({ limit: 50, direction: Direction.Backward });
}

// πŸ‘‚ Listen for thread updates
client.on('Thread.update', thread => {
  console.log(`Thread updated: ${thread.id}`);
  const latestEvent = thread.events[thread.events.length - 1];
  console.log('Latest message:', latestEvent.getContent().body);
});

🏒 Space Management

// πŸ—οΈ Create a space
const spaceId = await client.createRoom({
  name: 'Engineering Team',
  topic: 'Space for all engineering discussions',
  preset: 'private_chat',
  creation_content: {
    type: 'm.space',
  },
  power_level_content_override: {
    events: {
      'm.space.child': 100, // Only admins can add/remove rooms
      'm.room.name': 100,
      'm.room.avatar': 100,
    },
  },
});

// πŸ”— Add rooms to the space
await client.sendStateEvent(
  spaceId,
  'm.space.child',
  {
    via: ['example.com'],
    order: '01', // Display order
  },
  '!general:example.com'
);

await client.sendStateEvent(
  spaceId,
  'm.space.child',
  {
    via: ['example.com'],
    order: '02',
  },
  '!random:example.com'
);

// πŸ“‹ Get space hierarchy
const hierarchy = await client.getSpaceHierarchy(spaceId);
console.log('Space rooms:', hierarchy.rooms);

// 🎯 Filter rooms by space
const spaceRooms = client.getVisibleRooms().filter(room => {
  return client.getStateEvents(spaceId, 'm.space.child', room.getRoomId());
});

🎯 Best Practices

πŸš€ Performance Optimization

Lazy Loading

// πŸ‘₯ Enable lazy loading for large rooms
const client = createClient({
  // ... other options
  lazyLoadMembers: true, // Don't load all members immediately
});

await client.startClient({
  initialSyncLimit: 20, // Sync fewer recent messages initially
  includeArchivedRooms: false, // Skip archived rooms
  lazyLoadMembers: true, // Load members on demand
});

// πŸ“₯ Load members when needed
const room = client.getRoom(roomId);
if (room && room.currentState.isLazyLoadingEnabled()) {
  await room.loadMembersIfNeeded();
}

Efficient Event Handling

// βœ… Good: Use specific event filters
client.on('Room.timeline', (event, room, toStartOfTimeline) => {
  // Only process new messages, not historical ones
  if (toStartOfTimeline) return;

  // Only process specific message types
  if (event.getType() !== 'm.room.message') return;

  this.processMessage(event, room);
});

// ❌ Avoid: Processing all events
client.on('event', event => {
  // This gets called for EVERY event - very inefficient
  this.processAnyEvent(event);
});

Memory Management

class EfficientChatApp {
  private eventCache = new Map<string, XHubChatEvent>();
  private readonly MAX_CACHED_EVENTS = 1000;

  private cacheEvent(event: XHubChatEvent) {
    // 🧹 Clean old events from cache
    if (this.eventCache.size >= this.MAX_CACHED_EVENTS) {
      const oldestKey = this.eventCache.keys().next().value;
      this.eventCache.delete(oldestKey);
    }

    this.eventCache.set(event.getId()!, event);
  }

  private cleanup() {
    // πŸ—‘οΈ Clear caches periodically
    this.eventCache.clear();

    // πŸ”„ Force garbage collection (if available)
    if (window.gc) {
      window.gc();
    }
  }
}

πŸ”’ Security Best Practices

Token Management

class SecureTokenManager {
  private accessToken: string | null = null;
  private refreshToken: string | null = null;

  // πŸ” Secure token storage
  setTokens(access: string, refresh: string) {
    // Don't store in localStorage - use secure storage
    this.accessToken = access;
    this.refreshToken = refresh;

    // For web apps, consider using IndexedDB with encryption
    this.storeSecurely('access_token', access);
    this.storeSecurely('refresh_token', refresh);
  }

  // πŸ”„ Automatic token refresh
  async refreshAccessToken(): Promise<string> {
    if (!this.refreshToken) {
      throw new Error('No refresh token available');
    }

    const response = await fetch('/auth/refresh', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ refresh_token: this.refreshToken }),
    });

    if (!response.ok) {
      throw new Error('Token refresh failed');
    }

    const { access_token, refresh_token } = await response.json();
    this.setTokens(access_token, refresh_token);

    return access_token;
  }

  private storeSecurely(key: string, value: string) {
    // Implement secure storage (encrypted IndexedDB, etc.)
    // Never use localStorage for sensitive data in production
  }
}

// πŸ”Œ Use with client
const tokenManager = new SecureTokenManager();

const client = createClient({
  baseUrl: 'https://your-server.com',
  accessToken: tokenManager.getAccessToken(),
  // πŸ”„ Automatic token refresh
  tokenRefreshFunction: () => tokenManager.refreshAccessToken(),
});

Input Validation & Sanitization

class SecureMessageHandler {
  // πŸ›‘οΈ Sanitize user input
  private sanitizeMessage(message: string): string {
    // Remove potentially dangerous content
    return message
      .replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
      .replace(/javascript:/gi, '')
      .replace(/on\w+\s*=/gi, '')
      .trim()
      .slice(0, 4000); // Limit length
  }

  // βœ… Validate room permissions
  private async canSendToRoom(roomId: string): Promise<boolean> {
    const room = this.client.getRoom(roomId);
    if (!room) return false;

    const member = room.getMember(this.client.getUserId()!);
    if (!member || member.membership !== 'join') return false;

    // Check power level
    const powerLevel = room.getMembersPowerLevel(this.client.getUserId()!);
    const requiredLevel =
      room.currentState.getStateEvents('m.room.power_levels', '')?.getContent()?.events?.['m.room.message'] ?? 0;

    return powerLevel >= requiredLevel;
  }

  async sendSecureMessage(roomId: string, message: string) {
    // πŸ›‘οΈ Validate and sanitize
    if (!(await this.canSendToRoom(roomId))) {
      throw new Error('Insufficient permissions to send message');
    }

    const sanitizedMessage = this.sanitizeMessage(message);
    if (!sanitizedMessage) {
      throw new Error('Message is empty after sanitization');
    }

    // πŸ“€ Send the clean message
    await this.client.sendTextMessage(roomId, sanitizedMessage);
  }
}

🎯 Error Handling Patterns

Retry Logic

class RobustMessageSender {
  private readonly MAX_RETRIES = 3;
  private readonly RETRY_DELAYS = [1000, 2000, 4000]; // Exponential backoff

  async sendMessageWithRetry(roomId: string, content: any): Promise<void> {
    let lastError: Error;

    for (let attempt = 0; attempt <= this.MAX_RETRIES; attempt++) {
      try {
        await this.client.sendMessage(roomId, content);
        return; // Success!
      } catch (error) {
        lastError = error as Error;

        // Don't retry certain errors
        if (this.isNonRetryableError(error)) {
          throw error;
        }

        // Don't retry on final attempt
        if (attempt === this.MAX_RETRIES) {
          break;
        }

        // Wait before retrying
        await this.delay(this.RETRY_DELAYS[attempt]);
      }
    }

    throw new Error(`Failed to send message after ${this.MAX_RETRIES + 1} attempts: ${lastError.message}`);
  }

  private isNonRetryableError(error: any): boolean {
    // Don't retry client errors (4xx status codes)
    if (error.httpStatus >= 400 && error.httpStatus < 500) {
      return true;
    }

    // Don't retry if user lacks permissions
    if (error.httpStatus === 403) {
      return true;
    }

    return false;
  }

  private delay(ms: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}

Graceful Degradation

class ResilientChatClient {
  private client: XHubChatClient;
  private fallbackMode = false;

  constructor(options: ICreateClientOpts) {
    this.client = createClient(options);
    this.setupErrorHandling();
  }

  private setupErrorHandling() {
    // 🚨 Handle sync errors
    this.client.on('sync', (state, prevState, data) => {
      if (state === 'ERROR') {
        console.error('Sync error:', data.error);

        if (this.isCriticalError(data.error)) {
          this.enterFallbackMode();
        }
      }
    });

    // 🌐 Handle network errors
    this.client.on('http-api:error', error => {
      console.error('HTTP API error:', error);

      if (error.httpStatus >= 500) {
        this.handleServerError();
      }
    });
  }

  private isCriticalError(error: any): boolean {
    return error.name === 'ConnectionError' || error.httpStatus >= 500 || !navigator.onLine;
  }

  private enterFallbackMode() {
    this.fallbackMode = true;
    console.log('πŸ”„ Entering fallback mode - limited functionality');

    // Show user notification
    this.showNotification('Limited connectivity - some features may be unavailable', 'warning');

    // Implement fallback behavior
    this.enableOfflineMode();
  }

  private exitFallbackMode() {
    this.fallbackMode = false;
    console.log('βœ… Exiting fallback mode - full functionality restored');

    this.showNotification('Connection restored!', 'success');
    this.disableOfflineMode();
  }

  private showNotification(message: string, type: 'error' | 'warning' | 'success') {
    // Implement your notification system
    console.log(`${type.toUpperCase()}: ${message}`);
  }

  private enableOfflineMode() {
    // Implement offline-first behavior
    // - Queue messages
    // - Show cached data only
    // - Disable real-time features
  }

  private disableOfflineMode() {
    // Restore online behavior
    // - Send queued messages
    // - Re-enable real-time features
    // - Refresh data
  }
}

πŸ”§ Troubleshooting

🚨 Common Issues & Solutions

"Multiple xhubchat-sdk entrypoints detected!"

Problem: This error occurs when multiple instances of the SDK are loaded.

// ❌ Wrong - multiple imports
import xhubchat from '@xhub-chat/core';
import { createClient } from '@xhub-chat/core';
import * as XHubChat from '@xhub-chat/core';

// βœ… Correct - single import
import { createClient } from '@xhub-chat/core';

Sync Not Starting

Problem: Client appears to connect but sync never progresses.

// πŸ” Debug sync issues
client.on('sync', (state, prevState, data) => {
  console.log('Sync state:', { state, prevState, data });

  if (state === 'ERROR') {
    console.error('Sync error details:', {
      error: data.error,
      nextSyncToken: data.nextSyncToken,
      catchingUp: data.catchingUp,
    });
  }
});

// πŸ› οΈ Common fixes:
// 1. Check network connectivity
// 2. Verify access token is valid
// 3. Ensure server URL is correct
// 4. Check for CORS issues

Memory Leaks

Problem: Application memory usage grows over time.

// 🧹 Proper cleanup
class ChatApplication {
  private client: XHubChatClient;
  private eventListeners: Array<() => void> = [];

  constructor() {
    this.client = createClient({
      /* options */
    });
    this.setupEventListeners();
  }

  private setupEventListeners() {
    // πŸ‘‚ Store listeners for cleanup
    const timelineListener = (event: XHubChatEvent) => {
      /* handler */
    };
    this.client.on('Room.timeline', timelineListener);

    this.eventListeners.push(() => {
      this.client.off('Room.timeline', timelineListener);
    });
  }

  // 🧹 Clean up when done
  destroy() {
    // Remove all event listeners
    this.eventListeners.forEach(cleanup => cleanup());
    this.eventListeners = [];

    // Stop the client
    this.client.stopClient();

    // Clear any caches
    this.clearCaches();
  }
}

Encryption Issues

Problem: Messages appear encrypted in UI or fail to decrypt.

// πŸ” Debug crypto issues
client.on('crypto:error', error => {
  console.error('Crypto error:', error);
});

// πŸ› οΈ Common fixes:
await client.initCrypto(); // Ensure crypto is initialized

// Check device verification
const devices = await client.getStoredDevicesForUser('@user:example.com');
console.log('User devices:', devices);

// Force key sharing for debugging
await client.sendSharedHistoryKeys(roomId);

πŸ“Š Performance Debugging

Monitor Sync Performance

class SyncMonitor {
  private syncStartTime: number = 0;
  private syncMetrics: Array<{ duration: number; events: number; rooms: number }> = [];

  constructor(client: XHubChatClient) {
    this.setupMonitoring(client);
  }

  private setupMonitoring(client: XHubChatClient) {
    client.on('sync', (state, prevState, data) => {
      switch (state) {
        case 'SYNCING':
          this.syncStartTime = Date.now();
          break;

        case 'PREPARED':
          const duration = Date.now() - this.syncStartTime;
          const rooms = client.getRooms().length;

          this.syncMetrics.push({
            duration,
            events: data?.events?.length || 0,
            rooms,
          });

          console.log(`πŸ“Š Sync completed in ${duration}ms - ${rooms} rooms`);
          break;
      }
    });
  }

  getAverageSyncTime(): number {
    if (this.syncMetrics.length === 0) return 0;

    const totalTime = this.syncMetrics.reduce((sum, metric) => sum + metric.duration, 0);
    return totalTime / this.syncMetrics.length;
  }
}

Memory Usage Tracking

class MemoryMonitor {
  private intervalId: NodeJS.Timeout | null = null;

  startMonitoring(intervalMs = 30000) {
    this.intervalId = setInterval(() => {
      if ('memory' in performance) {
        const memory = (performance as any).memory;
        console.log('πŸ“Š Memory usage:', {
          used: `${(memory.usedJSHeapSize / 1024 / 1024).toFixed(2)}MB`,
          total: `${(memory.totalJSHeapSize / 1024 / 1024).toFixed(2)}MB`,
          limit: `${(memory.jsHeapSizeLimit / 1024 / 1024).toFixed(2)}MB`,
        });
      }
    }, intervalMs);
  }

  stopMonitoring() {
    if (this.intervalId) {
      clearInterval(this.intervalId);
      this.intervalId = null;
    }
  }
}

// Usage
const memoryMonitor = new MemoryMonitor();
memoryMonitor.startMonitoring(60000); // Check every minute

🌐 Network Debugging

// πŸ“‘ Monitor network requests
client.getHttpApi().on('request', requestData => {
  console.log('🌐 HTTP Request:', {
    method: requestData.method,
    url: requestData.url,
    timestamp: new Date().toISOString(),
  });
});

client.getHttpApi().on('response', responseData => {
  console.log('πŸ“₯ HTTP Response:', {
    status: responseData.status,
    duration: `${responseData.duration}ms`,
    url: responseData.url,
  });
});

// 🚨 Track failed requests
client.getHttpApi().on('error', error => {
  console.error('❌ HTTP Error:', {
    url: error.url,
    status: error.status,
    message: error.message,
    timestamp: new Date().toISOString(),
  });
});

πŸŽ“ Migration & Upgrade Guide

πŸ“ˆ Upgrading from Previous Versions

If you're upgrading from an older version of XHubChat SDK, here are the key changes to be aware of:

Version 1.x Migration

// ❌ Old way (pre-1.0)
import XHubChat from 'xhub-chat-old';
const client = XHubChat.createClient({
  // old configuration
});

// βœ… New way (1.0+)
import { createClient } from '@xhub-chat/core';
const client = createClient({
  // new configuration
});

Breaking Changes

  1. Import paths changed - Use @xhub-chat/core instead of old package names
  2. Configuration options - Some options were renamed or moved
  3. Event names - Some events were standardized (check the API reference)
  4. Crypto initialization - Now requires explicit initCrypto() call

🀝 Contributing & Support

πŸ“ž Getting Help

  • Documentation Issues: Check this guide first
  • Bug Reports: Contact TekNix support team
  • Feature Requests: Submit through official channels
  • Security Issues: Report privately to TekNix security team

This SDK is proprietary software owned by TekNix Corporation. All rights reserved. Usage is subject to license terms and conditions.


πŸ“‹ Appendix

πŸ“ Glossary

  • Room: A conversation space where users can send messages
  • Event: Any action or data update in the chat system
  • Sync: Process of synchronizing local state with server
  • Timeline: Chronological sequence of events in a room
  • State Event: Events that represent the current state of a room
  • Crypto Store: Storage system for encryption keys and crypto data

πŸš€ Ready to build amazing chat experiences with XHubChat SDK!

This documentation covers the essentials to get you started. For advanced use cases and detailed API references, explore the TypeScript definitions and example applications.