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

@avasapp/rozenite-plugin-ably

v1.2.0

Published

Ably Realtime inspector for React Native DevTools — inspect channels, stream live events, and preview decoded payloads.

Readme

@avasapp/rozenite-plugin-ably

npm

An Ably Realtime inspector for React Native DevTools, built on Rozenite.

The Network Activity panel already shows you websocket frames. This shows you Ably: which channels are attached right now, who is subscribed to them, what messages actually arrived, and what was inside them.

The Ably panel in React Native DevTools: attached channels with labels and counters on the left, a filterable event stream in the middle, and the decoded payload of the selected message on the right.

Install

npm install --save-dev @avasapp/rozenite-plugin-ably

Requires Rozenite 2.2 or later. Rozenite discovers the plugin automatically — no metro.config change is needed beyond having Rozenite itself set up.

The plugin ships no runtime dependencies of its own. @rozenite/plugin-bridge, @rozenite/agent-bridge and @rozenite/agent-shared are peer dependencies, so the plugin shares your app's copy of the bridge rather than resolving a second one alongside it — two bridge instances mean the panel never connects. Any Rozenite 2.x works; only a Rozenite major needs a matching release here.

Usage

Call the hook once, anywhere in your component tree, with your Ably.Realtime client:

import { useAblyDevTools } from '@avasapp/rozenite-plugin-ably'

function DevTools() {
  useAblyDevTools(ablyRealtimeClient)
  return null
}

It is a no-op outside __DEV__, and a no-op while the client is null, so it is safe to call before the client exists:

useAblyDevTools(isLoggedIn ? client : null)

Channel labels

Ably channel names are often opaque (device_7b41-…). If your app knows which feature subscribed to what, feed that in and the panel will show it on each channel:

useAblyDevTools(client, {
  labels: {
    getLabels: () => ({ 'device_7b41': ['telemetry-screen', 'chat'] }),
    subscribe: (onChange) => registry.onChange(onChange), // optional
  },
})
useAblyDevTools(getAblyJsClient(), {
  labels: {
    getLabels: () =>
      Object.fromEntries(
        getTransport()
          .getChannels()
          .map((c) => [
            c.name,
            c.listenerDetails
              .map((l) => l.label)
              .filter((l): l is string => Boolean(l)),
          ]),
      ),
    subscribe: (onChange) => getTransport().onChannelsChange(onChange),
  },
})

Options

| Option | Default | Description | | ----------------- | ------- | ----------------------------------------------------------------------------- | | labels | — | Maps channel names to human-readable labels. | | maxEvents | 1000 | Ring-buffer size. Older events are dropped once exceeded. | | captureProtocol | false | Capture raw ably-js protocol frames. Verbose; also toggleable from the panel. | | enabled | true | Escape hatch. The plugin is already inert outside __DEV__. |

What you get

Channels — every channel the client has touched, with its live attach state, subscriber count, per-channel in/out counters, error reason, and time in state. Detached and released channels are retained behind a toggle, because "the channel I expected isn't there" is the bug you most often need to see.

Events — a filterable stream of messages, presence, channel/connection state transitions, and errors. Filter by kind, by channel, or by free text that searches payload contents too.

Payloads — Ably delivers most payloads as a JSON string. The plugin parses it and keeps the original, so you get a real collapsible tree by default and the exact bytes on demand. Searching expands the tree far enough to reveal matches rather than filtering them out of their surrounding structure.

Agent tools

The same session is exposed to Rozenite for Agents as the avasapp/ably domain, so a coding agent can inspect realtime traffic from the terminal with no DevTools window open:

npx rozenite agent session create
npx rozenite agent avasapp/ably call --tool list-channels \
  --args '{"onlyErrored":true}' --session <id>

| Tool | | | --- | --- | | get-connection | State, failure reason, retryIn, capabilities. | | list-channels | Paginated. Filters: state, search, onlyErrored, includeReleased. | | read-channel | One channel in full, including its listeners. | | list-events | Paginated. Filters: channel, kind, dir, search, since, order. | | read-event | One event with its decoded payload, clipped to maxBytes (8 KB default). | | get-stats | Counters, options, retained vs dropped. | | set-options | paused, captureProtocol, maxEvents. | | clear | Discard captured events. | | channel-action | attach / detach / release. | | emit-event | Deliver a synthetic message to the app's own subscribers, locally. |

list-events deliberately omits payload bodies — a single message can carry hundreds of kilobytes, so a page of them would be unreadable. Find the id in a listing, then read-event for the one that matters, which itself clips to 8 KB unless you raise maxBytes. search covers decoded payload contents, which is how you answer "which message carried this device id".

Only set-options, clear, channel-action and emit-event change anything, and only channel-action touches Ably itself rather than what is recorded.

Firing an event without a backend

emit-event makes a realtime event arrive in the running app with no server, no publish, and no network round trip:

npx rozenite agent avasapp/ably call --tool emit-event --session <id> \
  --args '{"channel":"bid-orders","name":"ride_assignment","data":{"rideId":"r_42"}}'

It hands a fabricated Ably.Message straight to the listeners the app itself registered with subscribe(). It never publishes. A real publish would fan the message out to every other client attached to that channel — another developer's app, or a real device — and would need the network, so it could neither run offline nor complete deterministically. Local delivery is the only version that is both safe on a shared channel and usable in a test.

That also bounds what it can do: the app must already have subscribed, or there is no listener to deliver to. delivered: 0 says exactly that, with a note explaining whether the channel had no listener at all or every listener filtered it out. Event-name and MessageFilter subscriptions are honoured as Ably would honour them. The call returns once async listeners settle (up to 2s), so a rejected promise is reported in failed/errors like a synchronous throw.

Injected events are recorded in the stream like any other, marked injected: true and with their summary prefixed, so a listing never passes a synthetic event off as one Ably delivered. They are not counted in the inbound traffic counters, which describe only what Ably delivered.

Messages only — presence cannot be injected.

The name and the three arguments are a contract

emit-event is this plugin's implementation of a general capability — deliver an inbound message the app never asked for — and Ably is only one transport that could provide it. Push notifications and SSE deep links could satisfy the same shape later.

Rozenite's AgentToolTraits has no capability field, so a wrapper that wants to treat plugins as interchangeable providers has to discover them by convention: enumerate the domains, then probe each for a tool named exactly emit-event. That makes the short name a discovery key rather than a label, so it is not decorated and will not be renamed. channel, name and data are the generic arguments; everything else the tool accepts is optional Ably message metadata that another transport would simply not have.

Nothing application-specific belongs in the signature. An app's own event envelope is carried inside data, whose shape the tool never inspects.

For Node scripts built on @rozenite/agent-sdk, typed descriptors keep tool names and argument shapes checked:

import { ablyTools } from '@avasapp/rozenite-plugin-ably/sdk'

const { items } = await session.callTool(ablyTools.listChannels, {
  onlyErrored: true,
})

Agent skill

Rozenite serves its own agent docs from the CLI (npx rozenite skills list) and does not yet import skills from installed plugins, so this package ships one instead — install it with npx skills add avas-app/rozenite-plugin-ably, or point your agent at skills/ably-devtools/SKILL.md. It covers the tool surface plus the semantics the raw output does not convey, such as why a channel the app never subscribed to shows no inbound messages, and what the payload truncation markers mean.

Protocol capture

Enabling protocol capture calls client.setLog({ level: 4 }). That method is present at runtime but is not in ably-js's public typings, so it is feature-detected — if it is missing, the panel disables the toggle and says so. It is off by default because level 4 is genuinely noisy and slows the SDK.

Example app

example/ is a runnable Expo app that exercises the plugin with no Ably account, no API key, and no login. The Ably client is a fake that produces realistic traffic; everything else is real — it goes through the actual useAblyDevTools hook and the actual Rozenite bridge, so the panel shows exactly what a production app produces.

bun install && bun run build   # in the repo root — the panel is served from dist/
cd example
bun install                    # symlinks the plugin (see scripts/link-plugin.mjs)
bun start                      # then press `j`, and pick the "Ably" tab

The app has buttons for the cases worth eyeballing: nested session events, a 25-message burst, an outgoing publish, presence churn, a non-JSON payload, an oversized payload that hits truncation, a channel failing with error 40160, a full disconnect/recover cycle, and a channel release.

Two wiring details worth knowing if you adapt this setup:

  • Rozenite discovers plugins from the project's declared package.json dependencies — it does not crawl node_modules. Since the plugin lives one directory up, metro.config.js names it via include: ['@avasapp/rozenite-plugin-ably'].
  • Metro resolves the bare specifier to the plugin's TypeScript source, so SDK edits hot reload. Panel edits still need bun run build in the root, because the panel is served from dist/.

Development

bun install
bun test        # instrumentation test suite
bun typecheck
bun run build

bun dev starts Rozenite's browser dev host on localhost:8888 for quick panel iteration. Note that rozenite.config.ts cannot import anything — Rozenite evaluates it with new Function('module','exports', code) and no require in scope — so the dev presets there are literal payloads. Realistic traffic lives in example/ instead, where it can import the real SDK.

To iterate against a real app:

bun link                                   # in this repo
cd ../your-app && bun link @avasapp/rozenite-plugin-ably
ROZENITE_DEV_MODE=@avasapp/rozenite-plugin-ably bun dev

License

MIT