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

@kontsedal/olas-devtools

v1.0.1

Published

Olas in-app devtools — controller tree inspector, cache timeline, mutation log, signal graph.

Readme

@kontsedal/olas-devtools

In-app devtools UI for an Olas root, as two React components. <DevtoolsLauncher> is a floating draggable window with a launcher button. <DevtoolsPanel> is the panel itself, for embedding in your own chrome. Both read the same root.debug event stream.

Install

pnpm add @kontsedal/olas-devtools @kontsedal/olas-core @kontsedal/olas-react @preact/signals-core react

react >= 18, @kontsedal/olas-core and @kontsedal/olas-react are peer deps, and core brings its own peer, @preact/signals-core.

30-second example

import { createRoot, queryEngine } from '@kontsedal/olas-core'
import { DevtoolsLauncher } from '@kontsedal/olas-devtools'
import { OlasProvider } from '@kontsedal/olas-react'
import { App } from './App'
import { appController } from './app.controller'

const root = createRoot(appController, { deps: {}, queries: queryEngine() })

export function AppShell() {
  return (
    <OlasProvider root={root}>
      <App />
      {import.meta.env.DEV && <DevtoolsLauncher root={root} />}
    </OlasProvider>
  )
}

import.meta.env.DEV is Vite's development flag; use your bundler's equivalent. The package declares no side effects, so a production build where the flag is false drops the panel.

The panel reads the events that @kontsedal/olas-core emits only in its development build. Core's development export condition points at that build, and Vite's dev server, webpack and Rspack in development mode, and Next.js in dev resolve it without configuration. With esbuild or Rollup, add conditions: ['development'] to the dev config. Against core's default build, the panel shows the cache but no controller tree or timeline (SPEC §23).

DevtoolsLauncher renders a small launcher button in the bottom right; clicking it opens a draggable, resizable window with the panel. Position + size + open and minimized state persist to localStorage. The launcher records from the moment it mounts, and closing or minimizing the window keeps what it recorded.

A devtools error stays in the devtools. A render error inside the panel shows in place of the panel, with a Retry button, and the host app stays mounted.

If you'd rather host the panel yourself (e.g., fixed sidebar in a layout), import DevtoolsPanel directly and size it however you like. Styles are scoped to the .olas-devtools-* class prefix; no CSS imports needed.

What you'll see

A search box sits above the tabs. Press / anywhere in the panel to focus it. It finds controllers, query ids, key args, mutations, form fields and payload content, groups the results by kind, and Enter jumps to the row and highlights it.

| Tab | Content | |-----|---------| | Timeline | The default. Every event, newest first, grouped by cause: a mutation run, its optimistic write and its rollback read as one collapsible chain. A superseded, reset or disposed run ends its chain with cancel and the reason. A cache write expands to a before-and-after diff. A plugin's host.debug events carry the plugin's name, and a chip per lane shows or hides them. | | Tree | Live controller tree. Each node shows its path segment, lifecycle state and its ctx.debug variables, live. A disposed node stays for a while, greyed, with its variables frozen at dispose time. | | Cache | Chronological log of cache events. | | Inspector | Live state of every cache entry, refreshed on cache events. | | Mutations | Chronological log of mutation:run, success, error, rollback and cancel events. A settle or a cancel shows how long its own run took. | | Fields | Field-level validation outcomes. |

Every long view mounts only the rows in view, so a 10,000-event timeline scrolls like a short one. The Clear button empties the event logs and the timeline. The tree and the inspector show live state rather than a log, so Clear keeps them.

API

function DevtoolsLauncher(props: {
  root: Pick<Root<unknown>, 'debug'>
  defaultTab?: DevtoolsTab
  maxEntries?: number         // per-log cap, oldest drop first; default 100
  maxTimelineEntries?: number // timeline ring-buffer capacity; default 10,000
  urlHashKey?: string         // forwarded to the panel; persists tab + filters in the URL
  storageKey?: string         // localStorage key for window position/size; default 'olas-devtools-window'
  initial?: { x?: number; y?: number; w?: number; h?: number }
}): ReactElement

function DevtoolsPanel(props: {
  root: Pick<Root<unknown>, 'debug'>
  defaultTab?: DevtoolsTab
  maxEntries?: number
  maxTimelineEntries?: number
  urlHashKey?: string         // its own key=value segment of the hash; the rest stays as it was
  store?: DevtoolsStore       // render this store; its owner attaches it, and it outlives the panel
}): ReactElement

type DevtoolsTab = 'timeline' | 'tree' | 'cache' | 'inspector' | 'mutations' | 'fields'

// Lower-level store — exported so consumers can build their own UI.
class DevtoolsStore {
  constructor(options?: DevtoolsStoreOptions) // maxEntries, maxTimelineEntries, maxDisposedNodes, coalesce, now

  readonly tree$: ReadSignal<ControllerNode>
  readonly cache$: ReadSignal<CacheEntry[]>
  readonly mutations$: ReadSignal<MutationEntry[]>
  readonly fields$: ReadSignal<FieldEntry[]>
  readonly events$: ReadSignal<TimelineEvent[]>   // the timeline's ring buffer
  readonly droppedEvents$: ReadSignal<number>     // events the ring overwrote
  readonly cacheState$: Signal<DebugCacheEntry[]> // live cache entries, for the inspector
  readonly subscribers$: ReadSignal<ReadonlyMap<string, number>> // subscriptions per entry

  attach(root: Pick<Root<unknown>, 'debug'>): () => void // subscribes; returns unsubscribe
  handle(event: DebugEvent): void                        // for tests or programmatic feed
  pause(): void                                          // drop new events until resume()
  resume(): void
  clearLogs(): void
  search(query: string, limitPerKind?: number): SearchGroup[]
}

| Export | When to reach for it | |---|---| | <DevtoolsLauncher root> | The one-liner. Floating launcher button + a draggable, resizable panel window; position / size / open state persist to localStorage. | | <DevtoolsPanel root> | The panel alone — embed it in your own chrome (a fixed sidebar, a split pane). | | DevtoolsStore | The lower-level store behind the panel. attach(root) to subscribe, handle(event) to feed events, read tree$, events$ and the log signals, or call search — build your own UI on top. |

Important: what the panel keeps

The panel subscribes to root.debug on mount, and the bus replays the live controller tree to it, so the Tree is complete from the start. Cache, mutation and field events that fired before the mount are not replayed. <DevtoolsLauncher> subscribes when it mounts, not when its window opens, so render it next to your app. A <DevtoolsPanel> of your own records from its own mount.

With urlHashKey, the panel keeps its tab and filters in one key=value segment of the URL hash. It writes only when the rest of the hash is empty or key=value pairs too. It leaves a hash router's path or an anchor untouched, so there the state does not persist.

Memory stays bounded however long the session runs. The timeline keeps the newest maxTimelineEntries events, default 10,000, and its toolbar counts the ones it dropped. The cache, mutation and field logs keep maxEntries each, default 100. The tree drops the earliest-disposed subtrees beyond maxDisposedNodes, default 200, a DevtoolsStore option.

If you need historical state, build a DevtoolsStore early, next to createRoot, and attach it. Pass it to <DevtoolsPanel store={store}> or to a custom UI later. The panel renders a store it is given and leaves attaching it to you.

What's emitted by the runtime

Spec §20.9 lists the full DebugEvent union. In development builds the runtime emits:

  • controller: constructed, suspended, resumed, disposed and debug
  • cache: subscribed, unsubscribed, fetch-start, fetch-success, fetch-error, set-data, invalidated and gc
  • snapshot: push, rollback and finalize, for optimistic writes
  • mutation: run, success, error, rollback and cancel
  • field: validated
  • plugin: event, for each host.debug(payload) a plugin makes

A createQuery subscription sends cache:subscribed when it binds an entry and cache:unsubscribed when it lets go, with its controller's path. The store counts them per entry in subscribers$, and re-seeds the counts from each queryEntries() snapshot, so a panel attached late still counts the subscriptions made before it. The inspector shows the count beside each entry.

Further reading