/**
* useEngine Hook - Connects the Ink TUI to the Torm engine.
*
* This hook provides React components with access to the torrent engine,
* maintaining state synchronization through event subscriptions and
* managing the engine lifecycle.
*
* @module ui/hooks/useEngine
*/
import { useState, useEffect, useRef, useCallback } from 'react';
import { TormEngine } from '../../engine/TormEngine.js';
import type { Torrent, Peer } from '../../engine/types.js';
// =============================================================================
// Singleton Engine Instance
// =============================================================================
/**
* Singleton TormEngine instance shared across all hook consumers.
* This ensures a single engine manages all torrent operations.
*/
let engineInstance: TormEngine | null = null;
/**
* Gets or creates the singleton TormEngine instance.
*
* @returns The shared TormEngine instance
*/
function getEngineInstance(): TormEngine {
if (!engineInstance) {
engineInstance = new TormEngine();
}
return engineInstance;
}
// =============================================================================
// Hook Return Type
// =============================================================================
/**
* Return type for the useEngine hook.
*/
export interface UseEngineResult {
/** The TormEngine instance for direct API access */
engine: TormEngine;
/** Array of all torrents currently managed by the engine */
torrents: Torrent[];
/** Whether the engine has started and is ready for operations */
isReady: boolean;
/** Gets the list of peers for a given torrent by info hash */
getPeers: (infoHash: string) => Peer[];
}
// =============================================================================
// useEngine Hook
// =============================================================================
/**
* React hook that connects UI components to the Torm engine.
*
* This hook:
* - Provides access to a singleton TormEngine instance
* - Subscribes to engine events and updates React state accordingly
* - Automatically cleans up subscriptions on unmount
* - Tracks engine readiness and torrent list state
*
* @returns Object containing engine instance, torrents array, and ready state
*
* @example
* ```tsx
* import { useEngine } from '../hooks/useEngine.js';
*
* function TorrentList() {
* const { engine, torrents, isReady } = useEngine();
*
* if (!isReady) {
* return Starting engine...;
* }
*
* return (
*
* {torrents.map(t => (
* {t.name}: {t.progress * 100}%
* ))}
*
* );
* }
* ```
*/
export function useEngine(): UseEngineResult {
// Get the singleton engine instance
const engineRef = useRef(getEngineInstance());
const engine = engineRef.current;
// React state for torrents and readiness
const [torrents, setTorrents] = useState(() =>
engine.getAllTorrents()
);
const [isReady, setIsReady] = useState(() => engine.isRunning());
// ==========================================================================
// Helper Functions
// ==========================================================================
/**
* Gets the list of peers for a given torrent.
*
* @param infoHash - The info hash of the torrent
* @returns Array of peers connected to the torrent, or empty array if not found
*/
const getPeers = useCallback(
(infoHash: string): Peer[] => {
return engine.getPeers(infoHash);
},
[engine]
);
// ==========================================================================
// Event Handlers
// ==========================================================================
/**
* Handle engine:ready event - marks the engine as ready for operations.
*/
const handleEngineReady = useCallback(() => {
setIsReady(true);
}, []);
/**
* Handle engine:started event - marks the engine as ready.
*/
const handleEngineStarted = useCallback(() => {
setIsReady(true);
}, []);
/**
* Handle engine:stopped event - marks the engine as not ready.
*/
const handleEngineStopped = useCallback(() => {
setIsReady(false);
}, []);
/**
* Handle torrent:added event - adds new torrent to state.
* Re-fetches the torrent from the engine to ensure we have the canonical type.
*/
const handleTorrentAdded = useCallback(
(payload: { torrent: { infoHash: string } }) => {
const torrent = engine.getTorrent(payload.torrent.infoHash);
if (!torrent) {
return;
}
setTorrents((prev) => {
// Avoid duplicates by checking info hash
const exists = prev.some((t) => t.infoHash === torrent.infoHash);
if (exists) {
return prev;
}
return [...prev, torrent];
});
},
[engine]
);
/**
* Handle torrent:removed event - removes torrent from state.
*/
const handleTorrentRemoved = useCallback((payload: { infoHash: string }) => {
setTorrents((prev) => prev.filter((t) => t.infoHash !== payload.infoHash));
}, []);
/**
* Handle torrent:progress event - updates torrent progress in state.
*/
const handleTorrentProgress = useCallback(
(payload: {
infoHash: string;
progress: number;
downloadSpeed: number;
uploadSpeed: number;
peers: number;
}) => {
setTorrents((prev) =>
prev.map((t): Torrent => {
if (t.infoHash !== payload.infoHash) {
return t;
}
// Update the torrent with new progress data
return {
...t,
progress: payload.progress,
downloadSpeed: payload.downloadSpeed,
uploadSpeed: payload.uploadSpeed,
peers: payload.peers,
};
})
);
},
[]
);
// ==========================================================================
// Event Subscription Effect
// ==========================================================================
// Start the engine on mount if not already running
useEffect(() => {
if (!engine.isRunning()) {
engine.start().catch((err) => {
console.error('[useEngine] Failed to start engine:', err);
});
}
}, [engine]);
useEffect(() => {
// Subscribe to engine events
engine.on('engine:ready', handleEngineReady);
engine.on('engine:started', handleEngineStarted);
engine.on('engine:stopped', handleEngineStopped);
engine.on('torrent:added', handleTorrentAdded);
engine.on('torrent:removed', handleTorrentRemoved);
engine.on('torrent:progress', handleTorrentProgress);
// Sync initial state in case engine was already running
if (engine.isRunning() && !isReady) {
setIsReady(true);
}
// Sync torrents in case some were already added
const currentTorrents = engine.getAllTorrents();
if (currentTorrents.length > 0 && torrents.length === 0) {
setTorrents(currentTorrents);
}
// Cleanup: unsubscribe from all events on unmount
return () => {
engine.off('engine:ready', handleEngineReady);
engine.off('engine:started', handleEngineStarted);
engine.off('engine:stopped', handleEngineStopped);
engine.off('torrent:added', handleTorrentAdded);
engine.off('torrent:removed', handleTorrentRemoved);
engine.off('torrent:progress', handleTorrentProgress);
};
}, [
engine,
isReady,
torrents.length,
handleEngineReady,
handleEngineStarted,
handleEngineStopped,
handleTorrentAdded,
handleTorrentRemoved,
handleTorrentProgress,
]);
return {
engine,
torrents,
isReady,
getPeers,
};
}
// =============================================================================
// Exports
// =============================================================================
export default useEngine;