Skip to content

Repository files navigation

@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
  },
})
Example: deriving labels from a transport layer that already tracks them
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

About

A Rozenite plugin that turns ably-js into an inspectable panel: channels, events, payloads.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages