├── .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 | 130 | 131 | { self.render_state_dump() } 132 | 133 | }, 134 | BenchState::Stopped => html! { 135 | <> 136 |

{ "Benchmark WRaft" }

137 | { self.render_bench_result() } 138 | 139 | 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 | 168 |

169 |

170 | 171 | 181 |

182 |

183 |

184 | 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 |
247 | { url }
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 |
101 |
102 |
103 |

{ "todos" }

104 | { self.view_input() } 105 |
106 |
107 | 114 |
119 |
120 | 121 | { self.state.total() } 122 | { " item(s) left" } 123 | 124 |
    125 | { for Filter::iter().map(|flt| self.view_filter(flt)) } 126 |
127 | 130 |
131 |
132 | 137 |
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 |
      232 |
    • 233 |
    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 | 256 |
    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 | Build Status 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 | 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 | --------------------------------------------------------------------------------