Expand description
§flo_scene
flo_scene is a crate that provides a way to build large pieces of software by combining smaller ‘sub-programs’ that communicate via messages. This simplifies dependencies over more traditional object-oriented techniques, as sub-programs do not need to directly depend on each other. There are also benefits in terms of testing, code re-use and configurability.
flo_scene has some companion crates: flo_scene_pipe adds sockets for inter-process communication
and a command interface for interacting directly with the scene. flo_scene_wasm allows components
compiled as wasm to be loaded into a scene. flo_scene_guest can be used to write such components.
§Basic usage
Scenes are created using Scene::default() or Scene::empty(). The empty scene contains no
subprograms by default but the default scene contains some default ones, in particular a
control program that can be used to start other programs or define connections between programs.
use flo_scene::*;
let scene = Scene::default();Sub-programs read from a single input stream of messages and can write to any number of output
streams. Each output stream can be connected to the input of another program, and these connections
can be specified independently of the programs themselves. Messages need to implement the
SceneMessage trait, and subprograms can be added to a scene using the add_subprogram() function.
// Simple logger
#[derive(Debug, Serialize, Deserialize)]
pub enum LogMessage {
Info(String),
Warning(String),
Error(String)
}
impl SceneMessage for LogMessage { }
let log_program = SubProgramId::called("Logger");
scene.add_subprogram(log_program,
|mut log_messages: InputStream<LogMessage>, _context| async move {
while let Some(log_message) = log_messages.next().await {
match log_message {
LogMessage::Info(msg) => { println!("INFO: {:?}", msg); }
LogMessage::Warning(msg) => { println!("WARNING: {:?}", msg); }
LogMessage::Error(msg) => { println!("ERROR: {:?}", msg); }
}
}
},
10)Connections can be defined by the connect_programs() function. Note how this means that something
that generates log messages does not need to know their destination and that it’s possible to change
how something is logging at run-time if needed.
// Connect any program that writes log messages to our log program
// '()' means 'any source' here, it's possible to define connections on a per-subprogram basis if needed.
scene.connect_programs((), log_program, StreamId::with_message_type::<LogMessage>()).unwrap();Subprograms have a context that can be used to retrieve output streams, or send single messages, so after this connection is set up, anything can send log messages.
let test_program = SubProgramId::new();
scene.add_subprogram(test_program,
|_: InputStream<()>, context| async move {
// '()' means send to any target
let mut logger = context.send::<LogMessage>(()).unwrap();
// Will send to the logger program
logger.send(LogMessage::Warning("Hello".to_string())).await.unwrap();
},
0);Once set up, the scene needs to be run, in the async context of your choice:
executor::block_on(async {
scene.run_scene().await;
});A single scene can be run in multiple threads if needed and subprograms are naturally able to run asynchronously as they communicate with messages rather than by direct data access.
§A few more advanced things
When the scene is created with Scene::default(), a control program is present that allows
subprograms to start other subprograms or create connections:
/* ... */
context.send_message(SceneControl::start_program(new_program_id, |input, context| /* ... */, 10)).await.unwrap();
context.send_message(SceneControl::connect(some_program, some_other_program, StreamId::for_message_type::<MyMessage>())).await.unwrap();The empty scene does not get this control program (it’s possible to use the Scene struct directly though).
Filters make it possible to connect two subprograms that take different message types by transforming them. They need to be registered, then they can be used as a stream target:
// Note that the stream ID identifies the *source* stream: there's only one input stream for any program
let mine_to_yours_filter = FilterHandle::for_filter(|my_messages: InputStream<MyMessage>| my_messages.map(|msg| YourMessage::from(msg)));
scene.connect(my_program, StreamTarget::Filtered(mine_to_yours_filter, your_program), StreamId::with_message_type::<MyMessage>()).unwrap();The message type has some functions that can be overridden to provide some default behaviour which can remove the need to manually configure connections whenever a scene is created:
impl SceneMessage for MyMessage {
fn default_target() -> StreamTarget {
StreamTarget::Program(SubProgramId::called("MyMessageHandler"))
}
}A program can ‘upgrade’ its input stream to annotate the messages with their source if it needs this information:
scene.add_subprogram(log_program,
|log_messages: InputStream<LogMessage>, _context| async move {
let mut log_messages = log_messages.messages_with_sources();
while let Some((source, log_message)) = log_messages.next().await {
match log_message {
LogMessage::Info(msg) => { println!("INFO: [{:?}] {:?}", source, msg); }
LogMessage::Warning(msg) => { println!("WARNING: [{:?}] {:?}", source, msg); }
LogMessage::Error(msg) => { println!("ERROR: [{:?}] {:?}", source, msg); }
}
}
},
10)It is possible to request a stream directly to a particular program:
let mut logger = context.send::<LogMessage>(SubProgramId::called("MoreSpecificLogger"));But it’s also possible to redirect these with a connection request:
// The scene is the ultimate arbiter of who can talk to who, so if we don't want our program talking to the MoreSpecificLogger after all we can change that
// Take care as this can get confusing!
scene.connect(exception_program, standard_logger_program, StreamId::with_message_type::<LogMessage>().for_target(SubProgramId::called("MoreSpecificLogger")));You can create and run more than one Scene at once if needed.
Modules§
Structs§
- Blocked
Stream - A struct that unblocks an input stream when dropped
- Disconnected
Serialization Context - Serialization context that has no connection behind it (just returns errors when trying to send or receive streams or functions)
- Filter
Handle - A filter is a way to convert from a stream of one message type to another, and a filter handle references a predefined filter.
- Input
Stream - An input stream for a subprogram
- Input
Stream Blocker - An input stream blocker is used to disable input to an input stream temporarily
- Output
Sink - An output sink is a way for a subprogram to send messages to the input of another subprogram
- Scene
- A scene represents a set of running co-programs, creating a larger self-contained piece of software out of a set of smaller pieces of software that communicate via streams.
- Scene
Context - The scene context is a per-subprogram way to access output streams
- Scene
With Serializer - A scene being initialised with a serializer
- Serialized
Message - A message created by serializing another message
- Static
SubProgram Id - A static subprogram ID can be used to declare a subprogram ID in a static variable
- Stream
Id - Identifies a stream produced by a subprogram
- SubProgram
Id - A unique identifier for a subprogram in a scene
Enums§
- Connection
Error - Errors that can occur when trying to connect two subprograms in a scene
- Connection
Result - The possible outcomes of a successful connection request
- Scene
Send Error - Error that occurs while sending to a stream
- Serialization
Id - Identifies a serialized resource
- Serialized
Stream Target - Targets for a serialized stream
- Stream
Source - Describes the source of a stream
- Stream
Target - A stream target describes where the output of a particular stream should be sent
Traits§
- Command
- Commands are spawnable tasks that carry out actions on behalf of a parent subprogram. A command can send multiple messages to different targets and also can return a ‘standard’ output stream to to the subprogram that spawned it.
- Command
Ext - Extension functions that are implemented in terms of the standard command interface
- Filter
Handle Ext - Scene
Guest Message - Trait implemented by messages that can be sent via a scene, or a guest of a scene
- Scene
Initialisation Context - The initialisation context is used when setting up scenes and messages within scenes: it provides routines for creating and connecting programs.
- Scene
Message - Trait implemented by messages that can be sent via a scene
- Serialization
Context - The serialization context can be used to pass things like streams and function calls across a serialization boundary. This generally requires some kind of backing protocol (the guest protocol being the main one that’s used), as these are effectively ways to generate new connections or cause callbacks in a remote environment.
Functions§
- create_
default_ serializer_ filters - Creates the default serializer filters for a scene message
- install_
serializable_ type - Creates the data structures needed to serialize a particular type
- scene_
context - Returns the scene context set for the current thread
- serialization_
function - Returns a serialization function for changing a source type into a target type
- serializer_
filter - If installed, returns the filters to use to convert from a source type to a target type
- with_
scene_ context - Performs an action with the specified context set as the thread context
Type Aliases§
- Remote
Callback Fn - Remote callback functions are used to trigger a callback on the remote side of a connection