2026-07-18 09:38:16 +00:00
|
|
|
//! Core data model: the framed, channel-multiplexed stream (spec §2).
|
2026-06-05 07:25:43 +00:00
|
|
|
//!
|
2026-07-12 06:14:34 +00:00
|
|
|
//! A stream is identified by the producing node and lifetime. Frames carry a
|
|
|
|
|
//! stream-local numeric channel id plus the mux-assigned position and opaque
|
|
|
|
|
//! payload bytes. Channel names, payload content kinds, and display metadata live
|
|
|
|
|
//! in catalog descriptors that consumers receive before or alongside frames.
|
2026-06-05 07:25:43 +00:00
|
|
|
|
|
|
|
|
use std::fmt;
|
|
|
|
|
use std::sync::Arc;
|
|
|
|
|
|
2026-07-12 06:14:34 +00:00
|
|
|
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
|
|
|
|
|
2026-07-18 09:38:16 +00:00
|
|
|
/// A position assigned by a node's mux during drain (spec §2.3).
|
2026-06-05 07:25:43 +00:00
|
|
|
///
|
2026-07-18 09:38:16 +00:00
|
|
|
/// Assignment is monotonic and gap-free for accepted frames: the mux never
|
|
|
|
|
/// reuses one and never skips one while draining. A frame assigned a position
|
|
|
|
|
/// can still be lost by transport and later surface downstream as a detectable
|
|
|
|
|
/// gap.
|
2026-07-12 06:14:34 +00:00
|
|
|
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize, Deserialize)]
|
2026-06-05 07:25:43 +00:00
|
|
|
pub struct Position(pub u64);
|
|
|
|
|
|
|
|
|
|
impl fmt::Display for Position {
|
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
|
|
|
write!(f, "{}", self.0)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-12 06:14:34 +00:00
|
|
|
/// Stream-local numeric channel id.
|
2026-06-05 07:25:43 +00:00
|
|
|
///
|
2026-07-18 09:38:16 +00:00
|
|
|
/// A raw `ChannelId` is meaningful only together with its [`StreamId`]. The
|
|
|
|
|
/// public endpoint allocator currently starts at `ChannelId(1)`, leaving
|
|
|
|
|
/// `ChannelId(0)` unallocated by normal registration.
|
2026-07-12 06:14:34 +00:00
|
|
|
#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Debug, Serialize, Deserialize)]
|
|
|
|
|
pub struct ChannelId(pub u32);
|
2026-06-05 07:25:43 +00:00
|
|
|
|
2026-07-12 06:14:34 +00:00
|
|
|
impl fmt::Display for ChannelId {
|
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
|
|
|
write!(f, "{}", self.0)
|
2026-06-05 07:25:43 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-12 06:14:34 +00:00
|
|
|
/// Payload content kind without schema details. Used for subscription filters.
|
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
pub enum ChannelContentKind {
|
|
|
|
|
Bytes,
|
|
|
|
|
TextStream,
|
|
|
|
|
JsonRecord,
|
2026-06-05 07:25:43 +00:00
|
|
|
}
|
|
|
|
|
|
2026-07-12 06:14:34 +00:00
|
|
|
/// How consumers should decode/display payload bytes for a channel.
|
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
pub enum ChannelContent {
|
|
|
|
|
Bytes,
|
|
|
|
|
TextStream,
|
|
|
|
|
JsonRecord { schema: Option<String> },
|
2026-06-05 07:25:43 +00:00
|
|
|
}
|
|
|
|
|
|
2026-07-12 06:14:34 +00:00
|
|
|
impl ChannelContent {
|
|
|
|
|
pub fn kind(&self) -> ChannelContentKind {
|
|
|
|
|
match self {
|
|
|
|
|
ChannelContent::Bytes => ChannelContentKind::Bytes,
|
|
|
|
|
ChannelContent::TextStream => ChannelContentKind::TextStream,
|
|
|
|
|
ChannelContent::JsonRecord { .. } => ChannelContentKind::JsonRecord,
|
|
|
|
|
}
|
2026-06-05 07:25:43 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-12 06:14:34 +00:00
|
|
|
/// Where a stream originates from in the current process topology.
|
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
pub enum StreamOrigin {
|
|
|
|
|
Orchestrator,
|
|
|
|
|
Bootstrap,
|
|
|
|
|
RemoteNode,
|
2026-06-05 07:25:43 +00:00
|
|
|
}
|
|
|
|
|
|
2026-07-18 09:38:16 +00:00
|
|
|
/// The stable identity of a node that produces a stream (spec §2.2, §7.1).
|
2026-06-05 07:25:43 +00:00
|
|
|
#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
|
|
|
|
pub struct NodeId(Arc<str>);
|
|
|
|
|
|
|
|
|
|
impl NodeId {
|
|
|
|
|
/// Construct a node id from any string-like value.
|
|
|
|
|
pub fn new(id: impl AsRef<str>) -> Self {
|
|
|
|
|
NodeId(Arc::from(id.as_ref()))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The id as a string slice.
|
|
|
|
|
pub fn as_str(&self) -> &str {
|
|
|
|
|
&self.0
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl From<&str> for NodeId {
|
|
|
|
|
fn from(s: &str) -> Self {
|
|
|
|
|
NodeId::new(s)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-12 06:14:34 +00:00
|
|
|
impl From<String> for NodeId {
|
|
|
|
|
fn from(s: String) -> Self {
|
|
|
|
|
NodeId::new(s)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-05 07:25:43 +00:00
|
|
|
impl fmt::Display for NodeId {
|
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
|
|
|
f.write_str(&self.0)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl fmt::Debug for NodeId {
|
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
|
|
|
write!(f, "NodeId({:?})", &self.0)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-12 06:14:34 +00:00
|
|
|
impl Serialize for NodeId {
|
|
|
|
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
|
|
|
where
|
|
|
|
|
S: Serializer,
|
|
|
|
|
{
|
|
|
|
|
serializer.serialize_str(self.as_str())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<'de> Deserialize<'de> for NodeId {
|
|
|
|
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
|
|
|
where
|
|
|
|
|
D: Deserializer<'de>,
|
|
|
|
|
{
|
|
|
|
|
String::deserialize(deserializer).map(NodeId::new)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 09:38:16 +00:00
|
|
|
/// A lifetime discriminator distinguishing a node's incarnations (spec §2.2).
|
2026-07-12 06:14:34 +00:00
|
|
|
#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Debug, Serialize, Deserialize)]
|
2026-06-05 07:25:43 +00:00
|
|
|
pub struct Lifetime(pub u64);
|
|
|
|
|
|
2026-07-12 06:14:34 +00:00
|
|
|
/// Identifies exactly one stored stream: a node plus the life it was produced in.
|
|
|
|
|
#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Debug, Serialize, Deserialize)]
|
2026-06-05 07:25:43 +00:00
|
|
|
pub struct StreamId {
|
|
|
|
|
/// Which node produced the stream.
|
|
|
|
|
pub node: NodeId,
|
|
|
|
|
/// Which life of that node.
|
|
|
|
|
pub life: Lifetime,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl StreamId {
|
|
|
|
|
/// Construct a stream id from a node and a lifetime.
|
|
|
|
|
pub fn new(node: impl Into<NodeId>, life: Lifetime) -> Self {
|
2026-06-23 15:42:28 +00:00
|
|
|
StreamId {
|
|
|
|
|
node: node.into(),
|
|
|
|
|
life,
|
|
|
|
|
}
|
2026-06-05 07:25:43 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl fmt::Display for StreamId {
|
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
|
|
|
write!(f, "{}#{}", self.node, self.life.0)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-12 06:14:34 +00:00
|
|
|
/// Stream metadata declared by the stream owner.
|
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
pub struct StreamDescriptor {
|
|
|
|
|
pub stream: StreamId,
|
|
|
|
|
pub label: Option<String>,
|
|
|
|
|
pub origin: StreamOrigin,
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 09:38:16 +00:00
|
|
|
/// Catalog metadata declared by the stream owner; frames store only `id` and payload.
|
2026-07-12 06:14:34 +00:00
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
pub struct ChannelDescriptor {
|
|
|
|
|
pub stream: StreamId,
|
|
|
|
|
pub id: ChannelId,
|
|
|
|
|
pub name: String,
|
|
|
|
|
pub label: Option<String>,
|
|
|
|
|
pub content: ChannelContent,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Globally resolved channel identity: a stream plus that stream's numeric lane.
|
|
|
|
|
#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Debug, Serialize, Deserialize)]
|
|
|
|
|
pub struct ChannelRef {
|
|
|
|
|
pub stream: StreamId,
|
|
|
|
|
pub channel: ChannelId,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// A delivered frame with its stream-local channel resolved to a [`ChannelRef`].
|
|
|
|
|
#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
|
|
|
|
|
pub struct FrameDelivery {
|
|
|
|
|
pub channel: ChannelRef,
|
|
|
|
|
pub position: Position,
|
|
|
|
|
pub payload: Vec<u8>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Catalog and frame events delivered to subscribers.
|
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
pub enum DatastreamEvent {
|
|
|
|
|
StreamDeclared(StreamDescriptor),
|
|
|
|
|
ChannelDeclared(ChannelDescriptor),
|
|
|
|
|
Frame(FrameDelivery),
|
|
|
|
|
StreamEnded(StreamId),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Source filter used by subscribers.
|
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
pub enum SourceFilter {
|
|
|
|
|
All,
|
|
|
|
|
Origin(StreamOrigin),
|
|
|
|
|
Node(NodeId),
|
|
|
|
|
Stream(StreamId),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Channel filter used by subscribers.
|
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
pub enum ChannelFilter {
|
|
|
|
|
All,
|
|
|
|
|
Name(String),
|
|
|
|
|
Prefix(String),
|
|
|
|
|
Content(ChannelContentKind),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// A subscription request for catalog metadata and future frame events.
|
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
pub struct SubscriptionRequest {
|
|
|
|
|
pub sources: SourceFilter,
|
|
|
|
|
pub channels: ChannelFilter,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl SubscriptionRequest {
|
|
|
|
|
pub fn all() -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
sources: SourceFilter::All,
|
|
|
|
|
channels: ChannelFilter::All,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 09:38:16 +00:00
|
|
|
/// The unit the mux emits (spec §2.1): bytes tagged with a channel and a position.
|
2026-07-12 06:14:34 +00:00
|
|
|
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
|
2026-06-05 07:25:43 +00:00
|
|
|
pub struct Frame {
|
2026-07-12 06:14:34 +00:00
|
|
|
/// The stream-local lane these bytes belong to.
|
2026-06-05 07:25:43 +00:00
|
|
|
pub channel: ChannelId,
|
|
|
|
|
/// The mux-assigned position within the node's stream.
|
|
|
|
|
pub position: Position,
|
|
|
|
|
/// The opaque payload bytes.
|
|
|
|
|
pub payload: Vec<u8>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Frame {
|
|
|
|
|
/// Assemble a frame from its parts.
|
2026-07-12 06:14:34 +00:00
|
|
|
pub fn new(channel: ChannelId, position: Position, payload: Vec<u8>) -> Self {
|
2026-06-23 15:42:28 +00:00
|
|
|
Frame {
|
2026-07-12 06:14:34 +00:00
|
|
|
channel,
|
2026-06-23 15:42:28 +00:00
|
|
|
position,
|
|
|
|
|
payload,
|
|
|
|
|
}
|
2026-06-05 07:25:43 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl fmt::Debug for Frame {
|
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
|
|
|
let mut dbg = f.debug_struct("Frame");
|
2026-06-23 15:42:28 +00:00
|
|
|
dbg.field("channel", &self.channel)
|
|
|
|
|
.field("position", &self.position);
|
2026-06-05 07:25:43 +00:00
|
|
|
match std::str::from_utf8(&self.payload) {
|
|
|
|
|
Ok(text) => dbg.field("payload", &text),
|
|
|
|
|
Err(_) => dbg.field("payload", &format_args!("<{} bytes>", self.payload.len())),
|
|
|
|
|
};
|
|
|
|
|
dbg.finish()
|
|
|
|
|
}
|
|
|
|
|
}
|