More Sync Stuff (#438)

* broken sync example

* move to react

* filter owned ops more

* update deps

* working sync

* relation operations

* fix pnpm lock

* please rustfmt

* fix api

* compare strings properly

* please clippy

* feature gate logging

* use bleeding rspc in example

* use bleeding rspc

* use generated rspc client
This commit is contained in:
Brendan Allan
2022-10-28 14:12:28 +08:00
committed by GitHub
parent 168c77f203
commit ebddb873f0
72 changed files with 1988 additions and 557 deletions

BIN
Cargo.lock generated
View File

Binary file not shown.

View File

@@ -5,7 +5,7 @@ members = [
"crates/*",
# "crates/p2p/tunnel",
# "crates/p2p/tunnel/utils",
"crates/sync/example/src-tauri",
"crates/sync/example/api",
"apps/desktop/src-tauri",
"apps/mobile/rust",
"apps/server",
@@ -34,6 +34,6 @@ specta = { version = "0.0.4" }
# We use this patch so we can compile for the IOS simulator on M1
openssl-sys = { git = "https://github.com/spacedriveapp/rust-openssl", rev = "92c3dec225a9e984884d5b30a517e5d44a24d03b" }
rspc = { git = "https://github.com/oscartbeaumont/rspc", rev = "7c0a67c1176a8af33b604c68d8edcbf0d70b8429" } # TODO: Move back to crates.io when new jsonrpc executor is released
normi = { git = "https://github.com/oscartbeaumont/rspc", rev = "7c0a67c1176a8af33b604c68d8edcbf0d70b8429" } # TODO: When normi is released on crates.io
specta = { git = "https://github.com/oscartbeaumont/rspc", rev = "7c0a67c1176a8af33b604c68d8edcbf0d70b8429" } # TODO: When normi is released on crates.io
rspc = { git = "https://github.com/oscartbeaumont/rspc", rev = "73d60f9cb901661f9f0f8e953477b3558f2715d1" } # TODO: Move back to crates.io when new jsonrpc executor is released
normi = { git = "https://github.com/oscartbeaumont/rspc", rev = "73d60f9cb901661f9f0f8e953477b3558f2715d1" } # TODO: When normi is released on crates.io
specta = { git = "https://github.com/oscartbeaumont/rspc", rev = "73d60f9cb901661f9f0f8e953477b3558f2715d1" } # TODO: When normi is released on crates.io

View File

@@ -154,10 +154,8 @@ pub extern "system" fn Java_com_spacedrive_app_SDCore_handleCoreMsg(
"(Ljava/lang/Object;)V",
&[env
.new_string(
serde_json::to_string(
&resps.into_iter().filter_map(|v| v).collect::<Vec<_>>(),
)
.unwrap(),
serde_json::to_string(&resps.into_iter().flatten().collect::<Vec<_>>())
.unwrap(),
)
.expect("Couldn't create java string!")
.into()],

View File

@@ -20,10 +20,12 @@ async fn main() {
{
panic!("'$DATA_DIR' is not set ({})", _e)
}
std::env::current_dir()
.expect("Unable to get your current directory. Maybe try setting $DATA_DIR?")
.join("sdserver_data")
#[cfg(debug_assertions)]
{
std::env::current_dir()
.expect("Unable to get your current directory. Maybe try setting $DATA_DIR?")
.join("sdserver_data")
}
}
};

View File

@@ -21,7 +21,7 @@ pub(crate) fn mount() -> RouterBuilder {
t(|ctx, _: (), _| async move { Ok(ctx.jobs.get_running().await) })
})
.library_query("isRunning", |t| {
t(|ctx, _: (), _| async move { Ok(ctx.jobs.get_running().await.len() > 0) })
t(|ctx, _: (), _| async move { Ok(!ctx.jobs.get_running().await.is_empty()) })
})
.library_query("getHistory", |t| {
t(|_, _: (), library| async move { Ok(JobManager::get_history(&library).await?) })

View File

@@ -10,7 +10,7 @@ use tokio::{
sync::broadcast,
};
use tracing::{error, info};
use tracing_subscriber::{filter::LevelFilter, fmt, prelude::*, EnvFilter};
use tracing_subscriber::{prelude::*, EnvFilter};
pub mod api;
pub(crate) mod job;
@@ -37,11 +37,15 @@ pub struct Node {
event_bus: (broadcast::Sender<CoreEvent>, broadcast::Receiver<CoreEvent>),
}
#[cfg(debug_assertions)]
const CONSOLE_LOG_FILTER: LevelFilter = LevelFilter::DEBUG;
#[cfg(not(feature = "android"))]
const CONSOLE_LOG_FILTER: tracing_subscriber::filter::LevelFilter = {
use tracing_subscriber::filter::LevelFilter;
#[cfg(not(debug_assertions))]
const CONSOLE_LOG_FILTER: LevelFilter = LevelFilter::INFO;
match cfg!(debug_assertions) {
true => LevelFilter::DEBUG,
false => LevelFilter::INFO,
}
};
impl Node {
pub async fn new(data_dir: impl AsRef<Path>) -> Result<(Arc<Node>, Arc<Router>), NodeError> {
@@ -87,7 +91,7 @@ impl Node {
// ),
);
#[cfg(not(feature = "android"))]
let subscriber = subscriber.with(fmt::layer().with_filter(CONSOLE_LOG_FILTER));
let subscriber = subscriber.with(tracing_subscriber::fmt::layer().with_filter(CONSOLE_LOG_FILTER));
#[cfg(feature = "android")]
let subscriber = subscriber.with(tracing_android::layer("com.spacedrive.app").unwrap()); // TODO: This is not working
subscriber

View File

@@ -5,7 +5,8 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
rspc = { workspace = true, features = ["uuid"] }
rand = "0.8.5"
rspc = { workspace = true, features = ["uuid", "uhlc"] }
serde = "1.0.145"
serde_json = "1.0.85"
uhlc = "0.5.1"

View File

@@ -1,3 +0,0 @@
{
"printWidth": 80
}

View File

@@ -1,3 +0,0 @@
{
"recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"]
}

View File

@@ -1,7 +1,18 @@
# Tauri + Solid + Typescript
# Create rspc app
This template should help get you started developing with Tauri, Solid and Typescript in Vite.
This app was scaffolded using the [create-rspc-app](https://rspc.dev) CLI.
## Recommended IDE Setup
## Usage
- [VS Code](https://code.visualstudio.com/) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)
```bash
# Terminal One
cd web
pnpm i
pnpm dev
# Terminal Two
cd api/
cargo prisma generate
cargo prisma db push
cargo run
```

View File

@@ -0,0 +1,20 @@
[package]
name = "example-2"
default-run = "example-2"
version = "0.1.0"
edition = "2021"
rust-version = "1.64"
publish = false
[dependencies]
serde_json = "1.0.85"
serde = { version = "1.0.145", features = ["derive"] }
axum = "0.5.16"
rspc = { workspace = true, features = ["axum"] }
tokio = { version = "1.21.2", features = ["full"] }
# prisma-client-rust = { workspace = true }
dotenv = "0.15.0"
tower-http = { version = "0.3.4", features = ["cors"] }
sd-sync = { path = "../.." }
uuid = { version = "1.1.2", features = ["v4"] }
http = "0.2.8"

View File

@@ -0,0 +1,154 @@
use std::collections::HashMap;
use std::sync::Arc;
use rspc::*;
use sd_sync::*;
use serde_json::*;
use std::path::PathBuf;
use tokio::sync::Mutex;
use uuid::Uuid;
#[derive(Default)]
pub struct Ctx {
pub dbs: HashMap<Uuid, Db>,
}
type Router = rspc::Router<Arc<Mutex<Ctx>>>;
fn to_map(v: &impl serde::Serialize) -> serde_json::Map<String, Value> {
match to_value(&v).unwrap() {
Value::Object(m) => m,
_ => unreachable!(),
}
}
pub(crate) fn new() -> RouterBuilder<Arc<Mutex<Ctx>>> {
Router::new()
.config(Config::new().export_ts_bindings(
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../web/src/utils/bindings.ts"),
))
.mutation("createDatabase", |r| {
r(|ctx, _: String| async move {
let dbs = &mut ctx.lock().await.dbs;
let uuid = Uuid::new_v4();
dbs.insert(uuid, Db::new(uuid));
let ids = dbs.keys().copied().collect::<Vec<_>>();
for db in dbs.values_mut() {
for id in &ids {
db.register_node(*id);
}
}
Ok(uuid)
})
})
.mutation("removeDatabases", |r| {
r(|ctx, _: String| async move {
let dbs = &mut ctx.lock().await.dbs;
dbs.drain();
Ok(())
})
})
.query("dbs", |r| {
r(|ctx, _: String| async move {
let dbs = &mut ctx.lock().await.dbs;
Ok(dbs.iter().map(|(id, _)| *id).collect::<Vec<_>>())
})
})
.query("db.tags", |r| {
r(|ctx, id: String| async move {
let dbs = &mut ctx.lock().await.dbs;
let id = id.parse().unwrap();
Ok(dbs.get(&id).unwrap().tags.clone())
})
})
.query("file_path.list", |r| {
r(|ctx, id: String| async move {
let dbs = &mut ctx.lock().await.dbs;
let db = dbs.get(&id.parse().unwrap()).unwrap();
let file_paths = db.file_paths.values().map(Clone::clone).collect::<Vec<_>>();
Ok(file_paths)
})
})
.mutation("file_path.create", |r| {
r(|ctx, db: String| async move {
let dbs = &mut ctx.lock().await.dbs;
let db = dbs.get_mut(&db.parse().unwrap()).unwrap();
let id = Uuid::new_v4();
let file_path = FilePath {
id,
path: String::new(),
file: None,
};
let op = db.create_crdt_operation(CRDTOperationType::Owned(OwnedOperation {
model: "FilePath".to_string(),
items: vec![OwnedOperationItem {
id: serde_json::to_value(id).unwrap(),
data: OwnedOperationData::Create(to_map(&file_path)),
}],
}));
db.receive_crdt_operations(vec![op]);
file_path
})
})
.query("message.list", |r| {
r(|ctx, id: String| async move {
let dbs = &mut ctx.lock().await.dbs;
let db = dbs.get(&id.parse().unwrap()).unwrap();
Ok(db._operations.clone())
})
})
.mutation("pullOperations", |r| {
r(|ctx, db_id: String| async move {
let dbs = &mut ctx.lock().await.dbs;
let db_id = db_id.parse().unwrap();
let ops = dbs.values().flat_map(|db| db._operations.clone()).collect();
let db = dbs.get_mut(&db_id).unwrap();
db.receive_crdt_operations(ops);
Ok(())
})
})
.query("operations", |r| {
r(|ctx, _: String| async move {
let dbs = &mut ctx.lock().await.dbs;
let mut hashmap = HashMap::new();
for db in dbs.values_mut() {
for op in &db._operations {
hashmap.insert(op.id, op.clone());
}
}
let mut array = hashmap.into_iter().map(|(_, v)| v).collect::<Vec<_>>();
array.sort_by(|a, b| a.id.partial_cmp(&b.id).unwrap());
Ok(array)
})
})
}

View File

@@ -0,0 +1,40 @@
use axum::{
http::{HeaderValue, Method},
routing::get,
};
use std::{net::SocketAddr, sync::Arc};
use tokio::sync::Mutex;
use tower_http::cors::CorsLayer;
mod api;
// mod prisma;
mod utils;
fn router() -> axum::Router {
let router = api::new().build().arced();
let ctx = Arc::new(Mutex::new(Default::default()));
axum::Router::new()
.route("/", get(|| async { "Hello 'rspc'!" }))
.route("/rspc/:id", router.endpoint(move || ctx.clone()).axum())
.layer(
CorsLayer::new()
.allow_origin("http://localhost:3000".parse::<HeaderValue>().unwrap())
.allow_headers(vec![http::header::CONTENT_TYPE])
.allow_methods([Method::GET, Method::POST]),
)
}
#[tokio::main]
async fn main() {
dotenv::dotenv().ok();
let addr = "[::]:9000".parse::<SocketAddr>().unwrap(); // This listens on IPv6 and IPv4
println!("{} listening on http://{}", env!("CARGO_CRATE_NAME"), addr);
axum::Server::bind(&addr)
.serve(router().into_make_service())
.with_graceful_shutdown(utils::axum_shutdown_signal())
.await
.expect("Error with HTTP server!");
}

View File

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,28 @@
use tokio::signal;
/// shutdown_signal will inform axum to gracefully shutdown when the process is asked to shutdown.
pub async fn axum_shutdown_signal() {
let ctrl_c = async {
signal::ctrl_c()
.await
.expect("failed to install Ctrl+C handler");
};
#[cfg(unix)]
let terminate = async {
signal::unix::signal(signal::unix::SignalKind::terminate())
.expect("failed to install signal handler")
.recv()
.await;
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
tokio::select! {
_ = ctrl_c => {},
_ = terminate => {},
}
println!("signal received, starting graceful shutdown");
}

View File

@@ -1,5 +0,0 @@
# Ignore everything in this directory
*
# Except this file
!.gitignore
# This is done so that Tauri never complains that '../dist does not exist'

View File

@@ -1,17 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<link rel="icon" type="image/svg+xml" href="/src/assets/logo.svg" />
<title>Tauri + Solid + Typescript App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script src="/src/index.tsx" type="module"></script>
</body>
</html>

View File

@@ -1,32 +0,0 @@
{
"name": "@sd/sync-example",
"version": "0.0.0",
"description": "",
"scripts": {
"start": "vite",
"dev": "vite",
"build": "vite build",
"serve": "vite preview",
"tauri": "tauri"
},
"license": "MIT",
"devDependencies": {
"@tauri-apps/cli": "^1.1.1",
"@types/babel__core": "^7.1.19",
"@types/node": "^18.8.2",
"autoprefixer": "^10.4.12",
"postcss": "^8.4.17",
"tailwindcss": "^3.1.8",
"typescript": "^4.8.4",
"vite": "^3.1.4",
"vite-plugin-solid": "^2.3.9"
},
"dependencies": {
"@rspc/client": "~0.1.2",
"@rspc/solid": "~0.1.2",
"@rspc/tauri": "~0.1.2",
"@tanstack/solid-query": "4.10.1",
"clsx": "^1.2.1",
"solid-js": "^1.5.7"
}
}

View File

Binary file not shown.

View File

@@ -0,0 +1,12 @@
[package]
name = "prisma-cli"
version = "0.1.0"
edition = "2021"
[dependencies]
prisma-client-rust-cli = { workspace = true, features = [
"migrations",
] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio = { version = "1.13.0", features = ["full"] }

View File

@@ -0,0 +1,18 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
datasource db {
provider = "sqlite"
url = "file:dev.db"
}
generator client {
provider = "cargo prisma"
output = "../api/src/prisma.rs"
}
model User {
id Int @id @default(autoincrement())
name String
email String @unique
}

View File

@@ -0,0 +1,3 @@
fn main() {
prisma_client_rust_cli::run();
}

View File

@@ -1,6 +0,0 @@
<svg width="206" height="231" viewBox="0 0 206 231" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M143.143 84C143.143 96.1503 133.293 106 121.143 106C108.992 106 99.1426 96.1503 99.1426 84C99.1426 71.8497 108.992 62 121.143 62C133.293 62 143.143 71.8497 143.143 84Z" fill="#FFC131"/>
<ellipse cx="84.1426" cy="147" rx="22" ry="22" transform="rotate(180 84.1426 147)" fill="#24C8DB"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M166.738 154.548C157.86 160.286 148.023 164.269 137.757 166.341C139.858 160.282 141 153.774 141 147C141 144.543 140.85 142.121 140.558 139.743C144.975 138.204 149.215 136.139 153.183 133.575C162.73 127.404 170.292 118.608 174.961 108.244C179.63 97.8797 181.207 86.3876 179.502 75.1487C177.798 63.9098 172.884 53.4021 165.352 44.8883C157.82 36.3744 147.99 30.2165 137.042 27.1546C126.095 24.0926 114.496 24.2568 103.64 27.6274C92.7839 30.998 83.1319 37.4317 75.8437 46.1553C74.9102 47.2727 74.0206 48.4216 73.176 49.5993C61.9292 50.8488 51.0363 54.0318 40.9629 58.9556C44.2417 48.4586 49.5653 38.6591 56.679 30.1442C67.0505 17.7298 80.7861 8.57426 96.2354 3.77762C111.685 -1.01901 128.19 -1.25267 143.769 3.10474C159.348 7.46215 173.337 16.2252 184.056 28.3411C194.775 40.457 201.767 55.4101 204.193 71.404C206.619 87.3978 204.374 103.752 197.73 118.501C191.086 133.25 180.324 145.767 166.738 154.548ZM41.9631 74.275L62.5557 76.8042C63.0459 72.813 63.9401 68.9018 65.2138 65.1274C57.0465 67.0016 49.2088 70.087 41.9631 74.275Z" fill="#FFC131"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M38.4045 76.4519C47.3493 70.6709 57.2677 66.6712 67.6171 64.6132C65.2774 70.9669 64 77.8343 64 85.0001C64 87.1434 64.1143 89.26 64.3371 91.3442C60.0093 92.8732 55.8533 94.9092 51.9599 97.4256C42.4128 103.596 34.8505 112.392 30.1816 122.756C25.5126 133.12 23.9357 144.612 25.6403 155.851C27.3449 167.09 32.2584 177.598 39.7906 186.112C47.3227 194.626 57.153 200.784 68.1003 203.846C79.0476 206.907 90.6462 206.743 101.502 203.373C112.359 200.002 122.011 193.568 129.299 184.845C130.237 183.722 131.131 182.567 131.979 181.383C143.235 180.114 154.132 176.91 164.205 171.962C160.929 182.49 155.596 192.319 148.464 200.856C138.092 213.27 124.357 222.426 108.907 227.222C93.458 232.019 76.9524 232.253 61.3736 227.895C45.7948 223.538 31.8055 214.775 21.0867 202.659C10.3679 190.543 3.37557 175.59 0.949823 159.596C-1.47592 143.602 0.768139 127.248 7.41237 112.499C14.0566 97.7497 24.8183 85.2327 38.4045 76.4519ZM163.062 156.711L163.062 156.711C162.954 156.773 162.846 156.835 162.738 156.897C162.846 156.835 162.954 156.773 163.062 156.711Z" fill="#24C8DB"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,4 +0,0 @@
# Generated by Cargo
# will have compiled files and executables
/target/

View File

Binary file not shown.

View File

@@ -1,31 +0,0 @@
[package]
name = "example"
version = "0.0.0"
description = "A Tauri App"
authors = ["you"]
license = ""
repository = ""
edition = "2021"
rust-version = "1.57"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies]
tauri-build = { version = "1.1", features = [] }
[dependencies]
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "1.1", features = ["api-all"] }
rspc = { workspace = true, features = ["tauri", "uuid"] }
sd-sync = { path = "../.." }
tokio = { version = "1.21.2", features = ["macros"] }
uuid = { version = "1.1.2", features = ["v4"] }
[features]
# by default Tauri runs in production mode
# when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL
default = ["custom-protocol"]
# this feature is used used for production builds where `devPath` points to the filesystem
# DO NOT remove this
custom-protocol = ["tauri/custom-protocol"]

View File

@@ -1,3 +0,0 @@
fn main() {
tauri_build::build()
}

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 974 B

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 903 B

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

Binary file not shown.

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -1,77 +0,0 @@
#![cfg_attr(
all(not(debug_assertions), target_os = "windows"),
windows_subsystem = "windows"
)]
use std::{collections::HashMap, path::PathBuf, sync::Arc};
use tokio::sync::Mutex;
use uuid::Uuid;
use rspc::*;
use sd_sync::*;
#[derive(Default)]
struct Ctx {
pub dbs: HashMap<Uuid, Db>,
}
type Router = rspc::Router<Arc<Mutex<Ctx>>>;
#[tokio::main]
async fn main() {
let router = Arc::new(
<Router>::new()
.config(Config::new().export_ts_bindings(
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../src/bindings.ts"),
))
.mutation("createDatabase", |r| {
r(|ctx, _: ()| async move {
let dbs = &mut ctx.lock().await.dbs;
let uuid = Uuid::new_v4();
dbs.insert(uuid, Db::new(uuid));
println!("{:?}", dbs);
Ok(uuid)
})
})
.mutation("removeDatabases", |r| {
r(|ctx, _: ()| async move {
let dbs = &mut ctx.lock().await.dbs;
dbs.drain();
Ok(())
})
})
.query("dbs", |r| {
r(|ctx, _: ()| async move {
let dbs = &mut ctx.lock().await.dbs;
Ok(dbs.iter().map(|(id, _)| *id).collect::<Vec<_>>())
})
})
.query("db.tags", |r| {
r(|ctx, id: String| async move {
let dbs = &mut ctx.lock().await.dbs;
let id = id.parse().unwrap();
println!("{:?}", &dbs);
Ok(dbs.get(&id).unwrap().tags.clone())
})
})
.build(),
);
let ctx = Arc::new(Mutex::new(Default::default()));
tauri::Builder::default()
.plugin(rspc::integrations::tauri::plugin(router, move || {
ctx.clone()
}))
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

View File

@@ -1,66 +0,0 @@
{
"build": {
"beforeDevCommand": "pnpm dev",
"beforeBuildCommand": "pnpm build",
"devPath": "http://localhost:1420",
"distDir": "../dist",
"withGlobalTauri": false
},
"package": {
"productName": "example",
"version": "0.0.0"
},
"tauri": {
"allowlist": {
"all": true
},
"bundle": {
"active": true,
"category": "DeveloperTool",
"copyright": "",
"deb": {
"depends": []
},
"externalBin": [],
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"identifier": "com.tauri.dev",
"longDescription": "",
"macOS": {
"entitlements": null,
"exceptionDomain": "",
"frameworks": [],
"providerShortName": null,
"signingIdentity": null
},
"resources": [],
"shortDescription": "",
"targets": "all",
"windows": {
"certificateThumbprint": null,
"digestAlgorithm": "sha256",
"timestampUrl": ""
}
},
"security": {
"csp": null
},
"updater": {
"active": false
},
"windows": [
{
"fullscreen": false,
"height": 600,
"resizable": true,
"title": "example",
"width": 800
}
]
}
}

View File

@@ -1,69 +0,0 @@
import clsx from 'clsx';
import { For, JSX, Suspense, createSignal } from 'solid-js';
import { queryClient, rspc } from './rspc';
export function App() {
const dbs = rspc.createQuery(() => ['dbs']);
const createDb = rspc.createMutation('createDatabase', {
onSuccess: () => {
queryClient.invalidateQueries();
}
});
const removeDbs = rspc.createMutation('removeDatabases', {
onSuccess: () => queryClient.invalidateQueries()
});
return (
<div class="p-4 space-y-4">
<div class="space-x-4">
<Button onClick={() => createDb.mutate(undefined)}>Add Database</Button>
<Button onClick={() => removeDbs.mutate(undefined)}>Remove Databases</Button>
</div>
<ul class="gap-2 flex flex-row flex-wrap">
<For each={dbs.data}>
{(id) => (
<Suspense fallback={null}>
<DatabaseView id={id} />
</Suspense>
)}
</For>
</ul>
</div>
);
}
interface DatabaseViewProps {
id: string;
}
const TABS = ['Tags', 'Files', 'File Paths', 'Messages'];
function DatabaseView(props: DatabaseViewProps) {
const [currentTab, setCurrentTab] = createSignal<typeof TABS[number]>('Tags');
return (
<div class="bg-indigo-300 rounded-md min-w-[40rem] flex-1 overflow-hidden">
<h1 class="p-2 text-xl font-medium">{props.id}</h1>
<div>
<nav class="space-x-2">
<For each={TABS}>
{(tab) => (
<button
class={clsx('px-2 py-1', tab === currentTab() && 'bg-indigo-400')}
onClick={() => setCurrentTab(tab)}
>
{tab}
</button>
)}
</For>
</nav>
<div></div>
</div>
</div>
);
}
function Button(props: JSX.ButtonHTMLAttributes<HTMLButtonElement>) {
return <button {...props} class="bg-blue-500 text-white px-2 py-1 rounded-md" />;
}

View File

@@ -1,22 +0,0 @@
// This file was generated by [rspc](https://github.com/oscartbeaumont/rspc). Do not edit this file manually.
export type Procedures = {
queries:
| { key: 'db.tags'; input: string; result: Record<string, Tag> }
| { key: 'dbs'; input: never; result: Array<string> };
mutations:
| { key: 'createDatabase'; input: never; result: string }
| { key: 'removeDatabases'; input: never; result: null };
subscriptions: never;
};
export interface Color {
red: number;
green: number;
blue: number;
}
export interface Tag {
color: Color;
name: string;
}

View File

@@ -1,17 +0,0 @@
/* @refresh reload */
import { Suspense, render } from 'solid-js/web';
import { App } from './App';
import './index.css';
import { queryClient, rspc, rspcClient } from './rspc';
render(
() => (
<rspc.Provider client={rspcClient} queryClient={queryClient}>
<Suspense fallback={null}>
<App />
</Suspense>
</rspc.Provider>
),
document.getElementById('root') as HTMLElement
);

View File

@@ -1,24 +0,0 @@
import { createClient } from '@rspc/client';
import { createSolidQueryHooks } from '@rspc/solid';
import { TauriTransport } from '@rspc/tauri';
import { QueryClient } from '@tanstack/solid-query';
import type { Procedures } from './bindings';
// These were the bindings exported from your Rust code!
// You must provide the generated types as a generic and create a transport (in this example we are using HTTP Fetch) so that the client knows how to communicate with your API.
export const rspcClient = createClient<Procedures>({
// Refer to the integration your using for the correct transport.
transport: new TauriTransport()
});
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
suspense: true
}
}
});
export const rspc = createSolidQueryHooks<Procedures>();

View File

@@ -1,15 +0,0 @@
{
"compilerOptions": {
"strict": true,
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"jsx": "preserve",
"jsxImportSource": "solid-js",
"types": ["vite/client"],
"noEmit": true,
"isolatedModules": true
}
}

View File

@@ -1,25 +0,0 @@
import { defineConfig } from 'vite';
import solidPlugin from 'vite-plugin-solid';
export default defineConfig({
plugins: [solidPlugin()],
// Vite optons tailored for Tauri development and only applied in `tauri dev` or `tauri build`
// prevent vite from obscuring rust errors
clearScreen: false,
// tauri expects a fixed port, fail if that port is not available
server: {
port: 1420,
strictPort: true
},
// to make use of `TAURI_DEBUG` and other env variables
// https://tauri.studio/v1/api/config#buildconfig.beforedevcommand
envPrefix: ['VITE_', 'TAURI_'],
build: {
// Tauri supports es2021
target: ['es2021', 'chrome100', 'safari13'],
// don't minify for debug builds
minify: !process.env.TAURI_DEBUG ? 'esbuild' : false,
// produce sourcemaps for debug builds
sourcemap: !!process.env.TAURI_DEBUG
}
});

View File

@@ -0,0 +1,34 @@
## Usage
Those templates dependencies are maintained via [pnpm](https://pnpm.io) via `pnpm up -Lri`.
This is the reason you see a `pnpm-lock.yaml`. That being said, any package manager will work. This file can be safely be removed once you clone a template.
```bash
$ npm install # or pnpm install or yarn install
```
### Learn more on the [Solid Website](https://solidjs.com) and come chat with us on our [Discord](https://discord.com/invite/solidjs)
## Available Scripts
In the project directory, you can run:
### `npm dev` or `npm start`
Runs the app in the development mode.<br>
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.<br>
### `npm run build`
Builds the app for production to the `dist` folder.<br>
It correctly bundles Solid in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.<br>
Your app is ready to be deployed!
## Deployment
You can deploy the `dist` folder to any static host provider (netlify, surge, now, etc.)

View File

@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<title>Solid App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script src="/src/index.tsx" type="module"></script>
</body>
</html>

View File

@@ -0,0 +1,27 @@
{
"name": "example-2",
"version": "0.0.0",
"description": "",
"scripts": {
"dev": "vite",
"build": "vite build",
"serve": "vite preview",
"typecheck": "tsc --noEmit"
},
"license": "MIT",
"devDependencies": {
"@rspc/client": "^0.0.0-main-7c0a67c1",
"@rspc/react": "^0.0.0-main-7c0a67c1",
"@tanstack/react-query": "^4.10.1",
"@vitejs/plugin-react": "^2.1.0",
"typescript": "^4.8.2",
"vite": "^3.0.9"
},
"dependencies": {
"clsx": "^1.2.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"solid-js": "^1.5.1",
"tailwindcss": "^3.1.8"
}
}

View File

@@ -0,0 +1,148 @@
import clsx from 'clsx';
import { Suspense, useState } from 'react';
import { tests } from './test';
import { CRDTOperationType, rspc } from './utils/rspc';
export function App() {
const dbs = rspc.useQuery(['dbs', 'cringe']);
const operations = rspc.useQuery(['operations', 'cringe']);
const createDb = rspc.useMutation('createDatabase');
const removeDbs = rspc.useMutation('removeDatabases');
return (
<div className="w-screen h-screen flex flex-row divide-x divide-gray-300">
<div className="p-2 space-y-2 flex flex-col">
<div className="space-x-2">
<button className={ButtonStyles} onClick={() => createDb.mutate('pullOperations')}>
Add Database
</button>
<button className={ButtonStyles} onClick={() => removeDbs.mutate('pullOperations')}>
Remove Databases
</button>
</div>
<ul className="w-full">
{Object.entries(tests).map(([key, test]) => (
<li key={key}>
<button className="p-2 bg-green-300" onClick={() => test.run()}>
{test.name}
</button>
</li>
))}
</ul>
</div>
<div className="flex-1">
<ul className="p-2 gap-2 flex flex-row flex-wrap">
{dbs.data?.map((id) => (
<Suspense fallback={null} key={id}>
<DatabaseView id={id} />
</Suspense>
))}
</ul>
</div>
<div className="w-96 p-2 flex flex-col items-stretch">
<h1 className="text-center font-bold text-2xl">All Operations</h1>
<ul className="space-y-2">
{operations.data?.map((op) => (
<li key={op.id} className="bg-indigo-200 rounded-md p-2">
<p className="truncate">ID: {op.id}</p>
<p className="truncate">Timestamp: {op.timestamp.toString()}</p>
<p className="truncate">Node: {op.node}</p>
</li>
))}
</ul>
</div>
</div>
);
}
interface DatabaseViewProps {
id: string;
}
const TABS = ['File Paths', 'Objects', 'Tags', 'Operations'];
function DatabaseView(props: DatabaseViewProps) {
const [currentTab, setCurrentTab] = useState<typeof TABS[number]>('Operations');
const pullOperations = rspc.useMutation('pullOperations');
return (
<div className="bg-indigo-300 rounded-md min-w-[32rem] flex-1 overflow-hidden">
<div className="flex flex-row justify-between items-center mx-2">
<h1 className="p-2 text-xl font-medium">{props.id}</h1>
<button className={ButtonStyles} onClick={() => pullOperations.mutate(props.id)}>
Pull Operations
</button>
</div>
<div>
<nav className="space-x-2">
{TABS.map((tab) => (
<button
key={tab}
className={clsx('px-2 py-1', tab === currentTab && 'bg-indigo-400')}
onClick={() => setCurrentTab(tab)}
>
{tab}
</button>
))}
</nav>
<Suspense>
{currentTab === 'File Paths' && <FilePathList db={props.id} />}
{currentTab === 'Operations' && <OperationList db={props.id} />}
</Suspense>
</div>
</div>
);
}
function FilePathList(props: { db: string }) {
const createFilePath = rspc.useMutation('file_path.create');
const filePaths = rspc.useQuery(['file_path.list', props.db]);
return (
<div >
{filePaths.data && (
<ul className="font-mono">
{filePaths.data.sort((a, b) => a.id.localeCompare(b.id)).map((path) => (
<li key={path.id}>{JSON.stringify(path)}</li>
))}
</ul>
)}
<button className="text-center" onClick={() => createFilePath.mutate(props.db)}>
Create
</button>
</div>
);
}
function messageType(msg: CRDTOperationType) {
if ('items' in msg) {
return 'Owned';
} else if ('record_id' in msg) {
return 'Shared';
}
}
function OperationList(props: { db: string }) {
const messages = rspc.useQuery(['message.list', props.db]);
return (
<div>
{messages.data && (
<table className="font-mono border-spacing-x-4 border-separate">
{messages.data.sort((a, b) => Number(a.timestamp - b.timestamp)).map((message) => (
<tr key={message.id}>
<td className="border border-transparent">{message.id}</td>
<td className="border border-transparent">{new Date(Number(message.timestamp) / 10000000).toLocaleTimeString()}</td>
<td className="border border-transparent">{messageType(message.typ)}</td>
</tr>
))}
</table>
)}
</div>
);
}
const ButtonStyles = 'bg-blue-500 text-white px-2 py-1 rounded-md';

View File

@@ -0,0 +1,15 @@
/* @refresh reload */
import { Suspense } from 'react';
import { createRoot } from 'react-dom/client';
import { App } from './App';
import './index.css';
import { queryClient, rspc, rspcClient } from './utils/rspc';
createRoot(document.getElementById('root') as HTMLElement).render(
<rspc.Provider client={rspcClient} queryClient={queryClient}>
<Suspense fallback={null}>
<App />
</Suspense>
</rspc.Provider>
);

View File

@@ -0,0 +1,45 @@
import { queryClient, rspcClient } from './utils/rspc';
function test(fn: () => Promise<void>) {
return async () => {
await fn();
queryClient.invalidateQueries();
};
}
const wait = (ms: number) => new Promise((res) => setTimeout(res, ms));
export const tests = {
three: {
name: 'Three',
run: test(async () => {
const [db1, db2, db3] = await Promise.all([
rspcClient.mutation(['createDatabase', ' ']),
rspcClient.mutation(['createDatabase', ' ']),
rspcClient.mutation(['createDatabase', ' '])
]);
const dbs = await rspcClient.query(['dbs', 'cringe']);
for (const db of dbs) {
await rspcClient.mutation(['file_path.create', db]);
}
for (const db of dbs) {
await rspcClient.mutation(['pullOperations', db]);
}
await rspcClient.mutation(['file_path.create', dbs[0]]);
await rspcClient.mutation(['file_path.create', dbs[0]]);
for (const db of dbs) {
await rspcClient.mutation(['pullOperations', db]);
}
await rspcClient.mutation(['pullOperations', dbs[1]]);
await rspcClient.mutation(['pullOperations', dbs[1]]);
await rspcClient.mutation(['pullOperations', dbs[1]]);
await rspcClient.mutation(['pullOperations', dbs[1]]);
})
}
};

View File

@@ -0,0 +1,42 @@
// This file was generated by [rspc](https://github.com/oscartbeaumont/rspc). Do not edit this file manually.
export type Procedures = {
queries:
{ key: "db.tags", input: string, result: Record<string, Tag> } |
{ key: "dbs", input: string, result: Array<string> } |
{ key: "file_path.list", input: string, result: Array<FilePath> } |
{ key: "message.list", input: string, result: Array<CRDTOperation> } |
{ key: "operations", input: string, result: Array<CRDTOperation> },
mutations:
{ key: "createDatabase", input: string, result: string } |
{ key: "file_path.create", input: string, result: FilePath } |
{ key: "pullOperations", input: string, result: null } |
{ key: "removeDatabases", input: string, result: null },
subscriptions: never
};
export interface CRDTOperation { node: string, timestamp: bigint, id: string, typ: CRDTOperationType }
export type CRDTOperationType = SharedOperation | RelationOperation | OwnedOperation
export interface Color { red: number, green: number, blue: number }
export interface FilePath { id: string, path: string, file: string | null }
export interface OwnedOperation { model: string, items: Array<OwnedOperationItem> }
export type OwnedOperationData = { Create: Record<string, any> } | { Update: Record<string, any> } | "Delete"
export interface OwnedOperationItem { id: any, data: OwnedOperationData }
export interface RelationOperation { relation_item: string, relation_group: string, relation: string, data: RelationOperationData }
export type RelationOperationData = "Create" | { Update: { field: string, value: any } } | "Delete"
export interface SharedOperation { record_id: string, model: string, data: SharedOperationData }
export type SharedOperationCreateData = { Unique: Record<string, any> } | "Atomic"
export type SharedOperationData = { Create: SharedOperationCreateData } | { Update: { field: string, value: any } } | "Delete"
export interface Tag { color: Color, name: string }

View File

@@ -0,0 +1,28 @@
import { createClient, httpLink } from '@rspc/client';
import { createReactHooks } from '@rspc/react';
import { QueryClient } from '@tanstack/react-query';
import type { Procedures } from './bindings';
export * from './bindings';
// These are generated by rspc in Rust for you.
const rspc = createReactHooks<Procedures>();
const rspcClient = rspc.createClient({
links: [httpLink({ url: 'http://localhost:9000/rspc' })]
});
const queryClient = new QueryClient({
defaultOptions: {
queries: {
suspense: true
},
mutations: {
onSuccess: () => queryClient.invalidateQueries()
}
}
});
export { rspc, rspcClient, queryClient };

View File

@@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"jsx": "react-jsx",
"types": [
"vite/client"
],
"noEmit": true,
"isolatedModules": true
}
}

View File

@@ -0,0 +1,12 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
port: 3000
},
build: {
target: 'esnext'
}
});

View File

@@ -1,63 +1,79 @@
use std::fmt::Debug;
use rspc::Type;
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use uhlc::NTP64;
use uuid::Uuid;
#[derive(Serialize, Deserialize, Clone, Debug)]
#[derive(Serialize, Deserialize, Clone, Debug, Type)]
pub enum RelationOperationData {
Create,
Update { field: String, value: Value },
Delete,
}
#[derive(Serialize, Deserialize, Clone, Debug, Type)]
pub struct RelationOperation {
pub relation_item: Uuid,
pub relation_group: Uuid,
pub relation: String,
pub data: RelationOperationData,
}
#[derive(Serialize, Deserialize, Clone, Debug, Type)]
pub enum SharedOperationCreateData {
Unique(Map<String, Value>),
Atomic,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[derive(Serialize, Deserialize, Clone, Debug, Type)]
pub enum SharedOperationData {
Create(SharedOperationCreateData),
Update { field: String, value: Value },
Delete,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[derive(Serialize, Deserialize, Clone, Debug, Type)]
pub struct SharedOperation {
pub record_id: Value, // Uuid,
pub record_id: Uuid,
pub model: String,
pub data: SharedOperationData,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[derive(Serialize, Deserialize, Clone, Debug, Type)]
pub enum OwnedOperationData {
Create(Map<String, Value>),
Update(Map<String, Value>),
Delete,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[derive(Serialize, Deserialize, Clone, Debug, Type)]
pub struct OwnedOperationItem {
pub id: Value,
pub data: OwnedOperationData,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[derive(Serialize, Deserialize, Clone, Debug, Type)]
pub struct OwnedOperation {
pub model: String,
pub items: Vec<OwnedOperationItem>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[derive(Serialize, Deserialize, Clone, Debug, Type)]
#[serde(untagged)]
pub enum CRDTOperationType {
Shared(SharedOperation),
// Relation(RelationOperation),
Relation(RelationOperation),
Owned(OwnedOperation),
}
#[derive(Serialize, Deserialize, Clone)]
#[derive(Serialize, Deserialize, Clone, Type)]
pub struct CRDTOperation {
pub node: Uuid,
pub timestamp: NTP64,
pub id: Uuid,
#[serde(flatten)]
// #[serde(flatten)]
pub typ: CRDTOperationType,
}

View File

@@ -1,13 +1,14 @@
use std::collections::HashMap;
use std::{collections::HashMap, time::Duration};
use rspc::Type;
use serde::*;
use serde_json::*;
use uhlc::{HLCBuilder, Timestamp, HLC};
use uhlc::{HLCBuilder, Timestamp, HLC, NTP64};
use uuid::Uuid;
use super::crdt::*;
// Bytes
#[derive(Default, Debug, Serialize, Type, Clone)]
pub struct Color {
pub red: u8,
@@ -15,31 +16,41 @@ pub struct Color {
pub blue: u8,
}
/// Unique Shared
// Unique Shared
#[derive(Default, Debug, Serialize, Type, Clone)]
pub struct Tag {
pub color: Color,
pub name: String,
}
/// Atomic Shared
#[derive(Default, Debug, Serialize, Type, Clone)]
pub struct File {
pub struct TagOnObject {
pub tag_id: Uuid,
pub object_id: Uuid,
}
// Atomic Shared
#[derive(Default, Debug, Serialize, Type, Clone)]
pub struct Object {
pub id: Uuid,
pub name: String,
}
/// Owned
// Owned
#[derive(Serialize, Deserialize, Debug, Type, Clone)]
pub struct FilePath {
pub id: Uuid,
pub path: String,
pub file: Option<i32>,
pub file: Option<Uuid>,
}
pub struct Db {
pub files: HashMap<i32, File>,
pub file_paths: HashMap<i32, FilePath>,
pub objects: HashMap<Uuid, Object>,
pub file_paths: HashMap<Uuid, FilePath>,
pub tags: HashMap<Uuid, Tag>,
_operations: Vec<CRDTOperation>,
pub tags_on_objects: HashMap<(Uuid, Uuid), TagOnObject>,
pub _operations: Vec<CRDTOperation>,
pub _clocks: HashMap<Uuid, NTP64>,
_clock: HLC,
_node: Uuid,
}
@@ -47,7 +58,7 @@ pub struct Db {
impl std::fmt::Debug for Db {
fn fmt(&self, f: &mut __private::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Db")
.field("files", &self.files)
.field("files", &self.objects)
.field("file_paths", &self.file_paths)
.finish()
}
@@ -56,52 +67,74 @@ impl std::fmt::Debug for Db {
impl Db {
pub fn new(node: Uuid) -> Self {
Self {
files: Default::default(),
objects: Default::default(),
file_paths: Default::default(),
tags: Default::default(),
tags_on_objects: Default::default(),
_clocks: Default::default(),
_node: node,
_clock: HLCBuilder::new().with_id(node.into()).build(),
_operations: Default::default(),
}
}
pub fn create_crdt_operation(&self, typ: CRDTOperationType) -> CRDTOperation {
pub fn register_node(&mut self, id: Uuid) {
self._clocks
.entry(id)
.or_insert_with(|| Duration::from_millis(0).into());
}
pub fn create_crdt_operation(&mut self, typ: CRDTOperationType) -> CRDTOperation {
let hlc_timestamp = self._clock.new_timestamp();
let op = CRDTOperation {
CRDTOperation {
node: self._node,
timestamp: *hlc_timestamp.get_time(),
id: Uuid::new_v4(),
typ,
};
dbg!(op)
}
}
fn compare_messages(&self, operations: Vec<CRDTOperation>) -> Vec<(CRDTOperation, bool)> {
operations
.into_iter()
.map(|op| {
let old = match &op.typ {
CRDTOperationType::Owned(_) => false,
.map(|op| (op.id, op))
.collect::<HashMap<_, _>>()
.into_iter()
.filter_map(|(_, op)| {
match &op.typ {
CRDTOperationType::Owned(_) => {
self._operations.iter().find(|find_op| match &find_op.typ {
CRDTOperationType::Owned(_) => {
find_op.timestamp >= op.timestamp && find_op.node == op.node
}
_ => false,
})
}
CRDTOperationType::Shared(shared_op) => {
let similar_op = self._operations.iter().find(|find_op| {
if let CRDTOperationType::Shared(find_shared_op) = &find_op.typ {
self._operations.iter().find(|find_op| match &find_op.typ {
CRDTOperationType::Shared(find_shared_op) => {
shared_op.model == find_shared_op.model
&& shared_op.record_id == find_shared_op.record_id
&& op.timestamp >= find_op.timestamp
} else {
false
&& find_op.timestamp >= op.timestamp
}
});
similar_op
.map(|similar_op| similar_op.timestamp == op.timestamp)
.unwrap_or(false)
_ => false,
})
}
};
(op, old)
CRDTOperationType::Relation(relation_op) => {
self._operations.iter().find(|find_op| match &find_op.typ {
CRDTOperationType::Relation(find_relation_op) => {
relation_op.relation == find_relation_op.relation
&& relation_op.relation_item == find_relation_op.relation_item
&& relation_op.relation_group == find_relation_op.relation_group
}
_ => false,
})
}
}
.map(|old_op| (old_op.timestamp != op.timestamp).then_some(true))
.unwrap_or(Some(false))
.map(|old| (op, old))
})
.collect()
}
@@ -110,7 +143,9 @@ impl Db {
for op in &ops {
self._clock
.update_with_timestamp(&Timestamp::new(op.timestamp, op.node.into()))
.unwrap();
.ok();
self._clocks.insert(op.node, op.timestamp);
}
for (op, old) in self.compare_messages(ops) {
@@ -119,15 +154,21 @@ impl Db {
if !old {
match op.typ {
CRDTOperationType::Shared(shared_op) => match shared_op.model.as_str() {
"File" => {
let id = from_value(shared_op.record_id).unwrap();
"Object" => {
let id = shared_op.record_id;
match shared_op.data {
SharedOperationData::Create(SharedOperationCreateData::Atomic) => {
self.files.insert(id, Default::default());
self.objects.insert(
id,
Object {
id,
..Default::default()
},
);
}
SharedOperationData::Update { field, value } => {
let mut file = self.files.get_mut(&id).unwrap();
let mut file = self.objects.get_mut(&id).unwrap();
match field.as_str() {
"name" => {
@@ -137,7 +178,7 @@ impl Db {
}
}
SharedOperationData::Delete => {
self.files.remove(&id).unwrap();
self.objects.remove(&id).unwrap();
}
_ => {}
}
@@ -173,10 +214,155 @@ impl Db {
}
_ => unreachable!(),
},
CRDTOperationType::Relation(relation_op) => match relation_op.relation.as_str()
{
"TagOnObject" => match relation_op.data {
RelationOperationData::Create => {
self.tags_on_objects.insert(
(relation_op.relation_item, relation_op.relation_group),
TagOnObject {
object_id: relation_op.relation_item,
tag_id: relation_op.relation_group,
},
);
}
RelationOperationData::Update { field: _, value: _ } => {
// match field.as_str() {
// _ => unreachable!(),
// }
}
RelationOperationData::Delete => {
self.tags_on_objects
.remove(&(
relation_op.relation_item,
relation_op.relation_group,
))
.unwrap();
}
},
_ => unreachable!(),
},
}
}
self._operations.push(push_op)
self._operations.push(push_op)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn to_map(v: &impl serde::Serialize) -> serde_json::Map<String, Value> {
match to_value(&v).unwrap() {
Value::Object(m) => m,
_ => unreachable!(),
}
}
#[test]
fn test() {
let mut dbs = vec![];
for _ in 0..3 {
let id = Uuid::new_v4();
dbs.push(Db::new(id.clone()));
let ids = dbs.iter().map(|db| db._node.clone()).collect::<Vec<_>>();
for db in &mut dbs {
for id in &ids {
db.register_node(id.clone());
}
}
}
for db in &mut dbs {
let id = Uuid::new_v4();
let file_path = FilePath {
id,
path: String::new(),
file: None,
};
let op = db.create_crdt_operation(CRDTOperationType::Owned(OwnedOperation {
model: "FilePath".to_string(),
items: vec![OwnedOperationItem {
id: serde_json::to_value(id).unwrap(),
data: OwnedOperationData::Create(to_map(&file_path)),
}],
}));
db.receive_crdt_operations(vec![op]);
}
for db in &dbs {
assert_eq!(db._operations.len(), 1);
}
let ops = dbs
.iter()
.flat_map(|db| db._operations.clone())
.collect::<Vec<_>>();
for db in &mut dbs {
db.receive_crdt_operations(ops.clone())
}
for db in &dbs {
assert_eq!(db.file_paths.len(), 3);
assert_eq!(db._operations.len(), 3);
}
for _ in 0..2 {
let db = &mut dbs[0];
let id = Uuid::new_v4();
let file_path = FilePath {
id,
path: String::new(),
file: None,
};
let op = db.create_crdt_operation(CRDTOperationType::Owned(OwnedOperation {
model: "FilePath".to_string(),
items: vec![OwnedOperationItem {
id: serde_json::to_value(id).unwrap(),
data: OwnedOperationData::Create(to_map(&file_path)),
}],
}));
db.receive_crdt_operations(vec![op]);
}
let ops = dbs
.iter()
.flat_map(|db| db._operations.clone())
.collect::<Vec<_>>();
for db in &mut dbs {
db.receive_crdt_operations(ops.clone());
}
for db in &dbs {
assert_eq!(db.file_paths.len(), 5);
assert_eq!(db._operations.len(), 5);
}
for _ in 0..4 {
let ops = dbs
.iter()
.flat_map(|db| db._operations.clone())
.collect::<Vec<_>>();
dbs[0].receive_crdt_operations(ops);
}
for db in &dbs {
assert_eq!(db.file_paths.len(), 5);
assert_eq!(db._operations.len(), 5);
}
}
}

View File

@@ -1,60 +0,0 @@
use serde_json::*;
use sd_sync::*;
fn map<const N: usize>(arr: [(&str, Value); N]) -> Map<String, Value> {
arr.into_iter().map(|(k, v)| (k.to_string(), v)).collect()
}
fn main() {
let uuid = uuid::uuid!("00000000-0000-0000-0000-000000000001");
let mut db = Db::new(uuid);
db.receive_crdt_operations(vec![db.create_crdt_operation(CRDTOperationType::Owned(
OwnedOperation {
model: "FilePath".to_string(),
items: vec![OwnedOperationItem {
id: json!(0),
data: OwnedOperationData::Create(map([("path", json!("some/file.path"))])),
}],
},
))]);
dbg!(&db);
db.receive_crdt_operations(vec![db.create_crdt_operation(CRDTOperationType::Shared(
SharedOperation {
record_id: json!(0),
model: "File".to_string(),
data: SharedOperationData::Create(SharedOperationCreateData::Atomic),
},
))]);
dbg!(&db);
db.receive_crdt_operations(vec![db.create_crdt_operation(CRDTOperationType::Shared(
SharedOperation {
record_id: json!(0),
model: "File".to_string(),
data: SharedOperationData::Update {
field: "name".to_string(),
value: json!("Lmaoooo"),
},
},
))]);
dbg!(&db);
db.receive_crdt_operations(vec![db.create_crdt_operation(CRDTOperationType::Owned(
OwnedOperation {
model: "FilePath".to_string(),
items: vec![OwnedOperationItem {
id: json!(0),
data: OwnedOperationData::Update(map([("file", json!(0))])),
}],
},
))]);
dbg!(&db);
}

BIN
pnpm-lock.yaml generated
View File

Binary file not shown.

View File

@@ -2,5 +2,5 @@ packages:
- 'packages/*'
- 'apps/*'
- 'core'
- 'crates/sync/example'
- 'crates/sync/example/web'
- 'docs'