Files
citation-evidence/src/binder/BinderProvider.tsx
tegwick b28feaad42 Persist capture data per session (field values, schema, links)
Capture mode state lived only in React memory and was lost when
reopening a session or remounting EngineProvider.

- Add per-session localStorage capture snapshot (schema, values, links)
- Restore on session mount; persist on field/schema/link changes
- Seed binder links from storage without spurious bus events
- Clean up capture key when session is deleted
- Integration test for reload persistence
2026-06-08 01:23:48 +02:00

120 lines
3.5 KiB
TypeScript

/**
* 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<BinderServices | null>(null);
export function useBinder(): BinderServices {
const ctx = useContext(BinderServicesContext);
if (!ctx) throw new Error("useBinder: missing <BinderProvider />");
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<BinderServices, "rect">;
/**
* 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<BinderServices>(() => {
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 (
<BinderServicesContext.Provider value={built}>
<RectRegistryProvider value={built.rect}>
<ActiveStateProvider bus={bus}>{children}</ActiveStateProvider>
</RectRegistryProvider>
</BinderServicesContext.Provider>
);
}
export { useActiveState };