├── .dockerignore
├── .gitignore
├── .tool-versions
├── Dockerfile.webrtc-introducer
├── Dockerfile.wraft
├── LICENSE
├── Makefile
├── README.md
├── nginx.conf
├── webrtc-introducer-types
├── Cargo.lock
├── Cargo.toml
├── README.md
└── src
│ └── lib.rs
├── webrtc-introducer
├── Cargo.lock
├── Cargo.toml
├── README.md
└── src
│ ├── errors.rs
│ └── main.rs
└── wraft
├── Cargo.lock
├── Cargo.toml
├── README.md
├── src
├── benchmark.rs
├── lib.rs
├── raft
│ ├── client.rs
│ ├── errors.rs
│ ├── mod.rs
│ ├── rpc_server.rs
│ ├── storage.rs
│ └── worker.rs
├── raft_init.rs
├── ringbuf
│ └── mod.rs
├── todo.rs
├── todo_state.rs
├── util
│ └── mod.rs
└── webrtc_rpc
│ ├── introduction.rs
│ ├── mod.rs
│ └── transport.rs
├── tests
└── web.rs
└── www
├── .bin
└── create-wasm-app.js
├── .gitignore
├── .travis.yml
├── LICENSE-APACHE
├── LICENSE-MIT
├── README.md
├── bootstrap.js
├── index.html
├── index.js
├── package-lock.json
├── package.json
└── webpack.config.js
/.dockerignore:
--------------------------------------------------------------------------------
1 | */target
2 | **/node_modules
3 | **/dist
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | **/target
2 | wraft/target
3 | webrtc-introducer/target
4 | **/*.rs.bk
5 | bin/
6 | **/pkg/
7 | **/wasm-pack.log
8 |
--------------------------------------------------------------------------------
/.tool-versions:
--------------------------------------------------------------------------------
1 | nodejs 16.10.0
2 |
--------------------------------------------------------------------------------
/Dockerfile.webrtc-introducer:
--------------------------------------------------------------------------------
1 | FROM rust:1.56.0-bullseye AS builder
2 | COPY webrtc-introducer /build/webrtc-introducer
3 | COPY webrtc-introducer-types /build/webrtc-introducer-types
4 | RUN cd /build/webrtc-introducer && cargo build --release
5 |
6 | FROM gcr.io/distroless/cc-debian11
7 | COPY --from=builder /build/webrtc-introducer/target/release/webrtc-introducer /app
8 | USER 1000:1000
9 | CMD ["/app"]
10 |
--------------------------------------------------------------------------------
/Dockerfile.wraft:
--------------------------------------------------------------------------------
1 | FROM rust:1.56.0-bullseye AS wasm-builder
2 | RUN curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
3 | COPY webrtc-introducer-types /build/webrtc-introducer-types
4 | COPY wraft /build/wraft
5 | WORKDIR /build/wraft
6 | RUN wasm-pack build --release
7 |
8 | FROM node:gallium AS js-builder
9 | COPY --from=wasm-builder /build/wraft/pkg /build/pkg
10 | COPY wraft/www /build/www
11 | WORKDIR /build/www
12 | RUN npm install && npm run build
13 |
14 | FROM nginxinc/nginx-unprivileged:1.20.2-alpine
15 |
16 | COPY nginx.conf /etc/nginx/conf.d/default.conf
17 | COPY --from=js-builder /build/www/dist/ /usr/share/nginx/html/
18 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: build-images push-images
2 | INTRODUCER_VERSION=v1
3 | WRAFT_VERSION=v4
4 |
5 | build-images:
6 | docker build . -f Dockerfile.wraft -t harbor.eevans.me/library/wraft:$(WRAFT_VERSION)
7 | docker build . -f Dockerfile.webrtc-introducer -t harbor.eevans.me/library/webrtc-introducer:$(INTRODUCER_VERSION)
8 |
9 | push-images: build-images
10 | docker push harbor.eevans.me/library/wraft:$(WRAFT_VERSION)
11 | docker push harbor.eevans.me/library/webrtc-introducer:$(INTRODUCER_VERSION)
12 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # WRaft: Raft in WebAssembly
2 |
3 | ## What is this?
4 |
5 | A toy implementation of the [Raft Consensus Algorithm](https://raft.github.io/)
6 | for [WebAssembly](https://webassembly.org/), written in Rust. Basically, it
7 | synchronizes state across three browser windows in a peer-to-peer fashion.
8 |
9 | ## Why is this?
10 |
11 | Because I was curious to see if I could get it to work 😄. I can't think of any
12 | real-world use-cases off the top of my head, but if you can, please open an
13 | issue and let me know!
14 |
15 | ## How does it work?
16 |
17 | WRaft uses [WebRTC data
18 | channels](https://webrtc.org/getting-started/data-channels) to set up
19 | communication between the browser windows. Sadly [WebRTC isn't purely
20 | peer-to-peer](https://www.youtube.com/watch?v=Y1mx7cx6ckI), so there's a
21 | separate WebSocket-based service (`webrtc-introducer`) that "introduces" the
22 | browser windows to each other before the cluster can start. The browser windows
23 | can be on the same computer or different machines on a LAN (or different
24 | networks, theoretically, but I haven't tested that yet). Firefox and Chrome (or
25 | any combination of the two) seem to work; Safari seems to not work.
26 |
27 | Once the browser windows have been introduced to each other, a Raft cluster is
28 | started and messages sent to one browser window are reliably replicated to all
29 | three in a consistent order (as long as everything's working correctly 😉). The
30 | messages are persisted to the browser's local storage so they'll survive browser
31 | restarts. The cluster will continue functioning normally even if one window
32 | stops, and can recover if two windows stop.
33 |
34 | The "replicated messages" could be any
35 | [serializable](https://docs.serde.rs/serde/trait.Serialize.html) Rust type.
36 | There's a `raft::State` trait that allows the user to "plug in" any
37 | state-machine-like-thing into the Raft server (using Rust generics). For the
38 | example apps, I used [yew](https://yew.rs/) with the "state machine" more or
39 | less mapping to the application state. (You could also use a `HashMap` to get a
40 | distributed key/value store à la etcd.)
41 |
42 | There's currently no way to expand beyond three browser windows (or any notion
43 | of a "client" outside of the cluster servers). I have some ideas for how it
44 | might work, though.
45 |
46 | ## Can I try it?
47 |
48 | Yes! There are two basic "demo apps" included in the library, publicly hosted at
49 | https://wraft0.eevans.co/. The apps are:
50 |
51 | - [Synchronized TodoMVC](https://wraft0.eevans.co/todo).
52 | - An extremely basic [benchmarking tool](https://wraft0.eevans.co/bench) to get
53 | a sense of performance.
54 |
55 | To use either app, you'll need to open the app in different browser windows with
56 | different hosts (`wraft0.eevans.co`, `wraft1.eevans.co`, and `wraft2.eevans.co`)
57 | (they need to be different host names so they have independent Local
58 | Storage). The app should then start up and you can try it!
59 |
60 | ## Is it fast?
61 |
62 | I don't have much to compare it to, but from some basic testing it seems pretty
63 | fast to me! In the best-case scenario (three Chromium browsers on the same
64 | machine) I've seen ~2000 writes/second which should be enough for any use case I
65 | can think of 😄. (The bigger issue is that you hit the local storage quota
66 | pretty fast; log compaction would have to be implemented to work around that.)
67 | Firefox seems to top out at ~800 writes/second.
68 |
69 | ## What parts of Raft are implemented?
70 |
71 | For the Raft nerds out there, so far I've only implemented the "basic algorithm"
72 | (basically what's in the [TLA spec](https://github.com/ongardie/raft.tla)). To
73 | make it more useful, you'd probably need:
74 |
75 | - Log compaction/snapshots (the API is designed to make this possible, but it's
76 | completely unimplemented).
77 |
78 | - Cluster membership changes (I haven't really looked into this yet).
79 |
80 | No promises that either of those will ever happen, but I might try implementing
81 | them if I have time.
82 |
83 | ## Should I use it in production?
84 |
85 | **No!** (At least not in its current state.) Documentation, error handling, and
86 | testing are basically non-existent, and I haven't implemented some harder parts
87 | of Raft like log compaction and cluster membership changes. There are a few bugs
88 | I know about and almost certainly many more I don't!
89 |
90 | If you have an actual use case for this, open an issue to let me know and I'll
91 | think about turning it into a proper Crate and/or NPM package.
92 |
--------------------------------------------------------------------------------
/nginx.conf:
--------------------------------------------------------------------------------
1 | types {
2 | application/wasm wasm;
3 | }
4 |
5 | server {
6 | listen 8080 default_server;
7 |
8 | root /usr/share/nginx/html;
9 | index index.html;
10 | gzip on;
11 | gzip_types text/html text/css application/javascript application/wasm;
12 |
13 | location ~* \.wasm$ {
14 | add_header Cache-Control "max-age=31536000, public";
15 | }
16 |
17 | location / {
18 | add_header Cache-Control "no-cache, max-age=0";
19 | try_files $uri $uri/ /index.html;
20 | }
21 |
22 | error_page 404 /404.html;
23 | error_page 500 502 503 504 /50x.html;
24 | }
25 |
--------------------------------------------------------------------------------
/webrtc-introducer-types/Cargo.lock:
--------------------------------------------------------------------------------
1 | # This file is automatically @generated by Cargo.
2 | # It is not intended for manual editing.
3 | version = 3
4 |
5 | [[package]]
6 | name = "proc-macro2"
7 | version = "1.0.32"
8 | source = "registry+https://github.com/rust-lang/crates.io-index"
9 | checksum = "ba508cc11742c0dc5c1659771673afbab7a0efab23aa17e854cbab0837ed0b43"
10 | dependencies = [
11 | "unicode-xid",
12 | ]
13 |
14 | [[package]]
15 | name = "quote"
16 | version = "1.0.10"
17 | source = "registry+https://github.com/rust-lang/crates.io-index"
18 | checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05"
19 | dependencies = [
20 | "proc-macro2",
21 | ]
22 |
23 | [[package]]
24 | name = "serde"
25 | version = "1.0.130"
26 | source = "registry+https://github.com/rust-lang/crates.io-index"
27 | checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913"
28 | dependencies = [
29 | "serde_derive",
30 | ]
31 |
32 | [[package]]
33 | name = "serde_derive"
34 | version = "1.0.130"
35 | source = "registry+https://github.com/rust-lang/crates.io-index"
36 | checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b"
37 | dependencies = [
38 | "proc-macro2",
39 | "quote",
40 | "syn",
41 | ]
42 |
43 | [[package]]
44 | name = "syn"
45 | version = "1.0.81"
46 | source = "registry+https://github.com/rust-lang/crates.io-index"
47 | checksum = "f2afee18b8beb5a596ecb4a2dce128c719b4ba399d34126b9e4396e3f9860966"
48 | dependencies = [
49 | "proc-macro2",
50 | "quote",
51 | "unicode-xid",
52 | ]
53 |
54 | [[package]]
55 | name = "unicode-xid"
56 | version = "0.2.2"
57 | source = "registry+https://github.com/rust-lang/crates.io-index"
58 | checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
59 |
60 | [[package]]
61 | name = "webrtc-introducer-types"
62 | version = "0.1.0"
63 | dependencies = [
64 | "serde",
65 | ]
66 |
--------------------------------------------------------------------------------
/webrtc-introducer-types/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "webrtc-introducer-types"
3 | version = "0.1.0"
4 | edition = "2021"
5 | license = "Apache-2.0"
6 | repository = "https://github.com/shosti/wraft"
7 |
8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
9 |
10 | [dependencies]
11 | serde = { version = "1.0", features = ["derive"] }
12 |
--------------------------------------------------------------------------------
/webrtc-introducer-types/README.md:
--------------------------------------------------------------------------------
1 | # WebRTC Introducer Types
2 |
3 | This crate just includes basic types for WebRTC introduction communication.
4 |
--------------------------------------------------------------------------------
/webrtc-introducer-types/src/lib.rs:
--------------------------------------------------------------------------------
1 | use serde::{Deserialize, Serialize};
2 | use std::collections::HashSet;
3 |
4 | #[derive(Serialize, Deserialize, Debug, Clone)]
5 | pub enum Command {
6 | Join(Join),
7 | SessionStatus(Session),
8 | Offer(Offer),
9 | Answer(Offer),
10 | IceCandidate(IceCandidate),
11 | }
12 |
13 | #[derive(Serialize, Deserialize, Debug, Clone)]
14 | pub struct Join {
15 | pub node_id: u64,
16 | pub session_id: u128,
17 | }
18 |
19 | #[derive(Serialize, Deserialize, Debug, Clone)]
20 | pub struct Session {
21 | pub session_id: u128,
22 | pub online: HashSet,
23 | }
24 |
25 | #[derive(Serialize, Deserialize, Debug, Clone)]
26 | pub struct Offer {
27 | pub session_id: u128,
28 | pub node_id: u64,
29 | pub target_id: u64,
30 | pub sdp_data: String,
31 | }
32 |
33 | #[derive(Serialize, Deserialize, Debug, Clone)]
34 | pub struct IceCandidate {
35 | pub session_id: u128,
36 | pub node_id: u64,
37 | pub target_id: u64,
38 | pub candidate: String,
39 | pub sdp_mid: Option,
40 | }
41 |
--------------------------------------------------------------------------------
/webrtc-introducer/Cargo.lock:
--------------------------------------------------------------------------------
1 | # This file is automatically @generated by Cargo.
2 | # It is not intended for manual editing.
3 | version = 3
4 |
5 | [[package]]
6 | name = "autocfg"
7 | version = "1.0.1"
8 | source = "registry+https://github.com/rust-lang/crates.io-index"
9 | checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
10 |
11 | [[package]]
12 | name = "base64"
13 | version = "0.13.0"
14 | source = "registry+https://github.com/rust-lang/crates.io-index"
15 | checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
16 |
17 | [[package]]
18 | name = "bincode"
19 | version = "1.3.3"
20 | source = "registry+https://github.com/rust-lang/crates.io-index"
21 | checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
22 | dependencies = [
23 | "serde",
24 | ]
25 |
26 | [[package]]
27 | name = "block-buffer"
28 | version = "0.9.0"
29 | source = "registry+https://github.com/rust-lang/crates.io-index"
30 | checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
31 | dependencies = [
32 | "generic-array",
33 | ]
34 |
35 | [[package]]
36 | name = "byteorder"
37 | version = "1.4.3"
38 | source = "registry+https://github.com/rust-lang/crates.io-index"
39 | checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
40 |
41 | [[package]]
42 | name = "bytes"
43 | version = "1.1.0"
44 | source = "registry+https://github.com/rust-lang/crates.io-index"
45 | checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8"
46 |
47 | [[package]]
48 | name = "cfg-if"
49 | version = "1.0.0"
50 | source = "registry+https://github.com/rust-lang/crates.io-index"
51 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
52 |
53 | [[package]]
54 | name = "cpufeatures"
55 | version = "0.2.1"
56 | source = "registry+https://github.com/rust-lang/crates.io-index"
57 | checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469"
58 | dependencies = [
59 | "libc",
60 | ]
61 |
62 | [[package]]
63 | name = "digest"
64 | version = "0.9.0"
65 | source = "registry+https://github.com/rust-lang/crates.io-index"
66 | checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
67 | dependencies = [
68 | "generic-array",
69 | ]
70 |
71 | [[package]]
72 | name = "fnv"
73 | version = "1.0.7"
74 | source = "registry+https://github.com/rust-lang/crates.io-index"
75 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
76 |
77 | [[package]]
78 | name = "form_urlencoded"
79 | version = "1.0.1"
80 | source = "registry+https://github.com/rust-lang/crates.io-index"
81 | checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191"
82 | dependencies = [
83 | "matches",
84 | "percent-encoding",
85 | ]
86 |
87 | [[package]]
88 | name = "futures"
89 | version = "0.3.17"
90 | source = "registry+https://github.com/rust-lang/crates.io-index"
91 | checksum = "a12aa0eb539080d55c3f2d45a67c3b58b6b0773c1a3ca2dfec66d58c97fd66ca"
92 | dependencies = [
93 | "futures-channel",
94 | "futures-core",
95 | "futures-executor",
96 | "futures-io",
97 | "futures-sink",
98 | "futures-task",
99 | "futures-util",
100 | ]
101 |
102 | [[package]]
103 | name = "futures-channel"
104 | version = "0.3.17"
105 | source = "registry+https://github.com/rust-lang/crates.io-index"
106 | checksum = "5da6ba8c3bb3c165d3c7319fc1cc8304facf1fb8db99c5de877183c08a273888"
107 | dependencies = [
108 | "futures-core",
109 | "futures-sink",
110 | ]
111 |
112 | [[package]]
113 | name = "futures-core"
114 | version = "0.3.17"
115 | source = "registry+https://github.com/rust-lang/crates.io-index"
116 | checksum = "88d1c26957f23603395cd326b0ffe64124b818f4449552f960d815cfba83a53d"
117 |
118 | [[package]]
119 | name = "futures-executor"
120 | version = "0.3.17"
121 | source = "registry+https://github.com/rust-lang/crates.io-index"
122 | checksum = "45025be030969d763025784f7f355043dc6bc74093e4ecc5000ca4dc50d8745c"
123 | dependencies = [
124 | "futures-core",
125 | "futures-task",
126 | "futures-util",
127 | ]
128 |
129 | [[package]]
130 | name = "futures-io"
131 | version = "0.3.17"
132 | source = "registry+https://github.com/rust-lang/crates.io-index"
133 | checksum = "522de2a0fe3e380f1bc577ba0474108faf3f6b18321dbf60b3b9c39a75073377"
134 |
135 | [[package]]
136 | name = "futures-macro"
137 | version = "0.3.17"
138 | source = "registry+https://github.com/rust-lang/crates.io-index"
139 | checksum = "18e4a4b95cea4b4ccbcf1c5675ca7c4ee4e9e75eb79944d07defde18068f79bb"
140 | dependencies = [
141 | "autocfg",
142 | "proc-macro-hack",
143 | "proc-macro2",
144 | "quote",
145 | "syn",
146 | ]
147 |
148 | [[package]]
149 | name = "futures-sink"
150 | version = "0.3.17"
151 | source = "registry+https://github.com/rust-lang/crates.io-index"
152 | checksum = "36ea153c13024fe480590b3e3d4cad89a0cfacecc24577b68f86c6ced9c2bc11"
153 |
154 | [[package]]
155 | name = "futures-task"
156 | version = "0.3.17"
157 | source = "registry+https://github.com/rust-lang/crates.io-index"
158 | checksum = "1d3d00f4eddb73e498a54394f228cd55853bdf059259e8e7bc6e69d408892e99"
159 |
160 | [[package]]
161 | name = "futures-util"
162 | version = "0.3.17"
163 | source = "registry+https://github.com/rust-lang/crates.io-index"
164 | checksum = "36568465210a3a6ee45e1f165136d68671471a501e632e9a98d96872222b5481"
165 | dependencies = [
166 | "autocfg",
167 | "futures-channel",
168 | "futures-core",
169 | "futures-io",
170 | "futures-macro",
171 | "futures-sink",
172 | "futures-task",
173 | "memchr",
174 | "pin-project-lite",
175 | "pin-utils",
176 | "proc-macro-hack",
177 | "proc-macro-nested",
178 | "slab",
179 | ]
180 |
181 | [[package]]
182 | name = "generic-array"
183 | version = "0.14.4"
184 | source = "registry+https://github.com/rust-lang/crates.io-index"
185 | checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817"
186 | dependencies = [
187 | "typenum",
188 | "version_check",
189 | ]
190 |
191 | [[package]]
192 | name = "getrandom"
193 | version = "0.2.3"
194 | source = "registry+https://github.com/rust-lang/crates.io-index"
195 | checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753"
196 | dependencies = [
197 | "cfg-if",
198 | "libc",
199 | "wasi",
200 | ]
201 |
202 | [[package]]
203 | name = "hermit-abi"
204 | version = "0.1.19"
205 | source = "registry+https://github.com/rust-lang/crates.io-index"
206 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
207 | dependencies = [
208 | "libc",
209 | ]
210 |
211 | [[package]]
212 | name = "http"
213 | version = "0.2.5"
214 | source = "registry+https://github.com/rust-lang/crates.io-index"
215 | checksum = "1323096b05d41827dadeaee54c9981958c0f94e670bc94ed80037d1a7b8b186b"
216 | dependencies = [
217 | "bytes",
218 | "fnv",
219 | "itoa",
220 | ]
221 |
222 | [[package]]
223 | name = "httparse"
224 | version = "1.5.1"
225 | source = "registry+https://github.com/rust-lang/crates.io-index"
226 | checksum = "acd94fdbe1d4ff688b67b04eee2e17bd50995534a61539e45adfefb45e5e5503"
227 |
228 | [[package]]
229 | name = "idna"
230 | version = "0.2.3"
231 | source = "registry+https://github.com/rust-lang/crates.io-index"
232 | checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8"
233 | dependencies = [
234 | "matches",
235 | "unicode-bidi",
236 | "unicode-normalization",
237 | ]
238 |
239 | [[package]]
240 | name = "itoa"
241 | version = "0.4.8"
242 | source = "registry+https://github.com/rust-lang/crates.io-index"
243 | checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
244 |
245 | [[package]]
246 | name = "libc"
247 | version = "0.2.105"
248 | source = "registry+https://github.com/rust-lang/crates.io-index"
249 | checksum = "869d572136620d55835903746bcb5cdc54cb2851fd0aeec53220b4bb65ef3013"
250 |
251 | [[package]]
252 | name = "log"
253 | version = "0.4.14"
254 | source = "registry+https://github.com/rust-lang/crates.io-index"
255 | checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
256 | dependencies = [
257 | "cfg-if",
258 | ]
259 |
260 | [[package]]
261 | name = "matches"
262 | version = "0.1.9"
263 | source = "registry+https://github.com/rust-lang/crates.io-index"
264 | checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
265 |
266 | [[package]]
267 | name = "memchr"
268 | version = "2.4.1"
269 | source = "registry+https://github.com/rust-lang/crates.io-index"
270 | checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
271 |
272 | [[package]]
273 | name = "mio"
274 | version = "0.7.14"
275 | source = "registry+https://github.com/rust-lang/crates.io-index"
276 | checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc"
277 | dependencies = [
278 | "libc",
279 | "log",
280 | "miow",
281 | "ntapi",
282 | "winapi",
283 | ]
284 |
285 | [[package]]
286 | name = "miow"
287 | version = "0.3.7"
288 | source = "registry+https://github.com/rust-lang/crates.io-index"
289 | checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21"
290 | dependencies = [
291 | "winapi",
292 | ]
293 |
294 | [[package]]
295 | name = "ntapi"
296 | version = "0.3.6"
297 | source = "registry+https://github.com/rust-lang/crates.io-index"
298 | checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44"
299 | dependencies = [
300 | "winapi",
301 | ]
302 |
303 | [[package]]
304 | name = "num_cpus"
305 | version = "1.13.0"
306 | source = "registry+https://github.com/rust-lang/crates.io-index"
307 | checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3"
308 | dependencies = [
309 | "hermit-abi",
310 | "libc",
311 | ]
312 |
313 | [[package]]
314 | name = "once_cell"
315 | version = "1.8.0"
316 | source = "registry+https://github.com/rust-lang/crates.io-index"
317 | checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56"
318 |
319 | [[package]]
320 | name = "opaque-debug"
321 | version = "0.3.0"
322 | source = "registry+https://github.com/rust-lang/crates.io-index"
323 | checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
324 |
325 | [[package]]
326 | name = "percent-encoding"
327 | version = "2.1.0"
328 | source = "registry+https://github.com/rust-lang/crates.io-index"
329 | checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
330 |
331 | [[package]]
332 | name = "pin-project"
333 | version = "1.0.8"
334 | source = "registry+https://github.com/rust-lang/crates.io-index"
335 | checksum = "576bc800220cc65dac09e99e97b08b358cfab6e17078de8dc5fee223bd2d0c08"
336 | dependencies = [
337 | "pin-project-internal",
338 | ]
339 |
340 | [[package]]
341 | name = "pin-project-internal"
342 | version = "1.0.8"
343 | source = "registry+https://github.com/rust-lang/crates.io-index"
344 | checksum = "6e8fe8163d14ce7f0cdac2e040116f22eac817edabff0be91e8aff7e9accf389"
345 | dependencies = [
346 | "proc-macro2",
347 | "quote",
348 | "syn",
349 | ]
350 |
351 | [[package]]
352 | name = "pin-project-lite"
353 | version = "0.2.7"
354 | source = "registry+https://github.com/rust-lang/crates.io-index"
355 | checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443"
356 |
357 | [[package]]
358 | name = "pin-utils"
359 | version = "0.1.0"
360 | source = "registry+https://github.com/rust-lang/crates.io-index"
361 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
362 |
363 | [[package]]
364 | name = "ppv-lite86"
365 | version = "0.2.15"
366 | source = "registry+https://github.com/rust-lang/crates.io-index"
367 | checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba"
368 |
369 | [[package]]
370 | name = "proc-macro-hack"
371 | version = "0.5.19"
372 | source = "registry+https://github.com/rust-lang/crates.io-index"
373 | checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5"
374 |
375 | [[package]]
376 | name = "proc-macro-nested"
377 | version = "0.1.7"
378 | source = "registry+https://github.com/rust-lang/crates.io-index"
379 | checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086"
380 |
381 | [[package]]
382 | name = "proc-macro2"
383 | version = "1.0.32"
384 | source = "registry+https://github.com/rust-lang/crates.io-index"
385 | checksum = "ba508cc11742c0dc5c1659771673afbab7a0efab23aa17e854cbab0837ed0b43"
386 | dependencies = [
387 | "unicode-xid",
388 | ]
389 |
390 | [[package]]
391 | name = "quote"
392 | version = "1.0.10"
393 | source = "registry+https://github.com/rust-lang/crates.io-index"
394 | checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05"
395 | dependencies = [
396 | "proc-macro2",
397 | ]
398 |
399 | [[package]]
400 | name = "rand"
401 | version = "0.8.4"
402 | source = "registry+https://github.com/rust-lang/crates.io-index"
403 | checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8"
404 | dependencies = [
405 | "libc",
406 | "rand_chacha",
407 | "rand_core",
408 | "rand_hc",
409 | ]
410 |
411 | [[package]]
412 | name = "rand_chacha"
413 | version = "0.3.1"
414 | source = "registry+https://github.com/rust-lang/crates.io-index"
415 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
416 | dependencies = [
417 | "ppv-lite86",
418 | "rand_core",
419 | ]
420 |
421 | [[package]]
422 | name = "rand_core"
423 | version = "0.6.3"
424 | source = "registry+https://github.com/rust-lang/crates.io-index"
425 | checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
426 | dependencies = [
427 | "getrandom",
428 | ]
429 |
430 | [[package]]
431 | name = "rand_hc"
432 | version = "0.3.1"
433 | source = "registry+https://github.com/rust-lang/crates.io-index"
434 | checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7"
435 | dependencies = [
436 | "rand_core",
437 | ]
438 |
439 | [[package]]
440 | name = "serde"
441 | version = "1.0.130"
442 | source = "registry+https://github.com/rust-lang/crates.io-index"
443 | checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913"
444 | dependencies = [
445 | "serde_derive",
446 | ]
447 |
448 | [[package]]
449 | name = "serde_derive"
450 | version = "1.0.130"
451 | source = "registry+https://github.com/rust-lang/crates.io-index"
452 | checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b"
453 | dependencies = [
454 | "proc-macro2",
455 | "quote",
456 | "syn",
457 | ]
458 |
459 | [[package]]
460 | name = "sha-1"
461 | version = "0.9.8"
462 | source = "registry+https://github.com/rust-lang/crates.io-index"
463 | checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6"
464 | dependencies = [
465 | "block-buffer",
466 | "cfg-if",
467 | "cpufeatures",
468 | "digest",
469 | "opaque-debug",
470 | ]
471 |
472 | [[package]]
473 | name = "signal-hook-registry"
474 | version = "1.4.0"
475 | source = "registry+https://github.com/rust-lang/crates.io-index"
476 | checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0"
477 | dependencies = [
478 | "libc",
479 | ]
480 |
481 | [[package]]
482 | name = "slab"
483 | version = "0.4.5"
484 | source = "registry+https://github.com/rust-lang/crates.io-index"
485 | checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5"
486 |
487 | [[package]]
488 | name = "syn"
489 | version = "1.0.81"
490 | source = "registry+https://github.com/rust-lang/crates.io-index"
491 | checksum = "f2afee18b8beb5a596ecb4a2dce128c719b4ba399d34126b9e4396e3f9860966"
492 | dependencies = [
493 | "proc-macro2",
494 | "quote",
495 | "unicode-xid",
496 | ]
497 |
498 | [[package]]
499 | name = "thiserror"
500 | version = "1.0.30"
501 | source = "registry+https://github.com/rust-lang/crates.io-index"
502 | checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417"
503 | dependencies = [
504 | "thiserror-impl",
505 | ]
506 |
507 | [[package]]
508 | name = "thiserror-impl"
509 | version = "1.0.30"
510 | source = "registry+https://github.com/rust-lang/crates.io-index"
511 | checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b"
512 | dependencies = [
513 | "proc-macro2",
514 | "quote",
515 | "syn",
516 | ]
517 |
518 | [[package]]
519 | name = "tinyvec"
520 | version = "1.5.0"
521 | source = "registry+https://github.com/rust-lang/crates.io-index"
522 | checksum = "f83b2a3d4d9091d0abd7eba4dc2710b1718583bd4d8992e2190720ea38f391f7"
523 | dependencies = [
524 | "tinyvec_macros",
525 | ]
526 |
527 | [[package]]
528 | name = "tinyvec_macros"
529 | version = "0.1.0"
530 | source = "registry+https://github.com/rust-lang/crates.io-index"
531 | checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
532 |
533 | [[package]]
534 | name = "tokio"
535 | version = "1.13.0"
536 | source = "registry+https://github.com/rust-lang/crates.io-index"
537 | checksum = "588b2d10a336da58d877567cd8fb8a14b463e2104910f8132cd054b4b96e29ee"
538 | dependencies = [
539 | "autocfg",
540 | "bytes",
541 | "libc",
542 | "memchr",
543 | "mio",
544 | "num_cpus",
545 | "once_cell",
546 | "pin-project-lite",
547 | "signal-hook-registry",
548 | "tokio-macros",
549 | "winapi",
550 | ]
551 |
552 | [[package]]
553 | name = "tokio-macros"
554 | version = "1.5.0"
555 | source = "registry+https://github.com/rust-lang/crates.io-index"
556 | checksum = "b2dd85aeaba7b68df939bd357c6afb36c87951be9e80bf9c859f2fc3e9fca0fd"
557 | dependencies = [
558 | "proc-macro2",
559 | "quote",
560 | "syn",
561 | ]
562 |
563 | [[package]]
564 | name = "tokio-tungstenite"
565 | version = "0.15.0"
566 | source = "registry+https://github.com/rust-lang/crates.io-index"
567 | checksum = "511de3f85caf1c98983545490c3d09685fa8eb634e57eec22bb4db271f46cbd8"
568 | dependencies = [
569 | "futures-util",
570 | "log",
571 | "pin-project",
572 | "tokio",
573 | "tungstenite",
574 | ]
575 |
576 | [[package]]
577 | name = "tungstenite"
578 | version = "0.14.0"
579 | source = "registry+https://github.com/rust-lang/crates.io-index"
580 | checksum = "a0b2d8558abd2e276b0a8df5c05a2ec762609344191e5fd23e292c910e9165b5"
581 | dependencies = [
582 | "base64",
583 | "byteorder",
584 | "bytes",
585 | "http",
586 | "httparse",
587 | "log",
588 | "rand",
589 | "sha-1",
590 | "thiserror",
591 | "url",
592 | "utf-8",
593 | ]
594 |
595 | [[package]]
596 | name = "typenum"
597 | version = "1.14.0"
598 | source = "registry+https://github.com/rust-lang/crates.io-index"
599 | checksum = "b63708a265f51345575b27fe43f9500ad611579e764c79edbc2037b1121959ec"
600 |
601 | [[package]]
602 | name = "unicode-bidi"
603 | version = "0.3.7"
604 | source = "registry+https://github.com/rust-lang/crates.io-index"
605 | checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f"
606 |
607 | [[package]]
608 | name = "unicode-normalization"
609 | version = "0.1.19"
610 | source = "registry+https://github.com/rust-lang/crates.io-index"
611 | checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9"
612 | dependencies = [
613 | "tinyvec",
614 | ]
615 |
616 | [[package]]
617 | name = "unicode-xid"
618 | version = "0.2.2"
619 | source = "registry+https://github.com/rust-lang/crates.io-index"
620 | checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
621 |
622 | [[package]]
623 | name = "url"
624 | version = "2.2.2"
625 | source = "registry+https://github.com/rust-lang/crates.io-index"
626 | checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c"
627 | dependencies = [
628 | "form_urlencoded",
629 | "idna",
630 | "matches",
631 | "percent-encoding",
632 | ]
633 |
634 | [[package]]
635 | name = "utf-8"
636 | version = "0.7.6"
637 | source = "registry+https://github.com/rust-lang/crates.io-index"
638 | checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
639 |
640 | [[package]]
641 | name = "version_check"
642 | version = "0.9.3"
643 | source = "registry+https://github.com/rust-lang/crates.io-index"
644 | checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
645 |
646 | [[package]]
647 | name = "wasi"
648 | version = "0.10.2+wasi-snapshot-preview1"
649 | source = "registry+https://github.com/rust-lang/crates.io-index"
650 | checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
651 |
652 | [[package]]
653 | name = "webrtc-introducer"
654 | version = "0.1.0"
655 | dependencies = [
656 | "bincode",
657 | "futures",
658 | "http",
659 | "serde",
660 | "tokio",
661 | "tokio-tungstenite",
662 | "webrtc-introducer-types",
663 | ]
664 |
665 | [[package]]
666 | name = "webrtc-introducer-types"
667 | version = "0.1.0"
668 | dependencies = [
669 | "serde",
670 | ]
671 |
672 | [[package]]
673 | name = "winapi"
674 | version = "0.3.9"
675 | source = "registry+https://github.com/rust-lang/crates.io-index"
676 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
677 | dependencies = [
678 | "winapi-i686-pc-windows-gnu",
679 | "winapi-x86_64-pc-windows-gnu",
680 | ]
681 |
682 | [[package]]
683 | name = "winapi-i686-pc-windows-gnu"
684 | version = "0.4.0"
685 | source = "registry+https://github.com/rust-lang/crates.io-index"
686 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
687 |
688 | [[package]]
689 | name = "winapi-x86_64-pc-windows-gnu"
690 | version = "0.4.0"
691 | source = "registry+https://github.com/rust-lang/crates.io-index"
692 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
693 |
--------------------------------------------------------------------------------
/webrtc-introducer/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "webrtc-introducer"
3 | version = "0.1.0"
4 | edition = "2021"
5 | license = "Apache-2.0"
6 | repository = "https://github.com/shosti/wraft"
7 |
8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
9 |
10 | [dependencies]
11 | tokio = { version = "1.13", features = ["macros", "net", "rt-multi-thread", "sync", "time", "signal"] }
12 | tokio-tungstenite = "0.15"
13 | futures = "0.3"
14 | bincode = "1.3"
15 | serde = { version = "1.0", features = ["derive"] }
16 | webrtc-introducer-types = { path = "../webrtc-introducer-types" }
17 | http = "0.2"
18 |
--------------------------------------------------------------------------------
/webrtc-introducer/README.md:
--------------------------------------------------------------------------------
1 | # WebRTC Introducer
2 |
3 | This is a very basic WebRTC signaling server for use with WRaft. Basically, it
4 | just uses WebSockets to announce the presence of nodes for a given session key,
5 | as well as forwarding Join/ICE server requests to proper nodes.
6 |
--------------------------------------------------------------------------------
/webrtc-introducer/src/errors.rs:
--------------------------------------------------------------------------------
1 | #[derive(Debug)]
2 | pub enum Error {
3 | SessionNotFound(String),
4 | RustError(Box),
5 | }
6 |
7 | impl From> for Error
8 | where
9 | T: std::fmt::Debug + Send,
10 | {
11 | fn from(err: tokio::sync::broadcast::error::SendError) -> Self {
12 | Self::RustError(Box::new(err))
13 | }
14 | }
15 |
16 | impl From> for Error
17 | where
18 | T: std::fmt::Debug + Send,
19 | {
20 | fn from(err: tokio::sync::mpsc::error::SendError) -> Self {
21 | Self::RustError(Box::new(err))
22 | }
23 | }
24 |
25 | impl From for Error {
26 | fn from(err: tokio_tungstenite::tungstenite::Error) -> Self {
27 | Self::RustError(Box::new(err))
28 | }
29 | }
30 |
31 | impl From> for Error {
32 | fn from(err: Box) -> Self {
33 | Self::RustError(err)
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/webrtc-introducer/src/main.rs:
--------------------------------------------------------------------------------
1 | mod errors;
2 | use errors::Error;
3 | use futures::sink::SinkExt;
4 | use futures::stream::StreamExt;
5 | use std::collections::{HashMap, HashSet};
6 | use std::env;
7 | use std::sync::{Arc, RwLock};
8 | use std::time::Duration;
9 | use tokio::net::{TcpListener, TcpStream};
10 | use tokio::select;
11 | use tokio::signal::unix::{signal, SignalKind};
12 | use tokio::sync::broadcast::{channel, Sender};
13 | use tokio::sync::mpsc;
14 | use tokio_tungstenite::accept_hdr_async;
15 | use tokio_tungstenite::tungstenite;
16 | use tungstenite::handshake::server;
17 | use webrtc_introducer_types::{Command, Session};
18 |
19 | #[derive(Clone, Default)]
20 | struct Channels {
21 | messages: Arc>>>,
22 | online: Arc>>>,
23 | }
24 |
25 | struct Headers {}
26 |
27 | impl server::Callback for Headers {
28 | fn on_request(
29 | self,
30 | _request: &server::Request,
31 | mut response: server::Response,
32 | ) -> Result {
33 | response.headers_mut().insert(
34 | http::header::CONTENT_SECURITY_POLICY,
35 | "default-src *".parse().unwrap(),
36 | );
37 | Ok(response)
38 | }
39 | }
40 |
41 | impl Channels {
42 | pub fn get_messages(&self, session_id: u128) -> Option> {
43 | let m = self.messages.read().unwrap();
44 | m.get(&session_id).cloned()
45 | }
46 |
47 | pub fn joined(&self, node_id: u64, session_id: u128) -> Result<(), Error> {
48 | {
49 | let mut os = self.online.write().unwrap();
50 | let o = os.entry(session_id).or_insert_with(HashSet::new);
51 | o.insert(node_id);
52 | }
53 | self.broadcast_session_info(session_id)
54 | }
55 |
56 | pub fn left(&self, node_id: u64, session_id: u128) -> Result<(), Error> {
57 | {
58 | let mut o = self.online.write().unwrap();
59 | let online = o.get_mut(&session_id).unwrap();
60 | online.remove(&node_id);
61 | }
62 | self.broadcast_session_info(session_id)
63 | }
64 |
65 | pub fn broadcast(self, session_id: u128, command: Command) -> Result<(), Error> {
66 | let m = self.messages.read().unwrap();
67 | let tx = m
68 | .get(&session_id)
69 | .ok_or_else(|| Error::SessionNotFound(format!("{:032x}", session_id)))?;
70 | tx.send(command)?;
71 | Ok(())
72 | }
73 |
74 | pub fn broadcast_session_info(&self, session_id: u128) -> Result<(), Error> {
75 | let onlines = self.online.read().unwrap();
76 | let online = onlines.get(&session_id).unwrap();
77 | let status = Session {
78 | session_id,
79 | online: online.clone(),
80 | };
81 | let cmd = Command::SessionStatus(status);
82 | self.clone().broadcast(session_id, cmd)
83 | }
84 |
85 | pub fn ensure_session(&self, session_id: u128) {
86 | let mut m = self.messages.write().unwrap();
87 | if m.contains_key(&session_id) {
88 | return;
89 | }
90 | let (tx, _rx) = channel(10);
91 | m.insert(session_id, tx);
92 | }
93 | }
94 |
95 | #[tokio::main]
96 | async fn main() -> Result<(), Box> {
97 | let port = env::var("PORT").unwrap_or_else(|_| "3000".to_string());
98 | let listen = env::var("LISTEN_ADDR").unwrap_or_else(|_| "0.0.0.0".to_string());
99 | let addr = format!("{}:{}", listen, port);
100 | println!("listening on {}", addr);
101 | let listener = TcpListener::bind(addr).await?;
102 | let channels = Channels::default();
103 | tokio::spawn(async {
104 | let mut stream = signal(SignalKind::terminate()).unwrap();
105 | stream.recv().await;
106 | println!("got SIGTERM, exiting");
107 | std::process::exit(0);
108 | });
109 |
110 | loop {
111 | let ch = channels.clone();
112 | let (socket, _) = listener.accept().await?;
113 | tokio::spawn(async move {
114 | match process_socket(socket, ch).await {
115 | Ok(_) => (),
116 | Err(err) => {
117 | println!("ERROR: {:#?}", err);
118 | }
119 | }
120 | });
121 | }
122 | }
123 |
124 | async fn process_socket(socket: TcpStream, channels: Channels) -> Result<(), Error> {
125 | let cb = Headers {};
126 | let conn = accept_hdr_async(socket, cb).await?;
127 | let (mut send, mut recv) = conn.split();
128 | let (join_tx, mut join_rx) = mpsc::channel::<(u64, u128)>(1);
129 | let (left_tx, mut left_rx) = mpsc::channel::<()>(1);
130 | let ch = channels.clone();
131 | let recv_handle: tokio::task::JoinHandle> = tokio::spawn(async move {
132 | if let Some((node_id, session_id)) = join_rx.recv().await {
133 | ch.ensure_session(session_id);
134 | let mut rx = ch.get_messages(session_id).unwrap().subscribe();
135 | ch.joined(node_id, session_id)?;
136 | let mut ping_interval = tokio::time::interval(Duration::from_secs(5));
137 | loop {
138 | select! {
139 | Ok(msg) = rx.recv() => {
140 | match msg {
141 | Command::SessionStatus(_) => {
142 | // Always send session status to everyone
143 | let data = bincode::serialize(&msg)?;
144 | send.send(tungstenite::Message::Binary(data)).await?;
145 | }
146 | Command::Offer(ref offer) => {
147 | if offer.session_id == session_id && offer.target_id == node_id {
148 | let data = bincode::serialize(&msg)?;
149 | send.send(tungstenite::Message::Binary(data)).await?;
150 | }
151 | }
152 | Command::Answer(ref answer) => {
153 | if answer.session_id == session_id && answer.target_id == node_id {
154 | let data = bincode::serialize(&msg)?;
155 | send.send(tungstenite::Message::Binary(data)).await?;
156 | }
157 | }
158 | Command::IceCandidate(ref answer) => {
159 | if answer.session_id == session_id && answer.target_id == node_id {
160 | let data = bincode::serialize(&msg)?;
161 | send.send(tungstenite::Message::Binary(data)).await?;
162 | }
163 | }
164 | _ => unreachable!(),
165 | }
166 | }
167 | _ = ping_interval.tick() => {
168 | send.send(tungstenite::Message::Ping("hello".into())).await?;
169 | }
170 | _ = left_rx.recv() => return Ok(()),
171 | }
172 | }
173 | }
174 | Ok(())
175 | });
176 |
177 | let mut ids = None;
178 | while let Some(msg) = recv.next().await {
179 | let ch = channels.clone();
180 | match msg {
181 | Ok(tungstenite::Message::Binary(data)) => {
182 | match process_message(&data, ch, join_tx.clone()).await {
183 | Ok(info) => ids = info,
184 | Err(err) => println!("Error handling message: {:?}", err),
185 | }
186 | }
187 | Ok(tungstenite::Message::Pong(_)) => (),
188 | Ok(tungstenite::Message::Close(_)) => (),
189 | Ok(msg) => println!("Unexpected message: {}", msg),
190 | Err(err) => println!("Error handling receiving message: {:?}", err),
191 | }
192 | }
193 |
194 | left_tx.send(()).await?;
195 | recv_handle.await.unwrap()?;
196 | if let Some((node_id, session_id)) = ids {
197 | channels.left(node_id, session_id)?;
198 | }
199 | Ok(())
200 | }
201 |
202 | async fn process_message(
203 | data: &[u8],
204 | channels: Channels,
205 | join_tx: mpsc::Sender<(u64, u128)>,
206 | ) -> Result, Error> {
207 | let cmd: Command = bincode::deserialize(data)?;
208 | let mut ids = None;
209 | match cmd {
210 | Command::Join(join) => {
211 | let info = (join.node_id, join.session_id);
212 | ids = Some(info);
213 | join_tx.send(info).await?;
214 | }
215 | Command::Offer(ref offer) => {
216 | channels.broadcast(offer.session_id, cmd.clone())?;
217 | }
218 | Command::Answer(ref answer) => {
219 | channels.broadcast(answer.session_id, cmd.clone())?;
220 | }
221 | Command::IceCandidate(ref candidate) => {
222 | channels.broadcast(candidate.session_id, cmd.clone())?;
223 | }
224 | _ => unreachable!(),
225 | }
226 |
227 | Ok(ids)
228 | }
229 |
--------------------------------------------------------------------------------
/wraft/Cargo.lock:
--------------------------------------------------------------------------------
1 | # This file is automatically @generated by Cargo.
2 | # It is not intended for manual editing.
3 | version = 3
4 |
5 | [[package]]
6 | name = "anyhow"
7 | version = "1.0.45"
8 | source = "registry+https://github.com/rust-lang/crates.io-index"
9 | checksum = "ee10e43ae4a853c0a3591d4e2ada1719e553be18199d9da9d4a83f5927c2f5c7"
10 |
11 | [[package]]
12 | name = "anymap"
13 | version = "0.12.1"
14 | source = "registry+https://github.com/rust-lang/crates.io-index"
15 | checksum = "33954243bd79057c2de7338850b85983a44588021f8a5fee574a8888c6de4344"
16 |
17 | [[package]]
18 | name = "arrayvec"
19 | version = "0.5.2"
20 | source = "registry+https://github.com/rust-lang/crates.io-index"
21 | checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
22 |
23 | [[package]]
24 | name = "async-trait"
25 | version = "0.1.51"
26 | source = "registry+https://github.com/rust-lang/crates.io-index"
27 | checksum = "44318e776df68115a881de9a8fd1b9e53368d7a4a5ce4cc48517da3393233a5e"
28 | dependencies = [
29 | "proc-macro2",
30 | "quote",
31 | "syn",
32 | ]
33 |
34 | [[package]]
35 | name = "autocfg"
36 | version = "1.0.1"
37 | source = "registry+https://github.com/rust-lang/crates.io-index"
38 | checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
39 |
40 | [[package]]
41 | name = "base64"
42 | version = "0.13.0"
43 | source = "registry+https://github.com/rust-lang/crates.io-index"
44 | checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
45 |
46 | [[package]]
47 | name = "bincode"
48 | version = "1.3.3"
49 | source = "registry+https://github.com/rust-lang/crates.io-index"
50 | checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
51 | dependencies = [
52 | "serde",
53 | ]
54 |
55 | [[package]]
56 | name = "bitflags"
57 | version = "1.3.2"
58 | source = "registry+https://github.com/rust-lang/crates.io-index"
59 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
60 |
61 | [[package]]
62 | name = "boolinator"
63 | version = "2.4.0"
64 | source = "registry+https://github.com/rust-lang/crates.io-index"
65 | checksum = "cfa8873f51c92e232f9bac4065cddef41b714152812bfc5f7672ba16d6ef8cd9"
66 |
67 | [[package]]
68 | name = "bumpalo"
69 | version = "3.8.0"
70 | source = "registry+https://github.com/rust-lang/crates.io-index"
71 | checksum = "8f1e260c3a9040a7c19a12468758f4c16f31a81a1fe087482be9570ec864bb6c"
72 |
73 | [[package]]
74 | name = "bytes"
75 | version = "1.1.0"
76 | source = "registry+https://github.com/rust-lang/crates.io-index"
77 | checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8"
78 |
79 | [[package]]
80 | name = "cfg-if"
81 | version = "0.1.10"
82 | source = "registry+https://github.com/rust-lang/crates.io-index"
83 | checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
84 |
85 | [[package]]
86 | name = "cfg-if"
87 | version = "1.0.0"
88 | source = "registry+https://github.com/rust-lang/crates.io-index"
89 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
90 |
91 | [[package]]
92 | name = "cfg-match"
93 | version = "0.2.1"
94 | source = "registry+https://github.com/rust-lang/crates.io-index"
95 | checksum = "8100e46ff92eb85bf6dc2930c73f2a4f7176393c84a9446b3d501e1b354e7b34"
96 |
97 | [[package]]
98 | name = "console_error_panic_hook"
99 | version = "0.1.7"
100 | source = "registry+https://github.com/rust-lang/crates.io-index"
101 | checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
102 | dependencies = [
103 | "cfg-if 1.0.0",
104 | "wasm-bindgen",
105 | ]
106 |
107 | [[package]]
108 | name = "fnv"
109 | version = "1.0.7"
110 | source = "registry+https://github.com/rust-lang/crates.io-index"
111 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
112 |
113 | [[package]]
114 | name = "futures"
115 | version = "0.3.17"
116 | source = "registry+https://github.com/rust-lang/crates.io-index"
117 | checksum = "a12aa0eb539080d55c3f2d45a67c3b58b6b0773c1a3ca2dfec66d58c97fd66ca"
118 | dependencies = [
119 | "futures-channel",
120 | "futures-core",
121 | "futures-executor",
122 | "futures-io",
123 | "futures-sink",
124 | "futures-task",
125 | "futures-util",
126 | ]
127 |
128 | [[package]]
129 | name = "futures-channel"
130 | version = "0.3.17"
131 | source = "registry+https://github.com/rust-lang/crates.io-index"
132 | checksum = "5da6ba8c3bb3c165d3c7319fc1cc8304facf1fb8db99c5de877183c08a273888"
133 | dependencies = [
134 | "futures-core",
135 | "futures-sink",
136 | ]
137 |
138 | [[package]]
139 | name = "futures-core"
140 | version = "0.3.17"
141 | source = "registry+https://github.com/rust-lang/crates.io-index"
142 | checksum = "88d1c26957f23603395cd326b0ffe64124b818f4449552f960d815cfba83a53d"
143 |
144 | [[package]]
145 | name = "futures-executor"
146 | version = "0.3.17"
147 | source = "registry+https://github.com/rust-lang/crates.io-index"
148 | checksum = "45025be030969d763025784f7f355043dc6bc74093e4ecc5000ca4dc50d8745c"
149 | dependencies = [
150 | "futures-core",
151 | "futures-task",
152 | "futures-util",
153 | ]
154 |
155 | [[package]]
156 | name = "futures-io"
157 | version = "0.3.17"
158 | source = "registry+https://github.com/rust-lang/crates.io-index"
159 | checksum = "522de2a0fe3e380f1bc577ba0474108faf3f6b18321dbf60b3b9c39a75073377"
160 |
161 | [[package]]
162 | name = "futures-macro"
163 | version = "0.3.17"
164 | source = "registry+https://github.com/rust-lang/crates.io-index"
165 | checksum = "18e4a4b95cea4b4ccbcf1c5675ca7c4ee4e9e75eb79944d07defde18068f79bb"
166 | dependencies = [
167 | "autocfg",
168 | "proc-macro-hack",
169 | "proc-macro2",
170 | "quote",
171 | "syn",
172 | ]
173 |
174 | [[package]]
175 | name = "futures-sink"
176 | version = "0.3.17"
177 | source = "registry+https://github.com/rust-lang/crates.io-index"
178 | checksum = "36ea153c13024fe480590b3e3d4cad89a0cfacecc24577b68f86c6ced9c2bc11"
179 |
180 | [[package]]
181 | name = "futures-task"
182 | version = "0.3.17"
183 | source = "registry+https://github.com/rust-lang/crates.io-index"
184 | checksum = "1d3d00f4eddb73e498a54394f228cd55853bdf059259e8e7bc6e69d408892e99"
185 |
186 | [[package]]
187 | name = "futures-util"
188 | version = "0.3.17"
189 | source = "registry+https://github.com/rust-lang/crates.io-index"
190 | checksum = "36568465210a3a6ee45e1f165136d68671471a501e632e9a98d96872222b5481"
191 | dependencies = [
192 | "autocfg",
193 | "futures-channel",
194 | "futures-core",
195 | "futures-io",
196 | "futures-macro",
197 | "futures-sink",
198 | "futures-task",
199 | "memchr",
200 | "pin-project-lite",
201 | "pin-utils",
202 | "proc-macro-hack",
203 | "proc-macro-nested",
204 | "slab",
205 | ]
206 |
207 | [[package]]
208 | name = "getrandom"
209 | version = "0.2.3"
210 | source = "registry+https://github.com/rust-lang/crates.io-index"
211 | checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753"
212 | dependencies = [
213 | "cfg-if 1.0.0",
214 | "js-sys",
215 | "libc",
216 | "wasi",
217 | "wasm-bindgen",
218 | ]
219 |
220 | [[package]]
221 | name = "gloo"
222 | version = "0.2.1"
223 | source = "registry+https://github.com/rust-lang/crates.io-index"
224 | checksum = "68ce6f2dfa9f57f15b848efa2aade5e1850dc72986b87a2b0752d44ca08f4967"
225 | dependencies = [
226 | "gloo-console-timer",
227 | "gloo-events",
228 | "gloo-file",
229 | "gloo-timers",
230 | ]
231 |
232 | [[package]]
233 | name = "gloo-console-timer"
234 | version = "0.1.0"
235 | source = "registry+https://github.com/rust-lang/crates.io-index"
236 | checksum = "b48675544b29ac03402c6dffc31a912f716e38d19f7e74b78b7e900ec3c941ea"
237 | dependencies = [
238 | "web-sys",
239 | ]
240 |
241 | [[package]]
242 | name = "gloo-events"
243 | version = "0.1.1"
244 | source = "registry+https://github.com/rust-lang/crates.io-index"
245 | checksum = "088514ec8ef284891c762c88a66b639b3a730134714692ee31829765c5bc814f"
246 | dependencies = [
247 | "wasm-bindgen",
248 | "web-sys",
249 | ]
250 |
251 | [[package]]
252 | name = "gloo-file"
253 | version = "0.1.0"
254 | source = "registry+https://github.com/rust-lang/crates.io-index"
255 | checksum = "8f9fecfe46b5dc3cc46f58e98ba580cc714f2c93860796d002eb3527a465ef49"
256 | dependencies = [
257 | "gloo-events",
258 | "js-sys",
259 | "wasm-bindgen",
260 | "web-sys",
261 | ]
262 |
263 | [[package]]
264 | name = "gloo-timers"
265 | version = "0.2.1"
266 | source = "registry+https://github.com/rust-lang/crates.io-index"
267 | checksum = "47204a46aaff920a1ea58b11d03dec6f704287d27561724a4631e450654a891f"
268 | dependencies = [
269 | "js-sys",
270 | "wasm-bindgen",
271 | "web-sys",
272 | ]
273 |
274 | [[package]]
275 | name = "hashbrown"
276 | version = "0.11.2"
277 | source = "registry+https://github.com/rust-lang/crates.io-index"
278 | checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
279 |
280 | [[package]]
281 | name = "heck"
282 | version = "0.3.3"
283 | source = "registry+https://github.com/rust-lang/crates.io-index"
284 | checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c"
285 | dependencies = [
286 | "unicode-segmentation",
287 | ]
288 |
289 | [[package]]
290 | name = "http"
291 | version = "0.2.5"
292 | source = "registry+https://github.com/rust-lang/crates.io-index"
293 | checksum = "1323096b05d41827dadeaee54c9981958c0f94e670bc94ed80037d1a7b8b186b"
294 | dependencies = [
295 | "bytes",
296 | "fnv",
297 | "itoa",
298 | ]
299 |
300 | [[package]]
301 | name = "indexmap"
302 | version = "1.7.0"
303 | source = "registry+https://github.com/rust-lang/crates.io-index"
304 | checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5"
305 | dependencies = [
306 | "autocfg",
307 | "hashbrown",
308 | ]
309 |
310 | [[package]]
311 | name = "itoa"
312 | version = "0.4.8"
313 | source = "registry+https://github.com/rust-lang/crates.io-index"
314 | checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
315 |
316 | [[package]]
317 | name = "js-sys"
318 | version = "0.3.55"
319 | source = "registry+https://github.com/rust-lang/crates.io-index"
320 | checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84"
321 | dependencies = [
322 | "wasm-bindgen",
323 | ]
324 |
325 | [[package]]
326 | name = "lazy_static"
327 | version = "1.4.0"
328 | source = "registry+https://github.com/rust-lang/crates.io-index"
329 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
330 |
331 | [[package]]
332 | name = "lexical-core"
333 | version = "0.7.6"
334 | source = "registry+https://github.com/rust-lang/crates.io-index"
335 | checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe"
336 | dependencies = [
337 | "arrayvec",
338 | "bitflags",
339 | "cfg-if 1.0.0",
340 | "ryu",
341 | "static_assertions",
342 | ]
343 |
344 | [[package]]
345 | name = "libc"
346 | version = "0.2.104"
347 | source = "registry+https://github.com/rust-lang/crates.io-index"
348 | checksum = "7b2f96d100e1cf1929e7719b7edb3b90ab5298072638fccd77be9ce942ecdfce"
349 |
350 | [[package]]
351 | name = "log"
352 | version = "0.4.14"
353 | source = "registry+https://github.com/rust-lang/crates.io-index"
354 | checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
355 | dependencies = [
356 | "cfg-if 1.0.0",
357 | ]
358 |
359 | [[package]]
360 | name = "memchr"
361 | version = "2.4.1"
362 | source = "registry+https://github.com/rust-lang/crates.io-index"
363 | checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
364 |
365 | [[package]]
366 | name = "memory_units"
367 | version = "0.4.0"
368 | source = "registry+https://github.com/rust-lang/crates.io-index"
369 | checksum = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3"
370 |
371 | [[package]]
372 | name = "nom"
373 | version = "5.1.2"
374 | source = "registry+https://github.com/rust-lang/crates.io-index"
375 | checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af"
376 | dependencies = [
377 | "lexical-core",
378 | "memchr",
379 | "version_check",
380 | ]
381 |
382 | [[package]]
383 | name = "pin-project-lite"
384 | version = "0.2.7"
385 | source = "registry+https://github.com/rust-lang/crates.io-index"
386 | checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443"
387 |
388 | [[package]]
389 | name = "pin-utils"
390 | version = "0.1.0"
391 | source = "registry+https://github.com/rust-lang/crates.io-index"
392 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
393 |
394 | [[package]]
395 | name = "ppv-lite86"
396 | version = "0.2.15"
397 | source = "registry+https://github.com/rust-lang/crates.io-index"
398 | checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba"
399 |
400 | [[package]]
401 | name = "proc-macro-hack"
402 | version = "0.5.19"
403 | source = "registry+https://github.com/rust-lang/crates.io-index"
404 | checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5"
405 |
406 | [[package]]
407 | name = "proc-macro-nested"
408 | version = "0.1.7"
409 | source = "registry+https://github.com/rust-lang/crates.io-index"
410 | checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086"
411 |
412 | [[package]]
413 | name = "proc-macro2"
414 | version = "1.0.30"
415 | source = "registry+https://github.com/rust-lang/crates.io-index"
416 | checksum = "edc3358ebc67bc8b7fa0c007f945b0b18226f78437d61bec735a9eb96b61ee70"
417 | dependencies = [
418 | "unicode-xid",
419 | ]
420 |
421 | [[package]]
422 | name = "quote"
423 | version = "1.0.10"
424 | source = "registry+https://github.com/rust-lang/crates.io-index"
425 | checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05"
426 | dependencies = [
427 | "proc-macro2",
428 | ]
429 |
430 | [[package]]
431 | name = "rand"
432 | version = "0.8.4"
433 | source = "registry+https://github.com/rust-lang/crates.io-index"
434 | checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8"
435 | dependencies = [
436 | "libc",
437 | "rand_chacha",
438 | "rand_core",
439 | "rand_hc",
440 | ]
441 |
442 | [[package]]
443 | name = "rand_chacha"
444 | version = "0.3.1"
445 | source = "registry+https://github.com/rust-lang/crates.io-index"
446 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
447 | dependencies = [
448 | "ppv-lite86",
449 | "rand_core",
450 | ]
451 |
452 | [[package]]
453 | name = "rand_core"
454 | version = "0.6.3"
455 | source = "registry+https://github.com/rust-lang/crates.io-index"
456 | checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
457 | dependencies = [
458 | "getrandom",
459 | ]
460 |
461 | [[package]]
462 | name = "rand_hc"
463 | version = "0.3.1"
464 | source = "registry+https://github.com/rust-lang/crates.io-index"
465 | checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7"
466 | dependencies = [
467 | "rand_core",
468 | ]
469 |
470 | [[package]]
471 | name = "ryu"
472 | version = "1.0.5"
473 | source = "registry+https://github.com/rust-lang/crates.io-index"
474 | checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
475 |
476 | [[package]]
477 | name = "scoped-tls"
478 | version = "1.0.0"
479 | source = "registry+https://github.com/rust-lang/crates.io-index"
480 | checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2"
481 |
482 | [[package]]
483 | name = "serde"
484 | version = "1.0.130"
485 | source = "registry+https://github.com/rust-lang/crates.io-index"
486 | checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913"
487 | dependencies = [
488 | "serde_derive",
489 | ]
490 |
491 | [[package]]
492 | name = "serde_derive"
493 | version = "1.0.130"
494 | source = "registry+https://github.com/rust-lang/crates.io-index"
495 | checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b"
496 | dependencies = [
497 | "proc-macro2",
498 | "quote",
499 | "syn",
500 | ]
501 |
502 | [[package]]
503 | name = "serde_json"
504 | version = "1.0.69"
505 | source = "registry+https://github.com/rust-lang/crates.io-index"
506 | checksum = "e466864e431129c7e0d3476b92f20458e5879919a0596c6472738d9fa2d342f8"
507 | dependencies = [
508 | "itoa",
509 | "ryu",
510 | "serde",
511 | ]
512 |
513 | [[package]]
514 | name = "slab"
515 | version = "0.4.5"
516 | source = "registry+https://github.com/rust-lang/crates.io-index"
517 | checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5"
518 |
519 | [[package]]
520 | name = "static_assertions"
521 | version = "1.1.0"
522 | source = "registry+https://github.com/rust-lang/crates.io-index"
523 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
524 |
525 | [[package]]
526 | name = "strum"
527 | version = "0.22.0"
528 | source = "registry+https://github.com/rust-lang/crates.io-index"
529 | checksum = "f7ac893c7d471c8a21f31cfe213ec4f6d9afeed25537c772e08ef3f005f8729e"
530 |
531 | [[package]]
532 | name = "strum_macros"
533 | version = "0.22.0"
534 | source = "registry+https://github.com/rust-lang/crates.io-index"
535 | checksum = "339f799d8b549e3744c7ac7feb216383e4005d94bdb22561b3ab8f3b808ae9fb"
536 | dependencies = [
537 | "heck",
538 | "proc-macro2",
539 | "quote",
540 | "syn",
541 | ]
542 |
543 | [[package]]
544 | name = "syn"
545 | version = "1.0.80"
546 | source = "registry+https://github.com/rust-lang/crates.io-index"
547 | checksum = "d010a1623fbd906d51d650a9916aaefc05ffa0e4053ff7fe601167f3e715d194"
548 | dependencies = [
549 | "proc-macro2",
550 | "quote",
551 | "unicode-xid",
552 | ]
553 |
554 | [[package]]
555 | name = "thiserror"
556 | version = "1.0.30"
557 | source = "registry+https://github.com/rust-lang/crates.io-index"
558 | checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417"
559 | dependencies = [
560 | "thiserror-impl",
561 | ]
562 |
563 | [[package]]
564 | name = "thiserror-impl"
565 | version = "1.0.30"
566 | source = "registry+https://github.com/rust-lang/crates.io-index"
567 | checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b"
568 | dependencies = [
569 | "proc-macro2",
570 | "quote",
571 | "syn",
572 | ]
573 |
574 | [[package]]
575 | name = "unicode-segmentation"
576 | version = "1.8.0"
577 | source = "registry+https://github.com/rust-lang/crates.io-index"
578 | checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b"
579 |
580 | [[package]]
581 | name = "unicode-xid"
582 | version = "0.2.2"
583 | source = "registry+https://github.com/rust-lang/crates.io-index"
584 | checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
585 |
586 | [[package]]
587 | name = "version_check"
588 | version = "0.9.3"
589 | source = "registry+https://github.com/rust-lang/crates.io-index"
590 | checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
591 |
592 | [[package]]
593 | name = "wasi"
594 | version = "0.10.2+wasi-snapshot-preview1"
595 | source = "registry+https://github.com/rust-lang/crates.io-index"
596 | checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
597 |
598 | [[package]]
599 | name = "wasm-bindgen"
600 | version = "0.2.78"
601 | source = "registry+https://github.com/rust-lang/crates.io-index"
602 | checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce"
603 | dependencies = [
604 | "cfg-if 1.0.0",
605 | "wasm-bindgen-macro",
606 | ]
607 |
608 | [[package]]
609 | name = "wasm-bindgen-backend"
610 | version = "0.2.78"
611 | source = "registry+https://github.com/rust-lang/crates.io-index"
612 | checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b"
613 | dependencies = [
614 | "bumpalo",
615 | "lazy_static",
616 | "log",
617 | "proc-macro2",
618 | "quote",
619 | "syn",
620 | "wasm-bindgen-shared",
621 | ]
622 |
623 | [[package]]
624 | name = "wasm-bindgen-futures"
625 | version = "0.4.28"
626 | source = "registry+https://github.com/rust-lang/crates.io-index"
627 | checksum = "8e8d7523cb1f2a4c96c1317ca690031b714a51cc14e05f712446691f413f5d39"
628 | dependencies = [
629 | "cfg-if 1.0.0",
630 | "js-sys",
631 | "wasm-bindgen",
632 | "web-sys",
633 | ]
634 |
635 | [[package]]
636 | name = "wasm-bindgen-macro"
637 | version = "0.2.78"
638 | source = "registry+https://github.com/rust-lang/crates.io-index"
639 | checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9"
640 | dependencies = [
641 | "quote",
642 | "wasm-bindgen-macro-support",
643 | ]
644 |
645 | [[package]]
646 | name = "wasm-bindgen-macro-support"
647 | version = "0.2.78"
648 | source = "registry+https://github.com/rust-lang/crates.io-index"
649 | checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab"
650 | dependencies = [
651 | "proc-macro2",
652 | "quote",
653 | "syn",
654 | "wasm-bindgen-backend",
655 | "wasm-bindgen-shared",
656 | ]
657 |
658 | [[package]]
659 | name = "wasm-bindgen-shared"
660 | version = "0.2.78"
661 | source = "registry+https://github.com/rust-lang/crates.io-index"
662 | checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc"
663 |
664 | [[package]]
665 | name = "wasm-bindgen-test"
666 | version = "0.3.28"
667 | source = "registry+https://github.com/rust-lang/crates.io-index"
668 | checksum = "96f1aa7971fdf61ef0f353602102dbea75a56e225ed036c1e3740564b91e6b7e"
669 | dependencies = [
670 | "console_error_panic_hook",
671 | "js-sys",
672 | "scoped-tls",
673 | "wasm-bindgen",
674 | "wasm-bindgen-futures",
675 | "wasm-bindgen-test-macro",
676 | ]
677 |
678 | [[package]]
679 | name = "wasm-bindgen-test-macro"
680 | version = "0.3.28"
681 | source = "registry+https://github.com/rust-lang/crates.io-index"
682 | checksum = "6006f79628dfeb96a86d4db51fbf1344cd7fd8408f06fc9aa3c84913a4789688"
683 | dependencies = [
684 | "proc-macro2",
685 | "quote",
686 | ]
687 |
688 | [[package]]
689 | name = "web-sys"
690 | version = "0.3.55"
691 | source = "registry+https://github.com/rust-lang/crates.io-index"
692 | checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb"
693 | dependencies = [
694 | "js-sys",
695 | "wasm-bindgen",
696 | ]
697 |
698 | [[package]]
699 | name = "webrtc-introducer-types"
700 | version = "0.1.0"
701 | dependencies = [
702 | "serde",
703 | ]
704 |
705 | [[package]]
706 | name = "wee_alloc"
707 | version = "0.4.5"
708 | source = "registry+https://github.com/rust-lang/crates.io-index"
709 | checksum = "dbb3b5a6b2bb17cb6ad44a2e68a43e8d2722c997da10e928665c72ec6c0a0b8e"
710 | dependencies = [
711 | "cfg-if 0.1.10",
712 | "libc",
713 | "memory_units",
714 | "winapi",
715 | ]
716 |
717 | [[package]]
718 | name = "winapi"
719 | version = "0.3.9"
720 | source = "registry+https://github.com/rust-lang/crates.io-index"
721 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
722 | dependencies = [
723 | "winapi-i686-pc-windows-gnu",
724 | "winapi-x86_64-pc-windows-gnu",
725 | ]
726 |
727 | [[package]]
728 | name = "winapi-i686-pc-windows-gnu"
729 | version = "0.4.0"
730 | source = "registry+https://github.com/rust-lang/crates.io-index"
731 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
732 |
733 | [[package]]
734 | name = "winapi-x86_64-pc-windows-gnu"
735 | version = "0.4.0"
736 | source = "registry+https://github.com/rust-lang/crates.io-index"
737 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
738 |
739 | [[package]]
740 | name = "wraft"
741 | version = "0.1.0"
742 | dependencies = [
743 | "async-trait",
744 | "base64",
745 | "bincode",
746 | "console_error_panic_hook",
747 | "futures",
748 | "getrandom",
749 | "js-sys",
750 | "rand",
751 | "serde",
752 | "strum",
753 | "strum_macros",
754 | "wasm-bindgen",
755 | "wasm-bindgen-futures",
756 | "wasm-bindgen-test",
757 | "web-sys",
758 | "webrtc-introducer-types",
759 | "wee_alloc",
760 | "yew",
761 | "yew-router",
762 | ]
763 |
764 | [[package]]
765 | name = "yew"
766 | version = "0.18.0"
767 | source = "registry+https://github.com/rust-lang/crates.io-index"
768 | checksum = "e4d5154faef86dddd2eb333d4755ea5643787d20aca683e58759b0e53351409f"
769 | dependencies = [
770 | "anyhow",
771 | "anymap",
772 | "bincode",
773 | "cfg-if 1.0.0",
774 | "cfg-match",
775 | "console_error_panic_hook",
776 | "gloo",
777 | "http",
778 | "indexmap",
779 | "js-sys",
780 | "log",
781 | "ryu",
782 | "serde",
783 | "serde_json",
784 | "slab",
785 | "thiserror",
786 | "wasm-bindgen",
787 | "wasm-bindgen-futures",
788 | "web-sys",
789 | "yew-macro",
790 | ]
791 |
792 | [[package]]
793 | name = "yew-macro"
794 | version = "0.18.0"
795 | source = "registry+https://github.com/rust-lang/crates.io-index"
796 | checksum = "d6e23bfe3dc3933fbe9592d149c9985f3047d08c637a884b9344c21e56e092ef"
797 | dependencies = [
798 | "boolinator",
799 | "lazy_static",
800 | "proc-macro2",
801 | "quote",
802 | "syn",
803 | ]
804 |
805 | [[package]]
806 | name = "yew-router"
807 | version = "0.15.0"
808 | source = "registry+https://github.com/rust-lang/crates.io-index"
809 | checksum = "27666236d9597eac9be560e841e415e20ba67020bc8cd081076be178e159c8bc"
810 | dependencies = [
811 | "cfg-if 1.0.0",
812 | "cfg-match",
813 | "gloo",
814 | "js-sys",
815 | "log",
816 | "nom",
817 | "serde",
818 | "serde_json",
819 | "wasm-bindgen",
820 | "web-sys",
821 | "yew",
822 | "yew-router-macro",
823 | "yew-router-route-parser",
824 | ]
825 |
826 | [[package]]
827 | name = "yew-router-macro"
828 | version = "0.15.0"
829 | source = "registry+https://github.com/rust-lang/crates.io-index"
830 | checksum = "4c0ace2924b7a175e2d1c0e62ee7022a5ad840040dcd52414ce5f410ab322dba"
831 | dependencies = [
832 | "proc-macro2",
833 | "quote",
834 | "syn",
835 | "yew-router-route-parser",
836 | ]
837 |
838 | [[package]]
839 | name = "yew-router-route-parser"
840 | version = "0.15.0"
841 | source = "registry+https://github.com/rust-lang/crates.io-index"
842 | checksum = "de4a67208fb46b900af18a7397938b01f379dfc18da34799cfa8347eec715697"
843 | dependencies = [
844 | "nom",
845 | ]
846 |
--------------------------------------------------------------------------------
/wraft/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "wraft"
3 | description = "Raft replication for WebAssembly"
4 | version = "0.1.0"
5 | authors = ["Emanuel Evans "]
6 | edition = "2021"
7 | license = "Apache-2.0"
8 | repository = "https://github.com/shosti/wraft"
9 |
10 | [lib]
11 | crate-type = ["cdylib", "rlib"]
12 |
13 | [features]
14 | default = ["console_error_panic_hook"]
15 |
16 | [dependencies]
17 | futures = "0.3"
18 | wasm-bindgen = "0.2"
19 | wasm-bindgen-futures = "0.4"
20 | js-sys = "0.3"
21 | bincode = "1.3"
22 | serde = { version = "1.0", features = ["derive"] }
23 | webrtc-introducer-types = { path = "../webrtc-introducer-types" }
24 | getrandom = { version = "0.2", features = ["js"] }
25 | wee_alloc = "0.4"
26 | async-trait = "0.1"
27 | rand = "0.8"
28 | base64 = "0.13"
29 | yew = "0.18"
30 | yew-router = "0.15"
31 | strum = "0.22"
32 | strum_macros = "0.22"
33 |
34 | # The `console_error_panic_hook` crate provides better debugging of panics by
35 | # logging them with `console.error`. This is great for development, but requires
36 | # all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
37 | # code size when deploying.
38 | console_error_panic_hook = { version = "0.1.6", optional = true }
39 |
40 | [dependencies.web-sys]
41 | version = "0.3"
42 | features = [
43 | "BinaryType",
44 | "Document",
45 | "DomException",
46 | "Element",
47 | "Event",
48 | "HtmlButtonElement",
49 | "HtmlFormElement",
50 | "HtmlInputElement",
51 | "Location",
52 | "MessageEvent",
53 | "Performance",
54 | "RtcDataChannel",
55 | "RtcDataChannelEvent",
56 | "RtcDataChannelState",
57 | "RtcDataChannelType",
58 | "RtcIceCandidate",
59 | "RtcIceCandidateInit",
60 | "RtcIceConnectionState",
61 | "RtcPeerConnection",
62 | "RtcPeerConnectionIceEvent",
63 | "RtcSdpType",
64 | "RtcSessionDescriptionInit",
65 | "RtcSignalingState",
66 | "Storage",
67 | "WebSocket",
68 | "Window",
69 | ]
70 |
71 | [dev-dependencies]
72 | wasm-bindgen-test = "0.3.13"
73 |
74 | [profile.release]
75 | # Tell `rustc` to optimize for small code size.
76 | opt-level = "s"
77 |
--------------------------------------------------------------------------------
/wraft/README.md:
--------------------------------------------------------------------------------
1 | # WRaft: Raft in WebAssembly
2 |
3 | This crate contains the Rust code for the WebAssembly module for WRaft. It has
4 | a few modules:
5 |
6 | - The top-level module (at `src/lib.rs`) contains sample web applications that
7 | use the Raft library
8 |
9 | - The `src/webrtc_rpc` module contains code for making a makeshift "RPC
10 | framework" from WebRTC data channels (including code for introducing nodes to
11 | each other using a signaling server)
12 |
13 | - The `src/ringbuf` module contains an extremely simple ring buffer
14 | implementation that's used by the `src/webrtc_rpc` module
15 |
16 | - The `src/raft` module contains the actual Raft library code
17 |
--------------------------------------------------------------------------------
/wraft/src/benchmark.rs:
--------------------------------------------------------------------------------
1 | use crate::console_log;
2 | use crate::raft::{self, client::Client, Raft, RaftStateDump};
3 | use crate::raft_init::{self, RaftProps};
4 | use futures::channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender};
5 | use futures::StreamExt;
6 | use serde::{Deserialize, Serialize};
7 | use wasm_bindgen_futures::spawn_local;
8 | use web_sys::window;
9 | use yew::prelude::*;
10 |
11 | pub type Benchmark = raft_init::Model;
12 |
13 | pub struct Model {
14 | state: BenchState,
15 | raft_client: Client,
16 | link: ComponentLink,
17 | result: Option,
18 | state_dump: Option>,
19 | bench_toggle: UnboundedSender<()>,
20 | }
21 |
22 | #[derive(Serialize, Deserialize, Debug, Clone)]
23 | pub enum BenchMsg {
24 | Start,
25 | Iter,
26 | }
27 |
28 | impl raft::Command for BenchMsg {}
29 |
30 | #[derive(Serialize, Deserialize, Debug, Clone, Default)]
31 | pub struct State {}
32 |
33 | pub struct BenchResult {
34 | start: f64,
35 | end: f64,
36 | iters: usize,
37 | }
38 |
39 | impl raft::State for State {
40 | type Command = BenchMsg;
41 | type Item = BenchResult;
42 | type Key = ();
43 | type Notification = BenchMsg;
44 |
45 | fn apply(&mut self, cmd: Self::Command) -> BenchMsg {
46 | cmd
47 | }
48 |
49 | fn get(&self, _: ()) -> Option {
50 | None
51 | }
52 | }
53 |
54 | pub enum Msg {
55 | ToggleBenchmark,
56 | BenchResult(BenchResult),
57 | DumpState,
58 | StateDumped(Box),
59 | }
60 |
61 | pub enum BenchState {
62 | Stopped,
63 | Running,
64 | }
65 |
66 | impl Component for Model {
67 | type Message = Msg;
68 | type Properties = RaftProps;
69 |
70 | fn create(props: Self::Properties, link: ComponentLink) -> Self {
71 | let raft = props.raft.take().unwrap();
72 | let raft_client = raft.client();
73 | let (bench_toggle, bench_toggle_rx) = unbounded();
74 |
75 | Self::run_benchmarker(raft_client.clone(), bench_toggle_rx);
76 | Self::run_update_notifier(raft, link.clone());
77 |
78 | Self {
79 | link,
80 | raft_client,
81 | bench_toggle,
82 | state: BenchState::Stopped,
83 | result: None,
84 | state_dump: None,
85 | }
86 | }
87 |
88 | fn update(&mut self, msg: Self::Message) -> ShouldRender {
89 | match msg {
90 | Msg::ToggleBenchmark => {
91 | self.state = match self.state {
92 | BenchState::Running => BenchState::Stopped,
93 | BenchState::Stopped => BenchState::Running,
94 | };
95 | self.bench_toggle.unbounded_send(()).unwrap();
96 | true
97 | }
98 | Msg::BenchResult(result) => {
99 | self.result = Some(result);
100 | true
101 | }
102 | Msg::DumpState => {
103 | let client = self.raft_client.clone();
104 | let link = self.link.clone();
105 | spawn_local(async move {
106 | if let Ok(dump) = client.debug().await {
107 | link.send_message(Msg::StateDumped(dump));
108 | }
109 | });
110 | false
111 | }
112 | Msg::StateDumped(dump) => {
113 | self.state_dump = Some(dump);
114 | true
115 | }
116 | }
117 | }
118 |
119 | fn change(&mut self, _props: Self::Properties) -> ShouldRender {
120 | false
121 | }
122 |
123 | fn view(&self) -> Html {
124 | match &self.state {
125 | BenchState::Running { .. } => html! {
126 | <>
127 | { "Benchmark running..." }
128 | { self.render_bench_result() }
129 | { "Stop" }
130 | { "Show Raft State" }
131 | { self.render_state_dump() }
132 | >
133 | },
134 | BenchState::Stopped => html! {
135 | <>
136 | { "Benchmark WRaft" }
137 | { self.render_bench_result() }
138 | { "Start" }
139 | { "Show Raft State" }
140 | { self.render_state_dump() }
141 | >
142 | },
143 | }
144 | }
145 | }
146 |
147 | impl Model {
148 | fn run_benchmarker(mut raft_client: Client, mut bench_toggle: UnboundedReceiver<()>) {
149 | spawn_local(async move {
150 | loop {
151 | if let Some(()) = bench_toggle.next().await {
152 | Self::run_benchmark(&mut raft_client, &mut bench_toggle).await;
153 | }
154 | }
155 | });
156 | }
157 |
158 | fn run_update_notifier(mut raft: Raft, link: ComponentLink) {
159 | let performance = window().expect("no global window").performance().unwrap();
160 | spawn_local(async move {
161 | let mut start = performance.now();
162 | let mut iters = 0;
163 | while let Some(msg) = raft.next().await {
164 | match msg {
165 | BenchMsg::Start => {
166 | start = performance.now();
167 | iters = 0;
168 | }
169 | BenchMsg::Iter => {
170 | iters += 1;
171 | if iters % 50 == 0 {
172 | let result = BenchResult {
173 | iters,
174 | start,
175 | end: performance.now(),
176 | };
177 | link.send_message(Msg::BenchResult(result));
178 | }
179 | }
180 | }
181 | }
182 | });
183 | }
184 |
185 | async fn run_benchmark(
186 | raft_client: &mut Client,
187 | bench_toggle: &mut UnboundedReceiver<()>,
188 | ) {
189 | if let Err(err) = raft_client.send(BenchMsg::Start).await {
190 | console_log!("error: {:?}", err);
191 | return;
192 | }
193 | loop {
194 | if bench_toggle.try_next().is_ok() {
195 | return;
196 | }
197 | if let Err(err) = raft_client.send(BenchMsg::Iter).await {
198 | console_log!("error: {:?}", err);
199 | };
200 | }
201 | }
202 |
203 | fn render_bench_result(&self) -> Html {
204 | if let Some(res) = &self.result {
205 | let elapsed_secs = (res.end - res.start) / 1000.0;
206 | let iterations_per_sec = (res.iters as f64) / elapsed_secs;
207 |
208 | html! {
209 |
210 | { "Results:" }
211 | {
212 | format!(
213 | "{} iterations in {:.2} seconds ({:.2} iterations per second)",
214 | res.iters,
215 | elapsed_secs,
216 | iterations_per_sec
217 | )
218 | }
219 |
220 | }
221 | } else {
222 | html! {}
223 | }
224 | }
225 |
226 | fn render_state_dump(&self) -> Html {
227 | if let Some(dump) = &self.state_dump {
228 | html! {
229 | { format!("{:#?}", dump) }
230 | }
231 | } else {
232 | html! {}
233 | }
234 | }
235 | }
236 |
--------------------------------------------------------------------------------
/wraft/src/lib.rs:
--------------------------------------------------------------------------------
1 | pub mod raft;
2 | pub mod raft_init;
3 | pub mod todo_state;
4 |
5 | use todo::Todo;
6 | use yew::prelude::*;
7 | use yew_router::prelude::*;
8 | pub mod ringbuf;
9 | pub mod util;
10 | mod webrtc_rpc;
11 | use wasm_bindgen::prelude::*;
12 | mod benchmark;
13 | mod todo;
14 | use benchmark::Benchmark;
15 |
16 | // Use `wee_alloc` as the global allocator.
17 | #[global_allocator]
18 | static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
19 |
20 | #[derive(Switch, Debug, Clone)]
21 | pub enum Route {
22 | #[to = "/todo"]
23 | Todo,
24 | #[to = "/bench"]
25 | Benchmark,
26 | #[to = "/"]
27 | Home,
28 | }
29 |
30 | pub struct Model {}
31 |
32 | impl Component for Model {
33 | type Message = ();
34 | type Properties = ();
35 |
36 | fn create(_props: Self::Properties, _link: ComponentLink) -> Self {
37 | Self {}
38 | }
39 |
40 | fn update(&mut self, _msg: Self::Message) -> ShouldRender {
41 | false
42 | }
43 |
44 | fn change(&mut self, _props: Self::Properties) -> ShouldRender {
45 | false
46 | }
47 |
48 | fn view(&self) -> Html {
49 | html! {
50 |
51 | render=Router::render(|switch| {
52 | match switch {
53 | Route::Home => html! {
54 | <>
55 | { "Try out WRaft!" }
56 |
57 | route=Route::Todo>{ "Todos" } >
58 | route=Route::Benchmark>{ "Benchmark" } >
59 |
60 | { "What is this?" }
61 | { "This is a toy implementation of the " }{"Raft algorithm"} { " running in browser windows over WebRTC." }
62 | { "For more information, see the " }{ "code" } {" and accompanying "}{"blog post"} { "." }
63 | { "WRaft currently doesn't work on Safari or mobile." }
64 | >
65 | },
66 | Route::Todo => html! {
67 |
68 | },
69 | Route::Benchmark => html! {
70 |
71 | },
72 | }
73 | })
74 | />
75 | }
76 | }
77 | }
78 |
79 | fn get_session_key() -> Option {
80 | let hash = web_sys::window()
81 | .expect("no global window")
82 | .location()
83 | .hash()
84 | .ok()?;
85 | u128::from_str_radix(hash.strip_prefix('#')?, 16).ok()
86 | }
87 |
88 | #[wasm_bindgen(start)]
89 | pub fn start() {
90 | util::set_panic_hook();
91 |
92 | yew::start_app::();
93 | }
94 |
--------------------------------------------------------------------------------
/wraft/src/raft/client.rs:
--------------------------------------------------------------------------------
1 | use crate::{
2 | raft::{
3 | errors::ClientError, ClientMessage, ClientRequest, ClientResponse, RaftStateDump, State,
4 | StateGetRequest,
5 | },
6 | util::sleep,
7 | };
8 | use futures::{
9 | channel::{mpsc::Sender, oneshot},
10 | select, SinkExt,
11 | };
12 | use std::time::Duration;
13 |
14 | const CLIENT_REQUEST_TIMEOUT_MILLIS: u64 = 2000;
15 |
16 | pub struct Client {
17 | client_tx: Sender>,
18 | state_get_tx: Sender>,
19 | }
20 |
21 | impl Client {
22 | pub fn new(
23 | client_tx: Sender>,
24 | state_get_tx: Sender>,
25 | ) -> Self {
26 | Self {
27 | client_tx,
28 | state_get_tx,
29 | }
30 | }
31 |
32 | pub async fn send(&self, val: St::Command) -> Result<(), ClientError> {
33 | let req = ClientRequest::Apply(val);
34 | self.do_client_request(req).await.map(|_| ())
35 | }
36 |
37 | pub async fn get(&self, k: St::Key) -> Result, ClientError> {
38 | let (resp_tx, resp_rx) = oneshot::channel();
39 | let mut tx = self.state_get_tx.clone();
40 | tx.send((k, resp_tx))
41 | .await
42 | .map_err(|_| ClientError::Unavailable)?;
43 | resp_rx.await.map_err(|_| ClientError::Unavailable)
44 | }
45 |
46 | pub async fn debug(&self) -> Result, ClientError> {
47 | let req = ClientRequest::Debug;
48 | match self.do_client_request(req).await {
49 | Ok(ClientResponse::Debug(debug)) => Ok(debug),
50 | Err(err) => Err(err),
51 | _ => unreachable!(),
52 | }
53 | }
54 |
55 | async fn do_client_request(
56 | &self,
57 | req: ClientRequest,
58 | ) -> Result, ClientError> {
59 | let (resp_tx, mut resp_rx) = oneshot::channel();
60 | let mut tx = self.client_tx.clone();
61 | tx.send((req, resp_tx))
62 | .await
63 | .map_err(|_| ClientError::Unavailable)?;
64 | select! {
65 | res = resp_rx => {
66 | match res {
67 | Ok(Ok(resp)) => Ok(resp),
68 | Ok(Err(err)) => Err(err),
69 | Err(_) => Err(ClientError::Unavailable),
70 | }
71 | }
72 | _ = sleep(Duration::from_millis(CLIENT_REQUEST_TIMEOUT_MILLIS)) => {
73 | Err(ClientError::Timeout)
74 | }
75 | }
76 | }
77 | }
78 |
79 | impl Clone for Client {
80 | fn clone(&self) -> Self {
81 | Self {
82 | client_tx: self.client_tx.clone(),
83 | state_get_tx: self.state_get_tx.clone(),
84 | }
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/wraft/src/raft/errors.rs:
--------------------------------------------------------------------------------
1 | use crate::webrtc_rpc::transport;
2 | use serde::{Deserialize, Serialize};
3 | use wasm_bindgen::prelude::*;
4 | use wasm_bindgen::JsCast;
5 | use web_sys::Event;
6 |
7 | #[derive(Debug)]
8 | pub enum Error {
9 | Js(String),
10 | Transport(transport::Error),
11 | NotEnoughPeers,
12 | NotLeader,
13 | CommandTimeout,
14 | }
15 |
16 | #[derive(Serialize, Deserialize, Debug, Clone)]
17 | pub enum ClientError {
18 | Unavailable,
19 | Timeout,
20 | }
21 |
22 | impl From for Error {
23 | fn from(err: JsValue) -> Self {
24 | let msg = match err.as_string() {
25 | Some(e) => e,
26 | None => {
27 | if let Some(ev) = err.dyn_ref::() {
28 | format!(
29 | "error on event with type {} and target {:?}",
30 | ev.type_(),
31 | ev.target()
32 | )
33 | } else {
34 | format!("JS error: {:?}", err)
35 | }
36 | }
37 | };
38 | Error::Js(msg)
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/wraft/src/raft/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod client;
2 | pub mod errors;
3 | mod rpc_server;
4 | mod storage;
5 | mod worker;
6 |
7 | use self::worker::WorkerBuilder;
8 | use crate::console_log;
9 | use crate::webrtc_rpc::introduce;
10 | use crate::webrtc_rpc::transport;
11 | use base64::write::EncoderStringWriter;
12 | use errors::{ClientError, Error};
13 | use futures::channel::mpsc::{channel, unbounded, Receiver, Sender, UnboundedReceiver};
14 | use futures::channel::oneshot;
15 | use futures::select;
16 | use futures::stream::StreamExt;
17 | use futures::Stream;
18 | use rpc_server::RpcServer;
19 | use serde::de::DeserializeOwned;
20 | use serde::{Deserialize, Serialize};
21 | use std::collections::hash_map::DefaultHasher;
22 | use std::collections::HashMap;
23 | use std::fmt::Debug;
24 | use std::hash::{Hash, Hasher};
25 | use std::io::Cursor;
26 | use wasm_bindgen_futures::spawn_local;
27 |
28 | pub type LogIndex = u64;
29 | pub type TermIndex = u64;
30 | pub type NodeId = u64;
31 | pub type RpcMessage = (
32 | RpcRequest,
33 | oneshot::Sender, transport::Error>>,
34 | );
35 | pub type ClientMessage = (
36 | ClientRequest,
37 | oneshot::Sender, ClientError>>,
38 | );
39 |
40 | type StateGetRequest = (
41 | ::Key,
42 | oneshot::Sender::Item>>,
43 | );
44 |
45 | pub struct Raft {
46 | client_tx: Sender>,
47 | state_get_tx: Sender>,
48 | updates_rx: Receiver,
49 | }
50 |
51 | #[derive(Serialize, Deserialize, Debug, Clone)]
52 | pub enum RpcRequest {
53 | AppendEntries(AppendEntriesRequest),
54 | RequestVote(RequestVoteRequest),
55 | ForwardClientRequest(ClientRequest),
56 | }
57 |
58 | #[derive(Serialize, Deserialize, Debug, Clone)]
59 | pub enum RpcResponse {
60 | AppendEntries(AppendEntriesResponse),
61 | RequestVote(RequestVoteResponse),
62 | ForwardClientRequest(Result, ClientError>),
63 | }
64 |
65 | #[derive(Serialize, Deserialize, Debug, Clone)]
66 | pub enum ClientRequest {
67 | Apply(Cmd),
68 | Debug,
69 | }
70 |
71 | #[derive(Serialize, Deserialize, Debug, Clone)]
72 | pub enum ClientResponse {
73 | Ack,
74 | Get(Option),
75 | GetCurrentState(HashMap),
76 | Debug(Box),
77 | }
78 |
79 | #[derive(Serialize, Deserialize, Debug, Clone)]
80 | pub struct AppendEntriesRequest {
81 | term: TermIndex,
82 | leader_id: NodeId,
83 | prev_log_index: LogIndex,
84 | prev_log_term: TermIndex,
85 | entries: Vec>,
86 | leader_commit: LogIndex,
87 | }
88 |
89 | #[derive(Serialize, Deserialize, Debug, Clone)]
90 | pub struct RequestVoteRequest {
91 | term: TermIndex,
92 | candidate: NodeId,
93 | last_log_index: LogIndex,
94 | last_log_term: TermIndex,
95 | }
96 |
97 | #[derive(Serialize, Deserialize, Debug, Clone)]
98 | pub struct RequestVoteResponse {
99 | term: TermIndex,
100 | vote_granted: bool,
101 | }
102 |
103 | #[derive(Serialize, Deserialize, Debug, Clone)]
104 | pub struct AppendEntriesResponse {
105 | term: TermIndex,
106 | success: bool,
107 | }
108 |
109 | #[derive(Serialize, Deserialize, Debug, Clone)]
110 | pub struct LogEntry {
111 | pub cmd: Cmd,
112 | pub term: TermIndex,
113 | pub idx: LogIndex,
114 | }
115 |
116 | #[derive(Serialize, Deserialize, Debug, Clone)]
117 | pub struct RaftStateDump {
118 | state: String,
119 | leader_id: Option,
120 | node_id: NodeId,
121 | session_key: u128,
122 | cluster_size: usize,
123 | peers: Vec,
124 | online_peers: Vec,
125 | voted_for: Option,
126 | current_term: TermIndex,
127 | last_log_index: LogIndex,
128 |
129 | commit_index: LogIndex,
130 | last_applied: LogIndex,
131 | }
132 |
133 | impl Raft {
134 | pub async fn start(
135 | hostname: &str,
136 | session_key: u128,
137 | cluster_size: usize,
138 | ) -> Result {
139 | let (peers_tx, mut peers_rx) = channel(10);
140 | let node_id = Self::generate_node_id(hostname, session_key);
141 |
142 | spawn_local(introduce(node_id, session_key, peers_tx));
143 |
144 | let (rpc_tx, rpc_rx) = channel(100);
145 | let rpc_server = RpcServer::new(rpc_tx);
146 |
147 | let mut peer_clients = HashMap::new();
148 | let peers;
149 | if let Some(p) = Self::load_peer_configuration(session_key) {
150 | // We're rejoining a cluster that's already running
151 | peers = p;
152 | } else {
153 | // We've got to wait for cluster to bootstrap
154 | let mut servers = Vec::new();
155 | let target_size = cluster_size - 1;
156 | while servers.len() < target_size {
157 | let peer = peers_rx.next().await.ok_or(Error::NotEnoughPeers)?;
158 | console_log!("Got client: {}", peer.node_id());
159 |
160 | let peer_id = peer.node_id();
161 | let (client, server) = peer.start();
162 | peer_clients.insert(peer_id, client);
163 | servers.push(server);
164 | }
165 |
166 | while let Some(mut server) = servers.pop() {
167 | let s = rpc_server.clone();
168 | spawn_local(async move {
169 | server.serve(s).await;
170 | });
171 | }
172 | peers = peer_clients.keys().copied().collect();
173 | Self::store_peer_configuration(session_key, &peers);
174 | };
175 |
176 | let (state_machine_tx, state_machine_rx) = unbounded();
177 |
178 | let client_tx = WorkerBuilder {
179 | node_id,
180 | session_key,
181 | peers,
182 | rpc_rx,
183 | peers_rx,
184 | peer_clients,
185 | rpc_server,
186 | state_machine_tx,
187 | }
188 | .start();
189 |
190 | // if no one's listening we basically want to discard updates, so the
191 | // update channel has size 1
192 | let (updates_tx, updates_rx) = channel(1);
193 | let (state_get_tx, state_get_rx) = channel(10);
194 |
195 | spawn_local(Self::handle_state_machine(
196 | state_machine_rx,
197 | updates_tx,
198 | state_get_rx,
199 | ));
200 |
201 | Ok(Self {
202 | client_tx,
203 | state_get_tx,
204 | updates_rx,
205 | })
206 | }
207 |
208 | pub fn client(&self) -> client::Client {
209 | client::Client::new(self.client_tx.clone(), self.state_get_tx.clone())
210 | }
211 |
212 | async fn handle_state_machine(
213 | mut state_machine_rx: UnboundedReceiver,
214 | mut updates_tx: Sender,
215 | mut state_get_rx: Receiver>,
216 | ) {
217 | let mut state = St::default();
218 | loop {
219 | select! {
220 | res = state_machine_rx.next() => {
221 | match res {
222 | Some(cmd) => {
223 | let update = state.apply(cmd);
224 | if let Err(err) = updates_tx.try_send(update) {
225 | if err.is_disconnected() {
226 | console_log!("updates channel closed, exiting state machine handler");
227 | return;
228 | } else if !err.is_full() {
229 | panic!("unexpected error: {:?}", err);
230 | }
231 | // Otherwise, the channel is full; updates are
232 | // best-effort so not an issue really.
233 | }
234 | }
235 | None => {
236 | console_log!("state machine channel closed, exiting state machine handler");
237 | return;
238 | }
239 | }
240 | }
241 | res = state_get_rx.next() => {
242 | match res {
243 | Some((k, resp_tx)) => {
244 | let val = state.get(k);
245 | let _res = resp_tx.send(val);
246 | }
247 | None => {
248 | console_log!("state get channel closed, exiting state machine handler");
249 | return;
250 | }
251 | }
252 | }
253 | }
254 | }
255 | }
256 |
257 | fn generate_node_id(hostname: &str, session_key: u128) -> NodeId {
258 | let mut h = DefaultHasher::new();
259 | hostname.hash(&mut h);
260 | session_key.hash(&mut h);
261 | h.finish()
262 | }
263 |
264 | fn load_peer_configuration(session_key: u128) -> Option> {
265 | let key = Self::peer_configuration_key(session_key);
266 | if let Some(s) = Self::storage().get_item(&key).unwrap() {
267 | let mut data = Cursor::new(s);
268 | let buf = base64::read::DecoderReader::new(&mut data, base64::STANDARD);
269 | let conf = bincode::deserialize_from(buf).unwrap();
270 | Some(conf)
271 | } else {
272 | None
273 | }
274 | }
275 |
276 | fn store_peer_configuration(session_key: u128, peers: &[NodeId]) {
277 | let key = Self::peer_configuration_key(session_key);
278 | let mut buf = EncoderStringWriter::new(base64::STANDARD);
279 | bincode::serialize_into(&mut buf, peers).unwrap();
280 | let data = buf.into_inner();
281 |
282 | Self::storage().set_item(&key, &data).unwrap();
283 | }
284 |
285 | fn peer_configuration_key(session_key: u128) -> String {
286 | format!("peer-configuration-{}", session_key)
287 | }
288 |
289 | fn storage() -> web_sys::Storage {
290 | let window = web_sys::window().expect("no global window");
291 | window.local_storage().expect("no local storage").unwrap()
292 | }
293 | }
294 |
295 | impl Stream for Raft {
296 | type Item = St::Notification;
297 |
298 | fn poll_next(
299 | mut self: std::pin::Pin<&mut Self>,
300 | cx: &mut std::task::Context<'_>,
301 | ) -> std::task::Poll> {
302 | self.updates_rx.poll_next_unpin(cx)
303 | }
304 |
305 | fn size_hint(&self) -> (usize, Option) {
306 | self.updates_rx.size_hint()
307 | }
308 | }
309 |
310 | pub trait Command: Serialize + DeserializeOwned + Debug + Send + 'static {}
311 |
312 | pub trait State: Serialize + DeserializeOwned + Default + 'static {
313 | type Command: Command;
314 | type Item;
315 | type Key;
316 | type Notification;
317 |
318 | fn apply(&mut self, cmd: Self::Command) -> Self::Notification;
319 | fn get(&self, key: Self::Key) -> Option;
320 | }
321 |
322 | #[derive(Serialize, Deserialize, Debug)]
323 | pub enum HashMapCommand {
324 | Set(K, V),
325 | Delete(K),
326 | }
327 |
328 | impl Command for HashMapCommand
329 | where
330 | K: Serialize + DeserializeOwned + std::cmp::Eq + std::hash::Hash + Send + Debug + 'static,
331 | V: Serialize + DeserializeOwned + Send + Debug + 'static,
332 | {
333 | }
334 |
335 | impl State for HashMap
336 | where
337 | K: Serialize
338 | + DeserializeOwned
339 | + Clone
340 | + std::cmp::Eq
341 | + std::hash::Hash
342 | + Send
343 | + Debug
344 | + 'static,
345 | V: Serialize + DeserializeOwned + Clone + Send + Debug + 'static,
346 | S: std::hash::BuildHasher + Default + 'static,
347 | {
348 | type Command = HashMapCommand;
349 | type Item = V;
350 | type Key = K;
351 | type Notification = ();
352 |
353 | fn apply(&mut self, cmd: Self::Command) {
354 | match cmd {
355 | HashMapCommand::Set(k, v) => {
356 | self.insert(k, v);
357 | }
358 | HashMapCommand::Delete(k) => {
359 | self.remove(&k);
360 | }
361 | }
362 | }
363 |
364 | fn get(&self, k: Self::Key) -> Option {
365 | self.get(&k).cloned()
366 | }
367 | }
368 |
--------------------------------------------------------------------------------
/wraft/src/raft/rpc_server.rs:
--------------------------------------------------------------------------------
1 | use crate::raft::{RpcMessage, RpcRequest, RpcResponse};
2 | use crate::webrtc_rpc::transport::{self, RequestHandler};
3 | use async_trait::async_trait;
4 | use futures::channel::mpsc::Sender;
5 | use futures::channel::oneshot;
6 | use futures::SinkExt;
7 | use std::marker::Send;
8 |
9 | #[derive(Debug)]
10 | pub struct RpcServer {
11 | tx: Sender>,
12 | }
13 |
14 | impl RpcServer
15 | where
16 | Cmd: Send,
17 | {
18 | pub fn new(tx: Sender>) -> Self {
19 | Self { tx }
20 | }
21 | }
22 |
23 | impl Clone for RpcServer {
24 | fn clone(&self) -> Self {
25 | Self {
26 | tx: self.tx.clone(),
27 | }
28 | }
29 | }
30 |
31 | #[async_trait]
32 | impl RequestHandler, RpcResponse> for RpcServer
33 | where
34 | Cmd: Send,
35 | {
36 | async fn handle(&self, req: RpcRequest) -> Result, transport::Error> {
37 | let (resp_tx, resp_rx) = oneshot::channel();
38 | self.tx
39 | .clone()
40 | .send((req, resp_tx))
41 | .await
42 | .expect("request channel closed");
43 |
44 | match resp_rx.await {
45 | Ok(resp) => resp,
46 | // If the response channel is closed, we probably changed state
47 | // mid-request so there's no reasonable response
48 | Err(_) => Err(transport::Error::Unavailable),
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/wraft/src/raft/storage.rs:
--------------------------------------------------------------------------------
1 | use crate::raft::{Command, LogEntry, LogIndex, NodeId, TermIndex};
2 | use base64::write::EncoderStringWriter;
3 | use std::fmt::Debug;
4 | use std::io::Cursor;
5 | use std::marker::PhantomData;
6 |
7 | #[derive(Debug)]
8 | pub struct Storage {
9 | session_key: u128,
10 | last_log_index: LogIndex,
11 | last_log_index_key: String,
12 | current_term: TermIndex,
13 | current_term_key: String,
14 | voted_for: Option,
15 | voted_for_key: String,
16 | storage: web_sys::Storage,
17 | _record_type: PhantomData,
18 | }
19 |
20 | impl Storage
21 | where
22 | Cmd: Command,
23 | {
24 | pub fn new(session_key: u128) -> Self {
25 | let window = web_sys::window().expect("no global window");
26 | let storage = window.local_storage().expect("no local storage").unwrap();
27 |
28 | let mut state = Self {
29 | storage,
30 | session_key,
31 | last_log_index: 0,
32 | current_term: 0,
33 | voted_for: None,
34 | current_term_key: format!("current-term-{}", session_key),
35 | voted_for_key: format!("voted-for-{}", session_key),
36 | last_log_index_key: format!("last-log-index-{}", session_key),
37 | _record_type: PhantomData,
38 | };
39 |
40 | if let Some(term) = state.get_persistent(&state.current_term_key) {
41 | state.current_term = term.parse().unwrap();
42 | }
43 |
44 | if let Some(vote) = state.get_persistent(&state.voted_for_key) {
45 | state.voted_for = Some(vote.parse().unwrap());
46 | }
47 |
48 | if let Some(idx) = state.get_persistent(&state.last_log_index_key) {
49 | state.last_log_index = idx.parse().unwrap();
50 | }
51 |
52 | state
53 | }
54 |
55 | pub fn last_log_index(&self) -> LogIndex {
56 | self.last_log_index
57 | }
58 |
59 | fn increment_last_log_index(&mut self) -> LogIndex {
60 | let idx = self.last_log_index() + 1;
61 | self.set_last_log_index(idx);
62 | idx
63 | }
64 |
65 | fn set_last_log_index(&mut self, idx: LogIndex) {
66 | self.last_log_index = idx;
67 | let val = idx.to_string();
68 | self.set_persistent(&self.last_log_index_key, &val);
69 | }
70 |
71 | pub fn last_log_term(&self) -> TermIndex {
72 | match self.get_log(self.last_log_index()) {
73 | Some(entry) => entry.term,
74 | None => 0,
75 | }
76 | }
77 |
78 | // Returns true if the term needed to be updated
79 | pub fn update_term(&mut self, term: TermIndex) -> bool {
80 | if self.current_term() < term {
81 | self.set_voted_for(None);
82 | self.set_current_term(term);
83 | true
84 | } else {
85 | false
86 | }
87 | }
88 |
89 | pub fn increment_term(&mut self) {
90 | self.set_voted_for(None);
91 | self.set_current_term(self.current_term() + 1);
92 | }
93 |
94 | // This explodes if you use it wrong!
95 | pub fn append_log(&mut self, entry: &LogEntry) {
96 | let idx = self.increment_last_log_index();
97 | assert_eq!(idx, entry.idx);
98 |
99 | let key = self.log_key(idx);
100 | let mut buf = EncoderStringWriter::new(base64::STANDARD);
101 | bincode::serialize_into(&mut buf, entry).unwrap();
102 | let data = buf.into_inner();
103 | self.storage.set_item(&key, &data).unwrap();
104 | }
105 |
106 | // Sets log entry at entry.idx and truncates the log from then on out
107 | // (assuming all following logs are invalid)
108 | pub fn overwrite_log(&mut self, entry: &LogEntry) {
109 | assert!(entry.idx >= self.last_log_index());
110 | assert!(entry.idx != 0);
111 | self.set_last_log_index(entry.idx - 1);
112 | self.append_log(entry);
113 | }
114 |
115 | pub fn get_log(&self, idx: LogIndex) -> Option> {
116 | // log indices start at 1, as per the paper
117 | if idx == 0 || idx > self.last_log_index() {
118 | return None;
119 | }
120 | let key = self.log_key(idx);
121 | let mut data = Cursor::new(self.storage.get_item(&key).unwrap().unwrap()); // Christmas!
122 | let buf = base64::read::DecoderReader::new(&mut data, base64::STANDARD);
123 | let entry: LogEntry = bincode::deserialize_from(buf).unwrap();
124 | Some(entry)
125 | }
126 |
127 | pub fn sublog(&self, indices: impl Iterator- ) -> Vec
> {
128 | indices
129 | .map(|i| self.get_log(i))
130 | .filter(Option::is_some)
131 | .flatten()
132 | .collect()
133 | }
134 |
135 | pub fn current_term(&self) -> TermIndex {
136 | self.current_term
137 | }
138 |
139 | fn set_current_term(&mut self, term: TermIndex) {
140 | self.current_term = term;
141 | let val = term.to_string();
142 | self.set_persistent(&self.current_term_key, &val);
143 | }
144 |
145 | pub fn voted_for(&self) -> &Option {
146 | &self.voted_for
147 | }
148 |
149 | pub fn set_voted_for(&mut self, val: Option) {
150 | if let Some(val) = val {
151 | self.voted_for = Some(val);
152 | let sval = val.to_string();
153 | self.set_persistent(&self.voted_for_key, &sval);
154 | } else {
155 | self.storage.remove_item(&self.voted_for_key).unwrap();
156 | self.voted_for = None;
157 | }
158 | }
159 |
160 | fn get_persistent(&self, key: &str) -> Option {
161 | self.storage.get_item(key).unwrap()
162 | }
163 |
164 | fn set_persistent(&self, key: &str, val: &str) {
165 | self.storage.set_item(key, val).unwrap();
166 | }
167 |
168 | fn log_key(&self, idx: LogIndex) -> String {
169 | format!("log-{}-{}", self.session_key, idx)
170 | }
171 | }
172 |
--------------------------------------------------------------------------------
/wraft/src/raft_init.rs:
--------------------------------------------------------------------------------
1 | use crate::raft::{self, Raft};
2 | use rand::{thread_rng, Rng};
3 | use std::marker::PhantomData;
4 | use std::sync::{Arc, Mutex};
5 | use wasm_bindgen_futures::spawn_local;
6 | use web_sys::{Storage, Window};
7 | use yew::prelude::*;
8 | use yew::Properties;
9 |
10 | const CLUSTER_SIZE: usize = 3;
11 |
12 | pub struct RaftWrapper(Arc>>>);
13 |
14 | impl RaftWrapper {
15 | pub fn new(raft: Raft) -> Self {
16 | Self(Arc::new(Mutex::new(Some(raft))))
17 | }
18 |
19 | pub fn take(&self) -> Option> {
20 | let mut r = self.0.lock().unwrap();
21 | r.take()
22 | }
23 | }
24 |
25 | impl Clone for RaftWrapper {
26 | fn clone(&self) -> Self {
27 | Self(self.0.clone())
28 | }
29 | }
30 |
31 | #[derive(Properties, Clone)]
32 | pub struct Props {
33 | pub session_key: Option,
34 | }
35 |
36 | #[derive(Properties)]
37 | pub struct RaftProps {
38 | pub raft: RaftWrapper,
39 | }
40 |
41 | impl Clone for RaftProps {
42 | fn clone(&self) -> Self {
43 | Self {
44 | raft: self.raft.clone(),
45 | }
46 | }
47 | }
48 |
49 | pub enum Msg {
50 | UpdateSessionKey(String),
51 | StartCluster,
52 | JoinCluster,
53 | ClearStorage,
54 | ClusterStarted(RaftWrapper),
55 | }
56 |
57 | enum State {
58 | Setup,
59 | Waiting(u128),
60 | Running(RaftWrapper),
61 | }
62 |
63 | pub struct Model>, S>
64 | where
65 | S: raft::State + Clone,
66 | {
67 | link: ComponentLink,
68 | session_key: String,
69 | state: State,
70 | _component: PhantomData,
71 | _message: PhantomData,
72 | }
73 |
74 | impl>, S> Component for Model
75 | where
76 | S: raft::State + Clone,
77 | {
78 | type Message = Msg;
79 | type Properties = Props;
80 |
81 | fn create(props: Self::Properties, link: ComponentLink) -> Self {
82 | if let Some(session_key) = props.session_key {
83 | Self::start_raft(session_key, link.clone());
84 | Self {
85 | link,
86 | state: State::Waiting(session_key),
87 | session_key: "".into(),
88 | _component: PhantomData,
89 | _message: PhantomData,
90 | }
91 | } else {
92 | Self {
93 | link,
94 | state: State::Setup,
95 | session_key: "".into(),
96 | _component: PhantomData,
97 | _message: PhantomData,
98 | }
99 | }
100 | }
101 |
102 | fn update(&mut self, msg: Self::Message) -> ShouldRender {
103 | match msg {
104 | Msg::UpdateSessionKey(key) => {
105 | self.session_key = key;
106 | true
107 | }
108 | Msg::StartCluster => {
109 | let session_key = generate_session_key();
110 | Self::start_raft(session_key, self.link.clone());
111 | self.state = State::Waiting(session_key);
112 | true
113 | }
114 | Msg::JoinCluster => match u128::from_str_radix(&self.session_key, 16) {
115 | Ok(session_key) => {
116 | Self::start_raft(session_key, self.link.clone());
117 | self.state = State::Waiting(session_key);
118 | true
119 | }
120 | Err(_) => {
121 | web_sys::window()
122 | .unwrap()
123 | .alert_with_message("Invalid session key!")
124 | .unwrap();
125 | false
126 | }
127 | },
128 | Msg::ClearStorage => {
129 | local_storage().clear().unwrap();
130 | window()
131 | .alert_with_message("Storage cleared successfully!")
132 | .unwrap();
133 | true
134 | }
135 | Msg::ClusterStarted(raft) => {
136 | self.state = State::Running(raft);
137 | true
138 | }
139 | }
140 | }
141 |
142 | fn change(&mut self, _props: Self::Properties) -> ShouldRender {
143 | false
144 | }
145 |
146 | fn view(&self) -> Html {
147 | match &self.state {
148 | State::Setup => self.render_setup(),
149 | State::Waiting(session_key) => Self::render_waiting(*session_key),
150 | State::Running(raft) => Self::render_running(raft.clone()),
151 | }
152 | }
153 | }
154 |
155 | impl>, S> Model
156 | where
157 | S: raft::State + Clone,
158 | {
159 | fn render_setup(&self) -> Html {
160 | let start = self.link.callback(|_| Msg::StartCluster);
161 | let session_key = self.session_key.clone();
162 | let local_storage_items = local_storage().length().unwrap();
163 | html! {
164 | <>
165 | { "Startup" }
166 |
167 | { "Start new cluster" }
168 |
169 |
170 | { "Join existing cluster: " }
171 |
181 |
182 |
183 |
184 | {
185 | format!("Clear local storage (currently {} item(s))", local_storage_items)
186 | }
187 |
188 |
189 | >
190 | }
191 | }
192 |
193 | fn render_waiting(session_key: u128) -> Html {
194 | html! {
195 | <>
196 | { "Waiting for cluster to start..." }
197 | { format!("Cluster ID is {:032x}", session_key) }
198 |
199 | { "Open one browser window with each cluster member to start the cluster." }
200 |
{ "Other cluster members:" }
201 | { other_cluster_members(session_key) }
202 |
203 | >
204 | }
205 | }
206 |
207 | fn render_running(raft: RaftWrapper) -> Html {
208 | html! { }
209 | }
210 |
211 | fn start_raft(session_key: u128, link: ComponentLink) {
212 | spawn_local(async move {
213 | let hostname = hostname();
214 | let raft = Raft::start(&hostname, session_key, CLUSTER_SIZE)
215 | .await
216 | .unwrap();
217 | link.send_message(Msg::ClusterStarted(RaftWrapper::new(raft)));
218 | });
219 | }
220 | }
221 |
222 | fn generate_session_key() -> u128 {
223 | thread_rng().gen()
224 | }
225 |
226 | fn hostname() -> String {
227 | window().location().hostname().unwrap()
228 | }
229 |
230 | fn local_storage() -> Storage {
231 | window().local_storage().unwrap().unwrap()
232 | }
233 |
234 | fn window() -> Window {
235 | web_sys::window().unwrap()
236 | }
237 |
238 | fn other_cluster_members(session_key: u128) -> Vec {
239 | let all_targets = vec!["wraft0", "wraft1", "wraft2"];
240 | let hostname = hostname();
241 | let (me, domain) = hostname.split_once('.').unwrap_or((&hostname, ""));
242 | let path = window().location().pathname().unwrap();
243 | all_targets.iter().filter(|&t| t != &me).map(|t| {
244 | let url = format!("https://{}.{}{}#{:032x}", t, domain, path, session_key);
245 | html! {
246 |
248 | }
249 | }).collect()
250 | }
251 |
--------------------------------------------------------------------------------
/wraft/src/ringbuf/mod.rs:
--------------------------------------------------------------------------------
1 | pub struct RingBuf {
2 | data: Vec>,
3 | capacity: usize,
4 | min_idx: usize,
5 | next_idx: usize,
6 | }
7 |
8 | impl RingBuf {
9 | pub fn with_capacity(capacity: usize) -> Self {
10 | let mut data = Vec::with_capacity(capacity);
11 | for _ in 0..capacity {
12 | data.push(None);
13 | }
14 |
15 | Self {
16 | data,
17 | capacity,
18 | min_idx: 0,
19 | next_idx: 0,
20 | }
21 | }
22 |
23 | pub fn add(&mut self, value: T) -> Result> {
24 | let idx = self.next_idx;
25 | if idx - self.min_idx >= self.capacity {
26 | return Err(Error::Overflow(value));
27 | }
28 |
29 | let i = self.data_idx(idx);
30 | self.data[i] = Some((idx, value));
31 | self.next_idx += 1;
32 |
33 | Ok(idx)
34 | }
35 |
36 | pub fn remove(&mut self, idx: usize) -> Option {
37 | let i = self.data_idx(idx);
38 | let val = self.data.get(i).unwrap();
39 | if val.is_none() {
40 | return None;
41 | }
42 | if val.as_ref().unwrap().0 != idx {
43 | return None;
44 | }
45 |
46 | let entry = self.data.get_mut(i).unwrap();
47 | let (_, val) = entry.take().unwrap();
48 | while self.min_idx < self.next_idx && self.data[self.data_idx(self.min_idx)].is_none() {
49 | self.min_idx += 1;
50 | }
51 |
52 | Some(val)
53 | }
54 |
55 | fn data_idx(&self, idx: usize) -> usize {
56 | idx % self.capacity
57 | }
58 | }
59 |
60 | #[derive(Debug, PartialEq)]
61 | pub enum Error {
62 | Overflow(T),
63 | }
64 |
65 | #[cfg(test)]
66 | mod tests {
67 | use super::*;
68 |
69 | #[test]
70 | fn test_overflow() {
71 | let mut buf = RingBuf::with_capacity(4);
72 | for i in 0..4 {
73 | let ret = buf.add("foo");
74 | assert_eq!(ret.unwrap(), i);
75 | }
76 | let ohno = buf.add("nope!");
77 | assert_eq!(ohno, Err(Error::Overflow("nope!")));
78 |
79 | assert_eq!(buf.remove(0).unwrap(), "foo");
80 | assert_eq!(Ok(4), buf.add("ok now!"));
81 | }
82 |
83 | #[test]
84 | fn test_add_variations() {
85 | let mut buf = RingBuf::with_capacity(4);
86 | let a_idx = buf.add("a").unwrap();
87 | let b_idx = buf.add("b").unwrap();
88 | let c_idx = buf.add("c").unwrap();
89 | let d_idx = buf.add("d").unwrap();
90 |
91 | assert_eq!(buf.remove(b_idx).unwrap(), "b");
92 | assert_eq!(buf.remove(c_idx).unwrap(), "c");
93 | assert_eq!(buf.remove(d_idx).unwrap(), "d");
94 | assert_eq!(buf.remove(a_idx).unwrap(), "a");
95 | assert_eq!(buf.remove(b_idx), None);
96 |
97 | let foo_idx = buf.add("foo").unwrap();
98 | assert_eq!(buf.remove(a_idx), None);
99 | assert_eq!(buf.remove(foo_idx).unwrap(), "foo");
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/wraft/src/todo.rs:
--------------------------------------------------------------------------------
1 | use crate::console_log;
2 | use crate::raft::client::Client;
3 | use crate::raft::Raft;
4 | use crate::raft_init::{self, RaftProps};
5 | use crate::todo_state::{self, Entry, Filter, State};
6 | use crate::util::sleep;
7 | use futures::StreamExt;
8 | use serde::{Deserialize, Serialize};
9 | use std::time::Duration;
10 | use strum::IntoEnumIterator;
11 | use wasm_bindgen_futures::spawn_local;
12 | use yew::web_sys::HtmlInputElement as InputElement;
13 | use yew::{
14 | classes, html, Callback, Component, ComponentLink, Html, InputData, NodeRef, ShouldRender,
15 | };
16 | use yew::{events::KeyboardEvent, Classes};
17 |
18 | #[derive(Serialize, Deserialize, Debug, Clone)]
19 | pub enum Msg {
20 | StateUpdate,
21 | GotState(State),
22 | Focus,
23 | }
24 |
25 | pub struct Model {
26 | link: ComponentLink,
27 | focus_ref: NodeRef,
28 | raft_client: Client,
29 | state: State,
30 | }
31 |
32 | pub type Todo = raft_init::Model;
33 |
34 | impl Component for Model {
35 | type Message = Msg;
36 | type Properties = RaftProps;
37 |
38 | fn create(props: Self::Properties, link: ComponentLink) -> Self {
39 | let focus_ref = NodeRef::default();
40 | let raft = props.raft.take().unwrap();
41 | let raft_client = raft.client();
42 |
43 | Self::run_raft(raft, link.clone());
44 | Self {
45 | link,
46 | focus_ref,
47 | raft_client,
48 | state: State::default(),
49 | }
50 | }
51 |
52 | fn update(&mut self, msg: Self::Message) -> ShouldRender {
53 | match msg {
54 | Msg::Focus => {
55 | if let Some(input) = self.focus_ref.cast::() {
56 | input.focus().unwrap();
57 | true
58 | } else {
59 | false
60 | }
61 | }
62 | Msg::StateUpdate => {
63 | let link = self.link.clone();
64 | let raft_client = self.raft_client.clone();
65 | spawn_local(async move {
66 | if let Ok(Some(new_state)) = raft_client.get(()).await {
67 | link.send_message(Msg::GotState(new_state));
68 | }
69 | });
70 | false
71 | }
72 | Msg::GotState(state) => {
73 | self.state = state;
74 | true
75 | }
76 | }
77 | }
78 |
79 | fn change(&mut self, _: Self::Properties) -> ShouldRender {
80 | false
81 | }
82 |
83 | fn view(&self) -> Html {
84 | let hidden_class = if self.state.entries.is_empty() {
85 | "hidden"
86 | } else {
87 | ""
88 | };
89 | html! {
90 | <>
91 |
95 |
99 |
100 |
138 | >
139 | }
140 | }
141 | }
142 |
143 | impl Model {
144 | fn run_raft(mut raft: Raft, link: ComponentLink) {
145 | spawn_local(async move {
146 | while let Some(()) = raft.next().await {
147 | link.send_message(Msg::StateUpdate);
148 | }
149 | });
150 | }
151 |
152 | pub fn raft_callback(&self, function: F) -> Callback
153 | where
154 | F: Fn(IN) -> todo_state::Msg + 'static,
155 | {
156 | let raft_client = self.raft_client.clone();
157 | let closure = move |input| {
158 | let output = function(input);
159 | let r = raft_client.clone();
160 | spawn_local(async move {
161 | loop {
162 | match r.send(output.clone()).await {
163 | Ok(()) => return,
164 | Err(err) => {
165 | console_log!("err: {:?}, retrying...", err);
166 | sleep(Duration::from_millis(500)).await;
167 | }
168 | }
169 | }
170 | });
171 | };
172 | closure.into()
173 | }
174 |
175 | pub fn raft_batch_callback(&self, function: F) -> Callback
176 | where
177 | F: Fn(IN) -> Option + 'static,
178 | {
179 | let raft_client = self.raft_client.clone();
180 | let closure = move |input| {
181 | if let Some(output) = function(input) {
182 | let r = raft_client.clone();
183 | spawn_local(async move {
184 | loop {
185 | match r.send(output.clone()).await {
186 | Ok(()) => return,
187 | Err(err) => {
188 | console_log!("err: {:?}, retrying...", err);
189 | sleep(Duration::from_millis(500)).await;
190 | }
191 | }
192 | }
193 | });
194 | }
195 | };
196 | closure.into()
197 | }
198 |
199 | fn view_filter(&self, filter: Filter) -> Html {
200 | let cls = if self.state.filter == filter {
201 | "selected"
202 | } else {
203 | "not-selected"
204 | };
205 | html! {
206 |
207 |
211 | { filter }
212 |
213 |
214 | }
215 | }
216 |
217 | fn view_input(&self) -> Html {
218 | html! {
219 | // You can use standard Rust comments. One line:
220 | //
221 |
230 | /* Or multiline:
231 |
234 | */
235 | }
236 | }
237 |
238 | fn view_entry(&self, (idx, entry): (usize, &Entry)) -> Html {
239 | let mut class = Classes::from("todo");
240 | if entry.editing {
241 | class.push(" editing");
242 | }
243 | if entry.completed {
244 | class.push(" completed");
245 | }
246 | html! {
247 |
248 |
249 |
255 | { &entry.description }
256 |
257 |
258 | { self.view_entry_edit_input((idx, entry)) }
259 |
260 | }
261 | }
262 |
263 | fn view_entry_edit_input(&self, (idx, entry): (usize, &Entry)) -> Html {
264 | if entry.editing {
265 | html! {
266 |
278 | }
279 | } else {
280 | html! { }
281 | }
282 | }
283 | }
284 |
--------------------------------------------------------------------------------
/wraft/src/todo_state.rs:
--------------------------------------------------------------------------------
1 | use crate::raft;
2 | use serde::{Deserialize, Serialize};
3 | use strum_macros::{Display, EnumIter};
4 |
5 | #[derive(Debug, Serialize, Deserialize, Default, Clone)]
6 | pub struct State {
7 | pub entries: Vec,
8 | pub filter: Filter,
9 | pub value: String,
10 | pub edit_value: String,
11 | }
12 |
13 | #[derive(Serialize, Deserialize, Debug, Clone)]
14 | pub enum Msg {
15 | Add,
16 | Edit(usize),
17 | Update(String),
18 | UpdateEdit(String),
19 | Remove(usize),
20 | SetFilter(Filter),
21 | ToggleAll,
22 | ToggleEdit(usize),
23 | Toggle(usize),
24 | ClearCompleted,
25 | }
26 |
27 | impl raft::Command for Msg {}
28 |
29 | impl State {
30 | pub fn total(&self) -> usize {
31 | self.entries.len()
32 | }
33 |
34 | pub fn total_completed(&self) -> usize {
35 | self.entries
36 | .iter()
37 | .filter(|e| Filter::Completed.fits(e))
38 | .count()
39 | }
40 |
41 | pub fn is_all_completed(&self) -> bool {
42 | let mut filtered_iter = self
43 | .entries
44 | .iter()
45 | .filter(|e| self.filter.fits(e))
46 | .peekable();
47 |
48 | if filtered_iter.peek().is_none() {
49 | return false;
50 | }
51 |
52 | filtered_iter.all(|e| e.completed)
53 | }
54 |
55 | pub fn clear_completed(&mut self) {
56 | let entries = self
57 | .entries
58 | .drain(..)
59 | .filter(|e| Filter::Active.fits(e))
60 | .collect();
61 | self.entries = entries;
62 | }
63 |
64 | pub fn toggle(&mut self, idx: usize) {
65 | let filter = self.filter;
66 | let entry = self
67 | .entries
68 | .iter_mut()
69 | .filter(|e| filter.fits(e))
70 | .nth(idx)
71 | .unwrap();
72 | entry.completed = !entry.completed;
73 | }
74 |
75 | pub fn toggle_all(&mut self, value: bool) {
76 | for entry in &mut self.entries {
77 | if self.filter.fits(entry) {
78 | entry.completed = value;
79 | }
80 | }
81 | }
82 |
83 | pub fn toggle_edit(&mut self, idx: usize) {
84 | let filter = self.filter;
85 | let entry = self
86 | .entries
87 | .iter_mut()
88 | .filter(|e| filter.fits(e))
89 | .nth(idx)
90 | .unwrap();
91 | entry.editing = !entry.editing;
92 | }
93 |
94 | pub fn clear_all_edit(&mut self) {
95 | for entry in &mut self.entries {
96 | entry.editing = false;
97 | }
98 | }
99 |
100 | pub fn complete_edit(&mut self, idx: usize, val: String) {
101 | if val.is_empty() {
102 | self.remove(idx);
103 | } else {
104 | let filter = self.filter;
105 | let entry = self
106 | .entries
107 | .iter_mut()
108 | .filter(|e| filter.fits(e))
109 | .nth(idx)
110 | .unwrap();
111 | entry.description = val;
112 | entry.editing = !entry.editing;
113 | }
114 | }
115 |
116 | pub fn remove(&mut self, idx: usize) {
117 | let idx = {
118 | let entries = self
119 | .entries
120 | .iter()
121 | .enumerate()
122 | .filter(|&(_, e)| self.filter.fits(e))
123 | .collect::>();
124 | let &(idx, _) = entries.get(idx).unwrap();
125 | idx
126 | };
127 | self.entries.remove(idx);
128 | }
129 | }
130 |
131 | #[derive(Debug, Serialize, Deserialize, Clone)]
132 | pub struct Entry {
133 | pub description: String,
134 | pub completed: bool,
135 | pub editing: bool,
136 | }
137 |
138 | #[derive(Clone, Copy, Debug, EnumIter, Display, PartialEq, Serialize, Deserialize)]
139 | pub enum Filter {
140 | All,
141 | Active,
142 | Completed,
143 | }
144 | impl Filter {
145 | pub fn fits(&self, entry: &Entry) -> bool {
146 | match *self {
147 | Filter::All => true,
148 | Filter::Active => !entry.completed,
149 | Filter::Completed => entry.completed,
150 | }
151 | }
152 |
153 | pub fn as_href(&self) -> &'static str {
154 | match self {
155 | Filter::All => "#/",
156 | Filter::Active => "#/active",
157 | Filter::Completed => "#/completed",
158 | }
159 | }
160 | }
161 | impl Default for Filter {
162 | fn default() -> Self {
163 | Filter::All
164 | }
165 | }
166 |
167 | impl raft::State for State {
168 | type Command = Msg;
169 | type Item = Self;
170 | type Key = ();
171 | type Notification = ();
172 |
173 | fn apply(&mut self, cmd: Self::Command) {
174 | match cmd {
175 | Msg::Add => {
176 | let description = self.value.trim();
177 | if !description.is_empty() {
178 | let entry = Entry {
179 | description: description.to_string(),
180 | completed: false,
181 | editing: false,
182 | };
183 | self.entries.push(entry);
184 | }
185 | self.value = "".to_string();
186 | }
187 | Msg::Edit(idx) => {
188 | let edit_value = self.edit_value.trim().to_string();
189 | self.complete_edit(idx, edit_value);
190 | self.edit_value = "".to_string();
191 | }
192 | Msg::Update(val) => {
193 | self.value = val;
194 | }
195 | Msg::UpdateEdit(val) => {
196 | self.edit_value = val;
197 | }
198 | Msg::Remove(idx) => {
199 | self.remove(idx);
200 | }
201 | Msg::SetFilter(filter) => {
202 | self.filter = filter;
203 | }
204 | Msg::ToggleEdit(idx) => {
205 | self.edit_value = self.entries[idx].description.clone();
206 | self.clear_all_edit();
207 | self.toggle_edit(idx);
208 | }
209 | Msg::ToggleAll => {
210 | let status = !self.is_all_completed();
211 | self.toggle_all(status);
212 | }
213 | Msg::Toggle(idx) => {
214 | self.toggle(idx);
215 | }
216 | Msg::ClearCompleted => {
217 | self.clear_completed();
218 | }
219 | }
220 | }
221 |
222 | fn get(&self, _key: Self::Key) -> Option {
223 | Some(self.clone())
224 | }
225 | }
226 |
--------------------------------------------------------------------------------
/wraft/src/util/mod.rs:
--------------------------------------------------------------------------------
1 | use futures::channel::mpsc::{channel, Receiver};
2 | use futures::channel::oneshot;
3 | use futures::future::FusedFuture;
4 | use futures::stream::FusedStream;
5 | use futures::task::{Context, Poll};
6 | use futures::{Future, FutureExt, Stream};
7 | use futures::{SinkExt, StreamExt};
8 | use std::convert::TryInto;
9 | use std::pin::Pin;
10 | use std::time::Duration;
11 | use wasm_bindgen::prelude::*;
12 | use wasm_bindgen::JsCast;
13 | use wasm_bindgen_futures::spawn_local;
14 | use web_sys::{window, Window};
15 |
16 | #[wasm_bindgen]
17 | extern "C" {
18 | #[wasm_bindgen(js_namespace = console)]
19 | pub fn log(s: &str);
20 | }
21 |
22 | #[macro_export]
23 | macro_rules! console_log {
24 | ($($t:tt)*) => (crate::util::log(&format_args!($($t)*).to_string()))
25 | }
26 |
27 | pub fn set_panic_hook() {
28 | // When the `console_error_panic_hook` feature is enabled, we can call the
29 | // `set_panic_hook` function at least once during initialization, and then
30 | // we will get better error messages if our code ever panics.
31 | //
32 | // For more details see
33 | // https://github.com/rustwasm/console_error_panic_hook#readme
34 | #[cfg(feature = "console_error_panic_hook")]
35 | console_error_panic_hook::set_once();
36 | }
37 |
38 | pub struct Sleep {
39 | rx: oneshot::Receiver<()>,
40 | timeout_handle: i32,
41 | _cb: Closure,
42 | }
43 |
44 | impl Sleep {
45 | pub fn new(d: Duration) -> Self {
46 | let (tx, rx) = oneshot::channel::<()>();
47 | let _cb = Closure::once(move || {
48 | tx.send(()).unwrap();
49 | });
50 |
51 | let timeout_handle = get_window()
52 | .set_timeout_with_callback_and_timeout_and_arguments_0(
53 | _cb.as_ref().unchecked_ref(),
54 | d.as_millis().try_into().unwrap(),
55 | )
56 | .unwrap();
57 |
58 | Self {
59 | rx,
60 | timeout_handle,
61 | _cb,
62 | }
63 | }
64 | }
65 |
66 | impl Drop for Sleep {
67 | fn drop(&mut self) {
68 | get_window().clear_timeout_with_handle(self.timeout_handle);
69 | }
70 | }
71 |
72 | impl Future for Sleep {
73 | type Output = ();
74 |
75 | fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll {
76 | match self.rx.poll_unpin(cx) {
77 | Poll::Ready(Ok(())) => Poll::Ready(()),
78 | Poll::Ready(Err(err)) => panic!("sleep receiver poll failed: {}", err),
79 | Poll::Pending => Poll::Pending,
80 | }
81 | }
82 | }
83 |
84 | impl FusedFuture for Sleep {
85 | fn is_terminated(&self) -> bool {
86 | self.rx.is_terminated()
87 | }
88 | }
89 |
90 | pub fn get_window() -> Window {
91 | window().expect("no global window")
92 | }
93 |
94 | pub fn sleep(d: Duration) -> Sleep {
95 | Sleep::new(d)
96 | }
97 |
98 | pub struct Interval {
99 | rx: Receiver<()>,
100 | interval_id: i32,
101 | _cb: Closure,
102 | }
103 |
104 | impl Interval {
105 | pub fn new(d: Duration) -> Self {
106 | let (tx, rx) = channel(5);
107 |
108 | let cb = Closure::wrap(Box::new(move || {
109 | let mut t = tx.clone();
110 | spawn_local(async move {
111 | t.send(()).await.unwrap();
112 | });
113 | }) as Box);
114 | let interval_id = window()
115 | .expect("no global window")
116 | .set_interval_with_callback_and_timeout_and_arguments_0(
117 | cb.as_ref().unchecked_ref(),
118 | d.as_millis().try_into().unwrap(),
119 | )
120 | .unwrap();
121 |
122 | Self {
123 | interval_id,
124 | rx,
125 | _cb: cb,
126 | }
127 | }
128 | }
129 |
130 | pub fn interval(d: Duration) -> Interval {
131 | Interval::new(d)
132 | }
133 |
134 | impl Drop for Interval {
135 | fn drop(&mut self) {
136 | window()
137 | .expect("no global window")
138 | .clear_interval_with_handle(self.interval_id);
139 | }
140 | }
141 |
142 | impl Stream for Interval {
143 | type Item = ();
144 |
145 | fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> {
146 | self.rx.poll_next_unpin(cx)
147 | }
148 |
149 | fn size_hint(&self) -> (usize, Option) {
150 | self.rx.size_hint()
151 | }
152 | }
153 |
154 | impl FusedStream for Interval {
155 | fn is_terminated(&self) -> bool {
156 | self.rx.is_terminated()
157 | }
158 | }
159 |
--------------------------------------------------------------------------------
/wraft/src/webrtc_rpc/introduction.rs:
--------------------------------------------------------------------------------
1 | use crate::console_log;
2 | use crate::util::sleep;
3 | use crate::webrtc_rpc::error::Error;
4 | use crate::webrtc_rpc::transport::PeerTransport;
5 | use futures::channel::mpsc::{channel, Receiver, Sender};
6 | use futures::channel::oneshot;
7 | use futures::select;
8 | use futures::sink::SinkExt;
9 | use futures::stream::{FuturesUnordered, StreamExt};
10 | use js_sys::Reflect;
11 | use std::collections::{HashMap, HashSet};
12 | use std::sync::{Arc, RwLock};
13 | use std::time::Duration;
14 | use wasm_bindgen::prelude::*;
15 | use wasm_bindgen::JsCast;
16 | use wasm_bindgen_futures::{spawn_local, JsFuture};
17 | use web_sys::{
18 | Event, MessageEvent, RtcDataChannel, RtcDataChannelEvent, RtcDataChannelState,
19 | RtcIceCandidateInit, RtcPeerConnection, RtcPeerConnectionIceEvent, RtcSdpType,
20 | RtcSessionDescriptionInit, WebSocket,
21 | };
22 | use webrtc_introducer_types::{Command, IceCandidate, Join, Offer, Session};
23 |
24 | static INTRODUCER: &str = "wss://webrtc-introducer.eevans.co";
25 | static ACK: &str = "ACK";
26 |
27 | type PeerInfo = (u64, RtcPeerConnection, RtcDataChannel);
28 |
29 | #[derive(Clone)]
30 | struct State {
31 | node_id: u64,
32 | session_id: u128,
33 | peers: Arc>>,
34 | online: Arc>>,
35 | peer_tx: Sender,
36 | }
37 |
38 | impl State {
39 | pub fn new(node_id: u64, session_id: u128, peer_tx: Sender) -> Self {
40 | Self {
41 | node_id,
42 | session_id,
43 | peers: Arc::new(RwLock::new(HashMap::new())),
44 | online: Arc::new(RwLock::new(HashSet::new())),
45 | peer_tx,
46 | }
47 | }
48 |
49 | pub fn insert_peer(&self, peer_id: u64, pc: RtcPeerConnection) {
50 | let mut peers = self.peers.write().unwrap();
51 | let mut online = self.online.write().unwrap();
52 | peers.insert(peer_id, pc);
53 | online.insert(peer_id);
54 | }
55 |
56 | pub async fn send_peer(&self, peer_id: u64, dc: RtcDataChannel) -> Result<(), Error> {
57 | let mut tx = self.peer_tx.clone();
58 | let pc;
59 | {
60 | let mut peers = self.peers.write().unwrap();
61 | if let Some(p) = peers.remove(&peer_id) {
62 | pc = p;
63 | } else {
64 | panic!("peer {:#} sent twice", peer_id);
65 | }
66 | }
67 | tx.send((peer_id, pc, dc)).await?;
68 |
69 | Ok(())
70 | }
71 | }
72 |
73 | pub async fn initiate(
74 | node_id: u64,
75 | session_id: u128,
76 | mut peers: Sender,
77 | ) -> Result<(), Error> {
78 | let (peer_tx, mut peer_rx) = channel::(10);
79 | let state = State::new(node_id, session_id, peer_tx);
80 |
81 | let (ws, mut errors_rx) = init_ws(&state).await?;
82 | wait_for_ws_opened(ws.clone()).await;
83 | send_join_command(node_id, session_id, &ws)?;
84 |
85 | loop {
86 | select! {
87 | p = peer_rx.next() => {
88 | let (done_tx, done_rx) = oneshot::channel();
89 | let (peer_id, dc, pc) = p.unwrap();
90 | let peer = PeerTransport::new(peer_id, dc, pc, done_tx);
91 |
92 | let online = state.online.clone();
93 | spawn_local(async move {
94 | done_rx.await.expect("peer transport done channel dropped");
95 | let mut o = online.write().unwrap();
96 | o.remove(&peer_id);
97 | });
98 | peers.send(peer).await?;
99 | }
100 | err = errors_rx.next() => return Err(err.unwrap()),
101 | }
102 | }
103 | }
104 |
105 | async fn handle_message(e: MessageEvent, ws: WebSocket, state: State) -> Result<(), Error> {
106 | let abuf = e
107 | .data()
108 | .dyn_into::()
109 | .expect("Expected message in binary format");
110 | let data = js_sys::Uint8Array::new(&abuf).to_vec();
111 | let command: Command = bincode::deserialize(&data)?;
112 |
113 | match command {
114 | Command::SessionStatus(session) => handle_session_update(session, ws, state).await,
115 | Command::Offer(offer) => handle_offer(offer, ws, state).await,
116 | Command::Answer(answer) => handle_answer(answer, state).await,
117 | Command::IceCandidate(candidate) => handle_ice_candidate(candidate, state).await,
118 | _ => unreachable!(),
119 | }
120 | }
121 |
122 | async fn handle_session_update(session: Session, ws: WebSocket, state: State) -> Result<(), Error> {
123 | console_log!("SESSION UPDATE, ONLINE: {:?}", session.online);
124 | let mut introductions = session
125 | .online
126 | .iter()
127 | .filter(|&p| {
128 | let online = state.online.read().unwrap();
129 | p > &state.node_id && !online.contains(p)
130 | })
131 | .map(|peer| async { introduce(*peer, ws.clone(), state.clone()).await })
132 | .collect::>();
133 | while introductions.next().await.is_some() {}
134 | Ok(())
135 | }
136 |
137 | async fn introduce(peer_id: u64, ws: WebSocket, state: State) -> Result<(), Error> {
138 | let pc = new_peer_connection(peer_id, ws.clone(), &state)?;
139 | let dc = pc.create_data_channel(
140 | format!(
141 | "data-{:#x}-{:#x}-{:#x}",
142 | state.session_id, state.node_id, peer_id
143 | )
144 | .as_str(),
145 | );
146 | let sdp_data = local_description(&pc).await?;
147 | state.insert_peer(peer_id, pc);
148 |
149 | let (done_tx, mut done_rx) = futures::channel::oneshot::channel::<()>();
150 | let dc_clone = dc.clone();
151 | spawn_local(async move {
152 | wait_for_dc_initiated(dc_clone).await;
153 | done_tx.send(()).unwrap();
154 | });
155 |
156 | loop {
157 | send_offer(peer_id, &sdp_data, &ws, &state)?;
158 | let mut retry = sleep(Duration::from_secs(5));
159 | select! {
160 | res = done_rx => {
161 | res.unwrap();
162 | break;
163 | }
164 | _ = retry => {
165 | console_log!("retrying offer to {}", peer_id);
166 | }
167 | }
168 | }
169 | assert_eq!(dc.ready_state(), RtcDataChannelState::Open);
170 |
171 | state.send_peer(peer_id, dc).await?;
172 |
173 | Ok(())
174 | }
175 |
176 | fn new_peer_connection(
177 | peer_id: u64,
178 | ws: WebSocket,
179 | state: &State,
180 | ) -> Result {
181 | let pc = RtcPeerConnection::new()?;
182 | let session_id = state.session_id;
183 | let node_id = state.node_id;
184 | let ice_cb = Closure::wrap(Box::new(move |ev: RtcPeerConnectionIceEvent| {
185 | if let Some(candidate) = ev.candidate() {
186 | let cmd = Command::IceCandidate(IceCandidate {
187 | session_id,
188 | node_id,
189 | target_id: peer_id,
190 | candidate: candidate.candidate(),
191 | sdp_mid: candidate.sdp_mid(),
192 | });
193 | send_command(ws.clone().as_ref(), &cmd).unwrap();
194 | }
195 | }) as Box);
196 | pc.set_onicecandidate(Some(ice_cb.as_ref().unchecked_ref()));
197 | ice_cb.forget();
198 |
199 | Ok(pc)
200 | }
201 |
202 | fn send_command(ws: &WebSocket, command: &Command) -> Result<(), Error> {
203 | let data = bincode::serialize(command)?;
204 | ws.send_with_u8_array(&data)?;
205 | Ok(())
206 | }
207 |
208 | async fn handle_offer(offer: Offer, ws: WebSocket, state: State) -> Result<(), Error> {
209 | let peer_id = offer.node_id;
210 | let pc = new_peer_connection(peer_id, ws.clone(), &state)?;
211 | state.insert_peer(peer_id, pc.clone());
212 |
213 | let (dc_tx, dc_rx) = futures::channel::oneshot::channel::();
214 |
215 | let pc_clone = pc.clone();
216 | spawn_local(async move {
217 | let dc = wait_for_data_channel(pc_clone).await;
218 | dc_tx.send(dc).unwrap();
219 | });
220 |
221 | let answer_sdp = remote_description(&offer.sdp_data, &pc).await?;
222 | send_answer(peer_id, &answer_sdp, &ws, &state)?;
223 |
224 | let dc = dc_rx.await.unwrap();
225 | state.send_peer(peer_id, dc).await?;
226 |
227 | Ok(())
228 | }
229 |
230 | async fn handle_answer(answer: Offer, state: State) -> Result<(), Error> {
231 | let pc;
232 | {
233 | let peers = state.peers.read().unwrap();
234 | let pc_opt = peers.get(&answer.node_id);
235 | if let Some(p) = pc_opt {
236 | pc = p.clone();
237 | } else {
238 | return Err(Error::AlreadyInitialized(answer.node_id));
239 | }
240 | }
241 | let mut desc = RtcSessionDescriptionInit::new(RtcSdpType::Answer);
242 | desc.sdp(&answer.sdp_data);
243 | JsFuture::from(pc.set_remote_description(&desc)).await?;
244 |
245 | Ok(())
246 | }
247 |
248 | async fn handle_ice_candidate(candidate: IceCandidate, state: State) -> Result<(), Error> {
249 | let add_ice_promise;
250 | {
251 | let peers = state.peers.read().unwrap();
252 | let pc_opt = peers.get(&candidate.node_id);
253 | if let Some(pc) = pc_opt {
254 | let mut cand = RtcIceCandidateInit::new(&candidate.candidate);
255 | if let Some(sdp_mid) = candidate.sdp_mid {
256 | cand.sdp_mid(Some(&sdp_mid));
257 | } else {
258 | cand.sdp_mid(None);
259 | }
260 | add_ice_promise = pc.add_ice_candidate_with_opt_rtc_ice_candidate_init(Some(&cand));
261 | } else {
262 | // Can't really be an error because it happens "normally" sometimes
263 | console_log!(
264 | "got ICE candidate for {} but it's already initialized",
265 | candidate.node_id
266 | );
267 | return Ok(());
268 | }
269 | }
270 | JsFuture::from(add_ice_promise).await?;
271 | Ok(())
272 | }
273 |
274 | async fn wait_for_ws_opened(ws: WebSocket) {
275 | let (opened_tx, mut opened_rx) = channel::<()>(1);
276 | let onopen_cb = Closure::wrap(Box::new(move || {
277 | let mut tx = opened_tx.clone();
278 | spawn_local(async move {
279 | tx.send(()).await.unwrap();
280 | });
281 | }) as Box);
282 | ws.set_onopen(Some(onopen_cb.as_ref().unchecked_ref()));
283 |
284 | match opened_rx.next().await {
285 | Some(()) => (),
286 | None => unreachable!(),
287 | }
288 | ws.set_onopen(None);
289 | }
290 |
291 | async fn init_ws(state: &State) -> Result<(WebSocket, Receiver), Error> {
292 | let ws = WebSocket::new(INTRODUCER)?;
293 | ws.set_binary_type(web_sys::BinaryType::Arraybuffer);
294 |
295 | let (errors_tx, errors_rx) = channel::(10);
296 |
297 | let w0 = ws.clone();
298 | let message_state = state.clone();
299 | let message_cb = Closure::wrap(Box::new(move |e: MessageEvent| {
300 | let mut errors = errors_tx.clone();
301 | let s = message_state.clone();
302 | let w1 = w0.clone();
303 | spawn_local(async move {
304 | match handle_message(e, w1.clone(), s.clone()).await {
305 | Ok(()) => (),
306 | Err(err) => {
307 | errors.send(err).await.unwrap();
308 | }
309 | }
310 | });
311 | }) as Box);
312 |
313 | ws.set_onmessage(Some(message_cb.as_ref().unchecked_ref()));
314 | message_cb.forget();
315 |
316 | let onerror_cb = Closure::wrap(Box::new(move |ev: Event| {
317 | console_log!("WEBSOCKET ERROR: {:?}", ev);
318 | }) as Box);
319 | ws.set_onerror(Some(onerror_cb.as_ref().unchecked_ref()));
320 | onerror_cb.forget();
321 |
322 | Ok((ws, errors_rx))
323 | }
324 |
325 | async fn wait_for_dc_initiated(dc: RtcDataChannel) {
326 | let (done_tx, done_rx) = oneshot::channel::<()>();
327 |
328 | let data_cb = Closure::once(move |ev: MessageEvent| {
329 | match ev.data().as_string() {
330 | Some(msg) if msg == ACK => {
331 | // When we get a message from the peer, we know the data channel is
332 | // ready!
333 | spawn_local(async move {
334 | done_tx.send(()).unwrap();
335 | });
336 | }
337 | msg => panic!("Unexpected message on data stream: {:#?}", msg),
338 | }
339 | });
340 | dc.set_onmessage(Some(data_cb.as_ref().unchecked_ref()));
341 |
342 | done_rx.await.unwrap();
343 | dc.set_onmessage(None);
344 | }
345 |
346 | async fn local_description(pc: &RtcPeerConnection) -> Result {
347 | let offer = JsFuture::from(pc.create_offer()).await?;
348 | let sdp_data = Reflect::get(&offer, &JsValue::from_str("sdp"))?
349 | .as_string()
350 | .unwrap();
351 | let mut desc = RtcSessionDescriptionInit::new(RtcSdpType::Offer);
352 | desc.sdp(&sdp_data);
353 | JsFuture::from(pc.set_local_description(&desc)).await?;
354 |
355 | Ok(sdp_data)
356 | }
357 |
358 | async fn remote_description(sdp_data: &str, pc: &RtcPeerConnection) -> Result {
359 | let mut offer_desc = RtcSessionDescriptionInit::new(RtcSdpType::Offer);
360 | offer_desc.sdp(sdp_data);
361 | JsFuture::from(pc.set_remote_description(&offer_desc)).await?;
362 | let answer = JsFuture::from(pc.create_answer()).await?;
363 | let answer_sdp = Reflect::get(&answer, &JsValue::from_str("sdp"))?
364 | .as_string()
365 | .unwrap();
366 | let mut answer_desc = RtcSessionDescriptionInit::new(RtcSdpType::Answer);
367 | answer_desc.sdp(&answer_sdp);
368 | JsFuture::from(pc.set_local_description(&answer_desc)).await?;
369 |
370 | Ok(answer_sdp)
371 | }
372 |
373 | fn send_offer(
374 | peer_id: u64,
375 | sdp_data: &str,
376 | ws: &WebSocket,
377 | state: &State,
378 | ) -> Result<(), Error> {
379 | let cmd = Command::Offer(Offer {
380 | session_id: state.session_id,
381 | node_id: state.node_id,
382 | target_id: peer_id,
383 | sdp_data: sdp_data.to_string(),
384 | });
385 | send_command(ws, &cmd)
386 | }
387 |
388 | fn send_answer(
389 | peer_id: u64,
390 | answer_sdp: &str,
391 | ws: &WebSocket,
392 | state: &State,
393 | ) -> Result<(), Error> {
394 | let cmd = Command::Answer(Offer {
395 | session_id: state.session_id,
396 | node_id: state.node_id,
397 | target_id: peer_id,
398 | sdp_data: answer_sdp.to_string(),
399 | });
400 | send_command(ws, &cmd)
401 | }
402 |
403 | fn send_join_command(node_id: u64, session_id: u128, ws: &WebSocket) -> Result<(), Error> {
404 | let cmd = Command::Join(Join {
405 | node_id,
406 | session_id,
407 | });
408 | send_command(ws, &cmd)
409 | }
410 |
411 | async fn wait_for_data_channel(pc: RtcPeerConnection) -> RtcDataChannel {
412 | let (done_tx, mut done_rx) = channel::(1);
413 |
414 | let data_cb = Closure::wrap(Box::new(move |ev: RtcDataChannelEvent| {
415 | let dc = ev.channel();
416 | dc.send_with_str(ACK).unwrap();
417 |
418 | assert_eq!(dc.ready_state(), RtcDataChannelState::Open);
419 |
420 | let mut tx = done_tx.clone();
421 | spawn_local(async move {
422 | tx.send(dc).await.unwrap();
423 | });
424 | }) as Box);
425 | pc.set_ondatachannel(Some(data_cb.as_ref().unchecked_ref()));
426 |
427 | if let Some(dc) = done_rx.next().await {
428 | pc.set_ondatachannel(None);
429 | dc
430 | } else {
431 | panic!("No channel received")
432 | }
433 | }
434 |
--------------------------------------------------------------------------------
/wraft/src/webrtc_rpc/mod.rs:
--------------------------------------------------------------------------------
1 | mod introduction;
2 | pub mod transport;
3 | use futures::channel::mpsc::Sender;
4 | use transport::PeerTransport;
5 |
6 | pub async fn introduce(id: u64, session_id: u128, peers_tx: Sender) {
7 | introduction::initiate(id, session_id, peers_tx)
8 | .await
9 | .unwrap();
10 | }
11 |
12 | pub mod error {
13 | use wasm_bindgen::JsValue;
14 |
15 | #[derive(Debug)]
16 | pub enum Error {
17 | Js(JsValue),
18 | Rust(Box),
19 | AlreadyInitialized(u64),
20 | }
21 |
22 | impl From for Error {
23 | fn from(js_val: JsValue) -> Self {
24 | Error::Js(js_val)
25 | }
26 | }
27 |
28 | impl From> for Error {
29 | fn from(err: Box) -> Self {
30 | Error::Rust(err)
31 | }
32 | }
33 |
34 | impl From for Error {
35 | fn from(err: futures::channel::mpsc::SendError) -> Self {
36 | Error::Rust(Box::new(err))
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/wraft/src/webrtc_rpc/transport.rs:
--------------------------------------------------------------------------------
1 | use crate::console_log;
2 | use crate::ringbuf::{self, RingBuf};
3 | use crate::util::sleep;
4 | use async_trait::async_trait;
5 | use futures::channel::mpsc::{channel, Receiver, Sender};
6 | use futures::channel::oneshot;
7 | use futures::{select, SinkExt, StreamExt};
8 | use serde::de::DeserializeOwned;
9 | use serde::{Deserialize, Serialize};
10 | use std::fmt::Debug;
11 | use std::marker::PhantomData;
12 | use std::sync::atomic::{AtomicBool, Ordering};
13 | use std::sync::Arc;
14 | use std::time::Duration;
15 | use wasm_bindgen::prelude::*;
16 | use wasm_bindgen::JsCast;
17 | use wasm_bindgen_futures::spawn_local;
18 | use web_sys::{MessageEvent, RtcDataChannel, RtcPeerConnection};
19 |
20 | const CHANNEL_SIZE: usize = 1024;
21 | const MAX_IN_FLIGHT_REQUESTS: usize = 1024;
22 | const REQUEST_TIMEOUT_MILLIS: u64 = 500;
23 |
24 | type RequestMessage = (Req, oneshot::Sender>);
25 |
26 | #[derive(Serialize, Deserialize)]
27 | enum Message {
28 | Request {
29 | idx: usize,
30 | req: Req,
31 | },
32 | Response {
33 | idx: usize,
34 | resp: Result,
35 | },
36 | }
37 |
38 | pub struct PeerTransport {
39 | node_id: u64,
40 | connection: RtcPeerConnection,
41 | data_channel: RtcDataChannel,
42 | done: oneshot::Sender<()>,
43 | }
44 |
45 | #[derive(Debug)]
46 | pub struct Server {
47 | node_id: u64,
48 | connection: RtcPeerConnection,
49 | data_channel: RtcDataChannel,
50 | server_req_rx: Receiver>,
51 | done: Option>,
52 |
53 | // Keep references to JS closures for memory-management purposes
54 | _onmessage_cb: Option>,
55 | _onclose_cb: Option>,
56 | }
57 |
58 | #[derive(Debug)]
59 | pub struct Client {
60 | node_id: u64,
61 | connected: Arc,
62 | req_tx: Sender>,
63 | }
64 |
65 | impl Clone for Client {
66 | fn clone(&self) -> Self {
67 | Self {
68 | node_id: self.node_id,
69 | connected: self.connected.clone(),
70 | req_tx: self.req_tx.clone(),
71 | }
72 | }
73 | }
74 |
75 | struct RequestManager {
76 | dc: RtcDataChannel,
77 | in_flight: RingBuf>>,
78 | timeout_tx: Sender,
79 | _request: PhantomData,
80 | }
81 |
82 | #[async_trait]
83 | pub trait RequestHandler {
84 | async fn handle(&self, req: Req) -> Result;
85 | }
86 |
87 | impl PeerTransport {
88 | pub fn new(
89 | node_id: u64,
90 | connection: RtcPeerConnection,
91 | data_channel: RtcDataChannel,
92 | done: oneshot::Sender<()>,
93 | ) -> Self {
94 | Self {
95 | node_id,
96 | done,
97 | connection,
98 | data_channel,
99 | }
100 | }
101 |
102 | pub fn start(self) -> (Client, Server)
103 | where
104 | Req: Serialize + DeserializeOwned + Debug + 'static,
105 | Resp: Serialize + DeserializeOwned + Debug + 'static,
106 | {
107 | self.data_channel
108 | .set_binary_type(web_sys::RtcDataChannelType::Arraybuffer);
109 |
110 | let (client_req_tx, client_req_rx) = channel(CHANNEL_SIZE);
111 | let (client_resp_tx, client_resp_rx) = channel(CHANNEL_SIZE);
112 | let (server_req_tx, server_req_rx) = channel(CHANNEL_SIZE);
113 |
114 | spawn_local(handle_client_requests(
115 | client_req_rx,
116 | client_resp_rx,
117 | self.data_channel.clone(),
118 | ));
119 |
120 | let mut server = Server {
121 | server_req_rx,
122 | node_id: self.node_id,
123 | data_channel: self.data_channel,
124 | connection: self.connection,
125 | done: Some(self.done),
126 | _onmessage_cb: None,
127 | _onclose_cb: None,
128 | };
129 |
130 | server.set_onclose_callback(
131 | client_req_tx.clone(),
132 | client_resp_tx.clone(),
133 | server_req_tx.clone(),
134 | );
135 | server.set_onmessage_callback(client_resp_tx, server_req_tx);
136 |
137 | let client = Client {
138 | connected: Arc::new(true.into()),
139 | node_id: self.node_id,
140 | req_tx: client_req_tx,
141 | };
142 |
143 | (client, server)
144 | }
145 |
146 | pub fn node_id(&self) -> u64 {
147 | self.node_id
148 | }
149 | }
150 |
151 | impl Server
152 | where
153 | Req: Serialize + DeserializeOwned + Debug + 'static,
154 | Resp: Serialize + DeserializeOwned + Debug + 'static,
155 | {
156 | pub async fn serve(&mut self, handler: impl RequestHandler + 'static) {
157 | while let Some((req, tx)) = self.server_req_rx.next().await {
158 | let resp = handler.handle(req).await;
159 | if tx.send(resp).is_err() {
160 | // Server is down, so we're done serving requests
161 | break;
162 | }
163 | }
164 | }
165 |
166 | fn set_onmessage_callback(
167 | &mut self,
168 | client_resp_tx: Sender>,
169 | server_req_tx: Sender>,
170 | ) {
171 | let data_channel = self.data_channel.clone();
172 |
173 | let cb = Closure::wrap(Box::new(move |ev: MessageEvent| {
174 | let mut client_tx = client_resp_tx.clone();
175 | let mut server_tx = server_req_tx.clone();
176 | let dc = data_channel.clone();
177 | spawn_local(async move {
178 | let abuf = ev
179 | .data()
180 | .dyn_into::()
181 | .expect("Expected message in binary format");
182 | let data = js_sys::Uint8Array::new(&abuf).to_vec();
183 |
184 | match bincode::deserialize::>(&data).unwrap() {
185 | Message::Request { idx, req } => {
186 | // Got a request from the other side's client
187 | let (tx, rx) = oneshot::channel();
188 | if server_tx.send((req, tx)).await.is_err() {
189 | console_log!("server request channel closed");
190 | return;
191 | }
192 | if let Ok(resp) = rx.await {
193 | let msg: Message = Message::Response { idx, resp };
194 | let data = bincode::serialize(&msg).unwrap();
195 | if let Err(err) = dc.send_with_u8_array(&data) {
196 | console_log!("error sending data: {:?}", err);
197 | }
198 | }
199 | }
200 | msg @ Message::Response { idx: _, resp: _ } => {
201 | // Got a response to one of our requests, try to process
202 | // it on our end
203 | let _res = client_tx.send(msg).await;
204 | }
205 | }
206 | });
207 | }) as Box);
208 |
209 | self.data_channel
210 | .set_onmessage(Some(cb.as_ref().unchecked_ref()));
211 |
212 | self._onmessage_cb = Some(cb);
213 | }
214 |
215 | fn set_onclose_callback(
216 | &mut self,
217 | mut client_req_tx: Sender>,
218 | mut client_resp_tx: Sender>,
219 | mut server_req_tx: Sender>,
220 | ) {
221 | let node_id = self.node_id;
222 |
223 | let cb = Closure::once(move || {
224 | console_log!("lost data channel for {}", node_id);
225 |
226 | // Close channels and hope all the listeners clean up nicely after
227 | // themselves :)
228 | client_req_tx.close_channel();
229 | client_resp_tx.close_channel();
230 | server_req_tx.close_channel();
231 | });
232 |
233 | self.data_channel
234 | .set_onclose(Some(cb.as_ref().unchecked_ref()));
235 |
236 | self._onclose_cb = Some(cb);
237 | }
238 | }
239 |
240 | impl Drop for Server {
241 | fn drop(&mut self) {
242 | if let Some(done) = self.done.take() {
243 | let _res = done.send(());
244 | }
245 | self.connection.close();
246 | }
247 | }
248 |
249 | async fn handle_client_requests(
250 | req_rx: Receiver<(Req, oneshot::Sender>)>,
251 | resp_rx: Receiver>,
252 | dc: RtcDataChannel,
253 | ) where
254 | Req: Serialize + 'static,
255 | Resp: Serialize + 'static,
256 | {
257 | let (timeout_tx, timeout_rx) = channel::(MAX_IN_FLIGHT_REQUESTS);
258 | let m = RequestManager {
259 | in_flight: RingBuf::with_capacity(MAX_IN_FLIGHT_REQUESTS),
260 | dc,
261 | timeout_tx,
262 | _request: PhantomData,
263 | };
264 |
265 | m.run(req_rx, resp_rx, timeout_rx).await;
266 | }
267 |
268 | impl RequestManager
269 | where
270 | Req: Serialize + 'static,
271 | Resp: Serialize + 'static,
272 | {
273 | pub async fn run(
274 | mut self,
275 | mut req_rx: Receiver>,
276 | mut resp_rx: Receiver>,
277 | mut timeout_rx: Receiver,
278 | ) where
279 | Req: Serialize + 'static,
280 | Resp: Serialize + 'static,
281 | {
282 | loop {
283 | select! {
284 | res = req_rx.next() => {
285 | match res {
286 | Some((req, tx)) => self.handle_request(req, tx),
287 | None => {
288 | console_log!("request channel closed, stopping request manager");
289 | return;
290 | }
291 | }
292 | }
293 | res = resp_rx.next() => {
294 | match res {
295 | Some(msg) => self.handle_response(msg),
296 | None => {
297 | console_log!("response channel closed, stopping request manager");
298 | return;
299 | }
300 | }
301 | }
302 | res = timeout_rx.next() => {
303 | let idx = res.unwrap();
304 | self.handle_timeout(idx);
305 | }
306 | }
307 | }
308 | }
309 |
310 | fn handle_request(&mut self, req: Req, tx: oneshot::Sender>) {
311 | match self.in_flight.add(tx) {
312 | Ok(idx) => {
313 | let msg: Message = Message::Request { idx, req };
314 | let data = bincode::serialize(&msg).unwrap();
315 | if let Err(err) = self.dc.send_with_u8_array(&data) {
316 | let tx = self.in_flight.remove(idx).unwrap();
317 | let _res = tx.send(Err(Error::from(err)));
318 | return;
319 | }
320 | let mut timeout_tx = self.timeout_tx.clone();
321 | spawn_local(async move {
322 | sleep(Duration::from_millis(REQUEST_TIMEOUT_MILLIS)).await;
323 | let _res = timeout_tx.send(idx).await;
324 | });
325 | }
326 | Err(ringbuf::Error::Overflow(tx)) => {
327 | let _res = tx.send(Err(Error::TooManyInFlightRequests));
328 | }
329 | }
330 | }
331 |
332 | fn handle_response(&mut self, msg: Message) {
333 | if let Message::Response { idx, resp } = msg {
334 | match self.in_flight.remove(idx) {
335 | Some(tx) => {
336 | // Best-effort reply (if the caller is gone then one
337 | // can assume that the request has been canceled or
338 | // something).
339 | let _res = tx.send(resp);
340 | }
341 | None => {
342 | console_log!("request {} came in after request canceled", idx);
343 | }
344 | }
345 | }
346 | }
347 |
348 | fn handle_timeout(&mut self, idx: usize) {
349 | if let Some(tx) = self.in_flight.remove(idx) {
350 | console_log!("request {} timed out", idx);
351 | let _res = tx.send(Err(Error::RequestTimeout));
352 | }
353 | }
354 | }
355 |
356 | impl Client {
357 | pub fn node_id(&self) -> u64 {
358 | self.node_id
359 | }
360 |
361 | pub fn is_connected(&self) -> bool {
362 | self.connected.load(Ordering::SeqCst)
363 | }
364 |
365 | pub async fn call(&mut self, req: Req) -> Result {
366 | if !self.is_connected() {
367 | return Err(Error::Disconnected);
368 | }
369 |
370 | let (resp_tx, resp_rx) = oneshot::channel();
371 | match self.req_tx.send((req, resp_tx)).await {
372 | Ok(_) => resp_rx.await.map_err(|_| Error::Disconnected)?,
373 | Err(_) => {
374 | self.connected.store(false, Ordering::SeqCst);
375 | Err(Error::Disconnected)
376 | }
377 | }
378 | }
379 | }
380 |
381 | impl Debug for PeerTransport {
382 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
383 | write!(
384 | f,
385 | "PeerTransport for {} ({:?})",
386 | self.node_id,
387 | self.data_channel.ready_state()
388 | )
389 | }
390 | }
391 |
392 | #[derive(Debug, Serialize, Deserialize)]
393 | pub enum Error {
394 | Js(String),
395 | RequestTimeout,
396 | TooManyInFlightRequests,
397 | Disconnected,
398 | Unavailable,
399 | }
400 |
401 | impl From for Error {
402 | fn from(err: JsValue) -> Self {
403 | match err.as_string() {
404 | Some(err) => Error::Js(format!("JavaScript error: {}", err)),
405 | None => Error::Js(format!("JavaScript error: {:?}", err)),
406 | }
407 | }
408 | }
409 |
410 | impl std::fmt::Display for Error {
411 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
412 | match self {
413 | Error::Js(err) => write!(f, "{}", err),
414 | Error::RequestTimeout => write!(f, "request timeout"),
415 | Error::TooManyInFlightRequests => write!(f, "too many in-flight requests"),
416 | Error::Disconnected => write!(f, "data channel has been disconnected"),
417 | Error::Unavailable => write!(f, "rpc server is unavailable"),
418 | }
419 | }
420 | }
421 |
--------------------------------------------------------------------------------
/wraft/tests/web.rs:
--------------------------------------------------------------------------------
1 | //! Test suite for the Web and headless browsers.
2 |
3 | #![cfg(target_arch = "wasm32")]
4 |
5 | extern crate wasm_bindgen_test;
6 | use wasm_bindgen_test::*;
7 |
8 | wasm_bindgen_test_configure!(run_in_browser);
9 |
10 | #[wasm_bindgen_test]
11 | fn pass() {
12 | assert_eq!(1 + 1, 2);
13 | }
14 |
--------------------------------------------------------------------------------
/wraft/www/.bin/create-wasm-app.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | const { spawn } = require("child_process");
4 | const fs = require("fs");
5 |
6 | let folderName = '.';
7 |
8 | if (process.argv.length >= 3) {
9 | folderName = process.argv[2];
10 | if (!fs.existsSync(folderName)) {
11 | fs.mkdirSync(folderName);
12 | }
13 | }
14 |
15 | const clone = spawn("git", ["clone", "https://github.com/rustwasm/create-wasm-app.git", folderName]);
16 |
17 | clone.on("close", code => {
18 | if (code !== 0) {
19 | console.error("cloning the template failed!")
20 | process.exit(code);
21 | } else {
22 | console.log("🦀 Rust + 🕸 Wasm = ❤");
23 | }
24 | });
25 |
--------------------------------------------------------------------------------
/wraft/www/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 |
--------------------------------------------------------------------------------
/wraft/www/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js: "10"
3 |
4 | script:
5 | - ./node_modules/.bin/webpack
6 |
--------------------------------------------------------------------------------
/wraft/www/LICENSE-APACHE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/wraft/www/LICENSE-MIT:
--------------------------------------------------------------------------------
1 | Copyright (c) [year] [name]
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 |
--------------------------------------------------------------------------------
/wraft/www/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
create-wasm-app
4 |
5 |
An npm init
template for kick starting a project that uses NPM packages containing Rust-generated WebAssembly and bundles them with Webpack.
6 |
7 |
8 |
9 |
10 |
11 |
12 | Usage
13 | |
14 | Chat
15 |
16 |
17 |
Built with 🦀🕸 by The Rust and WebAssembly Working Group
18 |
19 |
20 | ## About
21 |
22 | This template is designed for depending on NPM packages that contain
23 | Rust-generated WebAssembly and using them to create a Website.
24 |
25 | * Want to create an NPM package with Rust and WebAssembly? [Check out
26 | `wasm-pack-template`.](https://github.com/rustwasm/wasm-pack-template)
27 | * Want to make a monorepo-style Website without publishing to NPM? Check out
28 | [`rust-webpack-template`](https://github.com/rustwasm/rust-webpack-template)
29 | and/or
30 | [`rust-parcel-template`](https://github.com/rustwasm/rust-parcel-template).
31 |
32 | ## 🚴 Usage
33 |
34 | ```
35 | npm init wasm-app
36 | ```
37 |
38 | ## 🔋 Batteries Included
39 |
40 | - `.gitignore`: ignores `node_modules`
41 | - `LICENSE-APACHE` and `LICENSE-MIT`: most Rust projects are licensed this way, so these are included for you
42 | - `README.md`: the file you are reading now!
43 | - `index.html`: a bare bones html document that includes the webpack bundle
44 | - `index.js`: example js file with a comment showing how to import and use a wasm pkg
45 | - `package.json` and `package-lock.json`:
46 | - pulls in devDependencies for using webpack:
47 | - [`webpack`](https://www.npmjs.com/package/webpack)
48 | - [`webpack-cli`](https://www.npmjs.com/package/webpack-cli)
49 | - [`webpack-dev-server`](https://www.npmjs.com/package/webpack-dev-server)
50 | - defines a `start` script to run `webpack-dev-server`
51 | - `webpack.config.js`: configuration file for bundling your js with webpack
52 |
53 | ## License
54 |
55 | Licensed under either of
56 |
57 | * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
58 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
59 |
60 | at your option.
61 |
62 | ### Contribution
63 |
64 | Unless you explicitly state otherwise, any contribution intentionally
65 | submitted for inclusion in the work by you, as defined in the Apache-2.0
66 | license, shall be dual licensed as above, without any additional terms or
67 | conditions.
68 |
--------------------------------------------------------------------------------
/wraft/www/bootstrap.js:
--------------------------------------------------------------------------------
1 | // A dependency graph that contains any wasm must all be imported
2 | // asynchronously. This `bootstrap.js` file does the single async import, so
3 | // that no one else needs to worry about it again.
4 | import("./index.js")
5 | .catch(e => console.error("Error importing `index.js`:", e));
6 |
--------------------------------------------------------------------------------
/wraft/www/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | WRaft
6 |
7 |
8 | This page contains webassembly and javascript content, please enable javascript in your browser.
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/wraft/www/index.js:
--------------------------------------------------------------------------------
1 | import * as wasm from "wraft";
2 |
--------------------------------------------------------------------------------
/wraft/www/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "create-wasm-app",
3 | "version": "0.1.0",
4 | "description": "create an app to consume rust-generated wasm packages",
5 | "main": "index.js",
6 | "bin": {
7 | "create-wasm-app": ".bin/create-wasm-app.js"
8 | },
9 | "scripts": {
10 | "build": "webpack --config webpack.config.js",
11 | "start": "webpack-dev-server"
12 | },
13 | "repository": {
14 | "type": "git",
15 | "url": "git+https://github.com/rustwasm/create-wasm-app.git"
16 | },
17 | "keywords": [
18 | "webassembly",
19 | "wasm",
20 | "rust",
21 | "webpack"
22 | ],
23 | "author": "Ashley Williams ",
24 | "license": "(MIT OR Apache-2.0)",
25 | "bugs": {
26 | "url": "https://github.com/rustwasm/create-wasm-app/issues"
27 | },
28 | "homepage": "https://github.com/rustwasm/create-wasm-app#readme",
29 | "dependencies": {
30 | "wraft": "file:../pkg"
31 | },
32 | "devDependencies": {
33 | "hello-wasm-pack": "^0.1.0",
34 | "webpack": "^4.29.3",
35 | "webpack-cli": "^4.9.1",
36 | "webpack-dev-server": "^4.4.0",
37 | "copy-webpack-plugin": "^5.0.0"
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/wraft/www/webpack.config.js:
--------------------------------------------------------------------------------
1 | const CopyWebpackPlugin = require("copy-webpack-plugin");
2 | const path = require('path');
3 |
4 | module.exports = {
5 | entry: "./bootstrap.js",
6 | output: {
7 | path: path.resolve(__dirname, "dist"),
8 | filename: "bootstrap.js",
9 | },
10 | mode: "development",
11 | plugins: [
12 | new CopyWebpackPlugin(['index.html'])
13 | ],
14 | devServer: {
15 | allowedHosts: ['wraft0', 'wraft1', 'wraft2'],
16 | },
17 | };
18 |
--------------------------------------------------------------------------------