Motivation
There's no way today to enumerate the Readables/Signals live in a running app. That's fine for production but blocks any future DevTools work — a browser panel that shows current values, subscriber counts, or a reactivity graph needs a way to reach into the running app and ask "what exists?"
Current state
SignalRegistry exists as a Context.Tag (packages/core/src/Signal.ts:294) with make and scoped methods, and a Live layer — but the top-level Signal.make function doesn't consult it. Users calling Signal.make(initial) bypass the registry entirely, so today the registry is a defined-but-inert extension point.
Proposed sequence
Small opt-in hooks — no runtime cost when no registry is provided.
-
Wire Signal.make through the registry, with a no-op fallback:
export const make = <A>(initial: A) => Effect.gen(function*() {
const opt = yield* Effect.serviceOption(SignalRegistry);
const sig = /* existing make impl */;
if (Option.isSome(opt)) opt.value.track(sig);
return sig;
});
-
Add a Signal.label(name) combinator so DevTools UIs can show "cart.total" instead of an anonymous Signal<object>.
-
Ship a SignalRegistry.Instrumented layer that keeps a WeakSet<Signal<unknown>> (or Set in debug mode) and exposes an accessor via window.__EFFEX_DEVTOOLS__.signals(). Users provide Instrumented instead of Live when they want tracking; production layer stack stays untouched.
Steps 1–3 alone give you Object.keys(window.__EFFEX_DEVTOOLS__.signals()) in the browser console — a rough MVP a real DevTools panel could build on top of.
Considerations before shipping
- Naming. Signals are anonymous closures today. A
Signal.make(initial, { name }) overload or a Signal.label combinator is needed for a legible UI; otherwise it's a wall of Signal<object> entries.
- Creation site. Capturing
new Error().stack at make time gives a "created at" reference for each entry. Costs a few microseconds; fine for dev, opt out for prod.
- Subscribers. SubscriptionRef's PubSub doesn't expose subscribers. Sizes are introspectable, but to know who is subscribed you'd need to wrap
.changes calls. Path to live subscriber graphs / time-travel later.
- Derived Readables.
Readable.map(sig, fn) creates a derived value; tracking the parent-child relationship at map time would let DevTools show a proper reactivity graph. Similar registry accessor pattern.
- GC pressure.
WeakSet won't retain signals past their scope (correct behavior, but DevTools snapshot changes constantly). Set retains them (leak in prod, useful in debug). Registry variant chooses.
Open questions
- What does a v0 DevTools UI actually look like? Chrome extension, a
Portal(...) in-page panel, or just a scriptable inspector? Depends on what value we want first — inspection vs. time-travel vs. reactivity graph.
- Should the registry track derived Readables as first-class citizens or only "source" Signals?
- Do we want per-signal history (last N values), or only current state?
Related
None yet — this is a fresh design chat between me and Claude.
Motivation
There's no way today to enumerate the Readables/Signals live in a running app. That's fine for production but blocks any future DevTools work — a browser panel that shows current values, subscriber counts, or a reactivity graph needs a way to reach into the running app and ask "what exists?"
Current state
SignalRegistryexists as aContext.Tag(packages/core/src/Signal.ts:294) withmakeandscopedmethods, and aLivelayer — but the top-levelSignal.makefunction doesn't consult it. Users callingSignal.make(initial)bypass the registry entirely, so today the registry is a defined-but-inert extension point.Proposed sequence
Small opt-in hooks — no runtime cost when no registry is provided.
Wire
Signal.makethrough the registry, with a no-op fallback:Add a
Signal.label(name)combinator so DevTools UIs can show"cart.total"instead of an anonymousSignal<object>.Ship a
SignalRegistry.Instrumentedlayer that keeps aWeakSet<Signal<unknown>>(orSetin debug mode) and exposes an accessor viawindow.__EFFEX_DEVTOOLS__.signals(). Users provideInstrumentedinstead ofLivewhen they want tracking; production layer stack stays untouched.Steps 1–3 alone give you
Object.keys(window.__EFFEX_DEVTOOLS__.signals())in the browser console — a rough MVP a real DevTools panel could build on top of.Considerations before shipping
Signal.make(initial, { name })overload or aSignal.labelcombinator is needed for a legible UI; otherwise it's a wall ofSignal<object>entries.new Error().stackat make time gives a "created at" reference for each entry. Costs a few microseconds; fine for dev, opt out for prod..changescalls. Path to live subscriber graphs / time-travel later.Readable.map(sig, fn)creates a derived value; tracking the parent-child relationship at map time would let DevTools show a proper reactivity graph. Similar registry accessor pattern.WeakSetwon't retain signals past their scope (correct behavior, but DevTools snapshot changes constantly).Setretains them (leak in prod, useful in debug). Registry variant chooses.Open questions
Portal(...)in-page panel, or just a scriptable inspector? Depends on what value we want first — inspection vs. time-travel vs. reactivity graph.Related
None yet — this is a fresh design chat between me and Claude.