/** * THE HOST DOOR — what a PACKAGE's contribution may read of the running * editor (ARCHITECTURE-CORE §The workbench, "direction": a package imports * only other packages' exports, never the host's internals). * * The editor registers this at boot as MODULE STATE, the same door shape as * `registerProjectModuleLoader` in `contributions.ts`: the SDK is one * identity in the editor's program (Vite dedupes it), so a contribution * served from a package sees the editor's registration. The surface is * deliberately small and grows one member per contribution that needs it; * a member nobody reads is cut. * * Reading it outside a host — in a test, or a package's own tooling — throws * by name rather than answering with an empty session: an inspector that * silently reads "no adapter" is a measurement nobody made. */ import type { AudioAdapter, NetworkingAdapter } from '@vgai/engine/adapter'; import { useSyncExternalStore } from 'react'; /** The live session's system adapters, as the editor inspects them: the * instance under inspection when several run, the solo one otherwise. */ export interface EditorHostSystems { inspectedNetworking(): NetworkingAdapter | null; /** Fires when the inspected networking adapter appears, changes or leaves. */ subscribeNetworking(listener: () => void): () => void; inspectedAudio(): AudioAdapter | null; /** Fires when the inspected audio adapter appears, changes or leaves. */ subscribeAudio(listener: () => void): () => void; /** Monotonic counter behind {@link subscribeAudio}, for `useSyncExternalStore`. */ audioVersion(): number; } /** The editor's ONE shared availability heartbeat (250ms, refcounted): the * signal for out-of-band changes a live session makes without notifying a * store — an adapter's connection state, a reader appearing. */ export interface EditorHostAvailability { subscribe(listener: () => void): () => void; version(): number; } export interface EditorHostWorkspace { /** Reveal a bottom-drawer utility by its registered id (`tool:` for a * contributed one). */ showUtility(id: string): void; } export interface EditorHost { readonly systems: EditorHostSystems; readonly availability: EditorHostAvailability; readonly workspace: EditorHostWorkspace; } /** * ONE registration across every copy of this module. Under the packaged * runtime the editor shell is a prebuilt bundle with this SDK inlined, while * a package's contribution is served from the project's own installed SDK — * two module instances, so plain module state would be an empty registry on * the contribution's side (measured 2026-09-17: "No editor host is * registered" from `@vgai/game`'s connection pill on a registry install). * The layout host (`layouts.tsx`) solved the same split with a `Symbol.for` * key on `globalThis`; this door does the same. */ const HOST_KEY = Symbol.for('vgai.editor.host'); const hosts = globalThis as typeof globalThis & { [HOST_KEY]?: EditorHost | null }; /** The editor's boot registers itself; `null` unregisters (tests). */ export function registerEditorHost(next: EditorHost | null): void { hosts[HOST_KEY] = next; } export function editorHost(): EditorHost { const host = hosts[HOST_KEY]; if (!host) throw new Error( 'No editor host is registered: this contribution is running outside the editor ' + '(`registerEditorHost` from `@vgai/editor-sdk/host` is called by the editor at boot).', ); return host; } /** * Re-render only when `select()`'s value changes, sampled on the host's * availability tick — the shape every session-state gate in the editor uses, * so a package's status item costs the same as a built-in one. */ export function useHostAvailabilitySelector(select: () => T): T { const { availability } = editorHost(); return useSyncExternalStore(availability.subscribe, select, select); }