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.
π 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
- Quick Start
- Installation
- Core Concepts
- API Reference
- Examples
- Advanced Usage
- Best Practices
- 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 eventsroom.*β room update eventspresence.*β 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 issuesMemory 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
- Import paths changed - Use
@xhub-chat/coreinstead of old package names - Configuration options - Some options were renamed or moved
- Event names - Some events were standardized (check the API reference)
- 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
π License & Legal
This SDK is proprietary software owned by TekNix Corporation. All rights reserved. Usage is subject to license terms and conditions.
π Appendix
π Useful Links
π 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.