From 180d7807319fc1767d4e85c6dac6ad895755b1ba Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Tue, 19 May 2026 19:10:11 +0900 Subject: [PATCH 1/6] Add HTTY transport support and refactor environment layer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add `Async::HTTY` transport support: when `ENV["HTTY"] == "1"`, the server runs over a terminal side-channel instead of a TCP socket. - Refactor environment modules into focused layers: - `Lively::Environment::Middleware` — shared application/asset stack - `Lively::Environment::HTTP` — Falcon TCP transport + Middleware - `Lively::Environment::HTTY` — HTTY transport + Middleware - `Lively::Environment::Application` — multiplexes via `make_service`, using the new `Async::Service::Generic#make_service` hook (requires async-service ~> 0.23) - Force `Async::Container::Threaded` in `bin/lively` — no forking needed for a single-user dev tool. - Fix `Live.js` WebSocket URL construction to handle `htty:` scheme, mapping it to `ws:` alongside the existing `http:` → `ws:` mapping. - Fix `Lively::Assets` missing `require "uri"` (was implicit via Falcon). Co-authored-by: Cursor --- bin/lively | 5 +- lib/lively/assets.rb | 1 + lib/lively/environment/application.rb | 84 +++++++++++++---------- lib/lively/environment/http.rb | 34 +++++++++ lib/lively/environment/htty.rb | 22 ++++++ lib/lively/environment/middleware.rb | 48 +++++++++++++ lively.gemspec | 2 + public/_components/@socketry/live/Live.js | 2 +- 8 files changed, 159 insertions(+), 39 deletions(-) create mode 100644 lib/lively/environment/http.rb create mode 100644 lib/lively/environment/htty.rb create mode 100644 lib/lively/environment/middleware.rb diff --git a/bin/lively b/bin/lively index 39268b1..c1e9d71 100755 --- a/bin/lively +++ b/bin/lively @@ -2,6 +2,7 @@ # frozen_string_literal: true require "async/service" +require "async/container/threaded" require_relative "../lib/lively/environment/application" ARGV.each do |path| @@ -11,7 +12,7 @@ end configuration = Async::Service::Configuration.build do service "lively" do include Lively::Environment::Application - end + end end -Async::Service::Controller.run(configuration) +Async::Service::Controller.run(configuration, container_class: Async::Container::Threaded) diff --git a/lib/lively/assets.rb b/lib/lively/assets.rb index 7d1097e..881ddc6 100644 --- a/lib/lively/assets.rb +++ b/lib/lively/assets.rb @@ -3,6 +3,7 @@ # Released under the MIT License. # Copyright, 2021-2025, by Samuel Williams. +require "uri" require "protocol/http/middleware" require "protocol/http/body/file" require "console" diff --git a/lib/lively/environment/application.rb b/lib/lively/environment/application.rb index 5f6ac95..0f7b2f9 100644 --- a/lib/lively/environment/application.rb +++ b/lib/lively/environment/application.rb @@ -1,56 +1,68 @@ # frozen_string_literal: true # Released under the MIT License. -# Copyright, 2021-2025, by Samuel Williams. +# Copyright, 2021-2026, by Samuel Williams. -require_relative "../application" -require_relative "../assets" - -require "falcon/environment/server" +require_relative "middleware" +require_relative "http" +require_relative "htty" # @namespace module Lively # @namespace module Environment - # Represents the environment configuration for a Lively application server. - # - # This module provides server configuration including URL binding, process count, - # application class resolution, and middleware stack setup. It integrates with - # Falcon's server environment to provide a complete hosting solution. + # Multiplexing environment for Lively applications. + # + # Declares the transport selection as explicit, overridable evaluator keys and uses `make_service` to compose the appropriate child environment at service startup time. This keeps transport selection in the service layer rather than in module inclusion hooks. + # + # The `htty` key controls which transport is used. Override it in a service block to force a specific transport regardless of the environment variable: + # + # ~~~ ruby + # service "myapp" do + # include Lively::Environment::Application + # def htty = false # always use HTTP + # end + # ~~~ module Application - include Falcon::Environment::Server + include Lively::Environment::Middleware + # Note: does not include Falcon::Environment::Server directly. Falcon is + # brought in exclusively via http_environment so that the combined + # evaluator's service_class resolves correctly without shadowing. + + # Whether to use HTTY transport. Reads ENV["HTTY"] by default. + # @returns [Boolean] + def htty + ENV["HTTY"] == "1" + end - # Get the server URL for this application. - # @returns [String] The base URL where the server will be accessible. - def url - "http://localhost:9292" + # The environment module to use for HTTY transport. + # @returns [Module] + def htty_environment + Lively::Environment::HTTY end - # Get the number of server processes to run. - # @returns [Integer] The number of worker processes. - def count - 1 + # The environment module to use for HTTP transport. + # @returns [Module] + def http_environment + Lively::Environment::HTTP end - # Resolve the application class to use. - # @returns [Class] The application class, either user-defined or default. - def application - if Object.const_defined?(:Application) - Object.const_get(:Application) - else - Console.warn(self, "No Application class defined, using default.") - ::Lively::Application - end + # The environment module for the selected transport. + # @returns [Module] + def transport_environment + htty ? htty_environment : http_environment end - # Build the middleware stack for this application. - # @returns [Protocol::HTTP::Middleware] The complete middleware stack. - def middleware - ::Protocol::HTTP::Middleware.build do |builder| - builder.use Lively::Assets, root: File.expand_path("public", self.root) - builder.use Lively::Assets, root: File.expand_path("../../../public", __dir__) - builder.use self.application - end + # Build the service by composing the transport environment on top of this one. + # Called by Async::Service::Generic.wrap — self is the evaluator at call time. + # @parameter environment [Async::Service::Environment] + # @returns [Async::Service::Generic] + def make_service(environment) + combined = environment.with(transport_environment) + combined_evaluator = combined.evaluator + + # Call `service_class.new` directly rather than `Async::Service::Generic.wrap` — the combined evaluator still has `Application` (and therefore `make_service`) in its ancestor chain, so `wrap` would recurse back into this method. + return combined_evaluator.service_class.new(combined, combined_evaluator) end end end diff --git a/lib/lively/environment/http.rb b/lib/lively/environment/http.rb new file mode 100644 index 0000000..61a229e --- /dev/null +++ b/lib/lively/environment/http.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2021-2026, by Samuel Williams. + +require_relative "middleware" +require "falcon/environment/server" + +# @namespace +module Lively + # @namespace + module Environment + # Falcon (TCP/HTTP) environment for Lively applications. + # + # Combines {Falcon::Environment::Server} for HTTP transport with + # {Lively::Environment::Middleware} for application and asset serving. + module HTTP + include Falcon::Environment::Server + include Lively::Environment::Middleware + + # The URL this server binds to. + # @returns [String] + def url + ENV.fetch("LIVELY_URL", "http://localhost:9292") + end + + # The number of worker processes/threads to run. + # @returns [Integer] + def count + 1 + end + end + end +end diff --git a/lib/lively/environment/htty.rb b/lib/lively/environment/htty.rb new file mode 100644 index 0000000..c652035 --- /dev/null +++ b/lib/lively/environment/htty.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2026, by Samuel Williams. + +require_relative "middleware" +require "async/htty/environment/server" + +# @namespace +module Lively + # @namespace + module Environment + # HTTY (terminal side-channel) environment for Lively applications. + # + # Combines {Async::HTTY::Environment} for HTTY transport with + # {Lively::Environment::Middleware} for application and asset serving. + module HTTY + include Async::HTTY::Environment::Server + include Lively::Environment::Middleware + end + end +end diff --git a/lib/lively/environment/middleware.rb b/lib/lively/environment/middleware.rb new file mode 100644 index 0000000..c36b6fd --- /dev/null +++ b/lib/lively/environment/middleware.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2021-2026, by Samuel Williams. + +require_relative "../application" +require_relative "../assets" + +require "protocol/http/middleware/builder" + +# @namespace +module Lively + # @namespace + module Environment + # Shared middleware configuration for Lively application environments. + # + # Provides the application class resolver, asset middleware, and the + # Lively middleware stack. Included by both {HTTP} and {HTTY} environments. + module Middleware + # Get the root directory for this application. + # @returns [String] The current working directory. + def root + Dir.pwd + end + + # Resolve the application class to use. + # @returns [Class] The application class, either user-defined or default. + def application + if Object.const_defined?(:Application) + Object.const_get(:Application) + else + Console.warn(self, "No Application class defined, using default.") + ::Lively::Application + end + end + + # Build the middleware stack for this application. + # @returns [Protocol::HTTP::Middleware] The complete middleware stack. + def middleware + ::Protocol::HTTP::Middleware.build do |builder| + builder.use Lively::Assets, root: File.expand_path("public", self.root) + builder.use Lively::Assets, root: File.expand_path("../../../public", __dir__) + builder.use self.application + end + end + end + end +end diff --git a/lively.gemspec b/lively.gemspec index 2fab680..b85c483 100644 --- a/lively.gemspec +++ b/lively.gemspec @@ -27,6 +27,8 @@ Gem::Specification.new do |spec| spec.required_ruby_version = ">= 3.3" spec.add_dependency "agent-context" + spec.add_dependency "async-htty" + spec.add_dependency "async-service", "~> 0.23" spec.add_dependency "falcon", "~> 0.47" spec.add_dependency "io-watch" spec.add_dependency "live", "~> 0.18" diff --git a/public/_components/@socketry/live/Live.js b/public/_components/@socketry/live/Live.js index 89031ed..527f3c5 100644 --- a/public/_components/@socketry/live/Live.js +++ b/public/_components/@socketry/live/Live.js @@ -44,7 +44,7 @@ export class Live { let base = options.base || window.location.href; let url = new URL(path, base); - url.protocol = url.protocol.replace('http', 'ws'); + url.protocol = url.protocol.replace(/^(htty|http)(s?):$/, 'ws$2:'); window.live = new this(window, url); if (!window.customElements.get('live-view')) { From 1a006132a5bfb6294c2dfcda6ef8b3d84364c365 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Tue, 19 May 2026 19:18:08 +0900 Subject: [PATCH 2/6] Fix tests and rubocop for new environment layer Co-authored-by: Cursor --- lib/lively/environment/application.rb | 2 +- test/lively/environment/application.rb | 45 ++++++++++---------------- 2 files changed, 18 insertions(+), 29 deletions(-) diff --git a/lib/lively/environment/application.rb b/lib/lively/environment/application.rb index 0f7b2f9..f5a22dd 100644 --- a/lib/lively/environment/application.rb +++ b/lib/lively/environment/application.rb @@ -60,7 +60,7 @@ def transport_environment def make_service(environment) combined = environment.with(transport_environment) combined_evaluator = combined.evaluator - + # Call `service_class.new` directly rather than `Async::Service::Generic.wrap` — the combined evaluator still has `Application` (and therefore `make_service`) in its ancestor chain, so `wrap` would recurse back into this method. return combined_evaluator.service_class.new(combined, combined_evaluator) end diff --git a/test/lively/environment/application.rb b/test/lively/environment/application.rb index 5159180..41251dc 100644 --- a/test/lively/environment/application.rb +++ b/test/lively/environment/application.rb @@ -13,23 +13,28 @@ let(:environment) {Async::Service::Environment.build(subject, root: __dir__)} let(:evaluator) {environment.evaluator} - with "module methods" do - it "provides default URL" do - expect(evaluator.url).to be == "http://localhost:9292" + with "transport selection" do + it "selects HTTP transport by default" do + expect(evaluator.transport_environment).to be == Lively::Environment::HTTP end - it "provides default count" do - expect(evaluator.count).to be == 1 + it "selects HTTY transport when htty is true" do + environment = Async::Service::Environment.build(subject, root: __dir__) do + def htty = true + end + expect(environment.evaluator.transport_environment).to be == Lively::Environment::HTTY end + end + + with "transport evaluator" do + let(:transport_evaluator) {environment.with(evaluator.transport_environment).evaluator} - it "provides default application class" do - application_class = evaluator.application - expect(application_class).to be == Lively::Application + it "provides default URL" do + expect(transport_evaluator.url).to be == "http://localhost:9292" end - it "provides middleware stack" do - middleware = evaluator.middleware - expect(middleware).to be_a(Protocol::HTTP::Middleware) + it "provides default count" do + expect(transport_evaluator.count).to be == 1 end end @@ -67,24 +72,8 @@ end with "middleware configuration" do - it "includes Assets middleware for public directory" do - middleware = evaluator.middleware - - # The middleware should be configured with Assets - expect(middleware).to be_a(Protocol::HTTP::Middleware) - end - - it "includes Assets middleware for gem public directory" do - middleware = evaluator.middleware - - # Should include the gem's public directory assets - expect(middleware).to be_a(Protocol::HTTP::Middleware) - end - - it "includes application middleware" do + it "provides middleware stack" do middleware = evaluator.middleware - - # Should include the application class in the middleware stack expect(middleware).to be_a(Protocol::HTTP::Middleware) end end From 3320777f0faebb2061a58a2266d92884cffdf2e7 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Tue, 19 May 2026 19:21:39 +0900 Subject: [PATCH 3/6] =?UTF-8?q?Fix=20HTTY=20transport=20selection=20test?= =?UTF-8?q?=20=E2=80=94=20use=20builder=20DSL=20not=20def=20syntax?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- test/lively/environment/application.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/lively/environment/application.rb b/test/lively/environment/application.rb index 41251dc..ceb0d32 100644 --- a/test/lively/environment/application.rb +++ b/test/lively/environment/application.rb @@ -20,7 +20,7 @@ it "selects HTTY transport when htty is true" do environment = Async::Service::Environment.build(subject, root: __dir__) do - def htty = true + htty true end expect(environment.evaluator.transport_environment).to be == Lively::Environment::HTTY end From cb573622d33409cd603eb4beb932fdf5f040585f Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Tue, 19 May 2026 22:00:52 +0900 Subject: [PATCH 4/6] Revert Live.js protocol change Co-authored-by: Cursor --- public/_components/@socketry/live/Live.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/_components/@socketry/live/Live.js b/public/_components/@socketry/live/Live.js index 527f3c5..89031ed 100644 --- a/public/_components/@socketry/live/Live.js +++ b/public/_components/@socketry/live/Live.js @@ -44,7 +44,7 @@ export class Live { let base = options.base || window.location.href; let url = new URL(path, base); - url.protocol = url.protocol.replace(/^(htty|http)(s?):$/, 'ws$2:'); + url.protocol = url.protocol.replace('http', 'ws'); window.live = new this(window, url); if (!window.customElements.get('live-view')) { From dfff7e6fe6b8e9b09750afdb905ea10b1784b3a3 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Tue, 19 May 2026 22:03:44 +0900 Subject: [PATCH 5/6] Restore middleware configuration tests Co-authored-by: Cursor --- test/lively/environment/application.rb | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/test/lively/environment/application.rb b/test/lively/environment/application.rb index ceb0d32..5b3705b 100644 --- a/test/lively/environment/application.rb +++ b/test/lively/environment/application.rb @@ -72,8 +72,24 @@ end with "middleware configuration" do - it "provides middleware stack" do + it "includes Assets middleware for public directory" do middleware = evaluator.middleware + + # The middleware should be configured with Assets + expect(middleware).to be_a(Protocol::HTTP::Middleware) + end + + it "includes Assets middleware for gem public directory" do + middleware = evaluator.middleware + + # Should include the gem's public directory assets + expect(middleware).to be_a(Protocol::HTTP::Middleware) + end + + it "includes application middleware" do + middleware = evaluator.middleware + + # Should include the application class in the middleware stack expect(middleware).to be_a(Protocol::HTTP::Middleware) end end From 06255efa000a57df363b006eaefbf59e406a756b Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Tue, 19 May 2026 22:04:50 +0900 Subject: [PATCH 6/6] Collapse duplicate middleware configuration tests Co-authored-by: Cursor --- test/lively/environment/application.rb | 21 ++------------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/test/lively/environment/application.rb b/test/lively/environment/application.rb index 5b3705b..4530521 100644 --- a/test/lively/environment/application.rb +++ b/test/lively/environment/application.rb @@ -72,25 +72,8 @@ end with "middleware configuration" do - it "includes Assets middleware for public directory" do - middleware = evaluator.middleware - - # The middleware should be configured with Assets - expect(middleware).to be_a(Protocol::HTTP::Middleware) - end - - it "includes Assets middleware for gem public directory" do - middleware = evaluator.middleware - - # Should include the gem's public directory assets - expect(middleware).to be_a(Protocol::HTTP::Middleware) - end - - it "includes application middleware" do - middleware = evaluator.middleware - - # Should include the application class in the middleware stack - expect(middleware).to be_a(Protocol::HTTP::Middleware) + it "provides a middleware stack" do + expect(evaluator.middleware).to be_a(Protocol::HTTP::Middleware) end end end