diff --git a/.github/workflows/deploy-demo.yml b/.github/workflows/deploy-demo.yml new file mode 100644 index 0000000..63d3ddd --- /dev/null +++ b/.github/workflows/deploy-demo.yml @@ -0,0 +1,70 @@ +name: Deploy Web Demo + +on: + push: + branches: [ main ] + paths: + - 'web-demo/**' + - 'doublets/**' + - '.github/workflows/deploy-demo.yml' + pull_request: + branches: [ main ] + paths: + - 'web-demo/**' + - 'doublets/**' + workflow_dispatch: + +jobs: + deploy: + runs-on: ubuntu-latest + permissions: + contents: read + pages: write + id-token: write + + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: wasm32-unknown-unknown + override: true + + - name: Install wasm-pack + run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh + + - name: Cache cargo dependencies + uses: actions/cache@v3 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + + - name: Build web demo + run: | + cd web-demo + wasm-pack build --target web --out-dir pkg + + - name: Setup Pages + if: github.ref == 'refs/heads/main' + uses: actions/configure-pages@v3 + + - name: Upload artifact + if: github.ref == 'refs/heads/main' + uses: actions/upload-pages-artifact@v2 + with: + path: ./web-demo + + - name: Deploy to GitHub Pages + if: github.ref == 'refs/heads/main' + id: deployment + uses: actions/deploy-pages@v2 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2f7896d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/Cargo.lock b/Cargo.lock index fbf94c6..37dfa3f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "aho-corasick" @@ -96,6 +96,16 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + [[package]] name = "criterion" version = "0.3.6" @@ -220,7 +230,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn", + "syn 1.0.98", ] [[package]] @@ -231,7 +241,7 @@ checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" dependencies = [ "darling_core", "quote", - "syn", + "syn 1.0.98", ] [[package]] @@ -242,7 +252,7 @@ checksum = "d70a2d4995466955a415223acf3c9c934b9ff2339631cdf4ffc893da4bacd717" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.98", ] [[package]] @@ -281,6 +291,19 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "doublets-web-demo" +version = "0.1.0" +dependencies = [ + "console_error_panic_hook", + "js-sys", + "serde", + "serde-wasm-bindgen", + "serde_json", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "either" version = "1.7.0" @@ -293,7 +316,7 @@ version = "0.1.0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.98", ] [[package]] @@ -323,7 +346,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn", + "syn 1.0.98", ] [[package]] @@ -624,9 +647,9 @@ checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" [[package]] name = "proc-macro2" -version = "1.0.42" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c278e965f1d8cf32d6e0e96de3d3e79712178ae67986d9cf9151f51e95aac89b" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ "unicode-ident", ] @@ -650,14 +673,14 @@ checksum = "b22a693222d716a9587786f37ac3f6b4faedb5b80c23914e7303ff5a1d8016e9" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.98", ] [[package]] name = "quote" -version = "1.0.20" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] @@ -777,6 +800,12 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + [[package]] name = "ryu" version = "1.0.10" @@ -807,6 +836,17 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-wasm-bindgen" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3b4c031cd0d9014307d82b8abf653c0290fbdaeb4c02d00c63cf52f728628bf" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + [[package]] name = "serde_cbor" version = "0.11.2" @@ -825,7 +865,7 @@ checksum = "6f2122636b9fe3b81f1cb25099fcf2d3f542cdb1d45940d56c713158884a05da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.98", ] [[package]] @@ -877,6 +917,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "2.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "tap" version = "1.0.1" @@ -923,7 +974,7 @@ checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.98", ] [[package]] @@ -980,7 +1031,7 @@ checksum = "11c75893af559bc8e10716548bdef5cb2b983f8e637db9d0e15126b61b484ee2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.98", ] [[package]] @@ -1020,9 +1071,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.2" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15c61ba63f9235225a22310255a29b806b907c9b8c964bcbd0a2c70f3f2deea7" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" [[package]] name = "unicode-width" @@ -1055,34 +1106,36 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.82" +version = "0.2.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7652e3f6c4706c8d9cd54832c4a4ccb9b5336e2c3bd154d5cccfbf1c1f5f7d" +checksum = "7e14915cadd45b529bb8d1f343c4ed0ac1de926144b746e2710f9cd05df6603b" dependencies = [ "cfg-if", + "once_cell", + "rustversion", "wasm-bindgen-macro", + "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.82" +version = "0.2.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "662cd44805586bd52971b9586b1df85cdbbd9112e4ef4d8f41559c334dc6ac3f" +checksum = "e28d1ba982ca7923fd01448d5c30c6864d0a14109560296a162f80f305fb93bb" dependencies = [ "bumpalo", "log", - "once_cell", "proc-macro2", "quote", - "syn", + "syn 2.0.106", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.82" +version = "0.2.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b260f13d3012071dfb1512849c033b1925038373aea48ced3012c09df952c602" +checksum = "7c3d463ae3eff775b0c45df9da45d68837702ac35af998361e2c84e7c5ec1b0d" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1090,22 +1143,25 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.82" +version = "0.2.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be8e654bdd9b79216c2929ab90721aa82faf65c48cdf08bdc4e7f51357b80da" +checksum = "7bb4ce89b08211f923caf51d527662b75bdc9c9c7aab40f86dcb9fb85ac552aa" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.106", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.82" +version = "0.2.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6598dd0bd3c7d51095ff6531a5b23e02acdc81804e30d8f07afb77b7215a140a" +checksum = "f143854a3b13752c6950862c906306adb27c7e839f7414cec8fea35beab624c1" +dependencies = [ + "unicode-ident", +] [[package]] name = "web-sys" diff --git a/Cargo.toml b/Cargo.toml index 8176e2d..baff8b5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,9 @@ members = [ "doublets-ffi", "doublets", + # web demo + "web-demo", + # dev "dev-deps/mem-rs", "dev-deps/data-rs", diff --git a/README.md b/README.md index 96d8b46..66d7345 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,16 @@ later later +## Web Demo + +Try the interactive WebAssembly demo: [**🔗 Doublets Playground**](https://linksplatform.github.io/doublets-rs/) + +The web demo allows you to: +- Create and manage doublets interactively +- Explore link relationships visually +- Learn doublets concepts in your browser +- See real-time operations and results + ## Example A basic operations in doublets: diff --git a/web-demo/Cargo.toml b/web-demo/Cargo.toml new file mode 100644 index 0000000..152c4fa --- /dev/null +++ b/web-demo/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "doublets-web-demo" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wasm-bindgen = "0.2" +js-sys = "0.3" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +serde-wasm-bindgen = "0.4" +console_error_panic_hook = "0.1" + +[dependencies.web-sys] +version = "0.3" +features = [ + "console", + "Document", + "Element", + "HtmlElement", + "Window", +] \ No newline at end of file diff --git a/web-demo/README.md b/web-demo/README.md new file mode 100644 index 0000000..f13cc16 --- /dev/null +++ b/web-demo/README.md @@ -0,0 +1,102 @@ +# Doublets Web Demo + +An interactive WebAssembly playground for exploring doublets operations in your browser. + +## Features + +- **Create Links**: Build custom doublets with specified source and target +- **Create Points**: Generate self-referencing links (points) +- **Delete Links**: Remove links by ID +- **Search & Filter**: Find links by source and/or target +- **Real-time Display**: Live view of all links in the store +- **Operations Log**: Track all operations performed + +## Quick Start + +### Prerequisites + +- Rust toolchain with WebAssembly target +- `wasm-pack` tool for building WebAssembly modules + +### Building + +1. Install the WebAssembly target: + ```bash + rustup target add wasm32-unknown-unknown + ``` + +2. Install wasm-pack (if not already installed): + ```bash + curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh + ``` + +3. Build the demo: + ```bash + cd web-demo + ./build.sh + ``` + +### Running + +Start a local web server: +```bash +python3 -m http.server 8000 +``` + +Then open [http://localhost:8000](http://localhost:8000) in your browser. + +## Usage + +### Creating Links + +1. **Points**: Click "Create Point" to create a self-referencing link (1 → 1) +2. **Custom Links**: Enter source and target values, then click "Create Link" + +### Managing Links + +- View all links in the table on the right +- Delete links by entering their ID and clicking "Delete Link" +- Search for specific links using source/target filters + +### Understanding Doublets + +Doublets are a fundamental data structure where: +- Each **link** connects a **source** to a **target** +- Links themselves have unique **IDs** +- **Points** are special links where source equals target +- The system maintains referential integrity + +## Architecture + +The demo consists of: + +- **Rust/WebAssembly Core**: Uses the `doublets` crate for all operations +- **JavaScript Interface**: Provides UI interactions and state management +- **HTML/CSS Frontend**: Responsive web interface + +## Example Operations + +```javascript +// Create a new doublets store +const demo = new DoubletsDemo(); + +// Create a point (self-link) +const point = demo.create_point(); // Returns: 1 + +// Create a custom link +const link = demo.create_link(2, 3); // Returns: 2 + +// Get all links +const allLinks = demo.get_all_links(); +// Returns: [ +// { id: 1, source: 1, target: 1 }, +// { id: 2, source: 2, target: 3 } +// ] + +// Search for links +const results = demo.search_links(2, null); // Find links with source=2 +``` + +## Contributing + +This demo is part of the [doublets-rs](https://github.com/linksplatform/doublets-rs) project. Contributions are welcome! \ No newline at end of file diff --git a/web-demo/build.sh b/web-demo/build.sh new file mode 100755 index 0000000..dcdff5c --- /dev/null +++ b/web-demo/build.sh @@ -0,0 +1,20 @@ +#!/bin/bash +set -e + +echo "Building Doublets WebAssembly Demo..." + +# Check if wasm-pack is installed +if ! command -v wasm-pack &> /dev/null; then + echo "Installing wasm-pack..." + curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh +fi + +# Build the WebAssembly module +echo "Building WebAssembly module..." +wasm-pack build --target web --out-dir pkg + +echo "Build complete! The demo is ready to serve." +echo "To run the demo locally:" +echo " cd web-demo" +echo " python3 -m http.server 8000" +echo " # Then open http://localhost:8000 in your browser" \ No newline at end of file diff --git a/web-demo/index.html b/web-demo/index.html new file mode 100644 index 0000000..ed09010 --- /dev/null +++ b/web-demo/index.html @@ -0,0 +1,465 @@ + + + + + Doublets Playground + + + +
+

🔗 Doublets Playground

+

Interactive WebAssembly demo for exploring doublets operations

+
+ +
+
+ Total Links: 0 +
+
+ Status: Ready +
+ +
+ +
+
+

🎛️ Operations

+ +
+ + +
+ +
+ + + + + +
+ +
+ + + + +
+ +
+ + + + + +
+ +
+ +
+
+ +
+

📊 Current Links

+
+ + + + + + + + + + + +
+
+
+ +
+

📝 Operations Log

+
+ Doublets playground initialized. Ready for operations! +
+
+ + + + \ No newline at end of file diff --git a/web-demo/package.json b/web-demo/package.json new file mode 100644 index 0000000..2e64b3b --- /dev/null +++ b/web-demo/package.json @@ -0,0 +1,16 @@ +{ + "name": "doublets-web-demo", + "version": "0.1.0", + "description": "WebAssembly demo playground for doublets-rs", + "scripts": { + "build": "wasm-pack build --target web --out-dir pkg", + "serve": "python3 -m http.server 8000", + "dev": "npm run build && npm run serve" + }, + "devDependencies": { + "wasm-pack": "^0.12.1" + }, + "keywords": ["doublets", "webassembly", "rust", "wasm", "demo"], + "author": "Doublets Team", + "license": "Unlicense" +} \ No newline at end of file diff --git a/web-demo/src/lib.rs b/web-demo/src/lib.rs new file mode 100644 index 0000000..a5fbefc --- /dev/null +++ b/web-demo/src/lib.rs @@ -0,0 +1,230 @@ +use std::collections::HashMap; +use wasm_bindgen::prelude::*; +use serde::{Deserialize, Serialize}; + +// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global +// allocator. +#[cfg(feature = "wee_alloc")] +#[global_allocator] +static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; + +// This is like the `extern` block in C. +#[wasm_bindgen] +extern "C" { + // Bind the `console.log` function from the browser. + #[wasm_bindgen(js_namespace = console)] + fn log(s: &str); +} + +// Define a macro to make it easier to call `console.log`. +macro_rules! console_log { + ($($t:tt)*) => (log(&format_args!($($t)*).to_string())) +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct LinkData { + pub id: usize, + pub source: usize, + pub target: usize, +} + +#[derive(Serialize, Deserialize)] +pub struct DoubletsState { + pub links: Vec, + pub count: usize, + pub operations: Vec, +} + +/// A simplified doublets store for demonstration purposes +/// This implements the core concept of doublets without requiring +/// the full complexity of the main doublets library +#[wasm_bindgen] +pub struct DoubletsDemo { + links: HashMap, + next_id: usize, + operations_log: Vec, +} + +#[wasm_bindgen] +impl DoubletsDemo { + #[wasm_bindgen(constructor)] + pub fn new() -> Result { + console_error_panic_hook::set_once(); + + let mut demo = DoubletsDemo { + links: HashMap::new(), + next_id: 1, + operations_log: Vec::new(), + }; + + demo.log_operation("Initialized doublets store (simplified demo version)".to_string()); + console_log!("Doublets demo initialized with simplified store"); + Ok(demo) + } + + #[wasm_bindgen] + pub fn create_link(&mut self, source: usize, target: usize) -> Result { + if source == 0 || target == 0 { + return Err(JsValue::from_str("Source and target must be > 0")); + } + + let link_id = self.next_id; + let link = LinkData { + id: link_id, + source, + target, + }; + + self.links.insert(link_id, link); + self.next_id += 1; + + self.log_operation(format!("Created link {}: {} -> {}", link_id, source, target)); + console_log!("Created link {}: {} -> {}", link_id, source, target); + Ok(link_id) + } + + #[wasm_bindgen] + pub fn create_point(&mut self) -> Result { + // Create a point: a link that points to itself + // We'll use the next available ID as both the link ID and the point value + let point_value = self.next_id; + let link_id = self.create_link(point_value, point_value)?; + + self.log_operation(format!("Created point: {} (self-referencing link)", point_value)); + console_log!("Created point: {}", point_value); + Ok(link_id) + } + + #[wasm_bindgen] + pub fn delete_link(&mut self, link_id: usize) -> Result { + if let Some(link) = self.links.remove(&link_id) { + self.log_operation(format!("Deleted link {}: {} -> {}", link_id, link.source, link.target)); + console_log!("Deleted link {}: {} -> {}", link_id, link.source, link.target); + Ok(true) + } else { + self.log_operation(format!("Link {} not found for deletion", link_id)); + console_log!("Link {} not found", link_id); + Ok(false) + } + } + + #[wasm_bindgen] + pub fn get_link_count(&self) -> usize { + self.links.len() + } + + #[wasm_bindgen] + pub fn get_all_links(&self) -> JsValue { + let mut links: Vec<&LinkData> = self.links.values().collect(); + links.sort_by_key(|link| link.id); + + serde_wasm_bindgen::to_value(&links).unwrap_or(JsValue::NULL) + } + + #[wasm_bindgen] + pub fn get_state(&self) -> JsValue { + let mut links: Vec<&LinkData> = self.links.values().collect(); + links.sort_by_key(|link| link.id); + + let state = DoubletsState { + links: links.into_iter().cloned().collect(), + count: self.links.len(), + operations: self.operations_log.clone(), + }; + + serde_wasm_bindgen::to_value(&state).unwrap_or(JsValue::NULL) + } + + #[wasm_bindgen] + pub fn clear_operations_log(&mut self) { + self.operations_log.clear(); + console_log!("Operations log cleared"); + } + + #[wasm_bindgen] + pub fn search_links(&self, source: Option, target: Option) -> JsValue { + let filtered_links: Vec<&LinkData> = self.links + .values() + .filter(|link| { + let source_match = source.map_or(true, |s| link.source == s); + let target_match = target.map_or(true, |t| link.target == t); + source_match && target_match + }) + .collect(); + + console_log!("Search found {} links", filtered_links.len()); + serde_wasm_bindgen::to_value(&filtered_links).unwrap_or(JsValue::NULL) + } + + #[wasm_bindgen] + pub fn get_link(&self, link_id: usize) -> JsValue { + if let Some(link) = self.links.get(&link_id) { + serde_wasm_bindgen::to_value(link).unwrap_or(JsValue::NULL) + } else { + JsValue::NULL + } + } + + #[wasm_bindgen] + pub fn update_link(&mut self, link_id: usize, new_source: usize, new_target: usize) -> Result { + if new_source == 0 || new_target == 0 { + return Err(JsValue::from_str("Source and target must be > 0")); + } + + if let Some(link) = self.links.get_mut(&link_id) { + let old_source = link.source; + let old_target = link.target; + link.source = new_source; + link.target = new_target; + + self.log_operation(format!( + "Updated link {}: {} -> {} (was {} -> {})", + link_id, new_source, new_target, old_source, old_target + )); + console_log!("Updated link {}: {} -> {}", link_id, new_source, new_target); + Ok(true) + } else { + self.log_operation(format!("Link {} not found for update", link_id)); + console_log!("Link {} not found", link_id); + Ok(false) + } + } + + #[wasm_bindgen] + pub fn clear_all_links(&mut self) { + let count = self.links.len(); + self.links.clear(); + self.next_id = 1; + + self.log_operation(format!("Cleared all {} links from store", count)); + console_log!("Cleared all {} links", count); + } + + fn log_operation(&mut self, operation: String) { + self.operations_log.push(operation); + } +} + +// Export a `greet` function from Rust to JavaScript, that alerts a greeting. +#[wasm_bindgen] +pub fn greet(name: &str) { + console_log!("Hello, {}! This is the Doublets WebAssembly demo.", name); +} + +// Called when the wasm module is instantiated +#[wasm_bindgen(start)] +pub fn main() { + console_log!("Doublets WebAssembly demo loaded!"); +} + +#[wasm_bindgen] +pub fn get_demo_info() -> JsValue { + let info = serde_json::json!({ + "name": "Doublets WebAssembly Demo", + "version": "0.1.0", + "description": "A simplified demonstration of doublets operations using WebAssembly", + "note": "This is a simplified version for demonstration purposes. The full doublets-rs library provides more advanced features." + }); + + serde_wasm_bindgen::to_value(&info).unwrap_or(JsValue::NULL) +} \ No newline at end of file