The JSON-RPC Component¶
The jsonrpc component exposes a running Sen process to outside programs without forcing
them to link against the Sen kernel. A browser UI, a Python script, a Node.js service, or any
other process can connect, inspect the live object graph, subscribe to property changes and
events, and invoke methods.
What JSON-RPC and WebSockets are¶
If those terms aren't already familiar:
- JSON-RPC 2.0 is a small request/response message format. A request is a JSON object saying "call this method with these arguments, here's an id"; the response is another JSON object pairing that id with a result (or an error). It is just a convention for what the JSON looks like -- it does not specify how the bytes travel between client and server.
- WebSocket is a connection-oriented bidirectional message channel kept open between a browser (or any program with a WebSocket library) and a server. Once the connection is established, either side can push messages without polling. Each side reads framed messages out of the socket as they arrive.
jsonrpc puts those two together: a long-lived WebSocket carries JSON-RPC request/response
pairs in both directions, plus server-initiated notifications the kernel pushes whenever
matched objects, properties, or events change.
When to reach for jsonrpc¶
It is one of the ways to talk to a running Sen process from the outside:
| Component | Transport | Best for |
|---|---|---|
shell |
Local TCP, text protocol | Quick interactive poking from a terminal |
jsonrpc |
WebSocket + JSON-RPC 2.0 | Long-lived programmatic sessions, streaming notifications |
The wire grammar is defined by the component's STL
(components/jsonrpc/stl/jsonrpc.stl).
Both the C++ dispatcher and the bundled TypeScript client are generated from it, so they
cannot drift.
Running the component¶
address and port are the only required fields. The full set of configuration knobs:
| Field | Default | Notes |
|---|---|---|
address |
-- | IP / hostname to bind. Use 0.0.0.0 to listen on all interfaces. |
port |
-- | TCP port. |
updateFreqHz |
60 |
Dispatcher tick rate. Higher = faster notification turnaround, more CPU. |
connectionLimits.idleTimeoutSeconds |
120 |
uWS closes a connection idle for this long. |
connectionLimits.maxPayloadBytes |
16384 |
Reject any inbound frame larger than this. |
connectionLimits.maxBackpressureBytes |
65536 |
Hard outbound buffer ceiling per connection -- crossing it drops the connection. |
connectionLimits.highBackpressureBytes |
32768 |
Soft threshold: above this, the server starts dropping unreliable notifications and emits notificationsDropped on recovery. See Backpressure below. |
controlBusName |
"jsonrpc_control" |
Sen bus on local. where the static-file-server registry is exposed. See Static file server below. |
The tls field is reserved in the schema but not yet wired through the uWebSockets SSL
backend. Setting it fails component start. Front the component with a TLS-terminating proxy
(nginx, Caddy, etc.) when wss:// is needed.
Wire surface¶
A typical session flows like this:
- Connect -- the client opens the WebSocket; the server runs the configured
Authenticatoron the upgrade. - Discover -- the client calls
listTopologyto see what sessions and buses exist, andgetTypes/getTypeto fetch class shapes. - Declare an interest --
createInterestopens a live query (a Sen Query Language string) and returns the current match-set. From there the client owns a logical interest it can read from, write to, and subscribe to. Ordering note: the server fills the initial match-set by firinginterestUpdate.addedbefore thecreateInterestresponse lands on the wire. Clients must be ready to receive pushes for an interest whose response hasn't acked yet; the@sen/clientlibrary handles this transparently. Hand-written clients should queue inbound notifications keyed byinterestNameuntil the response is observed. A query may name a bus that does not exist yet: the interest is accepted, starts empty, and resolves live once a publisher joins that bus — which is what makes re-declaring interests right after a server restart race-free. The flip side is that a typo'd bus name yields a forever-empty interest rather than an error; uselistTopologyto check what exists. A connection can hold at most 256 concurrent interests. - Read / write / invoke / subscribe -- per matched object:
getProperty,setProperty,invoke,subscribeProperty,subscribeEvent, etc. - Receive notifications -- as the kernel ticks, the server pushes
interestUpdate,propertyChanged,eventTriggerednotifications matching the client's subscriptions. - Release --
releaseInteresttears down subscriptions associated with the interest; closing the WebSocket tears down everything.
Methods (request / response):
ping-- round-trip check.listTopology,subscribeTopology,unsubscribeTopology-- discover sessions / buses.getTypes,getType-- enumerate and fetch the kernel's registered custom types.createInterest,releaseInterest,listObjects-- manage live queries.getProperty,setProperty-- read / write a property value on a matched object.subscribeProperty,unsubscribeProperty,subscribeEvent,unsubscribeEvent,subscribeAll,unsubscribeAll-- manage per-(interest, object, member) subscriptions.invoke-- call a method on a matched object.getObjectsBatchState-- read every property of one or many matched objects in a single round-trip.
Server-pushed notifications:
interestUpdate-- objects matched by an interest came or went; carries theCustomTypeSpecs the client hasn't seen yet.propertyChanged-- one or more properties changed on a matched object.eventTriggered-- an event fired on a matched object.notificationsDropped-- emitted on recovery from a high-backpressure window, with the cumulative count of unreliable notifications the dispatcher dropped during it.topologyChanged-- session / bus topology shifted (for subscribers).
The full envelope shapes are defined in jsonrpc.stl.
Static file server¶
jsonrpc ships an optional HTTP static-file responder that runs on the same listening
socket as the WebSocket. Other components register URL prefixes -> on-disk roots at runtime
over the control bus (default local.jsonrpc_control); the responder serves files under
those prefixes with appropriate MIME types. The same origin hosts the WebSocket, so a
browser-side client loaded from http://host:8080/explorer/ can open a WebSocket to
ws://host:8080 without crossing origins.
The canonical consumer is the webexplorer component: it subscribes to the control bus,
finds the StaticFileServer object, and calls its registerBundle(urlPrefix, diskRoot)
method to expose its compiled-and-bundled frontend at, e.g., /explorer/.
The feature is dormant when nothing registers a bundle. There is no plain-HTTP API exposed by default.
Backpressure¶
Sen pushes notifications eagerly. A slow client that subscribes to thousands of high-rate properties can fall behind the server's send rate, causing the outbound buffer to grow without bound. Two thresholds defend against this:
- Soft (
highBackpressureBytes, default 32 KiB): when the outbound buffer crosses this size, the dispatcher marks the connection as "high" and silently drops further unreliable notifications (sticky subscription updates, etc.). Reliable replies and events keep flowing. When the buffer drains back below the threshold, the server emits onenotificationsDropped {count: N}notification carrying the cumulative drop count for that window so the client can know it missed updates and refresh. - Hard (
maxBackpressureBytes, default 64 KiB): crossing this ceiling drops the connection. The client must reconnect and re-establish state.
Both thresholds are per-connection. A misbehaving client cannot affect well-behaved neighbors.
Clients¶
A TypeScript client (@sen/client) ships in-tree at
components/jsonrpc/clients/typescript/.
It wraps the wire surface in idiomatic browser / Node.js code and ships matching React
bindings (@sen/client/react). See the dedicated TypeScript client documentation for
install instructions, the Client / InterestHandle / ObjectHandle lifecycle, error
handling, reconnection, and the React hook surface.
The wire is JSON-RPC 2.0, so a client in any other language can be written against
jsonrpc.stl directly. The Python ecosystem has python-jsonrpc-client and similar
options; the Sen project does not maintain one in-tree.
Authentication¶
A pluggable seam in auth.h runs on every WebSocket upgrade. The default Authenticator
implementation (NoAuth) accepts every upgrade.
To require auth, subclass Authenticator, read the Authorization header (or any other
upgrade-request fields), validate, and return an Identity (or reject with 401). The
returned Identity is forwarded into every handler call. Until a non-default authenticator
is wired, gate jsonrpc at the network layer (firewall, reverse-proxy auth) when exposing
it beyond 127.0.0.1.
Component architecture¶
See components/jsonrpc/architecture.md
for the contributor-facing overview: threading model, layer diagram, per-module
responsibilities, key invariants.