Crate flo_scene

Crate flo_scene 

Source
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§

commands
error
programs
uuid_impl

Structs§

BlockedStream
A struct that unblocks an input stream when dropped
DisconnectedSerializationContext
Serialization context that has no connection behind it (just returns errors when trying to send or receive streams or functions)
FilterHandle
A filter is a way to convert from a stream of one message type to another, and a filter handle references a predefined filter.
InputStream
An input stream for a subprogram
InputStreamBlocker
An input stream blocker is used to disable input to an input stream temporarily
OutputSink
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.
SceneContext
The scene context is a per-subprogram way to access output streams
SceneWithSerializer
A scene being initialised with a serializer
SerializedMessage
A message created by serializing another message
StaticSubProgramId
A static subprogram ID can be used to declare a subprogram ID in a static variable
StreamId
Identifies a stream produced by a subprogram
SubProgramId
A unique identifier for a subprogram in a scene

Enums§

ConnectionError
Errors that can occur when trying to connect two subprograms in a scene
ConnectionResult
The possible outcomes of a successful connection request
SceneSendError
Error that occurs while sending to a stream
SerializationId
Identifies a serialized resource
SerializedStreamTarget
Targets for a serialized stream
StreamSource
Describes the source of a stream
StreamTarget
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.
CommandExt
Extension functions that are implemented in terms of the standard command interface
FilterHandleExt
SceneGuestMessage
Trait implemented by messages that can be sent via a scene, or a guest of a scene
SceneInitialisationContext
The initialisation context is used when setting up scenes and messages within scenes: it provides routines for creating and connecting programs.
SceneMessage
Trait implemented by messages that can be sent via a scene
SerializationContext
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§

RemoteCallbackFn
Remote callback functions are used to trigger a callback on the remote side of a connection