2026-11-02 12:44:33 +00:00
|
|
|
//! Transport-neutral dispatch from protocol requests to image API operations.
|
|
|
|
|
|
2026-11-03 14:36:21 +00:00
|
|
|
use std::io::{self, BufRead, Write};
|
|
|
|
|
use std::net::{SocketAddr, TcpListener};
|
2026-11-02 12:44:33 +00:00
|
|
|
use std::num::NonZeroUsize;
|
2026-11-03 14:36:21 +00:00
|
|
|
|
|
|
|
|
mod network;
|
|
|
|
|
pub use network::serve_websocket;
|
2026-11-02 12:44:33 +00:00
|
|
|
|
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
|
|
|
use runa_actor::{ActorRef, Event, Status};
|
2026-11-02 12:44:33 +00:00
|
|
|
use runa_image_api::ImageApi;
|
|
|
|
|
use runa_protocol::{
|
|
|
|
|
ActorSnapshotDto, ActorStatusDto, BehaviorDto, BehaviorVersionDto, Command, ErrorCode,
|
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
|
|
|
EventDto, ExecutionSnapshotDto, PROTOCOL_VERSION, ProtocolError, Reply, Request, Response,
|
2026-11-02 12:44:33 +00:00
|
|
|
};
|
|
|
|
|
|
2026-11-02 13:18:45 +00:00
|
|
|
pub const MAX_REQUEST_LINE_BYTES: usize = 1024 * 1024;
|
|
|
|
|
|
2026-11-02 12:44:33 +00:00
|
|
|
pub struct Server {
|
|
|
|
|
image: ImageApi,
|
|
|
|
|
}
|
|
|
|
|
|
2026-11-02 13:18:45 +00:00
|
|
|
/// Serve newline-delimited protocol requests over an already-connected stream.
|
|
|
|
|
///
|
|
|
|
|
/// Each valid request line receives one JSON response line. Malformed or
|
|
|
|
|
/// oversized input terminates the stream with `InvalidData`.
|
|
|
|
|
pub fn serve_json_lines<R: BufRead, W: Write>(
|
|
|
|
|
server: &mut Server,
|
2026-11-02 13:50:30 +00:00
|
|
|
reader: R,
|
|
|
|
|
writer: W,
|
2026-11-02 13:18:45 +00:00
|
|
|
) -> io::Result<()> {
|
2026-11-02 13:50:30 +00:00
|
|
|
serve_json_lines_with(reader, writer, |request| Ok(server.dispatch(request)))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Bind the protocol listener to an IPv4 or IPv6 loopback address only.
|
|
|
|
|
pub fn bind_loopback_listener(address: SocketAddr) -> io::Result<TcpListener> {
|
|
|
|
|
if !address.ip().is_loopback() {
|
|
|
|
|
return Err(io::Error::new(
|
|
|
|
|
io::ErrorKind::InvalidInput,
|
|
|
|
|
"network listener address must be a loopback address",
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TcpListener::bind(address)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn invalid_data(message: String) -> io::Error {
|
|
|
|
|
io::Error::new(io::ErrorKind::InvalidData, message)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn serve_json_lines_with<R, W, F>(mut reader: R, mut writer: W, mut dispatch: F) -> io::Result<()>
|
|
|
|
|
where
|
|
|
|
|
R: BufRead,
|
|
|
|
|
W: Write,
|
|
|
|
|
F: FnMut(Request) -> io::Result<Response>,
|
|
|
|
|
{
|
2026-11-02 13:18:45 +00:00
|
|
|
let mut line = Vec::new();
|
|
|
|
|
while read_bounded_line(&mut reader, &mut line)? {
|
2026-11-02 13:50:30 +00:00
|
|
|
let request: Request = serde_json::from_slice(&line)
|
|
|
|
|
.map_err(|error| invalid_data(format!("invalid protocol request JSON: {error}")))?;
|
|
|
|
|
let response = dispatch(request)?;
|
2026-11-02 13:18:45 +00:00
|
|
|
serde_json::to_writer(&mut writer, &response).map_err(io::Error::other)?;
|
|
|
|
|
writer.write_all(b"\n")?;
|
|
|
|
|
writer.flush()?;
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn read_bounded_line<R: BufRead>(reader: &mut R, line: &mut Vec<u8>) -> io::Result<bool> {
|
|
|
|
|
line.clear();
|
|
|
|
|
loop {
|
|
|
|
|
let available = reader.fill_buf()?;
|
|
|
|
|
if available.is_empty() {
|
|
|
|
|
return Ok(!line.is_empty());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let newline = available.iter().position(|byte| *byte == b'\n');
|
|
|
|
|
let payload_len = newline.unwrap_or(available.len());
|
|
|
|
|
if line.len().saturating_add(payload_len) > MAX_REQUEST_LINE_BYTES {
|
|
|
|
|
return Err(io::Error::new(
|
|
|
|
|
io::ErrorKind::InvalidData,
|
|
|
|
|
format!("request line exceeds {MAX_REQUEST_LINE_BYTES} bytes"),
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
line.extend_from_slice(&available[..payload_len]);
|
|
|
|
|
let consumed = payload_len + usize::from(newline.is_some());
|
|
|
|
|
reader.consume(consumed);
|
|
|
|
|
if newline.is_some() {
|
|
|
|
|
return Ok(true);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-11-02 12:44:33 +00:00
|
|
|
impl Default for Server {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
Self::new()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Server {
|
|
|
|
|
pub fn new() -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
image: ImageApi::new(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn with_image_api(image: ImageApi) -> Self {
|
|
|
|
|
Self { image }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn dispatch(&mut self, request: Request) -> Response {
|
|
|
|
|
let request_id = request.request_id.clone();
|
|
|
|
|
let result = request
|
|
|
|
|
.validate()
|
|
|
|
|
.and_then(|()| self.dispatch_command(request.command));
|
|
|
|
|
|
|
|
|
|
Response {
|
|
|
|
|
version: PROTOCOL_VERSION,
|
|
|
|
|
request_id,
|
|
|
|
|
result,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn dispatch_command(&mut self, command: Command) -> Result<Reply, ProtocolError> {
|
|
|
|
|
match command {
|
|
|
|
|
Command::PublishBehavior {
|
|
|
|
|
name,
|
|
|
|
|
expected_revision,
|
|
|
|
|
source,
|
|
|
|
|
} => self
|
|
|
|
|
.image
|
|
|
|
|
.publish_behavior(&name, expected_revision, &source)
|
|
|
|
|
.map(behavior_dto)
|
|
|
|
|
.map(Reply::Behavior)
|
|
|
|
|
.map_err(api_error),
|
|
|
|
|
Command::RollbackBehavior {
|
|
|
|
|
name,
|
|
|
|
|
expected_revision,
|
|
|
|
|
target_revision,
|
|
|
|
|
} => self
|
|
|
|
|
.image
|
|
|
|
|
.rollback_behavior(&name, expected_revision, target_revision)
|
|
|
|
|
.map(behavior_dto)
|
|
|
|
|
.map(Reply::Behavior)
|
|
|
|
|
.map_err(api_error),
|
|
|
|
|
Command::GetBehavior { name } => self
|
|
|
|
|
.image
|
|
|
|
|
.behavior(&name)
|
|
|
|
|
.map(behavior_dto)
|
|
|
|
|
.map(Reply::Behavior)
|
|
|
|
|
.ok_or_else(|| ProtocolError::new(ErrorCode::NotFound, "unknown behavior")),
|
|
|
|
|
Command::GetBehaviors => Ok(Reply::Behaviors(
|
|
|
|
|
self.image
|
|
|
|
|
.behaviors()
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(behavior_dto)
|
|
|
|
|
.collect(),
|
|
|
|
|
)),
|
|
|
|
|
Command::GetBehaviorSource { name, revision } => self
|
|
|
|
|
.image
|
|
|
|
|
.behavior_source(&name, revision)
|
|
|
|
|
.map(|source| Reply::BehaviorSource {
|
|
|
|
|
source: source.to_owned(),
|
|
|
|
|
})
|
|
|
|
|
.map_err(api_error),
|
|
|
|
|
Command::Spawn {
|
|
|
|
|
behavior_name,
|
|
|
|
|
state,
|
|
|
|
|
mailbox_capacity,
|
|
|
|
|
} => {
|
|
|
|
|
let capacity = NonZeroUsize::new(mailbox_capacity)
|
|
|
|
|
.ok_or_else(|| invalid_request("mailbox_capacity must be positive"))?;
|
|
|
|
|
self.image
|
|
|
|
|
.spawn(&behavior_name, state, capacity)
|
2026-11-02 14:19:25 +00:00
|
|
|
.map(|actor_ref| Reply::ActorSpawned {
|
|
|
|
|
actor_ref: actor_ref.as_str().to_owned(),
|
2026-11-02 12:44:33 +00:00
|
|
|
})
|
|
|
|
|
.map_err(api_error)
|
|
|
|
|
}
|
2026-11-02 14:19:25 +00:00
|
|
|
Command::Send { actor_ref, message } => {
|
|
|
|
|
let actor_ref = parse_actor_ref(&actor_ref)?;
|
|
|
|
|
self.image
|
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
|
|
|
.send(actor_ref, message)
|
2026-11-02 14:19:25 +00:00
|
|
|
.map(|()| Reply::Acknowledged)
|
|
|
|
|
.map_err(api_error)
|
|
|
|
|
}
|
2026-11-02 12:44:33 +00:00
|
|
|
Command::Tick {
|
|
|
|
|
ticks,
|
|
|
|
|
instruction_budget,
|
|
|
|
|
} => {
|
|
|
|
|
let budget = NonZeroUsize::new(instruction_budget)
|
|
|
|
|
.ok_or_else(|| invalid_request("instruction_budget must be positive"))?;
|
|
|
|
|
self.image.tick(ticks, budget);
|
|
|
|
|
Ok(Reply::TickCompleted { ticks })
|
|
|
|
|
}
|
2026-11-02 14:19:25 +00:00
|
|
|
Command::InspectActor { actor_ref } => {
|
|
|
|
|
let actor_ref = parse_actor_ref(&actor_ref)?;
|
|
|
|
|
self.image
|
|
|
|
|
.inspect(actor_ref)
|
|
|
|
|
.map(|snapshot| actor_snapshot_dto(&snapshot))
|
|
|
|
|
.map(Reply::Actor)
|
|
|
|
|
.map_err(api_error)
|
|
|
|
|
}
|
2026-11-02 12:44:33 +00:00
|
|
|
Command::ListActors => Ok(Reply::Actors(
|
|
|
|
|
self.image.actors().iter().map(actor_snapshot_dto).collect(),
|
|
|
|
|
)),
|
|
|
|
|
Command::GetEvents => Ok(Reply::Events(
|
|
|
|
|
self.image.events().iter().map(event_dto).collect(),
|
|
|
|
|
)),
|
2026-11-02 14:19:25 +00:00
|
|
|
Command::StopActor { actor_ref } => {
|
|
|
|
|
let actor_ref = parse_actor_ref(&actor_ref)?;
|
|
|
|
|
self.image
|
|
|
|
|
.stop(actor_ref)
|
|
|
|
|
.map(|()| Reply::Acknowledged)
|
|
|
|
|
.map_err(api_error)
|
|
|
|
|
}
|
|
|
|
|
Command::RestartActor { actor_ref } => {
|
|
|
|
|
let actor_ref = parse_actor_ref(&actor_ref)?;
|
|
|
|
|
self.image
|
|
|
|
|
.restart(actor_ref)
|
|
|
|
|
.map(|()| Reply::Acknowledged)
|
|
|
|
|
.map_err(api_error)
|
|
|
|
|
}
|
|
|
|
|
Command::PauseActor { actor_ref } => {
|
|
|
|
|
let actor_ref = parse_actor_ref(&actor_ref)?;
|
|
|
|
|
self.image
|
|
|
|
|
.pause(actor_ref)
|
|
|
|
|
.map(|()| Reply::Acknowledged)
|
|
|
|
|
.map_err(api_error)
|
|
|
|
|
}
|
|
|
|
|
Command::ResumeActor { actor_ref } => {
|
|
|
|
|
let actor_ref = parse_actor_ref(&actor_ref)?;
|
|
|
|
|
self.image
|
|
|
|
|
.resume(actor_ref)
|
|
|
|
|
.map(|()| Reply::Acknowledged)
|
|
|
|
|
.map_err(api_error)
|
|
|
|
|
}
|
|
|
|
|
Command::CancelActor { actor_ref } => {
|
|
|
|
|
let actor_ref = parse_actor_ref(&actor_ref)?;
|
|
|
|
|
self.image
|
|
|
|
|
.cancel(actor_ref)
|
|
|
|
|
.map(|()| Reply::Acknowledged)
|
|
|
|
|
.map_err(api_error)
|
|
|
|
|
}
|
2026-11-02 12:44:33 +00:00
|
|
|
Command::InstallBehavior {
|
2026-11-02 14:19:25 +00:00
|
|
|
actor_ref,
|
2026-11-02 12:44:33 +00:00
|
|
|
behavior_name,
|
2026-11-02 14:19:25 +00:00
|
|
|
} => {
|
|
|
|
|
let actor_ref = parse_actor_ref(&actor_ref)?;
|
|
|
|
|
self.image
|
|
|
|
|
.install(actor_ref, &behavior_name)
|
|
|
|
|
.map(|implementation_id| Reply::BehaviorInstalled {
|
|
|
|
|
implementation_id: implementation_id.to_string(),
|
|
|
|
|
})
|
|
|
|
|
.map_err(api_error)
|
|
|
|
|
}
|
2026-11-02 12:44:33 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn behavior_dto(behavior: runa_code::BehaviorSnapshot) -> BehaviorDto {
|
|
|
|
|
BehaviorDto {
|
|
|
|
|
name: behavior.name,
|
|
|
|
|
revision: behavior.revision,
|
|
|
|
|
current_implementation_id: behavior.current.to_string(),
|
|
|
|
|
history: behavior
|
|
|
|
|
.history
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|version| BehaviorVersionDto {
|
|
|
|
|
revision: version.revision,
|
|
|
|
|
implementation_id: version.implementation_id.to_string(),
|
|
|
|
|
})
|
|
|
|
|
.collect(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn actor_snapshot_dto(snapshot: &runa_actor::ActorSnapshot) -> ActorSnapshotDto {
|
|
|
|
|
ActorSnapshotDto {
|
2026-11-02 14:19:25 +00:00
|
|
|
actor_ref: snapshot.actor_ref.as_str().to_owned(),
|
2026-11-02 12:44:33 +00:00
|
|
|
mailbox_id: snapshot.mailbox.0,
|
|
|
|
|
implementation_id: snapshot.implementation.to_string(),
|
|
|
|
|
incarnation: snapshot.incarnation,
|
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
|
|
|
state: snapshot.state.clone(),
|
2026-11-02 12:44:33 +00:00
|
|
|
status: match &snapshot.status {
|
|
|
|
|
Status::Idle => ActorStatusDto::Idle,
|
|
|
|
|
Status::Running => ActorStatusDto::Running,
|
|
|
|
|
Status::Awaiting => ActorStatusDto::Awaiting,
|
|
|
|
|
Status::Paused => ActorStatusDto::Paused,
|
|
|
|
|
Status::Stopped => ActorStatusDto::Stopped,
|
|
|
|
|
Status::Failed(message) => ActorStatusDto::Failed(message.clone()),
|
|
|
|
|
},
|
|
|
|
|
execution: snapshot.execution.map(|execution| ExecutionSnapshotDto {
|
|
|
|
|
vm_steps: execution.vm_steps,
|
|
|
|
|
instruction_pointer: execution.instruction_pointer,
|
|
|
|
|
call_depth: execution.call_depth,
|
|
|
|
|
}),
|
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
|
|
|
queued: snapshot.queued.clone(),
|
|
|
|
|
reserved: snapshot.reserved.clone(),
|
2026-11-02 12:44:33 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn event_dto(event: &Event) -> EventDto {
|
|
|
|
|
match event {
|
2026-11-02 14:19:25 +00:00
|
|
|
Event::Spawned(actor_ref) => EventDto::Spawned {
|
|
|
|
|
actor_ref: actor_ref.as_str().to_owned(),
|
2026-11-02 12:44:33 +00:00
|
|
|
},
|
2026-11-02 14:19:25 +00:00
|
|
|
Event::Committed(actor_ref) => EventDto::Committed {
|
|
|
|
|
actor_ref: actor_ref.as_str().to_owned(),
|
2026-11-02 12:44:33 +00:00
|
|
|
},
|
2026-11-02 14:19:25 +00:00
|
|
|
Event::Failed(actor_ref, message) => EventDto::Failed {
|
|
|
|
|
actor_ref: actor_ref.as_str().to_owned(),
|
2026-11-02 12:44:33 +00:00
|
|
|
message: message.clone(),
|
|
|
|
|
},
|
2026-11-02 14:19:25 +00:00
|
|
|
Event::Stopped(actor_ref) => EventDto::Stopped {
|
|
|
|
|
actor_ref: actor_ref.as_str().to_owned(),
|
2026-11-02 12:44:33 +00:00
|
|
|
},
|
2026-11-02 14:19:25 +00:00
|
|
|
Event::Restarted(actor_ref) => EventDto::Restarted {
|
|
|
|
|
actor_ref: actor_ref.as_str().to_owned(),
|
2026-11-02 12:44:33 +00:00
|
|
|
},
|
2026-11-02 14:19:25 +00:00
|
|
|
Event::Paused(actor_ref) => EventDto::Paused {
|
|
|
|
|
actor_ref: actor_ref.as_str().to_owned(),
|
2026-11-02 12:44:33 +00:00
|
|
|
},
|
2026-11-02 14:19:25 +00:00
|
|
|
Event::Resumed(actor_ref) => EventDto::Resumed {
|
|
|
|
|
actor_ref: actor_ref.as_str().to_owned(),
|
2026-11-02 12:44:33 +00:00
|
|
|
},
|
2026-11-02 14:19:25 +00:00
|
|
|
Event::InvocationCancelled(actor_ref) => EventDto::InvocationCancelled {
|
|
|
|
|
actor_ref: actor_ref.as_str().to_owned(),
|
2026-11-02 12:44:33 +00:00
|
|
|
},
|
2026-11-02 14:19:25 +00:00
|
|
|
Event::BehaviorInstalled(actor_ref, implementation_id) => EventDto::BehaviorInstalled {
|
|
|
|
|
actor_ref: actor_ref.as_str().to_owned(),
|
2026-11-02 12:44:33 +00:00
|
|
|
implementation_id: implementation_id.to_string(),
|
|
|
|
|
},
|
2026-11-02 14:19:25 +00:00
|
|
|
Event::StateMigrated(actor_ref, previous_state, current_state) => EventDto::StateMigrated {
|
|
|
|
|
actor_ref: actor_ref.as_str().to_owned(),
|
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
|
|
|
previous_state: previous_state.clone(),
|
|
|
|
|
current_state: current_state.clone(),
|
2026-11-02 12:44:33 +00:00
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn api_error(error: impl std::fmt::Display) -> ProtocolError {
|
|
|
|
|
let message = error.to_string();
|
|
|
|
|
let code = if message.starts_with("behavior revision conflict:") {
|
|
|
|
|
ErrorCode::Conflict
|
|
|
|
|
} else if matches!(
|
|
|
|
|
message.as_str(),
|
|
|
|
|
"unknown actor"
|
|
|
|
|
| "unknown behavior"
|
|
|
|
|
| "unknown behavior revision"
|
|
|
|
|
| "unknown target behavior revision"
|
|
|
|
|
| "unknown implementation"
|
|
|
|
|
) {
|
|
|
|
|
ErrorCode::NotFound
|
|
|
|
|
} else if matches!(
|
|
|
|
|
message.as_str(),
|
|
|
|
|
"mailbox full" | "effect exceeds mailbox capacity"
|
|
|
|
|
) {
|
|
|
|
|
ErrorCode::CapacityExceeded
|
|
|
|
|
} else if message == "invalid behavior name" {
|
|
|
|
|
ErrorCode::InvalidRequest
|
|
|
|
|
} else {
|
|
|
|
|
ErrorCode::RuntimeFailure
|
|
|
|
|
};
|
|
|
|
|
ProtocolError::new(code, message)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn invalid_request(message: impl Into<String>) -> ProtocolError {
|
|
|
|
|
ProtocolError::new(ErrorCode::InvalidRequest, message)
|
|
|
|
|
}
|
|
|
|
|
|
2026-11-02 14:19:25 +00:00
|
|
|
fn parse_actor_ref(value: &str) -> Result<ActorRef, ProtocolError> {
|
|
|
|
|
ActorRef::parse(value).ok_or_else(|| invalid_request("actor_ref must be a valid Nano ID"))
|
|
|
|
|
}
|
|
|
|
|
|
2026-11-02 12:44:33 +00:00
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
|
|
|
use runa_protocol::Value;
|
|
|
|
|
|
|
|
|
|
fn test_message(kind: i64, value: i64, target: Option<ActorRef>) -> Value {
|
|
|
|
|
Value::Record(std::collections::BTreeMap::from([
|
|
|
|
|
("kind".to_owned(), Value::Integer(kind)),
|
|
|
|
|
("value".to_owned(), Value::Integer(value)),
|
|
|
|
|
(
|
|
|
|
|
"target".to_owned(),
|
|
|
|
|
target.map_or(Value::Null, Value::ActorRef),
|
|
|
|
|
),
|
|
|
|
|
]))
|
|
|
|
|
}
|
|
|
|
|
|
2026-11-02 12:44:33 +00:00
|
|
|
use runa_protocol::{Command, PROTOCOL_VERSION};
|
2026-11-03 14:36:21 +00:00
|
|
|
use std::io::Cursor;
|
2026-11-02 12:44:33 +00:00
|
|
|
|
|
|
|
|
fn request(command: Command) -> Request {
|
|
|
|
|
Request {
|
|
|
|
|
version: PROTOCOL_VERSION,
|
|
|
|
|
request_id: "test-request".to_owned(),
|
|
|
|
|
command,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn publish(server: &mut Server) -> BehaviorDto {
|
|
|
|
|
let response = server.dispatch(request(Command::PublishBehavior {
|
|
|
|
|
name: "counter".to_owned(),
|
|
|
|
|
expected_revision: None,
|
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
|
|
|
source: "pub fn handle(state, message) { (state + message.value, []) }".to_owned(),
|
2026-11-02 12:44:33 +00:00
|
|
|
}));
|
|
|
|
|
match response.result.unwrap() {
|
|
|
|
|
Reply::Behavior(behavior) => behavior,
|
|
|
|
|
other => panic!("unexpected reply: {other:?}"),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn dispatches_behavior_actor_and_event_operations() {
|
|
|
|
|
let mut server = Server::new();
|
|
|
|
|
let behavior = publish(&mut server);
|
|
|
|
|
assert_eq!(behavior.revision, 1);
|
|
|
|
|
assert_eq!(behavior.history.len(), 1);
|
|
|
|
|
|
|
|
|
|
let spawned = server.dispatch(request(Command::Spawn {
|
|
|
|
|
behavior_name: "counter".to_owned(),
|
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
|
|
|
state: Value::Integer(5),
|
2026-11-02 12:44:33 +00:00
|
|
|
mailbox_capacity: 2,
|
|
|
|
|
}));
|
2026-11-02 14:19:25 +00:00
|
|
|
let actor_ref = match spawned.result.unwrap() {
|
|
|
|
|
Reply::ActorSpawned { actor_ref } => actor_ref,
|
2026-11-02 12:44:33 +00:00
|
|
|
other => panic!("unexpected reply: {other:?}"),
|
|
|
|
|
};
|
2026-11-02 14:19:25 +00:00
|
|
|
assert_eq!(actor_ref.len(), 21);
|
|
|
|
|
assert!(!actor_ref.parse::<u64>().is_ok());
|
2026-11-02 12:44:33 +00:00
|
|
|
|
|
|
|
|
let sent = server.dispatch(request(Command::Send {
|
2026-11-02 14:19:25 +00:00
|
|
|
actor_ref: actor_ref.clone(),
|
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
|
|
|
message: test_message(0, 7, None),
|
2026-11-02 12:44:33 +00:00
|
|
|
}));
|
|
|
|
|
assert_eq!(sent.result, Ok(Reply::Acknowledged));
|
|
|
|
|
server.dispatch(request(Command::Tick {
|
|
|
|
|
ticks: 100,
|
|
|
|
|
instruction_budget: 10,
|
|
|
|
|
}));
|
|
|
|
|
|
2026-11-02 14:19:25 +00:00
|
|
|
let inspected = server.dispatch(request(Command::InspectActor {
|
|
|
|
|
actor_ref: actor_ref.clone(),
|
|
|
|
|
}));
|
2026-11-02 12:44:33 +00:00
|
|
|
match inspected.result.unwrap() {
|
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
|
|
|
Reply::Actor(snapshot) => assert_eq!(snapshot.state, Value::Integer(12)),
|
2026-11-02 12:44:33 +00:00
|
|
|
other => panic!("unexpected reply: {other:?}"),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let events = server.dispatch(request(Command::GetEvents));
|
|
|
|
|
match events.result.unwrap() {
|
|
|
|
|
Reply::Events(events) => assert!(events.iter().any(
|
2026-11-02 14:19:25 +00:00
|
|
|
|event| matches!(event, EventDto::Committed { actor_ref: id } if id == &actor_ref)
|
2026-11-02 12:44:33 +00:00
|
|
|
)),
|
|
|
|
|
other => panic!("unexpected reply: {other:?}"),
|
|
|
|
|
}
|
|
|
|
|
assert_eq!(
|
|
|
|
|
server.dispatch(request(Command::GetEvents)).result,
|
|
|
|
|
Ok(Reply::Events(Vec::new()))
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn dispatch_preserves_request_id_and_maps_validation_and_domain_errors() {
|
|
|
|
|
let mut server = Server::new();
|
|
|
|
|
let mut invalid = request(Command::GetEvents);
|
|
|
|
|
invalid.version += 1;
|
|
|
|
|
let response = server.dispatch(invalid);
|
|
|
|
|
assert_eq!(response.request_id, "test-request");
|
|
|
|
|
assert_eq!(
|
|
|
|
|
response.result.unwrap_err().code,
|
|
|
|
|
ErrorCode::UnsupportedVersion
|
|
|
|
|
);
|
|
|
|
|
|
2026-11-02 14:19:25 +00:00
|
|
|
let missing = server.dispatch(request(Command::InspectActor {
|
|
|
|
|
actor_ref: "000000000000000000000".to_owned(),
|
|
|
|
|
}));
|
2026-11-02 12:44:33 +00:00
|
|
|
assert_eq!(missing.result.unwrap_err().code, ErrorCode::NotFound);
|
|
|
|
|
|
|
|
|
|
publish(&mut server);
|
|
|
|
|
let conflict = server.dispatch(request(Command::PublishBehavior {
|
|
|
|
|
name: "counter".to_owned(),
|
|
|
|
|
expected_revision: Some(99),
|
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
|
|
|
source: "pub fn handle(state, message) { (state, []) }".to_owned(),
|
2026-11-02 12:44:33 +00:00
|
|
|
}));
|
|
|
|
|
assert_eq!(conflict.result.unwrap_err().code, ErrorCode::Conflict);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn dispatch_supports_behavior_history_source_and_safe_install() {
|
|
|
|
|
let mut server = Server::new();
|
|
|
|
|
let first = publish(&mut server);
|
|
|
|
|
let updated = server.dispatch(request(Command::PublishBehavior {
|
|
|
|
|
name: "counter".to_owned(),
|
|
|
|
|
expected_revision: Some(first.revision),
|
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
|
|
|
source: "pub fn handle(state, message) { (state * message.value, []) }".to_owned(),
|
2026-11-02 12:44:33 +00:00
|
|
|
}));
|
|
|
|
|
assert!(matches!(updated.result, Ok(Reply::Behavior(_))));
|
|
|
|
|
|
|
|
|
|
let source = server.dispatch(request(Command::GetBehaviorSource {
|
|
|
|
|
name: "counter".to_owned(),
|
|
|
|
|
revision: 1,
|
|
|
|
|
}));
|
|
|
|
|
assert_eq!(
|
|
|
|
|
source.result,
|
|
|
|
|
Ok(Reply::BehaviorSource {
|
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
|
|
|
source: "pub fn handle(state, message) { (state + message.value, []) }".to_owned()
|
2026-11-02 12:44:33 +00:00
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
|
2026-11-02 14:19:25 +00:00
|
|
|
let actor_ref = match server
|
2026-11-02 12:44:33 +00:00
|
|
|
.dispatch(request(Command::Spawn {
|
|
|
|
|
behavior_name: "counter".to_owned(),
|
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
|
|
|
state: Value::Integer(3),
|
2026-11-02 12:44:33 +00:00
|
|
|
mailbox_capacity: 1,
|
|
|
|
|
}))
|
|
|
|
|
.result
|
|
|
|
|
.unwrap()
|
|
|
|
|
{
|
2026-11-02 14:19:25 +00:00
|
|
|
Reply::ActorSpawned { actor_ref } => actor_ref,
|
2026-11-02 12:44:33 +00:00
|
|
|
other => panic!("unexpected reply: {other:?}"),
|
|
|
|
|
};
|
|
|
|
|
let installed = server.dispatch(request(Command::InstallBehavior {
|
2026-11-02 14:19:25 +00:00
|
|
|
actor_ref,
|
2026-11-02 12:44:33 +00:00
|
|
|
behavior_name: "counter".to_owned(),
|
|
|
|
|
}));
|
|
|
|
|
assert!(matches!(
|
|
|
|
|
installed.result,
|
|
|
|
|
Ok(Reply::BehaviorInstalled { .. })
|
|
|
|
|
));
|
|
|
|
|
}
|
2026-11-02 13:18:45 +00:00
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn json_lines_transport_dispatches_multiple_requests_and_accepts_final_line_without_newline() {
|
|
|
|
|
let publish_request = request(Command::PublishBehavior {
|
|
|
|
|
name: "counter".to_owned(),
|
|
|
|
|
expected_revision: None,
|
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
|
|
|
source: "pub fn handle(state, message) { (state + message.value, []) }".to_owned(),
|
2026-11-02 13:18:45 +00:00
|
|
|
});
|
|
|
|
|
let mut spawn_request = request(Command::Spawn {
|
|
|
|
|
behavior_name: "counter".to_owned(),
|
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
|
|
|
state: Value::Integer(4),
|
2026-11-02 13:18:45 +00:00
|
|
|
mailbox_capacity: 2,
|
|
|
|
|
});
|
|
|
|
|
spawn_request.request_id = "spawn-request".to_owned();
|
|
|
|
|
let mut list_request = request(Command::ListActors);
|
|
|
|
|
list_request.request_id = "list-request".to_owned();
|
|
|
|
|
let mut input = [publish_request, spawn_request]
|
|
|
|
|
.iter()
|
|
|
|
|
.map(serde_json::to_string)
|
|
|
|
|
.collect::<Result<Vec<_>, _>>()
|
|
|
|
|
.unwrap()
|
|
|
|
|
.join("\n");
|
|
|
|
|
input.push('\n');
|
|
|
|
|
input.push_str(&serde_json::to_string(&list_request).unwrap());
|
|
|
|
|
|
|
|
|
|
let mut output = Vec::new();
|
|
|
|
|
serve_json_lines(&mut Server::new(), Cursor::new(input), &mut output).unwrap();
|
|
|
|
|
|
|
|
|
|
let responses = output
|
|
|
|
|
.split(|byte| *byte == b'\n')
|
|
|
|
|
.filter(|line| !line.is_empty())
|
|
|
|
|
.map(serde_json::from_slice::<Response>)
|
|
|
|
|
.collect::<Result<Vec<_>, _>>()
|
|
|
|
|
.unwrap();
|
|
|
|
|
assert_eq!(responses.len(), 3);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
responses
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|response| response.request_id.as_str())
|
|
|
|
|
.collect::<Vec<_>>(),
|
|
|
|
|
vec!["test-request", "spawn-request", "list-request"]
|
|
|
|
|
);
|
|
|
|
|
assert!(matches!(responses[0].result, Ok(Reply::Behavior(_))));
|
2026-11-02 14:19:25 +00:00
|
|
|
assert!(matches!(
|
|
|
|
|
&responses[1].result,
|
|
|
|
|
Ok(Reply::ActorSpawned { actor_ref }) if actor_ref.len() == 21
|
|
|
|
|
));
|
2026-11-02 13:18:45 +00:00
|
|
|
assert!(matches!(
|
|
|
|
|
&responses[2].result,
|
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
|
|
|
Ok(Reply::Actors(actors)) if actors.len() == 1 && actors[0].state == Value::Integer(4)
|
2026-11-02 13:18:45 +00:00
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn json_lines_transport_rejects_malformed_and_oversized_input() {
|
|
|
|
|
let mut server = Server::new();
|
|
|
|
|
let malformed =
|
|
|
|
|
serve_json_lines(&mut server, Cursor::new(b"{not-json}\n"), Vec::new()).unwrap_err();
|
|
|
|
|
assert_eq!(malformed.kind(), io::ErrorKind::InvalidData);
|
|
|
|
|
|
|
|
|
|
let mut oversized = vec![b'a'; MAX_REQUEST_LINE_BYTES + 1];
|
|
|
|
|
oversized.push(b'\n');
|
|
|
|
|
let error = serve_json_lines(&mut server, Cursor::new(oversized), Vec::new()).unwrap_err();
|
|
|
|
|
assert_eq!(error.kind(), io::ErrorKind::InvalidData);
|
|
|
|
|
}
|
2026-11-02 13:50:30 +00:00
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn loopback_listener_rejects_non_loopback_addresses() {
|
|
|
|
|
let error = bind_loopback_listener("0.0.0.0:0".parse().unwrap()).unwrap_err();
|
|
|
|
|
assert_eq!(error.kind(), io::ErrorKind::InvalidInput);
|
|
|
|
|
|
|
|
|
|
let listener = bind_loopback_listener("127.0.0.1:0".parse().unwrap()).unwrap();
|
|
|
|
|
assert!(listener.local_addr().unwrap().ip().is_loopback());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
2026-11-03 14:36:21 +00:00
|
|
|
fn notification_contract_is_versioned_and_distinct_from_responses() {
|
|
|
|
|
let notification = runa_protocol::Notification {
|
|
|
|
|
version: PROTOCOL_VERSION,
|
|
|
|
|
event: runa_protocol::ImageEvent::ImageChanged { revision: 42 },
|
|
|
|
|
};
|
|
|
|
|
assert_eq!(
|
|
|
|
|
serde_json::to_value(notification).unwrap(),
|
|
|
|
|
serde_json::json!({
|
|
|
|
|
"version": 3,
|
|
|
|
|
"event": {"event": "image_changed", "data": {"revision": 42}}
|
|
|
|
|
})
|
|
|
|
|
);
|
2026-11-02 13:50:30 +00:00
|
|
|
}
|
2026-11-02 12:44:33 +00:00
|
|
|
}
|