import { AppState, AppStateStatus } from 'react-native'; import type { AnalyticsEvent, ScreenEvent, QueueConfig } from '../types'; type QueuedItem = | { type: 'track'; event: AnalyticsEvent } | { type: 'screen'; event: ScreenEvent }; type FlushHandler = (items: QueuedItem[]) => Promise; const DEFAULT_CONFIG: Required = { maxBatchSize: 20, flushInterval: 10_000, maxRetries: 3, persistQueue: true, }; /** * Buffers events in memory and flushes them in batches. * Handles offline detection via AppState and retries failed flushes * with exponential backoff. */ export class EventQueue { private queue: QueuedItem[] = []; private config: Required; private flushTimer: ReturnType | null = null; private onFlush: FlushHandler; private appStateSubscription?: any; private retryCount = 0; private isFlushing = false; constructor(onFlush: FlushHandler, config: QueueConfig = {}) { this.config = { ...DEFAULT_CONFIG, ...config }; this.onFlush = onFlush; } start(): void { this.flushTimer = setInterval(() => { this.flush(); }, this.config.flushInterval); this.appStateSubscription = AppState.addEventListener( 'change', this.handleAppStateChange ); } stop(): void { if (this.flushTimer) { clearInterval(this.flushTimer); this.flushTimer = null; } if (this.appStateSubscription) { this.appStateSubscription.remove(); } } enqueue(item: QueuedItem): void { this.queue.push(item); if (this.queue.length >= this.config.maxBatchSize) { this.flush(); } } async flush(): Promise { if (this.isFlushing || this.queue.length === 0) { return; } const batch = this.queue.splice(0, this.config.maxBatchSize); this.isFlushing = true; try { await this.onFlush(batch); this.retryCount = 0; } catch { // Put failed events back at the front of the queue this.queue.unshift(...batch); await this.retryFlush(); } finally { this.isFlushing = false; } } private async retryFlush(): Promise { if (this.retryCount >= this.config.maxRetries) { this.retryCount = 0; return; } this.retryCount++; const delay = Math.min(1000 * 2 ** this.retryCount, 30_000); await new Promise((resolve) => setTimeout(resolve, delay)); await this.flush(); } private handleAppStateChange = (state: AppStateStatus): void => { if (state === 'active') { // App came to foreground — flush any queued events this.flush(); } else if (state === 'background') { // App going to background — flush immediately this.flush(); } }; size(): number { return this.queue.length; } clear(): void { this.queue = []; } }