/** * BinderProvider — composition root for the binder subsystem. * * Wires the four binder concerns (rect registry, binding service, link * repo, active state machine) into one provider so a single mount inside * the EngineProvider gives every binder consumer (FormRenderer, evidence * picker, SVG overlay) what it needs. * * The provider is split out from the engine because in a future * subsystem-extraction these will live in separate packages — the engine * will publish only the event bus and the engine services, while * `evidence-binder` will export this provider. */ import { createContext, useContext, useEffect, useMemo, type ReactNode, } from "react"; import type { EvidenceLink } from "@shared/evidence-link"; import type { EventBus } from "@engine/events"; import { ActiveStateProvider, useActiveState, } from "./state/active"; import { createInMemoryLinkRepo, type EvidenceLinkRepository, } from "./repos/in-memory-links"; import { createBindingService, type BindingService, } from "./services/bindings"; import { RectRegistryProvider, createRectRegistryContextValue, type RectRegistryContextValue, } from "./visual-guide/react-hooks"; export interface BinderServices { readonly links: EvidenceLinkRepository; readonly bindings: BindingService; readonly rect: RectRegistryContextValue; } const BinderServicesContext = createContext(null); export function useBinder(): BinderServices { const ctx = useContext(BinderServicesContext); if (!ctx) throw new Error("useBinder: missing "); return ctx; } export interface BinderProviderProps { readonly children: ReactNode; /** * The engine's event bus, threaded in by the composition root so the * binder can emit §4 events without importing work/EngineContext * (work cannot be a dependency of binder — see DependencyMap §2). */ readonly bus: EventBus; /** * Tests can inject a pre-built service set; production constructs a * fresh one. The rect registry is *always* fresh per provider mount * because its observers attach to the current `window`. */ readonly services?: Omit; /** * Restored evidence links for this session. Seeded directly into the * repo (no bus events) so reload does not spuriously re-emit * `EvidenceLinkCreated`. */ readonly initialLinks?: readonly EvidenceLink[]; } export function BinderProvider({ children, bus, services, initialLinks, }: BinderProviderProps) { const built = useMemo(() => { const links = services?.links ?? createInMemoryLinkRepo(); const bindings = services?.bindings ?? createBindingService(links, bus); const rect = createRectRegistryContextValue(); return { links, bindings, rect }; }, [bus, services]); useEffect(() => { if (!initialLinks?.length || services?.links) return; for (const link of initialLinks) { if (!built.links.get(link.id)) { built.links.create(link); } } }, [built.links, initialLinks, services?.links]); // Disconnect rect observers + listeners on unmount. useEffect(() => { return () => { built.rect.observer.disconnect(); }; }, [built.rect]); return ( {children} ); } export { useActiveState };