166 lines
4.8 KiB
TypeScript
Executable File
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,
|
|
};
|
|
}
|