orbit-rs
orbit-rs is the primitive Orbit crate: a same-host runtime substrate
for recent facts shared by sibling processes.
It provides type-keyed bounded rings, optional POSIX shared-memory backing, fleet membership, heartbeat records, and small reusable substrates for cache, events, and contest/claim coordination.
orbit-rs is framework-agnostic. Application lifecycle and runtime
policy belong above this crate.
It is not a database, message broker, service process, global registry, or durable log.
Model
Fleet
process-local handle into a named fleet
OrbitTyped
Rust type -> stable KIND byte -> ring family
Ring / Frame
fixed-capacity append surface with bounded payloads
netid64::NetId64
runtime-bound id carried by every frame
Substrates
cache, event bus, contest, heartbeat, typed POD values
Frame identifiers come from the external
netid64 crate. Orbit uses that
runtime-bound identifier format; it does not define the identifier type
itself.
Fleet
Fleet is the per-process handle into Orbit.
Fleet::join(...)
-> process-local rings
Fleet::join_shm(...)
-> POSIX shared-memory rings visible to sibling processes
A fleet has a name, a NodeId, an expected fleet size, and one ring
registry. Every OrbitTyped::KIND maps to its own ring. Role hierarchy
is outside the crate: master, worker, standalone process, or sibling
tool can all join the same fleet if the embedder gives them compatible
configuration.
On Unix, shared-memory ring names are derived from:
/orbit-{fleet}-{kind}-{uid}
Rings
A ring is a fixed-capacity circular append surface. Writers reserve a
monotonic counter, mint a NetId64, and write a frame into:
counter % capacity
The frame shape is:
id: NetId64 | frame kind: u8 | version: u64 | payload: bytes
Two kind values are intentionally present:
id.kind()
the OrbitTyped value family, used to choose the ring
frame.kind
the message/opcode class inside that ring
Readers must tolerate wraparound. If a reader asks for a counter whose slot has already been overwritten, the frame is missing by design.
Typed Rings
OrbitTyped is the type-to-ring contract. A Rust type declares a stable
KIND, and that KIND selects the ring family used for its frames.
use OrbitTyped;
Callers do not pass ring names through the API. They pass their own type.
Every worker built from the same program knows the same KIND, so
Fleet::publish::<WorkerLoad> and Fleet::read_head::<WorkerLoad> meet
on the same ring.
Orbital<T> is the fixed-size value helper:
T: OrbitTyped + bytemuck::Pod
-> Orbital<T>::store(value)
-> Fleet::publish::<T>
-> ring selected by T::KIND
-> Orbital<T>::load()
-> T
This path is for small structs whose byte layout is known and stable. Variable-length records use explicit encoders in layers such as cache, event, contest, and metrics.
Cache
OrbitCache is a byte-oriented cache primitive over one dedicated ring.
It does not choose a serializer and it does not know application values.
Each mutation is one frame:
put key bytes, value bytes, optional expiry
delete key bytes
reset prefix bytes
Reads walk backward from the ring head. The newest matching frame wins:
putreturns a value unless expired;deleteshadows older puts;resetshadows older entries inside the cache prefix.
Values are inline and bounded by the ring payload size. Larger object caches should be built above this primitive, for example with a ring as mutation log plus a separate shared arena.
Event Bus
OrbitEventBus is an append-only event stream over one Orbit ring.
Events are not cache entries and not metrics snapshots:
cache asks: what is the newest value for this key?
metrics asks: what is the newest sample for each node/key?
events ask: which frames appeared since my cursor?
All topics deliberately share the same event ring. The topic is carried inside the frame payload, so adding a new event type does not allocate another shared-memory segment.
Each subscriber owns its own cursor. Polling advances that cursor across all frames, including frames later filtered out by topic. If a subscriber falls behind the fixed ring window, the poll result reports lag.
OrbitEventBus::publish(topic, payload)
-> topic/payload/timestamp frame
-> shared event ring
-> OrbitEventCursor
-> poll() / poll_topic()
Typed dispatch, application lifecycle hooks, acknowledgements, durable replay, and consumer groups belong above this primitive.
Contest
Contest is not a race primitive. It turns simultaneous interest in the
same typed subject into a small claim/yield protocol.
claim typed subject
-> observe active claims
-> earliest active claimant receives Guard
-> later claimants receive YieldTo(holder)
-> dropping Guard publishes release
Every peer may publish a claim. The earliest active claim receives a
drop-released Guard; later claimants receive YieldTo(holder).
A subject is a caller-defined ContestType::KIND plus a label. The
owner label is only for observation; Orbit does not interpret it.
The important bias is that followers yield. Orbit does not serialize a queue or make the holder faster. It lets one peer carry a typed subject while others can observe who carries it and back off.
TTL handles abandoned claims. Releasing is tied to the Guard lifetime,
so normal Rust scope becomes the release boundary for successful work.
Heartbeat
FleetHeartbeat is an Orbit substrate signal. It shows that a node is
still publishing into the shared fleet fabric.
It is not process supervision. Kill/restart/readiness policy belongs to the embedding runtime.
Dependency Boundary
Application lifecycle, typed runtime adapters, handlers, process
supervision, and product policy should live above orbit-rs.