API index and search · Build metadata
Source snapshot
packages/devtools/src/global.ts
1// The opt-in global registry (DEBUG-TOOLS-BROWSER-DESIGN.md §6.2). The client core stays clean —2// no global mutable singleton lives in `@rindle/client`/`@rindle/optimistic`. Instead a dev build3// calls `attachDevtools(app)`, which constructs a {@link DevtoolsCore} and registers it on4// `globalThis.__RINDLE_DEVTOOLS__`, the same discovery pattern TanStack/Redux DevTools use. A panel5// finds the hub with {@link getDevtoolsHub} and subscribes — so it works whether it mounts before or6// after the app attaches.78import { DevtoolsCore } from "./core.ts";9import type { DevtoolsCoreOptions, DevtoolsTarget } from "./types.ts";1011const GLOBAL_KEY = "__RINDLE_DEVTOOLS__";1213/** The discovery surface a panel binds to: the set of attached cores + a change subscription. */14export interface DevtoolsHub {15 readonly version: number;16 readonly cores: readonly DevtoolsCore[];17 /** Fires whenever a core attaches or detaches (so a panel can pick one up). */18 subscribe(listener: () => void): () => void;19 /** Register a core; returns its deregistration function. */20 register(core: DevtoolsCore): () => void;21}2223class Hub implements DevtoolsHub {24 readonly version = 1;25 readonly cores: DevtoolsCore[] = [];26 private readonly listeners = new Set<() => void>();2728 subscribe(listener: () => void): () => void {29 this.listeners.add(listener);30 return () => {31 this.listeners.delete(listener);32 };33 }3435 register(core: DevtoolsCore): () => void {36 this.cores.push(core);37 this.emit();38 return () => {39 const i = this.cores.indexOf(core);40 if (i >= 0) {41 this.cores.splice(i, 1);42 this.emit();43 }44 };45 }4647 private emit(): void {48 for (const l of this.listeners) l();49 }50}5152function globalSlot(): Record<typeof GLOBAL_KEY, Hub | undefined> {53 return globalThis as unknown as Record<typeof GLOBAL_KEY, Hub | undefined>;54}5556/** Get (creating on first use) the global devtools hub. A panel calls this to discover attached57 * clients; it is safe to call before any `attachDevtools`. */58export function getDevtoolsHub(): DevtoolsHub {59 const slot = globalSlot();60 return (slot[GLOBAL_KEY] ??= new Hub());61}6263/** The most recently attached core, or `undefined` — the common single-app convenience a panel uses. */64export function getDevtoolsCore(): DevtoolsCore | undefined {65 const cores = getDevtoolsHub().cores;66 return cores[cores.length - 1];67}6869/** Attach a devtools pane to a running client (DEBUG-TOOLS-BROWSER-DESIGN §6.2). Call this ONLY in a70 * dev build (e.g. behind `import.meta.env.DEV` with a dynamic `import("@rindle/devtools")`) so the71 * pane, its core, and this registration tree-shake out of production. Returns the {@link DevtoolsCore}72 * (also discoverable via {@link getDevtoolsHub}); call `core.detach()` to unwind. */73export function attachDevtools(target: DevtoolsTarget, opts?: DevtoolsCoreOptions): DevtoolsCore {74 // Default the safety-net poll on for real apps (a fold's debounced flush / a pending flip on an75 // already-`complete` query move state with no event); tests construct DevtoolsCore directly.76 const core = new DevtoolsCore(target, { pollMs: 500, ...opts });77 core.onDetach = getDevtoolsHub().register(core);78 return core;79}80