samod_core/lib.rs
1//! # `samod-core`
2//!
3//! This crate provides a sans-IO implementation of networking and storage
4//! protocols for synchronizing [`automerge`] docuemnts which is compatible with
5//! the `@automerge/automerge-repo` JavaScript package. `samod-core` is intended
6//! to be used via FFI and wrapped in a language-specific runtime which provides
7//! a more ergonomic API. See the `samod` crate for an example of this in Rust.
8//!
9//! ## Overview
10//!
11//! Automerge documents are in-memory data structures which represent the
12//! editing history of a JSON-like document.  `automerge` provides functionality
13//! for saving and loading files from storage and for synchronizing over the
14//! network, but it does not specify a wire protocol for doing this. `samod-core`
15//! provides a wire protocol and storage convention for storing and synchronizing
16//! many automerge documents.
17//!
18//! ## Actors
19//!
20//! Processing tasks for a document can be compute intensive, which means that
21//! we want to be able to take advantage of parallelism where we can.
22//! `samod-core` doesn't make any assumptions about the runtime though, so it
23//! doesn't start threads or spawn tasks. Instead, `samod-core` provides an
24//! actor based model. There are two kinds of actors:
25//!
26//! * [`Hub`](crate::actors::hub::Hub) - The central actor which manages a set of connected peers
27//! * [`DocumentActor`](crate::actors::document::DocumentActor) - An actor which is created for each document
28//!
29//! In both cases the actors expose an API wherein you pass some kind of event
30//! data structure to the actor representing something that happened and you
31//! get back a data structure describing "effects" that need to take place -
32//! retrieving things from storage or sending things to connected peers for
33//! example.
34//!
35//! The hub actor exposes an API for dispatching commands which manage the state
36//! of the running actors, whilst the document actor exposes an API for accessing
37//! and modifying the state of the document. Typical workflows then will involve
38//! creating a hub actor, then using commands to connect other peers, and create
39//! or find documents. Creating and finding a document will give you a [`DocumentActorId`]
40//! which can then be used to find the actor corresponding to a particular document
41//! and interact with the document.
42//!
43//! ## Typical Workflow
44//!
45//! A typical workflow for using this library will involve three stages:
46//!
47//! * Load the [`Hub`](crate::actors::hub::Hub) actor
48//! * Run a control loop which passes events to the
49//!   [`Hub`](crate::actors::hub::Hub) actor and handles effects requested by the
50//!   hub actor (including spawning document actors)
51//! * At some point the hub actor will receive a stop
52//!   command and stop all the actors before returning a
53//!   [`HubResults`](crate::actors::hub::HubResults) which has
54//!   `HubResults::stopped == true` at which point we can exit the loop
55//!
56//! ### Loading the Hub Actor
57//!
58//! Before the [`Hub`](crate::actors::hub::Hub) actor can even be up and running
59//! it needs to load some things from storage. This is represented by the
60//! [`SamodLoader`] type. To load the hub actor you first create a
61//! [`SamodLoader`], and then perform the IO tasks it requires until it finishes
62//! loading, at which point you have a [`Hub`](crate::actors::hub::Hub) actor.
63//!
64//! ### The Control Loop
65//!
66//! This is the main loop of the samod application. It's not quite a single
67//! loop because there is actually a loop per actor, we can describe this
68//! as one loop which manages the hub actor, and one which manages each
69//! document.
70//!
71//! #### Hub Loop
72//!
73//! The hub loop waits for new "events" to pass to the hub as a
74//! [`HubEvent`](crate::actors::hub::HubEvent). Events here are either
75//!
76//! a) New commands from the application (create a document, find a document etc.)
77//! b) Completed storage IO operations
78//! c) Network events (a message was received, a connection was created)
79//!
80//! The loop looks like this then:
81//!
82//! * Wait for an event
83//! * Create a new [`HubEvent`](crate::actors::hub::HubEvent) corresponding to
84//!   the incoming event
85//! * If the event is a command, note down the command ID returned from the
86//!   [`HubEvent`](crate::actors::hub::HubEvent) constructor
87//! * Pass the event to [`Hub::handle_event`](crate::actors::hub::Hub::handle_event)
88//! * Examine the returned [`HubResults`](crate::actors::hub::HubResults)
89//!   * If any command is completed (in
90//!     [`HubResults::completed_commands`](crate::actors::hub::HubResults::completed_commands))
91//!     note the result of the command completion to notify the application
92//!     of the result
93//!   * Dispatch any new storage or network events
94//!   * If any document actors need to be spawned, somehow enqueue spawning
95//!     of the actor
96//!   * Route any messages from the hub to document actors to the inboxes of
97//!     the document actors
98//!
99//! #### Document Actor Loop
100//!
101//! The document actor loop is a bit different to the hub actor loop because it
102//! doesn't need to handle commands. However, there is the additional
103//! complication that the application will typically need to interact with the
104//! automerge document the actor manages. This means that the document actor
105//! does not have a single `handle_event` method. Instead, there are three
106//! methods which are used to interact with the actor:
107//!
108//! * [`DocumentActor::handle_message`](crate::actors::document::DocumentActor::handle_message) which is used to handle a message from
109//!   the hub actor
110//! * [`DocumentActor::handle_io_complete`](crate::actors::document::DocumentActor::handle_io_complete) which is used to handle completed
111//!   storage requests
112//! * [`DocumentActor::with_document`](crate::actors::document::DocumentActor::with_document) which is passed a closure that has access
113//!   to the document
114//!
115//! The "loop" for a document actor then often looks a bit different to the hub
116//! loop. Typically the document actor is inside a mutex which holds both the
117//! actor, and a reference to whatever channel is used to communicate with the
118//! hub actor and IO. The ways we interact with the actor are then twofold,
119//! firstly in a control loop waiting for messages from the hub and IO, and
120//! secondly in some kind of lock on the mutex which the application uses.
121//!
122//! In the loop that would be something like this:
123//!
124//! * Wait for message from the hub or completed IO
125//! * Lock the mutex
126//! * Call either [`DocumentActor::handle_message`](crate::actors::document::DocumentActor::handle_message) or
127//!   [`DocumentActor::handle_io_complete`](crate::actors::document::DocumentActor::handle_io_complete)
128//! * Check the returned [`DocActorResult`](crate::actors::document::DocActorResult) and dispatch any new IO
129//! * Unlock the mutex
130//!
131//! Whilst the application driven interactions would be something like this:
132//!
133//! * Lock the mutex
134//! * Call [`DocumentActor::with_document`](crate::actors::document::DocumentActor::with_document) with a closure that performs the desired action
135//! * Check the returned [`DocActorResult`](crate::actors::document::DocActorResult) and dispatch any new IO
136//! * Unlock the mutex
137//!
138//! ## Commands
139//!
140//! When you want to perform some kind of action on the hub actor you create a
141//! [`HubEvent`](crate::actors::hub::HubEvent) and pass it to
142//! [`Hub::handle_event`](crate::actors::hub::Hub::handle_event). Some kinds of
143//! event are "commands", these are created via static methods on the
144//! [`HubEvent`](crate::actors::hub::HubEvent) and will return a
145//! [`DispatchedCommand`](crate::actors::hub::DispatchedCommand) which includes
146//! both a [`HubEvent`](crate::actors::hub::HubEvent) to pass to
147//! [`Hub::handle_event`](crate::actors::hub::Hub::handle_event) and a
148//! [`CommandId`]. At some point in the future the
149//! [`HubResults`](crate::actors::hub::HubResults) returned by
150//! [`Hub::handle_event`](crate::actors::hub::Hub::handle_event) will contain
151//! the result of the given command. One example of this would be
152//! [`HubEvent::create_document`](crate::actors::hub::HubEvent::create_document)
153//! which returns a command ID which will be marked as completed when the
154//! document is created and saved to storage.
155//!
156//! ## IO
157//!
158//! There are two kinds of IO which `samod-core` performs - interacting with
159//! storage and sending messages over the network. In both cases there is some
160//! kind of task which `samod-core` requests completion of, the runtime then
161//! performs the task and at a later date informs `samod-core` of the result. To
162//! track ongoing tasks we use the [`IoTask`](crate::io::IoTask) struct. This
163//! contains a [`IoTaskId`](crate::io::IoTaskId) which is used to identify the
164//! ongoing task and some kind of action to perform. When the task is completed
165//! the runtime constructs an [`IoResult`](crate::io::IoResult) using the
166//! original task ID and passes it to the actor which requested the task.
167//!
168//! ### Storage
169//!
170//! One very important part of IO is storage. `samod-core` assumes a very simple
171//! storage model. Storage is assumed to be a key-value store with range
172//! queries. The keys are lists of strings and the values are bytes. For
173//! example, a key might be `["documents", "1234567890abcdef"]` and the value
174//! might be the bytes of a document. Keys are represented by the [`StorageKey`]
175//! type and the operations which storage must support are represented by the
176//! [`StorageTask`](crate::io::StorageTask) type.
177//!
178//! ### Network Connections
179//!
180//! Network connections are assumed to be stream oriented and are represented by
181//! the [`ConnectionId`] type. You obtain a connection by passing a
182//! [`HubEvent::create_connection`](crate::actors::hub::HubEvent::create_connection)
183//! to the [`Hub`](crate::actors::hub::Hub) and waiting for the create command
184//! to complete, returning a [`ConnectionId`]. Messages can now be passed to the
185//! hub using [`HubEvent::receive`](crate::actors::hub::HubEvent::receive) and
186//! outbound messages will be found in
187//! [`HubResults::new_tasks`](crate::actors::hub::HubResults::new_tasks).
188//! Finally, the connection can be closed using
189//! [`HubEvent::connection_lost`](crate::actors::hub::HubEvent::connection_lost)
190//! or, if it is closed by some other event, the notification of that closure
191//! will be found in
192//! [`HubResults::connection_events`](crate::actors::hub::HubResults::connection_events).
193mod automerge_url;
194pub use actors::hub::{CommandId, CommandResult};
195pub use automerge_url::AutomergeUrl;
196pub mod actors;
197mod document_changed;
198mod document_id;
199mod ephemera;
200pub mod network;
201pub use network::ConnectionId;
202mod peer_id;
203
204pub use actors::document::{CompactionHash, DocumentActorId};
205pub use document_changed::DocumentChanged;
206pub use document_id::{BadDocumentId, DocumentId};
207pub mod io;
208pub use peer_id::{PeerId, PeerIdError};
209mod storage_key;
210pub use storage_key::{InvalidStorageKey, StorageKey};
211mod storage_id;
212pub use storage_id::{StorageId, StorageIdError};
213mod unix_timestamp;
214pub use unix_timestamp::UnixTimestamp;
215
216mod loader;
217pub use loader::{LoaderState, SamodLoader};