nativescript-matrix-sdk
Version:
Native Matrix SDK integration for NativeScript
279 lines (235 loc) • 8.91 kB
text/typescript
import { MatrixClient, MatrixChat, MatrixMessage, MessageOptions, MessageResult, PaginationToken, SendMessageOptions } from '../index.d';
import { optimizers } from './optimizations-registry';
import { Logger } from './logger';
/**
* Type for a method of MatrixClient
*/
type MatrixClientMethod = (...args: unknown[]) => unknown;
/**
* OptimizedMatrixClient wraps a standard MatrixClient and adds performance optimizations
* This uses the decorator pattern to add optimizations without modifying the original implementation
*/
export class OptimizedMatrixClient {
public readonly client: MatrixClient;
private readonly roomCache = new Map<string, {
lastAccessed: number;
messages: MatrixMessage[];
nextToken?: PaginationToken;
}>();
private readonly cacheLifetime = 5 * 60 * 1000; // 5 minutes cache lifetime
// Add index signature to allow dynamic property assignment
[key: string]: unknown;
constructor(client: MatrixClient) {
this.client = client;
Logger.info('[OptimizedMatrixClient] Initialized with performance optimizations');
// Set up periodic cache cleanup
setInterval(() => this.cleanupCache(), 60 * 1000); // Check every minute
// Set up dynamic method forwarding for methods we don't explicitly override
this.setupMethodForwarding();
}
/**
* Set up dynamic method forwarding for methods we don't explicitly implement
* This ensures we implement the full MatrixClient interface
*/
private setupMethodForwarding(): void {
// Get all method names from the client object
const clientMethodNames = Object.getOwnPropertyNames(
Object.getPrototypeOf(this.client)
);
// For each method that we haven't already implemented, create a forwarding function
for (const methodName of clientMethodNames) {
// Skip constructor and properties we already have
if (methodName === 'constructor' || this[methodName] !== undefined) {
continue;
}
// Check if the property is a method
const prop = this.client[methodName as keyof MatrixClient];
if (typeof prop === 'function') {
// Create a type-safe forwarding method
this[methodName] = (...args: unknown[]): unknown => {
Logger.debug(`[OptimizedMatrixClient] Forwarding method call: ${methodName}`);
// We've confirmed prop is a function above
return (prop as MatrixClientMethod).apply(this.client, args);
};
}
}
}
/**
* Clean up expired cache entries to prevent memory bloat
*/
private cleanupCache(): void {
const now = Date.now();
let expired = 0;
for (const [roomId, cache] of this.roomCache.entries()) {
if (now - cache.lastAccessed > this.cacheLifetime) {
this.roomCache.delete(roomId);
expired++;
}
}
if (expired > 0) {
Logger.debug(`[OptimizedMatrixClient] Cleaned up ${expired} expired cache entries`);
}
}
/**
* Get messages from a chat with optimized caching and pagination
* @param chatId - ID of the chat/room
* @param options - Pagination options
* @returns Promise with messages and pagination token
*/
async getMessages(chatId: string, options?: MessageOptions): Promise<MessageResult> {
// Check if we should use optimizations
if (optimizers.memory.isMemoryLow()) {
// Skip most optimizations if memory is low
Logger.info('[OptimizedMatrixClient] Memory is low, skipping message optimizations');
return this.client.getMessages(chatId, options);
}
// Default options
const limit = options?.limit || 20;
const direction = options?.direction || 'backwards';
// Check if we have this chat in cache
if (!this.roomCache.has(chatId)) {
this.roomCache.set(chatId, {
lastAccessed: Date.now(),
messages: [],
});
}
const cacheEntry = this.roomCache.get(chatId)!;
cacheEntry.lastAccessed = Date.now();
// If using a from token, it's a pagination request
if (options?.fromToken) {
// Check if we have preloaded messages
const preloaded = optimizers.pagination.getPreloadedMessages(chatId);
if (preloaded && preloaded.length > 0) {
Logger.debug(`[OptimizedMatrixClient] Using ${preloaded.length} preloaded messages for ${chatId}`);
// Create next token
const fromTokenStr = typeof options.fromToken === 'string'
? options.fromToken
: (options.fromToken as PaginationToken).token || '';
const nextToken: PaginationToken = {
token: fromTokenStr,
hasMore: preloaded.length >= limit
};
return {
messages: preloaded,
nextToken
};
}
// No preloaded messages, fetch from actual client
const result = await this.client.getMessages(chatId, options);
// Update cache
if (direction === 'backwards') {
// Add older messages to the end
cacheEntry.messages = [...cacheEntry.messages, ...result.messages];
} else {
// Add newer messages to the beginning
cacheEntry.messages = [...result.messages, ...cacheEntry.messages];
}
// Store the next token
cacheEntry.nextToken = result.nextToken;
// Preload next page if there are more
if (result.nextToken?.hasMore) {
optimizers.pagination.preloadNextPage(
chatId,
(token: string) => this.client.getMessages(chatId, {
...options,
fromToken: token
}),
result.nextToken.token
);
}
return result;
}
// This is an initial load
// Clear any existing messages in cache for this room
cacheEntry.messages = [];
// Fetch from actual client
const result = await this.client.getMessages(chatId, options);
// Update cache
cacheEntry.messages = result.messages;
cacheEntry.nextToken = result.nextToken;
// Cache the result for pagination optimizer
optimizers.pagination.cacheResults(chatId, result);
// Preload next page if there are more
if (result.nextToken?.hasMore) {
optimizers.pagination.preloadNextPage(
chatId,
(token: string) => this.client.getMessages(chatId, {
...options,
fromToken: token
}),
result.nextToken.token
);
}
return result;
}
/**
* Send a message with optimized handling
* @param chatId - ID of the chat/room
* @param content - Message content
* @param options - Message options
* @returns Promise with the sent message
*/
async sendMessage(
chatId: string,
content: string,
options?: SendMessageOptions
): Promise<MatrixMessage> {
// Check if we have local messages for this room
const cacheEntry = this.roomCache.get(chatId);
// Send through the client
const message = await this.client.sendMessage(chatId, content, options);
// Update local cache immediately for better UX
if (cacheEntry) {
// Add to the beginning (newest messages are first in cache)
cacheEntry.messages.unshift(message);
cacheEntry.lastAccessed = Date.now();
}
return message;
}
/**
* Get chats implementation that's optimized
* @returns Promise with list of chats
*/
async getChats(serverId?: string): Promise<MatrixChat[]> {
try {
const cacheKey = serverId ? `chats-${serverId}` : 'all-chats';
// Try to get from cache
const cachedChats = optimizers.cache.get(cacheKey);
if (cachedChats && Array.isArray(cachedChats)) {
return cachedChats as MatrixChat[];
}
// Get from client
const chats = await this.client.getChats(serverId || '');
// Store in cache
optimizers.cache.set(cacheKey, chats);
return chats;
} catch (error) {
Logger.error('[OptimizedMatrixClient] Error getting chats:', error);
throw error;
}
}
/**
* Leave a chat with optimized cleanup
* @param chatId - ID of the chat to leave
*/
async leaveChat(chatId: string): Promise<void> {
// Clear cache for this room when leaving
this.roomCache.delete(chatId);
optimizers.pagination.clearRoomCache(chatId);
return this.client.leaveChat(chatId);
}
/**
* Logout with proper cleanup
*/
async logout(): Promise<void> {
// Clear all caches when logging out
this.roomCache.clear();
optimizers.pagination.clearAll();
optimizers.cache.clear();
// Use a safer type assertion and check for the logout method
const client = this.client as unknown as { logout?: () => Promise<void> };
if (typeof client.logout === 'function') {
return client.logout();
}
}
}