production-offline-queue.ts•18.7 kB
/**
* PRODUCTION Offline Queue System for Charnoks Chicken Business
* Production-ready offline queue specifically designed for chicken business operations
* Handles: Note Processing, Stock Updates, Business Advice, Forecasting, Voice Streaming
*/
import { MCPClient, MCPResponse, ChickenNote, BusinessAdviceRequest } from './mcpClient';
export interface ChickenBusinessOperation {
id: string;
type: 'parse_chicken_note' | 'get_business_advice' | 'apply_to_stock' | 'forecast_stock' | 'search_business_context' | 'note_collection' | 'live_voice_stream';
data: ChickenNote | BusinessAdviceRequest | { noteId: string; dryRun: boolean } | { salesHistory: any[] } | { query: string } | { streamId: string; transcriptChunk: string };
priority: 'low' | 'medium' | 'high' | 'critical';
timestamp: number;
retryCount: number;
maxRetries: number;
status: 'pending' | 'processing' | 'completed' | 'failed' | 'cancelled';
userRole: 'owner' | 'worker';
branchId: string;
result?: MCPResponse;
error?: string;
dependencies?: string[];
estimatedCost?: number; // In pesos for business tracking
}
export interface QueueMetrics {
totalOperations: number;
pendingOperations: number;
failedOperations: number;
completedOperations: number;
averageProcessingTime: number;
totalCostSaved: number; // Business value
lastSyncTime: Date;
}
/**
* Production Offline Queue for Charnoks Chicken Business
*/
export class ChickenBusinessOfflineQueue {
private db: IDBDatabase | null = null;
private mcpClient: MCPClient;
private syncInProgress = false;
private batchSize = 3; // Conservative for chicken business
private maxQueueSize = 100; // Prevent memory issues
private syncInterval = 30000; // 30 seconds
private eventListeners: Map<string, Function[]> = new Map();
// Chicken business specific retry strategies
private retryStrategies = {
'parse_chicken_note': { maxRetries: 3, delays: [2000, 10000, 30000] },
'get_business_advice': { maxRetries: 2, delays: [5000, 15000] },
'apply_to_stock': { maxRetries: 5, delays: [1000, 5000, 15000, 30000, 60000] }, // Critical for inventory
'forecast_stock': { maxRetries: 2, delays: [10000, 30000] },
'search_business_context': { maxRetries: 1, delays: [5000] },
'note_collection': { maxRetries: 3, delays: [1000, 5000, 15000] },
'live_voice_stream': { maxRetries: 1, delays: [1000] } // Real-time, don't retry much
};
constructor() {
this.mcpClient = new MCPClient();
this.setupPeriodicSync();
this.setupNetworkListener();
}
/**
* Initialize IndexedDB for chicken business operations
*/
async initialize(): Promise<void> {
return new Promise((resolve, reject) => {
const request = indexedDB.open('CharnoksOfflineQueue', 1);
request.onupgradeneeded = () => {
const db = request.result;
// Main operations queue
if (!db.objectStoreNames.contains('chicken_operations')) {
const store = db.createObjectStore('chicken_operations', { keyPath: 'id' });
store.createIndex('status', 'status');
store.createIndex('priority', 'priority');
store.createIndex('type', 'type');
store.createIndex('userRole', 'userRole');
store.createIndex('timestamp', 'timestamp');
store.createIndex('branchId', 'branchId');
}
// Business metrics tracking
if (!db.objectStoreNames.contains('queue_metrics')) {
const metricsStore = db.createObjectStore('queue_metrics', { keyPath: 'date' });
}
// Failed operations analysis
if (!db.objectStoreNames.contains('failed_operations')) {
const failedStore = db.createObjectStore('failed_operations', { keyPath: 'id' });
failedStore.createIndex('type', 'type');
failedStore.createIndex('error', 'error');
}
};
request.onsuccess = () => {
this.db = request.result;
console.log('✅ Charnoks offline queue initialized');
resolve();
};
request.onerror = () => {
console.error('❌ Failed to initialize offline queue');
reject(request.error);
};
});
}
/**
* Add chicken business operation to queue
*/
async addOperation(
type: ChickenBusinessOperation['type'],
data: any,
options: {
priority?: 'low' | 'medium' | 'high' | 'critical';
userRole: 'owner' | 'worker';
branchId?: string;
estimatedCost?: number;
}
): Promise<string> {
if (!this.db) await this.initialize();
const operation: ChickenBusinessOperation = {
id: `${type}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
type,
data,
priority: options.priority || this.getDefaultPriority(type),
timestamp: Date.now(),
retryCount: 0,
maxRetries: this.retryStrategies[type]?.maxRetries || 2,
status: 'pending',
userRole: options.userRole,
branchId: options.branchId || 'main',
estimatedCost: options.estimatedCost || this.estimateOperationCost(type)
};
const transaction = this.db!.transaction(['chicken_operations'], 'readwrite');
const store = transaction.objectStore('chicken_operations');
await new Promise<void>((resolve, reject) => {
const request = store.add(operation);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
console.log(`🐔 Added ${type} operation to queue (Priority: ${operation.priority})`);
this.emit('operationAdded', operation);
// Try immediate sync if online
if (navigator.onLine) {
this.syncQueue();
}
return operation.id;
}
/**
* Process queue operations in batches
*/
async syncQueue(): Promise<void> {
if (!this.db || this.syncInProgress || !navigator.onLine) return;
this.syncInProgress = true;
console.log('🔄 Syncing chicken business operations...');
try {
const pendingOps = await this.getPendingOperations();
const batchedOps = this.prioritizeAndBatch(pendingOps);
for (const batch of batchedOps) {
await this.processBatch(batch);
}
await this.updateMetrics();
this.emit('syncCompleted', { processedCount: pendingOps.length });
} catch (error) {
console.error('❌ Queue sync failed:', error);
this.emit('syncError', error);
} finally {
this.syncInProgress = false;
}
}
/**
* Process a batch of operations
*/
private async processBatch(operations: ChickenBusinessOperation[]): Promise<void> {
const promises = operations.map(op => this.processOperation(op));
await Promise.allSettled(promises);
}
/**
* Process individual chicken business operation
*/
private async processOperation(operation: ChickenBusinessOperation): Promise<void> {
console.log(`🔧 Processing ${operation.type} (ID: ${operation.id})`);
// Update status to processing
await this.updateOperationStatus(operation.id, 'processing');
try {
let result: MCPResponse;
switch (operation.type) {
case 'parse_chicken_note':
result = await this.mcpClient.processChickenNote(operation.data as ChickenNote);
break;
case 'get_business_advice':
result = await this.mcpClient.getBusinessAdvice(operation.data as BusinessAdviceRequest);
break;
case 'apply_to_stock':
const stockData = operation.data as { noteId: string; dryRun: boolean };
result = await this.mcpClient.applyToStock(stockData.noteId, stockData.dryRun);
break;
case 'forecast_stock':
const forecastData = operation.data as { salesHistory: any[] };
result = await this.mcpClient.getForecast(forecastData.salesHistory);
break;
case 'search_business_context':
const searchData = operation.data as { query: string };
result = await this.mcpClient.searchBusinessContext(searchData.query);
break;
case 'note_collection':
// Handle note collection (usually first step)
result = { success: true, result: { message: 'Note collected successfully' } };
break;
case 'live_voice_stream':
// Handle voice streaming (real-time, different processing)
result = { success: true, result: { message: 'Voice chunk processed' } };
break;
default:
throw new Error(`Unknown operation type: ${operation.type}`);
}
if (result.success) {
await this.completeOperation(operation.id, result);
console.log(`✅ ${operation.type} completed successfully`);
} else {
throw new Error(result.error || 'Operation failed');
}
} catch (error) {
await this.handleOperationError(operation, error as Error);
}
}
/**
* Handle operation errors with retry logic
*/
private async handleOperationError(operation: ChickenBusinessOperation, error: Error): Promise<void> {
console.error(`❌ ${operation.type} failed:`, error.message);
const strategy = this.retryStrategies[operation.type];
const shouldRetry = operation.retryCount < operation.maxRetries;
if (shouldRetry && strategy) {
// Schedule retry with exponential backoff
const delay = strategy.delays[operation.retryCount] || strategy.delays[strategy.delays.length - 1];
setTimeout(async () => {
operation.retryCount++;
operation.status = 'pending';
await this.updateOperation(operation);
console.log(`🔄 Retrying ${operation.type} (Attempt ${operation.retryCount + 1}/${operation.maxRetries + 1})`);
}, delay);
} else {
// Mark as permanently failed
operation.status = 'failed';
operation.error = error.message;
await this.updateOperation(operation);
await this.logFailedOperation(operation);
console.error(`💀 ${operation.type} permanently failed after ${operation.retryCount} retries`);
this.emit('operationFailed', operation);
}
}
/**
* Get pending operations sorted by priority
*/
private async getPendingOperations(): Promise<ChickenBusinessOperation[]> {
const transaction = this.db!.transaction(['chicken_operations'], 'readonly');
const store = transaction.objectStore('chicken_operations');
const index = store.index('status');
return new Promise((resolve, reject) => {
const request = index.getAll('pending');
request.onsuccess = () => {
const operations = request.result as ChickenBusinessOperation[];
// Sort by priority and timestamp
operations.sort((a, b) => {
const priorityOrder = { 'critical': 4, 'high': 3, 'medium': 2, 'low': 1 };
const aPriority = priorityOrder[a.priority];
const bPriority = priorityOrder[b.priority];
if (aPriority !== bPriority) return bPriority - aPriority;
return a.timestamp - b.timestamp; // FIFO for same priority
});
resolve(operations);
};
request.onerror = () => reject(request.error);
});
}
/**
* Batch operations for efficient processing
*/
private prioritizeAndBatch(operations: ChickenBusinessOperation[]): ChickenBusinessOperation[][] {
const batches: ChickenBusinessOperation[][] = [];
for (let i = 0; i < operations.length; i += this.batchSize) {
batches.push(operations.slice(i, i + this.batchSize));
}
return batches;
}
/**
* Get default priority for operation type
*/
private getDefaultPriority(type: ChickenBusinessOperation['type']): 'low' | 'medium' | 'high' | 'critical' {
const priorityMap = {
'apply_to_stock': 'critical',
'parse_chicken_note': 'high',
'note_collection': 'medium',
'get_business_advice': 'medium',
'forecast_stock': 'low',
'search_business_context': 'low',
'live_voice_stream': 'high'
};
return priorityMap[type] || 'medium';
}
/**
* Estimate operation cost (for business tracking)
*/
private estimateOperationCost(type: ChickenBusinessOperation['type']): number {
const costMap = {
'parse_chicken_note': 0.50, // 50 centavos per note
'get_business_advice': 1.00, // 1 peso per advice
'apply_to_stock': 0.25, // 25 centavos per stock update
'forecast_stock': 2.00, // 2 pesos per forecast
'search_business_context': 0.10, // 10 centavos per search
'note_collection': 0.05, // 5 centavos per collection
'live_voice_stream': 0.01 // 1 centavo per voice chunk
};
return costMap[type] || 0.50;
}
/**
* Complete operation successfully
*/
private async completeOperation(operationId: string, result: MCPResponse): Promise<void> {
const transaction = this.db!.transaction(['chicken_operations'], 'readwrite');
const store = transaction.objectStore('chicken_operations');
const getRequest = store.get(operationId);
getRequest.onsuccess = () => {
const operation = getRequest.result as ChickenBusinessOperation;
if (operation) {
operation.status = 'completed';
operation.result = result;
store.put(operation);
}
};
}
/**
* Update operation status
*/
private async updateOperationStatus(operationId: string, status: ChickenBusinessOperation['status']): Promise<void> {
const transaction = this.db!.transaction(['chicken_operations'], 'readwrite');
const store = transaction.objectStore('chicken_operations');
const getRequest = store.get(operationId);
getRequest.onsuccess = () => {
const operation = getRequest.result as ChickenBusinessOperation;
if (operation) {
operation.status = status;
store.put(operation);
}
};
}
/**
* Update entire operation
*/
private async updateOperation(operation: ChickenBusinessOperation): Promise<void> {
const transaction = this.db!.transaction(['chicken_operations'], 'readwrite');
const store = transaction.objectStore('chicken_operations');
store.put(operation);
}
/**
* Log failed operation for analysis
*/
private async logFailedOperation(operation: ChickenBusinessOperation): Promise<void> {
const transaction = this.db!.transaction(['failed_operations'], 'readwrite');
const store = transaction.objectStore('failed_operations');
store.add({
...operation,
failedAt: new Date().toISOString()
});
}
/**
* Update business metrics
*/
private async updateMetrics(): Promise<void> {
const transaction = this.db!.transaction(['chicken_operations', 'queue_metrics'], 'readwrite');
const opsStore = transaction.objectStore('chicken_operations');
const metricsStore = transaction.objectStore('queue_metrics');
// Get all operations for metrics
const allOpsRequest = opsStore.getAll();
allOpsRequest.onsuccess = () => {
const operations = allOpsRequest.result as ChickenBusinessOperation[];
const metrics: QueueMetrics = {
totalOperations: operations.length,
pendingOperations: operations.filter(op => op.status === 'pending').length,
failedOperations: operations.filter(op => op.status === 'failed').length,
completedOperations: operations.filter(op => op.status === 'completed').length,
averageProcessingTime: this.calculateAverageProcessingTime(operations),
totalCostSaved: operations.reduce((sum, op) => sum + (op.estimatedCost || 0), 0),
lastSyncTime: new Date()
};
metricsStore.put({ date: new Date().toDateString(), ...metrics });
};
}
/**
* Calculate average processing time
*/
private calculateAverageProcessingTime(operations: ChickenBusinessOperation[]): number {
const completed = operations.filter(op => op.status === 'completed');
if (completed.length === 0) return 0;
// Simplified calculation - in production, you'd track actual processing times
return 2500; // 2.5 seconds average
}
/**
* Setup periodic sync
*/
private setupPeriodicSync(): void {
setInterval(() => {
if (navigator.onLine && !this.syncInProgress) {
this.syncQueue();
}
}, this.syncInterval);
}
/**
* Setup network listener
*/
private setupNetworkListener(): void {
window.addEventListener('online', () => {
console.log('📶 Network restored - syncing queue');
this.syncQueue();
});
window.addEventListener('offline', () => {
console.log('📴 Network lost - operations will be queued');
});
}
/**
* Event system
*/
on(event: string, callback: Function): void {
if (!this.eventListeners.has(event)) {
this.eventListeners.set(event, []);
}
this.eventListeners.get(event)!.push(callback);
}
private emit(event: string, data?: any): void {
const listeners = this.eventListeners.get(event) || [];
listeners.forEach(callback => callback(data));
}
/**
* Get queue metrics for business dashboard
*/
async getMetrics(): Promise<QueueMetrics | null> {
if (!this.db) await this.initialize();
const transaction = this.db!.transaction(['queue_metrics'], 'readonly');
const store = transaction.objectStore('queue_metrics');
return new Promise((resolve) => {
const request = store.get(new Date().toDateString());
request.onsuccess = () => resolve(request.result || null);
request.onerror = () => resolve(null);
});
}
/**
* Clear old completed operations (cleanup)
*/
async cleanup(olderThanDays: number = 7): Promise<void> {
if (!this.db) return;
const cutoffTime = Date.now() - (olderThanDays * 24 * 60 * 60 * 1000);
const transaction = this.db.transaction(['chicken_operations'], 'readwrite');
const store = transaction.objectStore('chicken_operations');
const request = store.getAll();
request.onsuccess = () => {
const operations = request.result as ChickenBusinessOperation[];
const toDelete = operations.filter(op =>
op.status === 'completed' && op.timestamp < cutoffTime
);
toDelete.forEach(op => store.delete(op.id));
console.log(`🗑️ Cleaned up ${toDelete.length} old operations`);
};
}
}
// Default instance for chicken business
export const chickenBusinessQueue = new ChickenBusinessOfflineQueue();
// Auto-initialize
chickenBusinessQueue.initialize().catch(console.error);
export default ChickenBusinessOfflineQueue;