Package Exports
- @sideband/runtime
- @sideband/runtime/package.json
Readme
@sideband/runtime
Transport-agnostic Sideband runtime pieces: session lifecycle, message routing, and RPC correlation. Most apps should use @sideband/peer; reach for @sideband/runtime when you need custom transports or want direct control over session and routing behavior.
Features
- SessionManager: Connect -> negotiate -> active with retry/backoff and lifecycle events
- Router: Subject validation, handler registry, RPC dispatch, and error mapping
- RpcCorrelationManager: Pending RPC tracking with timeouts and explicit
cidcorrelation - SbpNegotiator: Built-in SBP handshake negotiator for direct connections
Install
bun add @sideband/runtimeQuick start: session + router
import {
createRouter,
createSessionManager,
SbpNegotiator,
type Session,
} from "@sideband/runtime";
import { asPeerId, isMessageFrame } from "@sideband/protocol";
import { MemoryTransport, asTransportEndpoint } from "@sideband/transport";
const router = createRouter();
router.route("rpc/user.get", async (msg) => {
const userId = (msg.rpc?.params as { id: number }).id;
await msg.rpc?.reply({ id: userId, name: "Ada" });
});
let activeSession: Session | undefined;
const transport = new MemoryTransport();
const endpoint = asTransportEndpoint("memory://loop");
const manager = createSessionManager({
endpoint,
transportFactory: (endpoint) => transport.connect(endpoint),
negotiator: new SbpNegotiator({ peerId: asPeerId("browser-ui") }),
onFrame: async (frame) => {
if (!activeSession || !isMessageFrame(frame)) return;
const sessionLike = {
peerId: activeSession.peerId,
send: (data: Uint8Array) => activeSession.sendRaw(data),
};
const errorBytes = await router.dispatch(frame, sessionLike);
if (errorBytes) {
await activeSession.sendRaw(errorBytes);
}
},
});
activeSession = await manager.connect();Notes:
Router.dispatch()expectsMessageFrameand returns encoded ErrorFrame bytes when subject validation or RPC envelope decoding fails. Send those bytes back on the session channel.SessionManageronly decodes frames; higher layers own validation and routing.- The
MemoryTransportexample assumes a peer is listening on the same transport. Swap in your real transport for production.
Quick start: RPC correlation
import { RpcCorrelationManager } from "@sideband/runtime";
import { createRpcRequest, decodeRpcEnvelope, encodeRpcEnvelope } from "@sideband/rpc";
import { generateFrameId } from "@sideband/protocol";
const correlator = new RpcCorrelationManager(10_000);
const cid = generateFrameId();
const request = createRpcRequest("user.get", cid, { id: 42 });
const requestPayload = encodeRpcEnvelope(request);
const pending = correlator.registerRequest(cid);
// send requestPayload inside a MessageFrame over your transport...
const responsePayload = /* MessageFrame.data from remote peer */;
const envelope = decodeRpcEnvelope(responsePayload);
correlator.matchResponse(envelope.cid, envelope);
const response = await pending;Core concepts
SessionManager
connect()establishes a session with a negotiator and emits lifecycle events.on(event, handler)subscribes toconnecting,negotiating,active,retrying,closed, and identity events.- Retry is opt-in via
retryPolicyand uses exponential backoff with jitter.
Router
- Validates subjects against a policy and dispatches by deterministic ordering.
- RPC subjects use exclusive dispatch with timeout handling and error mapping.
- Event and custom subjects broadcast to all matching handlers.
Subject policy defaults
- Allowed prefixes:
rpc/,event/,stream/,app/ - Reserved prefixes:
stream/(rejected withErrorCode.UnsupportedFeature) - Use
createRouter(config, subjectPolicy)to override.
Negotiators
SbpNegotiatorimplements the SBP handshake (direct peer-to-peer).- SBRP is not built in. Integrate
@sideband/secure-relayby implementing a customNegotiatorthat returns a wrappedSessionChannel.
References
- Session lifecycle (ADR-009)
- RPC correlation (ADR-010)
- Routing semantics (ADR-011)
- Subject validation (ADR-008)
License
Apache-2.0