├── .editorconfig ├── .github └── workflows │ ├── coverage.yaml │ ├── documentation.yaml │ ├── test-external.yaml │ └── test.yaml ├── .gitignore ├── .mailmap ├── config └── sus.rb ├── examples ├── echo_server │ ├── client.rb │ └── config.ru └── interactive_browser │ ├── client.rb │ └── config.ru ├── gems.rb ├── guides ├── getting-started │ └── readme.md └── links.yaml ├── lib └── roda │ ├── plugins │ └── websockets.rb │ ├── websockets.rb │ └── websockets │ └── version.rb ├── license.md ├── readme.md ├── release.cert ├── roda-websockets.gemspec └── test └── roda └── websockets.rb /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | indent_size = 2 6 | 7 | [*.{yml,yaml}] 8 | indent_style = space 9 | indent_size = 2 10 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yaml: -------------------------------------------------------------------------------- 1 | name: Coverage 2 | 3 | on: [push, pull_request] 4 | 5 | permissions: 6 | contents: read 7 | 8 | env: 9 | CONSOLE_OUTPUT: XTerm 10 | COVERAGE: PartialSummary 11 | 12 | jobs: 13 | test: 14 | name: ${{matrix.ruby}} on ${{matrix.os}} 15 | runs-on: ${{matrix.os}}-latest 16 | 17 | strategy: 18 | matrix: 19 | os: 20 | - ubuntu 21 | - macos 22 | 23 | ruby: 24 | - "3.3" 25 | 26 | steps: 27 | - uses: actions/checkout@v4 28 | - uses: ruby/setup-ruby@v1 29 | with: 30 | ruby-version: ${{matrix.ruby}} 31 | bundler-cache: true 32 | 33 | - name: Run tests 34 | timeout-minutes: 5 35 | run: bundle exec bake test 36 | 37 | - uses: actions/upload-artifact@v3 38 | with: 39 | name: coverage-${{matrix.os}}-${{matrix.ruby}} 40 | path: .covered.db 41 | 42 | validate: 43 | needs: test 44 | runs-on: ubuntu-latest 45 | 46 | steps: 47 | - uses: actions/checkout@v4 48 | - uses: ruby/setup-ruby@v1 49 | with: 50 | ruby-version: "3.3" 51 | bundler-cache: true 52 | 53 | - uses: actions/download-artifact@v3 54 | 55 | - name: Validate coverage 56 | timeout-minutes: 5 57 | run: bundle exec bake covered:validate --paths */.covered.db \; 58 | -------------------------------------------------------------------------------- /.github/workflows/documentation.yaml: -------------------------------------------------------------------------------- 1 | name: Documentation 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages: 9 | permissions: 10 | contents: read 11 | pages: write 12 | id-token: write 13 | 14 | # Allow one concurrent deployment: 15 | concurrency: 16 | group: "pages" 17 | cancel-in-progress: true 18 | 19 | env: 20 | CONSOLE_OUTPUT: XTerm 21 | BUNDLE_WITH: maintenance 22 | 23 | jobs: 24 | generate: 25 | runs-on: ubuntu-latest 26 | 27 | steps: 28 | - uses: actions/checkout@v4 29 | 30 | - uses: ruby/setup-ruby@v1 31 | with: 32 | ruby-version: "3.3" 33 | bundler-cache: true 34 | 35 | - name: Installing packages 36 | run: sudo apt-get install wget 37 | 38 | - name: Generate documentation 39 | timeout-minutes: 5 40 | run: bundle exec bake utopia:project:static --force no 41 | 42 | - name: Upload documentation artifact 43 | uses: actions/upload-pages-artifact@v3 44 | with: 45 | path: docs 46 | 47 | deploy: 48 | runs-on: ubuntu-latest 49 | 50 | environment: 51 | name: github-pages 52 | url: ${{steps.deployment.outputs.page_url}} 53 | 54 | needs: generate 55 | steps: 56 | - name: Deploy to GitHub Pages 57 | id: deployment 58 | uses: actions/deploy-pages@v4 59 | -------------------------------------------------------------------------------- /.github/workflows/test-external.yaml: -------------------------------------------------------------------------------- 1 | name: Test External 2 | 3 | on: [push, pull_request] 4 | 5 | permissions: 6 | contents: read 7 | 8 | env: 9 | CONSOLE_OUTPUT: XTerm 10 | 11 | jobs: 12 | test: 13 | name: ${{matrix.ruby}} on ${{matrix.os}} 14 | runs-on: ${{matrix.os}}-latest 15 | 16 | strategy: 17 | matrix: 18 | os: 19 | - ubuntu 20 | - macos 21 | 22 | ruby: 23 | - "3.1" 24 | - "3.2" 25 | - "3.3" 26 | 27 | steps: 28 | - uses: actions/checkout@v4 29 | - uses: ruby/setup-ruby@v1 30 | with: 31 | ruby-version: ${{matrix.ruby}} 32 | bundler-cache: true 33 | 34 | - name: Run tests 35 | timeout-minutes: 10 36 | run: bundle exec bake test:external 37 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push, pull_request] 4 | 5 | permissions: 6 | contents: read 7 | 8 | env: 9 | CONSOLE_OUTPUT: XTerm 10 | 11 | jobs: 12 | test: 13 | name: ${{matrix.ruby}} on ${{matrix.os}} 14 | runs-on: ${{matrix.os}}-latest 15 | continue-on-error: ${{matrix.experimental}} 16 | 17 | strategy: 18 | matrix: 19 | os: 20 | - ubuntu 21 | - macos 22 | 23 | ruby: 24 | - "3.1" 25 | - "3.2" 26 | - "3.3" 27 | 28 | experimental: [false] 29 | 30 | include: 31 | - os: ubuntu 32 | ruby: truffleruby 33 | experimental: true 34 | - os: ubuntu 35 | ruby: jruby 36 | experimental: true 37 | - os: ubuntu 38 | ruby: head 39 | experimental: true 40 | 41 | steps: 42 | - uses: actions/checkout@v4 43 | - uses: ruby/setup-ruby@v1 44 | with: 45 | ruby-version: ${{matrix.ruby}} 46 | bundler-cache: true 47 | 48 | - name: Run tests 49 | timeout-minutes: 10 50 | run: bundle exec bake test 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /pkg/ 3 | /gems.locked 4 | /.covered.db 5 | /external 6 | -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | Jeffrey Lim -------------------------------------------------------------------------------- /config/sus.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2024, by Samuel Williams. 5 | 6 | require 'covered/sus' 7 | include Covered::Sus 8 | -------------------------------------------------------------------------------- /examples/echo_server/client.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2019, by Shannon Skipper. 5 | # Copyright, 2024, by Samuel Williams. 6 | 7 | require 'async' 8 | require 'async/http/endpoint' 9 | require 'async/websocket/client' 10 | 11 | module Client 12 | URL = 'https://localhost:9292' 13 | ENDPOINT = Async::HTTP::Endpoint.parse URL 14 | MESSAGE = ARGV.first || 'ping' 15 | 16 | module_function 17 | 18 | def call 19 | Async do 20 | Async::WebSocket::Client.connect ENDPOINT do |connection| 21 | puts "Sending message: #{MESSAGE}" 22 | connection.write MESSAGE 23 | connection.flush 24 | 25 | message = connection.read 26 | puts "Receiving message: #{message}" 27 | end 28 | end 29 | end 30 | end 31 | 32 | Client.call 33 | -------------------------------------------------------------------------------- /examples/echo_server/config.ru: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S falcon --count 1 --config 2 | # frozen_string_literal: true 3 | 4 | require 'roda' 5 | 6 | class App < Roda 7 | plugin :websockets 8 | 9 | def on_message(connection) 10 | Async do |task| 11 | message = connection.read 12 | 13 | sleep(3) # Async I/O here 14 | 15 | connection.write(message) 16 | connection.flush 17 | connection.close 18 | end 19 | end 20 | 21 | route do |r| 22 | r.is '' do 23 | r.websocket do |connection| 24 | on_message(connection).wait 25 | end 26 | end 27 | end 28 | end 29 | 30 | run App.freeze.app 31 | -------------------------------------------------------------------------------- /examples/interactive_browser/client.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2019, by Shannon Skipper. 5 | # Copyright, 2024, by Samuel Williams. 6 | 7 | require 'async' 8 | require 'async/http/endpoint' 9 | require 'async/websocket/client' 10 | 11 | module Client 12 | URL = 'https://localhost:9292' 13 | ENDPOINT = Async::HTTP::Endpoint.parse URL 14 | TIMEOUT = 15 15 | REPEAT = 3 16 | 17 | module_function 18 | 19 | def call 20 | Async do |task| 21 | task.with_timeout TIMEOUT do 22 | Async::WebSocket::Client.connect ENDPOINT do |connection| 23 | tasks = REPEAT.times.map do 24 | task.async do 25 | pinapple_count = rand 2..12 26 | puts "Sending message: #{pinapple_count}" 27 | connection.write pinapple_count 28 | connection.flush 29 | end 30 | end 31 | 32 | while (message = connection.read) 33 | puts "Receiving message: #{message}" 34 | end 35 | rescue Async::TimeoutError 36 | connection.close 37 | ensure 38 | tasks.each(&:stop) 39 | end 40 | end 41 | end 42 | end 43 | end 44 | 45 | Client.call 46 | -------------------------------------------------------------------------------- /examples/interactive_browser/config.ru: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S falcon serve --count 1 --config 2 | # frozen_string_literal: true 3 | 4 | require 'roda' 5 | 6 | ## 7 | # Run this example with `ruby client.rb` or browse to https://localhost:9292. 8 | class App < Roda 9 | # Roda usually extracts HTML to separate files, but we'll inline it here. 10 | BODY = <<~HTML 11 | 12 | 13 | 14 | 15 | WebSockets Example 16 | 17 | 18 | 36 | 37 | 38 | HTML 39 | 40 | plugin :websockets 41 | 42 | def on_message(connection, pineapple_count:) 43 | Async do |task| 44 | connection.write "Eating #{pineapple_count.buffer} pineapples." 45 | connection.flush 46 | 47 | pineapple_count.buffer.to_i.downto(1) do |n| 48 | task.sleep 1 49 | connection.write '🍍' * n 50 | connection.flush 51 | end 52 | task.sleep 1 53 | 54 | connection.write "Ate #{pineapple_count.buffer} pineapples." 55 | connection.flush 56 | end 57 | end 58 | 59 | def messages(connection) 60 | Enumerator.new do |yielder| 61 | loop do 62 | message = connection.read 63 | break unless message 64 | 65 | yielder << message 66 | end 67 | end 68 | end 69 | 70 | route do |r| 71 | r.is '' do 72 | r.websocket do |connection| 73 | messages(connection).each do |message| 74 | on_message(connection, pineapple_count: message) 75 | end 76 | end 77 | 78 | r.get do 79 | BODY 80 | end 81 | end 82 | end 83 | end 84 | 85 | run App.freeze.app 86 | -------------------------------------------------------------------------------- /gems.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2019, by Shannon Skipper. 5 | # Copyright, 2024, by Samuel Williams. 6 | 7 | source 'https://rubygems.org' 8 | 9 | gemspec 10 | 11 | group :maintenance, optional: true do 12 | gem "bake-gem" 13 | gem "bake-modernize" 14 | 15 | gem "utopia-project" 16 | end 17 | 18 | group :test do 19 | gem "sus" 20 | gem "covered" 21 | 22 | gem "sus-fixtures-async" 23 | gem "sus-fixtures-async-http" 24 | 25 | gem "bake-test" 26 | gem "bake-test-external" 27 | end 28 | -------------------------------------------------------------------------------- /guides/getting-started/readme.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | This guide will help you get started with the `roda-websockets` plugin. 4 | 5 | ## Installation 6 | 7 | Add the gem to your project: 8 | 9 | ``` sh 10 | $ bundle add roda-websockets 11 | ``` 12 | 13 | ## Usage 14 | 15 | `roda-websockets` requires that you use [Falcon](https://github.com/socketry/falcon) as your web server in order to establish asynchronous websocket connections. 16 | 17 | ``` bash 18 | $ falcon serve --count 1 19 | ``` 20 | 21 | `roda-websockets` is a roda plugin, so you need to load it into your roda application similar to other plugins: 22 | 23 | ``` ruby 24 | class App < Roda 25 | plugin :websockets 26 | end 27 | ``` 28 | 29 | In your routing block, you can use `r.websocket` to pass the routing to a websocket connection. 30 | 31 | ``` ruby 32 | r.websocket do |connection| 33 | # A simple echo server: 34 | while message = connection.read 35 | connection.write(message) 36 | end 37 | end 38 | ``` 39 | -------------------------------------------------------------------------------- /guides/links.yaml: -------------------------------------------------------------------------------- 1 | getting-started: 2 | order: 1 3 | -------------------------------------------------------------------------------- /lib/roda/plugins/websockets.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2019, by Shannon Skipper. 5 | # Copyright, 2024, by Samuel Williams. 6 | 7 | require 'async/websocket/adapters/rack' 8 | 9 | class Roda 10 | module RodaPlugins 11 | # The websockets plugin integrates the async-websocket gem into Roda's 12 | # routing tree. See the 13 | # (async-websocket docs)[https://github.com/socketry/async-websocket] 14 | # for usage details. 15 | module WebSockets 16 | module RequestMethods 17 | ARGS = {}.freeze 18 | 19 | def websocket? 20 | ::Async::WebSocket::Adapters::Rack.websocket?(env) 21 | end 22 | 23 | def websocket(args = ARGS, &block) 24 | return unless websocket? 25 | 26 | always do 27 | halt ::Async::WebSocket::Adapters::Rack.open(env, *args, &block) 28 | end 29 | end 30 | end 31 | end 32 | 33 | register_plugin(:websockets, WebSockets) 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/roda/websockets.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2024, by Samuel Williams. 5 | 6 | require_relative 'plugins/websockets' 7 | -------------------------------------------------------------------------------- /lib/roda/websockets/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2024, by Samuel Williams. 5 | 6 | class Roda 7 | module WebSockets 8 | VERSION = '0.1.0' 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright, 2019, by Shannon Skipper. 4 | Copyright, 2024, by Jeffrey Lim. 5 | Copyright, 2024, by Samuel Williams. 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # roda-websockets 2 | 3 | The roda-websockets gem integrates [async-websockets](https://github.com/socketry/async-websocket) into the [roda](http://roda.jeremyevans.net/) web toolkit. Use this plugin for asynchronous websockets alongside roda and falcon. 4 | 5 | [![Development Status](https://github.com/socketry/roda-websockets/workflows/Test/badge.svg)](https://github.com/socketry/roda-websockets/actions?workflow=Test) 6 | 7 | ## Usage 8 | 9 | Please see the [project documentation](https://socketry.github.io/roda-websockets/) for more details. 10 | 11 | - [Getting Started](https://socketry.github.io/roda-websockets/guides/getting-started/index) - This guide will help you get started with the `roda-websockets` plugin. 12 | 13 | ## Contributing 14 | 15 | We welcome contributions to this project. 16 | 17 | 1. Fork it. 18 | 2. Create your feature branch (`git checkout -b my-new-feature`). 19 | 3. Commit your changes (`git commit -am 'Add some feature'`). 20 | 4. Push to the branch (`git push origin my-new-feature`). 21 | 5. Create new Pull Request. 22 | 23 | ### Developer Certificate of Origin 24 | 25 | This project uses the [Developer Certificate of Origin](https://developercertificate.org/). All contributors to this project must agree to this document to have their contributions accepted. 26 | 27 | ### Contributor Covenant 28 | 29 | This project is governed by the [Contributor Covenant](https://www.contributor-covenant.org/). All contributors and participants agree to abide by its terms. 30 | -------------------------------------------------------------------------------- /release.cert: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIE2DCCA0CgAwIBAgIBATANBgkqhkiG9w0BAQsFADBhMRgwFgYDVQQDDA9zYW11 3 | ZWwud2lsbGlhbXMxHTAbBgoJkiaJk/IsZAEZFg1vcmlvbnRyYW5zZmVyMRIwEAYK 4 | CZImiZPyLGQBGRYCY28xEjAQBgoJkiaJk/IsZAEZFgJuejAeFw0yMjA4MDYwNDUz 5 | MjRaFw0zMjA4MDMwNDUzMjRaMGExGDAWBgNVBAMMD3NhbXVlbC53aWxsaWFtczEd 6 | MBsGCgmSJomT8ixkARkWDW9yaW9udHJhbnNmZXIxEjAQBgoJkiaJk/IsZAEZFgJj 7 | bzESMBAGCgmSJomT8ixkARkWAm56MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIB 8 | igKCAYEAomvSopQXQ24+9DBB6I6jxRI2auu3VVb4nOjmmHq7XWM4u3HL+pni63X2 9 | 9qZdoq9xt7H+RPbwL28LDpDNflYQXoOhoVhQ37Pjn9YDjl8/4/9xa9+NUpl9XDIW 10 | sGkaOY0eqsQm1pEWkHJr3zn/fxoKPZPfaJOglovdxf7dgsHz67Xgd/ka+Wo1YqoE 11 | e5AUKRwUuvaUaumAKgPH+4E4oiLXI4T1Ff5Q7xxv6yXvHuYtlMHhYfgNn8iiW8WN 12 | XibYXPNP7NtieSQqwR/xM6IRSoyXKuS+ZNGDPUUGk8RoiV/xvVN4LrVm9upSc0ss 13 | RZ6qwOQmXCo/lLcDUxJAgG95cPw//sI00tZan75VgsGzSWAOdjQpFM0l4dxvKwHn 14 | tUeT3ZsAgt0JnGqNm2Bkz81kG4A2hSyFZTFA8vZGhp+hz+8Q573tAR89y9YJBdYM 15 | zp0FM4zwMNEUwgfRzv1tEVVUEXmoFCyhzonUUw4nE4CFu/sE3ffhjKcXcY//qiSW 16 | xm4erY3XAgMBAAGjgZowgZcwCQYDVR0TBAIwADALBgNVHQ8EBAMCBLAwHQYDVR0O 17 | BBYEFO9t7XWuFf2SKLmuijgqR4sGDlRsMC4GA1UdEQQnMCWBI3NhbXVlbC53aWxs 18 | aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MC4GA1UdEgQnMCWBI3NhbXVlbC53aWxs 19 | aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MA0GCSqGSIb3DQEBCwUAA4IBgQB5sxkE 20 | cBsSYwK6fYpM+hA5B5yZY2+L0Z+27jF1pWGgbhPH8/FjjBLVn+VFok3CDpRqwXCl 21 | xCO40JEkKdznNy2avOMra6PFiQyOE74kCtv7P+Fdc+FhgqI5lMon6tt9rNeXmnW/ 22 | c1NaMRdxy999hmRGzUSFjozcCwxpy/LwabxtdXwXgSay4mQ32EDjqR1TixS1+smp 23 | 8C/NCWgpIfzpHGJsjvmH2wAfKtTTqB9CVKLCWEnCHyCaRVuKkrKjqhYCdmMBqCws 24 | JkxfQWC+jBVeG9ZtPhQgZpfhvh+6hMhraUYRQ6XGyvBqEUe+yo6DKIT3MtGE2+CP 25 | eX9i9ZWBydWb8/rvmwmX2kkcBbX0hZS1rcR593hGc61JR6lvkGYQ2MYskBveyaxt 26 | Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8 27 | voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg= 28 | -----END CERTIFICATE----- 29 | -------------------------------------------------------------------------------- /roda-websockets.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "lib/roda/websockets/version" 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = "roda-websockets" 7 | spec.version = Roda::WebSockets::VERSION 8 | 9 | spec.summary = "WebSocket integration for Roda" 10 | spec.authors = ["Shannon Skipper", "Jeffrey Lim", "Samuel Williams"] 11 | spec.license = "MIT" 12 | 13 | spec.cert_chain = ['release.cert'] 14 | spec.signing_key = File.expand_path('~/.gem/release.pem') 15 | 16 | spec.homepage = "https://github.com/socketry/roda-websockets" 17 | 18 | spec.metadata = { 19 | "documentation_uri" => "https://socketry.github.io/roda-websockets/", 20 | "source_code_uri" => "https://github.com/socketry/roda-websockets.git", 21 | } 22 | 23 | spec.files = Dir['{lib}/**/*', '*.md', base: __dir__] 24 | 25 | spec.required_ruby_version = ">= 3.1" 26 | 27 | spec.add_dependency "async-websocket", "~> 0.12" 28 | spec.add_dependency "falcon", "~> 0.33" 29 | spec.add_dependency "roda", "~> 3.0" 30 | end 31 | -------------------------------------------------------------------------------- /test/roda/websockets.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2019, by Shannon Skipper. 5 | # Copyright, 2024, by Samuel Williams. 6 | 7 | require 'async/websocket/client' 8 | require 'sus/fixtures/async/reactor_context' 9 | require 'sus/fixtures/async/http/server_context' 10 | 11 | require 'roda' 12 | require 'roda/websockets' 13 | 14 | describe Roda::WebSockets do 15 | include Sus::Fixtures::Async::ReactorContext 16 | include Sus::Fixtures::Async::HTTP::ServerContext 17 | 18 | let(:roda) do 19 | Class.new(::Roda).tap do |roda| 20 | roda.plugin :websockets 21 | 22 | roda.route do |r| 23 | r.root do 24 | r.websocket do |connection| 25 | %w[zxc spqr wombat].each do |message| 26 | connection.write(message) 27 | connection.flush 28 | end 29 | 30 | connection.close 31 | end 32 | 33 | 'bar' 34 | end 35 | end 36 | end 37 | end 38 | 39 | let(:app) do 40 | Protocol::Rack::Adapter.new(roda) 41 | end 42 | 43 | it 'supports regular requests' do 44 | response = client.get('/') 45 | expect(response.read).to be == 'bar' 46 | end 47 | 48 | it 'supports websocket requests' do 49 | Async::WebSocket::Client.connect(client_endpoint) do |connection| 50 | expect(connection.read).to be == 'zxc' 51 | expect(connection.read).to be == 'spqr' 52 | expect(connection.read).to be == 'wombat' 53 | end 54 | end 55 | end 56 | --------------------------------------------------------------------------------