Real-time Synchronization

Real-time data synchronization, WebSocket connections, and live updates in Smart Shelf.

Real-time Synchronization

This section covers real-time data synchronization using Supabase Realtime, WebSocket connections, and live update mechanisms in Smart Shelf.

Real-time Architecture Overview

Smart Shelf implements comprehensive real-time data synchronization to ensure all connected clients stay in sync with the latest data changes across the system.

Real-time Flow

graph TD
    A[User Action] --> B[Database Change]
    B --> C[Database Trigger]
    C --> D[Supabase Realtime]
    D --> E[WebSocket Broadcast]
    E --> F[Connected Clients]
    F --> G[UI Updates]
    
    style A fill:#e1f5fe
    style D fill:#fff3e0
    style G fill:#e8f5e8

Supabase Realtime Integration

Realtime Manager

// lib/realtime/client.ts
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs';
import { RealtimeChannel } from '@supabase/supabase-js';

export class RealtimeManager {
  private supabase = createClientComponentClient();
  private channels = new Map<string, RealtimeChannel>();
  private subscriptions = new Map<string, Set<(payload: any) => void>>();

  // Subscribe to table changes
  subscribeToTable<T>(
    table: string,
    filter?: string,
    callback?: (payload: any) => void
  ) {
    const channelName = `${table}${filter ? `-${filter}` : ''}`;
    
    if (!this.channels.has(channelName)) {
      const channel = this.supabase
        .channel(channelName)
        .on(
          'postgres_changes',
          {
            event: '*',
            schema: 'public',
            table,
            filter,
          },
          (payload) => {
            // Notify all subscribers
            const subscribers = this.subscriptions.get(channelName) || new Set();
            subscribers.forEach(cb => cb(payload));
          }
        )
        .subscribe();

      this.channels.set(channelName, channel);
      this.subscriptions.set(channelName, new Set());
    }

    // Add callback to subscribers
    if (callback) {
      const subscribers = this.subscriptions.get(channelName)!;
      subscribers.add(callback);
    }

    return () => {
      // Cleanup function
      const subscribers = this.subscriptions.get(channelName);
      if (subscribers && callback) {
        subscribers.delete(callback);
      }
    };
  }

  // Subscribe to custom events
  subscribeToEvent(
    event: string,
    callback: (payload: any) => void
  ) {
    const channel = this.supabase
      .channel('custom-events')
      .on('broadcast', { event }, callback)
      .subscribe();

    return () => {
      channel.unsubscribe();
    };
  }

  // Broadcast custom event
  broadcast(event: string, payload: any) {
    this.supabase
      .channel('custom-events')
      .send({
        type: 'broadcast',
        event,
        payload,
      });
  }

  // Cleanup all subscriptions
  cleanup() {
    this.channels.forEach(channel => {
      channel.unsubscribe();
    });
    this.channels.clear();
    this.subscriptions.clear();
  }
}

export const realtimeManager = new RealtimeManager();

Real-time Hooks

// hooks/use-realtime.ts
import { useEffect, useState, useCallback } from 'react';
import { realtimeManager } from '@/lib/realtime/client';

export function useRealtimeTable<T>(
  table: string,
  filter?: string,
  initialData?: T[]
) {
  const [data, setData] = useState<T[]>(initialData || []);
  const [loading, setLoading] = useState(!initialData);

  useEffect(() => {
    const unsubscribe = realtimeManager.subscribeToTable(
      table,
      filter,
      (payload) => {
        const { eventType, new: newRecord, old: oldRecord } = payload;

        setData(currentData => {
          switch (eventType) {
            case 'INSERT':
              return [...currentData, newRecord];
              
            case 'UPDATE':
              return currentData.map(item =>
                item.id === newRecord.id ? newRecord : item
              );
              
            case 'DELETE':
              return currentData.filter(item => item.id !== oldRecord.id);
              
            default:
              return currentData;
          }
        });
      }
    );

    setLoading(false);

    return unsubscribe;
  }, [table, filter]);

  const addRecord = useCallback((record: T) => {
    setData(current => [...current, record]);
  }, []);

  const updateRecord = useCallback((id: string, updates: Partial<T>) => {
    setData(current =>
      current.map(item =>
        item.id === id ? { ...item, ...updates } : item
      )
    );
  }, []);

  const removeRecord = useCallback((id: string) => {
    setData(current => current.filter(item => item.id !== id));
  }, []);

  return {
    data,
    loading,
    addRecord,
    updateRecord,
    removeRecord,
  };
}

// Inventory-specific real-time hook
export function useRealtimeInventory(warehouseId?: string) {
  const filter = warehouseId ? `warehouse_id=eq.${warehouseId}` : undefined;
  
  const {
    data: inventory,
    loading,
    updateRecord,
  } = useRealtimeTable('inventory', filter);

  // Listen for stock movements that affect inventory
  useEffect(() => {
    const unsubscribe = realtimeManager.subscribeToTable(
      'stock_movements',
      filter,
      (payload) => {
        if (payload.eventType === 'INSERT') {
          const movement = payload.new;
          
          // Update local inventory based on stock movement
          updateRecord(movement.product_id, (current) => ({
            ...current,
            quantity_on_hand: current.quantity_on_hand + movement.quantity,
            quantity_available: Math.max(0, 
              current.quantity_available + movement.quantity - current.quantity_allocated
            ),
          }));
        }
      }
    );

    return unsubscribe;
  }, [filter, updateRecord]);

  return { inventory, loading };
}

// Real-time order tracking
export function useRealtimeOrders(status?: string) {
  const filter = status ? `status=eq.${status}` : undefined;
  
  return useRealtimeTable('sales_orders', filter);
}

// Real-time notifications
export function useRealtimeNotifications(userId: string) {
  const [notifications, setNotifications] = useState<any[]>([]);

  useEffect(() => {
    const unsubscribe = realtimeManager.subscribeToEvent(
      `notification:${userId}`,
      (payload) => {
        setNotifications(current => [payload, ...current]);
      }
    );

    return unsubscribe;
  }, [userId]);

  const markAsRead = useCallback((notificationId: string) => {
    setNotifications(current =>
      current.map(notif =>
        notif.id === notificationId ? { ...notif, read: true } : notif
      )
    );
  }, []);

  return { notifications, markAsRead };
}

Real-time Components

Live Data Components

// components/realtime/live-inventory.tsx
import { useRealtimeInventory } from '@/hooks/use-realtime';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';

interface LiveInventoryProps {
  warehouseId?: string;
  productId?: string;
}

export function LiveInventory({ warehouseId, productId }: LiveInventoryProps) {
  const { inventory, loading } = useRealtimeInventory(warehouseId);

  const filteredInventory = productId
    ? inventory.filter(item => item.product_id === productId)
    : inventory;

  if (loading) {
    return <div>Loading inventory...</div>;
  }

  return (
    <div className="grid gap-4">
      {filteredInventory.map((item) => (
        <Card key={`${item.product_id}-${item.warehouse_id}`}>
          <CardHeader>
            <CardTitle className="flex items-center justify-between">
              {item.product.name}
              <Badge variant={item.quantity_available > 0 ? 'default' : 'destructive'}>
                {item.quantity_available > 0 ? 'In Stock' : 'Out of Stock'}
              </Badge>
            </CardTitle>
          </CardHeader>
          <CardContent>
            <div className="grid grid-cols-3 gap-4 text-sm">
              <div>
                <p className="text-muted-foreground">On Hand</p>
                <p className="font-medium">{item.quantity_on_hand}</p>
              </div>
              <div>
                <p className="text-muted-foreground">Available</p>
                <p className="font-medium">{item.quantity_available}</p>
              </div>
              <div>
                <p className="text-muted-foreground">Allocated</p>
                <p className="font-medium">{item.quantity_allocated}</p>
              </div>
            </div>
          </CardContent>
        </Card>
      ))}
    </div>
  );
}

Real-time Status Indicators

// components/realtime/status-indicator.tsx
import { useEffect, useState } from 'react';
import { realtimeManager } from '@/lib/realtime/client';
import { cn } from '@/lib/utils';

interface StatusIndicatorProps {
  table: string;
  recordId: string;
  statusField?: string;
  className?: string;
}

export function StatusIndicator({ 
  table, 
  recordId, 
  statusField = 'status',
  className 
}: StatusIndicatorProps) {
  const [status, setStatus] = useState<string>('unknown');
  const [lastUpdated, setLastUpdated] = useState<Date>();

  useEffect(() => {
    const unsubscribe = realtimeManager.subscribeToTable(
      table,
      `id=eq.${recordId}`,
      (payload) => {
        if (payload.eventType === 'UPDATE' && payload.new) {
          setStatus(payload.new[statusField]);
          setLastUpdated(new Date());
        }
      }
    );

    return unsubscribe;
  }, [table, recordId, statusField]);

  const getStatusColor = (status: string) => {
    switch (status) {
      case 'active':
      case 'completed':
      case 'delivered':
        return 'bg-green-500';
      case 'pending':
      case 'processing':
        return 'bg-yellow-500';
      case 'cancelled':
      case 'failed':
        return 'bg-red-500';
      default:
        return 'bg-gray-500';
    }
  };

  return (
    <div className={cn('flex items-center gap-2', className)}>
      <div className={cn('w-2 h-2 rounded-full', getStatusColor(status))} />
      <span className="text-sm capitalize">{status}</span>
      {lastUpdated && (
        <span className="text-xs text-muted-foreground">
          Updated {lastUpdated.toLocaleTimeString()}
        </span>
      )}
    </div>
  );
}

Real-time Events

Custom Event Broadcasting

// lib/realtime/events.ts
export enum RealtimeEvent {
  INVENTORY_UPDATED = 'inventory_updated',
  ORDER_STATUS_CHANGED = 'order_status_changed',
  USER_ACTIVITY = 'user_activity',
  SYSTEM_ALERT = 'system_alert',
  NOTIFICATION = 'notification',
}

export interface EventPayload {
  [RealtimeEvent.INVENTORY_UPDATED]: {
    productId: string;
    warehouseId: string;
    newQuantity: number;
    previousQuantity: number;
    userId: string;
  };
  
  [RealtimeEvent.ORDER_STATUS_CHANGED]: {
    orderId: string;
    newStatus: string;
    previousStatus: string;
    userId: string;
  };
  
  [RealtimeEvent.USER_ACTIVITY]: {
    userId: string;
    action: string;
    resourceType: string;
    resourceId: string;
  };
  
  [RealtimeEvent.SYSTEM_ALERT]: {
    type: 'warning' | 'error' | 'info';
    message: string;
    details?: any;
  };
  
  [RealtimeEvent.NOTIFICATION]: {
    userId: string;
    title: string;
    message: string;
    type: 'info' | 'success' | 'warning' | 'error';
    actionUrl?: string;
  };
}

export class EventBroadcaster {
  static broadcast<T extends RealtimeEvent>(
    event: T,
    payload: EventPayload[T]
  ): void {
    realtimeManager.broadcast(event, payload);
  }

  static broadcastToUser<T extends RealtimeEvent>(
    userId: string,
    event: T,
    payload: EventPayload[T]
  ): void {
    realtimeManager.broadcast(`${event}:${userId}`, payload);
  }

  static broadcastToWarehouse<T extends RealtimeEvent>(
    warehouseId: string,
    event: T,
    payload: EventPayload[T]
  ): void {
    realtimeManager.broadcast(`${event}:warehouse:${warehouseId}`, payload);
  }
}

// Usage examples
export function broadcastInventoryUpdate(
  productId: string,
  warehouseId: string,
  newQuantity: number,
  previousQuantity: number,
  userId: string
) {
  EventBroadcaster.broadcast(RealtimeEvent.INVENTORY_UPDATED, {
    productId,
    warehouseId,
    newQuantity,
    previousQuantity,
    userId,
  });
}

export function broadcastSystemAlert(
  type: 'warning' | 'error' | 'info',
  message: string,
  details?: any
) {
  EventBroadcaster.broadcast(RealtimeEvent.SYSTEM_ALERT, {
    type,
    message,
    details,
  });
}

Performance Considerations

Connection Management

// lib/realtime/connection-manager.ts
export class ConnectionManager {
  private connectionState: 'connecting' | 'connected' | 'disconnected' = 'disconnected';
  private reconnectAttempts = 0;
  private maxReconnectAttempts = 5;
  private reconnectDelay = 1000; // Start with 1 second

  constructor(private realtimeManager: RealtimeManager) {
    this.setupConnectionMonitoring();
  }

  private setupConnectionMonitoring() {
    // Monitor connection state
    setInterval(() => {
      this.checkConnectionHealth();
    }, 30000); // Check every 30 seconds

    // Handle window focus/blur for connection management
    window.addEventListener('focus', () => {
      if (this.connectionState === 'disconnected') {
        this.reconnect();
      }
    });

    window.addEventListener('beforeunload', () => {
      this.disconnect();
    });
  }

  private checkConnectionHealth() {
    // Implementation to check if connection is healthy
    // This could involve sending a ping or checking last received message timestamp
  }

  private async reconnect() {
    if (this.reconnectAttempts >= this.maxReconnectAttempts) {
      console.error('Max reconnection attempts reached');
      return;
    }

    this.connectionState = 'connecting';
    this.reconnectAttempts++;

    try {
      // Attempt to reconnect
      await this.connect();
      this.connectionState = 'connected';
      this.reconnectAttempts = 0;
      this.reconnectDelay = 1000; // Reset delay
    } catch (error) {
      console.error('Reconnection failed:', error);
      // Exponential backoff
      this.reconnectDelay = Math.min(this.reconnectDelay * 2, 30000);
      
      setTimeout(() => {
        this.reconnect();
      }, this.reconnectDelay);
    }
  }

  private async connect() {
    // Implementation to establish connection
  }

  private disconnect() {
    this.realtimeManager.cleanup();
    this.connectionState = 'disconnected';
  }

  getConnectionState() {
    return this.connectionState;
  }
}

Real-time Best Practices

Implementation Guidelines

  • Selective Subscriptions: Only subscribe to data you actually need
  • Connection Management: Handle reconnections and cleanup properly
  • Error Handling: Gracefully handle connection failures
  • Rate Limiting: Avoid overwhelming clients with too many updates
  • Batch Updates: Group related changes when possible

Performance Tips

  • Use filters to reduce unnecessary data transfer
  • Implement proper cleanup in useEffect hooks
  • Consider debouncing rapid updates
  • Monitor connection health and implement reconnection logic
  • Use optimistic updates for better user experience

This real-time synchronization system ensures Smart Shelf maintains data consistency across all connected clients while providing excellent performance and reliability.