2026-11-02 12:20:45 +00:00
|
|
|
//! Trusted, in-process application services for a running Runa image.
|
|
|
|
|
|
|
|
|
|
use std::num::NonZeroUsize;
|
|
|
|
|
use std::sync::Arc;
|
|
|
|
|
|
2026-11-02 14:19:25 +00:00
|
|
|
use runa_actor::{ActorRef, ActorSnapshot, Event, Message};
|
2026-11-02 12:20:45 +00:00
|
|
|
use runa_code::{BehaviorSnapshot, CodeStore};
|
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_core::{ImplementationId, Value};
|
2026-11-02 12:20:45 +00:00
|
|
|
use runa_runtime::{Runtime, RuntimeResult};
|
|
|
|
|
|
|
|
|
|
pub struct ImageApi {
|
|
|
|
|
code: CodeStore,
|
|
|
|
|
runtime: Runtime,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Default for ImageApi {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
Self::new()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ImageApi {
|
|
|
|
|
pub fn new() -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
code: CodeStore::default(),
|
|
|
|
|
runtime: Runtime::default(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn publish_behavior(
|
|
|
|
|
&mut self,
|
|
|
|
|
name: &str,
|
|
|
|
|
expected_revision: Option<u64>,
|
|
|
|
|
source: &str,
|
|
|
|
|
) -> RuntimeResult<BehaviorSnapshot> {
|
|
|
|
|
let artifact = self.code.compile(source)?;
|
|
|
|
|
self.runtime.publish(Arc::clone(&artifact))?;
|
|
|
|
|
self.code.publish_behavior(name, expected_revision, source)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn rollback_behavior(
|
|
|
|
|
&mut self,
|
|
|
|
|
name: &str,
|
|
|
|
|
expected_revision: u64,
|
|
|
|
|
target_revision: u64,
|
|
|
|
|
) -> RuntimeResult<BehaviorSnapshot> {
|
|
|
|
|
self.code
|
|
|
|
|
.rollback_behavior(name, expected_revision, target_revision)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn behavior(&self, name: &str) -> Option<BehaviorSnapshot> {
|
|
|
|
|
self.code.behavior(name)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn behaviors(&self) -> Vec<BehaviorSnapshot> {
|
|
|
|
|
self.code.behaviors()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn behavior_source(&self, name: &str, revision: u64) -> RuntimeResult<&str> {
|
|
|
|
|
let behavior = self.code.behavior(name).ok_or("unknown behavior")?;
|
|
|
|
|
let version = behavior
|
|
|
|
|
.history
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|version| version.revision == revision)
|
|
|
|
|
.ok_or("unknown behavior revision")?;
|
|
|
|
|
self.code
|
|
|
|
|
.artifact(version.implementation_id)
|
|
|
|
|
.map(|artifact| artifact.original_source())
|
|
|
|
|
.ok_or_else(|| "behavior artifact is unavailable".into())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn spawn(
|
|
|
|
|
&mut self,
|
|
|
|
|
behavior_name: &str,
|
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,
|
2026-11-02 12:20:45 +00:00
|
|
|
capacity: NonZeroUsize,
|
2026-11-02 14:19:25 +00:00
|
|
|
) -> RuntimeResult<ActorRef> {
|
2026-11-02 12:20:45 +00:00
|
|
|
let behavior = self
|
|
|
|
|
.code
|
|
|
|
|
.behavior(behavior_name)
|
|
|
|
|
.ok_or("unknown behavior")?;
|
|
|
|
|
self.runtime.spawn(behavior.current, state, capacity)
|
|
|
|
|
}
|
|
|
|
|
|
2026-11-02 14:19:25 +00:00
|
|
|
pub fn send(&mut self, actor: ActorRef, message: Message) -> RuntimeResult<()> {
|
2026-11-02 12:20:45 +00:00
|
|
|
self.runtime.send(actor, message)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn tick(&mut self, ticks: usize, budget: NonZeroUsize) {
|
|
|
|
|
for _ in 0..ticks {
|
|
|
|
|
self.runtime.tick(budget);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-11-02 14:19:25 +00:00
|
|
|
pub fn inspect(&self, actor: ActorRef) -> RuntimeResult<ActorSnapshot> {
|
2026-11-02 12:20:45 +00:00
|
|
|
self.runtime.inspect(actor)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn actors(&self) -> Vec<ActorSnapshot> {
|
|
|
|
|
self.runtime.actors()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn events(&mut self) -> Vec<Event> {
|
|
|
|
|
self.runtime.drain_events()
|
|
|
|
|
}
|
|
|
|
|
|
2026-11-02 14:19:25 +00:00
|
|
|
pub fn stop(&mut self, actor: ActorRef) -> RuntimeResult<()> {
|
2026-11-02 12:20:45 +00:00
|
|
|
self.runtime.stop(actor)
|
|
|
|
|
}
|
|
|
|
|
|
2026-11-02 14:19:25 +00:00
|
|
|
pub fn restart(&mut self, actor: ActorRef) -> RuntimeResult<()> {
|
2026-11-02 12:20:45 +00:00
|
|
|
self.runtime.restart(actor)
|
|
|
|
|
}
|
|
|
|
|
|
2026-11-02 14:19:25 +00:00
|
|
|
pub fn pause(&mut self, actor: ActorRef) -> RuntimeResult<()> {
|
2026-11-02 12:20:45 +00:00
|
|
|
self.runtime.pause(actor)
|
|
|
|
|
}
|
|
|
|
|
|
2026-11-02 14:19:25 +00:00
|
|
|
pub fn resume(&mut self, actor: ActorRef) -> RuntimeResult<()> {
|
2026-11-02 12:20:45 +00:00
|
|
|
self.runtime.resume(actor)
|
|
|
|
|
}
|
|
|
|
|
|
2026-11-02 14:19:25 +00:00
|
|
|
pub fn cancel(&mut self, actor: ActorRef) -> RuntimeResult<()> {
|
2026-11-02 12:20:45 +00:00
|
|
|
self.runtime.cancel(actor)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn install(
|
|
|
|
|
&mut self,
|
2026-11-02 14:19:25 +00:00
|
|
|
actor: ActorRef,
|
2026-11-02 12:20:45 +00:00
|
|
|
behavior_name: &str,
|
|
|
|
|
) -> RuntimeResult<ImplementationId> {
|
|
|
|
|
self.install_with_migration(actor, behavior_name, Ok)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn install_with_migration(
|
|
|
|
|
&mut self,
|
2026-11-02 14:19:25 +00:00
|
|
|
actor: ActorRef,
|
2026-11-02 12:20:45 +00:00
|
|
|
behavior_name: &str,
|
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
|
|
|
migrate: impl FnOnce(Value) -> RuntimeResult<Value>,
|
2026-11-02 12:20:45 +00:00
|
|
|
) -> RuntimeResult<ImplementationId> {
|
|
|
|
|
let behavior = self
|
|
|
|
|
.code
|
|
|
|
|
.behavior(behavior_name)
|
|
|
|
|
.ok_or("unknown behavior")?;
|
|
|
|
|
self.runtime
|
|
|
|
|
.install_with_migration(actor, behavior.current, migrate)?;
|
|
|
|
|
Ok(behavior.current)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[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
|
|
|
fn test_message(kind: i64, value: i64, target: Option<runa_core::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:20:45 +00:00
|
|
|
fn capacity(n: usize) -> NonZeroUsize {
|
|
|
|
|
NonZeroUsize::new(n).expect("capacity must be positive")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn image_api_publishes_spawns_inspects_and_exposes_events() -> RuntimeResult<()> {
|
|
|
|
|
let mut image = ImageApi::new();
|
|
|
|
|
let behavior = image.publish_behavior(
|
|
|
|
|
"counter",
|
|
|
|
|
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
|
|
|
"pub fn handle(state, message) { (state + message.value, []) }",
|
2026-11-02 12:20:45 +00:00
|
|
|
)?;
|
|
|
|
|
assert_eq!(
|
|
|
|
|
image.behavior_source("counter", behavior.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
|
|
|
"pub fn handle(state, message) { (state + message.value, []) }"
|
2026-11-02 12:20:45 +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
|
|
|
let actor = image.spawn("counter", Value::Integer(5), capacity(2))?;
|
|
|
|
|
image.send(actor, test_message(0, 7, None))?;
|
2026-11-02 12:20:45 +00:00
|
|
|
image.tick(100, capacity(10));
|
|
|
|
|
|
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
|
|
|
assert_eq!(image.inspect(actor)?.state, Value::Integer(12));
|
2026-11-02 12:20:45 +00:00
|
|
|
assert_eq!(image.inspect(actor)?.implementation, behavior.current);
|
|
|
|
|
assert!(
|
|
|
|
|
image
|
|
|
|
|
.events()
|
|
|
|
|
.iter()
|
2026-11-02 14:19:25 +00:00
|
|
|
.any(|event| matches!(event, Event::Committed(id) if id == &actor))
|
2026-11-02 12:20:45 +00:00
|
|
|
);
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn image_api_rejects_stale_behavior_updates_without_changing_current() -> RuntimeResult<()> {
|
|
|
|
|
let mut image = ImageApi::new();
|
|
|
|
|
let first = image.publish_behavior(
|
|
|
|
|
"counter",
|
|
|
|
|
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
|
|
|
"pub fn handle(state, message) { (state + message.value, []) }",
|
2026-11-02 12:20:45 +00:00
|
|
|
)?;
|
|
|
|
|
assert!(
|
|
|
|
|
image
|
|
|
|
|
.publish_behavior(
|
|
|
|
|
"counter",
|
|
|
|
|
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
|
|
|
"pub fn handle(state, message) { (state - message.value, []) }"
|
2026-11-02 12:20:45 +00:00
|
|
|
)
|
|
|
|
|
.is_err()
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(image.behavior("counter").unwrap().current, first.current);
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn image_api_installs_behavior_with_atomic_state_migration() -> RuntimeResult<()> {
|
|
|
|
|
let mut image = ImageApi::new();
|
|
|
|
|
let first = image.publish_behavior(
|
|
|
|
|
"counter",
|
|
|
|
|
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
|
|
|
"pub fn handle(state, message) { (state + message.value, []) }",
|
2026-11-02 12:20:45 +00:00
|
|
|
)?;
|
|
|
|
|
image.publish_behavior(
|
|
|
|
|
"counter",
|
|
|
|
|
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
|
|
|
"pub fn handle(state, message) { (state * message.value, []) }",
|
2026-11-02 12:20:45 +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
|
|
|
let actor = image.spawn("counter", Value::Integer(5), capacity(2))?;
|
2026-11-02 12:20:45 +00:00
|
|
|
let implementation = image.install_with_migration(actor, "counter", |state| {
|
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
|
|
|
let Value::Integer(state) = state else {
|
|
|
|
|
return Err("expected integer".into());
|
|
|
|
|
};
|
|
|
|
|
Ok(Value::Integer(
|
|
|
|
|
state.checked_mul(10).ok_or("migration overflow")?,
|
|
|
|
|
))
|
2026-11-02 12:20:45 +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
|
|
|
assert_eq!(image.inspect(actor)?.state, Value::Integer(50));
|
2026-11-02 12:20:45 +00:00
|
|
|
assert_eq!(image.inspect(actor)?.implementation, implementation);
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
}
|