ge-tool/utils/use-realtime-downloads.ts
2025-12-10 13:41:43 +07:00

166 lines
4.8 KiB
TypeScript
Executable File

/**
* Hook for Supabase Realtime Downloads
* Subscribes to downloads table changes and updates UI in realtime
*
* PERFORMANCE: Events are throttled/batched to prevent excessive re-renders
* - INSERT: Called immediately (low frequency)
* - UPDATE: Batched in 500ms windows (high frequency during downloads)
* - DELETE: Called immediately (low frequency)
*/
import { useEffect, useCallback, useRef } from 'react';
import { RealtimeChannel } from '@supabase/supabase-js';
import { supabase } from './supabase';
// Throttle interval for UPDATE events (ms)
const UPDATE_THROTTLE_MS = 500;
export interface DownloadRecord {
id: number;
batch_id: string;
ge_id: string;
lang: string;
file_name: string;
file_path: string;
mode: 'api' | 'sharing';
status: 'pending' | 'downloading' | 'completed' | 'failed' | 'cancelled';
destination_path: string | null;
file_size: number | null;
downloaded_size: number | null;
progress_percent: number | null;
created_at: string | null;
started_at: string | null;
completed_at: string | null;
error_message: string | null;
retry_count: number | null;
sharing_id: string | null;
mongodb_path: string | null;
aria2_gid: string | null;
}
interface UseRealtimeDownloadsOptions {
onInsert?: (record: DownloadRecord) => void;
onUpdate?: (record: DownloadRecord) => void;
onDelete?: (record: DownloadRecord) => void;
/** Optional: filter by batch_id */
batchId?: string;
}
/**
* Subscribe to realtime changes on downloads table
* UPDATE events are throttled to reduce re-renders during active downloads
*/
export function useRealtimeDownloads(options: UseRealtimeDownloadsOptions = {}) {
const { onInsert, onUpdate, onDelete, batchId } = options;
const channelRef = useRef<RealtimeChannel | null>(null);
// Throttle state for UPDATE events
const pendingUpdatesRef = useRef<Map<number, DownloadRecord>>(new Map());
const flushTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const onUpdateRef = useRef(onUpdate);
// Keep onUpdate ref current
useEffect(() => {
onUpdateRef.current = onUpdate;
}, [onUpdate]);
// Flush all pending updates
const flushUpdates = useCallback(() => {
if (pendingUpdatesRef.current.size > 0) {
// Call onUpdate for each pending record
pendingUpdatesRef.current.forEach(record => {
onUpdateRef.current?.(record);
});
pendingUpdatesRef.current.clear();
}
flushTimeoutRef.current = null;
}, []);
// Queue an update (throttled)
const queueUpdate = useCallback((record: DownloadRecord) => {
// Store latest update for this record (overwrites previous if exists)
pendingUpdatesRef.current.set(record.id, record);
// Schedule flush if not already scheduled
if (!flushTimeoutRef.current) {
flushTimeoutRef.current = setTimeout(flushUpdates, UPDATE_THROTTLE_MS);
}
}, [flushUpdates]);
const subscribe = useCallback(() => {
// Unsubscribe existing channel
if (channelRef.current) {
channelRef.current.unsubscribe();
}
// Create filter if batchId provided
const filter = batchId ? `batch_id=eq.${batchId}` : undefined;
const channel = supabase
.channel('downloads-changes')
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'downloads',
filter,
},
(payload) => {
onInsert?.(payload.new as DownloadRecord);
}
)
.on(
'postgres_changes',
{
event: 'UPDATE',
schema: 'public',
table: 'downloads',
filter,
},
(payload) => {
// Throttle UPDATE events - they happen frequently during downloads
queueUpdate(payload.new as DownloadRecord);
}
)
.on(
'postgres_changes',
{
event: 'DELETE',
schema: 'public',
table: 'downloads',
filter,
},
(payload) => {
onDelete?.(payload.old as DownloadRecord);
}
)
.subscribe();
channelRef.current = channel;
return () => {
channel.unsubscribe();
};
}, [onInsert, onDelete, batchId, queueUpdate]); // Removed onUpdate - now using ref
useEffect(() => {
const cleanup = subscribe();
return () => {
cleanup();
// Clear any pending throttle timeout on unmount
if (flushTimeoutRef.current) {
clearTimeout(flushTimeoutRef.current);
flushTimeoutRef.current = null;
}
// Flush any remaining updates
pendingUpdatesRef.current.clear();
};
}, [subscribe]);
return {
/** Manually resubscribe */
resubscribe: subscribe,
};
}