diff --git a/Cargo.lock b/Cargo.lock index 7debb5e..59b4048 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15,6 +15,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", + "getrandom 0.3.4", "once_cell", "version_check", "zerocopy", @@ -56,6 +57,16 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "async-compression" version = "0.4.42" @@ -89,6 +100,58 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "base-x" version = "0.2.11" @@ -148,6 +211,7 @@ dependencies = [ "roaring", "scc", "thiserror 2.0.18", + "tokio", ] [[package]] @@ -168,11 +232,40 @@ dependencies = [ "url", ] +[[package]] +name = "bobbin-record-lru" +version = "0.0.1" +dependencies = [ + "bobbin-types", + "bytes", + "jacquard-common", + "quick_cache", +] + +[[package]] +name = "bobbin-slingshot-client" +version = "0.0.1" +dependencies = [ + "bobbin-types", + "bytes", + "cid", + "futures", + "jacquard-common", + "reqwest", + "serde", + "serde_json", + "thiserror 2.0.18", + "tokio", + "url", + "wiremock", +] + [[package]] name = "bobbin-types" version = "0.0.1" dependencies = [ "anyhow", + "bytes", "chrono", "cid", "jacquard-common", @@ -185,6 +278,26 @@ dependencies = [ "walkdir", ] +[[package]] +name = "bobbin-xrpc" +version = "0.0.1" +dependencies = [ + "axum", + "bobbin-record-lru", + "bobbin-slingshot-client", + "bobbin-types", + "futures", + "http", + "jacquard-common", + "serde", + "serde_json", + "thiserror 2.0.18", + "tokio", + "tower", + "url", + "wiremock", +] + [[package]] name = "bon" version = "3.9.1" @@ -544,6 +657,24 @@ dependencies = [ "syn", ] +[[package]] +name = "deadpool" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0be2b1d1d6ec8d846f05e137292d0b89133caf95ef33695424c09568bdd39b1b" +dependencies = [ + "deadpool-runtime", + "lazy_static", + "num_cpus", + "tokio", +] + +[[package]] +name = "deadpool-runtime" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" + [[package]] name = "der" version = "0.7.10" @@ -987,6 +1118,12 @@ dependencies = [ "foldhash", ] +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + [[package]] name = "hashbrown" version = "0.17.0" @@ -1019,6 +1156,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "hex" version = "0.4.3" @@ -1073,6 +1216,12 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + [[package]] name = "hyper" version = "1.9.0" @@ -1087,6 +1236,7 @@ dependencies = [ "http", "http-body", "httparse", + "httpdate", "itoa", "pin-project-lite", "smallvec", @@ -1533,6 +1683,12 @@ dependencies = [ "regex-automata", ] +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + [[package]] name = "memchr" version = "2.8.0" @@ -1661,6 +1817,16 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -1724,6 +1890,16 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + [[package]] name = "parking_lot_core" version = "0.9.12" @@ -1914,6 +2090,18 @@ dependencies = [ "yansi", ] +[[package]] +name = "quick_cache" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a70b1b8b47e31d0498ecbc3c5470bb931399a8bfed1fd79d1717a61ce7f96e3" +dependencies = [ + "ahash", + "equivalent", + "hashbrown 0.16.1", + "parking_lot", +] + [[package]] name = "quinn" version = "0.11.9" @@ -2911,6 +3099,7 @@ dependencies = [ "tokio", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -2954,6 +3143,7 @@ version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -3517,6 +3707,29 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +[[package]] +name = "wiremock" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08db1edfb05d9b3c1542e521aea074442088292f00b5f28e435c714a98f85031" +dependencies = [ + "assert-json-diff", + "base64", + "deadpool", + "futures", + "http", + "http-body-util", + "hyper", + "hyper-util", + "log", + "once_cell", + "regex", + "serde", + "serde_json", + "tokio", + "url", +] + [[package]] name = "wit-bindgen" version = "0.57.1" diff --git a/Cargo.toml b/Cargo.toml index 379f035..53b8c4c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,9 @@ members = [ "crates/types", "crates/edge-index", "crates/ingest", + "crates/slingshot-client", + "crates/record-lru", + "crates/xrpc", ] [workspace.package] @@ -16,6 +19,9 @@ rust-version = "1.95" bobbin-types = { path = "crates/types" } bobbin-edge-index = { path = "crates/edge-index" } bobbin-ingest = { path = "crates/ingest" } +bobbin-slingshot-client = { path = "crates/slingshot-client" } +bobbin-record-lru = { path = "crates/record-lru" } +bobbin-xrpc = { path = "crates/xrpc" } jacquard-common = "0.12.0-beta.2" jacquard-derive = "0.12.0-beta.2" @@ -30,16 +36,25 @@ tokio-tungstenite = { version = "0.29", features = ["rustls-tls-webpki-roots"] } futures = "0.3" serde = { version = "1", features = ["derive"] } -serde_json = "1" +serde_json = { version = "1", features = ["raw_value"] } serde_ipld_dagcbor = "0.6" chrono = { version = "0.4", features = ["serde"] } cid = "0.11" url = "2" +bytes = "1" scc = "3" roaring = "0.11" lasso = { version = "0.7", features = ["multi-threaded"] } +quick_cache = "0.6" + +reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-webpki-roots", "http2", "json", "gzip", "stream"] } +axum = "0.8" +tower = { version = "0.5", features = ["util"] } +http = "1" + +wiremock = "0.6" tracing = "0.1" diff --git a/crates/types/Cargo.toml b/crates/types/Cargo.toml index d863a38..0232d26 100644 --- a/crates/types/Cargo.toml +++ b/crates/types/Cargo.toml @@ -6,6 +6,7 @@ license.workspace = true rust-version.workspace = true [dependencies] +bytes = { workspace = true } chrono = { workspace = true } cid = { workspace = true } jacquard-common = { workspace = true } diff --git a/crates/types/build.rs b/crates/types/build.rs index 1af9fe7..a562e14 100644 --- a/crates/types/build.rs +++ b/crates/types/build.rs @@ -61,11 +61,7 @@ fn stage_lexicons(src: &Path, dst: &Path) -> Result<()> { .extension() .is_some_and(|ext| ext.eq_ignore_ascii_case("json")) }) - .filter(|e| { - !e.path() - .components() - .any(|c| c.as_os_str() == TEMP_SEGMENT) - }) + .filter(|e| !e.path().components().any(|c| c.as_os_str() == TEMP_SEGMENT)) .try_for_each(|entry| -> Result<()> { let rel = entry.path().strip_prefix(src).context("strip src prefix")?; let target = dst.join(rel); diff --git a/crates/types/src/edges.rs b/crates/types/src/edges.rs index e6bc245..b1ea7cf 100644 --- a/crates/types/src/edges.rs +++ b/crates/types/src/edges.rs @@ -45,7 +45,7 @@ pub struct Edge { pub enum ExtractError { #[error("decode JSON record body: {0}")] DecodeJson(#[from] serde_json::Error), - #[error("invalid AT-URI synthesised from record subject: {0}")] + #[error("invalid AT-URI synthesized from record subject: {0}")] InvalidAtUri(#[from] AtStrError), #[error("unknown collection NSID: {0}")] UnknownCollection(alloc::string::String), @@ -152,10 +152,7 @@ impl Record { nsid_static(s) } - pub fn extract_edges( - &self, - source: &AtUri, - ) -> Result, ExtractError> { + pub fn extract_edges(&self, source: &AtUri) -> Result, ExtractError> { match self { Self::Star(r) => star_edges(source, r), Self::Reaction(r) => reaction_edges(source, r), @@ -175,9 +172,7 @@ impl Record { Self::SpindleMember(r) => spindle_member_edges(source, r), Self::Pipeline(r) => pipeline_edges(source, r), Self::Knot(_) => Ok(owner_self_edges("sh.tangled.knot", source)), - Self::LabelDefinition(_) => { - Ok(owner_self_edges("sh.tangled.label.definition", source)) - } + Self::LabelDefinition(_) => Ok(owner_self_edges("sh.tangled.label.definition", source)), Self::PublicKey(_) => Ok(owner_self_edges("sh.tangled.publicKey", source)), Self::Repo(_) => Ok(owner_self_edges("sh.tangled.repo", source)), Self::Spindle(_) => Ok(owner_self_edges("sh.tangled.spindle", source)), @@ -396,7 +391,11 @@ fn pipeline_edges( ) -> Result, ExtractError> { let trigger_repo = &record.trigger_metadata.repo; let repo_did = trigger_repo.repo_did.as_ref().unwrap_or(&trigger_repo.did); - Ok(one_edge("sh.tangled.pipeline", did_to_aturi(repo_did)?, source)) + Ok(one_edge( + "sh.tangled.pipeline", + did_to_aturi(repo_did)?, + source, + )) } fn owner_self_edges(kind: &'static str, source: &AtUri) -> Vec { @@ -440,11 +439,14 @@ mod tests { "subject": {"$type": "sh.tangled.feed.star#repo", "did": "did:plc:abalone"} }), ); - assert_eq!(edges, vec![Edge { - kind: nsid("sh.tangled.feed.star"), - subject: at("at://did:plc:abalone"), - source: at("at://did:plc:olaren/sh.tangled.feed.star/abcabcabcabcz"), - }]); + assert_eq!( + edges, + vec![Edge { + kind: nsid("sh.tangled.feed.star"), + subject: at("at://did:plc:abalone"), + source: at("at://did:plc:olaren/sh.tangled.feed.star/abcabcabcabcz"), + }] + ); } #[test] @@ -462,7 +464,10 @@ mod tests { }), ); assert_eq!(edges.len(), 1); - assert_eq!(edges[0].subject, at("at://did:plc:teq/sh.tangled.string/k1")); + assert_eq!( + edges[0].subject, + at("at://did:plc:teq/sh.tangled.string/k1") + ); } #[test] @@ -510,7 +515,10 @@ mod tests { }), ); assert_eq!(edges.len(), 1); - assert_eq!(edges[0].subject, at("at://did:plc:nel/sh.tangled.repo.issue/3lk1")); + assert_eq!( + edges[0].subject, + at("at://did:plc:nel/sh.tangled.repo.issue/3lk1") + ); } #[test] @@ -544,8 +552,12 @@ mod tests { } }); let obj = body.as_object_mut().expect("artifact_body returns object"); - if let Some(r) = repo { obj.insert("repo".into(), json!(r)); } - if let Some(d) = repo_did { obj.insert("repoDid".into(), json!(d)); } + if let Some(r) = repo { + obj.insert("repo".into(), json!(r)); + } + if let Some(d) = repo_did { + obj.insert("repoDid".into(), json!(d)); + } body } @@ -554,10 +566,16 @@ mod tests { let edges = extract( "sh.tangled.repo.artifact", "at://did:plc:nel/sh.tangled.repo.artifact/abcabcabcabcz", - artifact_body(Some("at://did:plc:abalone/sh.tangled.repo/r1"), Some("did:plc:lyna")), + artifact_body( + Some("at://did:plc:abalone/sh.tangled.repo/r1"), + Some("did:plc:lyna"), + ), ); assert_eq!(edges.len(), 1); - assert_eq!(edges[0].subject, at("at://did:plc:abalone/sh.tangled.repo/r1")); + assert_eq!( + edges[0].subject, + at("at://did:plc:abalone/sh.tangled.repo/r1") + ); } #[test] @@ -592,20 +610,35 @@ mod tests { "createdAt": "2026-05-01T00:00:00Z" }), ); - assert_eq!(edges, vec![Edge { - kind: nsid("sh.tangled.repo"), - subject: at("at://did:plc:teq"), - source: at("at://did:plc:teq/sh.tangled.repo/r1"), - }]); + assert_eq!( + edges, + vec![Edge { + kind: nsid("sh.tangled.repo"), + subject: at("at://did:plc:teq"), + source: at("at://did:plc:teq/sh.tangled.repo/r1"), + }] + ); } #[test] fn knot_spindle_publickey_string_label_all_self_edge_to_owner() { let cases = [ - ("sh.tangled.knot", json!({"$type": "sh.tangled.knot", "createdAt": "2026-05-01T00:00:00Z"})), - ("sh.tangled.spindle", json!({"$type": "sh.tangled.spindle", "createdAt": "2026-05-01T00:00:00Z"})), - ("sh.tangled.publicKey", json!({"$type": "sh.tangled.publicKey", "createdAt": "2026-05-01T00:00:00Z", "key": "ssh-ed25519 AAA", "name": "laptop"})), - ("sh.tangled.string", json!({"$type": "sh.tangled.string", "createdAt": "2026-05-01T00:00:00Z", "filename": "f.txt", "description": "x", "contents": "hi"})), + ( + "sh.tangled.knot", + json!({"$type": "sh.tangled.knot", "createdAt": "2026-05-01T00:00:00Z"}), + ), + ( + "sh.tangled.spindle", + json!({"$type": "sh.tangled.spindle", "createdAt": "2026-05-01T00:00:00Z"}), + ), + ( + "sh.tangled.publicKey", + json!({"$type": "sh.tangled.publicKey", "createdAt": "2026-05-01T00:00:00Z", "key": "ssh-ed25519 AAA", "name": "laptop"}), + ), + ( + "sh.tangled.string", + json!({"$type": "sh.tangled.string", "createdAt": "2026-05-01T00:00:00Z", "filename": "f.txt", "description": "x", "contents": "hi"}), + ), ( "sh.tangled.label.definition", json!({ diff --git a/crates/types/src/ids.rs b/crates/types/src/ids.rs index f47bcb9..5218532 100644 --- a/crates/types/src/ids.rs +++ b/crates/types/src/ids.rs @@ -1,7 +1,7 @@ +use jacquard_common::DefaultStr; use jacquard_common::types::did::Did; use jacquard_common::types::nsid::Nsid; use jacquard_common::types::string::AtUri; -use jacquard_common::DefaultStr; #[derive(Clone, Debug, Eq, PartialEq, Hash)] pub struct EdgeKey { @@ -39,7 +39,10 @@ mod tests { #[test] fn extract_did_from_did_only_uri() { - assert_eq!(did_from_aturi("at://did:plc:olaren"), Some(did("did:plc:olaren"))); + assert_eq!( + did_from_aturi("at://did:plc:olaren"), + Some(did("did:plc:olaren")) + ); } #[test] diff --git a/crates/types/src/lib.rs b/crates/types/src/lib.rs index 8e81fbf..4dead74 100644 --- a/crates/types/src/lib.rs +++ b/crates/types/src/lib.rs @@ -9,7 +9,7 @@ extern crate alloc; clippy::new_ret_no_self, clippy::new_without_default, clippy::should_implement_trait, - clippy::type_complexity, + clippy::type_complexity )] mod _lex; @@ -17,3 +17,4 @@ pub use _lex::*; pub mod edges; pub mod ids; +pub mod record; diff --git a/crates/types/src/record.rs b/crates/types/src/record.rs new file mode 100644 index 0000000..46e8373 --- /dev/null +++ b/crates/types/src/record.rs @@ -0,0 +1,21 @@ +use bytes::Bytes; +use jacquard_common::DefaultStr; +use jacquard_common::types::string::{AtUri, Cid}; + +#[derive(Clone, Debug)] +pub struct RecordBody { + pub uri: AtUri, + pub cid: Cid, + pub value: Bytes, +} + +impl RecordBody { + const FIXED_OVERHEAD: u64 = 128; + + pub fn weight(&self) -> u64 { + Self::FIXED_OVERHEAD + + self.value.len() as u64 + + self.uri.as_ref().len() as u64 + + self.cid.as_ref().len() as u64 + } +}