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.