From 5dcbf57d150182892294724f4f8c85ea1aebf653 Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Sat, 29 Mar 2025 20:32:25 -0400 Subject: [PATCH 1/7] wip: initial web server wiring --- Cargo.lock | 1 + Cargo.toml | 8 ++++++++ src/lib.rs | 20 +++++++++++++++++--- src/main.rs | 5 +++++ 4 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 src/main.rs diff --git a/Cargo.lock b/Cargo.lock index ae3ef42..ebe097c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -304,6 +304,7 @@ version = "0.1.0" dependencies = [ "axum", "serde", + "tokio", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index b2faf43..2cc5069 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,14 @@ name = "offline-web" version = "0.1.0" edition = "2021" +[lib] +path = "src/lib.rs" + +[[bin]] +name = "example" +path = "src/main.rs" + [dependencies] axum = "0.8.3" serde = { version = "1.0.219", features = ["derive", "rc"] } +tokio = "1.44.1" diff --git a/src/lib.rs b/src/lib.rs index 9175220..4e758f2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,10 +1,24 @@ +use std::rc::Rc; + use axum::{ routing::get, - Router + Router, + Json, }; +use datamodel::Reference; mod datamodel; -pub fn endpoints() -> Router { - Router::new().route("/api/v1/ref/all/username", get(|| async { "hello world" })) +async fn all_references() -> Json> { + Json(datamodel::Reference::new("silly id".to_string(), "all/username".to_string()).to_rc()) +} + +pub fn endpoints() -> Router { + Router::new().route("/api/v1/ref/all/username", get(all_references)) +} + +pub async fn serve() { + // run our app with hyper, listening globally on port 3000 + let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap(); + axum::serve(listener, endpoints()).await.unwrap(); } diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..7a64137 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,5 @@ + +#[tokio::main(flavor = "current_thread")] +async fn main() { + offline_web::serve().await; +} From 275590709acb44705be95cb91fb59816bed3ba7c Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Sun, 30 Mar 2025 15:02:46 -0400 Subject: [PATCH 2/7] wip: reference and oject endpoints --- DESIGN.md | 2 +- src/datamodel/mod.rs | 4 ++- src/lib.rs | 82 +++++++++++++++++++++++++++++++++++++++----- 3 files changed, 78 insertions(+), 10 deletions(-) diff --git a/DESIGN.md b/DESIGN.md index 6885137..bab44ab 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -90,7 +90,7 @@ operations. ### Bootstrapping -* Load `/api/v1/resource/all/` and then follow the sub resources to +* Load `/api/v1/ref/all/` and then follow the sub resources to load the entire dataset locally making sure to keep the content-addresses around for comparison. diff --git a/src/datamodel/mod.rs b/src/datamodel/mod.rs index 3b146e9..e8016a5 100644 --- a/src/datamodel/mod.rs +++ b/src/datamodel/mod.rs @@ -5,15 +5,17 @@ use serde::{Serialize, Deserialize}; #[derive(Serialize, Deserialize)] pub struct Reference { object_id: String, + content_address: String, path: String, #[serde(skip_serializing_if = "Vec::is_empty")] dependents: Vec>, } impl Reference { - pub fn new(object_id: String, path: String) -> Self { + pub fn new(object_id: String, content_address: String, path: String) -> Self { Self { object_id, + content_address, path, dependents: Vec::new(), } diff --git a/src/lib.rs b/src/lib.rs index 4e758f2..3bb3232 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,24 +1,90 @@ use std::rc::Rc; -use axum::{ - routing::get, - Router, - Json, -}; +use axum::{extract::Path, routing::get, Json, Router}; use datamodel::Reference; mod datamodel; async fn all_references() -> Json> { - Json(datamodel::Reference::new("silly id".to_string(), "all/username".to_string()).to_rc()) + let path_root = String::from("ref/0"); + let mut root_ref = Reference::new( + "username:0".to_string(), + String::from("0"), + path_root.clone(), + ); + for i in 1..=10 { + let mut item_ref = Reference::new( + format!("item:{}", i), + format!("0:{}", i), + format!("{}/item/{}", path_root, i), + ); + for j in 1..=10 { + item_ref = item_ref.add_dep(Rc::new(Reference::new( + format!("item:{}:subitem:{}", i, j), + format!("0:{}:{}", i, j), + format!("{}/item/{}/subitem/{}", path_root, i, j), + ))); + } + root_ref = root_ref.add_dep(Rc::new(item_ref)); + } + Json(root_ref.to_rc()) +} + +async fn dummy_item_ref(Path(i): Path) -> Json> { + let path_root = String::from("ref/0"); + let mut item_ref = Reference::new( + format!("item:{}", i), + format!("0:{}", i), + format!("{}/item/{}", path_root, i), + ); + for j in 1..=10 { + item_ref = item_ref.add_dep(Rc::new(Reference::new( + format!("item:{}:subitem:{}", i, j), + format!("0:{}:{}", i, j), + format!("{}/item/{}/subitem/{}", path_root, i, j), + ))); + } + Json(item_ref.to_rc()) +} + +async fn dummy_subitem_ref(Path(i): Path, Path(j): Path) -> Json> { + let path_root = String::from("ref/0"); + Json( + Reference::new( + format!("item:{}:subitem:{}", i, j), + format!("0:{}:{}", i, j), + format!("{}/item/{}/subitem/{}", path_root, i, j), + ) + .to_rc(), + ) +} + +async fn dummy_object(Path(addr): Path) -> String { + format!("I am object {}", addr) } pub fn endpoints() -> Router { - Router::new().route("/api/v1/ref/all/username", get(all_references)) + Router::new().nest( + "/api/v1", + Router::new().nest( + "/ref", + Router::new() + .route("/all/username", get(all_references)) + .route("/item/{i}", get(dummy_item_ref)) + .route("/item/{i}/subitem/{j}", get(dummy_subitem_ref)) + ).nest( + "/object", + Router::new() + .route("/{addr}", get(dummy_object)) + ), + ) } +// TODO(jwall): Javascript test script pub async fn serve() { // run our app with hyper, listening globally on port 3000 - let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap(); + let listener = tokio::net::TcpListener::bind("127.0.0.1:3000") + .await + .unwrap(); axum::serve(listener, endpoints()).await.unwrap(); } From 943bff4b3d8c9d09f054035924e780a2a887dac2 Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Sun, 30 Mar 2025 15:11:26 -0400 Subject: [PATCH 3/7] docs: update design notes --- DESIGN.md | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/DESIGN.md b/DESIGN.md index bab44ab..981549f 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -21,11 +21,20 @@ Resource reference paths are rooted at `/api/v1/ref/`. ```json { - "objectId": , + "objectId": , + "content_address": , "path": "/path/name0", "dependents": [ - { "path": "path/name1", "objectId": , content: }, - { "path": "path/name2", "objectId": , content: } + { + "path": "path/name1", + "content_address": , + "objectId": , content: + }, + { + "path": "path/name2", + "content_address": , + "objectId": , content: + } ], } ``` @@ -49,17 +58,25 @@ content key. ```json { - "objectId": , + "objectId": , "path": "/path/name0", "dependents": [ { - "objectId": , + "objectId": , "path": "path/name1", "content": { - "objectId": , + "objectId": , "dependents": [ - { "path": "path/name1", "objectId": , content: }, - { "path": "path/name2", "objectId": , content: } + { + "path": "path/name1", + "content_address": , + "objectId": , content: + }, + { + "path": "path/name2", + "content_address": , + "objectId": , content: + } ], } } From 2c0453411e52eaf1b3fa7bd293eda060beda1fef Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Sun, 30 Mar 2025 21:18:08 -0400 Subject: [PATCH 4/7] wip: javascript benchmarking script --- src/lib.rs | 11 ++++++- static/client.js | 81 +++++++++++++++++++++++++++++++++++++++++++++++ static/index.html | 26 +++++++++++++++ 3 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 static/client.js create mode 100644 static/index.html diff --git a/src/lib.rs b/src/lib.rs index 3bb3232..9f26d50 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,6 @@ use std::rc::Rc; -use axum::{extract::Path, routing::get, Json, Router}; +use axum::{extract::Path, http::{self, Response}, response::{AppendHeaders, Html, IntoResponse}, routing::get, Json, Router}; use datamodel::Reference; mod datamodel; @@ -63,6 +63,13 @@ async fn dummy_object(Path(addr): Path) -> String { format!("I am object {}", addr) } +async fn get_client_js() -> impl IntoResponse { + ( + [(http::header::CONTENT_TYPE, "application/javascript")], + include_str!("../static/client.js"), + ) +} + pub fn endpoints() -> Router { Router::new().nest( "/api/v1", @@ -78,6 +85,8 @@ pub fn endpoints() -> Router { .route("/{addr}", get(dummy_object)) ), ) + .route("/lib/client.js", get(get_client_js)) + .route("/ui/", get(|| async { Html(include_str!("../static/index.html")).into_response() })) } // TODO(jwall): Javascript test script diff --git a/static/client.js b/static/client.js new file mode 100644 index 0000000..a6f4a6f --- /dev/null +++ b/static/client.js @@ -0,0 +1,81 @@ +export { bootstrap }; +// 1. Record a start time. +// 2. Load the bootstrap document. +// 3. Load all the content_addresses from the bootstrap. +// 4. Record the end time. + +async function load_bootstrap() { + let response = await fetch("/api/v1/ref/all/username"); + if (!response.ok) { + throw new Error("Network response was not ok: " + response.statusText); + } + return await response.json(); +} + +/** + * @param {String} dbName + * @param {Array} storeNames + * @returns {Promise} + */ +async function openDatabase(dbName, storeNames) { + return await new Promise((resolve, reject) => { + const request = indexedDB.open(dbName, 1); + + request.onupgradeneeded = (event) => { + const db = event.target.result; + for (var storeName of storeNames) { + // Create the object store if it doesn't exist + if (!db.objectStoreNames.contains(storeName)) { + db.createObjectStore(storeName); + } + } + }; + + request.onsuccess = (event) => { + const db = event.target.result; + resolve(db); + }; + + request.onerror = (event) => { + reject(event.target.error); + }; + }); +} + +/** + * @param {IDBObjectStore} store + * @param {Object} reference +*/ +function load_reference_paths(store, reference) { + let root_path = reference.path; + return new Promise(async (resolve, reject) => { + if (reference.dependents) { + for (var dep of reference.dependents) { + await load_reference_paths(store, dep); + } + } + + const request = store.put(JSON.stringify(reference), root_path); + request.onerror = (evt) => { + reject(evt.target.error); + console.log("Failed to store object", evt); + }; + request.onsuccess = (evt) => { + resolve(evt.target.result); + }; + }); +} + +async function bootstrap() { + const refStoreName = "references" + const objectStoreName = "objects" + const databaseName = "MerkleStore"; + const start = new Date().getTime(); + let root = await load_bootstrap(); + let db = await openDatabase(databaseName, [refStoreName, objectStoreName]); + const transaction = db.transaction([refStoreName], "readwrite"); + const store = transaction.objectStore(refStoreName); + load_reference_paths(store, root); + var end = new Date().getTime(); + return end - start; +} diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..321d1a4 --- /dev/null +++ b/static/index.html @@ -0,0 +1,26 @@ + + + + + + Bootstrap Time Display + + +

Bootstrap Time

+
Loading...
+ + + + + From e4a53e9f92c8b83414b7c0a7cd262ef1da259a57 Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Tue, 1 Apr 2025 20:38:35 -0400 Subject: [PATCH 5/7] wip: object loading --- static/client.js | 111 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 91 insertions(+), 20 deletions(-) diff --git a/static/client.js b/static/client.js index a6f4a6f..7ab0fa4 100644 --- a/static/client.js +++ b/static/client.js @@ -1,8 +1,12 @@ export { bootstrap }; -// 1. Record a start time. -// 2. Load the bootstrap document. -// 3. Load all the content_addresses from the bootstrap. -// 4. Record the end time. + +/** + * @typedef {Object} Reference + * @property {Array} dependents + * @property {string} path + * @property {string} object_id + * @property {string} content_address + */ async function load_bootstrap() { let response = await fetch("/api/v1/ref/all/username"); @@ -43,18 +47,14 @@ async function openDatabase(dbName, storeNames) { } /** + * Stores a reference object in the IndexedDB. * @param {IDBObjectStore} store * @param {Object} reference -*/ -function load_reference_paths(store, reference) { - let root_path = reference.path; - return new Promise(async (resolve, reject) => { - if (reference.dependents) { - for (var dep of reference.dependents) { - await load_reference_paths(store, dep); - } - } - + * @param {string} root_path + * @returns {Promise} + */ +function storeObject(store, reference, root_path) { + return new Promise((resolve, reject) => { const request = store.put(JSON.stringify(reference), root_path); request.onerror = (evt) => { reject(evt.target.error); @@ -66,16 +66,87 @@ function load_reference_paths(store, reference) { }); } +/** + * @param {IDBObjectStore} refStore + * @param {Object} reference + * @returns {Promise>} An array of references +*/ +function load_reference_paths(refStore, reference) { + return new Promise(async (resolve, reject) => { + let references = []; + references.push(reference); + if (reference.dependents) { + for (var dep of reference.dependents) { + references = references.concat(await load_reference_paths(refStore, dep)); + } + } + + await storeObject(refStore, reference, reference.path); + resolve(references); + }); +} + +/** + * @param {IDBDatabase} db + * @param {string} storeName + * @param {Array} references +*/ +async function load_objects_and_store(db, references, storeName) { + let objects = [] + for (var ref of references) { + /** @type {Response} */ + let response = await fetch("/api/v1/object/" + ref.content_address); + if (!response.ok) { + throw new Error("Network response was not ok: " + response.statusText); + } + let object = await response.text(); + objects.push({ id: ref.content_address, content: object }); + } + const objectTrxAndStore = await getStoreAndTransaction(db, storeName); + for (var obj of objects) { + await storeObject(objectTrxAndStore.store, obj.content, obj.id); + } + await new Promise((resolve, reject) => { + objectTrxAndStore.trx.oncomplete = () => resolve(); + objectTrxAndStore.trx.onerror = (event) => reject(event.target.error); + }); +} + +/** + * @param {string} storeName + * @param {IDBDatabase} db + * @returns {Promise<{trx: IDBTransaction, store: IDBObjectStore}>} The transaction and object store. + */ +async function getStoreAndTransaction(db, storeName) { + const transaction = db.transaction([storeName], "readwrite"); + return { trx: transaction, store: transaction.objectStore(storeName) }; +} + +/** +* @returns {Number} The number of milliseconds it took to bootstrap. +*/ async function bootstrap() { - const refStoreName = "references" - const objectStoreName = "objects" + const refStoreName = "references"; + const objectStoreName = "objects"; const databaseName = "MerkleStore"; const start = new Date().getTime(); let root = await load_bootstrap(); - let db = await openDatabase(databaseName, [refStoreName, objectStoreName]); - const transaction = db.transaction([refStoreName], "readwrite"); - const store = transaction.objectStore(refStoreName); - load_reference_paths(store, root); + const db = await openDatabase(databaseName, [refStoreName, objectStoreName]); + const refTrxAndStore = await getStoreAndTransaction(db, refStoreName); + + // Use a promise to wait for the transaction to complete + const transactionComplete = new Promise((resolve, reject) => { + refTrxAndStore.trx.oncomplete = () => resolve(); + refTrxAndStore.trx.onerror = (event) => reject(event.target.error); + }); + + let refs = await load_reference_paths(refTrxAndStore.store, root); + + // Wait for the transaction to complete + await transactionComplete; + + await load_objects_and_store(db, refs, objectStoreName); + var end = new Date().getTime(); return end - start; } From be288dfcd633d81c54d2ea6087cd5939e341b2a0 Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Tue, 1 Apr 2025 21:34:58 -0400 Subject: [PATCH 6/7] wip: make the payloads for objects random --- Cargo.lock | 104 ++++++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 1 + src/lib.rs | 10 ++++- static/client.js | 8 ++-- 4 files changed, 116 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ebe097c..eda1f29 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -86,6 +86,12 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "bitflags" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" + [[package]] name = "bytes" version = "1.10.1" @@ -146,6 +152,18 @@ dependencies = [ "pin-utils", ] +[[package]] +name = "getrandom" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", +] + [[package]] name = "gimli" version = "0.31.1" @@ -285,7 +303,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys", ] @@ -303,6 +321,7 @@ name = "offline-web" version = "0.1.0" dependencies = [ "axum", + "rand", "serde", "tokio", ] @@ -331,6 +350,15 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "proc-macro2" version = "1.0.94" @@ -349,6 +377,42 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + +[[package]] +name = "rand" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" +dependencies = [ + "rand_chacha", + "rand_core", + "zerocopy", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -540,6 +604,15 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -612,3 +685,32 @@ name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] + +[[package]] +name = "zerocopy" +version = "0.8.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index 2cc5069..73259d1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,5 +12,6 @@ path = "src/main.rs" [dependencies] axum = "0.8.3" +rand = "0.9.0" serde = { version = "1.0.219", features = ["derive", "rc"] } tokio = "1.44.1" diff --git a/src/lib.rs b/src/lib.rs index 9f26d50..cc5732b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,6 @@ use std::rc::Rc; -use axum::{extract::Path, http::{self, Response}, response::{AppendHeaders, Html, IntoResponse}, routing::get, Json, Router}; +use axum::{extract::Path, http, response::{Html, IntoResponse}, routing::get, Json, Router}; use datamodel::Reference; mod datamodel; @@ -60,7 +60,13 @@ async fn dummy_subitem_ref(Path(i): Path, Path(j): Path) -> Json) -> String { - format!("I am object {}", addr) + use rand::Rng; + let mut rng = rand::rng(); + let random_size = rng.random_range(50..=4096); + let random_string: String = (0..random_size) + .map(|_| rng.sample(rand::distr::Alphanumeric) as char) + .collect(); + format!("I am object {} -- {}", addr, random_string) } async fn get_client_js() -> impl IntoResponse { diff --git a/static/client.js b/static/client.js index 7ab0fa4..b140133 100644 --- a/static/client.js +++ b/static/client.js @@ -99,7 +99,7 @@ async function load_objects_and_store(db, references, storeName) { if (!response.ok) { throw new Error("Network response was not ok: " + response.statusText); } - let object = await response.text(); + const object = await response.text(); objects.push({ id: ref.content_address, content: object }); } const objectTrxAndStore = await getStoreAndTransaction(db, storeName); @@ -130,7 +130,7 @@ async function bootstrap() { const objectStoreName = "objects"; const databaseName = "MerkleStore"; const start = new Date().getTime(); - let root = await load_bootstrap(); + const root = await load_bootstrap(); const db = await openDatabase(databaseName, [refStoreName, objectStoreName]); const refTrxAndStore = await getStoreAndTransaction(db, refStoreName); @@ -140,13 +140,13 @@ async function bootstrap() { refTrxAndStore.trx.onerror = (event) => reject(event.target.error); }); - let refs = await load_reference_paths(refTrxAndStore.store, root); + const refs = await load_reference_paths(refTrxAndStore.store, root); // Wait for the transaction to complete await transactionComplete; await load_objects_and_store(db, refs, objectStoreName); - var end = new Date().getTime(); + const end = new Date().getTime(); return end - start; } From 1c8c593de33850908e7d04f795f29e93f744133e Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Wed, 2 Apr 2025 18:23:03 -0400 Subject: [PATCH 7/7] wip: use proper hashes --- Cargo.lock | 80 ++++++++++++++++++++++ Cargo.toml | 3 +- src/datamodel/mod.rs | 22 +++--- src/lib.rs | 160 ++++++++++++++++++++++++++----------------- static/client.js | 3 + 5 files changed, 195 insertions(+), 73 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index eda1f29..82ff06e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -24,6 +24,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de45108900e1f9b9242f7f2e254aa3e2c029c921c258fe9e6b4217eeebd54288" dependencies = [ "axum-core", + "axum-macros", "bytes", "form_urlencoded", "futures-util", @@ -71,6 +72,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "backtrace" version = "0.3.74" @@ -92,6 +104,24 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bytes" version = "1.10.1" @@ -104,6 +134,27 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + [[package]] name = "fnv" version = "1.0.7" @@ -152,6 +203,16 @@ dependencies = [ "pin-utils", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.3.2" @@ -321,6 +382,7 @@ name = "offline-web" version = "0.1.0" dependencies = [ "axum", + "blake2", "rand", "serde", "tokio", @@ -501,6 +563,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "2.0.100" @@ -592,12 +660,24 @@ dependencies = [ "once_cell", ] +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + [[package]] name = "unicode-ident" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" diff --git a/Cargo.toml b/Cargo.toml index 73259d1..3dac612 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,8 @@ name = "example" path = "src/main.rs" [dependencies] -axum = "0.8.3" +axum = { version = "0.8.3", features = ["macros"] } +blake2 = "0.10.6" rand = "0.9.0" serde = { version = "1.0.219", features = ["derive", "rc"] } tokio = "1.44.1" diff --git a/src/datamodel/mod.rs b/src/datamodel/mod.rs index e8016a5..ab22961 100644 --- a/src/datamodel/mod.rs +++ b/src/datamodel/mod.rs @@ -1,14 +1,14 @@ -use std::rc::Rc; +use std::sync::Arc; use serde::{Serialize, Deserialize}; -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Debug)] pub struct Reference { - object_id: String, - content_address: String, - path: String, + pub object_id: String, + pub content_address: String, + pub path: String, #[serde(skip_serializing_if = "Vec::is_empty")] - dependents: Vec>, + pub dependents: Vec>, } impl Reference { @@ -21,12 +21,16 @@ impl Reference { } } - pub fn add_dep(mut self, dep: Rc) -> Self { + pub fn add_dep(mut self, dep: Arc) -> Self { self.dependents.push(dep); self } - pub fn to_rc(self) -> Rc { - Rc::new(self) + pub fn to_arc(self) -> Arc { + Arc::new(self) + } + + pub fn is_leaf(&self) -> bool { + return self.dependents.is_empty(); } } diff --git a/src/lib.rs b/src/lib.rs index cc5732b..6bf8767 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,72 +1,97 @@ -use std::rc::Rc; +use std::{collections::HashMap, sync::Arc}; use axum::{extract::Path, http, response::{Html, IntoResponse}, routing::get, Json, Router}; -use datamodel::Reference; +use blake2::{Blake2b512, Digest}; +use rand::Rng; +use datamodel::Reference; mod datamodel; -async fn all_references() -> Json> { - let path_root = String::from("ref/0"); - let mut root_ref = Reference::new( - "username:0".to_string(), - String::from("0"), - path_root.clone(), - ); - for i in 1..=10 { - let mut item_ref = Reference::new( - format!("item:{}", i), - format!("0:{}", i), - format!("{}/item/{}", path_root, i), - ); - for j in 1..=10 { - item_ref = item_ref.add_dep(Rc::new(Reference::new( - format!("item:{}:subitem:{}", i, j), - format!("0:{}:{}", i, j), - format!("{}/item/{}/subitem/{}", path_root, i, j), - ))); - } - root_ref = root_ref.add_dep(Rc::new(item_ref)); - } - Json(root_ref.to_rc()) +#[derive(Debug)] +pub struct AddressableObject { + pub address: String, + pub content: String, } -async fn dummy_item_ref(Path(i): Path) -> Json> { - let path_root = String::from("ref/0"); - let mut item_ref = Reference::new( - format!("item:{}", i), - format!("0:{}", i), - format!("{}/item/{}", path_root, i), - ); - for j in 1..=10 { - item_ref = item_ref.add_dep(Rc::new(Reference::new( - format!("item:{}:subitem:{}", i, j), - format!("0:{}:{}", i, j), - format!("{}/item/{}/subitem/{}", path_root, i, j), - ))); - } - Json(item_ref.to_rc()) -} - -async fn dummy_subitem_ref(Path(i): Path, Path(j): Path) -> Json> { - let path_root = String::from("ref/0"); - Json( - Reference::new( - format!("item:{}:subitem:{}", i, j), - format!("0:{}:{}", i, j), - format!("{}/item/{}/subitem/{}", path_root, i, j), - ) - .to_rc(), - ) -} - -async fn dummy_object(Path(addr): Path) -> String { - use rand::Rng; +fn random_object() -> AddressableObject { let mut rng = rand::rng(); let random_size = rng.random_range(50..=4096); let random_string: String = (0..random_size) .map(|_| rng.sample(rand::distr::Alphanumeric) as char) .collect(); - format!("I am object {} -- {}", addr, random_string) + + let mut hasher = Blake2b512::new(); + hasher.update(&random_string); + let hash = format!("{:x}", hasher.finalize()); + + AddressableObject { + address: hash, + content: random_string, + } +} + +fn random_references_and_objects() -> (Arc, Arc>>, Arc>) { + let path_root = String::from("ref/0"); + let mut objects = HashMap::new(); + let mut refs = HashMap::new(); + let mut root_ref = Reference::new( + "username:0".to_string(), + String::from("0"), + path_root.clone(), + ); + let mut root_hasher = Blake2b512::new(); + for i in 1..=10 { + let mut item_ref = Reference::new( + format!("item:{}", i), + format!("0:{}", i), + format!("/item/{}", i), + ); + let mut hasher = Blake2b512::new(); + for j in 1..=10 { + let object = random_object(); + hasher.update(&object.content); + let leaf_ref = Reference::new( + format!("item:{}:subitem:{}", i, j), + format!("{}", object.address), + format!("/item/{}/subitem/{}", i, j), + ).to_arc(); + item_ref = item_ref.add_dep(leaf_ref.clone()); + objects.insert(object.address.clone(), object); + hasher.update(&leaf_ref.content_address); + refs.insert(leaf_ref.path.clone(), leaf_ref); + } + let hash = format!("{:x}", hasher.finalize()); + item_ref.content_address = hash; + root_hasher.update(&item_ref.content_address); + let rc_ref = item_ref.to_arc(); + root_ref = root_ref.add_dep(rc_ref.clone()); + refs.insert(rc_ref.path.clone(), rc_ref); + } + root_ref.content_address = format!("{:x}", root_hasher.finalize()); + let rc_root = root_ref.to_arc(); + refs.insert(rc_root.path.clone(), rc_root.clone()); + dbg!(&objects); + (rc_root, Arc::new(refs), Arc::new(objects)) +} + +async fn all_references(root_ref: Arc) -> Json> { + Json(root_ref) +} + +async fn ref_path(refs: Arc>>, Path(path): Path) -> Json> { + let path = format!("/item/{}", path); + match refs.get(&path) { + Some(r) => Json(r.clone()), + None => todo!("Return a 404?"), + } +} + +async fn object_path(objects: Arc>, Path(path): Path) -> String { + dbg!(&path); + match objects.get(&path) { + Some(o) => o.content.clone(), + None => todo!("Return a 404?"), + } } async fn get_client_js() -> impl IntoResponse { @@ -76,19 +101,27 @@ async fn get_client_js() -> impl IntoResponse { ) } -pub fn endpoints() -> Router { +pub fn endpoints(root_ref: Arc, refs: Arc>>, objects: Arc>) -> Router { Router::new().nest( "/api/v1", Router::new().nest( "/ref", Router::new() - .route("/all/username", get(all_references)) - .route("/item/{i}", get(dummy_item_ref)) - .route("/item/{i}/subitem/{j}", get(dummy_subitem_ref)) + .route("/all/username", get({ + let state = root_ref.clone(); + move || all_references(state) + })) + .route("/item/{*path}", get({ + let refs = refs.clone(); + move |path| ref_path(refs, path) + })) ).nest( "/object", Router::new() - .route("/{addr}", get(dummy_object)) + .route("/{addr}", get({ + let objects = objects.clone(); + move |path| object_path(objects, path) + })) ), ) .route("/lib/client.js", get(get_client_js)) @@ -98,8 +131,9 @@ pub fn endpoints() -> Router { // TODO(jwall): Javascript test script pub async fn serve() { // run our app with hyper, listening globally on port 3000 + let (root_ref, refs, objects) = random_references_and_objects(); let listener = tokio::net::TcpListener::bind("127.0.0.1:3000") .await .unwrap(); - axum::serve(listener, endpoints()).await.unwrap(); + axum::serve(listener, endpoints(root_ref, refs, objects)).await.unwrap(); } diff --git a/static/client.js b/static/client.js index b140133..82de876 100644 --- a/static/client.js +++ b/static/client.js @@ -95,6 +95,9 @@ async function load_objects_and_store(db, references, storeName) { let objects = [] for (var ref of references) { /** @type {Response} */ + if (ref.dependents && ref.dependents.length != 0) { + continue; // not a leaf object + } let response = await fetch("/api/v1/object/" + ref.content_address); if (!response.ok) { throw new Error("Network response was not ok: " + response.statusText);