/** * SessionProvider (framework-wired render prop, slot terminal design §7) plus * the two internal channels the render machinery shares: the renderer host * context (written once by createSlotRenderer's root) and the per-session * binding context (written here, read by session-scope outlets). Both * contexts are in-package machinery — they are NOT exported from the package * index; business components see zero React contexts. */ import { createContext, useContext, type ReactNode } from 'react' import type { HostObservable, SessionCell, SlotRendererHost, SnapshotSelectorHook, } from '@deepseek-ai/dsh-client-ui-slots' import { bindSnapshotSelector } from './bind.ts' /** * A missing-provider assembly error: the shell wired the tree wrong. The slot * error boundary rethrows this class so misassembly stays fail-loud while * registrant errors (inject factories, entry components) are contained * per entry. */ export class SlotAssemblyError extends Error {} /** Renderer host channel: written by createSlotRenderer's root element (in-package machinery only). */ export const HostContext = createContext(null) /** * Read the installed renderer host; throws outside the rendered root tree * (framework components must not render detached from the renderer). * @returns the host surface. */ export function useHost(): SlotRendererHost { const host = useContext(HostContext) if (!host) throw new SlotAssemblyError('slot machinery rendered outside the installed renderer tree') return host } /** Per-session binding channel for the subtree under SessionProvider (in-package machinery only). */ const BindingContext = createContext(null) /** * Read the enclosing session cell; throws outside a SessionProvider subtree * (session slots must not render without a session). * @returns the enclosing cell. */ export function useSessionCell(): SessionCell { const cell = useContext(BindingContext) if (!cell) throw new SlotAssemblyError('session slot rendered outside SessionProvider') return cell } /** * Identity-stable selector hook per host observable. uSES resubscribes when * the subscribe reference changes, so the bound hook must be created once per * source — cached here by source identity (sources are host-owned singletons). * @param source - host-provided observable. * @returns the cached selector hook. */ export function observableHook(source: HostObservable): SnapshotSelectorHook { let hook = hookCache.get(source) if (hook === undefined) { hook = bindSnapshotSelector(source) hookCache.set(source, hook) } return hook as SnapshotSelectorHook } const hookCache = new WeakMap() /** SessionProvider surface: render-prop body plus the no-session branch. */ export interface SessionProviderProps { /** No-session body (also covers a current id whose session cannot be resolved). */ empty?: (() => ReactNode) | undefined /** Session body; remounted per session via key={sessionId}. */ children: (sessionId: string) => ReactNode } /** * Framework-wired session area: subscribes to the host's current-session * source (design fiat ① — selection authority lives with runtime sessions), * resolves the session cell, and remounts the body under key={sessionId} so * a session switch rebuilds the whole session subtree. Ids speak plain * string at this dependency-inverted layer; branding lands on the component * props seam (PropsRuntime). */ export function SessionProvider({ empty, children }: SessionProviderProps) { const host = useHost() const id = observableHook(host.sessions.current)((s) => s) const cell = id === undefined ? undefined : host.sessions.cell(id) if (id === undefined || cell === undefined) return <>{empty?.() ?? null} return ( {children(id)} ) }