Events
AppFlowy's backend defines all the events and generates the event's foreign function interface that supports Dart and TS event call.
Events are emitted in the frontend and are processed in the backend. Each event has its own handler in the backend.This mechanism uses a Protobuf-RPC like protocol under the hood to serialize requests and responses, all arguments and return data must be serializable to Protobuf.
This article introduces how AppFlowy uses protobuf buffer to exchange the data between the frontend and backend. The pattern as shown below:
 
Different frontend uses the corresponding FFI interface to communicate with the backend. For example:
- Dart 
class UserEventSignIn {
     SignInPayloadPB request;
     UserEventSignIn(this.request);
    Future<Either<UserProfilePB, FlowyError>> send() {
    final request = FFIRequest.create()
          ..event = UserEvent.SignIn.toString()
          ..payload = requestToBytes(this.request);
    return Dispatch.asyncRequest(request)
        .then((bytesResult) => bytesResult.fold(
           (okBytes) => left(UserProfilePB.fromBuffer(okBytes)),
           (errBytes) => right(FlowyError.fromBuffer(errBytes)),
        ));
    }
}- TS 
export async function UserEventSignIn(payload: pb.SignInPayloadPB): Promise<Result<pb.UserProfilePB, pb.FlowyError>> {
    let args = {
        request: {
            ty: pb.UserEvent[pb.UserEvent.SignIn],
            payload: Array.from(payload.serializeBinary()),
        },
    };
    let result: { code: number; payload: Uint8Array } = await invoke("invoke_request", args);
    if (result.code == 0) {
        let object = pb.UserProfilePB.deserializeBinary(result.payload);
        return Ok(object);
    } else {
        let error = pb.FlowyError.deserializeBinary(result.payload);
        return Err(error);
    }
}So, just calling the corresponding function and then let the backend handle it. The result will be returned asynchronously.
Code Generate Process
Let's introduce the generating process step by step.
Step One - Definitions
We define the Event and the Protobuf data struct in Rust, for example, the DocumentEvent defined in event_map.rs and ExportDataPB defined in entities.rs.
// event_map.rs
#[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)]
#[event_err = "FlowyError"]
pub enum DocumentEvent {
    #[event(input = "OpenDocumentContextPB", output = "DocumentSnapshotPB")]
    GetDocument = 0,
    #[event(input = "EditPayloadPB")]
    ApplyEdit = 1,
    #[event(input = "ExportPayloadPB", output = "ExportDataPB")]
    ExportDocument = 2,
}The annotation, #[event(input = Input struct, output = Output struct)] is used to generate the event FFI function.
- Input structmean the function receive the input parameter's type.
- Output structmean the function's return value's type
I think you noticed that there is aPB keyword appended to every struct. We use the PB keyword to identify this struct is in protobuf format.
// rust-lib/flowy-document/src/entities.rs
#[derive(Default, ProtoBuf)]
pub struct ExportDataPB {
    // The annotation, index = 1, match the syntax that defines proto file.
    #[pb(index = 1)] 
    pub data: String,
    #[pb(index = 2)]
    pub export_type: ExportType,
}The procedural macro, ProtoBuf, is used to mark this struct is going to generate the protobuf struct.
We use the syn to collect the AST information that will be used to generate the
proto file. If you interest in how to collect the information in details, you should check out the Procedural Macros.
Step Two - Configurations
We use toml to control which files should be included when doing the code generation. It supports specify a single file or a folder.
proto_input = ["src/event_map.rs", "src/entities.rs"]
event_files = ["src/event_map.rs"]proto_input
The proto_input receives path or file. The code gen process will parse the proto_input in order to generate the struct/enum.
event_files
The event_files receives file that define the event. The code gen process will parse the file in order to generate the corresponding language event class. The event class name consists of the Enum name and the Enum value defined in event_map.rs.
Step Three - Build configuration
Build Scripts is the perfect way to do the code generation. Let's check out some pseudocode. We use features flag to control generate process. If the dart feature is on then the Dart event FFI functions will be generated.
// build.rs
fn main() {
    let crate_name = env!("CARGO_PKG_NAME");
    flowy_codegen::protobuf_file::gen(crate_name);
    #[cfg(feature = "dart")]
    flowy_codegen::dart_event::gen(crate_name);
    #[cfg(feature = "ts")]
    flowy_codegen::ts_event::gen(crate_name);
}Step Four - Code Gen on Build
The code gen process is embedded in the AppFlowy build process. But you can run the build process manually. Just go to the corresponding crate directory(For example, frontend/flowy-text-block), and run:
cargo build --features=dart
or if you want to check the verbose output.
cargo build -vv --features=dart
The build scripts will be run before the crate gets compiled. Thanks to the cargo toolchain, we use cargo:rerun-if-changed=PATH to enable the build.rs will only run if the files were changed.
The rerun-if-changed instruction tells Cargo to re-run the build script if the file at the given path has changed. Currently, Cargo only uses the filesystem last-modified timestamp to determine if the file has changed. It compares against an internal cached timestamp of when the build script last ran.
After running the build.rs, it generates files in Dart/TS and Rust protobuf files using the same proto files.
Dart (with dart feature on):
- dart_event.dart
The file is located in packages/appflowy_backend/lib/dispatch/dart_event/flowy-document.
TS (with ts feature on):
- event.ts
The file is located in appflowy_tauri/src/services/backend/events.
Message passing
Let's see how the message passing from the frontend to the backend. Let use dart for demonstration (It's the same in TS).
- Repository constructs the - DocumentEventExportDocumentclass, and call- send()function.
- Frontend's FFI serializes the event and the - ExportPayloadPBto bytes.
- The bytes were sent to Backend. 
- Backend's FFI deserializes the bytes into the corresponding - eventand- ExportPayloadPB.
- The dispatcher sends the - ExportPayloadPBto the crate that registers as the event handler.
- ExportPayloadPBwill try to parse into- ExportParams. It will return an error if there are illegal fields in it.- For example: the - view_idfield in the- ExportPayloadPBshould not be empty.
- Crate's - export_handlerfunction gets called with the event and data.
- At the end, - export_handlerwill return 'ExportDataPB', which will be post to the frontend.
Last updated
Was this helpful?

