The transport-agnostic core the TypeWire inspector is built on: one timeline for REST, GraphQL, gRPC and WebSocket, a runtime override registry, and a live mirror of a query cache. Headless and dependency-free — it stores generic events and types every client structurally, so it imports neither typefetch nor typesocket nor the query engine.
Pair it with @tahanabavi/type-devtools for the React panel, or
render its snapshots yourself.
pnpm add @tahanabavi/type-devtools-coreThe bridge
InspectorBridge is the store: one ring buffer of source-tagged events plus an
override registry keyed by (source, label). Each transport plugs in as a
connect* call — that's what lets one timeline hold both without a branch per
transport:
import {
InspectorBridge,
connectTypeFetch,
connectTypeSocket,
selectEntries,
} from "@tahanabavi/type-devtools-core";
const bridge = new InspectorBridge(); // { limit } — default 500 events
connectTypeFetch(apiClient, bridge); // typefetch: REST, GraphQL, gRPC
connectTypeSocket(socketClient, bridge); // typesocket: WebSocket
// A start+success (or outbound+ack) pair collapses into one entry per call.
// Pass the progress store too, to join live transfers onto the rows.
const entries = selectEntries(bridge.getSnapshot(), bridge.getProgressSnapshot());bridge.subscribe / getSnapshot implement the Observable contract, so a
panel binds it with useSyncExternalStore directly.
source is the client; transport is the wire
One typefetch client now speaks REST, GraphQL and gRPC, so those two stopped being the same thing:
| means | on an entry | |
|---|---|---|
source | which client produced it — http (typefetch) or ws (typesocket) | always present |
transport | which wire carried it — http, graphql, grpc, or a third-party adapter's name | present when the client reports one |
The distinction is load-bearing rather than pedantic. source is the key space
for overrides and correlation ids, so it must stay stable per client — an
override registered under "graphql" would never match, because the connector
that resolves it is typefetch's. Show transport in a UI; key on source.
A client older than the transport registry reports no wire, and every call it
could make was HTTP — so connectTypeFetch fills in http rather than leaving
a blank that would read as "unknown".
Normalized failures
entry.errorKind carries typefetch's ErrorKind, hoisted out of the error so a
panel never reaches into a transport-specific body. 404, gRPC code 5 and
GraphQL extensions.code: "NOT_FOUND" all arrive as not_found — which is what
makes a mixed timeline readable, since gRPC and GraphQL carry no HTTP status at
all.
Transfer progress
Progress ticks are stored latest-only, outside the event log: a single large upload emits continuously, and appending each tick would evict a whole session's history from the ring buffer within one request.
bridge.getProgressSnapshot(); // ReadonlyMap<`${source}:${id}`, InspectorProgress>Both stores publish through the one subscribe, so a panel binds either or both.
An entry's progress is released the moment its call concludes, which is what
keeps the map bounded over a long session.