UNPKG

nativescript-matrix-sdk

Version:

Native Matrix SDK integration for NativeScript

279 lines (235 loc) 8.91 kB
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(); } } }