├── .github ├── PULL_REQUEST_TEMPLATE.md ├── funding.yml └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Cargo.toml ├── Cargo.yml ├── LICENSE ├── README.md ├── TODO.md ├── build.rs ├── ci ├── clippy.bash ├── doc.bash ├── server.bash └── test.bash ├── deny.toml ├── examples ├── README.md ├── chat_client │ ├── Cargo.toml │ ├── Cargo.yml │ ├── README.md │ ├── TODO.md │ ├── index.html │ ├── src │ │ ├── color.rs │ │ ├── e_handler.rs │ │ ├── entrypoint.rs │ │ └── user_list.rs │ └── style.css ├── chat_format │ ├── Cargo.toml │ ├── Cargo.yml │ └── src │ │ └── lib.rs └── chat_server │ ├── Cargo.toml │ ├── Cargo.yml │ └── src │ ├── error.rs │ └── main.rs ├── src ├── error.rs ├── lib.rs ├── ws_event.rs ├── ws_message.rs ├── ws_meta.rs ├── ws_state.rs ├── ws_stream.rs └── ws_stream_io.rs └── tests ├── events.rs ├── futures_codec.rs ├── tokio_codec.rs ├── ws_meta.rs └── ws_stream.rs /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.github/funding.yml: -------------------------------------------------------------------------------- 1 | liberapay: najamelan 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on : [push, pull_request] 3 | 4 | jobs: 5 | 6 | linux-stable: 7 | 8 | name: Linux Rust Stable 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | 13 | - name: Install latest stable Rust 14 | uses: actions-rs/toolchain@v1 15 | with: 16 | toolchain : stable 17 | override : true 18 | components : clippy 19 | target : wasm32-unknown-unknown 20 | 21 | 22 | - name: install wasm-pack 23 | uses: jetli/wasm-pack-action@v0.3.0 24 | with: 25 | # Optional version of wasm-pack to install(eg. 'v0.9.1', 'latest') 26 | version: 'latest' 27 | 28 | 29 | - name: Checkout crate 30 | uses: actions/checkout@v3 31 | 32 | 33 | - name: Checkout server 34 | run: bash ci/server.bash 35 | 36 | 37 | - name: Run tests 38 | run: bash ci/test.bash 39 | 40 | 41 | linux-nightly: 42 | 43 | name: Linux Rust Nightly 44 | runs-on: ubuntu-latest 45 | 46 | steps: 47 | 48 | - name: Install latest nightly Rust 49 | uses: actions-rs/toolchain@v1 50 | with: 51 | toolchain : nightly 52 | override : true 53 | components : clippy 54 | target : wasm32-unknown-unknown 55 | 56 | 57 | - name: install wasm-pack 58 | uses: jetli/wasm-pack-action@v0.3.0 59 | with: 60 | # Optional version of wasm-pack to install(eg. 'v0.9.1', 'latest') 61 | version: 'latest' 62 | 63 | 64 | - name: Checkout crate 65 | uses: actions/checkout@v3 66 | 67 | 68 | - name: Run clippy 69 | run : bash ci/clippy.bash 70 | 71 | 72 | - name: Build documentation 73 | run : bash ci/doc.bash 74 | 75 | 76 | - name: Checkout server 77 | run: bash ci/server.bash 78 | 79 | 80 | - name: Run tests 81 | run : bash ci/test.bash 82 | 83 | - name: Run cargo-deny 84 | uses: EmbarkStudios/cargo-deny-action@v2 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target 4 | examples/**/target 5 | 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 8 | Cargo.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | **/*.rs.bak 13 | 14 | # Ignore flamegraph output 15 | flamegraph.svg 16 | perf.data 17 | perf.data.old 18 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # ws_stream_wasm Changelog 2 | 3 | ## [Unreleased] 4 | 5 | [Unreleased]: https://github.com/najamelan/ws_stream_wasm/compare/release...dev 6 | 7 | 8 | ## [0.7.4] - 2023-01-29 9 | 10 | [0.7.4]: https://github.com/najamelan/ws_stream_wasm/compare/0.7.3...0.7.4 11 | 12 | ### Fixed 13 | 14 | - When the `WsMeta::connect` future is dropped, close websocket and unregister callbacks. 15 | This avoids some ugly error messages in the console. Thanks to @hamchapman for discovering 16 | and solving the issue and @danielhenrymantilla for reviewing the solution. 17 | 18 | ### Updated 19 | - tokio-util to 0.7 (dev-dependency) 20 | - send_wrapper to 0.6 21 | 22 | 23 | ## [0.7.3] - 2021-06-11 24 | 25 | [0.7.3]: https://github.com/najamelan/ws_stream_wasm/compare/0.7.2...0.7.3 26 | 27 | ### Added 28 | 29 | - on user demand also make `WsStream` `Sync` again. 30 | 31 | 32 | ## [0.7.2] - 2021-06-26 33 | 34 | [0.7.2]: https://github.com/najamelan/ws_stream_wasm/compare/0.7.1...0.7.2 35 | 36 | ### Added 37 | 38 | - Make `WsStream` `Send` again. 39 | 40 | 41 | ## [0.7.1] - 2021-06-11 42 | 43 | [0.7.1]: https://github.com/najamelan/ws_stream_wasm/compare/0.7.0...0.7.1 44 | 45 | ### Updated 46 | - Switch to asynchronous-codec. 47 | - Remove external_doc feature for nightly 1.54. 48 | 49 | ## 0.7.0 - 2021-02-17 50 | 51 | - **BREAKING CHANGE**: Update to tokio v1, pharos to 0.5 and async_io_stream to 0.3. 52 | - **BREAKING CHANGE**: Browsers stopped raising SECURITY_ERR when trying to connect to a [forbidden port](https://stackoverflow.com/questions/4313403/why-do-browsers-block-some-ports/4314070). It now just returns a connection failed, which is indistinguishable from any other network problems, or simply the server not listening on this port. This is an [intended change](https://bugzilla.mozilla.org/show_bug.cgi?id=1684620). 53 | - 54 | 55 | 56 | ## 0.7.0-beta.1 - 2020-11-03 57 | 58 | - Update to tokio 0.3, will be out of beta when tokio hits 1.0. 59 | 60 | 61 | ## 0.6.1 - 2020-10-02 62 | 63 | - Remove unnecessary `mut` in recent compiler versions. Travis stable on osx is still on 1.44 and will fail until they upgrade. 64 | - improve readme 65 | 66 | ## 0.6.0 - 2020-03-21 67 | 68 | - **BREAKING CHANGE**: rename the basic types. `WsStream` is now called `WsMeta` and `WsIo` is now called `WsStream`. 69 | - **BREAKING CHANGE**: `WsStream` no longer implements `AsyncRead`/`AsyncWrite` directly, you have to call `into_io()`. 70 | - **BREAKING CHANGE**: The error type is now renamed to `WsErr` and is an enum directly instead of having an error kind. 71 | - **BREAKING CHANGE**: Fix: `From for WsMessage` has become `TryFrom`. This is because things actually could 72 | go wrong here. 73 | 74 | - Implement tokio `AsyncRead`/`AsyncWrite` for WsStream (Behind a feature flag). 75 | - delegate implementation of `AsyncRead`/`AsyncWrite`/`AsyncBufRead` to _async_io_stream_. This allows 76 | sharing the functionality with _ws_stream_tungstenite_, fleshing it out to always fill and use entire buffers, 77 | polling the underlying stream several times if needed. 78 | - only build for default target on docs.rs. 79 | - exclude unneeded files from package build. 80 | - remove trace and debug statements. 81 | - `WsMessage` now implements `From>` and `From`. 82 | - `WsMeta` and `WsStream` are now `Send`. You should still only use them in a single thread though. This is fine because 83 | WASM has no threads, and is sometimes necessary because all the underlying types of _web-sys_ are `!Send`. 84 | - No longer set a close code if the user doesn't set one. 85 | - Fix: Make sure `WsStream` continues to function correctly if you drop `WsMeta`. 86 | 87 | 88 | ## 0.5.2 - 2020-01-06 89 | 90 | - fix version of futures-codec because they didn't bump their major version number after making a breaking change. 91 | 92 | 93 | ## 0.5.1 - 2019-11-14 94 | 95 | - update futures to 0.3.1. 96 | 97 | 98 | ## 0.5 - 2019-09-28 99 | 100 | - **BREAKING CHANGE**: update to pharos 0.4. Observable::observe is now fallible, so that is a breaking change for ws_stream_wasm 101 | - update to futures-codec 0.3 102 | 103 | 104 | ## 0.4.1 - 2019-09-23 105 | 106 | - fix some more errors in the readme 107 | 108 | ## 0.4.0 - 2019-09-23 109 | 110 | - **BREAKING CHANGE**: use the new filter feature from pharos, making `NextEvent` and `WsEventType` redundant. Those 111 | types have been removed from the library. The `observe` and method off `WsStream` now takes a `pharos::ObserveConfig` to filter event types. Please refer to the documentation of [pharos](https://docs.rs/pharos) for how to use them. 112 | - spell check all docs 113 | 114 | ## 0.3.0 - 2019-09-08 115 | 116 | - drop dependencies on async_runtime and failure and switch to std::error::Error for error handling 117 | - add a fullstack chat example (still needs documentation and cleanup) 118 | 119 | ## 0.2.1 - 2019-08-02 120 | 121 | - Fix incorrect link to changelog in readme 122 | 123 | 124 | ## 0.2.0 - 2019-08-02 125 | 126 | - **BREAKING CHANGE**: Fix: Correctly wake up tasks waiting for a next message if the connection gets closed externally. 127 | This prevents these tasks from hanging indefinitely. 128 | As a consequence, `WsStream::close` now returns a `Result`, taking into account that if the connection is already 129 | closed, we don't have the `CloseEvent`. Instead a `WsErr` of kind `WsErrKind::ConnectionNotOpen` is returned. 130 | - update to async_runtime 0.3 131 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | This repository accepts contributions. Ideas, questions, feature requests and bug reports can be filed through Github issues. 4 | 5 | Pull Requests are welcome on Github. By committing pull requests, you accept that your code might be modified and reformatted to fit the project coding style or to improve the implementation. Contributed code is considered licensed under the same license as the rest of the project unless explicitly agreed otherwise. See the `LICENCE` file. 6 | 7 | Please discuss what you want to see modified before filing a pull request if you don't want to be doing work that might be rejected. 8 | 9 | 10 | ## Code formatting 11 | 12 | I understand my code formatting style is quite uncommon, but it is deliberate and helps readability for me. Unfortunately, it cannot be achieved with automated tools like `rustfmt`. **Feel free to contribute code formatted however you are comfortable writing it**. I am happy to reformat during the review process. If you are uncomfortable reading my code, I suggest running `rustfmt` on the entire source tree or set your line-height to 1.2 instead of the common 1.5, which will make it look a lot less over the top. 13 | 14 | 15 | # git workflow 16 | 17 | Please file PR's against the `dev` branch, don't forget to update the documentation. 18 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | # Auto-generated from "Cargo.yml" 2 | [badges] 3 | [badges.maintenance] 4 | status = "actively-developed" 5 | 6 | [badges.travis-ci] 7 | repository = "najamelan/ws_stream_wasm" 8 | 9 | [build-dependencies] 10 | rustc_version = "^0.4" 11 | 12 | [dependencies] 13 | js-sys = "^0.3" 14 | log = "^0.4" 15 | send_wrapper = "^0.6" 16 | thiserror = "^2" 17 | wasm-bindgen = "^0.2" 18 | wasm-bindgen-futures = "^0.4" 19 | 20 | [dependencies.async_io_stream] 21 | features = ["map_pharos"] 22 | version = "^0.3" 23 | 24 | [dependencies.futures] 25 | default-features = false 26 | version = "^0.3" 27 | 28 | [dependencies.pharos] 29 | version = "^0.5" 30 | 31 | [dependencies.web-sys] 32 | features = ["BinaryType", "Blob", "console", "MessageEvent", "WebSocket", "CloseEvent", "DomException"] 33 | version = "^0.3" 34 | 35 | [dev-dependencies] 36 | bytes = "^1" 37 | console_error_panic_hook = "^0.1" 38 | console_log = "^1" 39 | rand = "^0.9" 40 | rand_xoshiro = "^0.7" 41 | serde_cbor = "^0.11" 42 | wasm-bindgen-test = "^0.3" 43 | 44 | [dev-dependencies.async_io_stream] 45 | features = ["tokio_io"] 46 | version = "^0.3" 47 | 48 | [dev-dependencies.asynchronous-codec] 49 | version = "^0.7" 50 | 51 | [dev-dependencies.getrandom] 52 | features = ["wasm_js"] 53 | version = "^0.3" 54 | 55 | [dev-dependencies.serde] 56 | features = ["derive"] 57 | version = "^1" 58 | 59 | [dev-dependencies.tokio] 60 | version = "^1" 61 | 62 | [dev-dependencies.tokio-serde-cbor] 63 | version = "^0.7" 64 | 65 | [dev-dependencies.tokio-util] 66 | default-features = false 67 | features = ["codec"] 68 | version = "^0.7" 69 | 70 | [features] 71 | tokio_io = ["async_io_stream/tokio_io"] 72 | 73 | [lib] 74 | crate-type = ["cdylib", "rlib"] 75 | 76 | [package] 77 | authors = ["Naja Melan "] 78 | categories = ["asynchronous", "network-programming", "api-bindings", "wasm", "web-programming::websocket"] 79 | description = "A convenience library for using websockets in WASM" 80 | documentation = "https://docs.rs/ws_stream_wasm" 81 | edition = "2018" 82 | exclude = ["tests", "examples"] 83 | homepage = "https://github.com/najamelan/ws_stream_wasm" 84 | keywords = ["wasm", "websocket", "tokio", "stream", "async"] 85 | license = "Unlicense" 86 | name = "ws_stream_wasm" 87 | readme = "README.md" 88 | repository = "https://github.com/najamelan/ws_stream_wasm" 89 | version = "0.7.4" 90 | 91 | [package.metadata] 92 | [package.metadata.docs] 93 | [package.metadata.docs.rs] 94 | all-features = true 95 | targets = [] 96 | -------------------------------------------------------------------------------- /Cargo.yml: -------------------------------------------------------------------------------- 1 | package: 2 | 3 | # When releasing to crates.io: 4 | # 5 | # - last check for all TODO, FIXME, expect, unwrap. 6 | # - recheck log statements (informative, none left that were just for development, ...) 7 | # - `cargo +nightly doc` and re-read and final polish of documentation. 8 | # 9 | # - Update CHANGELOG.md. 10 | # - Update version numbers in Cargo.yml, Cargo.toml, install section of readme. 11 | # 12 | # - `touch **/*.rs && cargo clippy --tests --examples --benches --all-features` 13 | # - `cargo update` 14 | # - `cargo udeps --all-targets --all-features` 15 | # - `cargo outdated --root-deps-only` 16 | # - `cargo audit` 17 | # - `cargo crev crate verify --show-all --recursive` and review. 18 | # - 'cargo test --all-targets --all-features' 19 | # 20 | # - push dev and verify CI result 21 | # - `cargo test` on dependent crates 22 | # 23 | # - cargo publish 24 | # - `git checkout release && git merge dev --no-ff` 25 | # - `git tag x.x.x` with version number. 26 | # - `git push && git push --tags` 27 | # 28 | version : 0.7.4 29 | name : ws_stream_wasm 30 | edition : '2018' 31 | authors : [ Naja Melan ] 32 | description : A convenience library for using websockets in WASM 33 | license : Unlicense 34 | documentation : https://docs.rs/ws_stream_wasm 35 | homepage : https://github.com/najamelan/ws_stream_wasm 36 | repository : https://github.com/najamelan/ws_stream_wasm 37 | readme : README.md 38 | keywords : [ wasm, websocket, tokio, stream, async ] 39 | categories : [ asynchronous, network-programming, api-bindings, wasm, "web-programming::websocket" ] 40 | exclude : [ tests, examples ] 41 | 42 | metadata: 43 | docs: 44 | rs: 45 | all-features: true 46 | targets : [] # should be wasm, but that's not available. 47 | 48 | badges: 49 | 50 | maintenance : { status : actively-developed } 51 | travis-ci : { repository : najamelan/ws_stream_wasm } 52 | 53 | 54 | features: 55 | 56 | tokio_io: [ async_io_stream/tokio_io ] 57 | 58 | 59 | lib: 60 | crate-type: [cdylib, rlib] 61 | 62 | 63 | dependencies: 64 | 65 | # public dependencies (bump major if changing version numbers here) 66 | # 67 | pharos : { version: ^0.5 } 68 | futures : { version: ^0.3, default-features: false } 69 | async_io_stream : { version: ^0.3, features: [ map_pharos ] } 70 | 71 | # We expose WebSocket and CloseEvent. 72 | # 73 | web-sys: 74 | 75 | version : ^0.3 76 | 77 | features: 78 | 79 | - BinaryType 80 | - Blob 81 | - console 82 | - MessageEvent 83 | - WebSocket 84 | - CloseEvent 85 | - DomException 86 | 87 | # private deps 88 | # 89 | js-sys : ^0.3 90 | thiserror : ^2 91 | send_wrapper : ^0.6 92 | wasm-bindgen : ^0.2 93 | wasm-bindgen-futures : ^0.4 94 | log : ^0.4 95 | 96 | 97 | dev-dependencies: 98 | 99 | # wasm-logger : ^0.1 100 | async_io_stream : { version: ^0.3, features: [ tokio_io ] } 101 | bytes : ^1 102 | console_error_panic_hook : ^0.1 103 | console_log : ^1 104 | # flexi_logger : ^0.16 105 | asynchronous-codec : { version: ^0.7 } 106 | # pretty_assertions : ^0.6 107 | getrandom : { version: ^0.3, features: [wasm_js] } 108 | rand : ^0.9 109 | rand_xoshiro : ^0.7 110 | serde : { version: ^1, features: [ derive ] } 111 | serde_cbor : ^0.11 112 | tokio : { version: ^1 } 113 | tokio-serde-cbor : { version: ^0.7 } 114 | tokio-util : { version: ^0.7, default-features: false, features: [codec] } 115 | wasm-bindgen-test : ^0.3 116 | 117 | 118 | build-dependencies: 119 | 120 | rustc_version: ^0.4 121 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ws_stream_wasm 2 | 3 | [![standard-readme compliant](https://img.shields.io/badge/readme%20style-standard-brightgreen.svg?style=flat-square)](https://github.com/RichardLitt/standard-readme) 4 | [![Build Status](https://github.com/najamelan/ws_stream_wasm/workflows/ci/badge.svg?branch=release)](https://github.com/najamelan/ws_stream_wasm/actions) 5 | [![Docs](https://docs.rs/ws_stream_wasm/badge.svg)](https://docs.rs/ws_stream_wasm) 6 | [![crates.io](https://img.shields.io/crates/v/ws_stream_wasm.svg)](https://crates.io/crates/ws_stream_wasm) 7 | 8 | 9 | > A convenience library for using web sockets in WASM 10 | 11 | The _web-sys_ bindings for websockets aren't very convenient to use directly. This crates hopes to alleviate that. Browsers can't create direct TCP connections, and by putting `AsyncRead`/`AsyncWrite` on top of websockets, we can use interfaces that work over any async byte streams from within the browser. The crate has 2 main types. The `WsMeta` type exists to allow access to the web API while you pass `WsStream` to combinators that take ownership of the stream. 12 | 13 | **features:** 14 | - [`WsMeta`]: A wrapper around [`web_sys::WebSocket`]. 15 | - [`WsMessage`]: A simple rusty representation of a WebSocket message. 16 | - [`WsStream`]: A _futures_ `Sink`/`Stream` of `WsMessage`. 17 | It also has a method `into_io()` which let's you get a wrapper that implements `AsyncRead`/`AsyncWrite`/`AsyncBufRead` (_tokio_ version behind the feature `tokio_io`). 18 | - [`WsEvent`]: [`WsMeta`] is observable with [pharos](https://crates.io/crates/pharos) for events (mainly useful for connection close). 19 | 20 | **NOTE:** this crate only works on WASM. If you want a server side equivalent that implements `AsyncRead`/`AsyncWrite` over 21 | WebSockets, check out [ws_stream_tungstenite](https://crates.io/crates/ws_stream_tungstenite). 22 | 23 | **missing features:** 24 | - no automatic reconnect 25 | - not all features are thoroughly tested. Notably, I have little use for extensions and sub-protocols. Tungstenite, 26 | which I use for the server end (and for automated testing) doesn't support these, making it hard to write unit tests. 27 | 28 | ## Table of Contents 29 | 30 | - [Install](#install) 31 | - [Upgrade](#upgrade) 32 | - [Dependencies](#dependencies) 33 | - [Usage](#usage) 34 | - [API](#api) 35 | - [References](#references) 36 | - [Contributing](#contributing) 37 | - [Code of Conduct](#code-of-conduct) 38 | - [License](#license) 39 | 40 | 41 | ## Install 42 | With [cargo add](https://github.com/killercup/cargo-edit): 43 | `cargo add ws_stream_wasm` 44 | 45 | With [cargo yaml](https://gitlab.com/storedbox/cargo-yaml): 46 | ```yaml 47 | dependencies: 48 | 49 | ws_stream_wasm: ^0.7 50 | ``` 51 | 52 | In Cargo.toml: 53 | ```toml 54 | [dependencies] 55 | 56 | ws_stream_wasm = "0.7" 57 | ``` 58 | 59 | ### Upgrade 60 | 61 | Please check out the [changelog](https://github.com/najamelan/ws_stream_wasm/blob/release/CHANGELOG.md) when upgrading. 62 | 63 | ### Dependencies 64 | 65 | This crate has few dependencies. Cargo will automatically handle it's dependencies for you. 66 | 67 | There is one optional features. The `tokio_io` features causes the `WsIo` returned from [`WsStream::into_io`] to implement the 68 | tokio version of AsyncRead/AsyncWrite. 69 | 70 | 71 | ## Usage 72 | 73 | The [integration tests](https://github.com/najamelan/ws_stream_wasm/tree/release/tests) show most features in action. The 74 | example directory doesn't currently hold any interesting examples. 75 | 76 | The types in this library are `Send` + `Sync` as far as the compiler is concerned. This is so that you can use them with general purpose 77 | libraries that also work on WASM but that require a connection to be `Send`/`Sync`. Currently WASM has no threads though and most 78 | underlying types we use aren't `Send`. The solution for the moment is to use [`send_wrapper::SendWrapper`]. This will panic 79 | if it's ever dereferenced on a different thread than where it's created. You have to consider that the types aren't `Send`, but 80 | on WASM it's safe to pass them to an API that requires `Send`, because there is not much multi-threading support. Thus passing it to 81 | the bindgen executor will be fine. However with webworkers you can make extra threads nevertheless. The responsibility is on you 82 | to assure you don't try to use the Web Api's on different threads. 83 | 84 | The main entrypoint you'll want to use, eg to connect, is [`WsMeta::connect`]. 85 | 86 | ### Basic events example 87 | ```rust 88 | use 89 | { 90 | ws_stream_wasm :: * , 91 | pharos :: * , 92 | wasm_bindgen :: UnwrapThrowExt , 93 | wasm_bindgen_futures :: futures_0_3::spawn_local , 94 | futures :: stream::StreamExt , 95 | }; 96 | 97 | let program = async 98 | { 99 | let (mut ws, _wsio) = WsMeta::connect( "ws://127.0.0.1:3012", None ).await 100 | 101 | .expect_throw( "assume the connection succeeds" ); 102 | 103 | let mut evts = ws.observe( ObserveConfig::default() ).expect_throw( "observe" ); 104 | 105 | ws.close().await; 106 | 107 | // Note that since WsMeta::connect resolves to an opened connection, we don't see 108 | // any Open events here. 109 | // 110 | assert!( evts.next().await.unwrap_throw().is_closing() ); 111 | assert!( evts.next().await.unwrap_throw().is_closed () ); 112 | }; 113 | 114 | spawn_local( program ); 115 | ``` 116 | 117 | ### Filter events example 118 | 119 | This shows how to filter events. The functionality comes from _pharos_ which we use to make 120 | [`WsMeta`] observable. 121 | 122 | ```rust 123 | use 124 | { 125 | ws_stream_wasm :: * , 126 | pharos :: * , 127 | wasm_bindgen :: UnwrapThrowExt , 128 | wasm_bindgen_futures :: futures_0_3::spawn_local , 129 | futures :: stream::StreamExt , 130 | }; 131 | 132 | let program = async 133 | { 134 | let (mut ws, _wsio) = WsMeta::connect( "ws://127.0.0.1:3012", None ).await 135 | 136 | .expect_throw( "assume the connection succeeds" ); 137 | 138 | // The Filter type comes from the pharos crate. 139 | // 140 | let mut evts = ws.observe( Filter::Pointer( WsEvent::is_closed ).into() ).expect_throw( "observe" ); 141 | 142 | ws.close().await; 143 | 144 | // Note we will only get the closed event here, the WsEvent::Closing has been filtered out. 145 | // 146 | assert!( evts.next().await.unwrap_throw().is_closed () ); 147 | }; 148 | 149 | spawn_local( program ); 150 | ``` 151 | 152 | ## API 153 | 154 | Api documentation can be found on [docs.rs](https://docs.rs/ws_stream_wasm). 155 | 156 | 157 | ## References 158 | The reference documents for understanding web sockets and how the browser handles them are: 159 | - [HTML Living Standard](https://html.spec.whatwg.org/multipage/web-sockets.html) 160 | - [RFC 6455 - The WebSocket Protocol](https://tools.ietf.org/html/rfc6455) 161 | 162 | 163 | ## Contributing 164 | 165 | Please check out the [contribution guidelines](https://github.com/najamelan/ws_stream_wasm/blob/release/CONTRIBUTING.md). 166 | 167 | 168 | ### Testing 169 | 170 | For testing we need back-end servers to echo data back to the tests. These are in the `ws_stream_tungstenite` crate. 171 | ```bash 172 | git clone https://github.com/najamelan/ws_stream_tungstenite 173 | cd ws_stream_tungstenite 174 | cargo run --example echo --release 175 | 176 | # in a different terminal: 177 | cargo run --example echo_tt --release -- "127.0.0.1:3312" 178 | 179 | # in a different terminal: 180 | cd examples/ssl 181 | cargo run --release -- "127.0.0.1:8443" 182 | 183 | # the second server is pure async-tungstenite without ws_stream_tungstenite wrapping it in AsyncRead/Write. This 184 | # is needed for testing a WsMessage::Text because ws_stream_tungstenite only does binary. 185 | 186 | # in a third terminal, in ws_stream_wasm you have different options: 187 | wasm-pack test --firefox [--headless] [--release] 188 | wasm-pack test --chrome [--headless] [--release] 189 | ``` 190 | 191 | In general chrome is well faster. When running it in the browser (without `--headless`) you get trace logging 192 | in the console, which helps debugging. In chrome you need to enable verbose output in the console, 193 | otherwise only info and up level are reported. 194 | 195 | ### Code of conduct 196 | 197 | Any of the behaviors described in [point 4 "Unacceptable Behavior" of the Citizens Code of Conduct](https://github.com/stumpsyn/policies/blob/master/citizen_code_of_conduct.md#4-unacceptable-behavior) are not welcome here and might get you banned. If anyone, including maintainers and moderators of the project, fail to respect these/your limits, you are entitled to call them out. 198 | 199 | ## License 200 | 201 | [Unlicence](https://unlicense.org/) 202 | 203 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | - remove this_error... 4 | - tests for new behavior of WsMessage tryfrom and error handling. 5 | - doc tests 6 | - look into proper changelogs, like the futures crate. 7 | - update tokio-util, breaking change, probably needs new version of tokio-serde-cbor 8 | - ci: https://rustwasm.github.io/docs/wasm-bindgen/wasm-bindgen-test/continuous-integration.html has some windows instructions. 9 | 10 | ## Features 11 | - when the connection is lost, can we know if it's the server that disconnected (correct shutdown exchange) 12 | or whether we have network problems. 13 | - reconnect? 14 | 15 | ## Testing 16 | 17 | ## Documentation 18 | - chat client example 19 | - automatic reconnect example using pharos to detect the close 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | // Detect the rustc channel 2 | // 3 | use rustc_version::{ version_meta, Channel }; 4 | 5 | fn main() 6 | { 7 | // Needed to avoid warnings for: 8 | // https://doc.rust-lang.org/nightly/rustc/check-cfg/cargo-specifics.html 9 | // 10 | println!("cargo::rustc-check-cfg=cfg(stable)"); 11 | println!("cargo::rustc-check-cfg=cfg(beta)"); 12 | println!("cargo::rustc-check-cfg=cfg(nightly)"); 13 | println!("cargo::rustc-check-cfg=cfg(rustc_dev)"); 14 | 15 | // Set cfg flags depending on release channel 16 | // 17 | match version_meta().unwrap().channel 18 | { 19 | Channel::Stable => println!( "cargo:rustc-cfg=stable" ), 20 | Channel::Beta => println!( "cargo:rustc-cfg=beta" ), 21 | Channel::Nightly => println!( "cargo:rustc-cfg=nightly" ), 22 | Channel::Dev => println!( "cargo:rustc-cfg=rustc_dev" ), 23 | } 24 | 25 | println!("cargo:rerun-if-changed=build.rs"); 26 | } 27 | -------------------------------------------------------------------------------- /ci/clippy.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | 3 | # fail fast 4 | # 5 | set -e 6 | 7 | # print each command before it's executed 8 | # 9 | set -x 10 | 11 | export RUSTFLAGS="-D warnings --cfg getrandom_backend=\"wasm_js\"" 12 | 13 | cargo clean 14 | cargo +nightly clippy --tests --examples --benches --all-features --target wasm32-unknown-unknown -- -D warnings 15 | 16 | -------------------------------------------------------------------------------- /ci/doc.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | 3 | # Only run on nightly. 4 | 5 | # fail fast 6 | # 7 | set -e 8 | 9 | # print each command before it's executed 10 | # 11 | set -x 12 | 13 | export RUSTFLAGS="-D warnings" 14 | 15 | 16 | cargo doc --all-features --no-deps 17 | 18 | # doc tests aren't working on wasm for now... 19 | # 20 | # cargo test --doc --all-features --target wasm32-unknown-unknown 21 | -------------------------------------------------------------------------------- /ci/server.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | 3 | # fail fast 4 | # 5 | set -e 6 | 7 | # print each command before it's executed 8 | # 9 | set -x 10 | 11 | export RUSTFLAGS="-D warnings" 12 | 13 | git clone --depth 1 https://github.com/najamelan/ws_stream_tungstenite 14 | cd ws_stream_tungstenite 15 | cargo build --example echo --release 16 | cargo build --example echo_tt --release 17 | 18 | cargo run --example echo --release & 19 | cargo run --example echo_tt --release -- "127.0.0.1:3312" & 20 | 21 | cd examples/ssl 22 | cargo build --release 23 | cargo run --release -- "127.0.0.1:8443" & 24 | -------------------------------------------------------------------------------- /ci/test.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | 3 | # fail fast 4 | # 5 | set -e 6 | 7 | # print each command before it's executed 8 | # 9 | set -x 10 | 11 | export RUSTFLAGS="-D warnings --cfg getrandom_backend=\"wasm_js\"" 12 | 13 | wasm-pack test --firefox --headless -- --all-features 14 | wasm-pack test --chrome --headless -- --all-features 15 | wasm-pack test --firefox --headless -- --all-features --release 16 | wasm-pack test --chrome --headless -- --all-features --release 17 | -------------------------------------------------------------------------------- /deny.toml: -------------------------------------------------------------------------------- 1 | # This template contains all of the possible sections and their default values 2 | 3 | # Note that all fields that take a lint level have these possible values: 4 | # * deny - An error will be produced and the check will fail 5 | # * warn - A warning will be produced, but the check will not fail 6 | # * allow - No warning or error will be produced, though in some cases a note 7 | # will be 8 | 9 | # The values provided in this template are the default values that will be used 10 | # when any section or field is not specified in your own configuration 11 | 12 | 13 | [graph] 14 | # If 1 or more target triples (and optionally, target_features) are specified, 15 | # only the specified targets will be checked when running `cargo deny check`. 16 | # This means, if a particular package is only ever used as a target specific 17 | # dependency, such as, for example, the `nix` crate only being used via the 18 | # `target_family = "unix"` configuration, that only having windows targets in 19 | # this list would mean the nix crate, as well as any of its exclusive 20 | # dependencies not shared by any other crates, would be ignored, as the target 21 | # list here is effectively saying which targets you are building for. 22 | targets = [ 23 | # The triple can be any string, but only the target triples built in to 24 | # rustc (as of 1.40) can be checked against actual config expressions 25 | #{ triple = "x86_64-unknown-linux-musl" }, 26 | # You can also specify which target_features you promise are enabled for a 27 | # particular target. target_features are currently not validated against 28 | # the actual valid features supported by the target architecture. 29 | #{ triple = "wasm32-unknown-unknown", features = ["atomics"] }, 30 | ] 31 | 32 | # This section is considered when running `cargo deny check advisories` 33 | # More documentation for the advisories section can be found here: 34 | # https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html 35 | [advisories] 36 | # The path where the advisory database is cloned/fetched into 37 | db-path = "~/.cargo/advisory-db" 38 | # The url of the advisory database to use 39 | db-urls = [ "https://github.com/rustsec/advisory-db" ] 40 | # A list of advisory IDs to ignore. Note that ignored advisories will still 41 | # output a note when they are encountered. 42 | ignore = [ 43 | #"RUSTSEC-0000-0000", 44 | ] 45 | # Threshold for security vulnerabilities, any vulnerability with a CVSS score 46 | # lower than the range specified will be ignored. Note that ignored advisories 47 | # will still output a note when they are encountered. 48 | # * None - CVSS Score 0.0 49 | # * Low - CVSS Score 0.1 - 3.9 50 | # * Medium - CVSS Score 4.0 - 6.9 51 | # * High - CVSS Score 7.0 - 8.9 52 | # * Critical - CVSS Score 9.0 - 10.0 53 | #severity-threshold = 54 | 55 | # This section is considered when running `cargo deny check licenses` 56 | # More documentation for the licenses section can be found here: 57 | # https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html 58 | [licenses] 59 | # List of explictly allowed licenses 60 | # See https://spdx.org/licenses/ for list of possible licenses 61 | # [possible values: any SPDX 3.7 short identifier (+ optional exception)]. 62 | allow = [ 63 | "MIT", 64 | "Apache-2.0", 65 | # "Apache-2.0 WITH LLVM-exception", 66 | "Unlicense", 67 | "Unicode-3.0", 68 | ] 69 | # The confidence threshold for detecting a license from license text. 70 | # The higher the value, the more closely the license text must be to the 71 | # canonical license text of a valid SPDX license file. 72 | # [possible values: any between 0.0 and 1.0]. 73 | confidence-threshold = 0.8 74 | # Allow 1 or more licenses on a per-crate basis, so that particular licenses 75 | # aren't accepted for every possible crate as with the normal allow list 76 | exceptions = [ 77 | # Each entry is the crate and version constraint, and its specific allow 78 | # list 79 | #{ allow = ["Zlib"], name = "adler32", version = "*" }, 80 | ] 81 | 82 | # Some crates don't have (easily) machine readable licensing information, 83 | # adding a clarification entry for it allows you to manually specify the 84 | # licensing information 85 | #[[licenses.clarify]] 86 | # The name of the crate the clarification applies to 87 | #name = "ring" 88 | # THe optional version constraint for the crate 89 | #version = "*" 90 | # The SPDX expression for the license requirements of the crate 91 | #expression = "MIT AND ISC AND OpenSSL" 92 | # One or more files in the crate's source used as the "source of truth" for 93 | # the license expression. If the contents match, the clarification will be used 94 | # when running the license check, otherwise the clarification will be ignored 95 | # and the crate will be checked normally, which may produce warnings or errors 96 | # depending on the rest of your configuration 97 | #license-files = [ 98 | # Each entry is a crate relative path, and the (opaque) hash of its contents 99 | #{ path = "LICENSE", hash = 0xbd0eed23 } 100 | #] 101 | 102 | [licenses.private] 103 | # If true, ignores workspace crates that aren't published, or are only 104 | # published to private registries 105 | ignore = false 106 | # One or more private registries that you might publish crates to, if a crate 107 | # is only published to private registries, and ignore is true, the crate will 108 | # not have its license(s) checked 109 | registries = [ 110 | #"https://sekretz.com/registry 111 | ] 112 | 113 | # This section is considered when running `cargo deny check bans`. 114 | # More documentation about the 'bans' section can be found here: 115 | # https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html 116 | [bans] 117 | # Lint level for when multiple versions of the same crate are detected 118 | multiple-versions = "warn" 119 | # The graph highlighting used when creating dotgraphs for crates 120 | # with multiple versions 121 | # * lowest-version - The path to the lowest versioned duplicate is highlighted 122 | # * simplest-path - The path to the version with the fewest edges is highlighted 123 | # * all - Both lowest-version and simplest-path are used 124 | highlight = "all" 125 | # List of crates that are allowed. Use with care! 126 | allow = [ 127 | #{ name = "ansi_term", version = "=0.11.0" }, 128 | ] 129 | # List of crates to deny 130 | deny = [ 131 | # Each entry the name of a crate and a version range. If version is 132 | # not specified, all versions will be matched. 133 | 134 | { name = "plutonium" }, # Crate intended to hide unsafe usage. 135 | { name = "fake-static" }, # Crate intended to demonstrate soundness bug in safe code. 136 | ] 137 | # Certain crates/versions that will be skipped when doing duplicate detection. 138 | skip = [ 139 | #{ name = "ansi_term", version = "=0.11.0" }, 140 | ] 141 | # Similarly to `skip` allows you to skip certain crates during duplicate 142 | # detection. Unlike skip, it also includes the entire tree of transitive 143 | # dependencies starting at the specified crate, up to a certain depth, which is 144 | # by default infinite 145 | skip-tree = [ 146 | #{ name = "ansi_term", version = "=0.11.0", depth = 20 }, 147 | ] 148 | 149 | # This section is considered when running `cargo deny check sources`. 150 | # More documentation about the 'sources' section can be found here: 151 | # https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html 152 | [sources] 153 | # Lint level for what to happen when a crate from a crate registry that is not 154 | # in the allow list is encountered 155 | unknown-registry = "warn" 156 | # Lint level for what to happen when a crate from a git repository that is not 157 | # in the allow list is encountered 158 | unknown-git = "warn" 159 | # List of URLs for allowed crate registries. Defaults to the crates.io index 160 | # if not specified. If it is specified but empty, no registries are allowed. 161 | allow-registry = ["https://github.com/rust-lang/crates.io-index"] 162 | # List of URLs for allowed Git repositories 163 | allow-git = [] 164 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | I started working on a chat application as an example for this library, but it's not finished. It's not up to date with the latest version of the library either, so it's probably not worth your time. 4 | -------------------------------------------------------------------------------- /examples/chat_client/Cargo.toml: -------------------------------------------------------------------------------- 1 | # Auto-generated from "Cargo.yml" 2 | [dependencies] 3 | console_error_panic_hook = "^0.1" 4 | futures_cbor_codec = "^0.1" 5 | futures_codec = "^0.2" 6 | js-sys = "^0.3" 7 | log = "^0.4" 8 | pin-utils = "^0.1.0-alpha" 9 | regex = "^1" 10 | wasm-bindgen = "^0.2" 11 | wasm-logger = "^0.1" 12 | 13 | [dependencies.chat_format] 14 | path = "../chat_format" 15 | 16 | [dependencies.futures-preview] 17 | features = ["async-await", "nightly"] 18 | version = "^0.3.0-alpha" 19 | 20 | [dependencies.getrandom] 21 | default-features = false 22 | features = ["wasm-bindgen"] 23 | version = "^0.1" 24 | 25 | [dependencies.gloo-events] 26 | git = "https://github.com/rustwasm/gloo" 27 | 28 | [dependencies.naja_async_runtime] 29 | default-features = false 30 | version = "^0.3" 31 | 32 | [dependencies.wasm-bindgen-futures] 33 | features = ["futures_0_3"] 34 | version = "^0.3" 35 | 36 | [dependencies.web-sys] 37 | features = ["console", "CssStyleDeclaration", "Document", "Element", "HtmlDivElement", "HtmlElement", "HtmlFormElement", "HtmlInputElement", "HtmlParagraphElement", "HtmlTextAreaElement", "KeyboardEvent", "Node", "Window"] 38 | version = "^0.3" 39 | 40 | [dependencies.ws_stream_wasm] 41 | path = "../../" 42 | 43 | [lib] 44 | crate-type = ["cdylib"] 45 | path = "src/entrypoint.rs" 46 | 47 | [package] 48 | authors = ["Naja Melan "] 49 | description = "An example for using websockets in rust WASM." 50 | edition = "2018" 51 | name = "ws_stream_wasm_chat_client" 52 | repository = "https::/github.com/najamelan/async_runtime" 53 | version = "0.1.0" 54 | -------------------------------------------------------------------------------- /examples/chat_client/Cargo.yml: -------------------------------------------------------------------------------- 1 | package: 2 | 3 | name : ws_stream_wasm_chat_client 4 | version : 0.1.0 5 | authors : [ Naja Melan ] 6 | edition : '2018' 7 | description: An example for using websockets in rust WASM. 8 | repository : https::/github.com/najamelan/async_runtime 9 | 10 | 11 | lib: 12 | 13 | crate-type : [ cdylib ] 14 | path : src/entrypoint.rs 15 | 16 | 17 | dependencies: 18 | 19 | chat_format : { path: "../chat_format" } 20 | console_error_panic_hook: ^0.1 21 | futures-preview : { version: ^0.3.0-alpha, features: [async-await, nightly] } 22 | futures_cbor_codec : ^0.1 23 | futures_codec : ^0.2 24 | getrandom : { version: ^0.1, default-features: false, features: [ wasm-bindgen ] } 25 | gloo-events : { git: "https://github.com/rustwasm/gloo" } 26 | js-sys : ^0.3 27 | log : ^0.4 28 | naja_async_runtime : { version: ^0.3, default-features: false } 29 | pin-utils : ^0.1.0-alpha 30 | regex : ^1 31 | wasm-bindgen : ^0.2 32 | wasm-bindgen-futures : { version: ^0.3, features: [ futures_0_3 ] } 33 | wasm-logger : ^0.1 34 | ws_stream_wasm : { path: ../../ } 35 | 36 | 37 | web-sys: 38 | 39 | version : ^0.3 40 | 41 | features : 42 | [ 43 | console , 44 | CssStyleDeclaration , 45 | Document , 46 | Element , 47 | HtmlDivElement , 48 | HtmlElement , 49 | HtmlFormElement , 50 | HtmlInputElement , 51 | HtmlParagraphElement, 52 | HtmlTextAreaElement , 53 | KeyboardEvent , 54 | Node , 55 | Window , 56 | ] 57 | -------------------------------------------------------------------------------- /examples/chat_client/README.md: -------------------------------------------------------------------------------- 1 | # Ws_stream_wasm chat client example 2 | 3 | Demonstration of `ws_stream_wasm` working in WASM. This example shows a rather realistic (error handling, security, basic features) chat client that communicates with a chat server over websockets. The communication with the server happens with 4 | a custom enum, serialized with a cbor codec (for futures-codec), over AsyncRead/AsyncWrite 0.3. 5 | 6 | What ws_stream_wasm adds here is that we just frame the connection with a codec instead of manually serializing our 7 | data structure, creating a websocket message with `web_sys`, and deal with all the potential errors on the connection 8 | by hand. 9 | 10 | In the future I shall rewrite this chat program using the thespis actor library showing how the design can be a lot more 11 | convenient when using actors. 12 | 13 | ## Install 14 | 15 | This requires you to run the chat_server example from [ws_stream](https://github.com/najamelan/ws_stream). You can tweak 16 | the ip:port to something else if you want (for the server you can pass it on the cmd line). 17 | 18 | You will need wasm-pack: 19 | ```bash 20 | cargo install wasm-pack 21 | 22 | # and compile the client 23 | # 24 | wasm-pack build --target web 25 | 26 | # in ws_stream repo 27 | # make sure this is running in the same network namespace as your browser 28 | # 29 | cargo run --example chat_server --release 30 | ``` 31 | 32 | ## Usage 33 | 34 | Now you can open the `index.html` from this crate in several web browser tabs and chat away. 35 | 36 | 37 | ## TODO 38 | - server side disconnect 39 | - reread all code and cleanup 40 | - document as example 41 | - gui 42 | - blog post? 43 | -------------------------------------------------------------------------------- /examples/chat_client/TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | - clean up program structure 4 | - write tutorial and document code 5 | -------------------------------------------------------------------------------- /examples/chat_client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ws_stream_wasm Chat Client Example 5 | 6 | 7 | 8 | 9 | 10 | 11 | 40 | 41 | 42 |

ws_stream_wasm Chat Client Example

43 | 44 | 45 |
46 |
47 | 48 | 49 | 50 | 52 | 53 |
54 | 55 | 56 | 57 | 58 |
59 | 60 | 61 |
62 | 63 |
64 | 65 |

66 | 67 | 71 | 72 | 76 | 77 | 82 | 83 |
84 | 85 |
86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /examples/chat_client/src/color.rs: -------------------------------------------------------------------------------- 1 | #![ allow( dead_code )] 2 | 3 | use crate::import::*; 4 | 5 | 6 | 7 | pub struct Color 8 | { 9 | r: u8, 10 | g: u8, 11 | b: u8, 12 | a: u8, 13 | } 14 | 15 | 16 | impl Color 17 | { 18 | pub fn new( r: u8, g: u8, b: u8, a: u8 ) -> Self 19 | { 20 | Self{ r, g, b, a } 21 | } 22 | 23 | pub fn random() -> Self 24 | { 25 | Self 26 | { 27 | r: ( Math::random() * 255_f64 ) as u8, 28 | g: ( Math::random() * 255_f64 ) as u8, 29 | b: ( Math::random() * 255_f64 ) as u8, 30 | a: ( Math::random() * 255_f64 ) as u8, 31 | } 32 | } 33 | 34 | 35 | // If this color is darker than half luminosity, it will be inverted 36 | // 37 | pub fn light( self ) -> Self 38 | { 39 | if self.is_dark() { self.invert() } 40 | else { self } 41 | } 42 | 43 | 44 | // If this color is lighter than half luminosity, it will be inverted 45 | // 46 | pub fn dark( self ) -> Self 47 | { 48 | if self.is_light() { self.invert() } 49 | else { self } 50 | } 51 | 52 | 53 | /// Invert color. 54 | // 55 | pub fn invert( mut self ) -> Self 56 | { 57 | self.r = 255 - self.r; 58 | self.g = 255 - self.g; 59 | self.b = 255 - self.b; 60 | self.a = 255 - self.a; 61 | 62 | self 63 | } 64 | 65 | 66 | // True if this color is lighter than half luminosity. 67 | // 68 | pub fn is_light( &self ) -> bool 69 | { 70 | self.r as u16 + self.g as u16 + self.b as u16 > 378 // 128 * 3 71 | } 72 | 73 | 74 | /// True if this color is darker than half luminosity. 75 | // 76 | pub fn is_dark( &self ) -> bool 77 | { 78 | !self.is_light() 79 | } 80 | 81 | 82 | // output a css string format: "#rrggbb" 83 | // 84 | pub fn to_css( &self ) -> String 85 | { 86 | format!( "#{:02x}{:02x}{:02x}", self.r, self.g, self.b ) 87 | } 88 | 89 | 90 | // output a css string format: "rgba( rrr, ggg, bbb, aaa )" 91 | // 92 | pub fn to_cssa( &self ) -> String 93 | { 94 | format!( "rgba({},{},{},{})", self.r, self.g, self.b, self.a ) 95 | } 96 | } 97 | 98 | 99 | #[ cfg(test) ] 100 | mod tests 101 | { 102 | use super::*; 103 | 104 | #[test] 105 | // 106 | fn padding() 107 | { 108 | let c = Color::new( 1, 1, 1, 1 ); 109 | 110 | assert_eq!( "#010101", c.to_css() ); 111 | } 112 | } 113 | 114 | 115 | -------------------------------------------------------------------------------- /examples/chat_client/src/e_handler.rs: -------------------------------------------------------------------------------- 1 | use crate::import::*; 2 | 3 | 4 | pub struct EHandler 5 | { 6 | receiver: UnboundedReceiver, 7 | 8 | // Automatically removed from the DOM on drop! 9 | // 10 | _listener: EventListener, 11 | } 12 | 13 | 14 | impl EHandler 15 | { 16 | pub fn new( target: &EventTarget, event: &'static str, passive: bool ) -> Self 17 | { 18 | // debug!( "set event handler" ); 19 | 20 | let (sender, receiver) = unbounded(); 21 | let options = match passive 22 | { 23 | false => EventListenerOptions::enable_prevent_default(), 24 | true => EventListenerOptions::default(), 25 | }; 26 | 27 | // Attach an event listener 28 | // 29 | let _listener = EventListener::new_with_options( &target, event, options, move |event| 30 | { 31 | sender.unbounded_send(event.clone()).unwrap_throw(); 32 | }); 33 | 34 | Self 35 | { 36 | receiver, 37 | _listener, 38 | } 39 | } 40 | } 41 | 42 | 43 | 44 | impl Stream for EHandler 45 | { 46 | type Item = Event; 47 | 48 | fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll> 49 | { 50 | Pin::new( &mut self.receiver ).poll_next(cx) 51 | } 52 | } 53 | 54 | -------------------------------------------------------------------------------- /examples/chat_client/src/entrypoint.rs: -------------------------------------------------------------------------------- 1 | #![ allow( unused_imports ) ] 2 | 3 | pub(crate) mod e_handler ; 4 | pub(crate) mod color ; 5 | pub(crate) mod user_list ; 6 | 7 | 8 | 9 | mod import 10 | { 11 | pub(crate) use 12 | { 13 | chat_format :: { ServerMsg, ClientMsg } , 14 | async_runtime :: { * } , 15 | web_sys :: { *, console::log_1 as dbg } , 16 | ws_stream_wasm :: { * } , 17 | futures_codec :: { * } , 18 | futures_cbor_codec :: { Codec, Error as CodecError } , 19 | futures :: { prelude::*, stream::SplitStream, select, ready } , 20 | futures :: { channel::{ mpsc::{ unbounded, UnboundedReceiver, UnboundedSender } } } , 21 | log :: { * } , 22 | web_sys :: { * } , 23 | wasm_bindgen :: { prelude::*, JsCast } , 24 | gloo_events :: { * } , 25 | std :: { rc::Rc, convert::TryInto, cell::RefCell, io } , 26 | std :: { task::*, pin::Pin, collections::HashMap, panic } , 27 | regex :: { Regex } , 28 | js_sys :: { Date, Math } , 29 | pin_utils :: { pin_mut } , 30 | wasm_bindgen_futures :: { futures_0_3::spawn_local } , 31 | }; 32 | } 33 | 34 | use crate::{ import::*, e_handler::*, color::*, user_list::* }; 35 | 36 | 37 | const HELP: &str = "Available commands: 38 | /nick NEWNAME # change nick (must be between 1 and 15 word characters) 39 | /help # Print available commands"; 40 | 41 | 42 | // Called when the wasm module is instantiated 43 | // 44 | #[ wasm_bindgen( start ) ] 45 | // 46 | pub fn main() -> Result<(), JsValue> 47 | { 48 | panic::set_hook(Box::new(console_error_panic_hook::hook)); 49 | 50 | // Let's only log output when in debug mode 51 | // 52 | #[ cfg( debug_assertions ) ] 53 | // 54 | wasm_logger::init( wasm_logger::Config::new(Level::Debug).message_on_new_line() ); 55 | 56 | let program = async 57 | { 58 | let cform = get_id( "connect_form" ); 59 | let tarea = get_id( "chat_input" ); 60 | let chat = get_id( "chat_form" ); 61 | 62 | let cnick: HtmlInputElement = get_id( "connect_nick" ).unchecked_into(); 63 | cnick.set_value( random_name() ); 64 | 65 | let enter_evts = EHandler::new( &tarea, "keypress", false ); 66 | let csubmit_evts = EHandler::new( &cform, "submit" , false ); 67 | let creset_evts = EHandler::new( &cform, "reset" , false ); 68 | let reset_evts = EHandler::new( &chat , "reset" , false ); 69 | 70 | let (tx, rx) = unbounded(); 71 | 72 | 73 | spawn_local( on_disconnect( reset_evts, rx ) ); 74 | spawn_local( on_key ( enter_evts ) ); 75 | spawn_local( on_cresets ( creset_evts ) ); 76 | 77 | on_connect( csubmit_evts, tx ).await; 78 | 79 | info!( "main function ends" ); 80 | }; 81 | 82 | spawn_local( program ); 83 | 84 | 85 | Ok(()) 86 | } 87 | 88 | 89 | fn append_line( chat: &Element, time: f64, nick: &str, line: &str, color: &Color, color_all: bool ) 90 | { 91 | let p: HtmlElement = document().create_element( "p" ).expect_throw( "create p" ).unchecked_into(); 92 | let n: HtmlElement = document().create_element( "span" ).expect_throw( "create span" ).unchecked_into(); 93 | let m: HtmlElement = document().create_element( "span" ).expect_throw( "create span" ).unchecked_into(); 94 | let t: HtmlElement = document().create_element( "span" ).expect_throw( "create span" ).unchecked_into(); 95 | 96 | n.style().set_property( "color", &color.to_css() ).expect_throw( "set color" ); 97 | 98 | if color_all 99 | { 100 | m.style().set_property( "color", &color.to_css() ).expect_throw( "set color" ); 101 | } 102 | 103 | // Js needs milliseconds, where the server sends seconds 104 | // 105 | let time = Date::new( &( time * 1000 as f64 ).into() ); 106 | 107 | n.set_inner_text( &format!( "{}: ", nick ) ); 108 | m.set_inner_text( line ); 109 | t.set_inner_text( &format!( "{:02}:{:02}:{:02} - ", time.get_hours(), time.get_minutes(), time.get_seconds() ) ); 110 | 111 | n.set_class_name( "nick" ); 112 | m.set_class_name( "message_text" ); 113 | t.set_class_name( "time" ); 114 | 115 | p.append_child( &t ).expect( "Coundn't append child" ); 116 | p.append_child( &n ).expect( "Coundn't append child" ); 117 | p.append_child( &m ).expect( "Coundn't append child" ); 118 | 119 | // order is important here, we need to measure the scroll before adding the item 120 | // 121 | let max_scroll = chat.scroll_height() - chat.client_height(); 122 | chat.append_child( &p ).expect( "Coundn't append child" ); 123 | 124 | // Check whether we are scolled to the bottom. If so, we autoscroll new messages 125 | // into vies. If the user has scrolled up, we don't. 126 | // 127 | // We keep a margin of up to 2 pixels, because sometimes the two numbers don't align exactly. 128 | // 129 | if ( chat.scroll_top() - max_scroll ).abs() < 3 130 | { 131 | p.scroll_into_view(); 132 | } 133 | } 134 | 135 | 136 | async fn on_msg( mut stream: impl Stream> + Unpin ) 137 | { 138 | let chat = get_id( "chat" ); 139 | let mut u_list = UserList::new(); 140 | 141 | let mut colors: HashMap = HashMap::new(); 142 | 143 | // for the server messages 144 | // 145 | colors.insert( 0, Color::random().light() ); 146 | 147 | 148 | while let Some( msg ) = stream.next().await 149 | { 150 | // TODO: handle io errors 151 | // 152 | let msg = match msg 153 | { 154 | Ok( msg ) => msg, 155 | _ => continue, 156 | }; 157 | 158 | 159 | 160 | debug!( "received message" ); 161 | 162 | 163 | match msg 164 | { 165 | ServerMsg::ChatMsg{ time, nick, sid, txt } => 166 | { 167 | let color = colors.entry( sid ).or_insert( Color::random().light() ); 168 | 169 | append_line( &chat, time as f64, &nick, &txt, color, false ); 170 | } 171 | 172 | 173 | ServerMsg::ServerMsg{ time, txt } => 174 | { 175 | append_line( &chat, time as f64, "Server", &txt, colors.get( &0 ).unwrap(), true ); 176 | } 177 | 178 | 179 | ServerMsg::Welcome{ time, users, txt } => 180 | { 181 | users.into_iter().for_each( |(s,n)| u_list.insert(s,n) ); 182 | 183 | let udiv = get_id( "users" ); 184 | u_list.render( udiv.unchecked_ref() ); 185 | 186 | append_line( &chat, time as f64, "Server", &txt, colors.get( &0 ).unwrap(), true ); 187 | 188 | 189 | // Client Welcome message 190 | // 191 | append_line( &chat, time as f64, "ws_stream_wasm Client", HELP, colors.get( &0 ).unwrap(), true ); 192 | } 193 | 194 | 195 | ServerMsg::NickChanged{ time, old, new, sid } => 196 | { 197 | append_line( &chat, time as f64, "Server", &format!( "{} has changed names => {}.", &old, &new ), colors.get( &0 ).unwrap(), true ); 198 | u_list.insert( sid, new ); 199 | } 200 | 201 | 202 | ServerMsg::NickUnchanged{ time, .. } => 203 | { 204 | append_line( &chat, time as f64, "Server", "Error: You specified your old nick instead of your new one, it's unchanged.", colors.get( &0 ).unwrap(), true ); 205 | } 206 | 207 | 208 | ServerMsg::NickInUse{ time, nick, .. } => 209 | { 210 | append_line( &chat, time as f64, "Server", &format!( "Error: The nick is already in use: '{}'.", &nick ), colors.get( &0 ).unwrap(), true ); 211 | } 212 | 213 | 214 | ServerMsg::NickInvalid{ time, nick, .. } => 215 | { 216 | append_line( &chat, time as f64, "Server", &format!( "Error: The nick you specify must be between 1 and 15 word characters, was invalid: '{}'.", &nick ), colors.get( &0 ).unwrap(), true ); 217 | } 218 | 219 | 220 | ServerMsg::UserJoined{ time, nick, sid } => 221 | { 222 | append_line( &chat, time as f64, "Server", &format!( "We welcome a new user, {}!", &nick ), colors.get( &0 ).unwrap(), true ); 223 | u_list.insert( sid, nick ); 224 | } 225 | 226 | 227 | ServerMsg::UserLeft{ time, nick, sid } => 228 | { 229 | u_list.remove( sid ); 230 | append_line( &chat, time as f64, "Server", &format!( "Sadly, {} has left us.", &nick ), colors.get( &0 ).unwrap(), true ); 231 | } 232 | 233 | _ => {} 234 | } 235 | } 236 | 237 | // The stream has closed, so we are disconnected 238 | // 239 | show_connect_form(); 240 | 241 | debug!( "leaving on_msg" ); 242 | } 243 | 244 | 245 | async fn on_submit 246 | ( 247 | mut submits: impl Stream< Item=Event > + Unpin , 248 | mut out : impl Sink < ClientMsg, Error=CodecError > + Unpin , 249 | ) 250 | { 251 | let chat = get_id( "chat" ); 252 | 253 | let nickre = Regex::new( r"^/nick (\w{1,15})" ).unwrap(); 254 | 255 | // Note that we always add a newline below, so we have to match it. 256 | // 257 | let helpre = Regex::new(r"^/help\n$").unwrap(); 258 | 259 | let textarea = get_id( "chat_input" ); 260 | let textarea: &HtmlTextAreaElement = textarea.unchecked_ref(); 261 | 262 | 263 | while let Some( evt ) = submits.next().await 264 | { 265 | debug!( "on_submit" ); 266 | 267 | evt.prevent_default(); 268 | 269 | let text = textarea.value().trim().to_string() + "\n"; 270 | textarea.set_value( "" ); 271 | let _ = textarea.focus(); 272 | 273 | if text == "\n" { continue; } 274 | 275 | let msg; 276 | 277 | 278 | // if this is a /nick somename message 279 | // 280 | if let Some( cap ) = nickre.captures( &text ) 281 | { 282 | debug!( "handle set nick: {:#?}", &text ); 283 | 284 | msg = ClientMsg::SetNick( cap[1].to_string() ); 285 | } 286 | 287 | 288 | // if this is a /help message 289 | // 290 | else if helpre.is_match( &text ) 291 | { 292 | debug!( "handle /help: {:#?}", &text ); 293 | 294 | append_line( &chat, Date::now(), "ws_stream_wasm Client", HELP, &Color::random().light(), true ); 295 | 296 | return; 297 | } 298 | 299 | 300 | else 301 | { 302 | debug!( "handle send: {:#?}", &text ); 303 | 304 | msg = ClientMsg::ChatMsg( text ); 305 | } 306 | 307 | 308 | match out.send( msg ).await 309 | { 310 | Ok(()) => {} 311 | 312 | Err(e) => 313 | { 314 | match e 315 | { 316 | // We lost the connection to the server 317 | // 318 | CodecError::Io(err) => match err.kind() 319 | { 320 | io::ErrorKind::NotConnected => 321 | { 322 | error!( "The connection to the server was lost" ); 323 | 324 | // Show login screen... 325 | // 326 | show_connect_form(); 327 | } 328 | 329 | _ => error!( "{}", &err ), 330 | 331 | }, 332 | 333 | _ => error!( "{}", &e ), 334 | } 335 | } 336 | }; 337 | }; 338 | } 339 | 340 | 341 | 342 | 343 | // When the user presses the Enter key in the textarea we submit, rather than adding a new line 344 | // for newline, the user can use shift+Enter. 345 | // 346 | // We use the click effect on the Send button, because form.submit() wont let our on_submit handler run. 347 | // 348 | async fn on_key 349 | ( 350 | mut keys: impl Stream< Item=Event > + Unpin , 351 | ) 352 | { 353 | let send: HtmlElement = get_id( "chat_submit" ).unchecked_into(); 354 | 355 | 356 | while let Some( evt ) = keys.next().await 357 | { 358 | let evt: KeyboardEvent = evt.unchecked_into(); 359 | 360 | if evt.code() == "Enter" && !evt.shift_key() 361 | { 362 | send.click(); 363 | evt.prevent_default(); 364 | } 365 | }; 366 | } 367 | 368 | 369 | 370 | async fn on_connect( mut evts: impl Stream< Item=Event > + Unpin, mut disconnect: UnboundedSender ) 371 | { 372 | while let Some(evt) = evts.next().await 373 | { 374 | info!( "Connect button clicked" ); 375 | 376 | evt.prevent_default(); 377 | 378 | // validate form 379 | // 380 | let (nick, url) = match validate_connect_form() 381 | { 382 | Ok(ok) => ok, 383 | 384 | Err( _ ) => 385 | { 386 | // report error to the user 387 | // continue loop 388 | // 389 | unreachable!() 390 | } 391 | }; 392 | 393 | 394 | let (ws, wsio) = match WsMeta::connect( url, None ).await 395 | { 396 | Ok(conn) => conn, 397 | 398 | Err(e) => 399 | { 400 | // report error to the user 401 | // 402 | error!( "{}", e ); 403 | continue; 404 | } 405 | }; 406 | 407 | let framed = Framed::new( wsio, Codec::new() ); 408 | let (mut out, mut msgs) = framed.split(); 409 | 410 | let form = get_id( "chat_form" ); 411 | let on_send = EHandler::new( &form, "submit", false ); 412 | 413 | 414 | // hide the connect form 415 | // 416 | let cform: HtmlElement = get_id( "connect_form" ).unchecked_into(); 417 | 418 | 419 | // Ask the server to join 420 | // 421 | match out.send( ClientMsg::Join( nick ) ).await 422 | { 423 | Ok(()) => {} 424 | Err(e) => { error!( "{}", e ); } 425 | }; 426 | 427 | 428 | // Error handling 429 | // 430 | let cerror: HtmlElement = get_id( "connect_error" ).unchecked_into(); 431 | 432 | 433 | if let Some(response) = msgs.next().await 434 | { 435 | match response 436 | { 437 | Ok( ServerMsg::JoinSuccess ) => 438 | { 439 | info!( "got joinsuccess" ); 440 | 441 | cform.style().set_property( "display", "none" ).expect_throw( "set cform display none" ); 442 | 443 | disconnect.send( ws ).await.expect_throw( "send ws to disconnect" ); 444 | 445 | let msg = on_msg ( msgs ).fuse(); 446 | let sub = on_submit( on_send , out ).fuse(); 447 | 448 | pin_mut!( msg ); 449 | pin_mut!( sub ); 450 | 451 | // on_msg will end when the stream get's closed by disconnect. This way we will 452 | // stop on_submit as well. 453 | // 454 | select! 455 | { 456 | _ = msg => {} 457 | _ = sub => {} 458 | } 459 | } 460 | 461 | // Show an error message on the connect form and let the user try again 462 | // 463 | Ok( ServerMsg::NickInUse{ .. } ) => 464 | { 465 | cerror.set_inner_text( "The nick name is already in use. Please choose another." ); 466 | cerror.style().set_property( "display", "block" ).expect_throw( "set display block on cerror" ); 467 | 468 | continue; 469 | } 470 | 471 | Ok( ServerMsg::NickInvalid{ .. } ) => 472 | { 473 | cerror.set_inner_text( "The nick name is invalid. It must be between 1 and 15 word characters." ); 474 | cerror.style().set_property( "display", "block" ).expect_throw( "set display block on cerror" ); 475 | 476 | continue; 477 | } 478 | 479 | // cbor decoding error 480 | // 481 | Err(e) => 482 | { 483 | error!( "{}", e ); 484 | } 485 | 486 | _ => { } 487 | } 488 | } 489 | } 490 | 491 | error!( "on_connect ends" ); 492 | } 493 | 494 | 495 | 496 | fn validate_connect_form() -> Result< (String, String), () > 497 | { 498 | let nick_field: HtmlInputElement = get_id( "connect_nick" ).unchecked_into(); 499 | let url_field : HtmlInputElement = get_id( "connect_url" ).unchecked_into(); 500 | 501 | let nick = nick_field.value(); 502 | let url = url_field .value(); 503 | 504 | Ok((nick, url)) 505 | } 506 | 507 | 508 | 509 | async fn on_cresets( mut evts: impl Stream< Item=Event > + Unpin ) 510 | { 511 | while let Some( evt ) = evts.next().await 512 | { 513 | evt.prevent_default(); 514 | 515 | let cnick: HtmlInputElement = get_id( "connect_nick" ).unchecked_into(); 516 | let curl : HtmlInputElement = get_id( "connect_url" ).unchecked_into(); 517 | 518 | cnick.set_value( random_name() ); 519 | curl .set_value( "ws://127.0.0.1:3412/chat" ); 520 | } 521 | } 522 | 523 | 524 | 525 | async fn on_disconnect( mut evts: impl Stream< Item=Event > + Unpin, mut wss: UnboundedReceiver ) 526 | { 527 | let ws1: Rc>> = Rc::new(RefCell::new( None )); 528 | let ws2 = ws1.clone(); 529 | 530 | 531 | let wss_in = async move 532 | { 533 | while let Some( ws ) = wss.next().await 534 | { 535 | *ws2.borrow_mut() = Some(ws); 536 | } 537 | }; 538 | 539 | spawn_local( wss_in ); 540 | 541 | 542 | while evts.next().await.is_some() 543 | { 544 | debug!( "on_disconnect" ); 545 | 546 | if let Some( ws_stream ) = ws1.borrow_mut().take() 547 | { 548 | ws_stream.close().await.expect_throw( "close ws" ); 549 | debug!( "connection closed by disconnect" ); 550 | show_connect_form(); 551 | } 552 | } 553 | } 554 | 555 | 556 | 557 | fn show_connect_form() 558 | { 559 | // show the connect form 560 | // 561 | let cform: HtmlElement = get_id( "connect_form" ).unchecked_into(); 562 | 563 | cform.style().set_property( "display", "flex" ).expect_throw( "set cform display none" ); 564 | 565 | get_id( "users" ).set_inner_html( "" ); 566 | get_id( "chat" ).set_inner_html( "" ); 567 | } 568 | 569 | 570 | 571 | pub fn document() -> Document 572 | { 573 | let window = web_sys::window().expect_throw( "no global `window` exists"); 574 | 575 | window.document().expect_throw( "should have a document on window" ) 576 | } 577 | 578 | 579 | 580 | // Return a random name 581 | // 582 | pub fn random_name() -> &'static str 583 | { 584 | // I wanted to use the crate scottish_names to generate a random username, but 585 | // it uses the rand crate which doesn't support wasm for now, so we're just using 586 | // a small sample. 587 | // 588 | let list = vec! 589 | [ 590 | "Aleeza" 591 | , "Aoun" 592 | , "Arya" 593 | , "Azaan" 594 | , "Ebony" 595 | , "Emke" 596 | , "Elena" 597 | , "Hafsa" 598 | , "Hailie" 599 | , "Inaaya" 600 | , "Iqra" 601 | , "Kobi" 602 | , "Noor" 603 | , "Nora" 604 | , "Nuala" 605 | , "Orin" 606 | , "Pippa" 607 | , "Rhuaridh" 608 | , "Salah" 609 | , "Susheela" 610 | , "Teya" 611 | ]; 612 | 613 | // pick one 614 | // 615 | list[ Math::floor( Math::random() * list.len() as f64 ) as usize ] 616 | } 617 | 618 | 619 | fn get_id( id: &str ) -> Element 620 | { 621 | document().get_element_by_id( id ).expect_throw( &format!( "find {}", id ) ) 622 | } 623 | 624 | 625 | 626 | 627 | 628 | -------------------------------------------------------------------------------- /examples/chat_client/src/user_list.rs: -------------------------------------------------------------------------------- 1 | use crate :: { import::*, color::*, document }; 2 | 3 | 4 | pub struct User 5 | { 6 | sid : usize , 7 | nick : String , 8 | color : Color , 9 | p : HtmlParagraphElement , 10 | indom : bool , 11 | } 12 | 13 | 14 | impl User 15 | { 16 | pub fn new( sid: usize, nick: String ) -> Self 17 | { 18 | Self 19 | { 20 | nick , 21 | sid , 22 | color: Color::random().light(), 23 | p : document().create_element( "p" ).expect( "create user p" ).unchecked_into(), 24 | indom: false, 25 | } 26 | } 27 | 28 | 29 | pub fn render( &mut self, parent: &HtmlElement ) 30 | { 31 | self.p.set_inner_text( &self.nick ); 32 | 33 | self.p.style().set_property( "color", &self.color.to_css() ).expect_throw( "set color" ); 34 | self.p.set_id( &format!( "user_{}", &self.sid ) ); 35 | 36 | parent.append_child( &self.p ).expect_throw( "append user to div" ); 37 | 38 | self.indom = true; 39 | } 40 | 41 | 42 | pub fn change_nick( &mut self, new: String ) 43 | { 44 | self.nick = new; 45 | 46 | if self.indom { self.render( &self.p.parent_node().expect_throw( "get user parent node" ).unchecked_into() ) } 47 | } 48 | } 49 | 50 | 51 | 52 | impl Drop for User 53 | { 54 | fn drop( &mut self ) 55 | { 56 | debug!( "removing user from dom" ); 57 | self.p.remove(); 58 | } 59 | } 60 | 61 | 62 | 63 | pub struct UserList 64 | { 65 | users: HashMap, 66 | div : HtmlDivElement , 67 | indom: bool , 68 | } 69 | 70 | 71 | 72 | impl UserList 73 | { 74 | pub fn new() -> Self 75 | { 76 | Self 77 | { 78 | users: HashMap::new() , 79 | div : document().create_element( "div" ).expect( "create userlist div" ).unchecked_into() , 80 | indom: false, 81 | } 82 | } 83 | 84 | 85 | pub fn insert( &mut self, sid: usize, nick: String ) 86 | { 87 | let mut render = false; 88 | 89 | let user = self.users.entry( sid ) 90 | 91 | // TODO: Get rid of clone 92 | // existing users know if they are in the dom, so we don't call render on them. 93 | // 94 | .and_modify( |usr| usr.change_nick( nick.clone() ) ) 95 | 96 | .or_insert_with ( || 97 | { 98 | render = true; 99 | User::new( sid, nick ) 100 | }) 101 | 102 | ; 103 | 104 | if render { user.render( &self.div ); } 105 | } 106 | 107 | 108 | pub fn remove( &mut self, sid: usize ) 109 | { 110 | self.users.remove( &sid ); 111 | } 112 | 113 | 114 | 115 | pub fn render( &mut self, parent: &HtmlElement ) 116 | { 117 | for ref mut user in self.users.values_mut() 118 | { 119 | user.render( &self.div ); 120 | } 121 | 122 | parent.append_child( &self.div ).expect_throw( "add udiv to dom" ); 123 | 124 | self.indom = true; 125 | } 126 | } 127 | 128 | 129 | impl Drop for UserList 130 | { 131 | fn drop( &mut self ) 132 | { 133 | // remove self from Dom 134 | // 135 | self.div.remove(); 136 | // 137 | // Delete children 138 | // 139 | 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /examples/chat_client/style.css: -------------------------------------------------------------------------------- 1 | :root 2 | { 3 | --main-bg-color : #222; 4 | --main-fg-color : #ddd; 5 | --dark-blue-bg : #111118; 6 | --darker-blue-bg : #080811; 7 | --chat-input-bg : black; 8 | --chat-input-fg : light-grey; 9 | --border-color : aliceblue; 10 | } 11 | 12 | p 13 | { 14 | margin:0; 15 | padding: .1rem; 16 | } 17 | 18 | body 19 | { 20 | background: var( --main-bg-color ); 21 | color : var( --main-fg-color ); 22 | display: grid; 23 | grid-template-columns: 5fr 1fr; 24 | grid-template-rows: 2fr 17fr 1fr; 25 | height: 100%; 26 | margin: 0; 27 | max-height: 100%; 28 | overflow:hidden; 29 | padding : .5rem; 30 | } 31 | 32 | #title_div 33 | { 34 | grid-column-start: 1; 35 | grid-column-end: 3; 36 | grid-row-start: 1; 37 | grid-row-end: 1; 38 | text-align: center; 39 | background: #363d59; 40 | border-bottom: var( --border-color ) 1px solid; 41 | } 42 | 43 | #title 44 | { 45 | } 46 | 47 | #chat 48 | { 49 | padding : .5rem; 50 | margin : 0 .2rem .2rem 0; 51 | overflow-y: auto; 52 | background: var( --dark-blue-bg ); 53 | color: white; 54 | grid-column-start: 1; 55 | grid-column-end: 2; 56 | grid-row-start: 2; 57 | grid-row-end: 3; 58 | } 59 | 60 | #users 61 | { 62 | background: var( --darker-blue-bg ); 63 | padding: .5rem; 64 | grid-column-start: 2; 65 | grid-column-end: 2; 66 | grid-row-start: 2; 67 | grid-row-end: 4; 68 | overflow-y: auto; 69 | border-left: var( --border-color ) 1px solid; 70 | 71 | } 72 | 73 | #chat_form 74 | { 75 | grid-column-start: 1; 76 | grid-column-end: 2; 77 | grid-row-start: 3; 78 | grid-row-end: 4; 79 | display: flex; 80 | margin: 0; 81 | } 82 | 83 | #chat_input 84 | { 85 | background : var( --chat-input-bg ); 86 | color : var( --chat-input-fg ); 87 | flex : auto; 88 | margin : 0; 89 | border-radius : 1rem; 90 | margin : .2rem; 91 | padding : .3rem; 92 | } 93 | 94 | #connect_form 95 | { 96 | margin: 0; 97 | background: gray; 98 | grid-column-start: 1; 99 | grid-column-end: 3; 100 | grid-row-start: 2; 101 | grid-row-end: 4; 102 | display: flex; 103 | justify-content: center; 104 | align-items: center; 105 | } 106 | 107 | #connect_form_content 108 | { 109 | margin: 0 auto; 110 | border: black 1px solid; 111 | border-radius: 1rem; 112 | max-width: 40rem; 113 | padding: 2rem; 114 | } 115 | 116 | #connect_form_content label 117 | { 118 | display: block; 119 | margin-bottom: 10px; 120 | } 121 | 122 | #connect_form_content label > span 123 | { 124 | width: 15rem; 125 | font-weight: bold; 126 | padding-top: .5rem; 127 | padding-right: .3rem; 128 | float: left; 129 | } 130 | 131 | #connect_form_content input[type="text"] 132 | { 133 | width: 24rem; 134 | } 135 | 136 | #connect_error 137 | { 138 | color : red ; 139 | font-weight : bold; 140 | display : none; 141 | } 142 | 143 | #chat_submit 144 | , #chat_disconnect 145 | { 146 | background: var( --main-bg-color ); 147 | color : white; 148 | border: yellow 1px solid; 149 | border-radius: 1rem; 150 | margin : .2rem; 151 | } 152 | -------------------------------------------------------------------------------- /examples/chat_format/Cargo.toml: -------------------------------------------------------------------------------- 1 | # Auto-generated from "Cargo.yml" 2 | [dependencies] 3 | [dependencies.serde] 4 | features = ["derive"] 5 | version = "^1" 6 | 7 | [package] 8 | authors = ["Naja Melan "] 9 | description = "An example for using websockets in rust WASM." 10 | edition = "2018" 11 | name = "chat_format" 12 | repository = "https::/github.com/najamelan/ws_stream" 13 | version = "0.1.0" 14 | -------------------------------------------------------------------------------- /examples/chat_format/Cargo.yml: -------------------------------------------------------------------------------- 1 | package: 2 | 3 | name : chat_format 4 | version : 0.1.0 5 | authors : [ Naja Melan ] 6 | edition : '2018' 7 | description: An example for using websockets in rust WASM. 8 | repository : https::/github.com/najamelan/ws_stream 9 | 10 | 11 | dependencies: 12 | 13 | serde : { version: ^1, features: [ derive ] } 14 | -------------------------------------------------------------------------------- /examples/chat_format/src/lib.rs: -------------------------------------------------------------------------------- 1 | use serde::{ Serialize, Deserialize }; 2 | 3 | 4 | /// Wire format for communication between the server and clients 5 | // 6 | #[ derive( Debug, Clone, PartialEq, Eq, Serialize, Deserialize ) ] 7 | // 8 | pub enum ClientMsg 9 | { 10 | ChatMsg( String ), 11 | SetNick( String ), 12 | Join ( String ), 13 | } 14 | 15 | 16 | /// Wire format for communication between the server and clients 17 | /// The time is in secods since epoch UTC 18 | // 19 | #[ derive( Debug, Clone, PartialEq, Eq, Serialize, Deserialize ) ] 20 | // 21 | pub enum ServerMsg 22 | { 23 | JoinSuccess , 24 | ServerMsg { time: i64, txt : String } , 25 | ChatMsg { time: i64, nick : String, sid: usize, txt: String } , 26 | UserJoined { time: i64, nick : String, sid: usize } , 27 | UserLeft { time: i64, nick : String, sid: usize } , 28 | NickChanged { time: i64, old : String, sid: usize, new: String } , 29 | NickUnchanged { time: i64, nick : String, sid: usize } , 30 | NickInUse { time: i64, nick : String, sid: usize } , 31 | NickInvalid { time: i64, nick : String, sid: usize } , 32 | Welcome { time: i64, users: Vec<(usize,String)>, txt: String } , 33 | } 34 | 35 | -------------------------------------------------------------------------------- /examples/chat_server/Cargo.toml: -------------------------------------------------------------------------------- 1 | # Auto-generated from "Cargo.yml" 2 | [dependencies] 3 | chrono = "^0.4" 4 | flexi_logger = "^0.14" 5 | futures_cbor_codec = "^0.1" 6 | futures_codec = "^0.2" 7 | log = "^0.4" 8 | once_cell = "^1.0.0-pre" 9 | pin-utils = "^0.1.0-alpha" 10 | regex = "^1" 11 | 12 | [dependencies.chat_format] 13 | path = "../chat_format" 14 | 15 | [dependencies.futures-preview] 16 | features = ["async-await", "nightly"] 17 | version = "^0.3.0-alpha" 18 | 19 | [dependencies.locks] 20 | package = "future-parking_lot" 21 | version = "^0.3.2-alpha" 22 | 23 | [dependencies.warp] 24 | default-features = false 25 | features = ["websocket"] 26 | version = "^0.1" 27 | 28 | [dependencies.ws_stream] 29 | path = "../../../ws_stream" 30 | 31 | [package] 32 | authors = ["Naja Melan "] 33 | description = "An example for using websockets in rust WASM." 34 | edition = "2018" 35 | name = "chat_server" 36 | repository = "https::/github.com/najamelan/ws_stream" 37 | version = "0.1.0" 38 | 39 | [patch] 40 | [patch.crates-io] 41 | [patch.crates-io.futures_codec] 42 | git = "https://github.com/matthunz/futures-codec.git" 43 | -------------------------------------------------------------------------------- /examples/chat_server/Cargo.yml: -------------------------------------------------------------------------------- 1 | package: 2 | 3 | name : chat_server 4 | version : 0.1.0 5 | authors : [ Naja Melan ] 6 | edition : '2018' 7 | description: An example for using websockets in rust WASM. 8 | repository : https::/github.com/najamelan/ws_stream 9 | 10 | 11 | 12 | dependencies: 13 | 14 | chat_format : { path: "../chat_format" } 15 | chrono : ^0.4 16 | flexi_logger : ^0.14 17 | futures-preview : { version: ^0.3.0-alpha, features: [ async-await, nightly ] } 18 | futures_cbor_codec: ^0.1 19 | futures_codec : ^0.2 20 | locks : { version: ^0.3.2-alpha, package: future-parking_lot } 21 | log : ^0.4 22 | once_cell : ^1.0.0-pre 23 | pin-utils : ^0.1.0-alpha 24 | regex : ^1 25 | warp : { version: ^0.1, default-features: false, features: [ websocket] } 26 | ws_stream : { path: ../../../ws_stream } 27 | 28 | patch: 29 | 30 | crates-io: 31 | 32 | futures_codec : { git: "https://github.com/matthunz/futures-codec.git" } 33 | -------------------------------------------------------------------------------- /examples/chat_server/src/error.rs: -------------------------------------------------------------------------------- 1 | use crate::{ import::* }; 2 | 3 | /// The error type for errors happening in `ws_stream_wasm`. 4 | /// 5 | /// Use [`ChatErr::kind()`] to know which kind of error happened. 6 | // 7 | #[ derive( Debug ) ] 8 | // 9 | pub struct ChatErr 10 | { 11 | inner: FailContext, 12 | } 13 | 14 | 15 | 16 | /// The different kind of errors that can happen when you use the `ws_stream_wasm` API. 17 | // 18 | #[ derive( Clone, PartialEq, Eq, Debug, Fail ) ] 19 | // 20 | pub enum ChatErrKind 21 | { 22 | /// Invalid nick 23 | /// 24 | #[ fail( display = "The nick you specify must be between 1 and 15 word characters, was invalid: '{}'.", _0 ) ] 25 | // 26 | NickInvalid( String ), 27 | 28 | /// Nick in use 29 | /// 30 | #[ fail( display = "The nick you chose is already in use: '{}'.", _0 ) ] 31 | // 32 | NickInUse( String ), 33 | 34 | /// Nick in use 35 | /// 36 | #[ fail( display = "The new nick you chose is the same as your old one: '{}'.", _0 ) ] 37 | // 38 | NickUnchanged( String ), 39 | } 40 | 41 | 42 | 43 | impl Fail for ChatErr 44 | { 45 | fn cause( &self ) -> Option< &dyn Fail > 46 | { 47 | self.inner.cause() 48 | } 49 | 50 | fn backtrace( &self ) -> Option< &Backtrace > 51 | { 52 | self.inner.backtrace() 53 | } 54 | } 55 | 56 | 57 | 58 | impl fmt::Display for ChatErr 59 | { 60 | fn fmt( &self, f: &mut fmt::Formatter<'_> ) -> fmt::Result 61 | { 62 | fmt::Display::fmt( &self.inner, f ) 63 | } 64 | } 65 | 66 | 67 | impl ChatErr 68 | { 69 | /// Allows matching on the error kind 70 | // 71 | pub fn kind( &self ) -> &ChatErrKind 72 | { 73 | self.inner.get_context() 74 | } 75 | } 76 | 77 | impl From for ChatErr 78 | { 79 | fn from( kind: ChatErrKind ) -> ChatErr 80 | { 81 | ChatErr { inner: FailContext::new( kind ) } 82 | } 83 | } 84 | 85 | impl From< FailContext > for ChatErr 86 | { 87 | fn from( inner: FailContext ) -> ChatErr 88 | { 89 | ChatErr { inner } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /examples/chat_server/src/main.rs: -------------------------------------------------------------------------------- 1 | //! This is an echo server that returns all incoming bytes, without framing. It is used for the tests in 2 | //! ws_stream_wasm. 3 | // 4 | #![ feature( async_closure ) ] 5 | 6 | // mod error; 7 | // use error::*; 8 | 9 | use 10 | { 11 | chat_format :: { ClientMsg, ServerMsg } , 12 | futures_cbor_codec :: { Codec } , 13 | chrono :: { Utc } , 14 | log :: { * } , 15 | regex :: { Regex } , 16 | ws_stream :: { * } , 17 | std :: { env, collections::HashMap, net::SocketAddr } , 18 | std::sync :: { Arc, atomic::{ AtomicUsize, Ordering } } , 19 | locks::rwlock :: { RwLock, read::FutureReadable, write::FutureWriteable } , 20 | futures_codec :: { Framed } , 21 | futures :: { select, sink::SinkExt, future::{ FutureExt, TryFutureExt } } , 22 | warp :: { Filter } , 23 | once_cell :: { sync::OnceCell } , 24 | pin_utils :: { pin_mut } , 25 | 26 | futures :: 27 | { 28 | StreamExt , 29 | channel::mpsc::{ unbounded, UnboundedSender } , 30 | }, 31 | }; 32 | 33 | 34 | type ConnMap = HashMap; 35 | 36 | 37 | struct Connection 38 | { 39 | nick : Arc> , 40 | sid : usize , 41 | tx : UnboundedSender , 42 | } 43 | 44 | 45 | static WELCOME : &str = "Welcome to the ws_stream Chat Server!" ; 46 | static CONNS : OnceCell> = OnceCell::new(); 47 | static CLIENT : AtomicUsize = AtomicUsize::new(0); 48 | 49 | 50 | fn main() 51 | { 52 | flexi_logger::Logger::with_str( "server=trace, ws_stream=error, tokio=warn" ).start().unwrap(); 53 | 54 | 55 | // This should be unfallible, so we ignore the result 56 | // 57 | let _ = CONNS.set( RwLock::new( HashMap::new() ) ); 58 | 59 | 60 | // GET /chat -> websocket upgrade 61 | // 62 | let chat = warp::path( "chat" ) 63 | 64 | // The `ws2()` filter will prepare Websocket handshake... 65 | // 66 | .and( warp::ws2() ) 67 | 68 | .and( warp::addr::remote() ) 69 | 70 | .map( |ws: warp::ws::Ws2, peer_addr: Option| 71 | { 72 | // This will call our function if the handshake succeeds. 73 | // 74 | ws.on_upgrade( move |socket| 75 | 76 | handle_conn( WarpWebSocket::new(socket), peer_addr.expect( "need peer_addr" )).boxed().compat() 77 | ) 78 | }) 79 | ; 80 | 81 | // GET / -> index html 82 | // 83 | let index = warp::path::end() .and( warp::fs::file( "../chat_client/index.html" ) ); 84 | let style = warp::path( "style.css" ).and( warp::fs::file( "../chat_client/style.css" ) ); 85 | let statics = warp::path( "pkg" ) .and( warp::fs::dir ( "../chat_client/pkg" ) ); 86 | 87 | let routes = index.or( style ).or( chat ).or( statics ); 88 | 89 | 90 | let addr: SocketAddr = env::args().nth(1).unwrap_or( "127.0.0.1:3412".to_string() ).parse().expect( "valid addr" ); 91 | println!( "server task listening at: {}", &addr ); 92 | 93 | warp::serve( routes ).run( addr ); 94 | } 95 | 96 | 97 | 98 | // Runs once for each incoming connection, ends when the stream closes or sending causes an 99 | // error. 100 | // 101 | async fn handle_conn( socket: WarpWebSocket, peer_addr: SocketAddr ) -> Result<(), ()> 102 | { 103 | let ws_stream = WsMeta::new( socket ); 104 | 105 | info!( "Incoming connection from: {:?}", peer_addr ); 106 | 107 | let (mut tx, rx) = unbounded(); 108 | let framed = Framed::new( ws_stream, Codec::new() ); 109 | let (out, mut msgs) = framed.split(); 110 | let nick = Arc::new( RwLock::new( peer_addr.to_string() ) ); 111 | 112 | 113 | // A unique sender id for this client 114 | // 115 | let sid: usize = CLIENT.fetch_add( 1, Ordering::Relaxed ); 116 | let mut joined = false; 117 | 118 | let nick2 = nick.clone(); 119 | 120 | // Listen to the channel for this connection and sends out each message that 121 | // arrives on the channel. 122 | // 123 | let outgoing = async move 124 | { 125 | // ignore result, just log 126 | // 127 | let _ = 128 | 129 | rx 130 | .map ( |res| Ok( res ) ) 131 | .forward ( out ) 132 | .await 133 | .map_err ( |e| error!( "{}", e ) ) 134 | ; 135 | }; 136 | 137 | 138 | 139 | let incoming = async move 140 | { 141 | // Incoming messages. Ends when stream returns None or an error. 142 | // 143 | while let Some( msg ) = msgs.next().await 144 | { 145 | debug!( "received client message" ); 146 | 147 | // TODO: handle io errors 148 | // 149 | let msg = match msg 150 | { 151 | Ok( msg ) => msg, 152 | _ => continue, 153 | }; 154 | 155 | let time = Utc::now().timestamp(); 156 | 157 | 158 | match msg 159 | { 160 | ClientMsg::Join( new_nick ) => 161 | { 162 | debug!( "received Join" ); 163 | 164 | // Clients should only join once. Since nothing bad can happen here, 165 | // we just ignore this message 166 | // 167 | if joined { continue; } 168 | 169 | let res = { validate_nick( sid, &nick.future_read().await.clone(), &new_nick ) }; 170 | 171 | match res 172 | { 173 | Ok(_) => 174 | { 175 | joined = true; 176 | *nick.future_write().await = new_nick.clone(); 177 | 178 | // Let all clients know there is a new kid on the block 179 | // 180 | broadcast( &ServerMsg::UserJoined { time: Utc::now().timestamp(), nick: new_nick, sid } ); 181 | 182 | // Welcome message 183 | // 184 | trace!( "sending welcome message" ); 185 | 186 | 187 | let all_users = 188 | { 189 | let mut conns = CONNS.get().unwrap().future_write().await; 190 | 191 | conns.insert 192 | ( 193 | peer_addr, 194 | Connection { tx: tx.clone(), nick: nick.clone(), sid }, 195 | ); 196 | 197 | conns.values().map( |c| (c.sid, c.nick.read().clone() )).collect() 198 | 199 | }; 200 | 201 | let welcome = ServerMsg::Welcome 202 | { 203 | time : Utc::now().timestamp() , 204 | txt : WELCOME.to_string() , 205 | users: all_users , 206 | }; 207 | 208 | send( sid, ServerMsg::JoinSuccess ); 209 | send( sid, welcome ); 210 | } 211 | 212 | Err( m ) => 213 | { 214 | tx.send( m ).await.expect( "send on channel" ); 215 | } 216 | } 217 | 218 | } 219 | 220 | 221 | ClientMsg::SetNick( new_nick ) => 222 | { 223 | debug!( "received SetNick" ); 224 | 225 | 226 | let res = validate_nick( sid, &nick.read(), &new_nick ); 227 | 228 | debug!( "validated new Nick" ); 229 | 230 | match res 231 | { 232 | Ok ( m ) => 233 | { 234 | broadcast( &m ); 235 | *nick.write() = new_nick; 236 | } 237 | 238 | Err( m ) => send( sid, m ), 239 | } 240 | } 241 | 242 | 243 | ClientMsg::ChatMsg( txt ) => 244 | { 245 | debug!( "received ChatMsg" ); 246 | 247 | broadcast( &ServerMsg::ChatMsg { time, nick: nick.future_read().await.clone(), sid, txt } ); 248 | } 249 | } 250 | }; 251 | 252 | debug!( "Broke from msgs loop" ); 253 | }; 254 | 255 | pin_mut!( incoming ); 256 | pin_mut!( outgoing ); 257 | 258 | select! 259 | { 260 | _ = incoming.fuse() => {} 261 | _ = outgoing.fuse() => {} 262 | }; 263 | 264 | 265 | // remove the client and let other clients know this client disconnected 266 | // 267 | let user = { CONNS.get().unwrap().future_write().await.remove( &peer_addr ) }; 268 | 269 | if user.is_some() 270 | { 271 | // let other clients know this client disconnected 272 | // 273 | broadcast( &ServerMsg::UserLeft { time: Utc::now().timestamp(), nick: nick2.future_read().await.clone(), sid } ); 274 | 275 | 276 | debug!( "Client disconnected: {}", peer_addr ); 277 | } 278 | 279 | 280 | info!( "End of connection from: {:?}", peer_addr ); 281 | 282 | Ok(()) 283 | 284 | } 285 | 286 | 287 | 288 | // Send a server message to all connected clients 289 | // 290 | fn broadcast( msg: &ServerMsg ) 291 | { 292 | debug!( "start broadcast" ); 293 | 294 | for client in CONNS.get().unwrap().read().values().map( |c| &c.tx ) 295 | { 296 | client.unbounded_send( msg.clone() ).expect( "send on unbounded" ); 297 | }; 298 | 299 | debug!( "finish broadcast" ); 300 | } 301 | 302 | 303 | 304 | // Send a server message to all connected clients 305 | // 306 | fn send( sid: usize, msg: ServerMsg ) 307 | { 308 | debug!( "start send" ); 309 | 310 | for client in CONNS.get().unwrap().read().values().filter( |c| c.sid == sid ).map( |c| &c.tx ) 311 | { 312 | client.unbounded_send( msg.clone() ).expect( "send on unbounded" ); 313 | }; 314 | 315 | debug!( "finish send" ); 316 | } 317 | 318 | 319 | 320 | // Send a server message to all connected clients 321 | // 322 | fn validate_nick( sid: usize, old: &str, new: &str ) -> Result 323 | { 324 | // Check whether it's unchanged 325 | // 326 | if old == new 327 | { 328 | return Err( ServerMsg::NickUnchanged{ time: Utc::now().timestamp(), sid, nick: old.to_string() } ); 329 | } 330 | 331 | 332 | // Check whether it's not already in use 333 | // 334 | if CONNS.get().unwrap().read().values().any( |c| *c.nick.read() == new ) 335 | { 336 | return Err( ServerMsg::NickInUse{ time: Utc::now().timestamp(), sid, nick: new.to_string() } ) 337 | } 338 | 339 | 340 | // Check whether it's valid 341 | // 342 | let nickre = Regex::new( r"^\w{1,15}$" ).unwrap(); 343 | 344 | if !nickre.is_match( new ) 345 | { 346 | error!( "Wrong nick: '{}'", new ); 347 | return Err( ServerMsg::NickInvalid{ time: Utc::now().timestamp(), sid, nick: new.to_string() } ) 348 | } 349 | 350 | 351 | // It's valid 352 | // 353 | Ok( ServerMsg::NickChanged{ time: Utc::now().timestamp(), old: old.to_string(), new: new.to_string(), sid } ) 354 | } 355 | 356 | 357 | 358 | // fn random_id() -> String 359 | // { 360 | // thread_rng() 361 | // .sample_iter( &Alphanumeric ) 362 | // .take(8) 363 | // .collect() 364 | // } 365 | 366 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | //! Crate specific errors. 2 | // 3 | use crate::{ import::*, CloseEvent }; 4 | 5 | 6 | /// The error type for errors happening in `ws_stream_wasm`. 7 | /// 8 | // 9 | #[ derive( Debug, Error, Clone, PartialEq, Eq ) ] #[ non_exhaustive ] 10 | // 11 | pub enum WsErr 12 | { 13 | /// Invalid input to [WsState::try_from( u16 )](crate::WsState). 14 | // 15 | #[ error( "Invalid input to conversion to WsReadyState: {supplied}" ) ] 16 | // 17 | InvalidWsState 18 | { 19 | /// The user supplied value that is invalid. 20 | // 21 | supplied: u16 22 | }, 23 | 24 | /// When trying to send and [WsState](crate::WsState) is anything but [WsState::Open](crate::WsState::Open) this error is returned. 25 | // 26 | #[ error( "The connection state is not \"Open\"." ) ] 27 | // 28 | ConnectionNotOpen, 29 | 30 | /// An invalid URL was given to [WsMeta::connect](crate::WsMeta::connect), please see: 31 | /// [HTML Living Standard](https://html.spec.whatwg.org/multipage/web-sockets.html#dom-websocket). 32 | // 33 | #[ error( "An invalid URL was given to the connect method: {supplied}" ) ] 34 | // 35 | InvalidUrl 36 | { 37 | /// The user supplied value that is invalid. 38 | // 39 | supplied: String 40 | }, 41 | 42 | /// An invalid close code was given to a close method. For valid close codes, please see: 43 | /// [MDN Documentation](https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent#Status_codes). 44 | // 45 | #[ error( "An invalid close code was given to a close method: {supplied}" ) ] 46 | // 47 | InvalidCloseCode 48 | { 49 | /// The user supplied value that is invalid. 50 | // 51 | supplied: u16 52 | }, 53 | 54 | 55 | /// The reason string given to a close method is longer than 123 bytes, please see: 56 | /// [MDN Documentation](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close). 57 | // 58 | #[ error( "The reason string given to a close method is to long." ) ] 59 | // 60 | ReasonStringToLong, 61 | 62 | 63 | /// Failed to connect to the server. 64 | // 65 | #[ error( "Failed to connect to the server. CloseEvent: {event:?}" ) ] 66 | // 67 | ConnectionFailed 68 | { 69 | /// The close event that might hold extra code and reason information. 70 | // 71 | event: CloseEvent 72 | }, 73 | 74 | 75 | /// When converting the JavaScript Message into a WsMessage, it's possible that 76 | /// a String message doesn't convert correctly as Js does not guarantee that 77 | /// strings are valid Unicode. Happens in `impl TryFrom< MessageEvent > for WsMessage`. 78 | // 79 | #[ error( "Received a String message that couldn't be decoded to valid UTF-8" ) ] 80 | // 81 | InvalidEncoding, 82 | 83 | 84 | /// When converting the JavaScript Message into a WsMessage, it's not possible to 85 | /// convert Blob type messages, as Blob is a streaming type, that needs to be read 86 | /// asynchronously. If you are using the type without setting up the connection with 87 | /// [`WsMeta::connect`](crate::WsMeta::connect), you have to make sure to set the binary 88 | /// type of the connection to `ArrayBuffer`. 89 | /// 90 | /// Happens in `impl TryFrom< MessageEvent > for WsMessage`. 91 | // 92 | #[ error( "Received a Blob message that couldn't converted." ) ] 93 | // 94 | CantDecodeBlob, 95 | 96 | 97 | /// When converting the JavaScript Message into a WsMessage, the data type was neither 98 | /// `Arraybuffer`, `String` nor `Blob`. This should never happen. If it does, please 99 | /// try to make a reproducible example and file an issue. 100 | /// 101 | /// Happens in `impl TryFrom< MessageEvent > for WsMessage`. 102 | // 103 | #[ error( "Received a message that is neither ArrayBuffer, String or Blob." ) ] 104 | // 105 | UnknownDataType, 106 | 107 | /// Fallback for unknown errors. 108 | #[error("`{0}`")] 109 | // 110 | Unknown(String), 111 | } 112 | 113 | 114 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr( nightly, feature(doc_cfg) )] 2 | #![cfg_attr( nightly, cfg_attr( nightly, doc = include_str!("../README.md") ))] 3 | #![doc = ""] // empty doc line to handle missing doc warning when the feature is missing. 4 | 5 | #![ doc ( html_root_url = "https://docs.rs/ws_stream_wasm" ) ] 6 | #![ forbid ( unsafe_code ) ] 7 | #![ allow ( clippy::suspicious_else_formatting, clippy::needless_return ) ] 8 | 9 | 10 | #![ warn 11 | ( 12 | missing_debug_implementations , 13 | missing_docs , 14 | nonstandard_style , 15 | rust_2018_idioms , 16 | trivial_casts , 17 | trivial_numeric_casts , 18 | unused_extern_crates , 19 | unused_qualifications , 20 | single_use_lifetimes , 21 | unreachable_pub , 22 | variant_size_differences , 23 | )] 24 | 25 | 26 | 27 | mod error ; 28 | mod ws_event ; 29 | mod ws_message ; 30 | mod ws_meta ; 31 | mod ws_state ; 32 | mod ws_stream ; 33 | mod ws_stream_io ; 34 | 35 | pub use 36 | { 37 | error :: { WsErr } , 38 | ws_event :: { WsEvent, CloseEvent } , 39 | ws_message :: { WsMessage } , 40 | ws_meta :: { WsMeta } , 41 | ws_state :: { WsState } , 42 | ws_stream :: { WsStream } , 43 | ws_stream_io :: { WsStreamIo } , 44 | }; 45 | 46 | 47 | 48 | mod import 49 | { 50 | pub(crate) use 51 | { 52 | futures :: { prelude::{ Stream, Sink }, ready, StreamExt, FutureExt } , 53 | std :: { io, collections::VecDeque, fmt, task::{ Context, Waker, Poll }, future::Future } , 54 | std :: { rc::Rc, cell::{ RefCell }, pin::Pin, convert::{ TryFrom, TryInto } } , 55 | js_sys :: { ArrayBuffer, Uint8Array } , 56 | wasm_bindgen :: { closure::Closure, JsCast, JsValue, UnwrapThrowExt } , 57 | web_sys :: { *, BinaryType, Blob, WebSocket, CloseEvent as JsCloseEvt, DomException } , 58 | js_sys :: { Array } , 59 | pharos :: { SharedPharos, PharErr, Observable, Observe, Filter, ObserveConfig, } , 60 | wasm_bindgen_futures :: { spawn_local } , 61 | async_io_stream :: { IoStream } , 62 | thiserror :: { Error } , 63 | send_wrapper :: { SendWrapper } , 64 | }; 65 | } 66 | 67 | 68 | use import::*; 69 | 70 | /// Helper function to reduce code bloat 71 | // 72 | pub(crate) fn notify( pharos: SharedPharos, evt: WsEvent ) 73 | { 74 | let notify = async move 75 | { 76 | pharos.notify( evt ).await 77 | 78 | .map_err( |e| unreachable!( "{:?}", e ) ).unwrap(); // only happens if we closed it. 79 | }; 80 | 81 | spawn_local( notify ); 82 | } 83 | -------------------------------------------------------------------------------- /src/ws_event.rs: -------------------------------------------------------------------------------- 1 | use crate::{ import::*, WsErr }; 2 | 3 | 4 | /// Events related to the WebSocket. You can filter like: 5 | /// 6 | /// ``` 7 | /// use 8 | ///{ 9 | /// ws_stream_wasm :: * , 10 | /// pharos :: * , 11 | /// wasm_bindgen :: UnwrapThrowExt , 12 | /// wasm_bindgen_futures :: spawn_local , 13 | /// futures :: stream::StreamExt , 14 | ///}; 15 | /// 16 | ///let program = async 17 | ///{ 18 | /// let (mut ws, _wsio) = WsMeta::connect( "127.0.0.1:3012", None ).await 19 | /// 20 | /// .expect_throw( "assume the connection succeeds" ); 21 | /// 22 | /// // The Filter type comes from the pharos crate. 23 | /// // 24 | /// let mut evts = ws.observe( Filter::Pointer( WsEvent::is_closed ).into() ) 25 | /// .await 26 | /// .expect("observe ws_meta") 27 | /// ; 28 | /// 29 | /// ws.close().await; 30 | /// 31 | /// // Note we will only get the closed event here, the WsEvent::Closing has been filtered out. 32 | /// // 33 | /// assert!( evts.next().await.unwrap_throw().is_closed () ); 34 | ///}; 35 | /// 36 | ///spawn_local( program ); 37 | ///``` 38 | // 39 | #[ derive( Clone, Debug, PartialEq, Eq ) ] 40 | // 41 | pub enum WsEvent 42 | { 43 | /// The connection is now Open and ready for use. 44 | // 45 | Open, 46 | 47 | /// An error happened on the connection. For more information about when this event 48 | /// occurs, see the [HTML Living Standard](https://html.spec.whatwg.org/multipage/web-sockets.html). 49 | /// Since the browser is not allowed to convey any information to the client code as to why an error 50 | /// happened (for security reasons), as described in the HTML specification, there usually is no extra 51 | /// information available. That's why this event has no data attached to it. 52 | // 53 | Error, 54 | 55 | /// The connection has started closing, but is not closed yet. You shouldn't try to send messages over 56 | /// it anymore. Trying to do so will result in an error. 57 | // 58 | Closing, 59 | 60 | /// The connection was closed. The enclosed [`CloseEvent`] has some extra information. 61 | // 62 | Closed( CloseEvent ), 63 | 64 | /// An error happened, not on the connection, but inside _ws_stream_wasm_. This currently happens 65 | /// when an incoming message can not be converted to Rust types, eg. a String message with invalid 66 | /// encoding. 67 | // 68 | WsErr( WsErr ) 69 | } 70 | 71 | 72 | impl WsEvent 73 | { 74 | /// Predicate indicating whether this is a [WsEvent::Open] event. Can be used as a filter for the 75 | /// event stream obtained with [`pharos::Observable::observe`] on [`WsMeta`](crate::WsMeta). 76 | // 77 | pub fn is_open( &self ) -> bool 78 | { 79 | matches!( self, Self::Open ) 80 | } 81 | 82 | /// Predicate indicating whether this is a [WsEvent::Error] event. Can be used as a filter for the 83 | /// event stream obtained with [`pharos::Observable::observe`] on [`WsMeta`](crate::WsMeta). 84 | // 85 | pub fn is_err( &self ) -> bool 86 | { 87 | matches!( self, Self::Error ) 88 | } 89 | 90 | /// Predicate indicating whether this is a [WsEvent::Closing] event. Can be used as a filter for the 91 | /// event stream obtained with [`pharos::Observable::observe`] on [`WsMeta`](crate::WsMeta). 92 | // 93 | pub fn is_closing( &self ) -> bool 94 | { 95 | matches!( self, Self::Closing ) 96 | } 97 | 98 | /// Predicate indicating whether this is a [WsEvent::Closed] event. Can be used as a filter for the 99 | /// event stream obtained with [`pharos::Observable::observe`] on [`WsMeta`](crate::WsMeta). 100 | // 101 | pub fn is_closed( &self ) -> bool 102 | { 103 | matches!( self, Self::Closed(_) ) 104 | } 105 | 106 | /// Predicate indicating whether this is a [WsEvent::WsErr] event. Can be used as a filter for the 107 | /// event stream obtained with [`pharos::Observable::observe`] on [`WsMeta`](crate::WsMeta). 108 | // 109 | pub fn is_ws_err( &self ) -> bool 110 | { 111 | matches!( self, Self::WsErr(_) ) 112 | } 113 | } 114 | 115 | 116 | 117 | /// An event holding information about how/why the connection was closed. 118 | /// 119 | // We use this wrapper because the web_sys version isn't Send and pharos requires events 120 | // to be Send. 121 | // 122 | #[ derive( Clone, Debug, PartialEq, Eq ) ] 123 | // 124 | pub struct CloseEvent 125 | { 126 | /// The close code. 127 | /// See: [MDN Documentation](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close). 128 | // 129 | pub code: u16, 130 | 131 | /// The reason why the connection was closed. 132 | /// See: [MDN Documentation](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close). 133 | // 134 | pub reason: String, 135 | 136 | /// Whether the connection was closed cleanly. 137 | /// See: [MDN Documentation](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close). 138 | // 139 | pub was_clean: bool, 140 | } 141 | 142 | 143 | impl From for CloseEvent 144 | { 145 | fn from( js_evt: JsCloseEvt ) -> Self 146 | { 147 | Self 148 | { 149 | code : js_evt.code () , 150 | reason : js_evt.reason () , 151 | was_clean: js_evt.was_clean() , 152 | } 153 | } 154 | } 155 | 156 | 157 | 158 | 159 | -------------------------------------------------------------------------------- /src/ws_message.rs: -------------------------------------------------------------------------------- 1 | use crate::{ import::*, WsErr }; 2 | 3 | 4 | /// Represents a WebSocket Message, after converting from JavaScript type. 5 | // 6 | #[ derive( Debug, Clone, PartialEq, Eq, Hash ) ] 7 | // 8 | pub enum WsMessage 9 | { 10 | /// The data of the message is a string. 11 | /// 12 | Text ( String ), 13 | 14 | /// The message contains binary data. 15 | /// 16 | Binary( Vec ), 17 | } 18 | 19 | 20 | 21 | /// This will convert the JavaScript event into a WsMessage. Note that this 22 | /// will only work if the connection is set to use the binary type ArrayBuffer. 23 | /// On binary type Blob, this will panic. 24 | // 25 | impl TryFrom< MessageEvent > for WsMessage 26 | { 27 | type Error = WsErr; 28 | 29 | fn try_from( evt: MessageEvent ) -> Result< Self, Self::Error > 30 | { 31 | match evt.data() 32 | { 33 | d if d.is_instance_of::< ArrayBuffer >() => 34 | { 35 | let buffy = Uint8Array::new( d.unchecked_ref() ); 36 | let mut v = vec![ 0; buffy.length() as usize ]; 37 | 38 | buffy.copy_to( &mut v ); // FIXME: get rid of this copy 39 | 40 | Ok( WsMessage::Binary( v ) ) 41 | } 42 | 43 | 44 | // We don't allow invalid encodings. In principle if needed, 45 | // we could add a variant to WsMessage with a CString or an OsString 46 | // to allow the user to access this data. However until there is a usecase, 47 | // I'm not inclined, amongst other things because the conversion from Js isn't very 48 | // clear and it would require a bunch of testing for something that's a rather bad 49 | // idea to begin with. If you need data that is not a valid string, use a binary 50 | // message. 51 | // 52 | d if d.is_string() => 53 | { 54 | match d.as_string() 55 | { 56 | Some(text) => Ok ( WsMessage::Text( text ) ), 57 | None => Err( WsErr::InvalidEncoding ), 58 | } 59 | } 60 | 61 | 62 | // We have set the binary mode to array buffer (WsMeta::connect), so normally this shouldn't happen. 63 | // That is as long as this is used within the context of the WsMeta constructor. 64 | // 65 | d if d.is_instance_of::< Blob >() => Err( WsErr::CantDecodeBlob ), 66 | 67 | 68 | // should never happen. 69 | // 70 | _ => Err( WsErr::UnknownDataType ), 71 | } 72 | } 73 | } 74 | 75 | 76 | impl From for Vec 77 | { 78 | fn from( msg: WsMessage ) -> Self 79 | { 80 | match msg 81 | { 82 | WsMessage::Text ( string ) => string.into(), 83 | WsMessage::Binary( vec ) => vec , 84 | } 85 | } 86 | } 87 | 88 | 89 | impl From> for WsMessage 90 | { 91 | fn from( vec: Vec ) -> Self 92 | { 93 | WsMessage::Binary( vec ) 94 | } 95 | } 96 | 97 | 98 | impl From for WsMessage 99 | { 100 | fn from( s: String ) -> Self 101 | { 102 | WsMessage::Text( s ) 103 | } 104 | } 105 | 106 | 107 | impl AsRef<[u8]> for WsMessage 108 | { 109 | fn as_ref( &self ) -> &[u8] 110 | { 111 | match self 112 | { 113 | WsMessage::Text ( string ) => string.as_ref() , 114 | WsMessage::Binary( vec ) => vec .as_ref() , 115 | } 116 | } 117 | } 118 | 119 | 120 | 121 | -------------------------------------------------------------------------------- /src/ws_meta.rs: -------------------------------------------------------------------------------- 1 | use crate::{ import::*, WsErr, WsState, WsStream, WsEvent, CloseEvent, notify }; 2 | 3 | 4 | /// The meta data related to a websocket. Allows access to the methods on the WebSocket API. 5 | /// This is split from the `Stream`/`Sink` so you can pass the latter to a combinator whilst 6 | /// continuing to use this API. 7 | /// 8 | /// A `WsMeta` instance is observable through the [`pharos::Observable`](https://docs.rs/pharos/0.4.3/pharos/trait.Observable.html) 9 | /// trait. The type of event is [WsEvent]. In the case of a Close event, there will be additional information included 10 | /// as a [CloseEvent]. 11 | /// 12 | /// When you drop this, the connection does not get closed, however when you drop [WsStream] it does. 13 | /// 14 | /// Most of the methods on this type directly map to the web API. For more documentation, check the 15 | /// [MDN WebSocket documentation](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/WebSocket). 16 | // 17 | pub struct WsMeta 18 | { 19 | ws : SendWrapper< Rc > , 20 | pharos: SharedPharos , 21 | } 22 | 23 | 24 | 25 | impl WsMeta 26 | { 27 | const OPEN_CLOSE: Filter = Filter::Pointer( |evt: &WsEvent| evt.is_open() | evt.is_closed() ); 28 | 29 | /// Connect to the server. The future will resolve when the connection has been established with a successful WebSocket 30 | /// handshake. 31 | /// 32 | /// This returns both a [WsMeta] (allow manipulating and requesting meta data for the connection) and 33 | /// a [WsStream] (`Stream`/`Sink` over [WsMessage](crate::WsMessage)). [WsStream] can be wrapped to obtain 34 | /// `AsyncRead`/`AsyncWrite`/`AsyncBufRead` with [WsStream::into_io]. 35 | /// 36 | /// ## Errors 37 | /// 38 | /// Browsers will forbid making websocket connections to certain ports. See this [Stack Overflow question](https://stackoverflow.com/questions/4313403/why-do-browsers-block-some-ports/4314070). 39 | /// `connect` will return a [WsErr::ConnectionFailed] as it is indistinguishable from other connection failures. 40 | /// 41 | /// If the URL is invalid, a [WsErr::InvalidUrl] is returned. See the [HTML Living Standard](https://html.spec.whatwg.org/multipage/web-sockets.html#dom-websocket) for more information. 42 | /// 43 | /// When the connection fails (server port not open, wrong ip, wss:// on ws:// server, ... See the [HTML Living Standard](https://html.spec.whatwg.org/multipage/web-sockets.html#dom-websocket) 44 | /// for details on all failure possibilities), a [WsErr::ConnectionFailed] is returned. 45 | /// 46 | /// **Note**: Sending protocols to a server that doesn't support them will make the connection fail. 47 | // 48 | pub async fn connect( url: impl AsRef, protocols: impl Into>> ) 49 | 50 | -> Result< (Self, WsStream), WsErr > 51 | { 52 | let res = match protocols.into() 53 | { 54 | None => WebSocket::new( url.as_ref() ), 55 | 56 | Some(v) => 57 | { 58 | let js_protos = v.iter().fold( Array::new(), |acc, proto| 59 | { 60 | acc.push( &JsValue::from_str( proto ) ); 61 | acc 62 | }); 63 | 64 | WebSocket::new_with_str_sequence( url.as_ref(), &js_protos ) 65 | } 66 | }; 67 | 68 | 69 | // Deal with errors from the WebSocket constructor. 70 | // 71 | let ws = match res 72 | { 73 | Ok(ws) => SendWrapper::new( Rc::new( ws ) ), 74 | 75 | Err(e) => 76 | { 77 | let de: &DomException = e.unchecked_ref(); 78 | 79 | match de.code() 80 | { 81 | DomException::SYNTAX_ERR => 82 | 83 | return Err( WsErr::InvalidUrl{ supplied: url.as_ref().to_string() } ), 84 | 85 | 86 | _ => return Err(WsErr::Unknown(de.message())), 87 | }; 88 | } 89 | }; 90 | 91 | 92 | // Create our pharos. 93 | // 94 | let mut pharos = SharedPharos::default(); 95 | let ph1 = pharos.clone(); 96 | let ph2 = pharos.clone(); 97 | let ph3 = pharos.clone(); 98 | let ph4 = pharos.clone(); 99 | 100 | 101 | // Setup our event listeners 102 | // 103 | #[ allow( trivial_casts ) ] 104 | // 105 | let on_open = Closure::wrap( Box::new( move || 106 | { 107 | // notify observers 108 | // 109 | notify( ph1.clone(), WsEvent::Open ) 110 | 111 | 112 | }) as Box< dyn FnMut() > ); 113 | 114 | 115 | // TODO: is there no information at all in an error? 116 | // 117 | #[ allow( trivial_casts ) ] 118 | // 119 | let on_error = Closure::wrap( Box::new( move || 120 | { 121 | // notify observers. 122 | // 123 | notify( ph2.clone(), WsEvent::Error ) 124 | 125 | }) as Box< dyn FnMut() > ); 126 | 127 | 128 | #[ allow( trivial_casts ) ] 129 | // 130 | let on_close = Closure::wrap( Box::new( move |evt: JsCloseEvt| 131 | { 132 | let c = WsEvent::Closed( CloseEvent 133 | { 134 | code : evt.code() , 135 | reason : evt.reason() , 136 | was_clean: evt.was_clean(), 137 | }); 138 | 139 | notify( ph3.clone(), c ) 140 | 141 | }) as Box< dyn FnMut( JsCloseEvt ) > ); 142 | 143 | 144 | ws.set_onopen ( Some( on_open .as_ref().unchecked_ref() )); 145 | ws.set_onclose( Some( on_close.as_ref().unchecked_ref() )); 146 | ws.set_onerror( Some( on_error.as_ref().unchecked_ref() )); 147 | 148 | // In case of future task cancellation the current task may be interrupted at an await, therefore not reaching 149 | // the `WsStream` construction, whose `Drop` glue would have been responsible for unregistering the callbacks. 150 | // We therefore use a guard to be responsible for unregistering the callbacks until the `WsStream` is 151 | // constructed. 152 | // 153 | let guard = 154 | { 155 | struct Guard<'lt> { ws: &'lt WebSocket } 156 | 157 | impl Drop for Guard<'_> 158 | { 159 | fn drop(&mut self) 160 | { 161 | self.ws.set_onopen(None); 162 | self.ws.set_onclose(None); 163 | self.ws.set_onerror(None); 164 | self.ws.close().unwrap_throw(); // cannot throw without code and reason. 165 | 166 | log::warn!( "WsMeta::connect future was dropped while connecting to: {}.", self.ws.url() ); 167 | } 168 | } 169 | 170 | Guard { ws: &ws } 171 | }; 172 | 173 | // Listen to the events to figure out whether the connection opens successfully. We don't want to deal with 174 | // the error event. Either a close event happens, in which case we want to recover the CloseEvent to return it 175 | // to the user, or an Open event happens in which case we are happy campers. 176 | // 177 | let mut evts = pharos.observe( Self::OPEN_CLOSE.into() ).await 178 | 179 | .expect( "we didn't close pharos" ) 180 | ; 181 | 182 | // If the connection is closed, return error 183 | // 184 | if let Some( WsEvent::Closed(evt) ) = evts.next().await 185 | { 186 | return Err( WsErr::ConnectionFailed{ event: evt } ) 187 | } 188 | 189 | // We have now passed all the `await` points in this function and so the `WsStream` construction is guaranteed 190 | // so we let it take over the responsibility of unregistering the callbacks by disabling our guard. 191 | // 192 | std::mem::forget(guard); 193 | 194 | // We don't handle Blob's 195 | // 196 | ws.set_binary_type( BinaryType::Arraybuffer ); 197 | 198 | 199 | Ok 200 | (( 201 | Self 202 | { 203 | pharos, 204 | ws: ws.clone(), 205 | }, 206 | 207 | WsStream::new 208 | ( 209 | ws, 210 | ph4, 211 | SendWrapper::new( on_open ), 212 | SendWrapper::new( on_error ), 213 | SendWrapper::new( on_close ), 214 | ) 215 | )) 216 | } 217 | 218 | 219 | 220 | /// Close the socket. The future will resolve once the socket's state has become `WsState::CLOSED`. 221 | /// See: [MDN Documentation](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close) 222 | // 223 | pub async fn close( &self ) -> Result< CloseEvent, WsErr > 224 | { 225 | match self.ready_state() 226 | { 227 | WsState::Closed => return Err( WsErr::ConnectionNotOpen ), 228 | WsState::Closing => {} 229 | 230 | _ => 231 | { 232 | // This can not throw normally, because the only errors the API can return is if we use a code or 233 | // a reason string, which we don't. 234 | // See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close#Exceptions_thrown). 235 | // 236 | self.ws.close().unwrap_throw(); 237 | 238 | 239 | // Notify Observers 240 | // 241 | notify( self.pharos.clone(), WsEvent::Closing ) 242 | } 243 | } 244 | 245 | 246 | let mut evts = match self.pharos.observe_shared( Filter::Pointer( WsEvent::is_closed ).into() ).await 247 | { 248 | Ok(events) => events , 249 | Err(e) => unreachable!( "{:?}", e ) , // only happens if we closed it. 250 | }; 251 | 252 | // We promised the user a CloseEvent, so we don't have much choice but to unwrap this. In any case, the stream will 253 | // never end and this will hang if the browser fails to send a close event. 254 | // 255 | let ce = evts.next().await.expect_throw( "receive a close event" ); 256 | 257 | if let WsEvent::Closed(e) = ce { Ok( e ) } 258 | else { unreachable!() } 259 | } 260 | 261 | 262 | 263 | 264 | /// Close the socket. The future will resolve once the socket's state has become `WsState::CLOSED`. 265 | /// See: [MDN Documentation](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close) 266 | // 267 | pub async fn close_code( &self, code: u16 ) -> Result 268 | { 269 | match self.ready_state() 270 | { 271 | WsState::Closed => return Err( WsErr::ConnectionNotOpen ), 272 | WsState::Closing => {} 273 | 274 | _ => 275 | { 276 | match self.ws.close_with_code( code ) 277 | { 278 | // Notify Observers 279 | // 280 | Ok(_) => notify( self.pharos.clone(), WsEvent::Closing ), 281 | 282 | 283 | Err(_) => 284 | { 285 | return Err( WsErr::InvalidCloseCode{ supplied: code } ); 286 | } 287 | } 288 | } 289 | } 290 | 291 | 292 | let mut evts = match self.pharos.observe_shared( Filter::Pointer( WsEvent::is_closed ).into() ).await 293 | { 294 | Ok(events) => events , 295 | Err(e) => unreachable!( "{:?}", e ) , // only happens if we closed it. 296 | }; 297 | 298 | let ce = evts.next().await.expect_throw( "receive a close event" ); 299 | 300 | if let WsEvent::Closed(e) = ce { Ok(e) } 301 | else { unreachable!() } 302 | } 303 | 304 | 305 | 306 | /// Close the socket. The future will resolve once the socket's state has become `WsState::CLOSED`. 307 | /// See: [MDN Documentation](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close) 308 | // 309 | pub async fn close_reason( &self, code: u16, reason: impl AsRef ) -> Result 310 | { 311 | match self.ready_state() 312 | { 313 | WsState::Closed => return Err( WsErr::ConnectionNotOpen ), 314 | WsState::Closing => {} 315 | 316 | _ => 317 | { 318 | if reason.as_ref().len() > 123 319 | { 320 | return Err( WsErr::ReasonStringToLong ); 321 | } 322 | 323 | 324 | match self.ws.close_with_code_and_reason( code, reason.as_ref() ) 325 | { 326 | // Notify Observers 327 | // 328 | Ok(_) => notify( self.pharos.clone(), WsEvent::Closing ), 329 | 330 | 331 | Err(_) => 332 | { 333 | return Err( WsErr::InvalidCloseCode{ supplied: code } ) 334 | } 335 | } 336 | } 337 | } 338 | 339 | let mut evts = match self.pharos.observe_shared( Filter::Pointer( WsEvent::is_closed ).into() ).await 340 | { 341 | Ok(events) => events , 342 | Err(e) => unreachable!( "{:?}", e ) , // only happens if we closed it. 343 | }; 344 | 345 | let ce = evts.next().await.expect_throw( "receive a close event" ); 346 | 347 | if let WsEvent::Closed(e) = ce { Ok(e) } 348 | else { unreachable!() } 349 | } 350 | 351 | 352 | 353 | /// Verify the [WsState] of the connection. 354 | // 355 | pub fn ready_state( &self ) -> WsState 356 | { 357 | self.ws.ready_state().try_into() 358 | 359 | // This can't throw unless the browser gives us an invalid ready state. 360 | // 361 | .expect_throw( "Convert ready state from browser API" ) 362 | } 363 | 364 | 365 | /// Access the wrapped [web_sys::WebSocket](https://docs.rs/web-sys/0.3.25/web_sys/struct.WebSocket.html) directly. 366 | /// 367 | /// _ws_stream_wasm_ tries to expose all useful functionality through an idiomatic rust API, so hopefully 368 | /// you won't need this, however if I missed something, you can. 369 | /// 370 | /// ## Caveats 371 | /// If you call `set_onopen`, `set_onerror`, `set_onmessage` or `set_onclose` on this, you will overwrite 372 | /// the event listeners from `ws_stream_wasm`, and things will break. 373 | // 374 | pub fn wrapped( &self ) -> &WebSocket 375 | { 376 | &self.ws 377 | } 378 | 379 | 380 | /// The number of bytes of data that have been queued but not yet transmitted to the network. 381 | /// 382 | /// **NOTE:** that this is the number of bytes buffered by the underlying platform WebSocket 383 | /// implementation. It does not reflect any buffering performed by _ws_stream_wasm_. 384 | // 385 | pub fn buffered_amount( &self ) -> u32 386 | { 387 | self.ws.buffered_amount() 388 | } 389 | 390 | 391 | /// The extensions selected by the server as negotiated during the connection. 392 | /// 393 | /// **NOTE**: This is an untested feature. The back-end server we use for testing (_tungstenite_) 394 | /// does not support Extensions. 395 | // 396 | pub fn extensions( &self ) -> String 397 | { 398 | self.ws.extensions() 399 | } 400 | 401 | 402 | /// The name of the sub-protocol the server selected during the connection. 403 | /// 404 | /// This will be one of the strings specified in the protocols parameter when 405 | /// creating this WsMeta instance. 406 | // 407 | pub fn protocol(&self) -> String 408 | { 409 | self.ws.protocol() 410 | } 411 | 412 | 413 | /// Retrieve the address to which this socket is connected. 414 | // 415 | pub fn url( &self ) -> String 416 | { 417 | self.ws.url() 418 | } 419 | } 420 | 421 | 422 | 423 | impl fmt::Debug for WsMeta 424 | { 425 | fn fmt( &self, f: &mut fmt::Formatter<'_> ) -> fmt::Result 426 | { 427 | write!( f, "WsMeta for connection: {}", self.url() ) 428 | } 429 | } 430 | 431 | 432 | 433 | impl Observable for WsMeta 434 | { 435 | type Error = PharErr; 436 | 437 | fn observe( &mut self, options: ObserveConfig ) -> Observe< '_, WsEvent, Self::Error > 438 | { 439 | self.pharos.observe( options ) 440 | } 441 | } 442 | 443 | 444 | -------------------------------------------------------------------------------- /src/ws_state.rs: -------------------------------------------------------------------------------- 1 | use crate :: { import::*, WsErr }; 2 | 3 | 4 | /// Indicates the state of a Websocket connection. The only state in which it's valid to send and receive messages 5 | /// is [WsState::Open]. 6 | /// 7 | /// See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState) for the ready state values. 8 | // 9 | #[ allow( missing_docs ) ] 10 | // 11 | #[ derive( Debug, Clone, Copy, PartialEq, Eq ) ] 12 | // 13 | pub enum WsState 14 | { 15 | Connecting, 16 | Open , 17 | Closing , 18 | Closed , 19 | } 20 | 21 | 22 | /// Internally ready state is a u16, so it's possible to create one from a u16. Only 0-3 are valid values. 23 | /// 24 | /// See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState) for the ready state values. 25 | // 26 | impl TryFrom for WsState 27 | { 28 | type Error = WsErr; 29 | 30 | fn try_from( state: u16 ) -> Result< Self, Self::Error > 31 | { 32 | match state 33 | { 34 | WebSocket::CONNECTING => Ok ( WsState::Connecting ) , 35 | WebSocket::OPEN => Ok ( WsState::Open ) , 36 | WebSocket::CLOSING => Ok ( WsState::Closing ) , 37 | WebSocket::CLOSED => Ok ( WsState::Closed ) , 38 | 39 | _ => Err( WsErr::InvalidWsState{ supplied: state } ) , 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/ws_stream.rs: -------------------------------------------------------------------------------- 1 | use crate::{ import::*, * }; 2 | 3 | 4 | /// A futures 0.3 Sink/Stream of [WsMessage]. Created with [WsMeta::connect](crate::WsMeta::connect). 5 | /// 6 | /// ## Closing the connection 7 | /// 8 | /// When this is dropped, the connection closes, but you should favor calling one of the close 9 | /// methods on [WsMeta](crate::WsMeta), which allow you to set a proper close code and reason. 10 | /// 11 | /// Since this implements [`Sink`], it has to have a close method. This method will call the 12 | /// web api [`WebSocket.close`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close) 13 | /// without parameters. Eg. a default value of `1005` will be assumed for the close code. The 14 | /// situation is the same when dropping without calling close. 15 | /// 16 | /// **Warning**: This object holds the callbacks needed to receive events from the browser. 17 | /// If you drop it before the close event was emitted, you will no longer receive events. Thus, 18 | /// observers will never receive a `Close` event. Drop will issue a `Closing` event and this 19 | /// will be the very last event observers receive. The the stream will end if `WsMeta` is also dropped. 20 | /// 21 | /// See the [integration tests](https://github.com/najamelan/ws_stream_wasm/blob/release/tests/futures_codec.rs) 22 | /// if you need an example. 23 | /// 24 | // 25 | pub struct WsStream 26 | { 27 | ws: SendWrapper< Rc< WebSocket > >, 28 | 29 | // The queue of received messages 30 | // 31 | queue: SendWrapper< Rc >> >, 32 | 33 | // Last waker of task that wants to read incoming messages to be woken up on a new message 34 | // 35 | waker: SendWrapper< Rc >> >, 36 | 37 | // Last waker of task that wants to write to the Sink 38 | // 39 | sink_waker: SendWrapper< Rc >> >, 40 | 41 | // A pointer to the pharos of WsMeta for when we need to listen to events 42 | // 43 | pharos: SharedPharos, 44 | 45 | // The callback closures. 46 | // 47 | _on_open : SendWrapper< Closure< dyn FnMut() > >, 48 | _on_error: SendWrapper< Closure< dyn FnMut() > >, 49 | _on_close: SendWrapper< Closure< dyn FnMut( JsCloseEvt ) > >, 50 | _on_mesg : SendWrapper< Closure< dyn FnMut( MessageEvent ) > >, 51 | 52 | // This allows us to store a future to poll when Sink::poll_close is called 53 | // 54 | closer: Option + Send >> >>, 55 | } 56 | 57 | 58 | impl WsStream 59 | { 60 | /// Create a new WsStream. 61 | // 62 | pub(crate) fn new 63 | ( 64 | ws : SendWrapper< Rc > , 65 | pharos : SharedPharos , 66 | on_open : SendWrapper< Closure< dyn FnMut() > > , 67 | on_error: SendWrapper< Closure< dyn FnMut() > > , 68 | on_close: SendWrapper< Closure< dyn FnMut( JsCloseEvt ) > > , 69 | 70 | ) -> Self 71 | 72 | { 73 | let waker : SendWrapper< Rc>> > = SendWrapper::new( Rc::new( RefCell::new( None )) ); 74 | let sink_waker: SendWrapper< Rc>> > = SendWrapper::new( Rc::new( RefCell::new( None )) ); 75 | 76 | let queue = SendWrapper::new( Rc::new( RefCell::new( VecDeque::new() ) ) ); 77 | let q2 = queue.clone(); 78 | let w2 = waker.clone(); 79 | let ph2 = pharos.clone(); 80 | 81 | 82 | // Send the incoming ws messages to the WsMeta object 83 | // 84 | #[ allow( trivial_casts ) ] 85 | // 86 | let on_mesg = Closure::wrap( Box::new( move |msg_evt: MessageEvent| 87 | { 88 | match WsMessage::try_from( msg_evt ) 89 | { 90 | Ok (msg) => q2.borrow_mut().push_back( msg ), 91 | Err(err) => notify( ph2.clone(), WsEvent::WsErr( err ) ), 92 | } 93 | 94 | if let Some( w ) = w2.borrow_mut().take() 95 | { 96 | w.wake() 97 | } 98 | 99 | }) as Box< dyn FnMut( MessageEvent ) > ); 100 | 101 | 102 | // Install callback 103 | // 104 | ws.set_onmessage ( Some( on_mesg.as_ref().unchecked_ref() ) ); 105 | 106 | 107 | // When the connection closes, we need to verify if there are any tasks 108 | // waiting on poll_next. We need to wake them up. 109 | // 110 | let ph = pharos .clone(); 111 | let wake = waker .clone(); 112 | let swake = sink_waker.clone(); 113 | 114 | let wake_on_close = async move 115 | { 116 | let mut rx; 117 | 118 | // Scope to avoid borrowing across await point. 119 | // 120 | { 121 | match ph.observe_shared( Filter::Pointer( WsEvent::is_closed ).into() ).await 122 | { 123 | Ok(events) => rx = events , 124 | Err(e) => unreachable!( "{:?}", e ) , // only happens if we closed it. 125 | } 126 | } 127 | 128 | rx.next().await; 129 | 130 | if let Some(w) = &*wake.borrow() 131 | { 132 | w.wake_by_ref(); 133 | } 134 | 135 | if let Some(w) = &*swake.borrow() 136 | { 137 | w.wake_by_ref(); 138 | } 139 | }; 140 | 141 | spawn_local( wake_on_close ); 142 | 143 | 144 | Self 145 | { 146 | ws , 147 | queue , 148 | waker , 149 | sink_waker , 150 | pharos , 151 | closer : None , 152 | _on_mesg : SendWrapper::new( on_mesg ) , 153 | _on_open : on_open , 154 | _on_error : on_error , 155 | _on_close : on_close , 156 | } 157 | } 158 | 159 | 160 | 161 | /// Verify the [WsState] of the connection. 162 | // 163 | pub fn ready_state( &self ) -> WsState 164 | { 165 | self.ws.ready_state().try_into() 166 | 167 | // This can't throw unless the browser gives us an invalid ready state 168 | // 169 | .expect_throw( "Convert ready state from browser API" ) 170 | } 171 | 172 | 173 | 174 | /// Access the wrapped [web_sys::WebSocket](https://docs.rs/web-sys/0.3.25/web_sys/struct.WebSocket.html) directly. 175 | /// 176 | /// _ws_stream_wasm_ tries to expose all useful functionality through an idiomatic rust API, so hopefully 177 | /// you won't need this, however if I missed something, you can. 178 | /// 179 | /// ## Caveats 180 | /// If you call `set_onopen`, `set_onerror`, `set_onmessage` or `set_onclose` on this, you will overwrite 181 | /// the event listeners from `ws_stream_wasm`, and things will break. 182 | // 183 | pub fn wrapped( &self ) -> &WebSocket 184 | { 185 | &self.ws 186 | } 187 | 188 | 189 | /// Wrap this object in [`IoStream`]. `IoStream` implements `AsyncRead`/`AsyncWrite`/`AsyncBufRead`. 190 | /// **Beware**: that this will transparenty include text messages as bytes. 191 | // 192 | pub fn into_io( self ) -> IoStream< WsStreamIo, Vec > 193 | { 194 | IoStream::new( WsStreamIo::new( self ) ) 195 | } 196 | } 197 | 198 | 199 | 200 | impl fmt::Debug for WsStream 201 | { 202 | fn fmt( &self, f: &mut fmt::Formatter<'_> ) -> fmt::Result 203 | { 204 | write!( f, "WsStream for connection: {}", self.ws.url() ) 205 | } 206 | } 207 | 208 | 209 | 210 | impl Drop for WsStream 211 | { 212 | // We don't block here, just tell the browser to close the connection and move on. 213 | // 214 | fn drop( &mut self ) 215 | { 216 | match self.ready_state() 217 | { 218 | WsState::Closing | WsState::Closed => {} 219 | 220 | _ => 221 | { 222 | // This can't fail. Only exceptions are related to invalid 223 | // close codes and reason strings to long. 224 | // 225 | self.ws.close().expect( "WsStream::drop - close ws socket" ); 226 | 227 | 228 | // Notify Observers. This event is not emitted by the websocket API. 229 | // 230 | notify( self.pharos.clone(), WsEvent::Closing ) 231 | } 232 | } 233 | 234 | self.ws.set_onmessage( None ); 235 | self.ws.set_onerror ( None ); 236 | self.ws.set_onopen ( None ); 237 | self.ws.set_onclose ( None ); 238 | } 239 | } 240 | 241 | 242 | 243 | impl Stream for WsStream 244 | { 245 | type Item = WsMessage; 246 | 247 | // Currently requires an unfortunate copy from Js memory to WASM memory. Hopefully one 248 | // day we will be able to receive the MessageEvt directly in WASM. 249 | // 250 | fn poll_next( self: Pin<&mut Self>, cx: &mut Context<'_> ) -> Poll> 251 | { 252 | // Once the queue is empty, check the state of the connection. 253 | // When it is closing or closed, no more messages will arrive, so 254 | // return Poll::Ready( None ) 255 | // 256 | if self.queue.borrow().is_empty() 257 | { 258 | *self.waker.borrow_mut() = Some( cx.waker().clone() ); 259 | 260 | match self.ready_state() 261 | { 262 | WsState::Open | WsState::Connecting => Poll::Pending , 263 | _ => None.into() , 264 | } 265 | } 266 | 267 | // As long as there is things in the queue, just keep reading 268 | // 269 | else { self.queue.borrow_mut().pop_front().into() } 270 | } 271 | } 272 | 273 | 274 | 275 | impl Sink for WsStream 276 | { 277 | type Error = WsErr; 278 | 279 | 280 | // Web API does not really seem to let us check for readiness, other than the connection state. 281 | // 282 | fn poll_ready( self: Pin<&mut Self>, cx: &mut Context<'_> ) -> Poll> 283 | { 284 | match self.ready_state() 285 | { 286 | WsState::Connecting => 287 | { 288 | *self.sink_waker.borrow_mut() = Some( cx.waker().clone() ); 289 | 290 | Poll::Pending 291 | } 292 | 293 | WsState::Open => Ok(()).into(), 294 | _ => Err( WsErr::ConnectionNotOpen ).into(), 295 | } 296 | } 297 | 298 | 299 | fn start_send( self: Pin<&mut Self>, item: WsMessage ) -> Result<(), Self::Error> 300 | { 301 | match self.ready_state() 302 | { 303 | WsState::Open => 304 | { 305 | // The send method can return 2 errors: 306 | // - unpaired surrogates in UTF (we shouldn't get those in rust strings) 307 | // - connection is already closed. 308 | // 309 | // So if this returns an error, we will return ConnectionNotOpen. In principle 310 | // we just checked that it's open, but this guarantees correctness. 311 | // 312 | match item 313 | { 314 | WsMessage::Binary( d ) => self.ws.send_with_u8_array( &d ).map_err( |_| WsErr::ConnectionNotOpen )? , 315 | WsMessage::Text ( s ) => self.ws.send_with_str ( &s ).map_err( |_| WsErr::ConnectionNotOpen )? , 316 | } 317 | 318 | Ok(()) 319 | }, 320 | 321 | 322 | // Connecting, Closing or Closed 323 | // 324 | _ => Err( WsErr::ConnectionNotOpen ), 325 | } 326 | } 327 | 328 | 329 | 330 | fn poll_flush( self: Pin<&mut Self>, _: &mut Context<'_> ) -> Poll> 331 | { 332 | Ok(()).into() 333 | } 334 | 335 | 336 | 337 | // TODO: find a simpler implementation, notably this needs to spawn a future. 338 | // this can be done by creating a custom future. If we are going to implement 339 | // events with pharos, that's probably a good time to re-evaluate this. 340 | // 341 | fn poll_close( mut self: Pin<&mut Self>, cx: &mut Context<'_> ) -> Poll> 342 | { 343 | let state = self.ready_state(); 344 | 345 | 346 | // First close the inner connection 347 | // 348 | if state == WsState::Connecting 349 | || state == WsState::Open 350 | { 351 | // Can't fail 352 | // 353 | self.ws.close().unwrap_throw(); 354 | 355 | notify( self.pharos.clone(), WsEvent::Closing ); 356 | } 357 | 358 | 359 | // Check whether it's closed 360 | // 361 | match state 362 | { 363 | WsState::Closed => Ok(()).into(), 364 | 365 | _ => 366 | { 367 | // Create a future that will resolve with the close event, so we can poll it. 368 | // 369 | if self.closer.is_none() 370 | { 371 | let mut ph = self.pharos.clone(); 372 | 373 | let closer = async move 374 | { 375 | let mut rx = match ph.observe( Filter::Pointer( WsEvent::is_closed ).into() ).await 376 | { 377 | Ok(events) => events , 378 | Err(e) => unreachable!( "{:?}", e ) , // only happens if we closed it. 379 | }; 380 | 381 | rx.next().await; 382 | }; 383 | 384 | self.closer = Some(SendWrapper::new( closer.boxed() )); 385 | } 386 | 387 | 388 | ready!( self.closer.as_mut().unwrap().as_mut().poll(cx) ); 389 | 390 | Ok(()).into() 391 | } 392 | } 393 | } 394 | } 395 | 396 | 397 | 398 | 399 | -------------------------------------------------------------------------------- /src/ws_stream_io.rs: -------------------------------------------------------------------------------- 1 | use crate::{ import::*, WsErr, WsStream }; 2 | 3 | 4 | /// A wrapper around WsStream that converts errors into io::Error so that it can be 5 | /// used for io (like `AsyncRead`/`AsyncWrite`). 6 | /// 7 | /// You shouldn't need to use this manually. It is passed to [`IoStream`] when calling 8 | /// [`WsStream::into_io`]. 9 | // 10 | #[ derive(Debug) ] 11 | // 12 | pub struct WsStreamIo 13 | { 14 | inner: WsStream 15 | } 16 | 17 | 18 | 19 | impl WsStreamIo 20 | { 21 | /// Create a new WsStreamIo. 22 | // 23 | pub fn new( inner: WsStream ) -> Self 24 | { 25 | Self { inner } 26 | } 27 | } 28 | 29 | 30 | 31 | impl Stream for WsStreamIo 32 | { 33 | type Item = Result< Vec, io::Error >; 34 | 35 | 36 | fn poll_next( mut self: Pin<&mut Self>, cx: &mut Context<'_> ) -> Poll> 37 | { 38 | Pin::new( &mut self.inner ).poll_next( cx ) 39 | 40 | .map( |opt| 41 | 42 | opt.map( |msg| Ok( msg.into() ) ) 43 | ) 44 | } 45 | } 46 | 47 | 48 | 49 | impl Sink< Vec > for WsStreamIo 50 | { 51 | type Error = io::Error; 52 | 53 | 54 | fn poll_ready( mut self: Pin<&mut Self>, cx: &mut Context<'_> ) -> Poll> 55 | { 56 | Pin::new( &mut self.inner ).poll_ready( cx ).map( convert_res_tuple ) 57 | } 58 | 59 | 60 | fn start_send( mut self: Pin<&mut Self>, item: Vec ) -> Result<(), Self::Error> 61 | { 62 | Pin::new( &mut self.inner ).start_send( item.into() ).map_err( convert_err ) 63 | } 64 | 65 | 66 | fn poll_flush( mut self: Pin<&mut Self>, cx: &mut Context<'_> ) -> Poll> 67 | { 68 | Pin::new( &mut self.inner ).poll_flush( cx ).map( convert_res_tuple ) 69 | } 70 | 71 | 72 | fn poll_close( mut self: Pin<&mut Self>, cx: &mut Context<'_> ) -> Poll> 73 | { 74 | Pin::new( &mut self.inner ).poll_close( cx ).map( convert_res_tuple ) 75 | } 76 | } 77 | 78 | 79 | 80 | fn convert_res_tuple( res: Result< (), WsErr> ) -> Result< (), io::Error > 81 | { 82 | res.map_err( convert_err ) 83 | } 84 | 85 | 86 | 87 | fn convert_err( err: WsErr ) -> io::Error 88 | { 89 | match err 90 | { 91 | WsErr::ConnectionNotOpen => return io::Error::from( io::ErrorKind::NotConnected ) , 92 | 93 | // This shouldn't happen, so panic for early detection. 94 | // 95 | _ => unreachable!(), 96 | } 97 | } 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /tests/events.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | wasm_bindgen_test_configure!(run_in_browser); 3 | 4 | // What's tested: 5 | // 6 | // Tests send to an echo server which just bounces back all data. 7 | // 8 | // ✔ Verify close events are emitted. 9 | // 10 | use 11 | { 12 | futures::prelude :: { * } , 13 | wasm_bindgen::prelude :: { * } , 14 | wasm_bindgen_test :: { * } , 15 | log :: { * } , 16 | ws_stream_wasm :: { * } , 17 | pharos :: { ObserveConfig, Observable } , 18 | // web_sys :: { console::log_1 as dbg } , 19 | }; 20 | 21 | 22 | const URL: &str = "ws://127.0.0.1:3212"; 23 | 24 | 25 | 26 | // Verify close events are emitted. 27 | // 28 | #[ wasm_bindgen_test ] 29 | // 30 | async fn close_events() 31 | { 32 | let _ = console_log::init_with_level( Level::Trace ); 33 | 34 | info!( "starting test: close_events" ); 35 | 36 | let (mut ws, _wsio) = WsMeta::connect( URL, None ).await.expect_throw( "Could not create websocket" ); 37 | 38 | let mut evts = ws.observe( ObserveConfig::default() ).await.expect( "observe" ); 39 | 40 | ws.close().await.expect_throw( "close ws" ); 41 | 42 | assert!( evts.next().await.unwrap_throw().is_closing() ); 43 | assert!( evts.next().await.unwrap_throw().is_closed() ); 44 | } 45 | 46 | -------------------------------------------------------------------------------- /tests/futures_codec.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | wasm_bindgen_test_configure!(run_in_browser); 3 | 4 | // What's tested: 5 | // 6 | // Tests send to an echo server which just bounces back all data. 7 | // 8 | // ✔ Frame with a BytesCodec and verify that a round trip returns identical data 9 | // ✔ Use a LinesCodec and get back identical lines 10 | // 11 | use 12 | { 13 | wasm_bindgen::prelude :: { * } , 14 | wasm_bindgen_test :: { * } , 15 | ws_stream_wasm :: { * } , 16 | log :: { * } , 17 | rand_xoshiro :: { * } , 18 | rand :: { RngCore, SeedableRng } , 19 | bytes :: { Bytes } , 20 | futures :: { stream::StreamExt, sink::SinkExt, } , 21 | asynchronous_codec :: { Framed, LinesCodec, BytesCodec } , 22 | serde :: { Serialize, Deserialize } , 23 | // web_sys :: { console::log_1 as dbg } , 24 | async_io_stream :: { IoStream } , 25 | }; 26 | 27 | 28 | const URL: &str = "ws://127.0.0.1:3212"; 29 | 30 | 31 | 32 | async fn connect() -> (WsMeta, IoStream>) 33 | { 34 | let (ws, wsio) = WsMeta::connect( URL, None ).await.expect_throw( "Could not create websocket" ); 35 | 36 | (ws, wsio.into_io()) 37 | } 38 | 39 | 40 | 41 | // Verify that a round trip to an echo server generates identical data. 42 | // 43 | #[ wasm_bindgen_test( async ) ] 44 | // 45 | async fn data_integrity() 46 | { 47 | let _ = console_log::init_with_level( Level::Trace ); 48 | 49 | info!( "starting test: data_integrity" ); 50 | 51 | let big_size = 10240; // bytes 52 | let mut random = vec![ 0; big_size ]; 53 | let mut rng = Xoshiro256Plus::from_seed( [ 1, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0 ] ); 54 | 55 | rng.fill_bytes( &mut random ); 56 | 57 | let dataset: Vec<(&str, usize, Bytes)> = vec! 58 | [ 59 | ( "basic" , 18, Bytes::from_static( b"Hello from browser" ) ), 60 | 61 | // 20 random bytes, not valid unicode 62 | // 63 | ( "random bytes", 20, Bytes::from( vec![ 72, 31, 238, 236, 85, 240, 197, 235, 149, 238, 245, 206, 227, 201, 139, 63, 173, 214, 158, 134 ] ) ), 64 | 65 | // Test with something big: 66 | // 67 | ( "big random" , big_size, Bytes::from( random ) ), 68 | ]; 69 | 70 | for data in dataset 71 | { 72 | echo( data.0, data.1, data.2 ).await; 73 | } 74 | } 75 | 76 | 77 | 78 | // Send data to an echo server and verify that what returns is exactly the same 79 | // We run 2 connections in parallel, the second one we verify that we can use a reference 80 | // to a WsMeta 81 | // 82 | async fn echo( name: &str, size: usize, data: Bytes ) 83 | { 84 | info!( " Enter echo: {}", name ); 85 | 86 | let (_ws , wsio) = connect().await; 87 | let mut framed = Framed::new( wsio, BytesCodec {} ); 88 | framed.send( data.clone() ).await.expect_throw( "Failed to write to websocket" ); 89 | 90 | let mut result: Vec = Vec::new(); 91 | 92 | while result.len() < size 93 | { 94 | let msg = framed.next().await.expect_throw( "Some" ).expect_throw( "Receive bytes" ); 95 | let buf: &[u8] = msg.as_ref(); 96 | result.extend( buf ); 97 | } 98 | 99 | assert_eq!( &data, &Bytes::from( result ) ); 100 | 101 | framed.close().await.expect_throw( "close" ); 102 | } 103 | 104 | 105 | 106 | 107 | 108 | ///////////////////// 109 | // With LinesCodec // 110 | ///////////////////// 111 | 112 | 113 | #[ derive( Debug, Clone, Serialize, Deserialize, PartialEq, Eq ) ] 114 | // 115 | struct Data 116 | { 117 | hello: String , 118 | data : Vec , 119 | num : u64 , 120 | } 121 | 122 | 123 | // Verify that a round trip to an echo server generates identical data. 124 | // 125 | #[ wasm_bindgen_test( async ) ] 126 | // 127 | async fn lines_integrity() 128 | { 129 | let _ = console_log::init_with_level( Level::Trace ); 130 | 131 | info!( "starting test: lines_integrity" ); 132 | 133 | 134 | let (_ws , wsio ) = connect().await; 135 | let mut framed = Framed::new( wsio, LinesCodec {} ); 136 | 137 | info!( "lines_integrity: start sending" ); 138 | 139 | framed.send( "A line\n" .to_string() ).await.expect_throw( "Send a line" ); 140 | framed.send( "A second line\n".to_string() ).await.expect_throw( "Send a second line" ); 141 | framed.send( "A third line\n" .to_string() ).await.expect_throw( "Send a third line" ); 142 | 143 | info!( "lines_integrity: start receiving" ); 144 | 145 | let one = framed.next().await.expect_throw( "Some" ).expect_throw( "Receive a line" ); 146 | let two = framed.next().await.expect_throw( "Some" ).expect_throw( "Receive a second line" ); 147 | let three = framed.next().await.expect_throw( "Some" ).expect_throw( "Receive a third line" ); 148 | 149 | info!( "lines_integrity: start asserting" ); 150 | 151 | assert_eq!( "A line\n" , &one ); 152 | assert_eq!( "A second line\n", &two ); 153 | assert_eq!( "A third line\n" , &three ); 154 | 155 | info!( "lines_integrity: done" ); 156 | 157 | framed.close().await.expect_throw( "close" ); 158 | } 159 | 160 | 161 | 162 | 163 | -------------------------------------------------------------------------------- /tests/tokio_codec.rs: -------------------------------------------------------------------------------- 1 | #![ cfg( feature = "tokio_io" ) ] 2 | 3 | #![allow(dead_code)] 4 | wasm_bindgen_test_configure!(run_in_browser); 5 | 6 | // What's tested: 7 | // 8 | // Tests send to an echo server which just bounces back all data. 9 | // 10 | // ✔ Frame with a BytesCodec and verify that a round trip returns identical data 11 | // ✔ Send 1MB data in a custom struct serialized with cbor_codec 12 | // 13 | use 14 | { 15 | wasm_bindgen::prelude :: { * } , 16 | wasm_bindgen_test :: { * } , 17 | ws_stream_wasm :: { * } , 18 | log :: { * } , 19 | rand_xoshiro :: { * } , 20 | rand :: { RngCore, SeedableRng } , 21 | tokio_util :: { codec::{ BytesCodec, Framed } } , 22 | bytes :: { Bytes } , 23 | futures :: { stream::{ StreamExt }, sink::SinkExt } , 24 | serde :: { Serialize, Deserialize } , 25 | tokio_serde_cbor :: { Codec } , 26 | async_io_stream :: { IoStream } , 27 | 28 | // web_sys :: { console::log_1 as dbg } , 29 | }; 30 | 31 | 32 | 33 | const URL: &str = "ws://127.0.0.1:3212"; 34 | 35 | 36 | 37 | async fn connect() -> (WsMeta, IoStream< WsStreamIo, Vec > ) 38 | { 39 | let (ws, wsio) = WsMeta::connect( URL, None ).await.expect_throw( "Could not create websocket" ); 40 | 41 | (ws, wsio.into_io()) 42 | } 43 | 44 | 45 | 46 | // Verify that a round trip to an echo server generates identical data. 47 | // 48 | #[ wasm_bindgen_test( async ) ] 49 | // 50 | async fn data_integrity() 51 | { 52 | // It's normal for this to fail, since different tests run in the same module and we can only 53 | // set the logger once. Since we don't know which test runs first, we ignore the Result. 54 | // Logging will only show up in the browser, not in wasm-pack test --headless. 55 | // 56 | let _ = console_log::init_with_level( Level::Trace ); 57 | 58 | info!( "starting test: data_integrity" ); 59 | 60 | let big_size = 10240; 61 | let mut random = vec![ 0u8; big_size ]; 62 | let mut rng = Xoshiro256Plus::from_seed( [ 1, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0 ] ); 63 | 64 | rng.fill_bytes( &mut random ); 65 | 66 | let dataset: Vec<(&str, usize, Bytes)> = vec! 67 | [ 68 | ( "basic", 18, Bytes::from_static( b"Hello from browser" ) ), 69 | 70 | // 20 random bytes, not valid unicode 71 | // 72 | ( "random bytes", 20, Bytes::from( vec![ 72, 31, 238, 236, 85, 240, 197, 235, 149, 238, 245, 206, 227, 201, 139, 63, 173, 214, 158, 134 ] ) ), 73 | 74 | // Test with something big: 75 | // 76 | ( "big random", big_size, Bytes::from( random ) ), 77 | ]; 78 | 79 | for data in dataset 80 | { 81 | echo( data.0, data.1, data.2 ).await; 82 | } 83 | } 84 | 85 | 86 | 87 | // Send data to an echo server and verify that what returns is exactly the same 88 | // 89 | async fn echo( name: &str, size: usize, data: Bytes ) 90 | { 91 | info!( " Enter echo: {}", name ); 92 | 93 | let (_ws, wsio) = connect().await; 94 | let (mut tx, mut rx) = Framed::new( wsio, BytesCodec::new() ).split(); 95 | 96 | tx.send( data.clone() ).await.expect_throw( "Failed to write to websocket" ); 97 | 98 | let mut result: Vec = Vec::new(); 99 | 100 | while result.len() < size 101 | { 102 | let msg = rx.next().await.expect_throw( "read message" ); 103 | let buf: &[u8] = msg.as_ref().expect_throw( "msg.as_ref()" ); 104 | result.extend_from_slice( buf ); 105 | } 106 | 107 | assert_eq!( &data, &Bytes::from( result ) ); 108 | 109 | tx.close().await.expect_throw( "close" ); 110 | } 111 | 112 | 113 | 114 | 115 | 116 | ///////////////////// 117 | // With serde-cbor // 118 | ///////////////////// 119 | 120 | 121 | #[ derive( Debug, Clone, Serialize, Deserialize, PartialEq, Eq ) ] 122 | // 123 | struct Data 124 | { 125 | hello: String , 126 | data : Vec , 127 | num : u64 , 128 | } 129 | 130 | 131 | // Verify that a round trip to an echo server generates identical data. This test includes a big (1MB) 132 | // piece of data. 133 | // 134 | #[ wasm_bindgen_test( async ) ] 135 | // 136 | async fn data_integrity_cbor() 137 | { 138 | // It's normal for this to fail, since different tests run in the same module and we can only 139 | // set the logger once. Since we don't know which test runs first, we ignore the Result. 140 | // Logging will only show up in the browser, not in wasm-pack test --headless. 141 | // 142 | let _ = console_log::init_with_level( Level::Trace ); 143 | 144 | info!( "starting test: data_integrity_cbor" ); 145 | 146 | let dataset: Vec = vec! 147 | [ 148 | Data{ hello: "Hello CBOR - basic".to_string(), data: vec![ 0, 33245, 3, 36 ], num: 3948594 }, 149 | 150 | // Test with something big 151 | // 152 | Data{ hello: "Hello CBOR - 1MB data".to_string(), data: vec![ 1; 1_024_000 ], num: 3948595 }, 153 | ]; 154 | 155 | for data in dataset 156 | { 157 | echo_cbor( data ).await; 158 | } 159 | } 160 | 161 | 162 | // Send data to an echo server and verify that what returns is exactly the same 163 | // 164 | async fn echo_cbor( data: Data ) 165 | { 166 | info!( " Enter echo_cbor: {}", &data.hello ); 167 | 168 | let (_ws, wsio) = connect().await; 169 | 170 | let codec: Codec = Codec::new().packed( true ); 171 | let (mut tx, mut rx) = Framed::new( wsio, codec ).split(); 172 | 173 | tx.send( data.clone() ).await.expect_throw( "Failed to write to websocket" ); 174 | 175 | let msg = rx.next().await.expect_throw( "read message" ).expect_throw( "msg" ); 176 | 177 | assert_eq!( data, msg ); 178 | 179 | tx.close().await.expect_throw( "close" ); 180 | } 181 | 182 | 183 | 184 | -------------------------------------------------------------------------------- /tests/ws_meta.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | wasm_bindgen_test_configure!(run_in_browser); 3 | 4 | // What's tested: 5 | // 6 | // Tests send to an echo server which just bounces back all data. 7 | // 8 | // ✔ WsMeta::connect: Verify error when connecting to a wrong port 9 | // ✔ WsMeta::connect: Verify error when connecting to a forbidden port 10 | // ✔ WsMeta::connect: Verify error when connecting to wss:// on ws:// server 11 | // ✔ WsMeta::connect: Verify error when connecting to a wrong scheme 12 | // ✔ Verify the state method 13 | // ✔ Verify closing from WsStream 14 | // ✔ Verify url method 15 | // ✔ Verify sending no subprotocols 16 | // note: we currently don't have a backend server that supports protocols, 17 | // so there is no test for testing usage of protocols 18 | // ✔ Verify closing with a valid code 19 | // ✔ Verify error upon closing with invalid code 20 | // ✔ Verify closing with a valid code and reason 21 | // ✔ Verfiy close_reason with an invalid close code 22 | // ✔ Verfiy close_reason with an invalid reason string 23 | // ✔ Verfiy Debug impl 24 | // 25 | use 26 | { 27 | futures :: { sink::SinkExt } , 28 | wasm_bindgen::prelude :: { * } , 29 | wasm_bindgen_test :: { * } , 30 | ws_stream_wasm :: { * } , 31 | log :: { * } , 32 | }; 33 | 34 | 35 | 36 | const URL: &str = "ws://127.0.0.1:3212/"; 37 | 38 | 39 | 40 | // WsMeta::connect: Verify error when connecting to a wrong port 41 | // 42 | #[ wasm_bindgen_test ] 43 | // 44 | async fn connect_wrong_port() 45 | { 46 | let _ = console_log::init_with_level( Level::Trace ); 47 | 48 | info!( "starting test: connect_wrong_port" ); 49 | 50 | let err = WsMeta::connect( "ws://127.0.0.1:33212/", None ).await; 51 | 52 | assert!( err.is_err() ); 53 | 54 | let err = err.unwrap_err(); 55 | 56 | assert_eq! 57 | ( 58 | WsErr::ConnectionFailed 59 | { 60 | event: CloseEvent 61 | { 62 | was_clean: false, 63 | code : 1006 , 64 | reason : "".to_string(), 65 | } 66 | }, 67 | 68 | err 69 | ); 70 | } 71 | 72 | 73 | 74 | // WsMeta::connect: Verify error when connecting to a forbidden port 75 | // 76 | #[ wasm_bindgen_test ] 77 | // 78 | async fn connect_forbidden_port() 79 | { 80 | let _ = console_log::init_with_level( Level::Trace ); 81 | 82 | info!( "starting test: connect_forbidden_port" ); 83 | 84 | let err = WsMeta::connect( "ws://127.0.0.1:6000/", None ).await; 85 | 86 | assert!( err.is_err() ); 87 | 88 | let err = err.unwrap_err(); 89 | 90 | assert!(matches!( err, WsErr::ConnectionFailed{..} )); 91 | } 92 | 93 | 94 | 95 | // WsMeta::connect: Verify error when connecting to wss:// on ws:// server 96 | // 97 | #[ wasm_bindgen_test ] 98 | // 99 | async fn connect_wrong_wss() 100 | { 101 | let _ = console_log::init_with_level( Level::Trace ); 102 | 103 | info!( "starting test: connect_wrong_wss" ); 104 | 105 | let err = WsMeta::connect( "wss://127.0.0.1:3212/", None ).await; 106 | 107 | assert!( err.is_err() ); 108 | 109 | let err = err.unwrap_err(); 110 | 111 | assert_eq! 112 | ( 113 | WsErr::ConnectionFailed 114 | { 115 | event: CloseEvent 116 | { 117 | was_clean: false, 118 | code : 1006 , 119 | reason : "".to_string(), 120 | } 121 | }, 122 | 123 | err 124 | ); 125 | } 126 | 127 | 128 | 129 | // WsMeta::connect: Verify we can connect to a TLS server. 130 | // 131 | #[ wasm_bindgen_test ] 132 | // 133 | async fn connect_to_tls() 134 | { 135 | let _ = console_log::init_with_level( Level::Trace ); 136 | 137 | info!( "starting test: connect_to_tls" ); 138 | 139 | let err = WsMeta::connect( "wss://127.0.0.1:8443/", None ).await; 140 | 141 | assert!( err.is_ok(), "{:?}", err ); 142 | } 143 | 144 | 145 | 146 | // WsMeta::connect: Verify error when connecting to wss:// on ws:// server 147 | // 148 | #[ wasm_bindgen_test ] 149 | // 150 | async fn connect_to_tls_wrong_protocol() 151 | { 152 | let _ = console_log::init_with_level( Level::Trace ); 153 | 154 | info!( "starting test: connect_to_tls_wrong_protocol" ); 155 | 156 | let err = WsMeta::connect( "ws://127.0.0.1:8443/", None ).await; 157 | 158 | assert!( err.is_err() ); 159 | 160 | let err = err.unwrap_err(); 161 | 162 | assert_eq! 163 | ( 164 | WsErr::ConnectionFailed 165 | { 166 | event: CloseEvent 167 | { 168 | was_clean: false, 169 | code : 1006 , 170 | reason : "".to_string(), 171 | } 172 | }, 173 | 174 | err 175 | ); 176 | } 177 | 178 | 179 | 180 | // WsMeta::connect: Verify we can connect using http protocol. 181 | // This used to be an error. 182 | // 183 | #[ wasm_bindgen_test ] 184 | // 185 | async fn connect_with_http_protocol() 186 | { 187 | let _ = console_log::init_with_level( Level::Trace ); 188 | 189 | info!( "starting test: connect_wrong_scheme" ); 190 | 191 | let conn = WsMeta::connect( "http://127.0.0.1:3212/", None ).await; 192 | 193 | assert!( conn.is_ok() ); 194 | } 195 | 196 | 197 | 198 | // Verify state method. 199 | // 200 | #[ wasm_bindgen_test ] 201 | // 202 | async fn state() 203 | { 204 | let _ = console_log::init_with_level( Level::Trace ); 205 | 206 | info!( "starting test: state" ); 207 | 208 | let (ws, wsio) = WsMeta::connect( URL, None ).await.expect_throw( "Could not create websocket" ); 209 | 210 | assert_eq!( WsState::Open, ws .ready_state() ); 211 | assert_eq!( WsState::Open, wsio.ready_state() ); 212 | 213 | ws.wrapped().close().expect_throw( "close WebSocket" ); 214 | 215 | assert_eq!( WsState::Closing, ws .ready_state() ); 216 | assert_eq!( WsState::Closing, wsio.ready_state() ); 217 | 218 | ws.close().await.expect_throw( "close ws" ); 219 | 220 | assert_eq!( WsState::Closed, ws .ready_state() ); 221 | assert_eq!( WsState::Closed, wsio.ready_state() ); 222 | } 223 | 224 | 225 | // Verify closing from WsStream. 226 | // 227 | #[ wasm_bindgen_test ] 228 | // 229 | async fn close_from_wsio() 230 | { 231 | let _ = console_log::init_with_level( Level::Trace ); 232 | 233 | info!( "starting test: close_from_wsio" ); 234 | 235 | let (ws, mut wsio) = WsMeta::connect( URL, None ).await.expect_throw( "Could not create websocket" ); 236 | 237 | assert_eq!( WsState::Open, ws.ready_state() ); 238 | 239 | SinkExt::close( &mut wsio ).await.expect( "close wsio sink" ); 240 | 241 | assert_eq!( WsState::Closed, wsio.ready_state() ); 242 | assert_eq!( WsState::Closed, ws .ready_state() ); 243 | } 244 | 245 | 246 | 247 | 248 | // Verify url method. 249 | // 250 | #[ wasm_bindgen_test ] 251 | // 252 | async fn url() 253 | { 254 | let _ = console_log::init_with_level( Level::Trace ); 255 | 256 | info!( "starting test: url" ); 257 | 258 | let (ws, _wsio) = WsMeta::connect( URL, None ).await.expect_throw( "Could not create websocket" ); 259 | 260 | assert_eq!( URL, ws.url() ); 261 | } 262 | 263 | 264 | 265 | 266 | // Verify protocols. 267 | // 268 | #[ wasm_bindgen_test ] 269 | // 270 | async fn no_protocols() 271 | { 272 | let _ = console_log::init_with_level( Level::Trace ); 273 | 274 | info!( "starting test: no_protocols" ); 275 | 276 | let (ws, _wsio) = WsMeta::connect( URL, None ).await.expect_throw( "Could not create websocket" ); 277 | 278 | assert_eq!( "", ws.protocol() ); 279 | } 280 | 281 | 282 | 283 | 284 | /* 285 | // Verify protocols. 286 | // This doesn't work with tungstenite for the moment. 287 | // 288 | #[ wasm_bindgen_test ] 289 | // 290 | async fn protocols_server_accept_none() 291 | { 292 | let _ = console_log::init_with_level( Level::Trace ); 293 | 294 | info!( "starting test: protocols_server_accept_none" ); 295 | 296 | let (ws, _wsio) = WsMeta::connect( URL, vec![ "chat" ] ).await.expect_throw( "Could not create websocket" ); 297 | 298 | assert_eq!( "", ws.protocol() ); 299 | } 300 | */ 301 | 302 | 303 | 304 | // Verify close_code method. 305 | // 306 | #[ wasm_bindgen_test ] 307 | // 308 | async fn close_twice() 309 | { 310 | let _ = console_log::init_with_level( Level::Trace ); 311 | 312 | info!( "starting test: close_twice" ); 313 | 314 | let (ws, _wsio) = WsMeta::connect( URL, None ).await.expect_throw( "Could not create websocket" ); 315 | 316 | let res = ws.close().await; 317 | 318 | assert!( res.is_ok() ); 319 | 320 | assert_eq!( ws.close () .await.unwrap_err(), WsErr::ConnectionNotOpen ); 321 | assert_eq!( ws.close_code ( 1000 ).await.unwrap_err(), WsErr::ConnectionNotOpen ); 322 | assert_eq!( ws.close_reason( 1000, "Normal shutdown" ).await.unwrap_err(), WsErr::ConnectionNotOpen ); 323 | } 324 | 325 | 326 | 327 | #[ wasm_bindgen_test ] 328 | // 329 | async fn close_code_valid() 330 | { 331 | let _ = console_log::init_with_level( Level::Trace ); 332 | 333 | info!( "starting test: close_code_valid" ); 334 | 335 | let (ws, _wsio) = WsMeta::connect( URL, None ).await.expect_throw( "Could not create websocket" ); 336 | 337 | let res = ws.close_code( 1000 ).await; 338 | 339 | assert!( res.is_ok() ); 340 | } 341 | 342 | 343 | // Verify close_code method. 344 | // 345 | #[ wasm_bindgen_test ] 346 | // 347 | async fn close_code_invalid() 348 | { 349 | let _ = console_log::init_with_level( Level::Trace ); 350 | 351 | info!( "starting test: close_code_invalid" ); 352 | 353 | let (ws, _wsio) = WsMeta::connect( URL, None ).await.expect_throw( "Could not create websocket" ); 354 | 355 | let res = ws.close_code( 500 ).await; 356 | 357 | assert_eq!( WsErr::InvalidCloseCode{ supplied: 500 }, res.unwrap_err() ); 358 | } 359 | 360 | 361 | // Verify close_code method. 362 | // 363 | #[ wasm_bindgen_test ] 364 | // 365 | async fn close_reason_valid() 366 | { 367 | let _ = console_log::init_with_level( Level::Trace ); 368 | 369 | info!( "starting test: close_reason_valid" ); 370 | 371 | let (ws, _wsio) = WsMeta::connect( URL, None ).await.expect_throw( "Could not create websocket" ); 372 | 373 | let res = ws.close_reason( 1000, "Normal shutdown" ).await; 374 | 375 | assert!( res.is_ok() ); 376 | } 377 | 378 | 379 | // Verify close_code method. 380 | // 381 | #[ wasm_bindgen_test ] 382 | // 383 | async fn close_reason_invalid_code() 384 | { 385 | let _ = console_log::init_with_level( Level::Trace ); 386 | 387 | info!( "starting test: close_reason_invalid_code" ); 388 | 389 | let (ws, _wsio) = WsMeta::connect( URL, None ).await.expect_throw( "Could not create websocket" ); 390 | 391 | let res = ws.close_reason( 500, "Normal Shutdown" ).await; 392 | 393 | assert_eq!( WsErr::InvalidCloseCode{ supplied: 500 }, res.unwrap_err() ); 394 | } 395 | 396 | 397 | // Verify close_code method. 398 | // 399 | #[ wasm_bindgen_test ] 400 | // 401 | async fn close_reason_invalid() 402 | { 403 | let _ = console_log::init_with_level( Level::Trace ); 404 | 405 | info!( "starting test: close_reason_invalid" ); 406 | 407 | let (ws, _wsio) = WsMeta::connect( URL, None ).await.expect_throw( "Could not create websocket" ); 408 | 409 | let res = ws.close_reason( 1000, vec![ "a"; 124 ].join( "" ) ).await; 410 | 411 | assert_eq!( WsErr::ReasonStringToLong, res.unwrap_err() ); 412 | } 413 | 414 | 415 | 416 | // Verify Debug impl. 417 | // 418 | #[ wasm_bindgen_test ] 419 | // 420 | async fn debug() 421 | { 422 | let _ = console_log::init_with_level( Level::Trace ); 423 | 424 | info!( "starting test: debug" ); 425 | 426 | let (ws, _wsio) = WsMeta::connect( URL, None ).await.expect_throw( "Could not create websocket" ); 427 | 428 | assert_eq!( format!( "WsMeta for connection: {URL}" ), format!( "{ws:?}" ) ); 429 | 430 | ws.close().await.expect_throw( "close" ); 431 | } 432 | 433 | -------------------------------------------------------------------------------- /tests/ws_stream.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | wasm_bindgen_test_configure!(run_in_browser); 3 | 4 | 5 | // What's tested: 6 | // 7 | // Tests send to an echo server which just bounces back all data. 8 | // 9 | // ✔ Send a WsMessage::Text and verify we get an identical WsMessage back. 10 | // ✔ Send a WsMessage::Binary and verify we get an identical WsMessage back. 11 | // ✔ Send while closing and verify the error 12 | // ✔ Send while closed and verify the error 13 | // ✔ Test Debug impl 14 | // 15 | // Note that AsyncRead/AsyncWrite are tested by futures_codec.rs and tokio_codec.rs 16 | // 17 | use 18 | { 19 | futures::prelude :: * , 20 | log :: * , 21 | pharos :: * , 22 | std::marker :: PhantomData , 23 | wasm_bindgen::prelude :: * , 24 | wasm_bindgen_futures :: spawn_local , 25 | wasm_bindgen_test :: * , 26 | ws_stream_wasm :: * , 27 | }; 28 | 29 | 30 | 31 | const URL : &str = "ws://127.0.0.1:3212/"; 32 | const URL_TT: &str = "ws://127.0.0.1:3312/"; 33 | 34 | 35 | 36 | // Verify that both WsStream and WsMeta are Send for now. The browser API's are not Send, 37 | // and this is not meant to be send accross threads. However some API's need to require 38 | // Send (eg async that can be spawned on a thread pool). However on wasm you can spawn them 39 | // on a single threaded executor and they shouldn't require Send. On the long term it would 40 | // be nice to have a better solution. 41 | // 42 | #[ wasm_bindgen_test ] 43 | // 44 | fn sendness() 45 | { 46 | struct SendNess{ _phantom: PhantomData } 47 | 48 | let _x = SendNess::{ _phantom: PhantomData }; 49 | let _x = SendNess::{ _phantom: PhantomData }; 50 | } 51 | 52 | 53 | 54 | // Verify that a round trip to an echo server generates identical textual data. 55 | // 56 | #[ wasm_bindgen_test ] 57 | // 58 | async fn round_trip_text() 59 | { 60 | let _ = console_log::init_with_level( Level::Trace ); 61 | 62 | info!( "starting test: round_trip_text" ); 63 | 64 | let (_ws, mut wsio) = WsMeta::connect( URL_TT, None ).await.expect_throw( "Could not create websocket" ); 65 | let message = "Hello from browser".to_string(); 66 | 67 | 68 | wsio.send( WsMessage::Text( message.clone() ) ).await 69 | 70 | .expect_throw( "Failed to write to websocket" ); 71 | 72 | 73 | let msg = wsio.next().await; 74 | let result = msg.expect_throw( "Stream closed" ); 75 | 76 | assert_eq!( WsMessage::Text( message ), result ); 77 | } 78 | 79 | 80 | 81 | // Verify that a round trip to an echo server generates identical binary data. 82 | // 83 | #[ wasm_bindgen_test ] 84 | // 85 | async fn round_trip_binary() 86 | { 87 | let _ = console_log::init_with_level( Level::Trace ); 88 | 89 | info!( "starting test: round_trip_binary" ); 90 | 91 | let (_ws, mut wsio) = WsMeta::connect( URL, None ).await.expect_throw( "Could not create websocket" ); 92 | let message = b"Hello from browser".to_vec(); 93 | 94 | 95 | wsio.send( WsMessage::Binary( message.clone() ) ).await 96 | 97 | .expect_throw( "Failed to write to websocket" ); 98 | 99 | 100 | let msg = wsio.next().await; 101 | let result = msg.expect_throw( "Stream closed" ); 102 | 103 | assert_eq!( WsMessage::Binary( message ), result ); 104 | } 105 | 106 | 107 | 108 | #[ wasm_bindgen_test ] 109 | // 110 | async fn send_while_closing() 111 | { 112 | info!( "starting test: send_while_closing" ); 113 | 114 | let (ws, mut wsio) = WsMeta::connect( URL, None ).await.expect_throw( "Could not create websocket" ); 115 | 116 | ws.wrapped().close().expect_throw( "close connection" ); 117 | 118 | let res = wsio.send( WsMessage::Text("Hello from browser".into() ) ).await; 119 | 120 | assert_eq!( WsErr::ConnectionNotOpen, res.unwrap_err() ); 121 | } 122 | 123 | 124 | 125 | #[ wasm_bindgen_test ] 126 | // 127 | async fn send_after_close() 128 | { 129 | info!( "starting test: send_after_close" ); 130 | 131 | let (ws, mut wsio) = WsMeta::connect( URL, None ).await.expect_throw( "Could not create websocket" ); 132 | 133 | ws.close().await.expect_throw( "close ws" ); 134 | 135 | let res = wsio.send( WsMessage::Text("Hello from browser".into() ) ).await; 136 | 137 | assert_eq!( WsErr::ConnectionNotOpen, res.unwrap_err() ); 138 | } 139 | 140 | 141 | 142 | // Verify closing that when closing from WsMeta, WsStream next() returns none. 143 | // 144 | #[ wasm_bindgen_test ] 145 | // 146 | async fn close_from_wsstream() 147 | { 148 | let _ = console_log::init_with_level( Level::Trace ); 149 | 150 | info!( "starting test: close_from_wsstream" ); 151 | 152 | let (ws, mut wsio) = WsMeta::connect( URL, None ).await.expect_throw( "Could not create websocket" ); 153 | 154 | ws.close().await.expect_throw( "close ws" ); 155 | 156 | assert!( wsio.next().await.is_none() ); 157 | } 158 | 159 | 160 | 161 | // Verify that closing wakes up a task pending on poll_next() 162 | // 163 | #[ wasm_bindgen_test ] 164 | // 165 | async fn close_from_wsstream_while_pending() 166 | { 167 | let _ = console_log::init_with_level( Level::Trace ); 168 | 169 | info!( "starting test: close_from_wsstream_while_pending" ); 170 | 171 | let (ws, mut wsio) = WsMeta::connect( URL, None ).await.expect_throw( "Could not create websocket" ); 172 | 173 | spawn_local( async move { ws.close().await.expect_throw( "close ws" ); } ); 174 | 175 | // if we don't wake up the task, this will hang 176 | // 177 | assert!( wsio.next().await.is_none() ); 178 | } 179 | 180 | 181 | 182 | // Verify that closing wakes up a task pending on poll_next() 183 | // 184 | #[ wasm_bindgen_test ] 185 | // 186 | async fn close_event_from_sink() 187 | { 188 | let _ = console_log::init_with_level( Level::Trace ); 189 | 190 | info!( "starting test: close_event_from_sink" ); 191 | 192 | let (mut ws, mut wsio) = WsMeta::connect( URL, None ).await.expect_throw( "Could not create websocket" ); 193 | 194 | let mut evts = ws.observe( ObserveConfig::default() ).await.expect( "observe" ); 195 | 196 | wsio.close().await.expect_throw( "close ws" ); 197 | 198 | assert!( evts.next().await.unwrap_throw().is_closing() ); 199 | assert!( evts.next().await.unwrap_throw().is_closed() ); 200 | } 201 | 202 | 203 | 204 | // Verify that closing wakes up a task pending on poll_next() 205 | // 206 | #[ wasm_bindgen_test ] 207 | // 208 | async fn close_event_from_async_write() 209 | { 210 | let _ = console_log::init_with_level( Level::Trace ); 211 | 212 | info!( "starting test: close_event_from_async_write" ); 213 | 214 | let (mut ws, stream) = WsMeta::connect( URL, None ).await.expect_throw( "Could not create websocket" ); 215 | let mut stream = stream.into_io(); 216 | 217 | let mut evts = ws.observe( ObserveConfig::default() ).await.expect( "observe" ); 218 | 219 | stream.close().await.expect_throw( "close ws" ); 220 | 221 | assert!( evts.next().await.unwrap_throw().is_closing() ); 222 | assert!( evts.next().await.unwrap_throw().is_closed() ); 223 | } 224 | 225 | 226 | 227 | // Verify Debug impl. 228 | // 229 | #[ wasm_bindgen_test ] 230 | // 231 | async fn debug() 232 | { 233 | let _ = console_log::init_with_level( Level::Trace ); 234 | 235 | info!( "starting test: debug" ); 236 | 237 | let (_ws, mut wsio) = WsMeta::connect( URL, None ).await.expect_throw( "Could not create websocket" ); 238 | 239 | assert_eq!( format!( "WsStream for connection: {URL}" ), format!( "{wsio:?}" ) ); 240 | 241 | wsio.close().await.expect_throw( "close" ); 242 | } 243 | --------------------------------------------------------------------------------