Skip to content

Devtools

@sh1n4ps/plasma-devtools gives you three ways to inspect a running plasma client:

  1. PlasmaDevtools — a React panel component you drop into your app during development.
  2. useDevtoolsSnapshot — a hook that returns the current sync state, if you want to build your own panel.
  3. attachDevtoolsBridge — a window.postMessage bridge for a Chrome DevTools extension (or any external inspector).
Terminal window
pnpm add @sh1n4ps/plasma-devtools

Nothing else in your app needs to change.

import { PlasmaDevtools } from "@sh1n4ps/plasma-devtools"
import { plasma } from "./main"
function App() {
return (
<>
<YourAppUI />
{import.meta.env.DEV && (
<PlasmaDevtools client={plasma} dbName="todos" schema={schema} />
)}
</>
)
}

The panel:

  • Docks to the bottom-right corner as a small badge (plasma · N⏳).
  • Expands to show:
    • Current clientID / clientGroupID
    • Pull cookie (base64 payload)
    • schemaVersion
    • Outbox depth + individual entry list
    • Recent onError events

Gate it behind import.meta.env.DEV so it doesn’t ship to production.

For building your own panel or a status widget:

import { useDevtoolsSnapshot } from "@sh1n4ps/plasma-devtools"
function SyncStatus() {
const snap = useDevtoolsSnapshot(client, {
dbName: "todos",
schema,
refreshIntervalMs: 500,
})
return (
<div>
Outbox: {snap.outbox.length}<br />
Cookie: {snap.cookie ?? "none"}<br />
Client: {snap.clientID ?? "..."}<br />
</div>
)
}

Returns a DevtoolsSnapshot:

interface DevtoolsSnapshot {
readonly clientID: string | null
readonly cookie: string | null
readonly outbox: readonly MutationEnvelope[]
readonly schemaVersion: string | null
}

The hook polls IDB on refreshIntervalMs (default 500ms) because IndexedDB doesn’t surface a change stream cheaply. Adjust the interval if you want faster feedback (25ms is fine for a hidden badge; 500ms is fine for a visible panel).

attachDevtoolsBridge — the postMessage bridge

Section titled “attachDevtoolsBridge — the postMessage bridge”

For a Chrome DevTools extension (or any external inspector) that wants to observe and control the client from outside the page:

import { attachDevtoolsBridge } from "@sh1n4ps/plasma-devtools"
const dispose = attachDevtoolsBridge(client, {
dbName: "todos",
schema,
refreshIntervalMs: 500,
// Post messages only to this origin (default: "*"). Narrow to the
// extension's origin for production sensitivity.
targetOrigin: "*",
// Which origins may SEND command messages to us (default:
// "same-origin"). Set to an array of extension origins, or "*" if
// you truly accept commands from anywhere.
allowedOrigins: "same-origin",
})
// later
dispose()

The bridge sends four message kinds via window.postMessage:

Direction Kind Purpose
page → extension hello “Runtime present” announcement on attach
page → extension state-update Snapshot of sync state (every refreshIntervalMs)
extension → page command Request an action: flush, pull, reset-local-state, ping
page → extension command-result OK/error response for a prior command, keyed by requestId

The source field on every message is "plasma-devtools" so receivers can filter unrelated messages:

window.addEventListener("message", (event) => {
if (!isDevtoolsMessage(event.data)) return
// ... handle the plasma-devtools message
})

The extension side isn’t shipped in @sh1n4ps/plasma-devtools (it’s a separate Chrome extension project). To build one:

  1. Content script injects a listener for plasma-devtools messages.
  2. Sends commands back via postMessage.
  3. Renders a DevTools panel using the state-update payloads.

The protocol types are all exported from @sh1n4ps/plasma-devtools:

import type {
DevtoolsStateSnapshot,
StateUpdateMessage,
CommandMessage,
CommandResultMessage,
HelloMessage,
DevtoolsMessage,
} from "@sh1n4ps/plasma-devtools"

Use the union type in your listener and let TypeScript exhaustively check every branch.