The presented PlantUML diagrams outline the architecture and workflow of a collaborative document editor, referred to as the AppFlowy application. It highlights the roles of different components and how they interact to create, open, edit, and synchronize documents.
At its core, the AppFlowy application comprises three essential components: flowy-folder
, flowy-database
, and flowy-document
. Alongside these, the Collab
and CollabPlugins
components play pivotal roles in data management and synchronization, connecting the core components of AppFlowy to a variety of data storage and synchronization plugins.
Designed with flexibility in mind, AppFlowy is engineered to interface seamlessly with various databases via CollabPlugins
. At present, it supports RocksDB and Supabase. RocksDB was chosen for its high performance and availability, which makes it an excellent local storage solution.
The core components of the AppFlowy application interact with their corresponding elements within the Collab
component. This Collab
component then interfaces with the CollabPlugins
. The modularity of AppFlowy's architecture allows for its functionality to be extended through the integration of new plugins into the CollabPlugins
component.
For example, the integration of specific plugins could enable the storage of collaborative data in services like AWS or Firebase. Peer-to-peer synchronization could be made possible through the RealtimePlugin
, while the ContentIndexingPlugin
could be used to index the content of the collaboration, thereby supporting search functionality.
To illustrate how collaboration works within AppFlowy, let's walk through the process of creating a document. The other kind of collab object workflow is similar to this one, so we will not go through it in detail.
The creation of a new document in AppFlowy involves a series of steps, initiated by the user and facilitated by several components of the application.
- The user initiates the process by clicking on the 'Create Document' button.
flowy_folder
responds by creating a view with the specified document type.- Subsequently,
flowy_document
generates a document using the ID of the view. - These updates are then propagated to all plugins through
collab_document
. RocksdbDiskPlugin
captures these updates and saves them to the local disk.- Finally,
SupabaseDBPlugin
sends the updates to the server, ensuring that the document is stored and ready for collaboration.
Opening a document and keeping it in sync with the server is a multi-step process:
- User opens the document.
Collab
calls thedid_init
method of all plugins.SupabaseDBPlugin
sends an initial synchronization request to the server.- The server sends back an initial synchronization response, which is received by the
SupabaseDBPlugin
.
Editing a document is an interactive process that involves user action and several components of the system:
- User types 'abc'.
collab_document
creates an update containing 'abc'.- Updates get saved locally via
RocksdbDiskPlugin
. - Updates get pushed to send queue via
SupabaseDBPlugin
. - The updates are sent to the server in order.
The real-time synchronization of a document across different users involves the following steps:
- User1 types 'abc'.
Collab
creates an update containing 'abc'.SupabaseDBPlugin
sends the update to the server.- The server acknowledges the receipt of the update.
- The server broadcasts the update.
- Other users (User2, User3) receive the update.
- Users apply the update.
- UI is refreshed to reflect the updates.
Each collab object is using the collab
crate that is built on top of the yrs to build
its collaboration structure. Let's say we want to create a new collab object called UserAwareness
. It stores the data associated with user. For example, the appearance settings and the reminder setting. The UserAwareness
can
be synced between different devices for the given user_id.
The UserAwareness
contains the following fields, as shown below:
appearance_settings
is a yrs map that contains the user's appearance settings.reminders
is a yrs array that contains the user's reminders. Each reminder can be serialized or deserialized using the yrs map.
Now, let's explore how to create the UserAwareness
collab object.
pub struct UserAwareness {
container: MapRefWrapper,
appearance_settings: AppearanceSettings,
reminders: Reminders,
}
impl UserAwareness {
pub fn create(collab: Arc<MutexCollab>) -> Self {
let collab_guard = collab.lock();
let (container, appearance_settings, reminders) = collab_guard.with_transact_mut(|txn| {
let awareness = collab_guard.insert_map_with_txn_if_not_exist(txn, USER);
let appearance_settings_container =
awareness.insert_map_with_txn_if_not_exist(txn, APPEARANCE_SETTINGS);
let appearance_settings = AppearanceSettings {
container: appearance_settings_container,
};
let reminder_container =
awareness.insert_array_if_not_exist_with_txn::<Reminder>(txn, REMINDERS, vec![]);
let reminders = Reminders {
container: reminder_container,
};
(awareness, appearance_settings, reminders)
});
Self {
container,
appearance_settings,
reminders,
}
}
/// Adds a new reminder to the `UserAwareness` object.
///
/// # Arguments
///
/// * `reminder` - The `Reminder` object to be added.
pub fn add_reminder(&self, reminder: Reminder) {
self.reminders.add(reminder);
}
/// Removes an existing reminder from the `UserAwareness` object.
///
/// # Arguments
///
/// * `reminder_id` - A string reference to the ID of the reminder to be removed.
pub fn remove_reminder(&self, reminder_id: &str) {
self.reminders.remove(reminder_id);
}
/// Updates an existing reminder in the `UserAwareness` object.
///
/// # Arguments
///
/// * `reminder_id` - A string reference to the ID of the reminder to be updated.
/// * `f` - A function or closure that takes `ReminderUpdate` as its argument and implements the changes to the reminder.
pub fn update_reminder<F>(&self, reminder_id: &str, f: F)
where
F: FnOnce(ReminderUpdate),
{
self.reminders.update_reminder(reminder_id, f);
}
}
Utilizing the UserAwareness
object is simple and straightforward. A new UserAwareness
object can be instantiated by calling the create
method. Subsequently, reminders can be added, removed, or updated using the add_reminder
, remove_reminder
, and update_reminder
methods, respectively. Each UserAwareness
object is associated with a unique user ID, stored in Arc<MutexCollab>
.
In situations where a user logs into multiple devices using the same user ID, the UserAwareness
object is synchronized across all these devices. For instance, creating a new reminder triggers an update, which is then sent to the server. The server, in turn, broadcasts this update to all connected devices through the realtime service (WebSocket). Each device subsequently applies this update to its UserAwareness
object, ensuring consistency across all devices.
Moreover, changes in the Reminders
object can be subscribed to, enabling the user interface to refresh the reminder list whenever the Reminders
object undergoes a change.