#distributed-database #distributed #p2p #database #sdk #bluetooth

dittolive-ditto

Ditto is a peer to peer cross-platform database that allows mobile, web, IoT and server apps to sync with or without an internet connection

121 releases (74 stable)

new 4.9.1-rc.1 Dec 9, 2024
4.9.0 Nov 21, 2024
4.9.0-rc.2 Oct 29, 2024
4.8.0-rc.1 Jul 11, 2024
1.0.0-alpha9 Mar 24, 2021

#14 in Database implementations

Download history 3171/week @ 2024-08-19 3042/week @ 2024-08-26 3963/week @ 2024-09-02 3242/week @ 2024-09-09 1748/week @ 2024-09-16 3523/week @ 2024-09-23 2248/week @ 2024-09-30 2348/week @ 2024-10-07 2024/week @ 2024-10-14 1917/week @ 2024-10-21 1508/week @ 2024-10-28 1538/week @ 2024-11-04 759/week @ 2024-11-11 1388/week @ 2024-11-18 1483/week @ 2024-11-25 1788/week @ 2024-12-02

5,458 downloads per month

Custom license

565KB
9K SLoC

What is Ditto?

Ditto is a cross-platform, peer-to-peer database that allows apps to sync with and without internet connectivity.

Install Ditto into your application, then use the APIs to read and write data into its storage system, and it will then automatically sync any changes to other devices.

Unlike other synchronization solutions, Ditto is designed for "peer-to-peer" scenarios where it can directly communicate with other devices even without an Internet connection.

Additionally, Ditto automatically manages the complexity of using multiple network transports, like Bluetooth, P2P Wi-Fi, and Local Area Network, to find and connect to other devices and then synchronize any changes.

Ditto Platform Docs

Visit https://docs.ditto.live to learn about the full Ditto platform, including multi-language SDKs, the Ditto Cloud offering, and more.

Rust developers should be sure to check out these essential topics:

Playground Quickstart

Ditto offers a "playground" mode that lets you start playing and developing with Ditto without any authentication hassle.

use std::sync::Arc;
use dittolive_ditto::prelude::*;

fn main() -> anyhow::Result<()> {
    let app_id = AppId::from_env("DITTO_APP_ID")?;
    let playground_token = std::env::var("DITTO_PLAYGROUND_TOKEN")?;
    let cloud_sync = true;
    let custom_auth_url = None;

    // Initialize Ditto
    let ditto = Ditto::builder()
        .with_root(Arc::new(PersistentRoot::from_current_exe()?))
        .with_identity(|ditto_root| {
            identity::OnlinePlayground::new(
                ditto_root,
                app_id,
                playground_token,
                cloud_sync,
                custom_auth_url,
            )
        })?
        .build()?;

    // Start syncing with peers
    ditto.start_sync()?;

    Ok(())
}

Write data using Ditto Query Language (DQL)

The preferred method to write data to Ditto is by using DQL. To do this, we'll first access the Ditto [Store][store], then execute a DQL insert statement.

use dittolive_ditto::prelude::*;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
struct Car {
    color: String,
    make: String,
}

async fn dql_insert_car(ditto: &Ditto, car: &Car) -> anyhow::Result<()> {
    let store = ditto.store();
    let query_result = store.execute(
        // `cars` is the collection name
        "INSERT INTO cars DOCUMENTS (:newCar)",
        Some(serde_json::json!({
            "newCar": car
        }).into())
    ).await?;

    // Optional: See the count of items inserted
    let item_count = query_result.item_count();

    // Optional: Inspect each item that was inserted
    for query_item in query_result.iter() {
        println!("Inserted: {}", query_item.json_string());
    }

    Ok(())
}

// To call:
async fn call_dql_insert(ditto: Ditto) -> anyhow::Result<()> {
    let my_car = Car {
        color: "blue".to_string(),
        make: "ford".to_string(),
    };
    dql_insert_car(&ditto, &my_car).await?;
    Ok(())
}
# Cargo.toml
[dependencies]
serde = { version = "1.0.204", features = ["derive"] }
serde_json = "1.0.120"

Read data using DQL

use dittolive_ditto::prelude::*;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
struct Car {
    color: String,
    make: String,
}

async fn dql_select_cars(ditto: &Ditto, color: &str) -> anyhow::Result<Vec<Car>> {
    let store = ditto.store();
    let query_result = store.execute(
        "SELECT * FROM cars WHERE color = :myColor",
        Some(serde_json::json!({
            "myColor": color
        }).into())
    ).await?;

    let cars = query_result.iter()
        .map(|query_item| query_item.deserialize_value::<Car>())
        .collect::<Result<Vec<Car>, _>>()?;

    Ok(cars)
}

// To call:
async fn call_dql_select(ditto: Ditto) -> anyhow::Result<()> {
    let cars: Vec<Car> = dql_select_cars(&ditto, "blue").await?;
    Ok(())
}

Notes about using a Rust-wrapped C library (advanced)

Please note: this crate uses sane defaults that should "just work". These notes should not be required to get started with Ditto, instead they're meant for people interested in more advanced use-cases such as dynamically linking or pre-downloading the companion C-library artifact.

Ditto's core functionality is released and packaged as a C library, which is then imported into Rust via the ::dittolive-ditto-sys crate.

Downloading the companion binary artifact

This crate will, at build time, download the appropriate binary artifact from https://software.ditto.live/rust/Ditto/<version>/<target>/release/[lib]dittoffi.{a,so,dylib,dll,lib}

If you wish to avoid this, you will have to do it yourself:

  1. Download the proper binary artifact;

  2. Instruct ::dittolive-ditto-sys' build.rs script to use it by setting the DITTOFFI_SEARCH_PATH environment variable appropriately (using an absolute path is recommended).

More precisely, the library search resolution order is as follows:

  1. $DITTOFFI_SEARCH_PATH (if set)
  2. $OUT_DIR (e.g. ${CARGO_TARGET_DIR}/<profile>/build/dittolive-ditto-sys-.../out)
  3. the current working directory ($PWD)
  4. $CARGO_TARGET_DIR
  5. Host built-in defaults (e.g. /usr/lib, /lib, /usr/local/lib, $HOME/lib) controlled by (system) linker setup.

If the library artifact is not found at any of these locations, the build script will attempt its own download into the $OUT_DIR (and use that path).

Linking

C linkage is typically accompanied with some idiosyncrasies, such as symbol conflicts, or path resolution errors.

The first question you should answer is whether you want your application to use static or dynamic linking to access the Ditto library.

This happens whenever the LIBDITTO_STATIC is explicitly set to 1, or unset.

If you have a special path to the libdittoffi.a/dittoffi.lib file (on Unix and Windows, respectively), then you can use the DITTOFFI_SEARCH_PATH env var to point to its location (using an absolute path), at linkage time (during cargo build exclusively).

Dynamically linking (advanced)

You can opt into this behavior by setting the LIBDITTO_STATIC=0 environment variable. When opting into this, you will have to handle library path resolution to the libdittoffi.so/libdittoffi.dylib/dittoffi.dll file (on Linux, macOS, and Windows, respectively).

That is, whilst the DITTOFFI_SEARCH_PATH is still important to help the cargo build / linkage step resolve the dynamic library, the actual usage of this file happens at runtime, when the (Rust) binary using ::dittolive_ditto is executed.

It is thus advisable to install the C dynamic library artifact under one of the system folders, such as /usr/lib or whatnot on Unix.

Otherwise, you would have to either:

  • meddle with link-time flags to set OS-specific loader metadata in the binary, such as the R{,UN}PATH / install_path, paying special attention to the choice of absolute paths, binary-relative paths (such as $ORIGIN/... on Linux), or even working-directory-relative paths, or

  • use env vars directives for the system dynamic loader, such as DYLD_FALLBACK_LIBRARY_PATH on macOS, or LD_LIBRARY_PATH on Linux.

(For the technically-savy, on macOS, the install_path of our .dylib artifact is set to $rpath/libdittoffi.dylib).

Dependencies

~11–19MB
~246K SLoC