lively 0.17.1 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c0dda7cfa299aaa995ebe540f947ab24a4f2d0f3e8dab116e064f0af1d545075
4
- data.tar.gz: 1ea180ab2040f3b5b7ae191fa4f731376e366b7cf658de422d2289d929078de6
3
+ metadata.gz: d4f90097ca71e02dffa056e9251b96a63f0d45262ad5eddd0f073d2d02d42b6e
4
+ data.tar.gz: 214a19575d3f3fa624a3d71494b888feccebc18774fa1e38255ec4ceb90c84a6
5
5
  SHA512:
6
- metadata.gz: d2bc8606510afc7f5dd935bd46f3c2bb7613b9ba8a3301cb290f27b50aa5c14b65c670e09bf5a109fd41e0688542169997a7200295c64bfa9378ecf2bd685c9d
7
- data.tar.gz: 327e99802f36ec9f354e5ad4195850e0e0ee7bfae7a9372b38f4442841500e7e94d80a13869096797e093da5ba8f5d37f04811dd801e854fb3f10e83127d9866
6
+ metadata.gz: 23cc87763d59471eaabdfee09f234f389c3576f7bb6d59df85f65e0205d7f6244710c20cfed77c3e0543513f4db581664e776062383c9a05491673ca8bfc88b8
7
+ data.tar.gz: 5b905206f71fcea3f15da3f6db3f0e8fcd9f1bd85f06278654d621affb4d72cf42828b828a1f6821602a98f8af5b8441fe701a4785a7626b4327f972dad8b03d
checksums.yaml.gz.sig CHANGED
Binary file
data/bin/lively CHANGED
@@ -2,6 +2,7 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "async/service"
5
+ require "async/container/threaded"
5
6
  require_relative "../lib/lively/environment/application"
6
7
 
7
8
  ARGV.each do |path|
@@ -11,7 +12,7 @@ end
11
12
  configuration = Async::Service::Configuration.build do
12
13
  service "lively" do
13
14
  include Lively::Environment::Application
14
- end
15
+ end
15
16
  end
16
17
 
17
- Async::Service::Controller.run(configuration)
18
+ Async::Service::Controller.run(configuration, container_class: Async::Container::Threaded)
@@ -99,7 +99,7 @@ Application = Lively::Application[GameView, game_state: GameState.new]
99
99
 
100
100
  The `game_state:` keyword is passed to every `GameView` instance — whether created by the initial page load or by a WebSocket reconnection. This means all connected browsers share the same `GameState` object.
101
101
 
102
- For more complex applications, subclass {ruby Lively::Application} and override `#state`, `#allowed_views`, and `#body`:
102
+ For more complex applications, subclass {ruby Lively::Application} and add routes with `#configure_routes`:
103
103
 
104
104
  ```ruby
105
105
  class Application < Lively::Application
@@ -116,26 +116,34 @@ class Application < Lively::Application
116
116
  super
117
117
  end
118
118
 
119
- def body(request)
120
- case request.path
121
- when "/"
122
- DisplayView.new(**state)
123
- when "/control"
124
- ControlView.new(**state)
119
+ def configure_routes(router)
120
+ super
121
+
122
+ router.get("/") do
123
+ render(DisplayView.new(**state))
124
+ end
125
+
126
+ router.get("/control") do |_request, parameters|
127
+ render(ControlView.new(**state, mode: parameters["mode"]))
125
128
  end
126
129
  end
127
130
 
131
+ # Unmatched routes are passed here:
128
132
  def handle(request)
129
- if body = self.body(request)
130
- page = Lively::Pages::Index.new(title: "My App", body: body)
131
- Protocol::HTTP::Response[200, [], [page.call]]
132
- else
133
- Protocol::HTTP::Response[404, [], ["Not Found"]]
134
- end
133
+ delegate.call(request)
134
+ end
135
+
136
+ private
137
+
138
+ def render(body)
139
+ page = Lively::Pages::Index.new(title: "My App", body: body)
140
+ Protocol::HTTP::Response[200, [], [page.call]]
135
141
  end
136
142
  end
137
143
  ```
138
144
 
145
+ Routes match exact paths and may accept one or more HTTP methods. Query parameters are decoded using `protocol-url` and passed to the handler as its second argument. Routes without an explicit method accept every method.
146
+
139
147
  ## Live Reloading
140
148
 
141
149
  To enable live reloading, add the `io-watch` gem to your `gems.rb` file:
@@ -8,6 +8,7 @@ require "protocol/http/middleware"
8
8
  require "async/websocket/adapters/http"
9
9
 
10
10
  require_relative "resolver"
11
+ require_relative "router"
11
12
  require_relative "pages/index"
12
13
  require_relative "hello_world"
13
14
 
@@ -22,7 +23,7 @@ module Lively
22
23
  #
23
24
  # Use {.[]} to create a simple application class for a single view, optionally
24
25
  # with shared state. For more complex applications, subclass and override
25
- # {#allowed_views}, {#state}, and {#body}.
26
+ # {#allowed_views}, {#state}, and {#configure_routes}.
26
27
  class Application < Protocol::HTTP::Middleware
27
28
  VIEWS = [HelloWorld].freeze
28
29
  STATE = {}.freeze
@@ -98,22 +99,36 @@ module Lively
98
99
  Pages::Index.new(title: self.title, body: self.body)
99
100
  end
100
101
 
101
- # Handle a standard HTTP request.
102
+ # Handle a standard HTTP request which did not match a configured route.
102
103
  # @parameter request [Protocol::HTTP::Request] The incoming HTTP request.
103
104
  # @returns [Protocol::HTTP::Response] The HTTP response with the rendered page.
104
105
  def handle(request)
105
106
  return Protocol::HTTP::Response[200, [], [self.index.call]]
106
107
  end
107
108
 
109
+ # Add the standard application routes to the given router.
110
+ # Override this method and call `super` to add application-specific routes.
111
+ # @parameter router [Router] The router to configure.
112
+ def configure_routes(router)
113
+ router.route("/live") do |request|
114
+ Async::WebSocket::Adapters::HTTP.open(request, &self.method(:live)) || Protocol::HTTP::Response[400]
115
+ end
116
+ end
117
+
118
+ # The router for this application. Unmatched requests are handled separately
119
+ # by {#handle}.
120
+ # @returns [Router] The configured router.
121
+ def router
122
+ @router ||= Router.new.tap do |router|
123
+ configure_routes(router)
124
+ end
125
+ end
126
+
108
127
  # Process an incoming HTTP request.
109
128
  # @parameter request [Protocol::HTTP::Request] The incoming HTTP request.
110
129
  # @returns [Protocol::HTTP::Response] The appropriate response for the request.
111
130
  def call(request)
112
- if request.path == "/live"
113
- return Async::WebSocket::Adapters::HTTP.open(request, &self.method(:live)) || Protocol::HTTP::Response[400]
114
- else
115
- return handle(request)
116
- end
131
+ return router.call(request) || handle(request)
117
132
  end
118
133
  end
119
134
  end
data/lib/lively/assets.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2021-2025, by Samuel Williams.
5
5
 
6
+ require "uri"
6
7
  require "protocol/http/middleware"
7
8
  require "protocol/http/body/file"
8
9
  require "console"
@@ -1,56 +1,68 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2021-2025, by Samuel Williams.
4
+ # Copyright, 2021-2026, by Samuel Williams.
5
5
 
6
- require_relative "../application"
7
- require_relative "../assets"
8
-
9
- require "falcon/environment/server"
6
+ require_relative "middleware"
7
+ require_relative "http"
8
+ require_relative "htty"
10
9
 
11
10
  # @namespace
12
11
  module Lively
13
12
  # @namespace
14
13
  module Environment
15
- # Represents the environment configuration for a Lively application server.
16
- #
17
- # This module provides server configuration including URL binding, process count,
18
- # application class resolution, and middleware stack setup. It integrates with
19
- # Falcon's server environment to provide a complete hosting solution.
14
+ # Multiplexing environment for Lively applications.
15
+ #
16
+ # 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.
17
+ #
18
+ # The `htty` key controls which transport is used. Override it in a service block to force a specific transport regardless of the environment variable:
19
+ #
20
+ # ~~~ ruby
21
+ # service "myapp" do
22
+ # include Lively::Environment::Application
23
+ # def htty = false # always use HTTP
24
+ # end
25
+ # ~~~
20
26
  module Application
21
- include Falcon::Environment::Server
27
+ include Lively::Environment::Middleware
28
+ # Note: does not include Falcon::Environment::Server directly. Falcon is
29
+ # brought in exclusively via http_environment so that the combined
30
+ # evaluator's service_class resolves correctly without shadowing.
31
+
32
+ # Whether to use HTTY transport. Reads ENV["HTTY"] by default.
33
+ # @returns [Boolean]
34
+ def htty
35
+ ENV["HTTY"] == "1"
36
+ end
22
37
 
23
- # Get the server URL for this application.
24
- # @returns [String] The base URL where the server will be accessible.
25
- def url
26
- "http://localhost:9292"
38
+ # The environment module to use for HTTY transport.
39
+ # @returns [Module]
40
+ def htty_environment
41
+ Lively::Environment::HTTY
27
42
  end
28
43
 
29
- # Get the number of server processes to run.
30
- # @returns [Integer] The number of worker processes.
31
- def count
32
- 1
44
+ # The environment module to use for HTTP transport.
45
+ # @returns [Module]
46
+ def http_environment
47
+ Lively::Environment::HTTP
33
48
  end
34
49
 
35
- # Resolve the application class to use.
36
- # @returns [Class] The application class, either user-defined or default.
37
- def application
38
- if Object.const_defined?(:Application)
39
- Object.const_get(:Application)
40
- else
41
- Console.warn(self, "No Application class defined, using default.")
42
- ::Lively::Application
43
- end
50
+ # The environment module for the selected transport.
51
+ # @returns [Module]
52
+ def transport_environment
53
+ htty ? htty_environment : http_environment
44
54
  end
45
55
 
46
- # Build the middleware stack for this application.
47
- # @returns [Protocol::HTTP::Middleware] The complete middleware stack.
48
- def middleware
49
- ::Protocol::HTTP::Middleware.build do |builder|
50
- builder.use Lively::Assets, root: File.expand_path("public", self.root)
51
- builder.use Lively::Assets, root: File.expand_path("../../../public", __dir__)
52
- builder.use self.application
53
- end
56
+ # Build the service by composing the transport environment on top of this one.
57
+ # Called by Async::Service::Generic.wrap self is the evaluator at call time.
58
+ # @parameter environment [Async::Service::Environment]
59
+ # @returns [Async::Service::Generic]
60
+ def make_service(environment)
61
+ combined = environment.with(transport_environment)
62
+ combined_evaluator = combined.evaluator
63
+
64
+ # 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.
65
+ return combined_evaluator.service_class.new(combined, combined_evaluator)
54
66
  end
55
67
  end
56
68
  end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2021-2026, by Samuel Williams.
5
+
6
+ require_relative "middleware"
7
+ require "falcon/environment/server"
8
+
9
+ # @namespace
10
+ module Lively
11
+ # @namespace
12
+ module Environment
13
+ # Falcon (TCP/HTTP) environment for Lively applications.
14
+ #
15
+ # Combines {Falcon::Environment::Server} for HTTP transport with
16
+ # {Lively::Environment::Middleware} for application and asset serving.
17
+ module HTTP
18
+ include Falcon::Environment::Server
19
+ include Lively::Environment::Middleware
20
+
21
+ # The URL this server binds to.
22
+ # @returns [String]
23
+ def url
24
+ ENV.fetch("LIVELY_URL", "http://localhost:9292")
25
+ end
26
+
27
+ # The number of worker processes/threads to run.
28
+ # @returns [Integer]
29
+ def count
30
+ 1
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ require_relative "middleware"
7
+ require "async/htty/environment/server"
8
+
9
+ # @namespace
10
+ module Lively
11
+ # @namespace
12
+ module Environment
13
+ # HTTY (terminal side-channel) environment for Lively applications.
14
+ #
15
+ # Combines {Async::HTTY::Environment} for HTTY transport with
16
+ # {Lively::Environment::Middleware} for application and asset serving.
17
+ module HTTY
18
+ include Async::HTTY::Environment::Server
19
+ include Lively::Environment::Middleware
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2021-2026, by Samuel Williams.
5
+
6
+ require_relative "../application"
7
+ require_relative "../assets"
8
+
9
+ require "protocol/http/middleware/builder"
10
+
11
+ # @namespace
12
+ module Lively
13
+ # @namespace
14
+ module Environment
15
+ # Shared middleware configuration for Lively application environments.
16
+ #
17
+ # Provides the application class resolver, asset middleware, and the
18
+ # Lively middleware stack. Included by both {HTTP} and {HTTY} environments.
19
+ module Middleware
20
+ # Get the root directory for this application.
21
+ # @returns [String] The current working directory.
22
+ def root
23
+ Dir.pwd
24
+ end
25
+
26
+ # Resolve the application class to use.
27
+ # @returns [Class] The application class, either user-defined or default.
28
+ def application
29
+ if Object.const_defined?(:Application)
30
+ Object.const_get(:Application)
31
+ else
32
+ Console.warn(self, "No Application class defined, using default.")
33
+ ::Lively::Application
34
+ end
35
+ end
36
+
37
+ # Build the middleware stack for this application.
38
+ # @returns [Protocol::HTTP::Middleware] The complete middleware stack.
39
+ def middleware
40
+ ::Protocol::HTTP::Middleware.build do |builder|
41
+ builder.use Lively::Assets, root: File.expand_path("public", self.root)
42
+ builder.use Lively::Assets, root: File.expand_path("../../../public", __dir__)
43
+ builder.use self.application
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ require "json"
7
+ require "xrb/markup"
8
+ require "xrb/tag"
9
+ require "xrb/template"
10
+
11
+ module Lively
12
+ # Represents a complete HTML document.
13
+ #
14
+ # A page combines application content with the stylesheets, import map,
15
+ # JavaScript modules, and body attributes required to present it. Applications
16
+ # can use this class directly or subclass it to provide shared defaults.
17
+ class Page
18
+ TEMPLATE = XRB::Template.load_file(File.expand_path("page.xrb", __dir__))
19
+
20
+ # Initialize a new page.
21
+ # @parameter title [String] The document title.
22
+ # @parameter body [Object | Nil] The document body. The result of `to_html` is interpolated into the template.
23
+ # @parameter icon [String | Nil] The favicon URL.
24
+ # @parameter stylesheets [Array(String | Hash)] Stylesheets in document order. Hash entries specify link attributes.
25
+ # @parameter imports [Hash] JavaScript import map entries.
26
+ # @parameter modules [Array(String)] JavaScript module URLs in document order.
27
+ # @parameter body_attributes [Hash] Attributes applied to the body element.
28
+ def initialize(title: "Lively", body: nil, icon: nil, stylesheets: [], imports: {}, modules: [], body_attributes: {})
29
+ @title = title
30
+ @body = body
31
+ @icon = icon
32
+ @stylesheets = stylesheets
33
+ @imports = imports
34
+ @modules = modules
35
+ @body_attributes = body_attributes
36
+ @template = TEMPLATE
37
+ end
38
+
39
+ # @attribute [String] The document title.
40
+ attr :title
41
+
42
+ # @attribute [Object | Nil] The document body.
43
+ attr :body
44
+
45
+ # @attribute [String | Nil] The favicon URL.
46
+ attr :icon
47
+
48
+ # @attribute [Array(String | Hash)] Stylesheets in document order.
49
+ attr :stylesheets
50
+
51
+ # @attribute [Hash] JavaScript import map entries.
52
+ attr :imports
53
+
54
+ # @attribute [Array(String)] JavaScript module URLs in document order.
55
+ attr :modules
56
+
57
+ # @attribute [Hash] Attributes applied to the body element.
58
+ attr :body_attributes
59
+
60
+ # @attribute [XRB::Template] The document template.
61
+ attr :template
62
+
63
+ # The opening body tag including configured attributes.
64
+ # @returns [XRB::Tag]
65
+ def body_tag
66
+ XRB::Tag.opened("body", @body_attributes)
67
+ end
68
+
69
+ # A stylesheet link tag for the given URL or attributes.
70
+ # @parameter stylesheet [String | Hash] The stylesheet URL or link attributes.
71
+ # @returns [XRB::Tag]
72
+ def stylesheet_tag(stylesheet)
73
+ attributes = if stylesheet.respond_to?(:to_hash)
74
+ stylesheet.to_hash
75
+ else
76
+ {href: stylesheet}
77
+ end
78
+
79
+ XRB::Tag.closed("link", {rel: "stylesheet", type: "text/css"}.merge(attributes))
80
+ end
81
+
82
+ # The rendered body content.
83
+ # @returns [Object]
84
+ def body_content
85
+ @body&.to_html || "No body specified!"
86
+ end
87
+
88
+ # The serialized JavaScript import map.
89
+ # @returns [XRB::MarkupString]
90
+ def import_map
91
+ json = JSON.pretty_generate(imports: @imports)
92
+ json = json.gsub("<", "\\u003c").gsub(">", "\\u003e").gsub("&", "\\u0026")
93
+
94
+ XRB::MarkupString.raw(json)
95
+ end
96
+
97
+ # Render this page to a string.
98
+ # @returns [String]
99
+ def call
100
+ @template.to_string(self)
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,30 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>#{self.title}</title>
5
+
6
+ <meta charset="UTF-8" />
7
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
8
+
9
+ <?r if self.icon ?>
10
+ <link rel="icon" type="image/png" href="#{self.icon}" />
11
+ <?r end ?>
12
+ <?r self.stylesheets.each do |stylesheet| ?>
13
+ #{self.stylesheet_tag(stylesheet)}
14
+ <?r end ?>
15
+
16
+ <?r unless self.imports.empty? ?>
17
+ <script type="importmap">
18
+ #{self.import_map}
19
+ </script>
20
+ <?r end ?>
21
+
22
+ <?r self.modules.each do |source| ?>
23
+ <script type="module" src="#{source}"></script>
24
+ <?r end ?>
25
+ </head>
26
+
27
+ #{self.body_tag}
28
+ #{self.body_content}
29
+ </body>
30
+ </html>
@@ -3,7 +3,7 @@
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2021-2026, by Samuel Williams.
5
5
 
6
- require "xrb/template"
6
+ require_relative "../page"
7
7
 
8
8
  # @namespace
9
9
  module Lively
@@ -14,31 +14,31 @@ module Lively
14
14
  # This class renders the initial HTML page that users see when they visit
15
15
  # a Lively application. It uses an XRB template to generate the page structure
16
16
  # and embeds the Live view component for dynamic content.
17
- class Index
17
+ class Index < Page
18
+ ICON = "/_static/icon.png"
19
+ STYLESHEETS = [
20
+ {href: "/_static/site.css", media: "screen"}.freeze,
21
+ {href: "/_static/index.css", media: "screen"}.freeze,
22
+ ].freeze
23
+ IMPORTS = {
24
+ "live" => "/_components/@socketry/live/Live.js",
25
+ "live-audio" => "/_components/@socketry/live-audio/Live/Audio.js",
26
+ "morphdom" => "/_components/morphdom/morphdom-esm.js",
27
+ }.freeze
28
+ MODULES = ["/application.js"].freeze
29
+
18
30
  # Initialize a new index page.
19
31
  # @parameter title [String] The title of the page.
20
32
  # @parameter body [Object] The body content of the page.
21
33
  def initialize(title: "Lively", body: nil)
22
- @title = title
23
- @body = body
24
-
25
- path = File.expand_path("index.xrb", __dir__)
26
- @template = XRB::Template.load_file(path)
27
- end
28
-
29
- # @attribute [String] The title of the page.
30
- attr :title
31
-
32
- # @attribute [Object] The body content of the page.
33
- attr :body
34
-
35
- # @attribute [XRB::Template] The XRB template for rendering the page.
36
- attr :template
37
-
38
- # Render this page to a string.
39
- # @returns [String] The rendered HTML for this page.
40
- def call
41
- @template.to_string(self)
34
+ super(
35
+ title: title,
36
+ body: body,
37
+ icon: ICON,
38
+ stylesheets: STYLESHEETS,
39
+ imports: IMPORTS,
40
+ modules: MODULES,
41
+ )
42
42
  end
43
43
  end
44
44
  end
@@ -13,7 +13,7 @@ module Lively
13
13
  class Resolver < Live::Resolver
14
14
  # Initialize a new resolver with shared state.
15
15
  # @parameter state [Hash] Key-value pairs to pass to view constructors as keyword arguments.
16
- def initialize(state = nil)
16
+ def initialize(state = {})
17
17
  super()
18
18
  @state = state
19
19
  end
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ require "protocol/http"
7
+ require "protocol/url"
8
+
9
+ module Lively
10
+ # Dispatches HTTP requests to handlers using exact path and method matches.
11
+ #
12
+ # Request targets are parsed with {Protocol::URL::Reference}. Handlers receive
13
+ # the original request and the decoded query parameters.
14
+ class Router
15
+ EMPTY_PARAMETERS = {}.freeze
16
+ ANY_METHOD = nil
17
+ private_constant :EMPTY_PARAMETERS, :ANY_METHOD
18
+
19
+ # Initialize a router.
20
+ # @yields {|router| ...} The router to configure.
21
+ def initialize
22
+ @routes = {}
23
+
24
+ yield self if block_given?
25
+ end
26
+
27
+ # Add a route.
28
+ #
29
+ # When `methods` is omitted, the handler accepts every HTTP method. Otherwise,
30
+ # it may be a single method or an array of methods.
31
+ #
32
+ # @parameter path [String] The absolute path to match.
33
+ # @parameter methods [String | Symbol | Array(String | Symbol) | Nil] The accepted HTTP methods.
34
+ # @yields {|request, parameters| ...} The route handler.
35
+ # @parameter request [Protocol::HTTP::Request] The original request.
36
+ # @parameter parameters [Hash] The decoded query parameters.
37
+ # @returns [Router] The router.
38
+ def route(path, methods: nil, &handler)
39
+ raise ArgumentError, "A route handler is required!" unless handler
40
+
41
+ path = route_path(path)
42
+ methods = route_methods(methods)
43
+ handlers = (@routes[path] ||= {})
44
+
45
+ methods.each do |method|
46
+ if handlers.key?(method)
47
+ raise ArgumentError, "Route already defined for #{method || "any method"} #{path}!"
48
+ end
49
+
50
+ handlers[method] = handler
51
+ end
52
+
53
+ return self
54
+ end
55
+
56
+ Protocol::HTTP::Methods.each do |name, method|
57
+ # Add a route for this HTTP method.
58
+ # @parameter path [String] The absolute path to match.
59
+ # @yields {|request, parameters| ...} The route handler.
60
+ # @returns [Router] The router.
61
+ define_method(name) do |path, &handler|
62
+ route(path, methods: method, &handler)
63
+ end
64
+ end
65
+
66
+ # Dispatch a request to a matching route.
67
+ # @parameter request [Protocol::HTTP::Request] The request to dispatch.
68
+ # @returns [Protocol::HTTP::Response | Nil] The handler or error response, or `nil` when no path matches.
69
+ def call(request)
70
+ reference = parse_reference(request.path)
71
+ return Protocol::HTTP::Response[400] unless reference
72
+
73
+ unless handlers = @routes[reference.path]
74
+ return nil
75
+ end
76
+
77
+ unless handler = handlers[request.method] || handlers[ANY_METHOD]
78
+ allowed_methods = handlers.keys.compact.sort.join(", ")
79
+ return Protocol::HTTP::Response[405, [["allow", allowed_methods]]]
80
+ end
81
+
82
+ parameters = parse_query(reference)
83
+ return Protocol::HTTP::Response[400] unless parameters
84
+
85
+ return handler.call(request, parameters)
86
+ end
87
+
88
+ private
89
+
90
+ def route_path(path)
91
+ url = Protocol::URL[path]
92
+
93
+ unless url && !url.is_a?(Protocol::URL::Absolute)
94
+ raise ArgumentError, "Route must be an absolute path: #{path.inspect}!"
95
+ end
96
+
97
+ reference = Protocol::URL::Reference[url]
98
+
99
+ unless reference.path.absolute?
100
+ raise ArgumentError, "Route must be an absolute path: #{path.inspect}!"
101
+ end
102
+
103
+ if reference.query? || reference.fragment?
104
+ raise ArgumentError, "Route path cannot include a query or fragment: #{path.inspect}!"
105
+ end
106
+
107
+ return reference.path.freeze
108
+ end
109
+
110
+ def route_methods(methods)
111
+ return [ANY_METHOD] unless methods
112
+
113
+ methods = Array(methods)
114
+ raise ArgumentError, "At least one HTTP method is required!" if methods.empty?
115
+
116
+ return methods.map do |method|
117
+ raise ArgumentError, "HTTP method cannot be nil!" unless method
118
+
119
+ method.to_s.upcase
120
+ end.uniq
121
+ end
122
+
123
+ def parse_reference(path)
124
+ reference = Protocol::URL::Reference[path]
125
+ return if reference&.fragment?
126
+
127
+ return reference
128
+ rescue ArgumentError
129
+ nil
130
+ end
131
+
132
+ def parse_query(reference)
133
+ reference.parse_query! || EMPTY_PARAMETERS
134
+ rescue ArgumentError
135
+ nil
136
+ end
137
+ end
138
+ end
@@ -5,5 +5,5 @@
5
5
 
6
6
  # @namespace
7
7
  module Lively
8
- VERSION = "0.17.1"
8
+ VERSION = "0.19.0"
9
9
  end
data/lib/lively.rb CHANGED
@@ -5,6 +5,8 @@
5
5
 
6
6
  require_relative "lively/version"
7
7
 
8
+ require_relative "lively/page"
9
+ require_relative "lively/router"
8
10
  require_relative "lively/assets"
9
11
  require_relative "lively/application"
10
12
 
data/readme.md CHANGED
@@ -20,6 +20,10 @@ Please see the [project documentation](https://socketry.github.io/lively/) for m
20
20
 
21
21
  Please see the [project releases](https://socketry.github.io/lively/releases/index) for all releases.
22
22
 
23
+ ### v0.18.0
24
+
25
+ - Add support for HTTY.
26
+
23
27
  ### v0.17.0
24
28
 
25
29
  - Expose shared application state via `Application[..., controller: Controller.new]`.
@@ -57,13 +61,6 @@ Please see the [project releases](https://socketry.github.io/lively/releases/ind
57
61
 
58
62
  - Tidied up gem dependencies.
59
63
 
60
- ### v0.13.0
61
-
62
- - Added `live-audio` support for background and positional audio in applications.
63
- - Added game audio example demonstrating audio playback.
64
- - Fixed serving non-existent asset paths gracefully.
65
- - Achieved 100% test and documentation coverage.
66
-
67
64
  ## See Also
68
65
 
69
66
  - [live](https://github.com/socketry/live) — Provides client-server communication using websockets.
data/releases.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Releases
2
2
 
3
+ ## v0.18.0
4
+
5
+ - Add support for HTTY.
6
+
3
7
  ## v0.17.0
4
8
 
5
9
  - Expose shared application state via `Application[..., controller: Controller.new]`.
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lively
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.17.1
4
+ version: 0.19.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -52,6 +52,34 @@ dependencies:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: async-htty
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: async-service
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.23'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.23'
55
83
  - !ruby/object:Gem::Dependency
56
84
  name: falcon
57
85
  requirement: !ruby/object:Gem::Requirement
@@ -94,6 +122,20 @@ dependencies:
94
122
  - - "~>"
95
123
  - !ruby/object:Gem::Version
96
124
  version: '0.18'
125
+ - !ruby/object:Gem::Dependency
126
+ name: protocol-url
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '0.18'
132
+ type: :runtime
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '0.18'
97
139
  - !ruby/object:Gem::Dependency
98
140
  name: xrb
99
141
  requirement: !ruby/object:Gem::Requirement
@@ -123,10 +165,15 @@ files:
123
165
  - lib/lively/application.rb
124
166
  - lib/lively/assets.rb
125
167
  - lib/lively/environment/application.rb
168
+ - lib/lively/environment/http.rb
169
+ - lib/lively/environment/htty.rb
170
+ - lib/lively/environment/middleware.rb
126
171
  - lib/lively/hello_world.rb
172
+ - lib/lively/page.rb
173
+ - lib/lively/page.xrb
127
174
  - lib/lively/pages/index.rb
128
- - lib/lively/pages/index.xrb
129
175
  - lib/lively/resolver.rb
176
+ - lib/lively/router.rb
130
177
  - lib/lively/version.rb
131
178
  - license.md
132
179
  - public/_components/@socketry/live-audio/Live/Audio.js
@@ -172,7 +219,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
172
219
  - !ruby/object:Gem::Version
173
220
  version: '0'
174
221
  requirements: []
175
- rubygems_version: 4.0.6
222
+ rubygems_version: 4.0.10
176
223
  specification_version: 4
177
224
  summary: A simple client-server SPA framework.
178
225
  test_files: []
metadata.gz.sig CHANGED
Binary file
@@ -1,29 +0,0 @@
1
- <!DOCTYPE html>
2
- <html>
3
- <head>
4
- <title>#{self.title}</title>
5
-
6
- <meta charset="UTF-8" />
7
- <meta name="viewport" content="width=device-width, initial-scale=1" />
8
-
9
- <link rel="icon" type="image/png" href="/_static/icon.png" />
10
- <link rel="stylesheet" href="/_static/site.css" type="text/css" media="screen" />
11
- <link rel="stylesheet" href="/_static/index.css" type="text/css" media="screen" />
12
-
13
- <script type="importmap">
14
- {
15
- "imports": {
16
- "live": "/_components/@socketry/live/Live.js",
17
- "live-audio": "/_components/@socketry/live-audio/Live/Audio.js",
18
- "morphdom": "/_components/morphdom/morphdom-esm.js"
19
- }
20
- }
21
- </script>
22
-
23
- <script type="module" src="/application.js"></script>
24
- </head>
25
-
26
- <body>
27
- #{self.body&.to_html || "No body specified!"}
28
- </body>
29
- </html>