UNPKG

nativescript-matrix-sdk

Version:

Native Matrix SDK integration for NativeScript

468 lines (412 loc) 14.5 kB
/** * Performance Optimizations for Matrix SDK * * This file contains utility functions and classes for optimizing the Matrix SDK * performance across both iOS and Android platforms. */ import { Logger } from './logger'; import { MatrixMessage, MessageResult } from '../index.d'; import { MatrixError, MatrixErrorType } from './errors'; /** * Generic operation result type */ type OperationResult = unknown; /** * Timer handle type */ type TimerHandle = ReturnType<typeof setTimeout>; /** * Enhanced operation with resolve/reject handlers */ interface EnhancedOperation<T> { data: T; resolve: (value: OperationResult) => void; reject: (reason?: unknown) => void; } /** * Batch queue entry containing operations and timer */ interface BatchQueueEntry<T> { operations: EnhancedOperation<T>[]; timer: TimerHandle; } /** * LRU (Least Recently Used) Cache implementation for Matrix SDK * Used to cache expensive operations or frequently accessed data */ export class MatrixLRUCache<V> { private cache = new Map<string, V>(); private readonly maxSize: number; constructor(maxSize = 100) { this.maxSize = maxSize; Logger.info(`[MatrixLRUCache] Initialized with max size: ${maxSize}`); } /** * Get a value from the cache * @param key - The cache key * @returns The cached value or undefined if not found */ get(key: string): V | undefined { if (!this.cache.has(key)) return undefined; // Move the item to the end of the map to mark as recently used const value = this.cache.get(key)!; this.cache.delete(key); this.cache.set(key, value); return value; } /** * Set a value in the cache * @param key - The cache key * @param value - The value to cache */ set(key: string, value: V): void { // If key exists, delete it first so it gets moved to the end if (this.cache.has(key)) { this.cache.delete(key); } // If we're at max size, delete the oldest item (first in the Map) else if (this.cache.size >= this.maxSize) { const firstKey = Array.from(this.cache.keys())[0]; if (firstKey) { this.cache.delete(firstKey); Logger.debug(`[MatrixLRUCache] Evicted oldest item from cache`); } } // Add the new item this.cache.set(key, value); } /** * Remove a value from the cache * @param key - The cache key to remove * @returns true if the key was found and removed, false otherwise */ delete(key: string): boolean { return this.cache.delete(key); } /** * Clear the entire cache */ clear(): void { this.cache.clear(); Logger.debug(`[MatrixLRUCache] Cache cleared`); } /** * Get the current size of the cache */ get size(): number { return this.cache.size; } } /** * Batch operation handler for Matrix SDK * Groups multiple similar operations to reduce network overhead */ export class BatchOperationHandler { private batchQueue: Map<string, BatchQueueEntry<unknown>> = new Map(); private readonly batchDelay: number; private readonly maxBatchSize: number; constructor(batchDelay = 50, maxBatchSize = 20) { this.batchDelay = batchDelay; this.maxBatchSize = maxBatchSize; Logger.info(`[BatchOperationHandler] Initialized with delay: ${batchDelay}ms, max size: ${maxBatchSize}`); } /** * Add an operation to the batch queue * @param queueName - Identifier for the batch queue * @param operation - The operation to add * @param executeCallback - Function to execute when the batch is processed * @returns Promise that resolves when the operation is processed */ addToBatch<T>( queueName: string, operation: T, executeCallback: (operations: T[]) => Promise<OperationResult[]> ): Promise<OperationResult> { return new Promise((resolve, reject) => { // Include resolve/reject with the operation const enhancedOperation: EnhancedOperation<T> = { data: operation, resolve, reject }; // Get or create the queue if (!this.batchQueue.has(queueName)) { this.batchQueue.set(queueName, { operations: [enhancedOperation] as EnhancedOperation<unknown>[], timer: setTimeout(() => this.processBatch(queueName, executeCallback as unknown as (operations: unknown[]) => Promise<OperationResult[]>), this.batchDelay) }); } else { const queue = this.batchQueue.get(queueName)!; (queue.operations as EnhancedOperation<T>[]).push(enhancedOperation); // If we've reached the max batch size, process immediately if (queue.operations.length >= this.maxBatchSize) { clearTimeout(queue.timer); this.processBatch(queueName, executeCallback as unknown as (operations: unknown[]) => Promise<OperationResult[]>); } } }); } /** * Process a batch of operations * @param queueName - Name of the batch queue * @param executeCallback - Function to execute the batch */ private async processBatch<T>( queueName: string, executeCallback: (operations: T[]) => Promise<OperationResult[]> ): Promise<void> { if (!this.batchQueue.has(queueName)) return; const queue = this.batchQueue.get(queueName)!; this.batchQueue.delete(queueName); try { // Extract just the data for processing const operations = queue.operations.map(op => op.data) as T[]; Logger.debug(`[BatchOperationHandler] Processing batch of ${operations.length} operations for ${queueName}`); // Execute the batch const results = await executeCallback(operations); // Resolve all promises queue.operations.forEach((op, index) => { op.resolve(results[index]); }); } catch (error) { Logger.error(`[BatchOperationHandler] Error processing batch for ${queueName}:`, error); // Create a proper error object const matrixError = new MatrixError( `Failed to process batch operations for ${queueName}`, MatrixErrorType.EVENT, error instanceof Error ? error : undefined, { queueName, operationCount: queue.operations.length } ); // Reject all promises with the enhanced error queue.operations.forEach(op => { op.reject(matrixError); }); } } /** * Cancel all pending batch operations */ cancelAll(): void { for (const queueName of this.batchQueue.keys()) { const queue = this.batchQueue.get(queueName)!; clearTimeout(queue.timer); queue.operations.forEach(op => { op.reject(new Error(`Batch operation cancelled for queue: ${queueName}`)); }); Logger.debug(`[BatchOperationHandler] Cancelled batch operations for queue: ${queueName}`); } this.batchQueue.clear(); Logger.debug(`[BatchOperationHandler] All batch operations cancelled`); } } /** * Message pagination optimizations */ export class PaginationOptimizer { private readonly roomCache: MatrixLRUCache<MessageResult>; private preloadedMessages: Map<string, MatrixMessage[]> = new Map(); private loadingMore: Map<string, boolean> = new Map(); constructor(cacheSize = 20) { this.roomCache = new MatrixLRUCache<MessageResult>(cacheSize); Logger.info(`[PaginationOptimizer] Initialized with cache size: ${cacheSize}`); } /** * Get cached pagination results * @param roomId - The room ID * @returns Cached pagination results or undefined */ getCachedResults(roomId: string): MessageResult | undefined { return this.roomCache.get(roomId); } /** * Store pagination results in cache * @param roomId - The room ID * @param results - The pagination results to cache */ cacheResults(roomId: string, results: MessageResult): void { this.roomCache.set(roomId, results); } /** * Preload next page of messages in background * @param roomId - The room ID * @param getNextPage - Function to fetch the next page * @param token - Pagination token */ preloadNextPage(roomId: string, getNextPage: (token: string) => Promise<MessageResult>, token: string): void { // Skip if already loading or no next token if (this.loadingMore.get(roomId) || !token) return; // Mark as loading this.loadingMore.set(roomId, true); // Fetch next page in background getNextPage(token) .then(results => { // Store preloaded messages this.preloadedMessages.set(roomId, results.messages); Logger.debug(`[PaginationOptimizer] Preloaded ${results.messages.length} messages for room ${roomId}`); // Reset loading state this.loadingMore.set(roomId, false); }) .catch(error => { Logger.error(`[PaginationOptimizer] Error preloading messages for room ${roomId}:`, error); // Reset loading state this.loadingMore.set(roomId, false); }); } /** * Get preloaded messages if available * @param roomId - The room ID * @returns Preloaded messages or undefined */ getPreloadedMessages(roomId: string): MatrixMessage[] | undefined { const messages = this.preloadedMessages.get(roomId); if (messages) { // Clear preloaded messages once used this.preloadedMessages.delete(roomId); } return messages; } /** * Clear cache for a specific room * @param roomId - The room ID to clear */ clearRoomCache(roomId: string): void { this.roomCache.delete(roomId); this.preloadedMessages.delete(roomId); this.loadingMore.delete(roomId); } /** * Clear all caches */ clearAll(): void { this.roomCache.clear(); this.preloadedMessages.clear(); this.loadingMore.clear(); } } /** * Matrix event data type */ interface MatrixEventData { id?: string; [key: string]: unknown; } /** * Event buffer entry type */ interface EventBufferEntry { events: MatrixEventData[]; timer: TimerHandle; } /** * Event optimization utilities */ export class EventOptimizer { private eventBuffers: Map<string, EventBufferEntry> = new Map(); private readonly bufferTime: number; private debouncedEvents: Map<string, { timer: TimerHandle, lastEventData: MatrixEventData }> = new Map(); private readonly debounceTime: number; constructor(bufferTime = 100, debounceTime = 300) { this.bufferTime = bufferTime; this.debounceTime = debounceTime; Logger.info(`[EventOptimizer] Initialized with buffer time: ${bufferTime}ms, debounce time: ${debounceTime}ms`); } /** * Buffer events to reduce the number of callbacks fired * @param eventType - The type of event * @param event - The event data * @param callback - Function to call with buffered events */ bufferEvent(eventType: string, event: MatrixEventData, callback: (events: MatrixEventData[]) => void): void { if (!this.eventBuffers.has(eventType)) { this.eventBuffers.set(eventType, { events: [event], timer: setTimeout(() => { if (this.eventBuffers.has(eventType)) { const buffer = this.eventBuffers.get(eventType)!; this.eventBuffers.delete(eventType); const uniqueEvents = this.deduplicateEvents(buffer.events); Logger.debug(`[EventOptimizer] Processing ${uniqueEvents.length} buffered events for ${eventType}`); callback(uniqueEvents); } }, this.bufferTime) }); } else { const buffer = this.eventBuffers.get(eventType)!; buffer.events.push(event); } } /** * Debounce events to reduce processing of rapidly-firing events * Only the last event in a series will trigger the callback * @param eventKey - Unique identifier for the debounced event * @param event - The event data * @param callback - Function to call with the final event */ debounceEvent(eventKey: string, event: MatrixEventData, callback: (event: MatrixEventData) => void): void { // If we already have a timer for this event, clear it if (this.debouncedEvents.has(eventKey)) { const existing = this.debouncedEvents.get(eventKey)!; clearTimeout(existing.timer); } // Set up a new timer const timer = setTimeout(() => { if (this.debouncedEvents.has(eventKey)) { const eventData = this.debouncedEvents.get(eventKey)!.lastEventData; this.debouncedEvents.delete(eventKey); Logger.debug(`[EventOptimizer] Processing debounced event: ${eventKey}`); callback(eventData); } }, this.debounceTime); // Store the new timer and event data this.debouncedEvents.set(eventKey, { timer, lastEventData: event }); } /** * Deduplicate events based on event ID * @param events - Array of events to deduplicate * @returns Deduplicated events array */ private deduplicateEvents(events: MatrixEventData[]): MatrixEventData[] { const seen = new Set<string>(); return events.filter(event => { // Skip events without ID if (!event.id) return true; // Only include each event ID once if (seen.has(event.id)) return false; seen.add(event.id); return true; }); } /** * Cancel all pending event buffers and debounced events */ cancelAll(): void { // Clear buffered events for (const eventType of this.eventBuffers.keys()) { const buffer = this.eventBuffers.get(eventType)!; clearTimeout(buffer.timer); Logger.debug(`[EventOptimizer] Cancelled event buffer for event type: ${eventType}`); } // Clear debounced events for (const eventKey of this.debouncedEvents.keys()) { const debounced = this.debouncedEvents.get(eventKey)!; clearTimeout(debounced.timer); Logger.debug(`[EventOptimizer] Cancelled debounced event: ${eventKey}`); } this.eventBuffers.clear(); this.debouncedEvents.clear(); Logger.debug(`[EventOptimizer] All event buffers and debounced events cancelled`); } } // This file exports utility classes for optimization: // - MatrixLRUCache: For efficient caching with automatic eviction // - BatchOperationHandler: For grouping similar operations // - PaginationOptimizer: For efficient message pagination // - EventOptimizer: For event buffering and debouncing // // These are imported by optimizations-registry.ts and exposed through the optimizers object.