From 350479bb185053c5953fcbe2dbca4c20986856e6 Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Sat, 12 Apr 2025 15:53:40 -0400 Subject: [PATCH] chore: move to a workspace setup --- Cargo.lock | 160 ++++++++++++++++-- Cargo.toml | 21 +-- exp1/Cargo.toml | 16 ++ exp1/README.md | 3 + {src => exp1/src}/main.rs | 3 +- src/lib.rs => exp1/src/serve.rs | 4 +- {static => exp1/static}/client.js | 0 {static => exp1/static}/index.html | 0 exp2/Cargo.toml | 16 ++ exp2/README.md | 3 + exp2/src/main.rs | 7 + exp2/src/serve.rs | 140 +++++++++++++++ exp2/static/client.js | 155 +++++++++++++++++ exp2/static/index.html | 26 +++ offline-web-model/Cargo.toml | 7 + .../mod.rs => offline-web-model/src/lib.rs | 0 16 files changed, 527 insertions(+), 34 deletions(-) create mode 100644 exp1/Cargo.toml create mode 100644 exp1/README.md rename {src => exp1/src}/main.rs (63%) rename src/lib.rs => exp1/src/serve.rs (97%) rename {static => exp1/static}/client.js (100%) rename {static => exp1/static}/index.html (100%) create mode 100644 exp2/Cargo.toml create mode 100644 exp2/README.md create mode 100644 exp2/src/main.rs create mode 100644 exp2/src/serve.rs create mode 100644 exp2/static/client.js create mode 100644 exp2/static/index.html create mode 100644 offline-web-model/Cargo.toml rename src/datamodel/mod.rs => offline-web-model/src/lib.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index 82ff06e..4053663 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index 3dac612..088f439 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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"] diff --git a/exp1/Cargo.toml b/exp1/Cargo.toml new file mode 100644 index 0000000..9429eac --- /dev/null +++ b/exp1/Cargo.toml @@ -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" } diff --git a/exp1/README.md b/exp1/README.md new file mode 100644 index 0000000..73e28bd --- /dev/null +++ b/exp1/README.md @@ -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. diff --git a/src/main.rs b/exp1/src/main.rs similarity index 63% rename from src/main.rs rename to exp1/src/main.rs index 7a64137..9e8821c 100644 --- a/src/main.rs +++ b/exp1/src/main.rs @@ -1,5 +1,6 @@ +mod serve; #[tokio::main(flavor = "current_thread")] async fn main() { - offline_web::serve().await; + serve::serve().await; } diff --git a/src/lib.rs b/exp1/src/serve.rs similarity index 97% rename from src/lib.rs rename to exp1/src/serve.rs index 6bf8767..14c133e 100644 --- a/src/lib.rs +++ b/exp1/src/serve.rs @@ -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, Arc) -> Json> { Json(root_ref) } diff --git a/static/client.js b/exp1/static/client.js similarity index 100% rename from static/client.js rename to exp1/static/client.js diff --git a/static/index.html b/exp1/static/index.html similarity index 100% rename from static/index.html rename to exp1/static/index.html diff --git a/exp2/Cargo.toml b/exp2/Cargo.toml new file mode 100644 index 0000000..4ee43dc --- /dev/null +++ b/exp2/Cargo.toml @@ -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" } diff --git a/exp2/README.md b/exp2/README.md new file mode 100644 index 0000000..f05ef4a --- /dev/null +++ b/exp2/README.md @@ -0,0 +1,3 @@ +# Experiment 2 utilizes websockets + +We use websockets to bootstrap the application state and capture timings. diff --git a/exp2/src/main.rs b/exp2/src/main.rs new file mode 100644 index 0000000..4d9b0e8 --- /dev/null +++ b/exp2/src/main.rs @@ -0,0 +1,7 @@ + +mod serve; + +#[tokio::main(flavor = "current_thread")] +async fn main() { + serve::serve().await; +} diff --git a/exp2/src/serve.rs b/exp2/src/serve.rs new file mode 100644 index 0000000..4380f59 --- /dev/null +++ b/exp2/src/serve.rs @@ -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, 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)) +} + +// TODO(jeremy): Allow this to autoexpand the content_addresses? +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 { + ( + [(http::header::CONTENT_TYPE, "application/javascript")], + include_str!("../static/client.js"), + ) +} + +pub fn endpoints(root_ref: Arc, refs: Arc>>, objects: Arc>) -> 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(); +} diff --git a/exp2/static/client.js b/exp2/static/client.js new file mode 100644 index 0000000..82de876 --- /dev/null +++ b/exp2/static/client.js @@ -0,0 +1,155 @@ +export { bootstrap }; + +/** + * @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"); + 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); + }; + }); +} + +/** + * Stores a reference object in the IndexedDB. + * @param {IDBObjectStore} store + * @param {Object} reference + * @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); + console.log("Failed to store object", evt); + }; + request.onsuccess = (evt) => { + resolve(evt.target.result); + }; + }); +} + +/** + * @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} */ + 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; +} diff --git a/exp2/static/index.html b/exp2/static/index.html new file mode 100644 index 0000000..321d1a4 --- /dev/null +++ b/exp2/static/index.html @@ -0,0 +1,26 @@ + + + + + + Bootstrap Time Display + + +

Bootstrap Time

+
Loading...
+ + + + + diff --git a/offline-web-model/Cargo.toml b/offline-web-model/Cargo.toml new file mode 100644 index 0000000..f0072dc --- /dev/null +++ b/offline-web-model/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "offline-web-model" +version = "0.1.0" +edition = "2024" + +[dependencies] +serde = { version = "1.0.219", features = ["derive", "rc"] } diff --git a/src/datamodel/mod.rs b/offline-web-model/src/lib.rs similarity index 100% rename from src/datamodel/mod.rs rename to offline-web-model/src/lib.rs