Rindle

API index and search · Build metadata

Source snapshot

packages/devtools/src/global.ts

Source revision 05d0bf2c2e56 · build details
Source revision: 05d0bf2c2e56.
TypeScript input SHA-256: aabe6cfcc4172b870d5e272142958e9ea8d8784c2aa23133156e5a7ee633318e
Generated 2026-09-04T23:58:25.590Z with TypeScript 6.0.3. Public TypeScript checks and declaration emit passed. Package runtime tests are separate.
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