1
0

session-provider.tsx 3.8 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
  1. /**
  2. * SessionProvider (framework-wired render prop, slot terminal design §7) plus
  3. * the two internal channels the render machinery shares: the renderer host
  4. * context (written once by createSlotRenderer's root) and the per-session
  5. * binding context (written here, read by session-scope outlets). Both
  6. * contexts are in-package machinery — they are NOT exported from the package
  7. * index; business components see zero React contexts.
  8. */
  9. import { createContext, useContext, type ReactNode } from 'react'
  10. import type {
  11. HostObservable, SessionCell, SlotRendererHost, SnapshotSelectorHook,
  12. } from '@deepseek-ai/dsh-client-ui-slots'
  13. import { bindSnapshotSelector } from './bind.ts'
  14. /**
  15. * A missing-provider assembly error: the shell wired the tree wrong. The slot
  16. * error boundary rethrows this class so misassembly stays fail-loud while
  17. * registrant errors (inject factories, entry components) are contained
  18. * per entry.
  19. */
  20. export class SlotAssemblyError extends Error {}
  21. /** Renderer host channel: written by createSlotRenderer's root element (in-package machinery only). */
  22. export const HostContext = createContext<SlotRendererHost | null>(null)
  23. /**
  24. * Read the installed renderer host; throws outside the rendered root tree
  25. * (framework components must not render detached from the renderer).
  26. * @returns the host surface.
  27. */
  28. export function useHost(): SlotRendererHost {
  29. const host = useContext(HostContext)
  30. if (!host) throw new SlotAssemblyError('slot machinery rendered outside the installed renderer tree')
  31. return host
  32. }
  33. /** Per-session binding channel for the subtree under SessionProvider (in-package machinery only). */
  34. const BindingContext = createContext<SessionCell | null>(null)
  35. /**
  36. * Read the enclosing session cell; throws outside a SessionProvider subtree
  37. * (session slots must not render without a session).
  38. * @returns the enclosing cell.
  39. */
  40. export function useSessionCell(): SessionCell {
  41. const cell = useContext(BindingContext)
  42. if (!cell) throw new SlotAssemblyError('session slot rendered outside SessionProvider')
  43. return cell
  44. }
  45. /**
  46. * Identity-stable selector hook per host observable. uSES resubscribes when
  47. * the subscribe reference changes, so the bound hook must be created once per
  48. * source — cached here by source identity (sources are host-owned singletons).
  49. * @param source - host-provided observable.
  50. * @returns the cached selector hook.
  51. */
  52. export function observableHook<T>(source: HostObservable<T>): SnapshotSelectorHook<T> {
  53. let hook = hookCache.get(source)
  54. if (hook === undefined) {
  55. hook = bindSnapshotSelector(source)
  56. hookCache.set(source, hook)
  57. }
  58. return hook as SnapshotSelectorHook<T>
  59. }
  60. const hookCache = new WeakMap<object, unknown>()
  61. /** SessionProvider surface: render-prop body plus the no-session branch. */
  62. export interface SessionProviderProps {
  63. /** No-session body (also covers a current id whose session cannot be resolved). */
  64. empty?: (() => ReactNode) | undefined
  65. /** Session body; remounted per session via key={sessionId}. */
  66. children: (sessionId: string) => ReactNode
  67. }
  68. /**
  69. * Framework-wired session area: subscribes to the host's current-session
  70. * source (design fiat ① — selection authority lives with runtime sessions),
  71. * resolves the session cell, and remounts the body under key={sessionId} so
  72. * a session switch rebuilds the whole session subtree. Ids speak plain
  73. * string at this dependency-inverted layer; branding lands on the component
  74. * props seam (PropsRuntime).
  75. */
  76. export function SessionProvider({ empty, children }: SessionProviderProps) {
  77. const host = useHost()
  78. const id = observableHook(host.sessions.current)((s) => s)
  79. const cell = id === undefined ? undefined : host.sessions.cell(id)
  80. if (id === undefined || cell === undefined) return <>{empty?.() ?? null}</>
  81. return (
  82. <BindingContext.Provider value={cell} key={id}>
  83. {children(id)}
  84. </BindingContext.Provider>
  85. )
  86. }