import { useCallback, useRef, useState } from 'react' import type { VaultEntry } from '../types' import type { ProbeResult, ProbeRunOptions, ProbeStep, ProbeTarget, ProbeWaiter, } from './editorMemoryProbeTypes' import { copyProbeResult, DEFAULT_PROBE_BATCH_SIZE, DEFAULT_PROBE_SETTLE_MS, loadProbeTarget, memoryDelta, readMemorySnapshot, resolveMountCounts, selectProbeEntries, settleAfterMount, summarizeTarget, PROBE_READY_TIMEOUT_MS, } from './editorMemoryProbeRuntime' function createProbeStep( mountedTargets: ProbeTarget[], snapshot: Awaited>, baseline: Awaited>, ): ProbeStep { return { mountedCount: mountedTargets.length, mountedPaths: mountedTargets.map(target => target.entry.path), snapshot, deltaBytes: memoryDelta(snapshot, baseline), } } function useProbeReadiness() { const readyPathsRef = useRef(new Set()) const waiterRef = useRef(null) const resolveWaiterIfReady = useCallback(() => { const waiter = waiterRef.current if (!waiter) return for (const path of waiter.paths) { if (!readyPathsRef.current.has(path)) return } window.clearTimeout(waiter.timer) waiterRef.current = null waiter.resolve() }, []) const handleReady = useCallback((path: string) => { readyPathsRef.current.add(path) resolveWaiterIfReady() }, [resolveWaiterIfReady]) const waitForReadyPaths = useCallback((paths: string[]) => { return new Promise((resolve) => { const unresolvedPaths = paths.filter(path => !readyPathsRef.current.has(path)) if (unresolvedPaths.length === 0) { resolve() return } const timer = window.setTimeout(() => { waiterRef.current = null console.warn('[memory-probe] Timed out waiting for hidden editors:', unresolvedPaths) resolve() }, PROBE_READY_TIMEOUT_MS) waiterRef.current = { paths: new Set(paths), resolve, timer } }) }, []) const clearReadiness = useCallback(() => { if (waiterRef.current) { window.clearTimeout(waiterRef.current.timer) waiterRef.current = null } readyPathsRef.current.clear() }, []) return { clearReadiness, handleReady, waitForReadyPaths } } export function useEditorMemoryProbeController(entries: VaultEntry[]) { const [targets, setTargets] = useState([]) const { clearReadiness, handleReady, waitForReadyPaths } = useProbeReadiness() const clear = useCallback(() => { clearReadiness() setTargets([]) }, [clearReadiness]) const run = useCallback(async (options: ProbeRunOptions = {}): Promise => { clear() try { await settleAfterMount(options.settleMs ?? DEFAULT_PROBE_SETTLE_MS) const baseline = await readMemorySnapshot() const selectedEntries = selectProbeEntries(entries, options) const loadedTargets = await Promise.all(selectedEntries.map(loadProbeTarget)) const afterContentLoad = await readMemorySnapshot() const batchSize = Math.max(1, options.batchSize ?? DEFAULT_PROBE_BATCH_SIZE) const settleMs = options.settleMs ?? DEFAULT_PROBE_SETTLE_MS const steps: ProbeStep[] = [] for (const count of resolveMountCounts(loadedTargets.length, batchSize)) { const mountedTargets = loadedTargets.slice(0, count) setTargets(mountedTargets) await waitForReadyPaths(mountedTargets.map(target => target.entry.path)) await settleAfterMount(settleMs) steps.push(createProbeStep(mountedTargets, await readMemorySnapshot(), baseline)) } return { targets: loadedTargets.map(summarizeTarget), baseline, afterContentLoad, contentLoadDeltaBytes: memoryDelta(afterContentLoad, baseline), steps, } } finally { clear() } }, [clear, entries, waitForReadyPaths]) const runAndCopy = useCallback(async (options: ProbeRunOptions = {}) => { const result = await run(options) await copyProbeResult(result) console.info('[memory-probe] Result copied to clipboard', result) return result }, [run]) return { clear, handleReady, run, runAndCopy, targets } }