nativescript-matrix-sdk
Version:
Native Matrix SDK integration for NativeScript
468 lines (412 loc) • 14.5 kB
text/typescript
/**
* 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.