chore: move to a workspace setup

This commit is contained in:
Jeremy Wall 2025-04-12 15:53:40 -04:00
parent 8d5ea957a6
commit 350479bb18
16 changed files with 527 additions and 34 deletions

160
Cargo.lock generated
View File

@ -17,6 +17,12 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
[[package]]
name = "autocfg"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "axum"
version = "0.8.3"
@ -25,6 +31,7 @@ checksum = "de45108900e1f9b9242f7f2e254aa3e2c029c921c258fe9e6b4217eeebd54288"
dependencies = [
"axum-core",
"axum-macros",
"base64",
"bytes",
"form_urlencoded",
"futures-util",
@ -44,8 +51,10 @@ dependencies = [
"serde_json",
"serde_path_to_error",
"serde_urlencoded",
"sha1",
"sync_wrapper",
"tokio",
"tokio-tungstenite",
"tower",
"tower-layer",
"tower-service",
@ -98,6 +107,12 @@ dependencies = [
"windows-targets",
]
[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bitflags"
version = "2.9.0"
@ -134,6 +149,15 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "cpufeatures"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
dependencies = [
"libc",
]
[[package]]
name = "crypto-common"
version = "0.1.6"
@ -144,6 +168,12 @@ dependencies = [
"typenum",
]
[[package]]
name = "data-encoding"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
[[package]]
name = "digest"
version = "0.10.7"
@ -185,6 +215,12 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
[[package]]
name = "futures-sink"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
[[package]]
name = "futures-task"
version = "0.3.31"
@ -198,9 +234,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [
"futures-core",
"futures-sink",
"futures-task",
"pin-project-lite",
"pin-utils",
"slab",
]
[[package]]
@ -298,9 +336,9 @@ dependencies = [
[[package]]
name = "hyper-util"
version = "0.1.10"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4"
checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2"
dependencies = [
"bytes",
"futures-util",
@ -350,9 +388,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "miniz_oxide"
version = "0.8.5"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5"
checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a"
dependencies = [
"adler2",
]
@ -378,11 +416,31 @@ dependencies = [
]
[[package]]
name = "offline-web"
name = "offline-web-http"
version = "0.1.0"
dependencies = [
"axum",
"blake2",
"offline-web-model",
"rand",
"serde",
"tokio",
]
[[package]]
name = "offline-web-model"
version = "0.1.0"
dependencies = [
"serde",
]
[[package]]
name = "offline-web-ws"
version = "0.1.0"
dependencies = [
"axum",
"blake2",
"offline-web-model",
"rand",
"serde",
"tokio",
@ -548,16 +606,36 @@ dependencies = [
]
[[package]]
name = "smallvec"
version = "1.14.0"
name = "sha1"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd"
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "slab"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
dependencies = [
"autocfg",
]
[[package]]
name = "smallvec"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
[[package]]
name = "socket2"
version = "0.5.8"
version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8"
checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef"
dependencies = [
"libc",
"windows-sys",
@ -587,12 +665,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
[[package]]
name = "tokio"
version = "1.44.1"
name = "thiserror"
version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a"
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tokio"
version = "1.44.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48"
dependencies = [
"backtrace",
"bytes",
"libc",
"mio",
"pin-project-lite",
@ -612,6 +711,18 @@ dependencies = [
"syn",
]
[[package]]
name = "tokio-tungstenite"
version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084"
dependencies = [
"futures-util",
"log",
"tokio",
"tungstenite",
]
[[package]]
name = "tower"
version = "0.5.2"
@ -660,6 +771,23 @@ dependencies = [
"once_cell",
]
[[package]]
name = "tungstenite"
version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13"
dependencies = [
"bytes",
"data-encoding",
"http",
"httparse",
"log",
"rand",
"sha1",
"thiserror",
"utf-8",
]
[[package]]
name = "typenum"
version = "1.18.0"
@ -672,6 +800,12 @@ version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "utf-8"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]]
name = "version_check"
version = "0.9.5"

View File

@ -1,18 +1,3 @@
[package]
name = "offline-web"
version = "0.1.0"
edition = "2021"
[lib]
path = "src/lib.rs"
[[bin]]
name = "example"
path = "src/main.rs"
[dependencies]
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"
[workspace]
resolver = "2"
members = ["exp1", "exp2", "offline-web-model"]

16
exp1/Cargo.toml Normal file
View File

@ -0,0 +1,16 @@
[package]
name = "offline-web-http"
version = "0.1.0"
edition = "2021"
[[bin]]
name = "exp1"
path = "src/main.rs"
[dependencies]
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"
offline-web-model = { path = "../offline-web-model" }

3
exp1/README.md Normal file
View File

@ -0,0 +1,3 @@
# Experiment 1 is a traditional http get/post/put restful approach
We use multiple rest requests for bootstrap the application state and capture timings.

View File

@ -1,5 +1,6 @@
mod serve;
#[tokio::main(flavor = "current_thread")]
async fn main() {
offline_web::serve().await;
serve::serve().await;
}

View File

@ -4,8 +4,7 @@ use axum::{extract::Path, http, response::{Html, IntoResponse}, routing::get, Js
use blake2::{Blake2b512, Digest};
use rand::Rng;
use datamodel::Reference;
mod datamodel;
use offline_web_model::Reference;
#[derive(Debug)]
pub struct AddressableObject {
@ -74,6 +73,7 @@ fn random_references_and_objects() -> (Arc<Reference>, Arc<HashMap<String, Arc<R
(rc_root, Arc::new(refs), Arc::new(objects))
}
// TODO(jeremy): Allow this to autoexpand the content_addresses?
async fn all_references(root_ref: Arc<Reference>) -> Json<Arc<Reference>> {
Json(root_ref)
}

16
exp2/Cargo.toml Normal file
View File

@ -0,0 +1,16 @@
[package]
name = "offline-web-ws"
version = "0.1.0"
edition = "2021"
[[bin]]
name = "exp2"
path = "src/main.rs"
[dependencies]
axum = { version = "0.8.3", features = ["macros", "ws"] }
blake2 = "0.10.6"
rand = "0.9.0"
serde = { version = "1.0.219", features = ["derive", "rc"] }
tokio = "1.44.1"
offline-web-model = { path = "../offline-web-model" }

3
exp2/README.md Normal file
View File

@ -0,0 +1,3 @@
# Experiment 2 utilizes websockets
We use websockets to bootstrap the application state and capture timings.

7
exp2/src/main.rs Normal file
View File

@ -0,0 +1,7 @@
mod serve;
#[tokio::main(flavor = "current_thread")]
async fn main() {
serve::serve().await;
}

140
exp2/src/serve.rs Normal file
View File

@ -0,0 +1,140 @@
use std::{collections::HashMap, sync::Arc};
use axum::{extract::Path, http, response::{Html, IntoResponse}, routing::get, Json, Router};
use blake2::{Blake2b512, Digest};
use rand::Rng;
use offline_web_model::Reference;
#[derive(Debug)]
pub struct AddressableObject {
pub address: String,
pub content: String,
}
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();
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<Reference>, Arc<HashMap<String, Arc<Reference>>>, Arc<HashMap<String, AddressableObject>>) {
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))
}
// TODO(jeremy): Allow this to autoexpand the content_addresses?
async fn all_references(root_ref: Arc<Reference>) -> Json<Arc<Reference>> {
Json(root_ref)
}
async fn ref_path(refs: Arc<HashMap<String, Arc<Reference>>>, Path(path): Path<String>) -> Json<Arc<Reference>> {
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<HashMap<String, AddressableObject>>, Path(path): Path<String>) -> String {
dbg!(&path);
match objects.get(&path) {
Some(o) => o.content.clone(),
None => todo!("Return a 404?"),
}
}
async fn get_client_js() -> impl IntoResponse {
(
[(http::header::CONTENT_TYPE, "application/javascript")],
include_str!("../static/client.js"),
)
}
pub fn endpoints(root_ref: Arc<Reference>, refs: Arc<HashMap<String, Arc<Reference>>>, objects: Arc<HashMap<String, AddressableObject>>) -> Router {
// TODO(zaphar): use websockets instead
Router::new().nest(
"/api/v1",
Router::new().nest(
"/ref",
Router::new()
.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({
let objects = objects.clone();
move |path| object_path(objects, path)
}))
),
)
.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
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(root_ref, refs, objects)).await.unwrap();
}

155
exp2/static/client.js Normal file
View File

@ -0,0 +1,155 @@
export { bootstrap };
/**
* @typedef {Object} Reference
* @property {Array<Reference>} 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");
if (!response.ok) {
throw new Error("Network response was not ok: " + response.statusText);
}
return await response.json();
}
/**
* @param {String} dbName
* @param {Array<String>} storeNames
* @returns {Promise<IDBDatabase>}
*/
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);
};
});
}
/**
* Stores a reference object in the IndexedDB.
* @param {IDBObjectStore} store
* @param {Object} reference
* @param {string} root_path
* @returns {Promise<any>}
*/
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);
console.log("Failed to store object", evt);
};
request.onsuccess = (evt) => {
resolve(evt.target.result);
};
});
}
/**
* @param {IDBObjectStore} refStore
* @param {Object} reference
* @returns {Promise<Array<Reference>>} 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<Reference>} references
*/
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);
}
const 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 databaseName = "MerkleStore";
const start = new Date().getTime();
const root = await load_bootstrap();
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);
});
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);
const end = new Date().getTime();
return end - start;
}

26
exp2/static/index.html Normal file
View File

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bootstrap Time Display</title>
</head>
<body>
<h1>Bootstrap Time</h1>
<div id="bootstrap-time">Loading...</div>
<script src="/lib/client.js" type="module"></script>
<script type="module">
import { bootstrap } from '/lib/client.js';
document.addEventListener('DOMContentLoaded', async () => {
try {
const bootstrapTime = await bootstrap(); // This function should be defined in your JS file
document.getElementById('bootstrap-time').textContent = `Bootstrap Time: ${bootstrapTime} ms`;
} catch (error) {
console.error('Error loading bootstrap time:', error);
document.getElementById('bootstrap-time').textContent = 'Error loading bootstrap time';
}
});
</script>
</body>
</html>

View File

@ -0,0 +1,7 @@
[package]
name = "offline-web-model"
version = "0.1.0"
edition = "2024"
[dependencies]
serde = { version = "1.0.219", features = ["derive", "rc"] }