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..f5a22dd 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/test/lively/environment/application.rb b/test/lively/environment/application.rb index 5159180..4530521 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 + 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,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