npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@zakkster/lite-studio

v1.1.0

Published

In-page visual debugger for @zakkster/lite-signal reactive graphs. A pure consumer of @zakkster/lite-devtools: live layered DAG view (SVG), node inspector, engine stats, lifecycle log, and DOT export. Non-self-instrumenting -- it never creates a signal, s

Downloads

301

Readme

@zakkster/lite-studio

npm version Zero-GC sponsor npm bundle size npm downloads npm total downloads lite-signal peer types Dependencies license

An in-page visual debugger for @zakkster/lite-signal reactive graphs. Mount it with a root handle and get a live, layered DAG view, a node inspector with dependency / observer chips, the engine's monitor() readout, a lifecycle log, a 60-sample leak sparkline, and one-click DOT export — rendered as a draggable SVG overlay on top of your running app, with zero nodes added to the graph it inspects.

import { signal, computed, effect } from "@zakkster/lite-signal";
import { mount } from "@zakkster/lite-studio";

const price = signal(100);
const qty   = signal(2);
const total = computed(() => price() * qty());
effect(() => console.log("total:", total()));

const studio = mount([price, qty]);   // panel appears; graph is live
// ... later
studio.unmount();

It is a consumer, and it is a ghost

lite-studio computes nothing about the graph itself. It is a pure consumer of @zakkster/lite-devtools — devtools walks and inspects the graph (the mechanism), studio renders and lets you interact with it (the UI). The four-layer stack:

flowchart LR
  SIG["@zakkster/lite-signal<br/>(reactive engine + introspection)"] --> DT["@zakkster/lite-devtools<br/>(graph / inspect / monitor / track / diff / leakWatch / toDot)"]
  DT --> ST["@zakkster/lite-studio<br/>(SVG panel: graph, inspector, stats, log, sparkline)"]
  TIME["@zakkster/lite-time<br/>(every -> drift-corrected poll cadence)"] --> ST

And it is non-self-instrumenting. lite-studio holds all of its own state in plain JavaScript variables (Map, Set, primitive counters) and updates the DOM imperatively. It never calls signal(), computed(), or effect(), so it adds zero nodes and zero observers to the graph it is inspecting. A debugger that instrumented itself would inflate activeNodes, corrupt monitor() counts, and trip leakWatch false positives — none of those happen here. Every read goes through devtools' non-perturbing, peek-based descriptor APIs.

The contract is pinned by a test: 02-extras.test.mjs records monitor() before mount, after mount, after a structural change, and after unmount; the deltas attributable to studio are exactly zero. It will fail the suite if a future refactor accidentally creates a single signal.

Install

studio has no runtime dependencies of its own. Three peers:

npm install @zakkster/lite-studio @zakkster/lite-devtools @zakkster/lite-time @zakkster/lite-signal

| Peer | Required version | Why | |---|---|---| | @zakkster/lite-signal | >= 1.2.0 | Engine, introspection, owner tree | | @zakkster/lite-devtools | ^1.1.0 | graph(), monitor(), track(), toDot(), diff(), watchGraph, leakWatch, capabilities | | @zakkster/lite-time | ^1.0.0 | Drift-corrected every() cadence (the same authority devtools uses, which stays out of the graph it measures) |

ESM only. Browser-only at runtime (it renders into the DOM). The data layer is fully tested headless via happy-dom.

What you see

Each panel has a job. None of them allocate per-tick in steady state.

Graph view

A layered DAG (sources left, observers right) rendered as SVG. Signals are ellipses, computeds boxes, effects diamonds, colour-coded by kind. Each node carries its live value inline (fmtValue ellipsises objects/arrays at 18 chars). Each node group also exposes data-id="<engine id>" and data-kind="signal|computed|effect" attributes, so downstream tooling — screenshot diffs, custom CSS, integration tests — can target a specific node by its stable engine id:

.ls-node[data-id="7"][data-kind="computed"] rect { stroke: orange; }

The layout is stable. It is recomputed only when the discovered node/edge set changes (a structureSignature(g) is hashed every tick and compared); a value flipping from true to false updates the label in place and flashes the node, so the graph never jumps under you. Newly-added nodes flash too: each tick diffs the previous graph() snapshot via devtools' diff() (1.1), so a fresh computed or effect appearing pulses just like a value change. Large graphs scroll (overflow: auto).

Inspector

Click any node: kind, stable id, current value (rendered as peek-based, never tracked), observed flag (does anything subscribe?), then its dependencies and observers as clickable chips. Clicking a chip selects that node and re-renders the inspector — you can walk a deep dependency chain by chip-clicking with no mouse-precise navigation.

Engine stats

The live monitor() readout: signals, computeds, effects, active links, active nodes, pooled links. As of 1.1 the six rows are pre-built at mount time and the cadence tick updates textContent on cached value-span refs — not innerHTML. Per-refresh allocation on a 21-node graph: ~300-2000 B/tick (was ~3 KB/tick before the rebuild-churn fix), pinned at a 4 KB ceiling by 02-extras.test.mjs.

Lifecycle log

A FIFO log (capped at 60 rows) of nodes appearing and disappearing as your app mounts and unmounts owner scopes, plus connect/disconnect transitions on the roots via track(). Disposal events from the lite-signal 1.2 owner-tree engine flow through the same diff() that drives the churn flash, so cascade teardowns are visible in real time.

Leak sparkline

A 60-sample SVG strip backed by devtools' leakWatch. Sampled once per second; the line tints red on a suspected-growth sample (the device drift-corrected at the same cadence as the structural poll). Plain SVG, imperative updates — the panel stays out of the graph it measures.

Copy DOT

One-click export of the current graph as Graphviz DOT (toDot) to your clipboard. Paste into Graphviz Online or any DOT renderer for high-fidelity static visualisation.

How updates work

studio uses the right update mechanism for the engine it finds, with a single code path that gracefully falls back.

On lite-signal >= 1.2.1 — event-driven push mode

When the engine exposes onGraphMutation (added in 1.2.1), studio drives structural updates via devtools' watchGraph — microtask-coalesced, fires exactly when the graph actually changed. No 300ms structural re-poll. The cadence is preserved at ≥1000ms only for the monitor() stats refresh, where values like activeLinks and pooledLinks need a periodic readout but don't need microtask-precise timing.

// (Inside Studio.js — illustrative)
if (capabilities.events) {
    stopWatch = watchGraph(rootList, { onChange: () => tick({ structural: true }) });
}

The structural re-render lands on the same microtask that committed the change. Latency between a signal.set(x) that adds a downstream computed and that computed appearing in the panel is sub-millisecond.

On lite-signal 1.2.0 — poll mode

When the engine doesn't expose onGraphMutation, studio polls graph() + monitor() on a lite-time cadence (~300ms by default, configurable via options.cadenceMs). Each tick compares the discovered node/edge set to the previous one via structureSignature(g); only a structural change triggers a relayout and redraw. Otherwise values are refreshed in place via refreshValues(g) with a flash. No synchronous hooks are attached to your application's links.

The churn-flash heuristic

Each tick (push or poll) also runs devtools' diff() against the previous graph snapshot. The diff returns {added, removed, changed} keyed by stable engine id. Added nodes are flashed on first appearance the same way value changes are flashed; removed nodes are logged. This makes structural churn visually equivalent to value churn — you don't have to watch the graph carefully to notice a computed appearing.

The ghost contract, formally

The "ghost" framing is not aspiration; it is an invariant the test suite asserts. Specifically, between any two monitor() snapshots S₀ (before mount) and S₁ (after mount + any number of structural changes + unmount), the following must hold for every counter — signals, computeds, effects, activeNodes, activeLinks, pooledLinks:

monitor()[k] at S₁ − monitor()[k] at S₀ = Δ introduced by the application itself, not by studio.

If this fails — if studio ever creates a single internal signal for any feature — the suite stops passing. Concretely, studio replaces every place it could have used a signal:

| Where another debugger might use a signal | What studio uses instead | |---|---| | selected-node-id state | let selectedId = null | | previous-graph cache for diff | let prevGraph = null (plain JS object) | | flash-timer registry | Map<id, number> (raw setTimeout handle) | | descriptor cache for the inspector | Map<id, descriptor> | | layout caches (position, group element) | Map<id, {x,y}>, Map<id, SVGGElement> | | disposers for track() subscriptions | Array<() => void> | | DOM updates | imperative textContent / setAttribute |

The cost of this discipline: no reactivity inside studio itself. Every update is wired manually. The gain: studio runs invisibly inside the graph it inspects.

Performance budget

Pinned by test/02-extras.test.mjs on a 21-node fixture graph:

| Metric | Steady-state | Ceiling (test fails if exceeded) | |---|---|---| | Heap delta per non-structural tick | ~300-2000 B | 4 KB | | Heap delta per structural relayout | proportional to (new nodes + new edges) | n/a | | flashTimers Map size at rest | 0 | bounded (regression test) | | logRows retained DOM references | 0 | 0 (fix in 1.1; regression-greppable from source) | | monitor() deltas attributable to studio | 0 | 0 (ghost contract) |

The 1.1 release fixed three steady-state allocation leaks that were eroding the budget:

  • logRows leak. A plain Array was being pushed to on every structural-log entry but never read. Meanwhile the DOM evicted old log rows via removeChild (capped at 60), but logRows kept the references alive, so detached DOM nodes accumulated indefinitely. Removed entirely; regression test greps the source.
  • flashTimers leak. Timeout handle entries were never deleted from the Map when the setTimeout callback fired. Over a long session with many flashed nodes the Map kept dead handles forever. Fixed by flashTimers.delete(id) as the callback's first line; regression test bounds the steady-state Map size.
  • renderStats rebuild churn. The cadence tick was clearing innerHTML on the stats panel and rebuilding 12 DOM nodes per tick (~3 KB/tick). Pre-built the 6 key/value rows once at mount time; the tick now updates textContent on cached value-span refs. Steady-state heap-delta per refresh dropped to the ~300-2000 B range cited above.

These three fixes are the kind of work a long-running development overlay actually needs. Studio is meant to be mounted at app boot and left running for the entire dev session.

API

const studio = mount(roots, options?);

roots is a single lite-signal handle or an array of handles. Studio walks the graph from these roots using devtools, so anything reachable from a root is rendered.

options:

| Field | Type | Default | Meaning | |---|---|---|---| | cadenceMs | number | 300 | Structural poll interval (poll mode only; ignored when onGraphMutation is available). Also caps the monitor() refresh cadence in event-driven mode. | | title | string | "lite-studio" | Panel header text. Followed by // signal graph in dim. |

Returns a controller:

| Method | Effect | |---|---| | studio.refresh() | Force a re-read of graph() + monitor() immediately. Relayout only if the structural signature changed; otherwise values are refreshed in place. | | studio.select(id) | Programmatically select a node by its stable engine id. Pass null to clear. If the id no longer exists in the current graph, selection is silently cleared. | | studio.unmount() | Remove the panel from the DOM, stop the cadence (stopCadence()), stop the watcher (stopWatch()), stop the leak sampler (leakStop()), dispose all track() listeners, clear all flashTimers. Idempotent — calling it twice is a no-op. |

Testing

The full test suite runs with npm test. Three themed files:

| File | Tests | Coverage | |---|---:|---| | 01-core.test.mjs | 2 | DAG render + diff-driven churn flash, non-self-instrumenting ghost contract | | 02-extras.test.mjs | 10 | logRows / flashTimers / renderStats regressions, ghost contract (monitor() stats unchanged), per-tick heap-delta budget, drag idempotency, empty-roots, unmount idempotency | | 03-event-driven-and-interactive.test.mjs | 12 | event-driven push mode (engines >= 1.2.1), leak sparkline DOM, select(id) + node-click flow, Copy DOT without clipboard, lifecycle log feed, post-unmount safety on refresh / select, non-DOM mount error |

npm test           # 23/24 pass (1 skip: --expose-gc heap-delta test)
npm run test:gc    # 24/24 pass with --expose-gc engaged

Tests run headless under happy-dom. The full suite runs in well under 2 seconds on a 2016 MacBook Pro. Engine compatibility is verified end-to-end against @zakkster/lite-signal 1.2.1 (mount → render → structural change → churn flash → unmount → counter parity).

When to use studio

  • During development, mounted at app boot, left running for the dev session. The ghost contract makes it safe to keep open even during perf measurement — it adds nothing to the graph it inspects.
  • For graph-shape debugging. When you suspect a computed isn't being subscribed correctly, or an effect is firing too often, the inspector + dependency/observer chips trace it in seconds.
  • For lifecycle audits. The log + churn flash surface owner-cascade disposals on the 1.2 owner-tree engine — useful when you want to verify that creating a transient scope actually tears down cleanly.
  • For documentation screenshots. The stable data-id / data-kind attributes let you target specific nodes from external screenshot-diff tooling.

When not to use studio

  • In shipped production. The overlay is fixed-position with z-index: 2147483000 — it will sit on top of your UI by design. Gate the mount() call behind a dev/debug flag.
  • In non-browser runtimes at runtime. mount() throws if document is undefined. (Tests run headless via happy-dom; that's fine, but production servers shouldn't import the module.)
  • On lite-signal engines < 1.2.0. The owner tree, onGraphMutation, and the introspection contracts studio relies on landed in 1.2. Use lite-devtools directly with earlier engines.

Architecture notes

A few decisions are worth recording, since they constrain future PRs:

  • Single file. Studio.js is ~640 lines, ESM-only, no build step. Styles are injected once via a <style id="lite-studio-style"> element scoped under .lite-studio (every selector). Mount, unmount, mount again — idempotent.
  • Imperative DOM, never declarative. Studio does not depend on any view library and never will. Every node update is a textContent / setAttribute / classList.toggle. This is deliberate: a view library would introduce its own reactivity, which would defeat the ghost contract.
  • Layout is greedy, not optimal. A simple BFS by depth from the roots, with stable id-ordered row assignment within each depth column. Re-layout is O(nodes + edges), runs only on structural change. v1 deliberately ships no force-directed layout; the graph scrolls instead.
  • No animation framework. Flash is a CSS transition on filter: drop-shadow(...) cleared by a setTimeout. Drag uses pointermove directly. Total animation cost is measured in microseconds per frame.

Roadmap / explicitly out of v1

Kept out to keep v1 simple and bulletproof:

  • Pan / zoom. The graph scrolls inside its container. A future minor could add pan-only (no zoom), driven by middle-mouse drag, without breaking the layout invariant.
  • Force-directed layout. Out of scope. Layered DAG is the right default for reactive graphs because sources-flow-to-observers is a meaningful ordering. Force-directed would obscure that.
  • Chrome DevTools extension panel. A possible future track. The renderer is fully decoupled from the data layer (it just consumes devtools APIs) — a future extension could feed the same renderer with bridged snapshots from a connected tab, without changing a line of Studio.js.

Project conventions

  • ASCII-only source (except × U+00D7 and µ U+00B5 where they appear in formulas). Mojibake-proof across editors, terminals, and tooling.
  • Zero runtime deps. Peers only.
  • sideEffects: false. Mount injects styles only when called.
  • .d.ts ships. Studio.d.ts is the public contract.
  • llms.txt ships. For LLM-assisted tooling.
  • MIT license.

License

MIT © Zahary Shinikchiev