├── .firebaserc ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── propose_design.md ├── dependabot.yml └── workflows │ ├── build-website.yml │ ├── lint-fmt.yml │ ├── publish-website.yml │ └── tests.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── crates ├── console │ ├── Cargo.toml │ ├── README.md │ ├── src │ │ ├── console_dbg.rs │ │ ├── counter.rs │ │ ├── externs.rs │ │ ├── lib.rs │ │ ├── macros.rs │ │ └── timer.rs │ └── tests │ │ └── web.rs ├── dialogs │ ├── Cargo.toml │ ├── README.md │ └── src │ │ └── lib.rs ├── events │ ├── Cargo.toml │ ├── README.md │ ├── src │ │ └── lib.rs │ └── tests │ │ └── web.rs ├── file │ ├── Cargo.toml │ ├── README.md │ ├── src │ │ ├── blob.rs │ │ ├── file_list.rs │ │ ├── file_reader.rs │ │ ├── lib.rs │ │ └── object_url.rs │ └── tests │ │ └── web.rs ├── history │ ├── Cargo.toml │ ├── README.md │ ├── src │ │ ├── any.rs │ │ ├── browser.rs │ │ ├── error.rs │ │ ├── hash.rs │ │ ├── history.rs │ │ ├── lib.rs │ │ ├── listener.rs │ │ ├── location.rs │ │ ├── memory.rs │ │ ├── query.rs │ │ ├── state.rs │ │ └── utils.rs │ └── tests │ │ ├── browser_history.rs │ │ ├── browser_history_feat_serialize.rs │ │ ├── hash_history.rs │ │ ├── hash_history_feat_serialize.rs │ │ ├── memory_history.rs │ │ ├── memory_history_feat_serialize.rs │ │ ├── query.rs │ │ └── utils.rs ├── net │ ├── Cargo.toml │ ├── README.md │ ├── src │ │ ├── error.rs │ │ ├── eventsource │ │ │ ├── futures.rs │ │ │ └── mod.rs │ │ ├── http │ │ │ ├── headers.rs │ │ │ ├── mod.rs │ │ │ ├── query.rs │ │ │ ├── request.rs │ │ │ └── response.rs │ │ ├── lib.rs │ │ └── websocket │ │ │ ├── events.rs │ │ │ ├── futures.rs │ │ │ ├── io_util.rs │ │ │ └── mod.rs │ └── tests │ │ ├── http.rs │ │ └── query.rs ├── render │ ├── Cargo.toml │ ├── README.md │ └── src │ │ └── lib.rs ├── storage │ ├── Cargo.toml │ ├── README.md │ ├── src │ │ ├── errors.rs │ │ ├── lib.rs │ │ ├── local_storage.rs │ │ └── session_storage.rs │ └── tests │ │ ├── local_storage.rs │ │ └── session_storage.rs ├── timers │ ├── Cargo.toml │ ├── README.md │ ├── src │ │ ├── callback.rs │ │ ├── future.rs │ │ └── lib.rs │ └── tests │ │ ├── node.rs │ │ └── web.rs ├── utils │ ├── Cargo.toml │ ├── README.md │ ├── src │ │ ├── errors.rs │ │ ├── format │ │ │ └── json.rs │ │ ├── iter.rs │ │ └── lib.rs │ └── tests │ │ ├── serde.js │ │ └── serde.rs ├── worker-macros │ ├── Cargo.toml │ ├── README.md │ ├── src │ │ ├── lib.rs │ │ ├── oneshot.rs │ │ ├── reactor.rs │ │ └── worker_fn.rs │ └── tests │ │ ├── oneshot.rs │ │ ├── oneshot │ │ ├── basic-pass.rs │ │ ├── many_input-fail.rs │ │ ├── many_input-fail.stderr │ │ ├── no_input-fail.rs │ │ ├── no_input-fail.stderr │ │ └── sync-pass.rs │ │ ├── reactor.rs │ │ └── reactor │ │ ├── basic-pass.rs │ │ ├── many_input-fail.rs │ │ ├── many_input-fail.stderr │ │ ├── return_type-fail.rs │ │ ├── return_type-fail.stderr │ │ ├── sync-fail.rs │ │ └── sync-fail.stderr └── worker │ ├── Cargo.toml │ ├── README.md │ └── src │ ├── actor │ ├── bridge.rs │ ├── handler_id.rs │ ├── lifecycle.rs │ ├── messages.rs │ ├── mod.rs │ ├── native_worker.rs │ ├── registrar.rs │ ├── scope.rs │ ├── spawner.rs │ └── traits.rs │ ├── codec.rs │ ├── lib.rs │ ├── oneshot │ ├── bridge.rs │ ├── mod.rs │ ├── registrar.rs │ ├── spawner.rs │ ├── traits.rs │ └── worker.rs │ ├── reactor │ ├── bridge.rs │ ├── messages.rs │ ├── mod.rs │ ├── registrar.rs │ ├── scope.rs │ ├── spawner.rs │ ├── traits.rs │ └── worker.rs │ └── traits.rs ├── examples ├── clock │ ├── .gitignore │ ├── Cargo.toml │ ├── README.md │ ├── index.html │ └── src │ │ └── lib.rs ├── file-hash │ ├── Cargo.toml │ ├── README.md │ ├── index.html │ └── src │ │ ├── bin │ │ ├── example_file_hash_app.rs │ │ └── example_file_hash_worker.rs │ │ ├── codec.rs │ │ └── lib.rs ├── history-wasi │ ├── Cargo.toml │ ├── README.md │ └── src │ │ └── main.rs ├── markdown │ ├── Cargo.toml │ ├── README.md │ ├── index.html │ └── src │ │ ├── bin │ │ ├── example_markdown_app.rs │ │ ├── example_markdown_test_server.rs │ │ └── example_markdown_worker.rs │ │ └── lib.rs └── prime │ ├── Cargo.toml │ ├── index.html │ └── src │ ├── bin │ ├── example_prime_app.rs │ ├── example_prime_test_server.rs │ └── example_prime_worker.rs │ └── lib.rs ├── firebase.json ├── new-design-workflow.dot ├── new-design-workflow.png ├── release.toml ├── src └── lib.rs ├── update-readmes.sh └── website ├── .gitignore ├── README.md ├── babel.config.js ├── blog ├── 2019-05-29-hello-world.md ├── 2021-07-27-new-release.md ├── 2021-10-10-new-release.md ├── 2022-1-22-new-release.md └── 2022-3-11-new-net-crate.md ├── docs └── getting-started.mdx ├── docusaurus.config.js ├── firebase-debug.log ├── package-lock.json ├── package.json ├── sidebars.js ├── src ├── css │ └── custom.css └── pages │ ├── __index.md │ ├── index.module.css │ └── index.tsx ├── static ├── .nojekyll └── img │ ├── Gloo-Logo.ico │ ├── Gloo-Logo.svg │ ├── docusaurus.png │ ├── favicon.ico │ ├── logo.svg │ ├── tutorial │ ├── docsVersionDropdown.png │ └── localeDropdown.png │ ├── undraw_docusaurus_mountain.svg │ ├── undraw_docusaurus_react.svg │ └── undraw_docusaurus_tree.svg └── tsconfig.json /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "gloo-rs" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # These are programmatically generated with `cargo readme` so don't clutter 2 | # diffs with them and don't try and merge them. 3 | crates/*/README.md -diff -merge 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "Bug report \U0001F41B" 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | --- 8 | 9 | ### Describe the Bug 10 | 11 | A clear and concise description of what the bug is. 12 | 13 | ### Steps to Reproduce 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | If applicable, add a link to a test case (as a zip file or link to a repository 21 | we can clone). 22 | 23 | ### Expected Behavior 24 | 25 | A clear and concise description of what you expected to happen. 26 | 27 | ### Actual Behavior 28 | 29 | A clear and concise description of what actually happened. 30 | 31 | If applicable, add screenshots to help explain your problem. 32 | 33 | ### Additional Context 34 | 35 | Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/propose_design.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Propose Design 3 | about: Propose an API or crate design for Gloo 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | ## Summary 10 | 11 | Short overview of the proposal. 12 | 13 | ## Motivation 14 | 15 | Why are we doing this? What problems does it solve? 16 | 17 | ## Detailed Explanation 18 | 19 | Introduce and explain the new APIs and concepts. How will this proposal be 20 | implemented? Provide representative and edge-case examples. 21 | 22 | Provide a skeleton of the proposed API by writing out types (don't need their 23 | members or full implementation) as well as function and method signatures 24 | (again, just the signature, don't need the function body): 25 | 26 | ```rust 27 | pub struct Whatever { ... } 28 | 29 | impl Whatever { 30 | pub fn new(raw: &web_sys::RawWhatever) -> Self { ... } 31 | pub fn another(&self) -> Another { ... } 32 | } 33 | 34 | pub struct Another { ... } 35 | 36 | // Does X, Y, and Z when dropped. 37 | impl Drop for Another {} 38 | ``` 39 | 40 | ## Drawbacks, Rationale, and Alternatives 41 | 42 | Does this design have drawbacks? Are there alternative approaches? Why is this 43 | design the best of all designs available? 44 | 45 | What prior art exists? There are many good sources of inspiration: Ember, React, 46 | Angular, Vue, Knockout, jQuery, Closure, Elm, Emscripten, ClojureScript, 47 | Polymer, etc.. 48 | 49 | ## Unresolved Questions 50 | 51 | What is not clear yet? What do we expect to clarify through implementation 52 | and/or usage experience? 53 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "cargo" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | day: "friday" 8 | open-pull-requests-limit: 2 9 | groups: 10 | cargo-deps: 11 | patterns: 12 | - "*" 13 | 14 | - package-ecosystem: "npm" 15 | directory: "/website" 16 | schedule: 17 | interval: "monthly" 18 | target-branch: "master" 19 | groups: 20 | website-deps: 21 | patterns: 22 | - "*" 23 | 24 | - package-ecosystem: "github-actions" 25 | directory: "/" 26 | schedule: 27 | interval: "monthly" 28 | target-branch: "master" 29 | -------------------------------------------------------------------------------- /.github/workflows/build-website.yml: -------------------------------------------------------------------------------- 1 | name: Build website 2 | on: 3 | pull_request: 4 | branches: [master] 5 | paths: 6 | - "website/**" 7 | - "firebase.json" 8 | - ".github/workflows/*-website.yml" 9 | push: 10 | branches: [master] 11 | paths: 12 | - "website/**" 13 | - "firebase.json" 14 | - ".github/workflows/*-website.yml" 15 | 16 | jobs: 17 | build: 18 | runs-on: ubuntu-latest 19 | env: 20 | PR_INFO_FILE: ".PR_INFO" 21 | steps: 22 | - uses: actions/checkout@v4 23 | - name: Setup node 24 | uses: actions/setup-node@v3 25 | with: 26 | node-version: "18" 27 | 28 | - name: Build 29 | run: | 30 | cd website 31 | npm install 32 | npm run build 33 | 34 | - name: Upload build artifact 35 | uses: actions/upload-artifact@v3 36 | with: 37 | name: website 38 | path: website/build/ 39 | retention-days: 1 40 | 41 | - if: github.event_name == 'pull_request' 42 | name: Build pr info 43 | run: | 44 | echo "${{ github.event.number }}" > $PR_INFO_FILE 45 | 46 | - if: github.event_name == 'pull_request' 47 | name: Upload pr info 48 | uses: actions/upload-artifact@v3 49 | with: 50 | name: pr-info 51 | path: "${{ env.PR_INFO_FILE }}" 52 | retention-days: 1 53 | -------------------------------------------------------------------------------- /.github/workflows/lint-fmt.yml: -------------------------------------------------------------------------------- 1 | name: Lint & Format 2 | on: 3 | push: 4 | branches: [ master ] 5 | pull_request: 6 | branches: [ master ] 7 | 8 | jobs: 9 | fmt: 10 | name: Rustfmt 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: dtolnay/rust-toolchain@master 15 | with: 16 | toolchain: stable 17 | components: rustfmt 18 | 19 | - uses: actions/cache@v4 20 | with: 21 | path: | 22 | ~/.cargo/registry 23 | ~/.cargo/git 24 | target 25 | key: cargo-${{ runner.os }}-fmt-${{ hashFiles('**/Cargo.toml') }} 26 | restore-keys: | 27 | cargo-${{ runner.os }}-fmt- 28 | cargo-${{ runner.os }}- 29 | 30 | - name: Run cargo fmt 31 | run: cargo fmt --all -- --check 32 | 33 | clippy: 34 | name: Clippy 35 | runs-on: ubuntu-latest 36 | steps: 37 | - uses: actions/checkout@v4 38 | - uses: dtolnay/rust-toolchain@master 39 | with: 40 | toolchain: stable 41 | components: clippy 42 | target: wasm32-unknown-unknown 43 | 44 | - uses: actions/cache@v4 45 | with: 46 | path: | 47 | ~/.cargo/registry 48 | ~/.cargo/git 49 | target 50 | key: cargo-${{ runner.os }}-clippy-${{ hashFiles('**/Cargo.toml') }} 51 | restore-keys: | 52 | cargo-${{ runner.os }}-clippy- 53 | cargo-${{ runner.os }}- 54 | 55 | - name: Run clippy 56 | run: cargo clippy --all-targets --all-features 57 | 58 | - name: Run clippy for gloo-net 59 | working-directory: crates/net 60 | run: | 61 | cargo clippy --features "http" --no-default-features 62 | cargo clippy --features "http,json" --no-default-features 63 | cargo clippy --features "websocket" --no-default-features 64 | cargo clippy --features "http" 65 | cargo clippy --features "http,json" 66 | cargo clippy --features "websocket" 67 | -------------------------------------------------------------------------------- /.github/workflows/publish-website.yml: -------------------------------------------------------------------------------- 1 | name: Publish website 2 | on: 3 | workflow_run: 4 | workflows: ["Build website"] 5 | types: 6 | - completed 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | env: 12 | PR_INFO_FILE: ".PR_INFO" 13 | steps: 14 | - if: github.event.workflow_run.conclusion != 'success' 15 | name: Abort if build failed 16 | run: | 17 | echo "build failed" 18 | exit 1 19 | 20 | # need to checkout to get "firebase.json", ".firebaserc" 21 | - uses: actions/checkout@v4 22 | 23 | - name: Download build artifact 24 | uses: dawidd6/action-download-artifact@v2 25 | with: 26 | github_token: "${{ secrets.GITHUB_TOKEN }}" 27 | workflow: build-website.yml 28 | run_id: ${{ github.event.workflow_run.id }} 29 | name: website 30 | path: website/build 31 | 32 | - if: github.event.workflow_run.event == 'pull_request' 33 | name: Download pr info 34 | uses: dawidd6/action-download-artifact@v2 35 | with: 36 | github_token: "${{ secrets.GITHUB_TOKEN }}" 37 | workflow: build-website.yml 38 | run_id: ${{ github.event.workflow_run.id }} 39 | name: pr-info 40 | 41 | - if: github.event.workflow_run.event == 'pull_request' 42 | name: Apply pull request environment 43 | run: | 44 | pr_number=$(cat "$PR_INFO_FILE") 45 | if ! [[ "$pr_number" =~ ^[0-9]+$ ]]; then 46 | echo "pr number invalid" 47 | exit 1 48 | fi 49 | echo "PR_NUMBER=$pr_number" >> $GITHUB_ENV 50 | echo "PR_BRANCH=${{ github.event.workflow_run.head_branch }}" >> $GITHUB_ENV 51 | echo "COMMIT_SHA=${{ github.event.workflow_run.head_sha }}" >> $GITHUB_ENV 52 | echo "CHANNEL_ID='$PR_NUMBER-$COMMIT_SHA'" >> $GITHUB_ENV 53 | 54 | - if: github.event.workflow_run.event == 'push' 55 | name: Apply push environment 56 | run: | 57 | echo "CHANNEL_ID=live" >> $GITHUB_ENV 58 | 59 | - name: Deploy to Firebase 60 | uses: FirebaseExtended/action-hosting-deploy@v0 61 | with: 62 | repoToken: "${{ secrets.GITHUB_TOKEN }}" 63 | firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_GLOO_RS }}' 64 | channelId: "${{ env.CHANNEL_ID }}" 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | 4 | examples/*/dist 5 | 6 | # editor configs 7 | .vscode 8 | .idea 9 | 10 | # hosting cache 11 | .firebase/hosting.*.cache 12 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Rust and WebAssembly Working Group"] 3 | description = "A modular toolkit for Rust and WebAssembly" 4 | edition = "2021" 5 | license = "MIT OR Apache-2.0" 6 | name = "gloo" 7 | readme = "README.md" 8 | version = "0.11.0" 9 | repository = "https://github.com/rustwasm/gloo" 10 | homepage = "https://gloo-rs.web.app/" 11 | documentation = "https://docs.rs/gloo/" 12 | categories = ["api-bindings", "wasm"] 13 | rust-version = "1.64" 14 | 15 | [dependencies] 16 | gloo-timers = { version = "0.3", path = "crates/timers", optional = true } 17 | gloo-events = { version = "0.2", path = "crates/events", optional = true } 18 | gloo-file = { version = "0.3", path = "crates/file", optional = true } 19 | gloo-dialogs = { version = "0.2", path = "crates/dialogs", optional = true } 20 | gloo-storage = { version = "0.3", path = "crates/storage", optional = true } 21 | gloo-render = { version = "0.2", path = "crates/render", optional = true } 22 | gloo-console = { version = "0.3", path = "crates/console", optional = true } 23 | gloo-utils = { version = "0.2", path = "crates/utils", optional = true } 24 | gloo-history = { version = "0.2", path = "crates/history", optional = true } 25 | gloo-worker = { version = "0.5", path = "crates/worker", optional = true } 26 | gloo-net = { version = "0.6", path = "crates/net", optional = true } 27 | 28 | [features] 29 | default = [ 30 | "timers", 31 | "events", 32 | "file", 33 | "dialogs", 34 | "storage", 35 | "render", 36 | "console", 37 | "utils", 38 | "history", 39 | "worker", 40 | "net", 41 | ] 42 | futures = [ 43 | "timers", 44 | "file", 45 | "worker", 46 | "gloo-timers/futures", 47 | "gloo-file/futures", 48 | "gloo-worker/futures", 49 | ] 50 | timers = ["gloo-timers"] 51 | events = ["gloo-events"] 52 | file = ["gloo-file"] 53 | dialogs = ["gloo-dialogs"] 54 | storage = ["gloo-storage"] 55 | render = ["gloo-render"] 56 | console = ["gloo-console"] 57 | utils = ["gloo-utils"] 58 | history = ["gloo-history"] 59 | worker = ["gloo-worker"] 60 | net = ["gloo-net"] 61 | 62 | [workspace] 63 | members = [ 64 | "crates/timers", 65 | "crates/events", 66 | "crates/net", 67 | "crates/file", 68 | "crates/dialogs", 69 | "crates/storage", 70 | "crates/console", 71 | "crates/utils", 72 | "crates/history", 73 | "crates/worker", 74 | "crates/worker-macros", 75 | "crates/net", 76 | 77 | "examples/markdown", 78 | "examples/clock", 79 | "examples/file-hash", 80 | "examples/history-wasi", 81 | "examples/prime", 82 | ] 83 | 84 | # Passing arguments to the docsrs builder in order to properly document cfg's. 85 | # More information: https://docs.rs/about/builds#cross-compiling 86 | [package.metadata.docs.rs] 87 | all-features = true 88 | rustdoc-args = ["--cfg", "docsrs"] 89 | rustc-args = ["--cfg", "docsrs"] 90 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Rust and WebAssembly Working Group 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /crates/console/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gloo-console" 3 | description = "Convenience crate for working with browser's console" 4 | version = "0.3.0" 5 | authors = ["Rust and WebAssembly Working Group"] 6 | edition = "2021" 7 | license = "MIT OR Apache-2.0" 8 | readme = "README.md" 9 | repository = "https://github.com/rustwasm/gloo/tree/master/crates/console" 10 | homepage = "https://github.com/rustwasm/gloo" 11 | documentation = "https://docs.rs/gloo-console/" 12 | categories = ["api-bindings", "development-tools::profiling", "wasm"] 13 | rust-version = "1.64" 14 | 15 | [dependencies] 16 | wasm-bindgen = "0.2" 17 | js-sys = "0.3" 18 | serde = { version = "1", features = ["derive"] } 19 | gloo-utils = { version = "0.2", path = "../utils", features = ["serde"] } 20 | [dependencies.web-sys] 21 | version = "0.3" 22 | features = ["console", "Document"] 23 | 24 | [dev-dependencies] 25 | wasm-bindgen-test = "0.3.4" 26 | gloo-timers = { version = "0.3.0", path = "../timers" } 27 | -------------------------------------------------------------------------------- /crates/console/README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

gloo-console

4 | 5 |

6 | Crates.io version 7 | Download 8 | docs.rs docs 9 |

10 | 11 |

12 | API Docs 13 | | 14 | Contributing 15 | | 16 | Chat 17 |

18 | 19 | Built with 🦀🕸 by The Rust and WebAssembly Working Group 20 |
21 | 22 | The JavaScript's `console` object provides access to the browser's console. 23 | Using the `console` object in Rust/WASM directly is cumbersome as it requires JavaScript glue code. 24 | This crate exists to solve this problem by providing a set of ergonomic Rust APIs to deal 25 | with the browser console. 26 | 27 | # Example 28 | 29 | The following example logs text to the console using `console.log` 30 | 31 | ```rust 32 | use gloo_console::log; 33 | let object = JsValue::from("any JsValue can be logged"); 34 | log!("text", object); 35 | ``` 36 | -------------------------------------------------------------------------------- /crates/console/src/console_dbg.rs: -------------------------------------------------------------------------------- 1 | /// A macro similar to [`dbg!`] that logs [`JsValue`][wasm_bindgen::JsValue]s to console. 2 | /// 3 | /// See the [stdlib documentation][std::dbg] to learn more. This macro calls `console.log` 4 | /// instead of `eprintln!` for `JsValue`s. The formatting is done by the browser. If you want 5 | /// [`Debug`][std::fmt::Debug] implementation to be used instead, consider using [`console_dbg`] 6 | #[macro_export] 7 | macro_rules! console { 8 | () => { 9 | $crate::log!( 10 | ::std::format!("%c[{}:{}] ", ::std::file!(), ::std::line!()), 11 | "font-weight: bold" 12 | ); 13 | }; 14 | ($val:expr $(,)?) => { 15 | { 16 | let v = $val; 17 | $crate::__console_inner!(v $val) 18 | } 19 | }; 20 | ($($val:expr),+ $(,)?) => { 21 | ($($crate::console!($val)),+,) 22 | }; 23 | } 24 | 25 | /// A macro similar to [`dbg!`] to log to browser console. 26 | /// 27 | /// See the [stdlib documentation][std::dbg] to learn more. This macro calls `console.log` 28 | /// instead of `eprintln!`. This macro passing the values to [`console`] after formatting them using 29 | /// the [`Debug`][std::fmt::Debug] implementation. 30 | #[macro_export] 31 | macro_rules! console_dbg { 32 | () => { 33 | $crate::console!() 34 | }; 35 | ($val:expr $(,)?) => { 36 | { 37 | let v: $crate::__macro::JsValue = ::std::format!("{:?}", $val).into(); 38 | $crate::__console_inner!(v $val) 39 | } 40 | }; 41 | ($($val:expr),+ $(,)?) => { 42 | ($($crate::console_dbg!($val)),+,) 43 | }; 44 | } 45 | 46 | /// This is an implementation detail and *should not* be called directly! 47 | #[doc(hidden)] 48 | #[macro_export] 49 | macro_rules! __console_inner { 50 | ($js_value:ident $val:expr) => {{ 51 | $crate::log!( 52 | ::std::format!("%c[{}:{}] ", ::std::file!(), ::std::line!()), 53 | "font-weight: bold", 54 | ::std::format!("{} = ", ::std::stringify!($val)), 55 | &$js_value 56 | ); 57 | $js_value 58 | }}; 59 | } 60 | 61 | #[cfg(test)] 62 | mod tests { 63 | #![allow(dead_code)] 64 | //! These exist to ensure code compiles 65 | use wasm_bindgen::JsValue; 66 | 67 | fn console_works() { 68 | console!(); 69 | { 70 | let js_value = JsValue::from("test"); 71 | console!(js_value); 72 | } 73 | { 74 | let js_value_1 = JsValue::from("test 1"); 75 | let js_value_2 = JsValue::from("test 2"); 76 | console!(js_value_1, js_value_2); 77 | } 78 | } 79 | 80 | fn console_dbg_works() { 81 | #[derive(Debug)] 82 | struct Value(&'static str); 83 | 84 | console_dbg!(); 85 | { 86 | let value = Value("test"); 87 | console_dbg!(value); 88 | } 89 | { 90 | let value_1 = Value("test 1"); 91 | let value_2 = Value("test 2"); 92 | console_dbg!(value_1, value_2); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /crates/console/src/counter.rs: -------------------------------------------------------------------------------- 1 | //! The `console.count` and `console.countReset` functions allow you to run a counter 2 | //! amd log it to the browser's developer tools console. You 3 | //! call `console.count("foo")` when the counter begins, and call 4 | //! `console.countReset("foo")` when it is to be reset. 5 | //! 6 | //! [See MDN for more info](https://developer.mozilla.org/en-US/docs/Web/API/Console/count). 7 | //! 8 | //! This API wraps both the `count` and `countReset` calls into a single type 9 | //! named `Counter`, ensuring both are called. 10 | //! 11 | //! The counter is started with 12 | //! 13 | //! ```no_run 14 | //! use gloo_console::Counter; 15 | //! 16 | //! let counter = Counter::new("foo"); 17 | //! 18 | //! counter.count(); 19 | //! counter.count(); 20 | //! ``` 21 | 22 | use web_sys::console; 23 | 24 | /// A console time measurement. 25 | /// 26 | /// Dropping this will reset the counter to 0. 27 | #[derive(Debug)] 28 | pub struct Counter<'a> { 29 | label: &'a str, 30 | } 31 | 32 | impl<'a> Counter<'a> { 33 | /// Starts a console time measurement. The measurement 34 | /// ends when the constructed `ConsoleTimer` object is dropped. 35 | /// 36 | /// # Example 37 | /// 38 | /// ```no_run 39 | /// use gloo_console::Counter; 40 | /// 41 | /// let _timer = Counter::new("foo"); 42 | /// ``` 43 | pub fn new(label: &'a str) -> Counter<'a> { 44 | console::count_with_label(label); 45 | Counter { label } 46 | } 47 | 48 | /// Increments the counter 49 | pub fn count(&self) { 50 | console::count_with_label(self.label); 51 | } 52 | } 53 | 54 | impl<'a> Drop for Counter<'a> { 55 | fn drop(&mut self) { 56 | console::count_reset_with_label(self.label); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /crates/console/src/externs.rs: -------------------------------------------------------------------------------- 1 | use js_sys::Array; 2 | use std::boxed::Box; 3 | use wasm_bindgen::prelude::*; 4 | 5 | #[wasm_bindgen] 6 | extern "C" { 7 | #[wasm_bindgen(js_namespace = console)] 8 | pub fn assert(assertion: bool, objs: Box<[JsValue]>); 9 | 10 | #[wasm_bindgen(js_namespace = console)] 11 | pub fn clear(); 12 | 13 | // TODO console.count() 14 | // TODO console.countReset() 15 | 16 | #[wasm_bindgen(js_namespace = console, variadic)] 17 | pub fn debug(items: Box<[JsValue]>); 18 | 19 | #[wasm_bindgen(js_namespace = console)] 20 | pub fn dir(items: &JsValue); 21 | 22 | #[wasm_bindgen(js_namespace = console)] 23 | pub fn dirxml(items: &JsValue); 24 | 25 | #[wasm_bindgen(js_namespace = console, variadic)] 26 | pub fn error(items: Box<[JsValue]>); 27 | 28 | #[wasm_bindgen(js_namespace = console, variadic)] 29 | pub fn group(items: Box<[JsValue]>); 30 | 31 | #[wasm_bindgen(js_namespace = console, js_name = groupCollapsed, variadic)] 32 | pub fn group_collapsed(items: Box<[JsValue]>); 33 | 34 | #[wasm_bindgen(js_namespace = console, js_name = groupEnd)] 35 | pub fn group_end(); 36 | 37 | #[wasm_bindgen(js_namespace = console, variadic)] 38 | pub fn info(items: Box<[JsValue]>); 39 | 40 | #[wasm_bindgen(js_namespace = console, variadic)] 41 | pub fn log(items: Box<[JsValue]>); 42 | 43 | #[wasm_bindgen(js_namespace = console, js_name = table)] 44 | pub fn table_with_data(data: JsValue); 45 | 46 | #[wasm_bindgen(js_namespace = console, js_name = table)] 47 | pub fn table_with_data_and_columns(data: JsValue, columns: Array); 48 | 49 | #[wasm_bindgen(js_namespace = console, variadic)] 50 | pub fn trace(items: Box<[JsValue]>); 51 | 52 | #[wasm_bindgen(js_namespace = console, variadic)] 53 | pub fn warn(items: Box<[JsValue]>); 54 | 55 | } 56 | -------------------------------------------------------------------------------- /crates/console/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! The JavaScript's `console` object provides access to the browser's console. 2 | //! Using the `console` object in Rust/WASM directly is cumbersome as it requires JavaScript glue code. 3 | //! This crate exists to solve this problem by providing a set of ergonomic Rust APIs to deal 4 | //! with the browser console. 5 | //! 6 | //! # Example 7 | //! 8 | //! The following example logs text to the console using `console.log` 9 | //! 10 | //! ```no_run, rust 11 | //! # use wasm_bindgen::JsValue; 12 | //! use gloo_console::log; 13 | //! 14 | //! let object = JsValue::from("any JsValue can be logged"); 15 | //! log!("text", object) 16 | //! ``` 17 | 18 | #![deny(missing_docs, missing_debug_implementations)] 19 | 20 | mod console_dbg; 21 | mod counter; 22 | #[doc(hidden)] 23 | pub mod externs; 24 | mod macros; 25 | mod timer; 26 | 27 | pub use counter::Counter; 28 | pub use macros::*; 29 | pub use timer::Timer; 30 | 31 | #[doc(hidden)] 32 | pub mod __macro { 33 | use gloo_utils::format::JsValueSerdeExt; 34 | pub use js_sys::Array; 35 | pub use wasm_bindgen::JsValue; 36 | use wasm_bindgen::UnwrapThrowExt; 37 | 38 | pub fn table_with_data_and_columns<'a>( 39 | data: impl serde::Serialize, 40 | columns: impl IntoIterator, 41 | ) { 42 | let data = ::from_serde(&data).unwrap_throw(); 43 | let columns = columns.into_iter().map(JsValue::from_str).collect(); 44 | 45 | crate::externs::table_with_data_and_columns(data, columns); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /crates/console/src/timer.rs: -------------------------------------------------------------------------------- 1 | //! The `console.time` and `console.timeEnd` functions allow you to log the 2 | //! timing of named operations to the browser's developer tools console. You 3 | //! call `console.time("foo")` when the operation begins, and call 4 | //! `console.timeEnd("foo")` when it finishes. 5 | //! 6 | //! Additionally, these measurements will show up in your browser's profiler's 7 | //! "timeline" or "waterfall" view. 8 | //! 9 | //! [See MDN for more info](https://developer.mozilla.org/en-US/docs/Web/API/console#Timers). 10 | //! 11 | //! This API wraps both the `time` and `timeEnd` calls into a single type 12 | //! named `ConsoleTimer`, ensuring both are called. 13 | //! 14 | //! ## Scoped Measurement 15 | //! 16 | //! Wrap code to be measured in a closure with [`Timer::scope`]. 17 | //! 18 | //! ```no_run 19 | //! use gloo_console::Timer; 20 | //! 21 | //! let value = Timer::scope("foo", || { 22 | //! // Place code to be measured here 23 | //! // Optionally return a value. 24 | //! }); 25 | //! ``` 26 | //! 27 | //! ## RAII-Style Measurement 28 | //! 29 | //! For scenarios where [`Timer::scope`] can't be used, like with 30 | //! asynchronous operations, you can use `ConsoleTimer::new` to create a timer. 31 | //! The measurement ends when the timer object goes out of scope / is dropped. 32 | //! 33 | //! ```no_run 34 | //! use gloo_console::Timer; 35 | //! use gloo_timers::callback::Timeout; 36 | //! 37 | //! // Start timing a new operation. 38 | //! let timer = Timer::new("foo"); 39 | //! 40 | //! // And then asynchronously finish timing. 41 | //! let timeout = Timeout::new(1_000, move || { 42 | //! drop(timer); 43 | //! }); 44 | //! ``` 45 | 46 | use web_sys::console; 47 | 48 | /// A console time measurement. 49 | /// 50 | /// See [`Timer::scope`] for starting a labeled time measurement 51 | /// of code wrapped in a closure. 52 | #[derive(Debug)] 53 | pub struct Timer<'a> { 54 | label: &'a str, 55 | } 56 | 57 | impl<'a> Timer<'a> { 58 | /// Starts a console time measurement. The measurement 59 | /// ends when the constructed `ConsoleTimer` object is dropped. 60 | /// 61 | /// # Example 62 | /// 63 | /// ```no_run 64 | /// use gloo_console::Timer; 65 | /// 66 | /// let _timer = Timer::new("foo"); 67 | /// ``` 68 | pub fn new(label: &'a str) -> Timer<'a> { 69 | console::time_with_label(label); 70 | Timer { label } 71 | } 72 | 73 | /// Starts a scoped console time measurement 74 | /// 75 | /// # Example 76 | /// 77 | /// ```no_run 78 | /// use gloo_console::Timer; 79 | /// 80 | /// let value = Timer::scope("foo", || { 81 | /// // Code to measure here 82 | /// }); 83 | /// ``` 84 | pub fn scope(label: &str, f: F) -> T 85 | where 86 | F: FnOnce() -> T, 87 | { 88 | let _timer = Timer::new(label); 89 | f() 90 | } 91 | } 92 | 93 | impl<'a> Drop for Timer<'a> { 94 | fn drop(&mut self) { 95 | console::time_end_with_label(self.label); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /crates/console/tests/web.rs: -------------------------------------------------------------------------------- 1 | //! Test suite for the Web and headless browsers. 2 | 3 | #![cfg(target_arch = "wasm32")] 4 | 5 | use gloo_console::Timer; 6 | use wasm_bindgen_test::*; 7 | 8 | wasm_bindgen_test_configure!(run_in_browser); 9 | 10 | #[wasm_bindgen_test] 11 | fn scoped_timer_returns_value() { 12 | let value = Timer::scope("foo", || true); 13 | 14 | assert!(value); 15 | } 16 | -------------------------------------------------------------------------------- /crates/dialogs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gloo-dialogs" 3 | description = "Convenience crate for working with dialogs in browser" 4 | version = "0.2.0" 5 | authors = ["Rust and WebAssembly Working Group"] 6 | edition = "2021" 7 | license = "MIT OR Apache-2.0" 8 | readme = "README.md" 9 | repository = "https://github.com/rustwasm/gloo/tree/master/crates/dialogs" 10 | homepage = "https://github.com/rustwasm/gloo" 11 | categories = ["api-bindings", "asynchronous", "wasm"] 12 | rust-version = "1.64" 13 | 14 | [dependencies] 15 | wasm-bindgen = "0.2" 16 | 17 | [dependencies.web-sys] 18 | version = "0.3" 19 | features = ["Window"] 20 | -------------------------------------------------------------------------------- /crates/dialogs/README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

gloo-dialogs

4 | 5 |

6 | Crates.io version 7 | Download 8 | docs.rs docs 9 |

10 | 11 |

12 | API Docs 13 | | 14 | Contributing 15 | | 16 | Chat 17 |

18 | 19 | Built with 🦀🕸 by The Rust and WebAssembly Working Group 20 |
21 | 22 | This crate provides wrappers for the following functions. 23 | - [`alert`](https://developer.mozilla.org/en-US/docs/Web/API/Window/alert) 24 | - [`confirm`](https://developer.mozilla.org/en-US/docs/Web/API/Window/confirm) 25 | - [`prompt`](https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt) 26 | 27 | `web-sys` provides a raw API which is hard to use. This crate provides an easy-to-use, 28 | idiomatic Rust API for these functions. 29 | 30 | See the [API documentation](https://docs.rs/gloo-dialogs) to learn more. 31 | -------------------------------------------------------------------------------- /crates/dialogs/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This crate provides wrapper for `alert`, `prompt` and `confirm` functions. 2 | //! `web-sys` provides a raw API which is hard to use. This crate provides an easy-to-use, 3 | //! idiomatic Rust API for these functions. 4 | //! 5 | //! See the documentation for [`alert`], [`prompt`] and [`confirm`] for more information. 6 | 7 | use wasm_bindgen::prelude::*; 8 | 9 | /// Calls the alert function. 10 | /// 11 | /// [MDN Documentation](https://developer.mozilla.org/en-US/docs/Web/API/Window/alert) 12 | pub fn alert(message: &str) { 13 | window().alert_with_message(message).unwrap_throw() 14 | } 15 | 16 | /// Calls the confirm function. 17 | /// 18 | /// [MDN Documentation](https://developer.mozilla.org/en-US/docs/Web/API/Window/confirm) 19 | pub fn confirm(message: &str) -> bool { 20 | window().confirm_with_message(message).unwrap_throw() 21 | } 22 | 23 | /// Calls the `prompt` function. 24 | /// 25 | /// A default value can be supplied which will be returned if the user doesn't input anything. 26 | /// This function will return `None` if the value of `default` is `None` and the user cancels 27 | /// the operation. 28 | /// 29 | /// [MDN Documentation](https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt) 30 | pub fn prompt(message: &str, default: Option<&str>) -> Option { 31 | match default { 32 | Some(default) => window() 33 | .prompt_with_message_and_default(message, default) 34 | .expect_throw("can't read input"), 35 | None => window() 36 | .prompt_with_message(message) 37 | .expect_throw("can't read input"), 38 | } 39 | } 40 | 41 | #[inline] 42 | fn window() -> web_sys::Window { 43 | web_sys::window().expect_throw("can't access window") 44 | } 45 | -------------------------------------------------------------------------------- /crates/events/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gloo-events" 3 | description = "Convenience crate for working with DOM event listeners" 4 | version = "0.2.0" 5 | authors = ["Rust and WebAssembly Working Group"] 6 | edition = "2021" 7 | license = "MIT OR Apache-2.0" 8 | readme = "README.md" 9 | repository = "https://github.com/rustwasm/gloo/tree/master/crates/events" 10 | homepage = "https://github.com/rustwasm/gloo" 11 | categories = ["api-bindings", "asynchronous", "web-programming", "wasm"] 12 | rust-version = "1.64" 13 | 14 | [dependencies] 15 | wasm-bindgen = "0.2" 16 | 17 | [dependencies.web-sys] 18 | version = "0.3.31" 19 | features = ["Event", "EventTarget", "AddEventListenerOptions"] 20 | 21 | [dev-dependencies] 22 | js-sys = "0.3.31" 23 | futures = "0.3" 24 | wasm-bindgen-test = "0.3.4" 25 | 26 | [dev-dependencies.web-sys] 27 | version = "0.3.31" 28 | features = [ 29 | "HtmlElement", 30 | "Window", 31 | "Document", 32 | "Element", 33 | "MouseEvent", 34 | "ProgressEvent", 35 | ] 36 | -------------------------------------------------------------------------------- /crates/events/README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

gloo-events

4 | 5 |

6 | Crates.io version 7 | Download 8 | docs.rs docs 9 |

10 | 11 |

12 | API Docs 13 | | 14 | Contributing 15 | | 16 | Chat 17 |

18 | 19 | Built with 🦀🕸 by The Rust and WebAssembly Working Group 20 |
21 | 22 | Using event listeners with [`web-sys`](https://crates.io/crates/web-sys) is hard! This crate 23 | provides an [`EventListener`] type which makes it easy! 24 | 25 | See the documentation for [`EventListener`] for more information. 26 | 27 | [`EventListener`]: https://docs.rs/gloo-events/latest/gloo_events/struct.EventListener.html 28 | -------------------------------------------------------------------------------- /crates/file/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gloo-file" 3 | description = "Convenience crate for working with JavaScript files and blobs" 4 | version = "0.3.0" 5 | authors = ["Rust and WebAssembly Working Group"] 6 | edition = "2021" 7 | license = "MIT OR Apache-2.0" 8 | readme = "README.md" 9 | repository = "https://github.com/rustwasm/gloo/tree/master/crates/file" 10 | homepage = "https://github.com/rustwasm/gloo" 11 | categories = ["api-bindings", "asynchronous", "wasm"] 12 | rust-version = "1.64" 13 | 14 | [package.metadata.docs.rs] 15 | features = ["futures"] 16 | 17 | [dependencies] 18 | wasm-bindgen = "0.2" 19 | js-sys = "0.3.31" 20 | gloo-events = { path = "../events", version = "0.2" } 21 | mime = { version = "0.3.13", optional = true } 22 | futures-channel = { version = "0.3", optional = true } 23 | 24 | [dependencies.web-sys] 25 | version = "0.3.31" 26 | features = [ 27 | "Blob", 28 | "File", 29 | "FileList", 30 | "FileReader", 31 | "HtmlInputElement", 32 | "BlobPropertyBag", 33 | "FilePropertyBag", 34 | "DomException", 35 | "Url", 36 | ] 37 | 38 | [dev-dependencies] 39 | futures_rs = { version = "0.3", package = "futures" } 40 | wasm-bindgen-test = "0.3.4" 41 | wasm-bindgen-futures = "0.4" 42 | chrono = { version = "0.4.10", features = ["wasmbind"] } 43 | 44 | [dev-dependencies.web-sys] 45 | version = "0.3.31" 46 | features = ["Window", "Response"] 47 | 48 | [features] 49 | default = [] 50 | futures = ["futures-channel"] 51 | -------------------------------------------------------------------------------- /crates/file/README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

gloo-file

4 | 5 |

6 | Crates.io version 7 | Download 8 | docs.rs docs 9 |

10 | 11 |

12 | API Docs 13 | | 14 | Contributing 15 | | 16 | Chat 17 |

18 | 19 | Built with 🦀🕸 by The Rust and WebAssembly Working Group 20 |
21 | 22 | 23 | Working with files and blobs on the Web. 24 | 25 | These APIs come in two flavors: 26 | 27 | 1. a callback style (that more directly mimics the JavaScript APIs), and 28 | 2. a `Future` API. 29 | -------------------------------------------------------------------------------- /crates/file/src/file_list.rs: -------------------------------------------------------------------------------- 1 | use crate::blob::File; 2 | use wasm_bindgen::prelude::*; 3 | 4 | /// A list of files, for example from an ``. 5 | #[derive(Debug, Clone, PartialEq, Eq)] 6 | pub struct FileList { 7 | inner: Vec, 8 | } 9 | 10 | impl From for FileList { 11 | fn from(raw: web_sys::FileList) -> Self { 12 | let length = raw.length(); 13 | 14 | let inner = (0..length) 15 | .map(|i| File::from(raw.get(i).unwrap_throw())) 16 | .collect(); 17 | 18 | FileList { inner } 19 | } 20 | } 21 | 22 | impl std::ops::Deref for FileList { 23 | type Target = [File]; 24 | 25 | fn deref(&self) -> &Self::Target { 26 | &self.inner 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /crates/file/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! Working with files and blobs on the Web. 3 | //! 4 | //! These APIs come in two flavors: 5 | //! 6 | //! 1. a callback style (that more directly mimics the JavaScript APIs), and 7 | //! 2. a `Future` API. 8 | 9 | mod blob; 10 | mod file_list; 11 | mod file_reader; 12 | mod object_url; 13 | 14 | pub use blob::*; 15 | pub use file_list::*; 16 | pub use file_reader::*; 17 | pub use object_url::*; 18 | 19 | mod sealed { 20 | pub trait Sealed {} 21 | } 22 | use sealed::Sealed; 23 | -------------------------------------------------------------------------------- /crates/file/src/object_url.rs: -------------------------------------------------------------------------------- 1 | use crate::{Blob, File}; 2 | use std::{ops::Deref, rc::Rc}; 3 | 4 | use wasm_bindgen::UnwrapThrowExt; 5 | use web_sys::Url; 6 | 7 | struct ObjectUrlAllocation { 8 | url: String, 9 | } 10 | 11 | impl Drop for ObjectUrlAllocation { 12 | fn drop(&mut self) { 13 | web_sys::Url::revoke_object_url(&self.url).unwrap_throw(); 14 | } 15 | } 16 | 17 | /// A resource wrapper around [`URL.createObjectURL`] / [`URL.revokeObjectURL`]. 18 | /// 19 | /// A [`Blob`], in particular a [`File`], can be converted to a short URL representing its data with the above methods. 20 | /// An [`ObjectUrl`] can be cheaply cloned and shared and revokes the underlying URL when the last reference is dropped. 21 | /// 22 | /// Note that multiple urls can be created for the same blob, without being guaranteed to be de-deduplicated. 23 | /// 24 | /// # Example 25 | /// 26 | /// ```rust,no_run 27 | /// use gloo_file::{Blob, ObjectUrl}; 28 | /// 29 | /// let blob = Blob::new("hello world"); 30 | /// let object_url = ObjectUrl::from(blob); 31 | /// ``` 32 | /// 33 | /// [`URL.createObjectURL`]: https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL 34 | /// [`URL.revokeObjectURL`]: https://developer.mozilla.org/en-US/docs/Web/API/URL/revokeObjectURL 35 | /// [`File`]: crate::File 36 | #[derive(Clone)] 37 | pub struct ObjectUrl { 38 | inner: Rc, 39 | } 40 | 41 | impl From for ObjectUrl { 42 | fn from(file: File) -> Self { 43 | Blob::from(file).into() 44 | } 45 | } 46 | 47 | impl From for ObjectUrl { 48 | fn from(blob: Blob) -> Self { 49 | web_sys::Blob::from(blob).into() 50 | } 51 | } 52 | 53 | impl From for ObjectUrl { 54 | fn from(blob: web_sys::Blob) -> Self { 55 | let url = Url::create_object_url_with_blob(&blob).unwrap_throw(); 56 | let inner = Rc::new(ObjectUrlAllocation { url }); 57 | Self { inner } 58 | } 59 | } 60 | 61 | // Note: some browsers support Url::create_object_url_with_source but this is deprecated! 62 | // https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL#using_object_urls_for_media_streams 63 | 64 | impl Deref for ObjectUrl { 65 | type Target = str; 66 | 67 | fn deref(&self) -> &Self::Target { 68 | &self.inner.url 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /crates/history/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gloo-history" 3 | version = "0.2.2" 4 | description = "Universal Session History" 5 | authors = ["Rust and WebAssembly Working Group"] 6 | edition = "2021" 7 | license = "MIT OR Apache-2.0" 8 | readme = "README.md" 9 | repository = "https://github.com/rustwasm/gloo/tree/master/crates/history" 10 | homepage = "https://github.com/rustwasm/gloo" 11 | categories = ["api-bindings", "history", "wasm"] 12 | rust-version = "1.64" 13 | 14 | [dependencies] 15 | gloo-utils = { version = "0.2.0", path = "../utils" } 16 | gloo-events = { version = "0.2.0", path = "../events" } 17 | serde = { version = "1", features = ["derive"] } 18 | serde-wasm-bindgen = "0.6.0" 19 | serde_urlencoded = { version = "0.7", optional = true } 20 | thiserror = { version = "1.0", optional = true } 21 | wasm-bindgen = "0.2.88" 22 | 23 | [dependencies.web-sys] 24 | version = "0.3" 25 | features = ["History", "Window", "Location", "Url"] 26 | 27 | [target.'cfg(target_arch = "wasm32")'.dependencies] 28 | getrandom = { version = "0.2.10", features = ["js"] } 29 | 30 | [dev-dependencies] 31 | wasm-bindgen-test = "0.3" 32 | gloo-timers = { version = "0.3.0", features = ["futures"], path = "../timers" } 33 | 34 | [features] 35 | query = ["thiserror", "serde_urlencoded"] 36 | default = ["query"] 37 | -------------------------------------------------------------------------------- /crates/history/README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

gloo-history

4 | 5 |

6 | Crates.io version 7 | Download 8 | docs.rs docs 9 |

10 | 11 |

12 | API Docs 13 | | 14 | Contributing 15 | | 16 | Chat 17 |

18 | 19 | Built with 🦀🕸 by The Rust and WebAssembly Working Group 20 |
21 | 22 | This crate provides wrappers for the History API. See API docs to learn more 23 | -------------------------------------------------------------------------------- /crates/history/src/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | /// The Error type for History. 4 | #[derive(Error, Debug)] 5 | pub enum HistoryError { 6 | /// Failed to serialize query. 7 | #[cfg(feature = "query")] 8 | #[error("failed to serialize query.")] 9 | QuerySer(#[from] serde_urlencoded::ser::Error), 10 | /// Failed to deserialize query. 11 | #[cfg(feature = "query")] 12 | #[error("failed to deserialize query.")] 13 | QueryDe(#[from] serde_urlencoded::de::Error), 14 | } 15 | 16 | /// The Result type for History. 17 | pub type HistoryResult = std::result::Result; 18 | -------------------------------------------------------------------------------- /crates/history/src/history.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use crate::listener::HistoryListener; 4 | use crate::location::Location; 5 | #[cfg(feature = "query")] 6 | use crate::{error::HistoryResult, query::ToQuery}; 7 | 8 | /// A trait to provide [`History`] access. 9 | /// 10 | /// # Warning 11 | /// 12 | /// The behaviour of this trait is not well-defined when you mix multiple history kinds in the same application 13 | /// or use `window().history()` to update session history. 14 | pub trait History: Clone + PartialEq { 15 | /// Returns the number of elements in [`History`]. 16 | fn len(&self) -> usize; 17 | 18 | /// Returns true if the current [`History`] is empty. 19 | fn is_empty(&self) -> bool { 20 | self.len() == 0 21 | } 22 | 23 | /// Moves back 1 page in [`History`]. 24 | fn back(&self) { 25 | self.go(-1); 26 | } 27 | 28 | /// Moves forward 1 page in [`History`]. 29 | fn forward(&self) { 30 | self.go(1); 31 | } 32 | 33 | /// Loads a specific page in [`History`] with a `delta` relative to current page. 34 | /// 35 | /// See: 36 | fn go(&self, delta: isize); 37 | 38 | /// Pushes a route entry with [`None`] being the state. 39 | fn push<'a>(&self, route: impl Into>); 40 | 41 | /// Replaces the current history entry with provided route and [`None`] state. 42 | fn replace<'a>(&self, route: impl Into>); 43 | 44 | /// Pushes a route entry with state. 45 | fn push_with_state<'a, T>(&self, route: impl Into>, state: T) 46 | where 47 | T: 'static; 48 | 49 | /// Replaces the current history entry with provided route and state. 50 | fn replace_with_state<'a, T>(&self, route: impl Into>, state: T) 51 | where 52 | T: 'static; 53 | 54 | /// Same as `.push()` but affix the queries to the end of the route. 55 | #[cfg(feature = "query")] 56 | fn push_with_query<'a, Q>( 57 | &self, 58 | route: impl Into>, 59 | query: Q, 60 | ) -> HistoryResult<(), Q::Error> 61 | where 62 | Q: ToQuery; 63 | 64 | /// Same as `.replace()` but affix the queries to the end of the route. 65 | #[cfg(feature = "query")] 66 | fn replace_with_query<'a, Q>( 67 | &self, 68 | route: impl Into>, 69 | query: Q, 70 | ) -> HistoryResult<(), Q::Error> 71 | where 72 | Q: ToQuery; 73 | 74 | /// Same as `.push_with_state()` but affix the queries to the end of the route. 75 | #[cfg(feature = "query")] 76 | fn push_with_query_and_state<'a, Q, T>( 77 | &self, 78 | route: impl Into>, 79 | query: Q, 80 | state: T, 81 | ) -> HistoryResult<(), Q::Error> 82 | where 83 | Q: ToQuery, 84 | T: 'static; 85 | 86 | /// Same as `.replace_with_state()` but affix the queries to the end of the route. 87 | #[cfg(feature = "query")] 88 | fn replace_with_query_and_state<'a, Q, T>( 89 | &self, 90 | route: impl Into>, 91 | query: Q, 92 | state: T, 93 | ) -> HistoryResult<(), Q::Error> 94 | where 95 | Q: ToQuery, 96 | T: 'static; 97 | 98 | /// Creates a Listener that will be notified when current state changes. 99 | /// 100 | /// This method returns a [`HistoryListener`] that will automatically unregister the callback 101 | /// when dropped. 102 | fn listen(&self, callback: CB) -> HistoryListener 103 | where 104 | CB: Fn() + 'static; 105 | 106 | /// Returns current [`Location`]. 107 | fn location(&self) -> Location; 108 | } 109 | -------------------------------------------------------------------------------- /crates/history/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A module that provides universal session history and location information. 2 | 3 | #![deny(clippy::all)] 4 | #![deny(missing_docs, missing_debug_implementations)] 5 | 6 | mod any; 7 | mod browser; 8 | #[cfg(feature = "query")] 9 | mod error; 10 | mod hash; 11 | mod history; 12 | mod listener; 13 | mod location; 14 | mod memory; 15 | #[cfg(feature = "query")] 16 | pub mod query; 17 | mod state; 18 | mod utils; 19 | 20 | pub use any::AnyHistory; 21 | pub use browser::BrowserHistory; 22 | pub use hash::HashHistory; 23 | pub use memory::MemoryHistory; 24 | 25 | #[cfg(feature = "query")] 26 | pub use error::{HistoryError, HistoryResult}; 27 | pub use history::History; 28 | pub use listener::HistoryListener; 29 | pub use location::Location; 30 | -------------------------------------------------------------------------------- /crates/history/src/listener.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::rc::Rc; 3 | 4 | /// A History Listener to manage callbacks registered on a [`History`][crate::History]. 5 | /// 6 | /// This Listener has the same behaviour as the [`EventListener`][gloo_events::EventListener] from 7 | /// `gloo` that the underlying callback will be unregistered when the listener is dropped. 8 | #[must_use = "the listener is removed when `HistoryListener` is dropped"] 9 | pub struct HistoryListener { 10 | pub(crate) _listener: Rc, 11 | } 12 | 13 | impl fmt::Debug for HistoryListener { 14 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 15 | f.debug_struct("HistoryListener").finish() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /crates/history/src/location.rs: -------------------------------------------------------------------------------- 1 | use std::any::Any; 2 | use std::rc::Rc; 3 | 4 | #[cfg(feature = "query")] 5 | use crate::{error::HistoryResult, query::FromQuery}; 6 | 7 | /// A history location. 8 | /// 9 | /// This struct provides location information at the time 10 | /// [`History::location`][crate::History::location] is called. 11 | #[derive(Clone, Debug)] 12 | pub struct Location { 13 | pub(crate) path: Rc, 14 | pub(crate) query_str: Rc, 15 | pub(crate) hash: Rc, 16 | pub(crate) state: Option>, 17 | pub(crate) id: Option, 18 | } 19 | 20 | impl Location { 21 | /// Returns a unique id of current location. 22 | /// 23 | /// Returns [`None`] if current location is not created by `gloo::history`. 24 | /// 25 | /// # Warning 26 | /// 27 | /// Depending on the situation, the id may or may not be sequential / incremental. 28 | pub fn id(&self) -> Option { 29 | self.id 30 | } 31 | 32 | /// Returns the `pathname` of current location. 33 | pub fn path(&self) -> &str { 34 | &self.path 35 | } 36 | 37 | /// Returns the queries of current URL in [`&str`]. 38 | pub fn query_str(&self) -> &str { 39 | &self.query_str 40 | } 41 | 42 | /// Returns the queries of current URL parsed as `T`. 43 | #[cfg(feature = "query")] 44 | pub fn query(&self) -> HistoryResult 45 | where 46 | T: FromQuery, 47 | { 48 | let query = self.query_str().strip_prefix('?').unwrap_or(""); 49 | T::from_query(query) 50 | } 51 | 52 | /// Returns the hash fragment of current URL. 53 | pub fn hash(&self) -> &str { 54 | &self.hash 55 | } 56 | 57 | /// Returns an Rc'ed state of current location. 58 | /// 59 | /// Returns [`None`] if state is not created by `gloo::history`, or state fails to downcast. 60 | pub fn state(&self) -> Option> 61 | where 62 | T: 'static, 63 | { 64 | self.state.clone().and_then(|m| m.downcast().ok()) 65 | } 66 | } 67 | 68 | impl PartialEq for Location { 69 | fn eq(&self, rhs: &Self) -> bool { 70 | if let Some(lhs) = self.id() { 71 | if let Some(rhs) = rhs.id() { 72 | return lhs == rhs; 73 | } 74 | } 75 | false 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /crates/history/src/state.rs: -------------------------------------------------------------------------------- 1 | use std::any::Any; 2 | use std::collections::HashMap; 3 | use std::rc::Rc; 4 | 5 | use serde::{Deserialize, Serialize}; 6 | 7 | use crate::utils::get_id; 8 | 9 | /// A constant to prevent state collision. 10 | #[derive(Debug, Clone, Serialize, Deserialize)] 11 | enum HistoryStateKind { 12 | #[serde(rename = "gloo_history_state")] 13 | Gloo, 14 | } 15 | 16 | /// The state used by browser history to store history id. 17 | #[derive(Debug, Clone, Serialize, Deserialize)] 18 | pub(crate) struct HistoryState { 19 | id: u32, 20 | kind: HistoryStateKind, 21 | } 22 | 23 | impl HistoryState { 24 | pub fn new() -> HistoryState { 25 | Self { 26 | id: get_id(), 27 | kind: HistoryStateKind::Gloo, 28 | } 29 | } 30 | 31 | pub fn id(&self) -> u32 { 32 | self.id 33 | } 34 | } 35 | 36 | pub(crate) type StateMap = HashMap>; 37 | -------------------------------------------------------------------------------- /crates/history/src/utils.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::rc::{Rc, Weak}; 3 | use std::sync::atomic::{AtomicU32, Ordering}; 4 | 5 | #[cfg(not(target_os = "wasi"))] 6 | use wasm_bindgen::throw_str; 7 | 8 | #[cfg(any(not(target_arch = "wasm32"), target_os = "wasi"))] 9 | pub(crate) fn get_id() -> u32 { 10 | static ID_CTR: AtomicU32 = AtomicU32::new(0); 11 | 12 | ID_CTR.fetch_add(1, Ordering::SeqCst) 13 | } 14 | 15 | #[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))] 16 | pub(crate) fn get_id() -> u32 { 17 | static ID_CTR: AtomicU32 = AtomicU32::new(0); 18 | static INIT: std::sync::Once = std::sync::Once::new(); 19 | 20 | INIT.call_once(|| { 21 | let mut start: [u8; 4] = [0; 4]; 22 | // If it fails then the start is not or only partly filled. 23 | // But since this method should not fail, we take what we get. 24 | let _ = getrandom::getrandom(&mut start); 25 | // Using a high initial value is not an issue as `fetch_add` does wrap around. 26 | ID_CTR.store(u32::from_ne_bytes(start), Ordering::SeqCst); 27 | }); 28 | 29 | ID_CTR.fetch_add(1, Ordering::SeqCst) 30 | } 31 | 32 | pub(crate) fn assert_absolute_path(path: &str) { 33 | if !path.starts_with('/') { 34 | #[cfg(not(target_os = "wasi"))] 35 | throw_str("You cannot use relative path with this history type."); 36 | #[cfg(target_os = "wasi")] 37 | panic!("You cannot use relative path with this history type."); 38 | } 39 | } 40 | 41 | pub(crate) fn assert_no_query(path: &str) { 42 | if path.contains('?') { 43 | #[cfg(not(target_os = "wasi"))] 44 | throw_str("You cannot have query in path, try use a variant of this method with `_query`."); 45 | #[cfg(target_os = "wasi")] 46 | panic!("You cannot have query in path, try use a variant of this method with `_query`."); 47 | } 48 | } 49 | 50 | pub(crate) fn assert_no_fragment(path: &str) { 51 | if path.contains('#') { 52 | #[cfg(not(target_os = "wasi"))] 53 | throw_str("You cannot use fragments (hash) in memory history."); 54 | #[cfg(target_os = "wasi")] 55 | panic!("You cannot use fragments (hash) in memory history."); 56 | } 57 | } 58 | 59 | pub(crate) type WeakCallback = Weak; 60 | 61 | pub(crate) fn notify_callbacks(callbacks: Rc>>) { 62 | let callables = { 63 | let mut callbacks_ref = callbacks.borrow_mut(); 64 | 65 | // Any gone weak references are removed when called. 66 | let (callbacks, callbacks_weak) = callbacks_ref.iter().cloned().fold( 67 | (Vec::new(), Vec::new()), 68 | |(mut callbacks, mut callbacks_weak), m| { 69 | if let Some(m_strong) = m.clone().upgrade() { 70 | callbacks.push(m_strong); 71 | callbacks_weak.push(m); 72 | } 73 | 74 | (callbacks, callbacks_weak) 75 | }, 76 | ); 77 | 78 | *callbacks_ref = callbacks_weak; 79 | 80 | callbacks 81 | }; 82 | 83 | for callback in callables { 84 | callback() 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /crates/history/tests/browser_history.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; 4 | 5 | use gloo_history::{BrowserHistory, History}; 6 | 7 | wasm_bindgen_test_configure!(run_in_browser); 8 | 9 | mod utils; 10 | use utils::delayed_assert_eq; 11 | 12 | #[test] 13 | async fn history_works() { 14 | let history = BrowserHistory::new(); 15 | { 16 | let history = history.clone(); 17 | delayed_assert_eq(move || history.location().path().to_owned(), || "/").await; 18 | } 19 | 20 | history.push("/path-a"); 21 | 22 | { 23 | let history = history.clone(); 24 | delayed_assert_eq(move || history.location().path().to_owned(), || "/path-a").await; 25 | } 26 | 27 | history.replace("/path-b"); 28 | 29 | { 30 | let history = history.clone(); 31 | delayed_assert_eq(move || history.location().path().to_owned(), || "/path-b").await; 32 | } 33 | 34 | history.back(); 35 | 36 | { 37 | let history = history.clone(); 38 | delayed_assert_eq(move || history.location().path().to_owned(), || "/").await; 39 | } 40 | 41 | history.forward(); 42 | 43 | { 44 | let history = history.clone(); 45 | delayed_assert_eq(move || history.location().path().to_owned(), || "/path-b").await; 46 | } 47 | 48 | let _listener = history.listen({ 49 | let history = history.clone(); 50 | move || { 51 | let location = history.location(); 52 | let state: Option> = location.state(); 53 | assert_eq!(state, Some(Rc::new(location.path().to_owned()))); 54 | } 55 | }); 56 | 57 | history.push_with_state("/fish", String::from("/fish")); 58 | } 59 | -------------------------------------------------------------------------------- /crates/history/tests/hash_history.rs: -------------------------------------------------------------------------------- 1 | use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; 2 | 3 | use gloo_history::{HashHistory, History}; 4 | use gloo_utils::window; 5 | 6 | wasm_bindgen_test_configure!(run_in_browser); 7 | 8 | mod utils; 9 | use utils::delayed_assert_eq; 10 | 11 | #[test] 12 | async fn history_works() { 13 | let history = HashHistory::new(); 14 | 15 | { 16 | let history = history.clone(); 17 | delayed_assert_eq(|| history.location().path().to_owned(), || "/").await; 18 | } 19 | delayed_assert_eq(|| window().location().pathname().unwrap(), || "/").await; 20 | delayed_assert_eq(|| window().location().hash().unwrap(), || "#/").await; 21 | 22 | history.push("/path-a"); 23 | { 24 | let history = history.clone(); 25 | delayed_assert_eq(|| history.location().path().to_owned(), || "/path-a").await; 26 | } 27 | delayed_assert_eq(|| window().location().pathname().unwrap(), || "/").await; 28 | delayed_assert_eq(|| window().location().hash().unwrap(), || "#/path-a").await; 29 | 30 | history.replace("/path-b"); 31 | { 32 | let history = history.clone(); 33 | delayed_assert_eq(|| history.location().path().to_owned(), || "/path-b").await; 34 | } 35 | delayed_assert_eq(|| window().location().pathname().unwrap(), || "/").await; 36 | delayed_assert_eq(|| window().location().hash().unwrap(), || "#/path-b").await; 37 | 38 | history.back(); 39 | { 40 | let history = history.clone(); 41 | delayed_assert_eq(|| history.location().path().to_owned(), || "/").await; 42 | } 43 | delayed_assert_eq(|| window().location().pathname().unwrap(), || "/").await; 44 | delayed_assert_eq(|| window().location().hash().unwrap(), || "#/").await; 45 | 46 | history.forward(); 47 | { 48 | let history = history.clone(); 49 | delayed_assert_eq(|| history.location().path().to_owned(), || "/path-b").await; 50 | } 51 | delayed_assert_eq(|| window().location().pathname().unwrap(), || "/").await; 52 | delayed_assert_eq(|| window().location().hash().unwrap(), || "#/path-b").await; 53 | } 54 | -------------------------------------------------------------------------------- /crates/history/tests/memory_history.rs: -------------------------------------------------------------------------------- 1 | use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; 2 | 3 | use gloo_history::{History, MemoryHistory}; 4 | 5 | wasm_bindgen_test_configure!(run_in_browser); 6 | 7 | #[test] 8 | fn history_works() { 9 | let history = MemoryHistory::new(); 10 | assert_eq!(history.location().path(), "/"); 11 | 12 | history.push("/path-a"); 13 | assert_eq!(history.location().path(), "/path-a"); 14 | 15 | history.replace("/path-b"); 16 | assert_eq!(history.location().path(), "/path-b"); 17 | 18 | history.back(); 19 | assert_eq!(history.location().path(), "/"); 20 | 21 | history.forward(); 22 | assert_eq!(history.location().path(), "/path-b"); 23 | } 24 | -------------------------------------------------------------------------------- /crates/history/tests/memory_history_feat_serialize.rs: -------------------------------------------------------------------------------- 1 | use wasm_bindgen_test::wasm_bindgen_test_configure; 2 | 3 | wasm_bindgen_test_configure!(run_in_browser); 4 | 5 | #[cfg(feature = "query")] 6 | mod feat_serialize { 7 | use wasm_bindgen_test::wasm_bindgen_test as test; 8 | 9 | use std::rc::Rc; 10 | 11 | use serde::{Deserialize, Serialize}; 12 | 13 | use gloo_history::{History, MemoryHistory}; 14 | 15 | #[derive(Debug, Serialize, Deserialize, PartialEq)] 16 | struct Query { 17 | a: String, 18 | b: u64, 19 | } 20 | 21 | #[derive(Debug, Serialize, Deserialize, PartialEq)] 22 | struct State { 23 | i: String, 24 | ii: u64, 25 | } 26 | 27 | #[test] 28 | fn history_serialize_works() { 29 | let history = MemoryHistory::new(); 30 | assert_eq!(history.location().path(), "/"); 31 | 32 | history.push("/path-a"); 33 | assert_eq!(history.location().path(), "/path-a"); 34 | 35 | history.replace("/path-b"); 36 | assert_eq!(history.location().path(), "/path-b"); 37 | 38 | history.back(); 39 | assert_eq!(history.location().path(), "/"); 40 | 41 | history.forward(); 42 | assert_eq!(history.location().path(), "/path-b"); 43 | 44 | history 45 | .push_with_query( 46 | "/path", 47 | Query { 48 | a: "something".to_string(), 49 | b: 123, 50 | }, 51 | ) 52 | .unwrap(); 53 | 54 | assert_eq!(history.location().path(), "/path"); 55 | assert_eq!(history.location().query_str(), "?a=something&b=123"); 56 | assert_eq!( 57 | history.location().query::().unwrap(), 58 | Query { 59 | a: "something".to_string(), 60 | b: 123, 61 | } 62 | ); 63 | 64 | history.push_with_state( 65 | "/path-c", 66 | State { 67 | i: "something".to_string(), 68 | ii: 123, 69 | }, 70 | ); 71 | 72 | assert_eq!(history.location().path(), "/path-c"); 73 | assert_eq!( 74 | history.location().state::().unwrap(), 75 | Rc::new(State { 76 | i: "something".to_string(), 77 | ii: 123, 78 | }) 79 | ); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /crates/history/tests/query.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "query")] 2 | 3 | #[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))] 4 | use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; 5 | #[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))] 6 | wasm_bindgen_test_configure!(run_in_browser); 7 | 8 | use gloo_history::query::*; 9 | use serde::{Deserialize, Serialize}; 10 | 11 | #[derive(Serialize, Deserialize, PartialEq, Debug)] 12 | struct SimpleQuery { 13 | string: String, 14 | number: u64, 15 | optional: Option, 16 | boolean: bool, 17 | } 18 | 19 | #[test] 20 | fn test_raw_encode_simple() { 21 | let query = Raw("name=value&other=that"); 22 | assert_eq!(query.to_query().unwrap(), "name=value&other=that"); 23 | } 24 | 25 | #[test] 26 | fn test_raw_decode_simple() { 27 | let query = "name=value&other=that"; 28 | let decoded = >::from_query(query).unwrap(); 29 | assert_eq!(decoded, query); 30 | } 31 | 32 | #[test] 33 | fn test_urlencoded_encode_simple() { 34 | let query = SimpleQuery { 35 | string: "test".into(), 36 | number: 42, 37 | optional: None, 38 | boolean: true, 39 | }; 40 | 41 | let encoded = query.to_query().unwrap(); 42 | assert_eq!(encoded, "string=test&number=42&boolean=true"); 43 | } 44 | 45 | #[test] 46 | fn test_urlencoded_decode_simple() { 47 | let encoded = "string=test&number=42&boolean=true"; 48 | let data = SimpleQuery::from_query(encoded).unwrap(); 49 | assert_eq!( 50 | data, 51 | SimpleQuery { 52 | string: "test".into(), 53 | number: 42, 54 | optional: None, 55 | boolean: true, 56 | } 57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /crates/history/tests/utils.rs: -------------------------------------------------------------------------------- 1 | use gloo_timers::future::sleep; 2 | use std::fmt::Debug; 3 | use std::time::Duration; 4 | 5 | pub async fn delayed_assert_eq(left: FL, right: FR) 6 | where 7 | FL: Fn() -> L, 8 | FR: Fn() -> R, 9 | L: PartialEq, 10 | L: Debug, 11 | R: Debug, 12 | { 13 | 'outer: for i in 0..2 { 14 | if i > 0 { 15 | assert_eq!(left(), right()); 16 | } 17 | 18 | for _ in 0..100 { 19 | sleep(Duration::from_millis(10)).await; 20 | if left() == right() { 21 | break 'outer; 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /crates/net/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gloo-net" 3 | version = "0.6.0" 4 | authors = [ 5 | "Rust and WebAssembly Working Group", 6 | "Elina ", 7 | ] 8 | edition = "2021" 9 | license = "MIT OR Apache-2.0" 10 | repository = "https://github.com/rustwasm/gloo" 11 | description = "HTTP requests library for WASM Apps" 12 | readme = "README.md" 13 | keywords = ["requests", "http", "wasm", "websockets"] 14 | categories = ["wasm", "web-programming::http-client", "api-bindings"] 15 | rust-version = "1.64" 16 | 17 | [package.metadata.docs.rs] 18 | all-features = true 19 | rustdoc-args = ["--cfg", "docsrs"] 20 | 21 | [dependencies] 22 | wasm-bindgen = "0.2" 23 | web-sys = "0.3" 24 | js-sys = "0.3" 25 | gloo-utils = { version = "0.2", path = "../utils", default-features = false } 26 | 27 | wasm-bindgen-futures = "0.4" 28 | futures-core = { version = "0.3", optional = true } 29 | futures-sink = { version = "0.3", optional = true } 30 | futures-io = { version = "0.3", optional = true } 31 | 32 | thiserror = "1.0" 33 | 34 | serde = { version = "1.0", optional = true } 35 | serde_json = { version = "1.0", optional = true } 36 | 37 | futures-channel = { version = "0.3", optional = true } 38 | pin-project = { version = "1.0", optional = true } 39 | http = "1.0" 40 | 41 | [dev-dependencies] 42 | wasm-bindgen-test = "0.3" 43 | futures = "0.3" 44 | serde = { version = "1.0", features = ["derive"] } 45 | 46 | once_cell = "1" 47 | 48 | [features] 49 | default = ["json", "websocket", "http", "eventsource"] 50 | 51 | # Enables `.json()` on `Response` 52 | json = ["serde", "serde_json", "gloo-utils/serde"] 53 | # Enables the WebSocket API 54 | websocket = [ 55 | 'web-sys/WebSocket', 56 | 'web-sys/AddEventListenerOptions', 57 | 'web-sys/ErrorEvent', 58 | 'web-sys/FileReader', 59 | 'web-sys/MessageEvent', 60 | 'web-sys/ProgressEvent', 61 | 'web-sys/CloseEvent', 62 | 'web-sys/CloseEventInit', 63 | 'web-sys/BinaryType', 64 | 'web-sys/Blob', 65 | "futures-channel", 66 | "futures-core", 67 | "futures-sink", 68 | "pin-project", 69 | ] 70 | # Enables the HTTP API 71 | http = [ 72 | 'web-sys/Headers', 73 | 'web-sys/UrlSearchParams', 74 | 'web-sys/Url', 75 | 'web-sys/Request', 76 | 'web-sys/RequestInit', 77 | 'web-sys/RequestMode', 78 | 'web-sys/Response', 79 | 'web-sys/ResponseInit', 80 | 'web-sys/ResponseType', 81 | 'web-sys/RequestCache', 82 | 'web-sys/RequestCredentials', 83 | 'web-sys/ObserverCallback', 84 | 'web-sys/RequestRedirect', 85 | 'web-sys/ReferrerPolicy', 86 | 'web-sys/AbortSignal', 87 | 'web-sys/ReadableStream', 88 | 'web-sys/Blob', 89 | 'web-sys/FormData', 90 | ] 91 | # Enables the EventSource API 92 | eventsource = [ 93 | "futures-channel", 94 | "futures-core", 95 | "pin-project", 96 | 'web-sys/Event', 97 | 'web-sys/EventTarget', 98 | 'web-sys/EventSource', 99 | 'web-sys/MessageEvent', 100 | ] 101 | # As of now, only implements `AsyncRead` and `AsyncWrite` on `WebSocket` 102 | io-util = ["futures-io"] 103 | # For test runner only. Enables browser tests. 104 | browser-test = [] 105 | -------------------------------------------------------------------------------- /crates/net/README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

gloo-net

4 | 5 |

6 | Crates.io version 7 | Download 8 | docs.rs docs 9 |

10 | 11 |

12 | API Docs 13 | | 14 | Contributing 15 | | 16 | Chat 17 |

18 | 19 | Built with 🦀🕸 by The Rust and WebAssembly Working Group 20 |
21 | 22 | HTTP requests library for WASM Apps. It provides idiomatic Rust bindings for the `web_sys` [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API), [`WebSocket`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) and [`EventSource`](https://developer.mozilla.org/en-US/docs/Web/API/EventSource) APIs. 23 | 24 | ## Examples 25 | 26 | ### HTTP 27 | 28 | ```rust 29 | let resp = Request::get("/path") 30 | .send() 31 | .await 32 | .unwrap(); 33 | assert_eq!(resp.status(), 200); 34 | ``` 35 | 36 | ### WebSocket 37 | 38 | ```rust 39 | use gloo_net::websocket::{Message, futures::WebSocket}; 40 | use wasm_bindgen_futures::spawn_local; 41 | use futures::{SinkExt, StreamExt}; 42 | 43 | let mut ws = WebSocket::open("wss://echo.websocket.org").unwrap(); 44 | let (mut write, mut read) = ws.split(); 45 | 46 | spawn_local(async move { 47 | write.send(Message::Text(String::from("test"))).await.unwrap(); 48 | write.send(Message::Text(String::from("test 2"))).await.unwrap(); 49 | }); 50 | 51 | spawn_local(async move { 52 | while let Some(msg) = read.next().await { 53 | console_log!(format!("1. {:?}", msg)) 54 | } 55 | console_log!("WebSocket Closed") 56 | }) 57 | ``` 58 | 59 | ### EventSource 60 | 61 | ```rust 62 | use gloo_net::eventsource::futures::EventSource; 63 | use wasm_bindgen_futures::spawn_local; 64 | use futures::{stream, StreamExt}; 65 | 66 | let mut es = EventSource::new("http://api.example.com/ssedemo.php").unwrap(); 67 | let stream_1 = es.subscribe("some-event-type").unwrap(); 68 | let stream_2 = es.subscribe("another-event-type").unwrap(); 69 | 70 | spawn_local(async move { 71 | let mut all_streams = stream::select(stream_1, stream_2); 72 | while let Some(Ok((event_type, msg))) = all_streams.next().await { 73 | console_log!(format!("1. {}: {:?}", event_type, msg)) 74 | } 75 | console_log!("EventSource Closed"); 76 | }) 77 | ``` 78 | -------------------------------------------------------------------------------- /crates/net/src/error.rs: -------------------------------------------------------------------------------- 1 | use gloo_utils::errors::JsError; 2 | use thiserror::Error as ThisError; 3 | 4 | /// All the errors returned by this crate. 5 | #[derive(Debug, ThisError)] 6 | pub enum Error { 7 | /// Error returned by JavaScript. 8 | #[error("{0}")] 9 | JsError(JsError), 10 | /// Error returned by `serde` during deserialization. 11 | #[cfg(feature = "json")] 12 | #[cfg_attr(docsrs, doc(cfg(feature = "json")))] 13 | #[error("{0}")] 14 | SerdeError( 15 | #[source] 16 | #[from] 17 | serde_json::Error, 18 | ), 19 | /// Error returned by this crate 20 | #[error("{0}")] 21 | GlooError(String), 22 | } 23 | 24 | #[cfg(any(feature = "http", feature = "websocket", feature = "eventsource"))] 25 | pub(crate) use conversion::*; 26 | #[cfg(any(feature = "http", feature = "websocket", feature = "eventsource"))] 27 | mod conversion { 28 | use gloo_utils::errors::JsError; 29 | use std::convert::TryFrom; 30 | use wasm_bindgen::JsValue; 31 | 32 | #[cfg(feature = "http")] 33 | pub(crate) fn js_to_error(js_value: JsValue) -> super::Error { 34 | super::Error::JsError(js_to_js_error(js_value)) 35 | } 36 | 37 | pub(crate) fn js_to_js_error(js_value: JsValue) -> JsError { 38 | match JsError::try_from(js_value) { 39 | Ok(error) => error, 40 | Err(_) => unreachable!("JsValue passed is not an Error type -- this is a bug"), 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /crates/net/src/eventsource/mod.rs: -------------------------------------------------------------------------------- 1 | //! Wrapper around the `EventSource` API 2 | //! 3 | //! This API is provided in the following flavors: 4 | //! - [Futures API][futures] 5 | 6 | pub mod futures; 7 | 8 | use std::fmt; 9 | 10 | /// The state of the EventSource. 11 | /// 12 | /// See [`EventSource.readyState` on MDN](https://developer.mozilla.org/en-US/docs/Web/API/EventSource/readyState) 13 | /// to learn more. 14 | #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] 15 | pub enum State { 16 | /// The connection has not yet been established. 17 | Connecting, 18 | /// The EventSource connection is established and communication is possible. 19 | Open, 20 | /// The connection has been closed or could not be opened. 21 | Closed, 22 | } 23 | 24 | /// Error returned by the EventSource 25 | #[derive(Clone, Debug, Eq, PartialEq)] 26 | #[non_exhaustive] 27 | #[allow(missing_copy_implementations)] 28 | pub enum EventSourceError { 29 | /// The `error` event 30 | ConnectionError, 31 | } 32 | 33 | impl fmt::Display for EventSourceError { 34 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 35 | match self { 36 | EventSourceError::ConnectionError => write!(f, "EventSource connection failed"), 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /crates/net/src/http/mod.rs: -------------------------------------------------------------------------------- 1 | //! Wrapper around the `fetch` API. 2 | //! 3 | //! # Example 4 | //! 5 | //! ``` 6 | //! # use gloo_net::http::Request; 7 | //! # async fn no_run() { 8 | //! let resp = Request::get("/path") 9 | //! .send() 10 | //! .await 11 | //! .unwrap(); 12 | //! assert_eq!(resp.status(), 200); 13 | //! # } 14 | //! ``` 15 | 16 | mod headers; 17 | mod query; 18 | mod request; 19 | mod response; 20 | 21 | pub use headers::Headers; 22 | #[doc(inline)] 23 | pub use http::Method; 24 | pub use query::QueryParams; 25 | 26 | pub use request::{Request, RequestBuilder}; 27 | pub use response::{IntoRawResponse, Response, ResponseBuilder}; 28 | -------------------------------------------------------------------------------- /crates/net/src/http/query.rs: -------------------------------------------------------------------------------- 1 | use gloo_utils::iter::UncheckedIter; 2 | use js_sys::{Array, Map}; 3 | use std::fmt; 4 | use wasm_bindgen::{JsCast, UnwrapThrowExt}; 5 | 6 | /// A sequence of URL query parameters, wrapping [`web_sys::UrlSearchParams`]. 7 | pub struct QueryParams { 8 | raw: web_sys::UrlSearchParams, 9 | } 10 | 11 | impl Default for QueryParams { 12 | fn default() -> Self { 13 | Self::new() 14 | } 15 | } 16 | 17 | #[allow(dead_code)] 18 | impl QueryParams { 19 | /// Create a new empty query parameters object. 20 | pub fn new() -> Self { 21 | // pretty sure this will never throw. 22 | Self { 23 | raw: web_sys::UrlSearchParams::new().unwrap_throw(), 24 | } 25 | } 26 | 27 | /// Create [`QueryParams`] from [`web_sys::UrlSearchParams`] object. 28 | pub fn from_raw(raw: web_sys::UrlSearchParams) -> Self { 29 | Self { raw } 30 | } 31 | 32 | /// Append a parameter to the query string. 33 | pub fn append(&self, name: &str, value: &str) { 34 | self.raw.append(name, value) 35 | } 36 | 37 | /// Get the value of a parameter. If the parameter has multiple occurrences, the first value is 38 | /// returned. 39 | pub fn get(&self, name: &str) -> Option { 40 | self.raw.get(name) 41 | } 42 | 43 | /// Get all associated values of a parameter. 44 | pub fn get_all(&self, name: &str) -> Vec { 45 | self.raw 46 | .get_all(name) 47 | .iter() 48 | .map(|jsval| jsval.as_string().unwrap_throw()) 49 | .collect() 50 | } 51 | 52 | /// Remove all occurrences of a parameter from the query string. 53 | pub fn delete(&self, name: &str) { 54 | self.raw.delete(name) 55 | } 56 | 57 | /// Iterate over (name, value) pairs of the query parameters. 58 | pub fn iter(&self) -> impl Iterator { 59 | // Here we cheat and cast to a map even though `self` isn't, because the method names match 60 | // and everything works. Is there a better way? Should there be a `MapLike` or 61 | // `MapIterator` type in `js_sys`? 62 | let fake_map: &Map = self.raw.unchecked_ref(); 63 | UncheckedIter::from(fake_map.entries()).map(|entry| { 64 | let entry: Array = entry.unchecked_into(); 65 | let key = entry.get(0); 66 | let value = entry.get(1); 67 | ( 68 | key.as_string().unwrap_throw(), 69 | value.as_string().unwrap_throw(), 70 | ) 71 | }) 72 | } 73 | } 74 | 75 | /// The formatted query parameters ready to be used in a URL query string. 76 | /// 77 | /// # Examples 78 | /// 79 | /// The resulting string does not contain a leading `?` and is properly encoded: 80 | /// 81 | /// ``` 82 | /// # fn no_run() { 83 | /// use gloo_net::http::QueryParams; 84 | /// 85 | /// let params = QueryParams::new(); 86 | /// params.append("a", "1"); 87 | /// params.append("b", "2"); 88 | /// assert_eq!(params.to_string(), "a=1&b=2".to_string()); 89 | /// 90 | /// params.append("key", "ab&c"); 91 | /// assert_eq!(params.to_string(), "a=1&b=2&key=ab%26c"); 92 | /// # } 93 | /// ``` 94 | impl fmt::Display for QueryParams { 95 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 96 | write!(f, "{}", self.raw.to_string()) 97 | } 98 | } 99 | 100 | impl fmt::Debug for QueryParams { 101 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 102 | f.debug_list().entries(self.iter()).finish() 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /crates/net/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! HTTP requests library for WASM apps. It provides idiomatic Rust bindings for the `web_sys` 2 | //! `fetch` and `WebSocket` API. 3 | //! 4 | //! See module level documentation for [`http`] and [`websocket`] to learn more. 5 | 6 | #![deny( 7 | missing_docs, 8 | missing_debug_implementations, 9 | missing_copy_implementations 10 | )] 11 | #![cfg_attr(docsrs, feature(doc_cfg))] 12 | 13 | mod error; 14 | #[cfg(feature = "eventsource")] 15 | #[cfg_attr(docsrs, doc(cfg(feature = "eventsource")))] 16 | pub mod eventsource; 17 | #[cfg(feature = "http")] 18 | #[cfg_attr(docsrs, doc(cfg(feature = "http")))] 19 | pub mod http; 20 | #[cfg(feature = "websocket")] 21 | #[cfg_attr(docsrs, doc(cfg(feature = "websocket")))] 22 | pub mod websocket; 23 | 24 | pub use error::*; 25 | -------------------------------------------------------------------------------- /crates/net/src/websocket/events.rs: -------------------------------------------------------------------------------- 1 | //! WebSocket Events 2 | 3 | /// Data emitted by `onclose` event 4 | #[derive(Clone, Debug)] 5 | pub struct CloseEvent { 6 | /// Close code 7 | pub code: u16, 8 | /// Close reason 9 | pub reason: String, 10 | /// If the websockets was closed cleanly 11 | pub was_clean: bool, 12 | } 13 | -------------------------------------------------------------------------------- /crates/net/src/websocket/mod.rs: -------------------------------------------------------------------------------- 1 | //! Wrapper around `WebSocket` API 2 | //! 3 | //! This API is provided in the following flavors: 4 | //! - [Futures API][futures] 5 | 6 | pub mod events; 7 | pub mod futures; 8 | 9 | #[cfg(feature = "io-util")] 10 | mod io_util; 11 | 12 | use events::CloseEvent; 13 | use gloo_utils::errors::JsError; 14 | use std::fmt; 15 | 16 | /// Message sent to and received from WebSocket. 17 | #[derive(Debug, PartialEq, Eq, Clone)] 18 | pub enum Message { 19 | /// String message 20 | Text(String), 21 | /// ArrayBuffer parsed into bytes 22 | Bytes(Vec), 23 | } 24 | 25 | /// The state of the websocket. 26 | /// 27 | /// See [`WebSocket.readyState` on MDN](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState) 28 | /// to learn more. 29 | #[derive(Copy, Clone, Debug)] 30 | pub enum State { 31 | /// The connection has not yet been established. 32 | Connecting, 33 | /// The WebSocket connection is established and communication is possible. 34 | Open, 35 | /// The connection is going through the closing handshake, or the close() method has been 36 | /// invoked. 37 | Closing, 38 | /// The connection has been closed or could not be opened. 39 | Closed, 40 | } 41 | 42 | /// Error returned by WebSocket 43 | #[derive(Debug)] 44 | #[non_exhaustive] 45 | pub enum WebSocketError { 46 | /// The `error` event 47 | ConnectionError, 48 | /// The `close` event 49 | ConnectionClose(CloseEvent), 50 | /// Message failed to send. 51 | MessageSendError(JsError), 52 | } 53 | 54 | impl fmt::Display for WebSocketError { 55 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 56 | match self { 57 | WebSocketError::ConnectionError => write!(f, "WebSocket connection failed"), 58 | WebSocketError::ConnectionClose(e) => write!( 59 | f, 60 | "WebSocket Closed: code: {}, reason: {}", 61 | e.code, e.reason 62 | ), 63 | WebSocketError::MessageSendError(e) => write!(f, "{e}"), 64 | } 65 | } 66 | } 67 | 68 | impl std::error::Error for WebSocketError {} 69 | -------------------------------------------------------------------------------- /crates/net/tests/query.rs: -------------------------------------------------------------------------------- 1 | use gloo_net::http::QueryParams; 2 | use wasm_bindgen_test::*; 3 | 4 | #[cfg(feature = "browser_test")] 5 | wasm_bindgen_test_configure!(run_in_browser); 6 | 7 | #[wasm_bindgen_test] 8 | fn query_params_iter() { 9 | let params = QueryParams::new(); 10 | params.append("a", "1"); 11 | params.append("b", "value"); 12 | let mut entries = params.iter(); 13 | assert_eq!(entries.next(), Some(("a".into(), "1".into()))); 14 | assert_eq!(entries.next(), Some(("b".into(), "value".into()))); 15 | assert_eq!(entries.next(), None); 16 | } 17 | 18 | #[wasm_bindgen_test] 19 | fn query_params_get() { 20 | let params = QueryParams::new(); 21 | params.append("a", "1"); 22 | params.append("a", "value"); 23 | assert_eq!(params.get("a"), Some("1".to_string())); 24 | assert!(params.get("b").is_none()); 25 | assert_eq!( 26 | params.get_all("a"), 27 | vec!["1".to_string(), "value".to_string()] 28 | ); 29 | } 30 | 31 | #[wasm_bindgen_test] 32 | fn query_params_delete() { 33 | let params = QueryParams::new(); 34 | params.append("a", "1"); 35 | params.append("a", "value"); 36 | params.delete("a"); 37 | assert!(params.get("a").is_none()); 38 | } 39 | 40 | #[wasm_bindgen_test] 41 | fn query_params_escape() { 42 | let params = QueryParams::new(); 43 | params.append("a", "1"); 44 | assert_eq!(params.to_string(), "a=1".to_string()); 45 | 46 | params.append("key", "ab&c"); 47 | assert_eq!(params.to_string(), "a=1&key=ab%26c"); 48 | } 49 | -------------------------------------------------------------------------------- /crates/render/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gloo-render" 3 | description = "Convenience crate for working with browser's requestAnimationFrame" 4 | version = "0.2.0" 5 | authors = ["Rust and WebAssembly Working Group"] 6 | edition = "2021" 7 | license = "MIT OR Apache-2.0" 8 | readme = "README.md" 9 | repository = "https://github.com/rustwasm/gloo/tree/master/crates/storage" 10 | homepage = "https://github.com/rustwasm/gloo" 11 | categories = ["api-bindings", "storage", "wasm"] 12 | rust-version = "1.64" 13 | 14 | [dependencies] 15 | wasm-bindgen = "0.2" 16 | 17 | [dependencies.web-sys] 18 | version = "0.3" 19 | features = ["Window"] 20 | -------------------------------------------------------------------------------- /crates/render/README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

gloo-render

4 | 5 |

6 | Crates.io version 7 | Download 8 | docs.rs docs 9 |

10 | 11 |

12 | API Docs 13 | | 14 | Contributing 15 | | 16 | Chat 17 |

18 | 19 | Built with 🦀🕸 by The Rust and WebAssembly Working Group 20 |
21 | 22 | Crate that provides wrapper for 23 | [requestAnimationFrame](https://developer.mozilla.org/en-US/docs/Web/API/Window/requestAnimationFrame) 24 | -------------------------------------------------------------------------------- /crates/render/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Crate that provides wrapper for 2 | //! [requestAnimationFrame](https://developer.mozilla.org/en-US/docs/Web/API/Window/requestAnimationFrame) 3 | 4 | #![deny(missing_docs, missing_debug_implementations)] 5 | 6 | use std::cell::RefCell; 7 | use std::fmt; 8 | use std::rc::Rc; 9 | use wasm_bindgen::prelude::*; 10 | use wasm_bindgen::JsCast; 11 | 12 | /// Handle for [`request_animation_frame`]. 13 | #[derive(Debug)] 14 | pub struct AnimationFrame { 15 | render_id: i32, 16 | _closure: Closure, 17 | callback_wrapper: Rc>>, 18 | } 19 | 20 | struct CallbackWrapper(Box); 21 | impl fmt::Debug for CallbackWrapper { 22 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 23 | f.write_str("CallbackWrapper") 24 | } 25 | } 26 | 27 | impl Drop for AnimationFrame { 28 | fn drop(&mut self) { 29 | if self.callback_wrapper.borrow_mut().is_some() { 30 | web_sys::window() 31 | .unwrap_throw() 32 | .cancel_animation_frame(self.render_id) 33 | .unwrap_throw() 34 | } 35 | } 36 | } 37 | 38 | /// Calls browser's `requestAnimationFrame`. It is cancelled when the handler is dropped. 39 | /// 40 | /// [MDN Documentation](https://developer.mozilla.org/en-US/docs/Web/API/Window/requestAnimationFrame) 41 | pub fn request_animation_frame(callback_once: F) -> AnimationFrame 42 | where 43 | F: FnOnce(f64) + 'static, 44 | { 45 | let callback_wrapper = Rc::new(RefCell::new(Some(CallbackWrapper(Box::new(callback_once))))); 46 | let callback: Closure = { 47 | let callback_wrapper = Rc::clone(&callback_wrapper); 48 | Closure::wrap(Box::new(move |v: JsValue| { 49 | let time: f64 = v.as_f64().unwrap_or(0.0); 50 | let callback = callback_wrapper.borrow_mut().take().unwrap().0; 51 | callback(time); 52 | })) 53 | }; 54 | 55 | let render_id = web_sys::window() 56 | .unwrap_throw() 57 | .request_animation_frame(callback.as_ref().unchecked_ref()) 58 | .unwrap_throw(); 59 | 60 | AnimationFrame { 61 | render_id, 62 | _closure: callback, 63 | callback_wrapper, 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /crates/storage/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gloo-storage" 3 | description = "Convenience crate for working with local and session storage in browser" 4 | version = "0.3.0" 5 | authors = ["Rust and WebAssembly Working Group"] 6 | edition = "2021" 7 | license = "MIT OR Apache-2.0" 8 | readme = "README.md" 9 | repository = "https://github.com/rustwasm/gloo/tree/master/crates/storage" 10 | homepage = "https://github.com/rustwasm/gloo" 11 | categories = ["api-bindings", "storage", "wasm"] 12 | rust-version = "1.64" 13 | 14 | [dependencies] 15 | wasm-bindgen = "0.2" 16 | serde = "1.0" 17 | serde_json = "1.0" 18 | thiserror = "1.0" 19 | js-sys = "0.3" 20 | gloo-utils = { version = "0.2", path = "../utils" } 21 | [dependencies.web-sys] 22 | version = "0.3" 23 | features = ["Storage", "Window"] 24 | 25 | [dev-dependencies] 26 | wasm-bindgen-test = "0.3" 27 | serde = { version = "1.0", features = ["derive"] } 28 | -------------------------------------------------------------------------------- /crates/storage/README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

gloo-storage

4 | 5 |

6 | Crates.io version 7 | Download 8 | docs.rs docs 9 |

10 | 11 |

12 | API Docs 13 | | 14 | Contributing 15 | | 16 | Chat 17 |

18 | 19 | Built with 🦀🕸 by The Rust and WebAssembly Working Group 20 |
21 | 22 | This crate provides wrappers for the 23 | [Web Storage API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API) 24 | 25 | The data is stored in JSON form. We use [`serde`](https://serde.rs) for 26 | serialization and deserialization. 27 | -------------------------------------------------------------------------------- /crates/storage/src/errors.rs: -------------------------------------------------------------------------------- 1 | //! All the errors. 2 | 3 | use gloo_utils::errors::JsError; 4 | use wasm_bindgen::{JsCast, JsValue}; 5 | 6 | /// Error returned by this crate 7 | #[derive(Debug, thiserror::Error)] 8 | pub enum StorageError { 9 | /// Error from `serde` 10 | #[error("{0}")] 11 | SerdeError(#[from] serde_json::Error), 12 | /// Error if the requested key is not found 13 | #[error("key {0} not found")] 14 | KeyNotFound(String), 15 | /// Error returned from JavaScript 16 | #[error("{0}")] 17 | JsError(JsError), 18 | } 19 | 20 | pub(crate) fn js_to_error(js_value: JsValue) -> StorageError { 21 | match js_value.dyn_into::() { 22 | Ok(error) => StorageError::JsError(JsError::from(error)), 23 | Err(_) => unreachable!("JsValue passed is not an Error type - this is a bug"), 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /crates/storage/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This crate provides wrappers for the 2 | //! [Web Storage API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API) 3 | //! 4 | //! The data is stored in JSON form. We use [`serde`](https://serde.rs) for 5 | //! serialization and deserialization. 6 | 7 | #![deny(missing_docs, missing_debug_implementations)] 8 | 9 | use serde::{Deserialize, Serialize}; 10 | use wasm_bindgen::prelude::*; 11 | 12 | use crate::errors::js_to_error; 13 | use errors::StorageError; 14 | use serde_json::{Map, Value}; 15 | 16 | pub mod errors; 17 | mod local_storage; 18 | mod session_storage; 19 | pub use local_storage::LocalStorage; 20 | pub use session_storage::SessionStorage; 21 | 22 | /// `gloo-storage`'s `Result` 23 | pub type Result = std::result::Result; 24 | 25 | /// Trait which provides implementations for managing storage in the browser. 26 | pub trait Storage { 27 | /// Get the raw [`web_sys::Storage`] instance 28 | fn raw() -> web_sys::Storage; 29 | 30 | /// Get the value for the specified key 31 | fn get(key: impl AsRef) -> Result 32 | where 33 | T: for<'de> Deserialize<'de>, 34 | { 35 | let key = key.as_ref(); 36 | let item = Self::raw() 37 | .get_item(key) 38 | .expect_throw("unreachable: get_item does not throw an exception") 39 | .ok_or_else(|| StorageError::KeyNotFound(key.to_string()))?; 40 | let item = serde_json::from_str(&item)?; 41 | Ok(item) 42 | } 43 | 44 | /// Get all the stored keys and their values 45 | fn get_all() -> Result 46 | where 47 | T: for<'a> Deserialize<'a>, 48 | { 49 | let local_storage = Self::raw(); 50 | let length = Self::length(); 51 | let mut map = Map::with_capacity(length as usize); 52 | for index in 0..length { 53 | let key = local_storage 54 | .key(index) 55 | .map_err(js_to_error)? 56 | .unwrap_throw(); 57 | let value: Value = Self::get(&key)?; 58 | map.insert(key, value); 59 | } 60 | Ok(serde_json::from_value(Value::Object(map))?) 61 | } 62 | 63 | /// Insert a value for the specified key 64 | fn set(key: impl AsRef, value: T) -> Result<()> 65 | where 66 | T: Serialize, 67 | { 68 | let key = key.as_ref(); 69 | let value = serde_json::to_string(&value)?; 70 | Self::raw() 71 | .set_item(key, &value) 72 | .map_err(errors::js_to_error)?; 73 | Ok(()) 74 | } 75 | 76 | /// Remove a key and it's stored value 77 | fn delete(key: impl AsRef) { 78 | let key = key.as_ref(); 79 | Self::raw() 80 | .remove_item(key) 81 | .expect_throw("unreachable: remove_item does not throw an exception"); 82 | } 83 | 84 | /// Remove all the stored data 85 | fn clear() { 86 | Self::raw() 87 | .clear() 88 | .expect_throw("unreachable: clear does not throw an exception"); 89 | } 90 | 91 | /// Get the number of items stored 92 | fn length() -> u32 { 93 | Self::raw() 94 | .length() 95 | .expect_throw("unreachable: length does not throw an exception") 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /crates/storage/src/local_storage.rs: -------------------------------------------------------------------------------- 1 | use wasm_bindgen::UnwrapThrowExt; 2 | 3 | use crate::Storage; 4 | 5 | /// Provides API to deal with `localStorage` 6 | #[derive(Debug)] 7 | pub struct LocalStorage; 8 | 9 | impl Storage for LocalStorage { 10 | fn raw() -> web_sys::Storage { 11 | web_sys::window() 12 | .expect_throw("no window") 13 | .local_storage() 14 | .expect_throw("failed to get local_storage") 15 | .expect_throw("no local storage") 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /crates/storage/src/session_storage.rs: -------------------------------------------------------------------------------- 1 | use wasm_bindgen::UnwrapThrowExt; 2 | 3 | use crate::Storage; 4 | 5 | /// Provides API to deal with `sessionStorage` 6 | #[derive(Debug)] 7 | pub struct SessionStorage; 8 | 9 | impl Storage for SessionStorage { 10 | fn raw() -> web_sys::Storage { 11 | web_sys::window() 12 | .expect_throw("no window") 13 | .session_storage() 14 | .expect_throw("failed to get session_storage") 15 | .expect_throw("no session storage") 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /crates/storage/tests/local_storage.rs: -------------------------------------------------------------------------------- 1 | use gloo_storage::{LocalStorage, Storage}; 2 | use serde::Deserialize; 3 | use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; 4 | 5 | wasm_bindgen_test_configure!(run_in_browser); 6 | 7 | #[test] 8 | fn get() { 9 | let key = "key"; 10 | let value = "value"; 11 | LocalStorage::set(key, value).unwrap(); 12 | 13 | let obtained_value: String = LocalStorage::get(key).unwrap(); 14 | 15 | assert_eq!(value, obtained_value) 16 | } 17 | 18 | #[derive(Deserialize)] 19 | struct Data { 20 | key1: String, 21 | key2: String, 22 | } 23 | 24 | #[test] 25 | fn get_all() { 26 | LocalStorage::set("key1", "value").unwrap(); 27 | LocalStorage::set("key2", "value").unwrap(); 28 | 29 | let data: Data = LocalStorage::get_all().unwrap(); 30 | assert_eq!(data.key1, "value"); 31 | assert_eq!(data.key2, "value"); 32 | } 33 | 34 | #[test] 35 | fn set_and_length() { 36 | LocalStorage::clear(); 37 | assert_eq!(LocalStorage::length(), 0); 38 | LocalStorage::set("key", "value").unwrap(); 39 | assert_eq!(LocalStorage::length(), 1); 40 | LocalStorage::clear(); 41 | assert_eq!(LocalStorage::length(), 0); 42 | } 43 | -------------------------------------------------------------------------------- /crates/storage/tests/session_storage.rs: -------------------------------------------------------------------------------- 1 | use gloo_storage::{SessionStorage, Storage}; 2 | use serde::Deserialize; 3 | use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; 4 | 5 | wasm_bindgen_test_configure!(run_in_browser); 6 | 7 | #[test] 8 | fn get() { 9 | let key = "key"; 10 | let value = "value"; 11 | SessionStorage::set(key, value).unwrap(); 12 | 13 | let obtained_value: String = SessionStorage::get(key).unwrap(); 14 | 15 | assert_eq!(value, obtained_value) 16 | } 17 | 18 | #[derive(Deserialize)] 19 | struct Data { 20 | key1: String, 21 | key2: String, 22 | } 23 | 24 | #[test] 25 | fn get_all() { 26 | SessionStorage::set("key1", "value").unwrap(); 27 | SessionStorage::set("key2", "value").unwrap(); 28 | 29 | let data: Data = SessionStorage::get_all().unwrap(); 30 | assert_eq!(data.key1, "value"); 31 | assert_eq!(data.key2, "value"); 32 | } 33 | 34 | #[test] 35 | fn set_and_length() { 36 | SessionStorage::clear(); 37 | assert_eq!(SessionStorage::length(), 0); 38 | SessionStorage::set("key", "value").unwrap(); 39 | assert_eq!(SessionStorage::length(), 1); 40 | SessionStorage::clear(); 41 | assert_eq!(SessionStorage::length(), 0); 42 | } 43 | -------------------------------------------------------------------------------- /crates/timers/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gloo-timers" 3 | description = "Convenience crate for working with JavaScript timers" 4 | version = "0.3.0" 5 | authors = ["Rust and WebAssembly Working Group"] 6 | edition = "2021" 7 | license = "MIT OR Apache-2.0" 8 | readme = "README.md" 9 | repository = "https://github.com/rustwasm/gloo/tree/master/crates/timers" 10 | homepage = "https://github.com/rustwasm/gloo" 11 | categories = ["api-bindings", "asynchronous", "wasm"] 12 | rust-version = "1.64" 13 | 14 | [package.metadata.docs.rs] 15 | features = ["futures"] 16 | 17 | [dependencies] 18 | wasm-bindgen = "0.2" 19 | js-sys = "0.3.31" 20 | futures-core = { version = "0.3", optional = true } 21 | futures-channel = { version = "0.3", optional = true } 22 | 23 | [features] 24 | default = [] 25 | futures = ["futures-core", "futures-channel"] 26 | 27 | 28 | [dev-dependencies] 29 | wasm-bindgen-futures = "0.4.4" 30 | wasm-bindgen-test = "0.3.4" 31 | futures-util = "0.3" 32 | -------------------------------------------------------------------------------- /crates/timers/README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

gloo-timers

4 | 5 |

6 | Crates.io version 7 | Download 8 | docs.rs docs 9 |

10 | 11 |

12 | API Docs 13 | | 14 | Contributing 15 | | 16 | Chat 17 |

18 | 19 | Built with 🦀🕸 by The Rust and WebAssembly Working Group 20 |
21 | 22 | 23 | Working with timers on the Web: `setTimeout` and `setInterval`. 24 | 25 | These APIs come in two flavors: 26 | 27 | 1. a callback style (that more directly mimics the JavaScript APIs), and 28 | 2. a `Future`s and `Stream`s API. 29 | 30 | ### Timeouts 31 | 32 | Timeouts fire once after a period of time (measured in milliseconds). 33 | 34 | #### Timeouts with a Callback Function 35 | 36 | ```rust 37 | use gloo_timers::callback::Timeout; 38 | 39 | let timeout = Timeout::new(1_000, move || { 40 | // Do something after the one second timeout is up! 41 | }); 42 | 43 | // Since we don't plan on cancelling the timeout, call `forget`. 44 | timeout.forget(); 45 | ``` 46 | 47 | #### Timeouts as `Future`s 48 | 49 | With the `futures` feature enabled, a `future` module containing futures-based 50 | timers is exposed. 51 | 52 | -------------------------------------------------------------------------------- /crates/timers/src/lib.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | 3 | Working with timers on the Web: `setTimeout` and `setInterval`. 4 | 5 | These APIs come in two flavors: 6 | 7 | 1. a callback style (that more directly mimics the JavaScript APIs), and 8 | 2. a `Future`s and `Stream`s API. 9 | 10 | ## Timeouts 11 | 12 | Timeouts fire once after a period of time (measured in milliseconds). 13 | 14 | ### Timeouts with a Callback Function 15 | 16 | ```no_run 17 | use gloo_timers::callback::Timeout; 18 | 19 | let timeout = Timeout::new(1_000, move || { 20 | // Do something after the one second timeout is up! 21 | }); 22 | 23 | // Since we don't plan on cancelling the timeout, call `forget`. 24 | timeout.forget(); 25 | ``` 26 | 27 | ### Timeouts as `Future`s 28 | 29 | With the `futures` feature enabled, a `future` module containing futures-based 30 | timers is exposed. 31 | 32 | */ 33 | #![cfg_attr(feature = "futures", doc = "```no_run")] 34 | #![cfg_attr(not(feature = "futures"), doc = "```ignore")] 35 | /*! 36 | use gloo_timers::future::TimeoutFuture; 37 | use wasm_bindgen_futures::spawn_local; 38 | 39 | // Spawn the `timeout` future on the local thread. If we just dropped it, then 40 | // the timeout would be cancelled with `clearTimeout`. 41 | spawn_local(async { 42 | TimeoutFuture::new(1_000).await; 43 | // Do something here after the one second timeout is up! 44 | }); 45 | ``` 46 | 47 | ## Intervals 48 | 49 | Intervals fire repeatedly every *n* milliseconds. 50 | 51 | ### Intervals with a Callback Function 52 | 53 | TODO 54 | 55 | ### Intervals as `Stream`s 56 | 57 | TODO 58 | 59 | */ 60 | 61 | #![deny(missing_docs, missing_debug_implementations)] 62 | 63 | pub mod callback; 64 | 65 | #[cfg(feature = "futures")] 66 | pub mod future; 67 | -------------------------------------------------------------------------------- /crates/timers/tests/node.rs: -------------------------------------------------------------------------------- 1 | #![cfg(all(target_family = "wasm", feature = "futures"))] 2 | 3 | use futures_channel::{mpsc, oneshot}; 4 | use futures_util::{ 5 | future::{select, Either, FutureExt}, 6 | stream::StreamExt, 7 | }; 8 | use gloo_timers::{ 9 | callback::{Interval, Timeout}, 10 | future::{sleep, IntervalStream, TimeoutFuture}, 11 | }; 12 | use std::cell::Cell; 13 | use std::rc::Rc; 14 | use std::time::Duration; 15 | use wasm_bindgen_test::*; 16 | 17 | #[wasm_bindgen_test] 18 | async fn timeout() { 19 | let (sender, receiver) = oneshot::channel(); 20 | Timeout::new(1, || sender.send(()).unwrap()).forget(); 21 | receiver.await.unwrap(); 22 | } 23 | 24 | #[wasm_bindgen_test] 25 | async fn timeout_cancel() { 26 | let cell = Rc::new(Cell::new(false)); 27 | 28 | let t = Timeout::new(1, { 29 | let cell = cell.clone(); 30 | move || { 31 | cell.set(true); 32 | panic!("should have been cancelled"); 33 | } 34 | }); 35 | t.cancel(); 36 | 37 | let (sender, receiver) = oneshot::channel(); 38 | 39 | Timeout::new(2, move || { 40 | sender.send(()).unwrap(); 41 | assert_eq!(cell.get(), false); 42 | }) 43 | .forget(); 44 | 45 | receiver.await.unwrap(); 46 | } 47 | 48 | #[wasm_bindgen_test] 49 | async fn timeout_future() { 50 | TimeoutFuture::new(1).await; 51 | } 52 | 53 | #[wasm_bindgen_test] 54 | async fn timeout_future_cancel() { 55 | let cell = Rc::new(Cell::new(false)); 56 | 57 | let a = TimeoutFuture::new(1).map({ 58 | let cell = cell.clone(); 59 | move |_| { 60 | assert_eq!(cell.get(), false); 61 | 1 62 | } 63 | }); 64 | 65 | let b = TimeoutFuture::new(2).map({ 66 | let cell = cell.clone(); 67 | move |_| { 68 | cell.set(true); 69 | 2u32 70 | } 71 | }); 72 | 73 | let (who, other) = match select(a, b).await { 74 | Either::Left(x) => x, 75 | Either::Right(_) => panic!("Timer for 2 ms finished before timer for 1 ms"), 76 | }; 77 | assert_eq!(who, 1); 78 | // Drop `b` so that its timer is canceled. 79 | drop(other); 80 | TimeoutFuture::new(3).await; 81 | // We should never have fired `b`'s timer. 82 | assert_eq!(cell.get(), false); 83 | } 84 | 85 | #[wasm_bindgen_test] 86 | async fn interval() { 87 | let (mut sender, receiver) = mpsc::channel(1); 88 | let i = Interval::new(1, move || { 89 | if !sender.is_closed() { 90 | sender.try_send(()).unwrap() 91 | } 92 | }); 93 | 94 | let results: Vec<_> = receiver.take(5).collect().await; 95 | drop(i); 96 | assert_eq!(results.len(), 5); 97 | } 98 | 99 | #[wasm_bindgen_test] 100 | async fn interval_cancel() { 101 | let i = Interval::new(10, move || { 102 | panic!("This should never be called"); 103 | }); 104 | i.cancel(); 105 | 106 | // This keeps us live for long enough that if any erroneous Interval callbacks fired, we'll have seen them. 107 | sleep(Duration::from_millis(100)).await; 108 | } 109 | 110 | #[wasm_bindgen_test] 111 | async fn interval_stream() { 112 | let results: Vec<_> = IntervalStream::new(1).take(5).collect().await; 113 | assert_eq!(results.len(), 5); 114 | } 115 | -------------------------------------------------------------------------------- /crates/timers/tests/web.rs: -------------------------------------------------------------------------------- 1 | #![cfg(all(target_family = "wasm", feature = "futures"))] 2 | 3 | use futures_channel::{mpsc, oneshot}; 4 | use futures_util::{ 5 | future::{select, Either, FutureExt}, 6 | stream::StreamExt, 7 | }; 8 | use gloo_timers::{ 9 | callback::{Interval, Timeout}, 10 | future::{sleep, IntervalStream, TimeoutFuture}, 11 | }; 12 | use std::cell::Cell; 13 | use std::rc::Rc; 14 | use std::time::Duration; 15 | use wasm_bindgen_test::*; 16 | 17 | wasm_bindgen_test_configure!(run_in_browser); 18 | 19 | #[wasm_bindgen_test] 20 | async fn timeout() { 21 | let (sender, receiver) = oneshot::channel(); 22 | Timeout::new(1, || sender.send(()).unwrap()).forget(); 23 | receiver.await.unwrap(); 24 | } 25 | 26 | #[wasm_bindgen_test] 27 | async fn timeout_cancel() { 28 | let cell = Rc::new(Cell::new(false)); 29 | 30 | let t = Timeout::new(1, { 31 | let cell = cell.clone(); 32 | move || { 33 | cell.set(true); 34 | panic!("should have been cancelled"); 35 | } 36 | }); 37 | t.cancel(); 38 | 39 | let (sender, receiver) = oneshot::channel(); 40 | 41 | Timeout::new(2, move || { 42 | sender.send(()).unwrap(); 43 | assert_eq!(cell.get(), false); 44 | }) 45 | .forget(); 46 | 47 | receiver.await.unwrap(); 48 | } 49 | 50 | #[wasm_bindgen_test] 51 | async fn timeout_future() { 52 | TimeoutFuture::new(1).await; 53 | } 54 | 55 | #[wasm_bindgen_test] 56 | async fn timeout_future_cancel() { 57 | let cell = Rc::new(Cell::new(false)); 58 | 59 | let a = TimeoutFuture::new(1).map({ 60 | let cell = cell.clone(); 61 | move |_| { 62 | assert_eq!(cell.get(), false); 63 | 1 64 | } 65 | }); 66 | 67 | let b = TimeoutFuture::new(2).map({ 68 | let cell = cell.clone(); 69 | move |_| { 70 | cell.set(true); 71 | 2u32 72 | } 73 | }); 74 | 75 | let (who, other) = match select(a, b).await { 76 | Either::Left(x) => x, 77 | Either::Right(_) => panic!("Timer for 2 ms finished before timer for 1 ms"), 78 | }; 79 | assert_eq!(who, 1); 80 | // Drop `b` so that its timer is canceled. 81 | drop(other); 82 | TimeoutFuture::new(3).await; 83 | // We should never have fired `b`'s timer. 84 | assert_eq!(cell.get(), false); 85 | } 86 | 87 | #[wasm_bindgen_test] 88 | async fn interval() { 89 | let (mut sender, receiver) = mpsc::channel(1); 90 | let i = Interval::new(1, move || { 91 | if !sender.is_closed() { 92 | sender.try_send(()).unwrap() 93 | } 94 | }); 95 | 96 | let results: Vec<_> = receiver.take(5).collect().await; 97 | drop(i); 98 | assert_eq!(results.len(), 5); 99 | } 100 | 101 | #[wasm_bindgen_test] 102 | async fn interval_cancel() { 103 | let i = Interval::new(10, move || { 104 | panic!("This should never be called"); 105 | }); 106 | i.cancel(); 107 | 108 | // This keeps us live for long enough that if any erroneous Interval callbacks fired, we'll have seen them. 109 | sleep(Duration::from_millis(100)).await; 110 | } 111 | 112 | #[wasm_bindgen_test] 113 | async fn interval_stream() { 114 | let results: Vec<_> = IntervalStream::new(1).take(5).collect().await; 115 | assert_eq!(results.len(), 5); 116 | } 117 | -------------------------------------------------------------------------------- /crates/utils/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gloo-utils" 3 | version = "0.2.0" 4 | edition = "2021" 5 | description = "Convenience crate for common `web_sys` features" 6 | authors = ["Rust and WebAssembly Working Group"] 7 | license = "MIT OR Apache-2.0" 8 | readme = "README.md" 9 | repository = "https://github.com/rustwasm/gloo/tree/master/crates/utils" 10 | homepage = "https://github.com/rustwasm/gloo" 11 | categories = ["api-bindings", "wasm"] 12 | rust-version = "1.64" 13 | 14 | 15 | [dependencies] 16 | wasm-bindgen = "0.2" 17 | js-sys = "0.3" 18 | serde = { version = "1.0", optional = true } 19 | serde_json = { version = "1.0", optional = true } 20 | 21 | [dependencies.web-sys] 22 | version = "0.3" 23 | features = [ 24 | "Document", 25 | "History", 26 | "HtmlElement", 27 | "Location", 28 | "Window", 29 | "HtmlHeadElement", 30 | "Element", 31 | ] 32 | 33 | [features] 34 | default = ["serde"] 35 | serde = ["dep:serde", "dep:serde_json"] 36 | 37 | [dev-dependencies] 38 | wasm-bindgen-test = "0.3" 39 | serde_derive = "1.0" 40 | 41 | [package.metadata.docs.rs] 42 | all-features = true 43 | rustdoc-args = ["--cfg", "docsrs"] 44 | -------------------------------------------------------------------------------- /crates/utils/README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

gloo-utils

4 | 5 |

6 | Crates.io version 7 | Download 8 | docs.rs docs 9 |

10 | 11 |

12 | API Docs 13 | | 14 | Contributing 15 | | 16 | Chat 17 |

18 | 19 | Built with 🦀🕸 by The Rust and WebAssembly Working Group 20 |
21 | 22 | Wraps common `web_sys` features with a cleaner API. 23 | 24 | See the API docs to learn more 25 | -------------------------------------------------------------------------------- /crates/utils/src/errors.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryFrom; 2 | use std::fmt; 3 | use wasm_bindgen::{JsCast, JsValue}; 4 | 5 | /// Wrapper type around [`js_sys::Error`] 6 | /// 7 | /// [`Display`][fmt::Display] impl returns the result `error.toString()` from JavaScript 8 | pub struct JsError { 9 | /// `name` from [`js_sys::Error`] 10 | pub name: String, 11 | /// `message` from [`js_sys::Error`] 12 | pub message: String, 13 | js_to_string: String, 14 | } 15 | 16 | impl From for JsError { 17 | fn from(error: js_sys::Error) -> Self { 18 | JsError { 19 | name: String::from(error.name()), 20 | message: String::from(error.message()), 21 | js_to_string: String::from(error.to_string()), 22 | } 23 | } 24 | } 25 | 26 | /// The [`JsValue`] is not a JavaScript's `Error`. 27 | pub struct NotJsError { 28 | pub js_value: JsValue, 29 | js_to_string: String, 30 | } 31 | 32 | impl fmt::Debug for NotJsError { 33 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 34 | f.debug_struct("NotJsError") 35 | .field("js_value", &self.js_value) 36 | .finish() 37 | } 38 | } 39 | 40 | impl fmt::Display for NotJsError { 41 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 42 | f.write_str(&self.js_to_string) 43 | } 44 | } 45 | 46 | impl std::error::Error for NotJsError {} 47 | 48 | impl TryFrom for JsError { 49 | type Error = NotJsError; 50 | 51 | fn try_from(value: JsValue) -> Result { 52 | match value.dyn_into::() { 53 | Ok(error) => Ok(JsError::from(error)), 54 | Err(js_value) => { 55 | let js_to_string = String::from(js_sys::JsString::from(js_value.clone())); 56 | Err(NotJsError { 57 | js_value, 58 | js_to_string, 59 | }) 60 | } 61 | } 62 | } 63 | } 64 | 65 | impl fmt::Display for JsError { 66 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 67 | write!(f, "{}", self.js_to_string) 68 | } 69 | } 70 | 71 | impl fmt::Debug for JsError { 72 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 73 | f.debug_struct("JsError") 74 | .field("name", &self.name) 75 | .field("message", &self.message) 76 | .finish() 77 | } 78 | } 79 | 80 | impl std::error::Error for JsError {} 81 | -------------------------------------------------------------------------------- /crates/utils/src/iter.rs: -------------------------------------------------------------------------------- 1 | use wasm_bindgen::{JsValue, UnwrapThrowExt}; 2 | 3 | /// A wrapper around JS Iterator so it can be consumed from Rust. 4 | /// 5 | /// This type implements [`Iterator`] trait and will keep yielding [`JsValue`] 6 | /// until the underlying [`js_sys::Iterator`] is exuasted. 7 | /// 8 | /// This type is called `UncheckedIter` because it does no checking for 9 | /// the underlying type of the [`js_sys::Iterator`] and yields [`JsValue`]s. 10 | /// 11 | /// # Example 12 | /// 13 | /// ```rust 14 | /// use gloo_utils::iter::UncheckedIter; 15 | /// use wasm_bindgen::{JsCast, JsValue, UnwrapThrowExt}; 16 | /// 17 | /// # fn no_run() { 18 | /// let map = js_sys::Map::new(); 19 | /// map.set(&JsValue::from("one"), &JsValue::from(1_f64)); 20 | /// 21 | /// let mut iter = UncheckedIter::from(map.entries()).map(|js_value| { 22 | /// let array: js_sys::Array = js_value.unchecked_into(); 23 | /// ( 24 | /// array.get(0).as_string().unwrap_throw(), 25 | /// array.get(1).as_f64().unwrap_throw(), 26 | /// ) 27 | /// }); 28 | /// 29 | /// assert_eq!(iter.next(), Some((String::from("one"), 1_f64))); 30 | /// assert_eq!(iter.next(), None); 31 | /// # } 32 | /// ``` 33 | pub struct UncheckedIter(js_sys::Iterator); 34 | 35 | impl UncheckedIter { 36 | /// Obtain the raw [`js_sys::Iterator`] 37 | pub fn into_raw(self) -> js_sys::Iterator { 38 | self.0 39 | } 40 | } 41 | 42 | impl From for UncheckedIter { 43 | fn from(iter: js_sys::Iterator) -> Self { 44 | Self(iter) 45 | } 46 | } 47 | 48 | impl Iterator for UncheckedIter { 49 | type Item = JsValue; 50 | 51 | fn next(&mut self) -> Option { 52 | // we don't check for errors. Only use this type on things we know conform to the iterator 53 | // interface. 54 | let next = self.0.next().unwrap_throw(); 55 | if next.done() { 56 | None 57 | } else { 58 | Some(next.value()) 59 | } 60 | } 61 | } 62 | 63 | #[cfg(test)] 64 | mod tests { 65 | use super::*; 66 | use wasm_bindgen_test::*; 67 | 68 | wasm_bindgen_test_configure!(run_in_browser); 69 | 70 | #[wasm_bindgen_test] 71 | fn it_works() { 72 | let map = js_sys::Map::new(); 73 | macro_rules! map_set { 74 | ($key:expr => $value:expr) => { 75 | map.set(&JsValue::from($key), &JsValue::from($value)); 76 | }; 77 | } 78 | 79 | map_set!("one" => 1_f64); 80 | map_set!("two" => 2_f64); 81 | map_set!("three" => 3_f64); 82 | 83 | let mut iter = UncheckedIter::from(map.entries()).map(|js_value| { 84 | let array = js_sys::Array::from(&js_value); 85 | let array = array.to_vec(); 86 | ( 87 | array[0].as_string().expect_throw("not string"), 88 | array[1].as_f64().expect_throw("not f64"), 89 | ) 90 | }); 91 | 92 | assert_eq!(iter.next(), Some((String::from("one"), 1_f64))); 93 | assert_eq!(iter.next(), Some((String::from("two"), 2_f64))); 94 | assert_eq!(iter.next(), Some((String::from("three"), 3_f64))); 95 | assert_eq!(iter.next(), None); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /crates/utils/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(docsrs, feature(doc_cfg))] 2 | 3 | pub mod errors; 4 | pub mod iter; 5 | pub mod format { 6 | mod json; 7 | #[cfg(feature = "serde")] 8 | pub use json::JsValueSerdeExt; 9 | } 10 | use wasm_bindgen::UnwrapThrowExt; 11 | 12 | /// Convenience function to avoid repeating expect logic. 13 | pub fn window() -> web_sys::Window { 14 | web_sys::window().expect_throw("Can't find the global Window") 15 | } 16 | 17 | /// Convenience function to access the head element. 18 | pub fn head() -> web_sys::HtmlHeadElement { 19 | document() 20 | .head() 21 | .expect_throw("Can't find the head element") 22 | } 23 | 24 | /// Convenience function to access the web_sys DOM document. 25 | pub fn document() -> web_sys::Document { 26 | window().document().expect_throw("Can't find document") 27 | } 28 | 29 | /// Convenience function to access `document.body`. 30 | pub fn body() -> web_sys::HtmlElement { 31 | document().body().expect_throw("Can't find document body") 32 | } 33 | 34 | /// Convenience function to access `document.documentElement`. 35 | pub fn document_element() -> web_sys::Element { 36 | document() 37 | .document_element() 38 | .expect_throw("Can't find document element") 39 | } 40 | 41 | /// Convenience function to access the web_sys history. 42 | pub fn history() -> web_sys::History { 43 | window().history().expect_throw("Can't find history") 44 | } 45 | -------------------------------------------------------------------------------- /crates/utils/tests/serde.js: -------------------------------------------------------------------------------- 1 | function deepStrictEqual(left, right) { 2 | var left_json = JSON.stringify(left); 3 | var right_json = JSON.stringify(right); 4 | if (left_json !== right_json) { 5 | throw Error(`${left_json} != ${right_json}`) 6 | } 7 | } 8 | 9 | export function verify_serde (a) { 10 | deepStrictEqual(a, { 11 | a: 0, 12 | b: 'foo', 13 | c: null, 14 | d: { a: 1 } 15 | }); 16 | }; 17 | 18 | export function make_js_value() { 19 | return { 20 | a: 2, 21 | b: 'bar', 22 | c: { a: 3 }, 23 | d: { a: 4 }, 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /crates/utils/tests/serde.rs: -------------------------------------------------------------------------------- 1 | #![cfg(target_arch = "wasm32")] 2 | #![cfg(feature = "serde")] 3 | extern crate wasm_bindgen; 4 | extern crate wasm_bindgen_test; 5 | 6 | use wasm_bindgen::prelude::*; 7 | use wasm_bindgen_test::*; 8 | 9 | use gloo_utils::format::JsValueSerdeExt; 10 | 11 | use serde_derive::{Deserialize, Serialize}; 12 | 13 | wasm_bindgen_test_configure!(run_in_browser); 14 | 15 | #[wasm_bindgen(start)] 16 | pub fn start() { 17 | panic!(); 18 | } 19 | 20 | #[wasm_bindgen(module = "/tests/serde.js")] 21 | extern "C" { 22 | fn verify_serde(val: JsValue); 23 | fn make_js_value() -> JsValue; 24 | } 25 | 26 | #[derive(Deserialize, Serialize, Debug)] 27 | pub struct SerdeFoo { 28 | a: u32, 29 | b: String, 30 | c: Option, 31 | d: SerdeBar, 32 | } 33 | 34 | #[derive(Deserialize, Serialize, Debug)] 35 | pub struct SerdeBar { 36 | a: u32, 37 | } 38 | 39 | #[wasm_bindgen_test] 40 | fn from_serde() { 41 | let js = JsValue::from_serde("foo").unwrap(); 42 | assert_eq!(js.as_string(), Some("foo".to_string())); 43 | 44 | verify_serde( 45 | JsValue::from_serde(&SerdeFoo { 46 | a: 0, 47 | b: "foo".to_string(), 48 | c: None, 49 | d: SerdeBar { a: 1 }, 50 | }) 51 | .unwrap(), 52 | ); 53 | } 54 | 55 | #[wasm_bindgen_test] 56 | fn into_serde() { 57 | let js_value = make_js_value(); 58 | let foo = js_value.into_serde::().unwrap(); 59 | assert_eq!(foo.a, 2); 60 | assert_eq!(foo.b, "bar"); 61 | assert!(foo.c.is_some()); 62 | assert_eq!(foo.c.as_ref().unwrap().a, 3); 63 | assert_eq!(foo.d.a, 4); 64 | 65 | assert_eq!(JsValue::from("bar").into_serde::().unwrap(), "bar"); 66 | assert_eq!(JsValue::undefined().into_serde::().ok(), None); 67 | assert_eq!(JsValue::null().into_serde::().ok(), None); 68 | } 69 | -------------------------------------------------------------------------------- /crates/worker-macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gloo-worker-macros" 3 | version = "0.1.0" 4 | authors = ["Rust and WebAssembly Working Group"] 5 | edition = "2021" 6 | readme = "README.md" 7 | description = "Convenience crate for working with Web Workers" 8 | repository = "https://github.com/rustwasm/gloo/tree/master/crates/worker" 9 | homepage = "https://github.com/rustwasm/gloo" 10 | license = "MIT OR Apache-2.0" 11 | categories = ["api-bindings", "asynchronous", "wasm"] 12 | rust-version = "1.64" 13 | 14 | [lib] 15 | proc-macro = true 16 | 17 | [dependencies] 18 | proc-macro-crate = "1.2.1" 19 | proc-macro2 = "1.0.47" 20 | quote = "1.0.21" 21 | syn = { version = "2.0.15", features = ["full"] } 22 | 23 | [dev-dependencies] 24 | trybuild = "1" 25 | gloo = { path = "../..", features = ["futures"] } 26 | -------------------------------------------------------------------------------- /crates/worker-macros/README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

gloo-worker

4 | 5 |

6 | Crates.io version 7 | Download 8 | docs.rs docs 9 |

10 | 11 |

12 | API Docs 13 | | 14 | Contributing 15 | | 16 | Chat 17 |

18 | 19 | Built with 🦀🕸 by The Rust and WebAssembly Working Group 20 |
21 | 22 | Gloo workers are a way to offload tasks to web workers. These are run concurrently using 23 | [web-workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers). 24 | It provides a neat abstraction over the browser's Web Workers API which can be consumed from anywhere. 25 | -------------------------------------------------------------------------------- /crates/worker-macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | use syn::parse_macro_input; 3 | 4 | mod oneshot; 5 | mod reactor; 6 | mod worker_fn; 7 | 8 | use oneshot::{oneshot_impl, OneshotFn}; 9 | use reactor::{reactor_impl, ReactorFn}; 10 | use worker_fn::{WorkerFn, WorkerName}; 11 | 12 | #[proc_macro_attribute] 13 | pub fn reactor(attr: TokenStream, item: TokenStream) -> TokenStream { 14 | let item = parse_macro_input!(item as WorkerFn); 15 | let attr = parse_macro_input!(attr as WorkerName); 16 | 17 | reactor_impl(attr, item) 18 | .unwrap_or_else(|err| err.to_compile_error()) 19 | .into() 20 | } 21 | 22 | #[proc_macro_attribute] 23 | pub fn oneshot(attr: TokenStream, item: TokenStream) -> TokenStream { 24 | let item = parse_macro_input!(item as WorkerFn); 25 | let attr = parse_macro_input!(attr as WorkerName); 26 | 27 | oneshot_impl(attr, item) 28 | .unwrap_or_else(|err| err.to_compile_error()) 29 | .into() 30 | } 31 | -------------------------------------------------------------------------------- /crates/worker-macros/tests/oneshot.rs: -------------------------------------------------------------------------------- 1 | #![cfg(not(target_arch = "wasm32"))] 2 | 3 | #[test] 4 | fn macro_tests() { 5 | let t = trybuild::TestCases::new(); 6 | t.compile_fail("tests/oneshot/*-fail.rs"); 7 | t.pass("tests/oneshot/*-pass.rs"); 8 | } 9 | -------------------------------------------------------------------------------- /crates/worker-macros/tests/oneshot/basic-pass.rs: -------------------------------------------------------------------------------- 1 | #![no_implicit_prelude] 2 | 3 | #[::gloo::worker::oneshot::oneshot] 4 | async fn Worker(input: u32) -> u32 { 5 | input 6 | } 7 | 8 | fn main() {} 9 | -------------------------------------------------------------------------------- /crates/worker-macros/tests/oneshot/many_input-fail.rs: -------------------------------------------------------------------------------- 1 | #[gloo::worker::oneshot::oneshot] 2 | async fn Worker(input_1: u32, input_2: u32) -> u32 { 3 | 0 4 | } 5 | 6 | fn main() {} 7 | -------------------------------------------------------------------------------- /crates/worker-macros/tests/oneshot/many_input-fail.stderr: -------------------------------------------------------------------------------- 1 | error: oneshot worker can accept at most 1 argument 2 | --> tests/oneshot/many_input-fail.rs:2:31 3 | | 4 | 2 | async fn Worker(input_1: u32, input_2: u32) -> u32 { 5 | | ^^^^^^^^^^^^ 6 | -------------------------------------------------------------------------------- /crates/worker-macros/tests/oneshot/no_input-fail.rs: -------------------------------------------------------------------------------- 1 | #[gloo::worker::oneshot::oneshot] 2 | async fn Worker() -> u32 { 3 | 0 4 | } 5 | 6 | fn main() {} 7 | -------------------------------------------------------------------------------- /crates/worker-macros/tests/oneshot/no_input-fail.stderr: -------------------------------------------------------------------------------- 1 | error: expected 1 argument 2 | --> tests/oneshot/no_input-fail.rs:2:10 3 | | 4 | 2 | async fn Worker() -> u32 { 5 | | ^^^^^^ 6 | -------------------------------------------------------------------------------- /crates/worker-macros/tests/oneshot/sync-pass.rs: -------------------------------------------------------------------------------- 1 | #![no_implicit_prelude] 2 | 3 | #[::gloo::worker::oneshot::oneshot] 4 | fn Worker(input: u32) -> u32 { 5 | input 6 | } 7 | 8 | fn main() {} 9 | -------------------------------------------------------------------------------- /crates/worker-macros/tests/reactor.rs: -------------------------------------------------------------------------------- 1 | #![cfg(not(target_arch = "wasm32"))] 2 | 3 | #[test] 4 | fn macro_tests() { 5 | let t = trybuild::TestCases::new(); 6 | t.compile_fail("tests/reactor/*-fail.rs"); 7 | t.pass("tests/reactor/*-pass.rs"); 8 | } 9 | -------------------------------------------------------------------------------- /crates/worker-macros/tests/reactor/basic-pass.rs: -------------------------------------------------------------------------------- 1 | #![no_implicit_prelude] 2 | 3 | #[::gloo::worker::reactor::reactor] 4 | async fn Worker(_scope: ::gloo::worker::reactor::ReactorScope<(), ()>) {} 5 | 6 | fn main() {} 7 | -------------------------------------------------------------------------------- /crates/worker-macros/tests/reactor/many_input-fail.rs: -------------------------------------------------------------------------------- 1 | #[gloo::worker::reactor::reactor] 2 | async fn Worker(input_1: u32, input_2: u32) { 3 | 0 4 | } 5 | 6 | fn main() {} 7 | -------------------------------------------------------------------------------- /crates/worker-macros/tests/reactor/many_input-fail.stderr: -------------------------------------------------------------------------------- 1 | error: reactor worker can accept at most 1 argument 2 | --> tests/reactor/many_input-fail.rs:2:31 3 | | 4 | 2 | async fn Worker(input_1: u32, input_2: u32) { 5 | | ^^^^^^^^^^^^ 6 | -------------------------------------------------------------------------------- /crates/worker-macros/tests/reactor/return_type-fail.rs: -------------------------------------------------------------------------------- 1 | #![no_implicit_prelude] 2 | 3 | #[::gloo::worker::reactor::reactor] 4 | fn Worker(_scope: ::gloo::worker::reactor::ReactorScope<(), ()>) -> u32 { 5 | 0 6 | } 7 | 8 | fn main() {} 9 | -------------------------------------------------------------------------------- /crates/worker-macros/tests/reactor/return_type-fail.stderr: -------------------------------------------------------------------------------- 1 | error: reactor workers cannot return any value 2 | --> tests/reactor/return_type-fail.rs:4:69 3 | | 4 | 4 | fn Worker(_scope: ::gloo::worker::reactor::ReactorScope<(), ()>) -> u32 { 5 | | ^^^ 6 | -------------------------------------------------------------------------------- /crates/worker-macros/tests/reactor/sync-fail.rs: -------------------------------------------------------------------------------- 1 | #![no_implicit_prelude] 2 | 3 | #[::gloo::worker::reactor::reactor] 4 | fn Worker(_scope: ::gloo::worker::reactor::ReactorScope<(), ()>) {} 5 | 6 | fn main() {} 7 | -------------------------------------------------------------------------------- /crates/worker-macros/tests/reactor/sync-fail.stderr: -------------------------------------------------------------------------------- 1 | error: reactor workers must be asynchronous 2 | --> tests/reactor/sync-fail.rs:4:4 3 | | 4 | 4 | fn Worker(_scope: ::gloo::worker::reactor::ReactorScope<(), ()>) {} 5 | | ^^^^^^ 6 | -------------------------------------------------------------------------------- /crates/worker/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gloo-worker" 3 | version = "0.5.0" 4 | authors = ["Rust and WebAssembly Working Group"] 5 | edition = "2021" 6 | readme = "README.md" 7 | description = "Convenience crate for working with Web Workers" 8 | repository = "https://github.com/rustwasm/gloo/tree/master/crates/worker" 9 | homepage = "https://github.com/rustwasm/gloo" 10 | license = "MIT OR Apache-2.0" 11 | categories = ["api-bindings", "asynchronous", "wasm"] 12 | rust-version = "1.64" 13 | 14 | [package.metadata.docs.rs] 15 | all-features = true 16 | 17 | rustdoc-args = ["--cfg", "docsrs"] 18 | 19 | 20 | [dependencies] 21 | bincode = "1" 22 | gloo-utils = { path = "../utils", version = "0.2" } 23 | gloo-worker-macros = { path = "../worker-macros", version = "0.1" } 24 | js-sys = "0.3" 25 | pinned = "0.1.0" 26 | serde = { version = "1", features = ["derive"] } 27 | wasm-bindgen = "0.2" 28 | wasm-bindgen-futures = { version = "0.4" } 29 | futures = { version = "0.3", features = ["std"], default-features = false } 30 | thiserror = "1.0.37" 31 | 32 | [dependencies.web-sys] 33 | version = "0.3" 34 | features = [ 35 | "Blob", 36 | "BlobPropertyBag", 37 | "DedicatedWorkerGlobalScope", 38 | "MessageEvent", 39 | "Url", 40 | "Worker", 41 | "WorkerOptions", 42 | ] 43 | 44 | [features] 45 | default = [] 46 | futures = [] 47 | -------------------------------------------------------------------------------- /crates/worker/README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

gloo-worker

4 | 5 |

6 | Crates.io version 7 | Download 8 | docs.rs docs 9 |

10 | 11 |

12 | API Docs 13 | | 14 | Contributing 15 | | 16 | Chat 17 |

18 | 19 | Built with 🦀🕸 by The Rust and WebAssembly Working Group 20 |
21 | 22 | Gloo workers are a way to offload tasks to web workers. These are run concurrently using 23 | [web-workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers). 24 | It provides a neat abstraction over the browser's Web Workers API which can be consumed from anywhere. 25 | -------------------------------------------------------------------------------- /crates/worker/src/actor/handler_id.rs: -------------------------------------------------------------------------------- 1 | use std::sync::atomic::{AtomicUsize, Ordering}; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | /// Identifier to send output to bridges. 6 | #[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Clone, Copy)] 7 | pub struct HandlerId(usize); 8 | 9 | impl HandlerId { 10 | pub(crate) fn new() -> Self { 11 | static CTR: AtomicUsize = AtomicUsize::new(0); 12 | 13 | let id = CTR.fetch_add(1, Ordering::SeqCst); 14 | 15 | HandlerId(id) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /crates/worker/src/actor/messages.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use super::handler_id::HandlerId; 4 | use super::traits::Worker; 5 | 6 | /// Serializable messages to worker 7 | #[derive(Serialize, Deserialize, Debug)] 8 | pub(crate) enum ToWorker 9 | where 10 | W: Worker, 11 | { 12 | /// Client is connected 13 | Connected(HandlerId), 14 | /// Incoming message to Worker 15 | ProcessInput(HandlerId, W::Input), 16 | /// Client is disconnected 17 | Disconnected(HandlerId), 18 | /// Worker should be terminated 19 | Destroy, 20 | } 21 | 22 | /// Serializable messages sent by worker to consumer 23 | #[derive(Serialize, Deserialize, Debug)] 24 | pub(crate) enum FromWorker 25 | where 26 | W: Worker, 27 | { 28 | /// Worker sends this message when `wasm` bundle has loaded. 29 | WorkerLoaded, 30 | /// Outgoing message to consumer 31 | ProcessOutput(HandlerId, W::Output), 32 | } 33 | -------------------------------------------------------------------------------- /crates/worker/src/actor/mod.rs: -------------------------------------------------------------------------------- 1 | //! A worker that follows the Actor Model. 2 | 3 | use std::cell::RefCell; 4 | use std::rc::Rc; 5 | 6 | mod bridge; 7 | mod handler_id; 8 | mod lifecycle; 9 | mod messages; 10 | mod native_worker; 11 | mod registrar; 12 | mod scope; 13 | mod spawner; 14 | mod traits; 15 | 16 | pub use bridge::WorkerBridge; 17 | pub use handler_id::HandlerId; 18 | pub use registrar::WorkerRegistrar; 19 | pub use scope::{WorkerDestroyHandle, WorkerScope}; 20 | pub use spawner::WorkerSpawner; 21 | pub use traits::Worker; 22 | 23 | /// Alias for `Rc>` 24 | type Shared = Rc>; 25 | 26 | /// Alias for `Rc` 27 | type Callback = Rc; 28 | -------------------------------------------------------------------------------- /crates/worker/src/actor/native_worker.rs: -------------------------------------------------------------------------------- 1 | use crate::codec::Codec; 2 | use serde::{Deserialize, Serialize}; 3 | use wasm_bindgen::closure::Closure; 4 | use wasm_bindgen::prelude::*; 5 | use wasm_bindgen::{JsCast, JsValue}; 6 | pub(crate) use web_sys::Worker as DedicatedWorker; 7 | use web_sys::{DedicatedWorkerGlobalScope, MessageEvent}; 8 | 9 | pub(crate) trait WorkerSelf { 10 | type GlobalScope; 11 | 12 | fn worker_self() -> Self::GlobalScope; 13 | } 14 | 15 | impl WorkerSelf for DedicatedWorker { 16 | type GlobalScope = DedicatedWorkerGlobalScope; 17 | 18 | fn worker_self() -> Self::GlobalScope { 19 | JsValue::from(js_sys::global()).into() 20 | } 21 | } 22 | 23 | pub(crate) trait NativeWorkerExt { 24 | fn set_on_packed_message(&self, handler: F) 25 | where 26 | T: Serialize + for<'de> Deserialize<'de>, 27 | CODEC: Codec, 28 | F: 'static + Fn(T); 29 | 30 | fn post_packed_message(&self, data: T) 31 | where 32 | T: Serialize + for<'de> Deserialize<'de>, 33 | CODEC: Codec; 34 | } 35 | 36 | macro_rules! worker_ext_impl { 37 | ($($type:path),+) => {$( 38 | impl NativeWorkerExt for $type { 39 | fn set_on_packed_message(&self, handler: F) 40 | where 41 | T: Serialize + for<'de> Deserialize<'de>, 42 | CODEC: Codec, 43 | F: 'static + Fn(T) 44 | { 45 | let handler = move |message: MessageEvent| { 46 | let msg = CODEC::decode(message.data()); 47 | handler(msg); 48 | }; 49 | let closure = Closure::wrap(Box::new(handler) as Box).into_js_value(); 50 | self.set_onmessage(Some(closure.as_ref().unchecked_ref())); 51 | } 52 | 53 | fn post_packed_message(&self, data: T) 54 | where 55 | T: Serialize + for<'de> Deserialize<'de>, 56 | CODEC: Codec 57 | { 58 | self.post_message(&CODEC::encode(data)) 59 | .expect_throw("failed to post message"); 60 | } 61 | } 62 | )+}; 63 | } 64 | 65 | worker_ext_impl! { 66 | DedicatedWorker, DedicatedWorkerGlobalScope 67 | } 68 | -------------------------------------------------------------------------------- /crates/worker/src/actor/registrar.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::marker::PhantomData; 3 | 4 | use serde::de::Deserialize; 5 | use serde::ser::Serialize; 6 | 7 | use super::lifecycle::WorkerLifecycleEvent; 8 | use super::messages::{FromWorker, ToWorker}; 9 | use super::native_worker::{DedicatedWorker, NativeWorkerExt, WorkerSelf}; 10 | use super::scope::WorkerScope; 11 | use super::traits::Worker; 12 | use crate::codec::{Bincode, Codec}; 13 | 14 | /// A Worker Registrar. 15 | pub struct WorkerRegistrar 16 | where 17 | W: Worker, 18 | CODEC: Codec, 19 | { 20 | _marker: PhantomData<(W, CODEC)>, 21 | } 22 | 23 | impl fmt::Debug for WorkerRegistrar { 24 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 25 | f.write_str("WorkerRegistrar<_>") 26 | } 27 | } 28 | 29 | impl WorkerRegistrar 30 | where 31 | W: Worker + 'static, 32 | CODEC: Codec, 33 | { 34 | pub(crate) fn new() -> Self { 35 | Self { 36 | _marker: PhantomData, 37 | } 38 | } 39 | 40 | /// Sets a new message encoding. 41 | pub fn encoding(&self) -> WorkerRegistrar 42 | where 43 | C: Codec, 44 | { 45 | WorkerRegistrar::new() 46 | } 47 | 48 | /// Executes an worker in the current environment. 49 | pub fn register(&self) 50 | where 51 | CODEC: Codec, 52 | W::Input: Serialize + for<'de> Deserialize<'de>, 53 | W::Output: Serialize + for<'de> Deserialize<'de>, 54 | { 55 | let scope = WorkerScope::::new::(); 56 | let upd = WorkerLifecycleEvent::Create(scope.clone()); 57 | scope.send(upd); 58 | let handler = move |msg: ToWorker| { 59 | let upd = WorkerLifecycleEvent::Remote(msg); 60 | scope.send(upd); 61 | }; 62 | let loaded: FromWorker = FromWorker::WorkerLoaded; 63 | let worker = DedicatedWorker::worker_self(); 64 | worker.set_on_packed_message::<_, CODEC, _>(handler); 65 | worker.post_packed_message::<_, CODEC>(loaded); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /crates/worker/src/actor/traits.rs: -------------------------------------------------------------------------------- 1 | use super::handler_id::HandlerId; 2 | use super::registrar::WorkerRegistrar; 3 | use super::scope::{WorkerDestroyHandle, WorkerScope}; 4 | use super::spawner::WorkerSpawner; 5 | use crate::traits::{Registrable, Spawnable}; 6 | 7 | /// Declares the behaviour of a worker. 8 | pub trait Worker: Sized { 9 | /// Update message type. 10 | type Message; 11 | /// Incoming message type. 12 | type Input; 13 | /// Outgoing message type. 14 | type Output; 15 | 16 | /// Creates an instance of a worker. 17 | fn create(scope: &WorkerScope) -> Self; 18 | 19 | /// Receives an update. 20 | /// 21 | /// This method is called when the worker send messages to itself via [`WorkerScope::send_message`]. 22 | fn update(&mut self, scope: &WorkerScope, msg: Self::Message); 23 | 24 | /// New bridge created. 25 | /// 26 | /// When a new bridge is created by [`WorkerSpawner::spawn`](crate::WorkerSpawner) 27 | /// or [`WorkerBridge::fork`](crate::WorkerBridge::fork), 28 | /// the worker will be notified the [`HandlerId`] of the created bridge via this method. 29 | fn connected(&mut self, scope: &WorkerScope, id: HandlerId) { 30 | let _scope = scope; 31 | let _id = id; 32 | } 33 | 34 | /// Receives an input from a connected bridge. 35 | /// 36 | /// When a bridge sends an input via [`WorkerBridge::send`](crate::WorkerBridge::send), the worker will receive the 37 | /// input via this method. 38 | fn received(&mut self, scope: &WorkerScope, msg: Self::Input, id: HandlerId); 39 | 40 | /// Existing bridge destroyed. 41 | /// 42 | /// When a bridge is dropped, the worker will be notified with this method. 43 | fn disconnected(&mut self, scope: &WorkerScope, id: HandlerId) { 44 | let _scope = scope; 45 | let _id = id; 46 | } 47 | 48 | /// Destroys the current worker. 49 | /// 50 | /// When all bridges are dropped, the method will be invoked. 51 | /// 52 | /// This method is provided a destroy handle where when it is dropped, the worker is closed. 53 | /// If the worker is closed immediately, then it can ignore the destroy handle. 54 | /// Otherwise hold the destroy handle until the clean up task is finished. 55 | /// 56 | /// # Note 57 | /// 58 | /// This method will only be called after all bridges are disconnected. 59 | /// Attempting to send messages after this method is called will have no effect. 60 | fn destroy(&mut self, scope: &WorkerScope, destruct: WorkerDestroyHandle) { 61 | let _scope = scope; 62 | let _destruct = destruct; 63 | } 64 | } 65 | 66 | impl Spawnable for W 67 | where 68 | W: Worker + 'static, 69 | { 70 | type Spawner = WorkerSpawner; 71 | 72 | fn spawner() -> WorkerSpawner { 73 | WorkerSpawner::new() 74 | } 75 | } 76 | 77 | impl Registrable for W 78 | where 79 | W: Worker + 'static, 80 | { 81 | type Registrar = WorkerRegistrar; 82 | 83 | fn registrar() -> WorkerRegistrar { 84 | WorkerRegistrar::new() 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /crates/worker/src/codec.rs: -------------------------------------------------------------------------------- 1 | use js_sys::Uint8Array; 2 | use serde::{Deserialize, Serialize}; 3 | use wasm_bindgen::JsValue; 4 | 5 | /// Message Encoding and Decoding Format 6 | pub trait Codec { 7 | /// Encode an input to JsValue 8 | fn encode(input: I) -> JsValue 9 | where 10 | I: Serialize; 11 | 12 | /// Decode a message to a type 13 | fn decode(input: JsValue) -> O 14 | where 15 | O: for<'de> Deserialize<'de>; 16 | } 17 | 18 | /// Default message encoding with [bincode]. 19 | #[derive(Debug)] 20 | pub struct Bincode; 21 | 22 | impl Codec for Bincode { 23 | fn encode(input: I) -> JsValue 24 | where 25 | I: Serialize, 26 | { 27 | let buf = bincode::serialize(&input).expect("can't serialize an worker message"); 28 | Uint8Array::from(buf.as_slice()).into() 29 | } 30 | 31 | fn decode(input: JsValue) -> O 32 | where 33 | O: for<'de> Deserialize<'de>, 34 | { 35 | let data = Uint8Array::from(input).to_vec(); 36 | bincode::deserialize(&data).expect("can't deserialize an worker message") 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /crates/worker/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Workers are a way to offload tasks to web workers. These are run concurrently using 2 | //! [web-workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers). 3 | //! 4 | //! # Communicating with workers 5 | //! 6 | //! ### Bridges 7 | //! 8 | //! After a Worker is spawned, a bridge is created. 9 | //! A Bridge allows bi-directional communication between an worker and a component. 10 | //! Bridges also allow workers to communicate with one another. 11 | //! 12 | //! ### Scopes 13 | //! 14 | //! Scopes are used by workers to communicates with bridges and send updates to itself after 15 | //! a task is finished. 16 | //! 17 | //! ### Overhead 18 | //! 19 | //! Gloo Workers use web workers. They incur a serialization overhead on the 20 | //! messages they send and receive. Bridges use [bincode](https://github.com/servo/bincode) 21 | //! by default to communicate with workers, so the cost is substantially higher 22 | //! than just calling a function. 23 | //! 24 | //! # API 25 | //! 26 | //! The API is exposed in two different ways. 27 | //! 1. Using the `Worker` trait. 28 | //! 2. Using the `#[oneshot]` and `#[reactor]` macros. 29 | //! 30 | //! ## Worker trait 31 | //! 32 | //! The [`Worker`] trait is the core of the API. It allows you to spawn workers and communicate 33 | //! with them. It provides an actor model to communicate with for workers. 34 | //! 35 | //! See the [`Worker`] trait for more information. 36 | //! 37 | //! ## Macros 38 | //! 39 | //! The macros provide a function-like syntax to spawn workers and communicate with them. 40 | //! There are two macros: 41 | //! 1. [`#[oneshot]`](oneshot) - Worker where each input produces a single output. 42 | //! 2. [`#[reactor]`](reactor) - Worker that receives input(s) and may produce output(s). 43 | 44 | #![deny( 45 | clippy::all, 46 | missing_docs, 47 | missing_debug_implementations, 48 | bare_trait_objects, 49 | anonymous_parameters, 50 | elided_lifetimes_in_paths 51 | )] 52 | #![cfg_attr(docsrs, feature(doc_cfg))] 53 | 54 | mod actor; 55 | mod codec; 56 | #[cfg(feature = "futures")] 57 | pub mod oneshot; 58 | #[cfg(feature = "futures")] 59 | pub mod reactor; 60 | mod traits; 61 | 62 | pub use actor::*; 63 | pub use codec::{Bincode, Codec}; 64 | pub use traits::*; 65 | -------------------------------------------------------------------------------- /crates/worker/src/oneshot/bridge.rs: -------------------------------------------------------------------------------- 1 | use futures::stream::StreamExt; 2 | use pinned::mpsc; 3 | use pinned::mpsc::UnboundedReceiver; 4 | 5 | use super::traits::Oneshot; 6 | use super::worker::OneshotWorker; 7 | use crate::actor::{WorkerBridge, WorkerSpawner}; 8 | use crate::codec::Codec; 9 | 10 | /// A connection manager for components interaction with oneshot workers. 11 | #[derive(Debug)] 12 | pub struct OneshotBridge 13 | where 14 | N: Oneshot + 'static, 15 | { 16 | inner: WorkerBridge>, 17 | rx: UnboundedReceiver, 18 | } 19 | 20 | impl OneshotBridge 21 | where 22 | N: Oneshot + 'static, 23 | { 24 | #[inline(always)] 25 | pub(crate) fn new( 26 | inner: WorkerBridge>, 27 | rx: UnboundedReceiver, 28 | ) -> Self { 29 | Self { inner, rx } 30 | } 31 | 32 | #[inline(always)] 33 | pub(crate) fn register_callback( 34 | spawner: &mut WorkerSpawner, CODEC>, 35 | ) -> UnboundedReceiver 36 | where 37 | CODEC: Codec, 38 | { 39 | let (tx, rx) = mpsc::unbounded(); 40 | spawner.callback(move |output| { 41 | let _ = tx.send_now(output); 42 | }); 43 | 44 | rx 45 | } 46 | 47 | /// Forks the bridge. 48 | /// 49 | /// This method creates a new bridge that can be used to execute tasks on the same worker instance. 50 | pub fn fork(&self) -> Self { 51 | let (tx, rx) = mpsc::unbounded(); 52 | let inner = self.inner.fork(Some(move |output| { 53 | let _ = tx.send_now(output); 54 | })); 55 | 56 | Self { inner, rx } 57 | } 58 | 59 | /// Run the the current oneshot worker once in the current worker instance. 60 | pub async fn run(&mut self, input: N::Input) -> N::Output { 61 | // &mut self guarantees that the bridge will be 62 | // exclusively borrowed during the time the oneshot worker is running. 63 | self.inner.send(input); 64 | 65 | // For each bridge, there can only be 1 active task running on the worker instance. 66 | // The next output will be the output for the input that we just sent. 67 | self.rx 68 | .next() 69 | .await 70 | .expect("failed to receive result from worker") 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /crates/worker/src/oneshot/mod.rs: -------------------------------------------------------------------------------- 1 | //! A future-based worker that for each input, one output is produced. 2 | //! 3 | //! ## Example 4 | //! 5 | //! ```rust, no_run 6 | //! use gloo_worker::oneshot::oneshot; 7 | //! use gloo_worker::Spawnable; 8 | //! 9 | //! #[oneshot] 10 | //! async fn Squared(input: u32) -> u32 { 11 | //! input.pow(2) 12 | //! } 13 | //! 14 | //! # async { 15 | //! // consuming the worker 16 | //! let mut squared_bridge = Squared::spawner().spawn("..."); 17 | //! assert_eq!(squared_bridge.run(2).await, 4); 18 | //! # }; 19 | //! ``` 20 | 21 | mod bridge; 22 | mod registrar; 23 | mod spawner; 24 | mod traits; 25 | mod worker; 26 | 27 | pub use bridge::OneshotBridge; 28 | pub use registrar::OneshotRegistrar; 29 | pub use spawner::OneshotSpawner; 30 | pub use traits::Oneshot; 31 | 32 | /// Creates an oneshot worker. 33 | /// 34 | /// See [module level documentation](self) for more information. 35 | #[doc(inline)] 36 | #[cfg(feature = "futures")] 37 | pub use gloo_worker_macros::oneshot; 38 | -------------------------------------------------------------------------------- /crates/worker/src/oneshot/registrar.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use serde::de::Deserialize; 4 | use serde::ser::Serialize; 5 | 6 | use super::traits::Oneshot; 7 | use super::worker::OneshotWorker; 8 | use crate::actor::WorkerRegistrar; 9 | use crate::codec::{Bincode, Codec}; 10 | use crate::traits::Registrable; 11 | 12 | /// A registrar for oneshot workers. 13 | pub struct OneshotRegistrar 14 | where 15 | T: Oneshot + 'static, 16 | CODEC: Codec + 'static, 17 | { 18 | inner: WorkerRegistrar, CODEC>, 19 | } 20 | 21 | impl Default for OneshotRegistrar 22 | where 23 | T: Oneshot + 'static, 24 | CODEC: Codec + 'static, 25 | { 26 | fn default() -> Self { 27 | Self::new() 28 | } 29 | } 30 | 31 | impl OneshotRegistrar 32 | where 33 | N: Oneshot + 'static, 34 | CODEC: Codec + 'static, 35 | { 36 | /// Creates a new Oneshot Registrar. 37 | pub fn new() -> Self { 38 | Self { 39 | inner: OneshotWorker::::registrar().encoding::(), 40 | } 41 | } 42 | 43 | /// Sets the encoding. 44 | pub fn encoding(&self) -> OneshotRegistrar 45 | where 46 | C: Codec + 'static, 47 | { 48 | OneshotRegistrar { 49 | inner: self.inner.encoding::(), 50 | } 51 | } 52 | 53 | /// Registers the worker. 54 | pub fn register(&self) 55 | where 56 | N::Input: Serialize + for<'de> Deserialize<'de>, 57 | N::Output: Serialize + for<'de> Deserialize<'de>, 58 | { 59 | self.inner.register() 60 | } 61 | } 62 | 63 | impl fmt::Debug for OneshotRegistrar 64 | where 65 | T: Oneshot + 'static, 66 | CODEC: Codec + 'static, 67 | { 68 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 69 | f.debug_struct("OneshotRegistrar<_>").finish() 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /crates/worker/src/oneshot/spawner.rs: -------------------------------------------------------------------------------- 1 | use serde::de::Deserialize; 2 | use serde::ser::Serialize; 3 | 4 | use super::bridge::OneshotBridge; 5 | use super::traits::Oneshot; 6 | use super::worker::OneshotWorker; 7 | use crate::actor::WorkerSpawner; 8 | use crate::codec::{Bincode, Codec}; 9 | 10 | /// A spawner to create oneshot workers. 11 | #[derive(Debug, Default)] 12 | pub struct OneshotSpawner 13 | where 14 | N: Oneshot + 'static, 15 | CODEC: Codec, 16 | { 17 | inner: WorkerSpawner, CODEC>, 18 | } 19 | 20 | impl OneshotSpawner 21 | where 22 | N: Oneshot + 'static, 23 | CODEC: Codec, 24 | { 25 | /// Creates a [OneshotSpawner]. 26 | pub const fn new() -> Self { 27 | Self { 28 | inner: WorkerSpawner::, CODEC>::new(), 29 | } 30 | } 31 | 32 | /// Sets a new message encoding. 33 | pub const fn encoding(&self) -> OneshotSpawner 34 | where 35 | C: Codec, 36 | { 37 | OneshotSpawner { 38 | inner: WorkerSpawner::, C>::new(), 39 | } 40 | } 41 | 42 | /// Spawns an Oneshot Worker. 43 | pub fn spawn(mut self, path: &str) -> OneshotBridge 44 | where 45 | N::Input: Serialize + for<'de> Deserialize<'de>, 46 | N::Output: Serialize + for<'de> Deserialize<'de>, 47 | { 48 | let rx = OneshotBridge::register_callback(&mut self.inner); 49 | 50 | let inner = self.inner.spawn(path); 51 | 52 | OneshotBridge::new(inner, rx) 53 | } 54 | 55 | /// Spawns an Oneshot Worker with a loader shim script. 56 | pub fn spawn_with_loader(mut self, loader_path: &str) -> OneshotBridge 57 | where 58 | N::Input: Serialize + for<'de> Deserialize<'de>, 59 | N::Output: Serialize + for<'de> Deserialize<'de>, 60 | { 61 | let rx = OneshotBridge::register_callback(&mut self.inner); 62 | 63 | let inner = self.inner.spawn_with_loader(loader_path); 64 | 65 | OneshotBridge::new(inner, rx) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /crates/worker/src/oneshot/traits.rs: -------------------------------------------------------------------------------- 1 | use std::future::Future; 2 | 3 | /// A future-based worker that for each input, one output is produced. 4 | pub trait Oneshot: Future { 5 | /// Incoming message type. 6 | type Input; 7 | 8 | /// Creates an oneshot worker. 9 | fn create(input: Self::Input) -> Self; 10 | } 11 | -------------------------------------------------------------------------------- /crates/worker/src/oneshot/worker.rs: -------------------------------------------------------------------------------- 1 | use super::traits::Oneshot; 2 | use crate::actor::{HandlerId, Worker, WorkerDestroyHandle, WorkerScope}; 3 | 4 | pub(crate) enum Message 5 | where 6 | T: Oneshot, 7 | { 8 | Finished { 9 | handler_id: HandlerId, 10 | output: T::Output, 11 | }, 12 | } 13 | 14 | pub(crate) struct OneshotWorker 15 | where 16 | T: 'static + Oneshot, 17 | { 18 | running_tasks: usize, 19 | destruct_handle: Option>, 20 | } 21 | 22 | impl Worker for OneshotWorker 23 | where 24 | T: 'static + Oneshot, 25 | { 26 | type Input = T::Input; 27 | type Message = Message; 28 | type Output = T::Output; 29 | 30 | fn create(_scope: &WorkerScope) -> Self { 31 | Self { 32 | running_tasks: 0, 33 | destruct_handle: None, 34 | } 35 | } 36 | 37 | fn update(&mut self, scope: &WorkerScope, msg: Self::Message) { 38 | let Message::Finished { handler_id, output } = msg; 39 | 40 | self.running_tasks -= 1; 41 | 42 | scope.respond(handler_id, output); 43 | 44 | if self.running_tasks == 0 { 45 | self.destruct_handle = None; 46 | } 47 | } 48 | 49 | fn received(&mut self, scope: &WorkerScope, input: Self::Input, handler_id: HandlerId) { 50 | self.running_tasks += 1; 51 | 52 | scope.send_future(async move { 53 | let output = T::create(input).await; 54 | 55 | Message::Finished { handler_id, output } 56 | }); 57 | } 58 | 59 | fn destroy(&mut self, _scope: &WorkerScope, destruct: WorkerDestroyHandle) { 60 | if self.running_tasks > 0 { 61 | self.destruct_handle = Some(destruct); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /crates/worker/src/reactor/messages.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | /// The Bridge Input. 4 | #[derive(Debug, Serialize, Deserialize)] 5 | pub(crate) enum ReactorInput { 6 | /// An input message. 7 | Input(I), 8 | } 9 | 10 | /// The Bridge Output. 11 | #[derive(Debug, Serialize, Deserialize)] 12 | pub enum ReactorOutput { 13 | /// An output message has been received. 14 | Output(O), 15 | /// Reactor for current bridge has exited. 16 | Finish, 17 | } 18 | -------------------------------------------------------------------------------- /crates/worker/src/reactor/mod.rs: -------------------------------------------------------------------------------- 1 | //! A future-based worker that can consume many inputs and produce many outputs. 2 | //! 3 | //! ## Example 4 | //! 5 | //! ```rust, no_run 6 | //! use gloo_worker::reactor::{reactor, ReactorScope}; 7 | //! use gloo_worker::Spawnable; 8 | //! use futures::{sink::SinkExt, StreamExt}; 9 | //! 10 | //! #[reactor] 11 | //! async fn SquaredOnDemand(mut scope: ReactorScope) { 12 | //! while let Some(m) = scope.next().await { 13 | //! if scope.send(m.pow(2)).await.is_err() { 14 | //! break; 15 | //! } 16 | //! } 17 | //! } 18 | //! # async { 19 | //! let mut bridge = SquaredOnDemand::spawner().spawn("..."); 20 | //! 21 | //! bridge.send_input(2); 22 | //! 23 | //! assert_eq!(bridge.next().await, Some(4)); 24 | //! assert_eq!(bridge.next().await, None); 25 | //! # }; 26 | //! ``` 27 | 28 | mod bridge; 29 | mod messages; 30 | mod registrar; 31 | mod scope; 32 | mod spawner; 33 | mod traits; 34 | mod worker; 35 | 36 | pub use bridge::{ReactorBridge, ReactorBridgeSinkError}; 37 | pub use registrar::ReactorRegistrar; 38 | pub use scope::{ReactorScope, ReactorScoped}; 39 | pub use spawner::ReactorSpawner; 40 | pub use traits::Reactor; 41 | 42 | /// Creates a reactor worker. 43 | /// 44 | /// See [module level documentation](self) for more information. 45 | #[doc(inline)] 46 | #[cfg(feature = "futures")] 47 | pub use gloo_worker_macros::reactor; 48 | -------------------------------------------------------------------------------- /crates/worker/src/reactor/registrar.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use serde::de::Deserialize; 4 | use serde::ser::Serialize; 5 | 6 | use super::scope::ReactorScoped; 7 | use super::traits::Reactor; 8 | use super::worker::ReactorWorker; 9 | use crate::actor::WorkerRegistrar; 10 | use crate::codec::{Bincode, Codec}; 11 | use crate::traits::Registrable; 12 | 13 | /// A registrar for reactor workers. 14 | pub struct ReactorRegistrar 15 | where 16 | R: Reactor + 'static, 17 | CODEC: Codec + 'static, 18 | { 19 | inner: WorkerRegistrar, CODEC>, 20 | } 21 | 22 | impl Default for ReactorRegistrar 23 | where 24 | R: Reactor + 'static, 25 | CODEC: Codec + 'static, 26 | { 27 | fn default() -> Self { 28 | Self::new() 29 | } 30 | } 31 | 32 | impl ReactorRegistrar 33 | where 34 | R: Reactor + 'static, 35 | CODEC: Codec + 'static, 36 | { 37 | /// Creates a new reactor registrar. 38 | pub fn new() -> Self { 39 | Self { 40 | inner: ReactorWorker::::registrar().encoding::(), 41 | } 42 | } 43 | 44 | /// Sets the encoding. 45 | pub fn encoding(&self) -> ReactorRegistrar 46 | where 47 | C: Codec + 'static, 48 | { 49 | ReactorRegistrar { 50 | inner: self.inner.encoding::(), 51 | } 52 | } 53 | 54 | /// Registers the worker. 55 | pub fn register(&self) 56 | where 57 | ::Input: Serialize + for<'de> Deserialize<'de>, 58 | ::Output: Serialize + for<'de> Deserialize<'de>, 59 | { 60 | self.inner.register() 61 | } 62 | } 63 | 64 | impl fmt::Debug for ReactorRegistrar 65 | where 66 | R: Reactor + 'static, 67 | CODEC: Codec + 'static, 68 | { 69 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 70 | f.debug_struct("ReactorRegistrar<_>").finish() 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /crates/worker/src/reactor/scope.rs: -------------------------------------------------------------------------------- 1 | use std::convert::Infallible; 2 | use std::fmt; 3 | use std::pin::Pin; 4 | 5 | use futures::stream::{FusedStream, Stream}; 6 | use futures::task::{Context, Poll}; 7 | use futures::Sink; 8 | 9 | /// A handle to communicate with bridges. 10 | pub struct ReactorScope { 11 | input_stream: Pin>>, 12 | output_sink: Pin>>, 13 | } 14 | 15 | impl fmt::Debug for ReactorScope { 16 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 17 | f.debug_struct("ReactorScope<_>").finish() 18 | } 19 | } 20 | 21 | impl Stream for ReactorScope { 22 | type Item = I; 23 | 24 | #[inline(always)] 25 | fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 26 | Pin::new(&mut self.input_stream).poll_next(cx) 27 | } 28 | 29 | #[inline(always)] 30 | fn size_hint(&self) -> (usize, Option) { 31 | self.input_stream.size_hint() 32 | } 33 | } 34 | 35 | impl FusedStream for ReactorScope { 36 | #[inline(always)] 37 | fn is_terminated(&self) -> bool { 38 | self.input_stream.is_terminated() 39 | } 40 | } 41 | 42 | /// A helper trait to extract the input and output type from a [ReactorStream]. 43 | pub trait ReactorScoped: Stream + FusedStream { 44 | /// The Input Message. 45 | type Input; 46 | /// The Output Message. 47 | type Output; 48 | 49 | /// Creates a ReactorReceiver. 50 | fn new(input_stream: IS, output_sink: OS) -> Self 51 | where 52 | IS: Stream + FusedStream + 'static, 53 | OS: Sink + 'static; 54 | } 55 | 56 | impl ReactorScoped for ReactorScope { 57 | type Input = I; 58 | type Output = O; 59 | 60 | #[inline] 61 | fn new(input_stream: IS, output_sink: OS) -> Self 62 | where 63 | IS: Stream + FusedStream + 'static, 64 | OS: Sink + 'static, 65 | { 66 | Self { 67 | input_stream: Box::pin(input_stream), 68 | output_sink: Box::pin(output_sink), 69 | } 70 | } 71 | } 72 | 73 | impl Sink for ReactorScope { 74 | type Error = Infallible; 75 | 76 | fn start_send(mut self: Pin<&mut Self>, item: O) -> Result<(), Self::Error> { 77 | Pin::new(&mut self.output_sink).start_send(item) 78 | } 79 | 80 | fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 81 | Pin::new(&mut self.output_sink).poll_close(cx) 82 | } 83 | 84 | fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 85 | Pin::new(&mut self.output_sink).poll_flush(cx) 86 | } 87 | 88 | fn poll_ready(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 89 | Pin::new(&mut self.output_sink).poll_flush(cx) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /crates/worker/src/reactor/spawner.rs: -------------------------------------------------------------------------------- 1 | use serde::de::Deserialize; 2 | use serde::ser::Serialize; 3 | 4 | use super::bridge::ReactorBridge; 5 | use super::scope::ReactorScoped; 6 | use super::traits::Reactor; 7 | use super::worker::ReactorWorker; 8 | use crate::actor::WorkerSpawner; 9 | use crate::codec::{Bincode, Codec}; 10 | 11 | /// A spawner to create oneshot workers. 12 | #[derive(Debug, Default)] 13 | pub struct ReactorSpawner 14 | where 15 | R: Reactor + 'static, 16 | CODEC: Codec, 17 | { 18 | inner: WorkerSpawner, CODEC>, 19 | } 20 | 21 | impl ReactorSpawner 22 | where 23 | R: Reactor + 'static, 24 | CODEC: Codec, 25 | { 26 | /// Creates a ReactorSpawner. 27 | pub const fn new() -> Self { 28 | Self { 29 | inner: WorkerSpawner::, CODEC>::new(), 30 | } 31 | } 32 | 33 | /// Sets a new message encoding. 34 | pub const fn encoding(&self) -> ReactorSpawner 35 | where 36 | C: Codec, 37 | { 38 | ReactorSpawner { 39 | inner: WorkerSpawner::, C>::new(), 40 | } 41 | } 42 | 43 | /// Spawns a reactor worker. 44 | pub fn spawn(mut self, path: &str) -> ReactorBridge 45 | where 46 | ::Input: Serialize + for<'de> Deserialize<'de>, 47 | ::Output: Serialize + for<'de> Deserialize<'de>, 48 | { 49 | let rx = ReactorBridge::register_callback(&mut self.inner); 50 | 51 | let inner = self.inner.spawn(path); 52 | 53 | ReactorBridge::new(inner, rx) 54 | } 55 | 56 | /// Spawns a Reactor Worker with a loader shim script. 57 | pub fn spawn_with_loader(mut self, loader_path: &str) -> ReactorBridge 58 | where 59 | ::Input: Serialize + for<'de> Deserialize<'de>, 60 | ::Output: Serialize + for<'de> Deserialize<'de>, 61 | { 62 | let rx = ReactorBridge::register_callback(&mut self.inner); 63 | 64 | let inner = self.inner.spawn_with_loader(loader_path); 65 | 66 | ReactorBridge::new(inner, rx) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /crates/worker/src/reactor/traits.rs: -------------------------------------------------------------------------------- 1 | use std::future::Future; 2 | 3 | use super::scope::ReactorScoped; 4 | 5 | /// A reactor worker. 6 | pub trait Reactor: Future { 7 | /// The Reactor Scope 8 | type Scope: ReactorScoped; 9 | 10 | /// Creates a reactor worker. 11 | fn create(scope: Self::Scope) -> Self; 12 | } 13 | -------------------------------------------------------------------------------- /crates/worker/src/traits.rs: -------------------------------------------------------------------------------- 1 | /// A Worker that can be spawned by a spawner. 2 | pub trait Spawnable { 3 | /// Spawner Type. 4 | type Spawner; 5 | 6 | /// Creates a spawner. 7 | fn spawner() -> Self::Spawner; 8 | } 9 | 10 | /// A trait to enable public workers being registered in a web worker. 11 | pub trait Registrable { 12 | /// Registrar Type. 13 | type Registrar; 14 | 15 | /// Creates a registrar for the current worker. 16 | fn registrar() -> Self::Registrar; 17 | } 18 | -------------------------------------------------------------------------------- /examples/clock/.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | -------------------------------------------------------------------------------- /examples/clock/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example-clock" 3 | version = "0.1.0" 4 | authors = ["Rust and WebAssembly Working Group"] 5 | edition = "2021" 6 | publish = false 7 | rust-version = "1.64" 8 | 9 | [lib] 10 | crate-type = ["cdylib"] 11 | 12 | [dependencies] 13 | gloo = { path = "../..", features = ["futures"] } 14 | wasm-bindgen = "0.2.54" 15 | wasm-bindgen-futures = "0.4.4" 16 | futures-util = "0.3" 17 | chrono = { version = "0.4.10", features = ["wasmbind"] } 18 | console_error_panic_hook = "0.1.6" 19 | 20 | [dependencies.web-sys] 21 | version = "0.3.19" 22 | features = ["console", "Window", "Document", "Element", "Node"] 23 | -------------------------------------------------------------------------------- /examples/clock/README.md: -------------------------------------------------------------------------------- 1 | # Clock example 2 | 3 | This is a simple example showcasing the Gloo timers. 4 | 5 | First, [install wasm-pack](https://rustwasm.github.io/wasm-pack/installer/) if needed. 6 | 7 | Then build the clock example by running `wasm-pack build --target no-modules` and open your browser to load `index.html`. -------------------------------------------------------------------------------- /examples/clock/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Clock 7 | 8 | 9 |

10 |         
11 |         
14 |     
15 | 
16 | 


--------------------------------------------------------------------------------
/examples/clock/src/lib.rs:
--------------------------------------------------------------------------------
 1 | use chrono::Timelike;
 2 | use futures_util::{future::ready, stream::StreamExt};
 3 | use gloo::timers::future::IntervalStream;
 4 | use wasm_bindgen::prelude::*;
 5 | use wasm_bindgen_futures::spawn_local;
 6 | 
 7 | #[wasm_bindgen(start)]
 8 | pub fn main() {
 9 |     console_error_panic_hook::set_once();
10 | 
11 |     let document = web_sys::window().unwrap_throw().document().unwrap_throw();
12 | 
13 |     let el = document.get_element_by_id("clock").unwrap_throw();
14 | 
15 |     // render the date, then set it to re-render every second.
16 |     render_date(&el);
17 | 
18 |     spawn_local(async move {
19 |         IntervalStream::new(1_000)
20 |             .for_each(|_| {
21 |                 render_date(&el);
22 |                 ready(())
23 |             })
24 |             .await;
25 |     });
26 | }
27 | 
28 | /// Render the date with the `:` flashing on and off every second into `el`.
29 | fn render_date(el: &web_sys::Element) {
30 |     // print the current date
31 |     let date = chrono::Local::now();
32 | 
33 |     let format_str = if date.second() % 2 == 0 {
34 |         "%Y-%m-%d %H %M"
35 |     } else {
36 |         "%Y-%m-%d %H:%M"
37 |     };
38 | 
39 |     let date_str = date.format(format_str).to_string();
40 | 
41 |     // Set the contents of `el` to our date string
42 |     el.set_text_content(Some(&date_str));
43 | }
44 | 


--------------------------------------------------------------------------------
/examples/file-hash/Cargo.toml:
--------------------------------------------------------------------------------
 1 | [package]
 2 | name = "example-file-hash"
 3 | version = "0.1.0"
 4 | edition = "2021"
 5 | authors = ["Rust and WebAssembly Working Group"]
 6 | publish = false
 7 | 
 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 9 | 
10 | [dependencies]
11 | # We use the gloo-worker directly to avoid dependency conflicts as Yew also uses gloo.
12 | gloo-worker = { path = "../../crates/worker" }
13 | serde = "1.0.163"
14 | web-sys = { version = "0.3.63", features = ["File", "Blob", "ReadableStream"] }
15 | wasm-bindgen-futures = { version = "0.4" }
16 | wasm-streams = "0.3.0"
17 | wasm-bindgen = "0.2.86"
18 | futures = "0.3.28"
19 | sha2 = "0.10.8"
20 | console_error_panic_hook = "0.1.7"
21 | yew = { version = "0.20.0", features = ["csr"] }
22 | serde-wasm-bindgen = "0.6.0"
23 | js-sys = "0.3.63"
24 | hex = "0.4.3"
25 | 


--------------------------------------------------------------------------------
/examples/file-hash/README.md:
--------------------------------------------------------------------------------
1 | # File Hash example
2 | 
3 | This is a simple example showcasing the Gloo Workers custom codec and how to send transferrable types to workers.
4 | 
5 | You can run this example with `trunk serve --open`
6 | 


--------------------------------------------------------------------------------
/examples/file-hash/index.html:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 |     
 4 |         
 5 |         
 6 |         File Hasher
 7 | 
 8 |         
 9 |         
10 |     
11 | 
12 | 


--------------------------------------------------------------------------------
/examples/file-hash/src/bin/example_file_hash_app.rs:
--------------------------------------------------------------------------------
 1 | use std::ops::Deref;
 2 | 
 3 | use example_file_hash::codec::TransferrableCodec;
 4 | use example_file_hash::{HashInput, HashWorker};
 5 | use gloo_worker::Spawnable;
 6 | use web_sys::HtmlInputElement;
 7 | use yew::prelude::*;
 8 | 
 9 | #[function_component]
10 | fn App() -> Html {
11 |     let result = use_state(|| None);
12 |     let calculating = use_state(|| false);
13 | 
14 |     let worker = {
15 |         let calculating = calculating.clone();
16 |         let result = result.clone();
17 | 
18 |         use_memo(
19 |             move |_| {
20 |                 HashWorker::spawner()
21 |                     .callback(move |o| {
22 |                         calculating.set(false);
23 |                         result.set(Some(o.hash));
24 |                     })
25 |                     .encoding::()
26 |                     .spawn_with_loader("/example_file_hash_worker_loader.js")
27 |             },
28 |             (),
29 |         )
30 |     };
31 | 
32 |     let on_choose_file = {
33 |         let calculating = calculating.clone();
34 |         let result = result.clone();
35 |         use_callback(
36 |             move |e: Event, _i| {
37 |                 let el: HtmlInputElement = e.target_unchecked_into();
38 |                 if let Some(f) = el.files().and_then(|m| m.item(0)) {
39 |                     calculating.set(true);
40 |                     result.set(None);
41 |                     let input = HashInput { file: f };
42 |                     worker.send(input);
43 |                 }
44 |             },
45 |             (),
46 |         )
47 |     };
48 | 
49 |     html! {
50 |         
51 |

{"To calculate file hash, please select a file below:"}

52 |

53 | if let Some(m) = result.deref().clone() { 54 |

{"SHA256: "}{&m}

55 | } 56 | if *calculating { 57 |

{"Calculating..."}

58 | } 59 |
60 | } 61 | } 62 | 63 | fn main() { 64 | console_error_panic_hook::set_once(); 65 | yew::Renderer::::new().render(); 66 | } 67 | -------------------------------------------------------------------------------- /examples/file-hash/src/bin/example_file_hash_worker.rs: -------------------------------------------------------------------------------- 1 | use example_file_hash::codec::TransferrableCodec; 2 | use example_file_hash::HashWorker; 3 | 4 | use gloo_worker::Registrable; 5 | 6 | fn main() { 7 | console_error_panic_hook::set_once(); 8 | HashWorker::registrar() 9 | .encoding::() 10 | .register(); 11 | } 12 | -------------------------------------------------------------------------------- /examples/file-hash/src/codec.rs: -------------------------------------------------------------------------------- 1 | use gloo_worker::Codec; 2 | 3 | pub struct TransferrableCodec {} 4 | 5 | // This codec implementation relys on some internal implementation details about gloo worker message types. 6 | // Fields marked with `#[serde(with = "serde_wasm_bindgen::preserve")]` will be passed as-is. 7 | impl Codec for TransferrableCodec { 8 | fn encode(input: I) -> wasm_bindgen::JsValue 9 | where 10 | I: serde::Serialize, 11 | { 12 | serde_wasm_bindgen::to_value(&input).expect("failed to encode") 13 | } 14 | 15 | fn decode(input: wasm_bindgen::JsValue) -> O 16 | where 17 | O: for<'de> serde::Deserialize<'de>, 18 | { 19 | serde_wasm_bindgen::from_value(input).expect("failed to decode") 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/file-hash/src/lib.rs: -------------------------------------------------------------------------------- 1 | use futures::TryStreamExt; 2 | use gloo_worker::{HandlerId, Worker, WorkerScope}; 3 | use js_sys::Uint8Array; 4 | use serde::{Deserialize, Serialize}; 5 | use sha2::{Digest, Sha256}; 6 | use wasm_bindgen::prelude::*; 7 | use wasm_bindgen_futures::spawn_local; 8 | use wasm_streams::ReadableStream; 9 | 10 | pub mod codec; 11 | 12 | #[derive(Serialize, Deserialize)] 13 | pub struct HashInput { 14 | #[serde(with = "serde_wasm_bindgen::preserve")] 15 | pub file: web_sys::File, 16 | } 17 | 18 | #[derive(Serialize, Deserialize)] 19 | pub struct HashOutput { 20 | pub hash: String, 21 | } 22 | 23 | pub struct HashWorker {} 24 | 25 | impl Worker for HashWorker { 26 | type Input = HashInput; 27 | type Output = HashOutput; 28 | type Message = (); 29 | 30 | fn create(_scope: &WorkerScope) -> Self { 31 | Self {} 32 | } 33 | 34 | fn connected(&mut self, _scope: &WorkerScope, _id: HandlerId) {} 35 | 36 | fn update(&mut self, _scope: &WorkerScope, _msg: Self::Message) {} 37 | 38 | fn received(&mut self, scope: &WorkerScope, msg: Self::Input, id: HandlerId) { 39 | let scope = scope.clone(); 40 | 41 | spawn_local(async move { 42 | // This is a demonstration of codec and how to pass transferrable types to worker. 43 | // 44 | // If you are trying to calculate hashes in browsers for your application, 45 | // please consider using subtle crypto. 46 | // 47 | // This example does not use subtle crypto 48 | // because calculating hashes with subtle crypto doesn't need to be sent to a worker. 49 | let mut hasher = Sha256::new(); 50 | 51 | // We assume that this file is big and cannot be loaded into the memory in one chunk. 52 | // So we process this as a stream. 53 | let mut s = ReadableStream::from_raw(msg.file.stream().unchecked_into()).into_stream(); 54 | 55 | while let Some(chunk) = s.try_next().await.unwrap() { 56 | hasher.update(chunk.unchecked_into::().to_vec()); 57 | } 58 | 59 | let hash = hasher.finalize(); 60 | 61 | let hash_hex = hex::encode(hash); 62 | 63 | scope.respond(id, HashOutput { hash: hash_hex }); 64 | }); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /examples/history-wasi/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example-history-wasi" 3 | version = "0.1.0" 4 | authors = ["Rust and WebAssembly Working Group"] 5 | edition = "2021" 6 | publish = false 7 | rust-version = "1.64" 8 | 9 | [dependencies] 10 | gloo = { path = "../..", default-features = false, features = ["history"] } 11 | -------------------------------------------------------------------------------- /examples/history-wasi/README.md: -------------------------------------------------------------------------------- 1 | # History example on WASI 2 | 3 | This is a simple example showcasing the Gloo History on WASI. 4 | 5 | You can run this example with: 6 | 7 | ```bash 8 | cargo build --manifest-path examples/history-wasi/Cargo.toml --target wasm32-wasi 9 | wasmtime examples/history-wasi/target/wasm32-wasi/debug/example-history-wasi.wasm 10 | ``` 11 | -------------------------------------------------------------------------------- /examples/history-wasi/src/main.rs: -------------------------------------------------------------------------------- 1 | use gloo::history::{History, MemoryHistory}; 2 | 3 | fn main() { 4 | let history = MemoryHistory::new(); 5 | println!("Current path: {}", history.location().path()); 6 | assert_eq!(history.location().path(), "/"); 7 | 8 | history.push("/home"); 9 | println!("Current path: {}", history.location().path()); 10 | assert_eq!(history.location().path(), "/home"); 11 | 12 | history.push("/about"); 13 | println!("Current path: {}", history.location().path()); 14 | assert_eq!(history.location().path(), "/about"); 15 | 16 | history.push("/contact"); 17 | println!("Current path: {}", history.location().path()); 18 | assert_eq!(history.location().path(), "/contact"); 19 | 20 | history.go(-2); 21 | println!("Current path: {}", history.location().path()); 22 | assert_eq!(history.location().path(), "/home"); 23 | } 24 | -------------------------------------------------------------------------------- /examples/markdown/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example-markdown" 3 | version = "0.1.0" 4 | authors = ["Rust and WebAssembly Working Group"] 5 | edition = "2021" 6 | publish = false 7 | rust-version = "1.64" 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [dependencies] 12 | pulldown-cmark = { version = "0.9.1", default-features = false } 13 | gloo = { path = "../..", features = ["futures"] } 14 | 15 | console_error_panic_hook = "0.1.7" 16 | wasm-bindgen = "0.2" 17 | wasm-bindgen-futures = { version = "0.4" } 18 | js-sys = "0.3" 19 | wasm-bindgen-test = "0.3.4" 20 | futures = "0.3" 21 | 22 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies] 23 | tokio = { version = "1", features = ["full"] } 24 | warp = "0.3" 25 | -------------------------------------------------------------------------------- /examples/markdown/README.md: -------------------------------------------------------------------------------- 1 | # Markdown example 2 | 3 | This is a simple example showcasing the Gloo Workers. 4 | 5 | You can run this example with `trunk serve --open` 6 | -------------------------------------------------------------------------------- /examples/markdown/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Markdown 8 | 9 | 10 | 11 | 12 | 13 | 14 |
Rendering...
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/markdown/src/bin/example_markdown_app.rs: -------------------------------------------------------------------------------- 1 | use example_markdown::MarkdownWorker; 2 | use wasm_bindgen::prelude::*; 3 | 4 | use gloo::utils::document; 5 | use gloo::worker::Spawnable; 6 | 7 | use wasm_bindgen_futures::spawn_local; 8 | 9 | static MARKDOWN_CONTENT: &str = r#" 10 | ## Hello 11 | 12 | This content is *rendered* by a **web worker**. 13 | 14 | "#; 15 | 16 | fn main() { 17 | console_error_panic_hook::set_once(); 18 | 19 | let root = document() 20 | .query_selector("#root") 21 | .ok() 22 | .flatten() 23 | .expect_throw("failed to query root element"); 24 | 25 | let mut bridge = 26 | MarkdownWorker::spawner().spawn_with_loader("/example_markdown_worker_loader.js"); 27 | 28 | spawn_local(async move { 29 | let content = bridge.run(MARKDOWN_CONTENT.to_owned()).await; 30 | root.set_inner_html(&content); 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /examples/markdown/src/bin/example_markdown_test_server.rs: -------------------------------------------------------------------------------- 1 | #![cfg(not(target_arch = "wasm32"))] 2 | 3 | use warp::reply::with_header; 4 | use warp::Filter; 5 | 6 | // This server is purely to faclitate testing. 7 | // Please read the instruction in lib.rs about how to run tests. 8 | // 9 | // If you are not running tests, you can simply ignore this file. 10 | #[tokio::main] 11 | async fn main() { 12 | let dir = std::env::args().nth(1).expect("expected a target dir."); 13 | 14 | let route = warp::fs::dir(dir) 15 | .with( 16 | // We need a server that serves the request with cross origin resource sharing. 17 | warp::cors() 18 | .allow_method("GET") 19 | .allow_method("HEAD") 20 | .allow_method("OPTIONS") 21 | .allow_any_origin(), 22 | ) 23 | .map(|m| with_header(m, "cross-origin-resource-policy", "cross-origin")); 24 | 25 | println!("Test server is running at: http://127.0.0.1:9999/"); 26 | 27 | warp::serve(route).run(([127, 0, 0, 1], 9999)).await; 28 | } 29 | -------------------------------------------------------------------------------- /examples/markdown/src/bin/example_markdown_worker.rs: -------------------------------------------------------------------------------- 1 | use example_markdown::MarkdownWorker; 2 | 3 | use gloo::worker::Registrable; 4 | 5 | fn main() { 6 | console_error_panic_hook::set_once(); 7 | 8 | MarkdownWorker::registrar().register(); 9 | } 10 | -------------------------------------------------------------------------------- /examples/markdown/src/lib.rs: -------------------------------------------------------------------------------- 1 | use gloo::worker::oneshot::oneshot; 2 | use pulldown_cmark::{html, Parser}; 3 | 4 | #[oneshot] 5 | pub async fn MarkdownWorker(input: String) -> String { 6 | let parser = Parser::new(&input); 7 | 8 | let mut output = String::new(); 9 | html::push_html(&mut output, parser); 10 | 11 | output 12 | } 13 | 14 | // wasm-bindgen-test does not support serving additional files 15 | // and trunk serve does not support CORS. 16 | // 17 | // To run tests against web workers, a test server with CORS support needs to be set up 18 | // with the following commands: 19 | // 20 | // trunk build examples/markdown/index.html 21 | // cargo run -p example-markdown --bin example_markdown_test_server -- examples/markdown/dist 22 | // 23 | // wasm-pack test --headless --firefox examples/markdown 24 | #[cfg(test)] 25 | mod tests { 26 | use super::*; 27 | 28 | use gloo::worker::Spawnable; 29 | use wasm_bindgen_test::*; 30 | 31 | wasm_bindgen_test_configure!(run_in_browser); 32 | 33 | static MARKDOWN_CONTENT: &str = r#" 34 | ## Hello 35 | 36 | This content is *rendered* by a **web worker**. 37 | 38 | "#; 39 | 40 | #[wasm_bindgen_test] 41 | async fn markdown_worker_works() { 42 | let mut bridge = 43 | MarkdownWorker::spawner().spawn("http://127.0.0.1:9999/example_markdown_worker.js"); 44 | 45 | let content = bridge.run(MARKDOWN_CONTENT.to_owned()).await; 46 | 47 | assert_eq!( 48 | &content, 49 | r#"

Hello

50 |

This content is rendered by a web worker.

51 | "# 52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /examples/prime/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example-prime" 3 | version = "0.1.0" 4 | edition = "2021" 5 | rust-version = "1.64" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | futures = "0.3.25" 11 | gloo = { path = "../..", features = ["futures"] } 12 | primes = "0.3.0" 13 | wasm-bindgen-futures = "0.4.33" 14 | console_error_panic_hook = "0.1.7" 15 | serde = { version = "1.0.147", features = ["derive"] } 16 | 17 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies] 18 | tokio = { version = "1", features = ["full"] } 19 | warp = "0.3" 20 | 21 | [dev-dependencies] 22 | wasm-bindgen-test = "0.3.36" 23 | -------------------------------------------------------------------------------- /examples/prime/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Prime Calculator 8 | 9 | 10 | 11 | 18 | 19 | 20 | 21 |

Find Prime

22 |

This page demonstrates how to calculate prime in a web worker.

23 | 24 |
25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /examples/prime/src/bin/example_prime_app.rs: -------------------------------------------------------------------------------- 1 | use std::cell::{Cell, RefCell}; 2 | use std::rc::Rc; 3 | use std::time::Duration; 4 | 5 | use example_prime::{ControlSignal, Prime}; 6 | use futures::sink::SinkExt; 7 | use futures::stream::StreamExt; 8 | use gloo::timers::future::sleep; 9 | use gloo::worker::Spawnable; 10 | use wasm_bindgen_futures::spawn_local; 11 | 12 | fn main() { 13 | console_error_panic_hook::set_once(); 14 | 15 | let start_btn = gloo::utils::body() 16 | .query_selector("#start-btn") 17 | .unwrap() 18 | .unwrap(); 19 | 20 | let result_div = gloo::utils::body() 21 | .query_selector("#result") 22 | .unwrap() 23 | .unwrap(); 24 | 25 | let started = Rc::new(Cell::new(false)); 26 | 27 | let (bridge_sink, mut bridge_stream) = Prime::spawner() 28 | .spawn_with_loader("/example_prime_worker_loader.js") 29 | .split(); 30 | 31 | { 32 | let result_div = result_div.clone(); 33 | spawn_local(async move { 34 | while let Some(m) = bridge_stream.next().await { 35 | let el = gloo::utils::document().create_element("div").unwrap(); 36 | el.set_attribute("class", "result-item").unwrap(); 37 | el.set_text_content(Some(&m.to_string())); 38 | 39 | result_div.append_child(&el).unwrap(); 40 | 41 | sleep(Duration::ZERO).await; 42 | } 43 | }); 44 | } 45 | 46 | let bridge_sink = Rc::new(RefCell::new(bridge_sink)); 47 | 48 | let listener = gloo::events::EventListener::new(&start_btn.clone(), "click", move |_| { 49 | let bridge_sink = bridge_sink.clone(); 50 | 51 | if started.get() { 52 | start_btn.set_text_content(Some("Start")); 53 | spawn_local(async move { 54 | let mut bridge_sink = bridge_sink.borrow_mut(); 55 | bridge_sink.send(ControlSignal::Stop).await.unwrap(); 56 | }); 57 | 58 | started.set(false); 59 | } else { 60 | start_btn.set_text_content(Some("Stop")); 61 | result_div.set_inner_html(""); 62 | 63 | spawn_local(async move { 64 | let mut bridge_sink = bridge_sink.borrow_mut(); 65 | bridge_sink.send(ControlSignal::Start).await.unwrap(); 66 | }); 67 | 68 | started.set(true); 69 | } 70 | }); 71 | 72 | spawn_local(async move { 73 | let _listener = listener; 74 | 75 | // We create a loop so that listeners can be held for forever. 76 | loop { 77 | sleep(Duration::from_secs(3600)).await; 78 | } 79 | }); 80 | } 81 | -------------------------------------------------------------------------------- /examples/prime/src/bin/example_prime_test_server.rs: -------------------------------------------------------------------------------- 1 | #![cfg(not(target_arch = "wasm32"))] 2 | 3 | use warp::reply::with_header; 4 | use warp::Filter; 5 | 6 | // This server is purely to faclitate testing. 7 | // Please read the instruction in lib.rs about how to run tests. 8 | // 9 | // If you are not running tests, you can simply ignore this file. 10 | #[tokio::main] 11 | async fn main() { 12 | let dir = std::env::args().nth(1).expect("expected a target dir."); 13 | 14 | let route = warp::fs::dir(dir) 15 | .with( 16 | // We need a server that serves the request with cross origin resource sharing. 17 | warp::cors() 18 | .allow_method("GET") 19 | .allow_method("HEAD") 20 | .allow_method("OPTIONS") 21 | .allow_any_origin(), 22 | ) 23 | .map(|m| with_header(m, "cross-origin-resource-policy", "cross-origin")); 24 | 25 | println!("Test server is running at: http://127.0.0.1:9999/"); 26 | 27 | warp::serve(route).run(([127, 0, 0, 1], 9999)).await; 28 | } 29 | -------------------------------------------------------------------------------- /examples/prime/src/bin/example_prime_worker.rs: -------------------------------------------------------------------------------- 1 | use example_prime::Prime; 2 | use gloo::worker::Registrable; 3 | 4 | fn main() { 5 | console_error_panic_hook::set_once(); 6 | 7 | Prime::registrar().register(); 8 | } 9 | -------------------------------------------------------------------------------- /examples/prime/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use futures::{FutureExt, StreamExt}; 4 | use gloo::timers::future::sleep; 5 | use gloo::worker::reactor::{reactor, ReactorScope}; 6 | 7 | use futures::sink::SinkExt; 8 | use serde::{Deserialize, Serialize}; 9 | 10 | #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] 11 | pub enum ControlSignal { 12 | Start, 13 | Stop, 14 | } 15 | 16 | #[reactor] 17 | pub async fn Prime(mut scope: ReactorScope) { 18 | while let Some(m) = scope.next().await { 19 | if m == ControlSignal::Start { 20 | 'inner: for i in 1.. { 21 | // This is not the most efficient way to calculate prime, 22 | // but this example is here to demonstrate how primes can be 23 | // sent to the application in an ascending order. 24 | if primes::is_prime(i) { 25 | scope.send(i).await.unwrap(); 26 | } 27 | 28 | futures::select! { 29 | m = scope.next() => { 30 | if m == Some(ControlSignal::Stop) { 31 | break 'inner; 32 | } 33 | }, 34 | _ = sleep(Duration::from_millis(100)).fuse() => {}, 35 | } 36 | } 37 | } 38 | } 39 | } 40 | // wasm-bindgen-test does not support serving additional files 41 | // and trunk serve does not support CORS. 42 | // 43 | // To run tests against web workers, a test server with CORS support needs to be set up 44 | // with the following commands: 45 | // 46 | // trunk build examples/prime/index.html 47 | // cargo run -p example-prime --bin example_prime_test_server -- examples/prime/dist 48 | // 49 | // wasm-pack test --headless --firefox examples/prime 50 | #[cfg(test)] 51 | mod tests { 52 | use super::*; 53 | 54 | use gloo::worker::Spawnable; 55 | use wasm_bindgen_test::*; 56 | 57 | wasm_bindgen_test_configure!(run_in_browser); 58 | 59 | #[wasm_bindgen_test] 60 | async fn prime_worker_works() { 61 | gloo::console::log!("running test"); 62 | let mut bridge = Prime::spawner().spawn("http://127.0.0.1:9999/example_prime_worker.js"); 63 | 64 | bridge 65 | .send(ControlSignal::Start) 66 | .await 67 | .expect("failed to send start signal"); 68 | 69 | sleep(Duration::from_millis(1050)).await; 70 | 71 | bridge 72 | .send(ControlSignal::Stop) 73 | .await 74 | .expect("failed to send stop signal"); 75 | 76 | // 5 primes should be sent in 1 second. 77 | let primes: Vec<_> = bridge.take(5).collect().await; 78 | assert_eq!(primes, vec![2, 3, 5, 7, 11]); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "website/build", 4 | "ignore": [ 5 | "firebase.json", 6 | "**/.*", 7 | "**/node_modules/**" 8 | ], 9 | "rewrites": [ 10 | { 11 | "source": "**", 12 | "destination": "/index.html" 13 | } 14 | ] 15 | }, 16 | "emulators": { 17 | "hosting": { 18 | "port": 5000 19 | }, 20 | "ui": { 21 | "enabled": true 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /new-design-workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustwasm/gloo/d600d3ac934b1c62b7009c22eabe2f91be4bd974/new-design-workflow.png -------------------------------------------------------------------------------- /release.toml: -------------------------------------------------------------------------------- 1 | sign-tag = true 2 | sign-commit = true 3 | consolidate-commits = false 4 | tag-message = 'Release `{{crate_name}}` v{{version}}' 5 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Gloo is a modular toolkit for building fast and reliable libraries and apps 2 | //! with Rust and WebAssembly. 3 | 4 | #![deny(missing_docs, missing_debug_implementations)] 5 | #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] 6 | 7 | // Re-exports of toolkit crates. 8 | #[cfg(feature = "console")] 9 | #[cfg_attr(docsrs, doc(cfg(feature = "console")))] 10 | #[doc(inline)] 11 | pub use gloo_console as console; 12 | #[cfg(feature = "dialogs")] 13 | #[cfg_attr(docsrs, doc(cfg(feature = "dialogs")))] 14 | #[doc(inline)] 15 | pub use gloo_dialogs as dialogs; 16 | #[cfg(feature = "events")] 17 | #[cfg_attr(docsrs, doc(cfg(feature = "events")))] 18 | #[doc(inline)] 19 | pub use gloo_events as events; 20 | #[cfg(feature = "file")] 21 | #[cfg_attr(docsrs, doc(cfg(feature = "file")))] 22 | #[doc(inline)] 23 | pub use gloo_file as file; 24 | #[cfg(feature = "history")] 25 | #[cfg_attr(docsrs, doc(cfg(feature = "history")))] 26 | #[doc(inline)] 27 | pub use gloo_history as history; 28 | #[cfg(feature = "net")] 29 | #[cfg_attr(docsrs, doc(cfg(feature = "net")))] 30 | #[doc(inline)] 31 | pub use gloo_net as net; 32 | #[cfg(feature = "render")] 33 | #[cfg_attr(docsrs, doc(cfg(feature = "render")))] 34 | #[doc(inline)] 35 | pub use gloo_render as render; 36 | #[cfg(feature = "storage")] 37 | #[cfg_attr(docsrs, doc(cfg(feature = "storage")))] 38 | #[doc(inline)] 39 | pub use gloo_storage as storage; 40 | #[cfg(feature = "timers")] 41 | #[cfg_attr(docsrs, doc(cfg(feature = "timers")))] 42 | #[doc(inline)] 43 | pub use gloo_timers as timers; 44 | #[cfg(feature = "utils")] 45 | #[cfg_attr(docsrs, doc(cfg(feature = "utils")))] 46 | #[doc(inline)] 47 | pub use gloo_utils as utils; 48 | #[cfg(feature = "worker")] 49 | #[cfg_attr(docsrs, doc(cfg(feature = "worker")))] 50 | #[doc(inline)] 51 | pub use gloo_worker as worker; 52 | -------------------------------------------------------------------------------- /update-readmes.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eux 4 | cd $(dirname $0) 5 | 6 | for c in crates/*; do 7 | cd "$c" 8 | cargo readme --template ../../.README.tpl > README.md 9 | cd - 10 | done 11 | -------------------------------------------------------------------------------- /website/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /website/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator. 4 | 5 | ## Installation 6 | 7 | ```console 8 | yarn install 9 | ``` 10 | 11 | ## Local Development 12 | 13 | ```console 14 | yarn start 15 | ``` 16 | 17 | This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. 18 | 19 | ## Build 20 | 21 | ```console 22 | yarn build 23 | ``` 24 | 25 | This command generates static content into the `build` directory and can be served using any static contents hosting service. 26 | 27 | ## Deployment 28 | 29 | ```console 30 | GIT_USER= USE_SSH=true yarn deploy 31 | ``` 32 | 33 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. 34 | -------------------------------------------------------------------------------- /website/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')], 3 | }; 4 | -------------------------------------------------------------------------------- /website/blog/2019-05-29-hello-world.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: hello-world 3 | title: Hello World 4 | author: Muhammad Hamza 5 | author_title: Maintainer of Gloo 6 | author_url: https://github.com/hamza1311 7 | author_image_url: https://avatars.githubusercontent.com/u/47357913?v=4 8 | tags: [hello] 9 | --- 10 | 11 | Welcome to the first blog post of gloo. Gloo is a toolkit for building web applications and libraries with Rust and Wasm, 12 | composed of modular crates. 13 | 14 | If you are interested in creating content for the blog or contributing to Gloo, please reach out to us. 15 | We will be happy to have you work with us. 16 | -------------------------------------------------------------------------------- /website/blog/2021-07-27-new-release.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: release-0.3.0 3 | title: Releasing v0.3.0 4 | author: Muhammad Hamza 5 | author_title: Maintainer of Gloo 6 | author_url: https://github.com/hamza1311 7 | author_image_url: https://avatars.githubusercontent.com/u/47357913?v=4 8 | tags: [release] 9 | --- 10 | 11 | The Gloo team is happy to announce a new, long overdue, version of Gloo: v0.3.0. 12 | Gloo is a modular toolkit for building fast, reliable Web applications and libraries with Rust and WASM. 13 | 14 | ## What's new 15 | 16 | This release focuses on adding new features and crates. 17 | 18 | ### New crates 19 | 20 | #### `gloo-console` 21 | 22 | `gloo-console` provides an ergonomic way to access the browser's console API using macros: 23 | 24 | ```rust 25 | log!("text"); 26 | ``` 27 | 28 | The formatting is done on the browser side. Any `JsValue` can be provided and it'll be logged as-is: 29 | 30 | ```rust 31 | let object = JsValue::from("any JsValue can be logged"); 32 | log!(object); 33 | ``` 34 | 35 | Multiple values can also be provided: 36 | 37 | ```rust 38 | let object = JsValue::from("Some JsValue"); 39 | log!("literal", object); 40 | ``` 41 | 42 | #### `gloo-dialogs` 43 | 44 | `gloo-dialogs` provides wrappers for the following functions: 45 | 46 | - [`alert`](https://developer.mozilla.org/en-US/docs/Web/API/Window/alert) 47 | - [`confirm`](https://developer.mozilla.org/en-US/docs/Web/API/Window/confirm) 48 | - [`prompt`](https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt) 49 | 50 | ```rust 51 | alert("Hello World!"); 52 | ``` 53 | 54 | ```rust 55 | prompt("What do you want to say?"); 56 | ``` 57 | 58 | 59 | ```rust 60 | confirm("Are you sure?"); 61 | ``` 62 | 63 | #### `gloo-render` 64 | 65 | `gloo-render` provides wrapper for 66 | [`requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame): 67 | 68 | ```rust 69 | request_animation_frame(|_| { 70 | // inside animation frame. 71 | }) 72 | ``` 73 | 74 | #### `gloo-storage` 75 | 76 | `gloo-storage` provides wrappers for the [Web Storage API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API). 77 | It can be used to access both local storage and session storage. 78 | 79 | ### Other changes 80 | 81 | - We now use GitHub Actions instead of Azure for CI 82 | - READMEs and crate level docs are no longer synced 83 | - This website exists!! 84 | 85 | ## Looking for contributors 86 | 87 | Gloo project is in need of contributors. I recently became maintainer of this project, and I'm trying to revive it. 88 | It would be really appreciated if you could contribute or raise awareness about the Gloo project. 89 | -------------------------------------------------------------------------------- /website/blog/2021-10-10-new-release.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: release-0.4.0 3 | title: Releasing v0.4.0 4 | author: Muhammad Hamza 5 | author_title: Maintainer of Gloo 6 | author_url: https://github.com/hamza1311 7 | author_image_url: https://avatars.githubusercontent.com/u/47357913?v=4 8 | tags: [release] 9 | --- 10 | 11 | The Gloo team is happy to announce a new version of Gloo: v0.4.0. Gloo is a modular toolkit for building fast, reliable 12 | Web applications and libraries with Rust and WASM. 13 | 14 | ## What's new 15 | 16 | This release focuses on adding new features and crates. 17 | 18 | ### Features 19 | 20 | * `gloo-utils` crate: `gloo_utils` wraps common `web_sys` features to provide cleaner API for accessing `window`, 21 | working with JS Errors, etc. 22 | * Add `dbg!` equivalent in `gloo_console` for easy console.log debugging. 23 | 24 | ### Fixes 25 | 26 | * Remove the unnecessary copy in `Blob::new` ([#152](https://github.com/rustwasm/gloo/pull/152)) 27 | * Fix dir, dirxml macros in `gloo-console` ([#154](https://github.com/rustwasm/gloo/pull/154)) 28 | 29 | ## Looking for contributors 30 | 31 | Gloo project is in need of contributors. It would be really appreciated if you could contribute or raise awareness about 32 | the Gloo project. 33 | -------------------------------------------------------------------------------- /website/blog/2022-1-22-new-release.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: release-0.6.0 3 | title: Releasing v0.6.0 4 | author: Muhammad Hamza 5 | author_title: Maintainer of Gloo 6 | author_url: https://github.com/hamza1311 7 | author_image_url: https://avatars.githubusercontent.com/u/47357913?v=4 8 | tags: [release] 9 | --- 10 | 11 | The Gloo team is happy to announce a new version of Gloo: v0.6.0. Gloo is a modular toolkit for building fast, reliable 12 | Web applications and libraries with Rust and WASM. 13 | 14 | ## What's new 15 | 16 | This release focuses on adding new features and crates. 17 | 18 | ### New crate: `gloo-worker` 19 | 20 | Gloo workers are a way to offload tasks to web workers. These are run concurrently using 21 | [web-workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers). 22 | 23 | This feature has been requested and overdue for a while. Gloo-worker is made by 24 | moving [`yew-agent`](https://yew.rs/docs/concepts/agents) to Gloo, while making it framework independent in the process. 25 | This allows us to have a neat abstraction over the browser's Web Workers API which can be consumed from anywhere. 26 | 27 | ### Features 28 | 29 | This release has been light on new features. The only improvement is `gloo_utils` now providing a new wrapper 30 | to obtain the document `head`. 31 | 32 | ## Notable mention from last release 33 | 34 | Last release, Gloo v0.5.0 did not receive its own blog post. That released introduced one major new crate: `gloo-history` 35 | amongst other small improvements, which can be found in the [GitHub changelog](https://github.com/rustwasm/gloo/releases/tag/0.5.0). 36 | 37 | ### `gloo-history` 38 | 39 | Gloo history provides wrappers for browser's history API. It exposes ergonomic Rust APIs for the browser's APIs which 40 | can be used to build other tools. In fact, [`yew-router`](https://github.com/yewstack/yew/pull/2239) has been 41 | reworked to use `gloo-history` under-the-hood. 42 | 43 | ## Looking for contributors 44 | 45 | Gloo project is in need of contributors. It would be really appreciated if you could contribute or raise awareness about 46 | the Gloo project. 47 | -------------------------------------------------------------------------------- /website/blog/2022-3-11-new-net-crate.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: release-gloo-net 3 | title: Gloo gets a new crate gloo-net 4 | author: Muhammad Hamza 5 | author_title: Maintainer of Gloo 6 | author_url: https://github.com/hamza1311 7 | author_image_url: https://avatars.githubusercontent.com/u/47357913?v=4 8 | tags: [release] 9 | --- 10 | 11 | The Gloo team is happy to announce a new addition to the Gloo crates family: `gloo-net`. It is an HTTP requests 12 | library built specially for WASM apps and provides idiomatic Rust bindings for the `fetch` and `WebSocket` API. 13 | This has been a long requested library, and it has finally arrived. 14 | 15 | ## Features 16 | 17 | - HTTP: complete wrappers around the browser's `fetch` API. 18 | - WebSocket: complete wrappers around the browser's `WebSocket` API using Rust futures. 19 | 20 | We have plans to also provide wrapper for `XMLHttpRequest` in the future. 21 | 22 | ## Looking for contributors 23 | 24 | Gloo project is in need of contributors. It would be really appreciated if you could contribute or raise awareness about 25 | the Gloo project. 26 | -------------------------------------------------------------------------------- /website/docs/getting-started.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Getting Started" 3 | description: "Using the Gloo toolkit" 4 | --- 5 | 6 | ## Using the Whole Toolkit 7 | 8 | Using the whole toolkit via the umbrella `gloo` crate lets you hit the ground 9 | running, with everything at your fingertips. This is a good choice for people 10 | making Web applications, or top-level crates that are compiled into Wasm 11 | binaries. 12 | 13 | ### `Cargo.toml` 14 | 15 | Add a `gloo` dependency to your `Cargo.toml`: 16 | 17 | ```toml 18 | [dependencies] 19 | gloo = "0.3" 20 | ``` 21 | 22 | ### `src/lib.rs` 23 | 24 | Use various bits of `gloo` via its submodules, for example timers and intervals 25 | are in `gloo::timers` and event listeners are in `gloo::events`: 26 | 27 | ```rust 28 | use gloo::{timers, events}; 29 | 30 | // ... 31 | ``` 32 | 33 | ## Using a Single Crate 34 | 35 | Each crate in the Gloo toolkit can also be used independently from the rest of 36 | the toolkit. This is a good choice for people making library crates that are 37 | intended to be used by other people making Web applications or top-level crates 38 | that are compiled into Wasm binaries. 39 | 40 | ### `Cargo.toml` 41 | 42 | If we want to use only the Gloo functionality that wraps `setTimeout`, we can 43 | add `gloo-timers` to our dependencies in `Cargo.toml`: 44 | 45 | ```toml 46 | [dependencies] 47 | gloo-timers = "0.2" 48 | ``` 49 | 50 | ### `src/lib.rs` 51 | 52 | Next, import the functionality you need from the `gloo_timers` crate, and go to 53 | town with it: 54 | 55 | ```rust 56 | use gloo_timers::callback::Timeout; 57 | 58 | // ... 59 | ``` 60 | -------------------------------------------------------------------------------- /website/firebase-debug.log: -------------------------------------------------------------------------------- 1 | [debug] [2021-06-27T16:45:09.943Z] ---------------------------------------------------------------------- 2 | [debug] [2021-06-27T16:45:09.945Z] Command: /home/hamza/.nvm/versions/node/v14.17.0/bin/node /home/hamza/.nvm/versions/node/v14.17.0/bin/firebase init 3 | [debug] [2021-06-27T16:45:09.946Z] CLI Version: 9.14.0 4 | [debug] [2021-06-27T16:45:09.946Z] Platform: linux 5 | [debug] [2021-06-27T16:45:09.946Z] Node Version: v14.17.0 6 | [debug] [2021-06-27T16:45:09.958Z] Time: Sun Jun 27 2021 21:45:09 GMT+0500 (Pakistan Standard Time) 7 | [debug] [2021-06-27T16:45:09.958Z] ---------------------------------------------------------------------- 8 | [debug] 9 | [debug] [2021-06-27T16:45:09.960Z] >>> [apiv2][query] GET https://firebase-public.firebaseio.com/cli.json [none] 10 | [debug] [2021-06-27T16:45:09.978Z] > command requires scopes: ["email","openid","https://www.googleapis.com/auth/cloudplatformprojects.readonly","https://www.googleapis.com/auth/firebase","https://www.googleapis.com/auth/cloud-platform"] 11 | [debug] [2021-06-27T16:45:09.978Z] > authorizing via signed-in user (muhammadhamza1311@gmail.com) 12 | [info] 13 | ######## #### ######## ######## ######## ### ###### ######## 14 | ## ## ## ## ## ## ## ## ## ## ## 15 | ###### ## ######## ###### ######## ######### ###### ###### 16 | ## ## ## ## ## ## ## ## ## ## ## 17 | ## #### ## ## ######## ######## ## ## ###### ######## 18 | 19 | You're about to initialize a Firebase project in this directory: 20 | 21 | /home/hamza/code/gloo/website 22 | 23 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gloo", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "start": "docusaurus start", 8 | "build": "docusaurus build", 9 | "swizzle": "docusaurus swizzle", 10 | "deploy": "docusaurus deploy", 11 | "clear": "docusaurus clear", 12 | "serve": "docusaurus serve", 13 | "write-translations": "docusaurus write-translations", 14 | "write-heading-ids": "docusaurus write-heading-ids" 15 | }, 16 | "dependencies": { 17 | "@docusaurus/core": "3.0.0-beta.0", 18 | "@docusaurus/preset-classic": "3.0.0-beta.0", 19 | "@mdx-js/react": "^2.3.0", 20 | "clsx": "^2.0.0", 21 | "prism-react-renderer": "^1.3.5", 22 | "react": "^18.0.0", 23 | "react-dom": "^18.0.0" 24 | }, 25 | "devDependencies": { 26 | "@docusaurus/module-type-aliases": "3.0.0-beta.0" 27 | }, 28 | "browserslist": { 29 | "production": [ 30 | ">0.5%", 31 | "not dead", 32 | "not op_mini all" 33 | ], 34 | "development": [ 35 | "last 1 chrome version", 36 | "last 1 firefox version", 37 | "last 1 safari version" 38 | ] 39 | }, 40 | "engines": { 41 | "node": ">=16.14" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /website/sidebars.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creating a sidebar enables you to: 3 | - create an ordered group of docs 4 | - render a sidebar for each doc of that group 5 | - provide next/previous navigation 6 | 7 | The sidebars can be generated from the filesystem, or explicitly defined here. 8 | 9 | Create as many sidebars as you want. 10 | */ 11 | 12 | module.exports = { 13 | // Un comment ones for which sidebar is needed 14 | /*consoleTimerSidebar: [{type: 'autogenerated', dirName: 'console-timer'}], 15 | dialogSidebar: [{type: 'autogenerated', dirName: 'dialog'}], 16 | eventSidebar: [{type: 'autogenerated', dirName: 'event'}], 17 | fileSidebar: [{type: 'autogenerated', dirName: 'file'}], 18 | storageSidebar: [{type: 'autogenerated', dirName: 'storage'}], 19 | timerSidebar: [{type: 'autogenerated', dirName: 'timer'}],*/ 20 | 21 | 22 | // But you can create a sidebar manually 23 | /* 24 | tutorialSidebar: [ 25 | { 26 | type: 'category', 27 | label: 'Tutorial', 28 | items: ['hello'], 29 | }, 30 | ], 31 | */ 32 | }; 33 | -------------------------------------------------------------------------------- /website/src/css/custom.css: -------------------------------------------------------------------------------- 1 | /* stylelint-disable docusaurus/copyright-header */ 2 | /** 3 | * Any CSS included here will be global. The classic template 4 | * bundles Infima by default. Infima is a CSS framework designed to 5 | * work well for content-centric websites. 6 | */ 7 | 8 | /* You can override the default Infima variables here. */ 9 | :root { 10 | --ifm-color-primary: #ff9600; 11 | --ifm-color-primary-dark: #e68700; 12 | --ifm-color-primary-darker: #d98000; 13 | --ifm-color-primary-darkest: #b36900; 14 | --ifm-color-primary-light: #ffa11a; 15 | --ifm-color-primary-lighter: #ffa626; 16 | --ifm-color-primary-lightest: #ffb64d; 17 | --ifm-code-font-size: 95%; 18 | } 19 | 20 | .docusaurus-highlight-code-line { 21 | background-color: rgba(0, 0, 0, 0.1); 22 | display: block; 23 | margin: 0 calc(-1 * var(--ifm-pre-padding)); 24 | padding: 0 var(--ifm-pre-padding); 25 | } 26 | 27 | html[data-theme='dark'] .docusaurus-highlight-code-line { 28 | background-color: rgba(0, 0, 0, 0.3); 29 | } 30 | -------------------------------------------------------------------------------- /website/src/pages/__index.md: -------------------------------------------------------------------------------- 1 | Gloo is a toolkit for building web applications and libraries with Rust and Wasm, composed of 2 | modular crates. [Gloo crates](https://github.com/rustwasm/gloo/tree/master/crates) 3 | include example code, both in their respective `example` folders, and commented in their code, as well 4 | as [API documentation](https://docs.rs/gloo/). 5 | 6 | ## Gloo Crates 7 | 8 | 1. [`console`](https://crates.io/crates/gloo-console) 9 | 2. [`dialogs`](https://crates.io/crates/gloo-dialogs) 10 | 3. [`events`](https://crates.io/crates/gloo-events) 11 | 4. [`file`](https://crates.io/crates/gloo-file) 12 | 5. [`history`](https://crates.io/crates/gloo-history) 13 | 6. [`net`](https://crates.io/crates/gloo-net) 14 | 7. [`render`](https://crates.io/crates/gloo-render) 15 | 8. [`storage`](https://crates.io/crates/gloo-storage) 16 | 9. [`timers`](https://crates.io/crates/gloo-timers) 17 | 10. [`utils`](https://crates.io/crates/gloo-utils) 18 | 11. [`worker`](https://crates.io/crates/gloo-worker) 19 | 20 | ## Using Gloo 21 | 22 | Gloo is a *modular* toolkit: Each of its crates can either be used via the 23 | umbrella `gloo` crate, which re-exports all of them for a stream-lined, holistic 24 | experience, or each crate can be used independently. 25 | 26 | ## Getting Started 27 | 28 | Read our [Getting Started](docs/getting-started) guide to learn more. 29 | -------------------------------------------------------------------------------- /website/src/pages/index.module.css: -------------------------------------------------------------------------------- 1 | /* stylelint-disable docusaurus/copyright-header */ 2 | 3 | /** 4 | * CSS files with the .module.css suffix will be treated as CSS modules 5 | * and scoped locally. 6 | */ 7 | 8 | .heroBanner { 9 | padding: 4rem 0; 10 | text-align: center; 11 | position: relative; 12 | overflow: hidden; 13 | } 14 | 15 | @media screen and (max-width: 966px) { 16 | .heroBanner { 17 | padding: 2rem; 18 | } 19 | } 20 | 21 | main { 22 | padding: 2rem 3em; 23 | } 24 | -------------------------------------------------------------------------------- /website/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | import Layout from '@theme/Layout'; 4 | import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; 5 | import styles from './index.module.css'; 6 | import HomepageContent from './__index.md' 7 | 8 | function HomepageHeader() { 9 | const {siteConfig} = useDocusaurusContext(); 10 | return ( 11 |
12 |
13 |

{siteConfig.title}

14 |

{siteConfig.tagline}

15 |
16 |
17 | ); 18 | } 19 | 20 | export default function Home() { 21 | const {siteConfig} = useDocusaurusContext(); 22 | return ( 23 | 25 | 26 |
27 | 28 |
29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /website/static/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustwasm/gloo/d600d3ac934b1c62b7009c22eabe2f91be4bd974/website/static/.nojekyll -------------------------------------------------------------------------------- /website/static/img/Gloo-Logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustwasm/gloo/d600d3ac934b1c62b7009c22eabe2f91be4bd974/website/static/img/Gloo-Logo.ico -------------------------------------------------------------------------------- /website/static/img/docusaurus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustwasm/gloo/d600d3ac934b1c62b7009c22eabe2f91be4bd974/website/static/img/docusaurus.png -------------------------------------------------------------------------------- /website/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustwasm/gloo/d600d3ac934b1c62b7009c22eabe2f91be4bd974/website/static/img/favicon.ico -------------------------------------------------------------------------------- /website/static/img/tutorial/docsVersionDropdown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustwasm/gloo/d600d3ac934b1c62b7009c22eabe2f91be4bd974/website/static/img/tutorial/docsVersionDropdown.png -------------------------------------------------------------------------------- /website/static/img/tutorial/localeDropdown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustwasm/gloo/d600d3ac934b1c62b7009c22eabe2f91be4bd974/website/static/img/tutorial/localeDropdown.png -------------------------------------------------------------------------------- /website/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/docusaurus/tsconfig.json", 3 | "include": [ 4 | "src/" 5 | ], 6 | "compilerOptions": { 7 | "jsx": "react" 8 | } 9 | } 10 | --------------------------------------------------------------------------------