├── .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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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