lively 0.12.0 → 0.13.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: ec2c7506f7a9fa9662a11a4d676e524a7cdf58fa43b3adbca8b94e2092a2eba3
4
- data.tar.gz: d473035b8db2a7092b918c0d993521a9dca1736e20d4ac4a8bd58550a1655e86
3
+ metadata.gz: de5825bdffb0c6db7ba03f20137602787e927eada6835b2391baa6618be733ae
4
+ data.tar.gz: b26947450326f69eeb142d4088feedabf56328420df485d758ebeac8bc98f814
5
5
  SHA512:
6
- metadata.gz: 417cda1546265cae2b10d075020e05c92573457d6c892a0d594f74bb9253fe1b8a864be75ce7167a5bfa28e17ba26709df05dbf29f3076f6073b846245164e5c
7
- data.tar.gz: 2ea896d2553f5261202b99e1877fddd668ba954fd1fc2fd8cc4f74faf77effaad6952cce7c39a039bd92cb4d04856e7550330a144cbbf81f57de712359e30bb8
6
+ metadata.gz: 055545fee2f0b6843be7ba956af7b142db8ab06e12012f30a358a16ee919c71ac91362450cb0a92475a7359b00218c025a2f7f63c78ec4a44256a91be28ecf75
7
+ data.tar.gz: e367a553e43123cb86d726a2223a58abed8db9de9f8c0db5dcd09c3b8f6e1ad850f747f2d200bc504fe3417efa66ed12d8fec542878a7af9db778d2980d2cff0
checksums.yaml.gz.sig CHANGED
Binary file
@@ -10,8 +10,18 @@ require "async/websocket/adapters/http"
10
10
  require_relative "pages/index"
11
11
  require_relative "hello_world"
12
12
 
13
+ # @namespace
13
14
  module Lively
15
+ # Represents the main Lively application middleware.
16
+ #
17
+ # This class serves as the entry point for Lively applications, handling both
18
+ # standard HTTP requests for the initial page load and WebSocket connections
19
+ # for live updates. It integrates with the Live framework to provide real-time
20
+ # interactive web applications.
14
21
  class Application < Protocol::HTTP::Middleware
22
+ # Create a new application class configured for a specific Live view tag.
23
+ # @parameter tag [Class] The Live view class to use as the application body.
24
+ # @returns [Class] A new application class configured for the specified tag.
15
25
  def self.[](tag)
16
26
  klass = Class.new(self)
17
27
 
@@ -26,36 +36,64 @@ module Lively
26
36
  return klass
27
37
  end
28
38
 
39
+ # Get the default resolver for this application.
40
+ # @returns [Live::Resolver] A resolver configured to allow HelloWorld components.
29
41
  def self.resolver
30
42
  Live::Resolver.allow(HelloWorld)
31
43
  end
32
44
 
45
+ # Initialize a new Lively application.
46
+ # @parameter delegate [Protocol::HTTP::Middleware] The next middleware in the chain.
47
+ # @parameter resolver [Live::Resolver] The resolver for Live components.
33
48
  def initialize(delegate, resolver: self.class.resolver)
34
49
  super(delegate)
35
50
 
36
51
  @resolver = resolver
37
52
  end
38
53
 
54
+ # @attribute [Live::Resolver] The resolver for live components.
55
+ attr :resolver
56
+
57
+ # @attribute [Protocol::HTTP::Middleware] The delegate middleware for request handling.
58
+ attr :delegate
59
+
60
+ # Handle a WebSocket connection for live updates.
61
+ # @parameter connection [Async::WebSocket::Connection] The WebSocket connection.
39
62
  def live(connection)
40
63
  Live::Page.new(@resolver).run(connection)
41
64
  end
42
65
 
66
+ # Get the title for this application.
67
+ # @returns [String] The class name of this application.
43
68
  def title
44
69
  self.class.name
45
70
  end
46
71
 
72
+ # Create the body content for this application.
73
+ # @parameter **options [Hash] Additional options to pass to the body constructor.
74
+ # @returns [HelloWorld] A new HelloWorld instance.
47
75
  def body(...)
48
76
  HelloWorld.new(...)
49
77
  end
50
78
 
79
+ # Create the index page for this application.
80
+ # @parameter **options [Hash] Additional options to pass to the index constructor.
81
+ # @returns [Pages::Index] A new index page instance.
51
82
  def index(...)
52
83
  Pages::Index.new(title: self.title, body: self.body(...))
53
84
  end
54
85
 
86
+ # Handle a standard HTTP request.
87
+ # @parameter request [Protocol::HTTP::Request] The incoming HTTP request.
88
+ # @parameter **options [Hash] Additional options.
89
+ # @returns [Protocol::HTTP::Response] The HTTP response with the rendered page.
55
90
  def handle(request, ...)
56
91
  return Protocol::HTTP::Response[200, [], [self.index(...).call]]
57
92
  end
58
93
 
94
+ # Process an incoming HTTP request.
95
+ # @parameter request [Protocol::HTTP::Request] The incoming HTTP request.
96
+ # @returns [Protocol::HTTP::Response] The appropriate response for the request.
59
97
  def call(request)
60
98
  if request.path == "/live"
61
99
  return Async::WebSocket::Adapters::HTTP.open(request, &self.method(:live)) || Protocol::HTTP::Response[400]
data/lib/lively/assets.rb CHANGED
@@ -5,8 +5,15 @@
5
5
 
6
6
  require "protocol/http/middleware"
7
7
  require "protocol/http/body/file"
8
+ require "console"
8
9
 
9
10
  module Lively
11
+ # Represents an HTTP middleware for serving static assets.
12
+ #
13
+ # This middleware serves static files from a configured root directory with
14
+ # appropriate content type headers and caching controls. It supports a wide
15
+ # range of web file formats including HTML, CSS, JavaScript, images, fonts,
16
+ # audio, video, and WebAssembly files.
10
17
  class Assets < Protocol::HTTP::Middleware
11
18
  DEFAULT_CACHE_CONTROL = "no-store, no-cache, must-revalidate, max-age=0"
12
19
 
@@ -14,24 +21,53 @@ module Lively
14
21
  ".html" => "text/html",
15
22
  ".css" => "text/css",
16
23
  ".js" => "application/javascript",
24
+ ".mjs" => "application/javascript",
25
+ ".json" => "application/json",
17
26
  ".png" => "image/png",
27
+ ".jpg" => "image/jpeg",
18
28
  ".jpeg" => "image/jpeg",
19
29
  ".gif" => "image/gif",
30
+ ".webp" => "image/webp",
31
+ ".svg" => "image/svg+xml",
32
+ ".ico" => "image/x-icon",
20
33
  ".mp3" => "audio/mpeg",
21
34
  ".wav" => "audio/wav",
35
+ ".ogg" => "audio/ogg",
36
+ ".mp4" => "video/mp4",
37
+ ".webm" => "video/webm",
38
+ ".ttf" => "font/ttf",
39
+ ".woff" => "font/woff",
40
+ ".woff2" => "font/woff2",
41
+ ".wasm" => "application/wasm",
22
42
  }
23
43
 
24
44
  PUBLIC_ROOT = File.expand_path("../../public", __dir__)
25
45
 
46
+ # Initialize a new assets middleware instance.
47
+ # @parameter delegate [Protocol::HTTP::Middleware] The next middleware in the chain.
48
+ # @parameter root [String] The root directory path for serving assets.
49
+ # @parameter content_types [Hash] Mapping of file extensions to MIME types.
50
+ # @parameter cache_control [String] Cache control header value for responses.
26
51
  def initialize(delegate, root: PUBLIC_ROOT, content_types: DEFAULT_CONTENT_TYPES, cache_control: DEFAULT_CACHE_CONTROL)
27
52
  super(delegate)
28
53
 
29
- @root = root
54
+ @root = File.expand_path(root)
30
55
 
31
56
  @content_types = content_types
32
57
  @cache_control = cache_control
33
58
  end
34
59
 
60
+ # @attribute [String] The absolute path to the root directory for serving assets.
61
+ attr :root
62
+
63
+ # @attribute [Hash] Mapping of file extensions to content types.
64
+ attr :content_types
65
+
66
+ # @attribute [String] Cache control header value for asset responses.
67
+ attr :cache_control
68
+
69
+ # Freeze this middleware instance and all its configuration.
70
+ # @returns [Assets] Returns self for method chaining.
35
71
  def freeze
36
72
  return self if frozen?
37
73
 
@@ -42,29 +78,45 @@ module Lively
42
78
  super
43
79
  end
44
80
 
45
- def response_for(path, content_type)
46
- headers = [
47
- ["content-type", content_type],
48
- ["cache-control", @cache_control],
49
- ]
81
+ # Generate an HTTP response for the given file path.
82
+ # @parameter path [String] The absolute file path to serve.
83
+ # @returns [Protocol::HTTP::Response] HTTP response with file content or error.
84
+ def response_for(path)
85
+ extension = File.extname(path)
50
86
 
51
- return Protocol::HTTP::Response[200, headers, Protocol::HTTP::Body::File.open(path)]
87
+ if content_type = @content_types[extension]
88
+ headers = [
89
+ ["content-type", content_type],
90
+ ["cache-control", @cache_control],
91
+ ]
92
+
93
+ return Protocol::HTTP::Response[200, headers, Protocol::HTTP::Body::File.open(path)]
94
+ else
95
+ Console.warn(self, "Unsupported media type!", path: path)
96
+ return Protocol::HTTP::Response[415, [["content-type", "text/plain"]], "Unsupported media type: #{extension.inspect}!"]
97
+ end
52
98
  end
53
99
 
100
+ # Expand a relative path to an absolute path within the asset root.
101
+ # @parameter path [String] The relative path to expand.
102
+ # @returns [String | Nil] The absolute path if valid, `nil` if the file doesn't exist.
54
103
  def expand_path(path)
55
- File.realpath(File.join(@root, path))
104
+ root = File.realpath(@root)
105
+ path = File.realpath(File.join(@root, path))
106
+
107
+ if path.start_with?(root) && File.file?(path)
108
+ return path
109
+ end
56
110
  rescue Errno::ENOENT
57
111
  nil
58
112
  end
59
113
 
114
+ # Handle an incoming HTTP request.
115
+ # @parameter request [Protocol::HTTP::Request] The incoming HTTP request.
116
+ # @returns [Protocol::HTTP::Response] The HTTP response for the asset or delegates to next middleware.
60
117
  def call(request)
61
118
  if path = expand_path(request.path)
62
- extension = File.extname(path)
63
- content_type = @content_types[extension]
64
-
65
- if path.start_with?(@root) && File.exist?(path) && content_type
66
- return response_for(path, content_type)
67
- end
119
+ return response_for(path)
68
120
  end
69
121
 
70
122
  super
@@ -1,35 +1,50 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2021-2024, by Samuel Williams.
4
+ # Copyright, 2021-2025, by Samuel Williams.
5
5
 
6
6
  require_relative "../application"
7
7
  require_relative "../assets"
8
8
 
9
9
  require "falcon/environment/server"
10
10
 
11
+ # @namespace
11
12
  module Lively
13
+ # @namespace
12
14
  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.
13
20
  module Application
14
21
  include Falcon::Environment::Server
15
22
 
23
+ # Get the server URL for this application.
24
+ # @returns [String] The base URL where the server will be accessible.
16
25
  def url
17
26
  "http://localhost:9292"
18
27
  end
19
28
 
29
+ # Get the number of server processes to run.
30
+ # @returns [Integer] The number of worker processes.
20
31
  def count
21
32
  1
22
33
  end
23
34
 
35
+ # Resolve the application class to use.
36
+ # @returns [Class] The application class, either user-defined or default.
24
37
  def application
25
38
  if Object.const_defined?(:Application)
26
- ::Application
39
+ Object.const_get(:Application)
27
40
  else
28
41
  Console.warn(self, "No Application class defined, using default.")
29
42
  ::Lively::Application
30
43
  end
31
44
  end
32
45
 
46
+ # Build the middleware stack for this application.
47
+ # @returns [Protocol::HTTP::Middleware] The complete middleware stack.
33
48
  def middleware
34
49
  ::Protocol::HTTP::Middleware.build do |builder|
35
50
  builder.use Lively::Assets, root: File.expand_path("public", self.root)
@@ -3,14 +3,27 @@
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2024-2025, by Samuel Williams.
5
5
 
6
+ # @namespace
6
7
  module Lively
8
+ # Represents a "Hello World" Live view component.
9
+ #
10
+ # This component displays a simple greeting message with a real-time clock
11
+ # that updates every second. It serves as a demonstration of Lively's
12
+ # live update capabilities and provides basic usage instructions.
7
13
  class HelloWorld < Live::View
14
+ # Initialize a new HelloWorld view.
15
+ # @parameter **options [Hash] Additional options passed to the parent view.
8
16
  def initialize(...)
9
17
  super
10
18
 
11
19
  @clock = nil
12
20
  end
13
21
 
22
+ # @attribute [Async::Task | Nil] The background task that updates the view periodically.
23
+ attr :clock
24
+
25
+ # Bind this view to a page and start the update clock.
26
+ # @parameter page [Live::Page] The page this view is bound to.
14
27
  def bind(page)
15
28
  super
16
29
 
@@ -23,12 +36,15 @@ module Lively
23
36
  end
24
37
  end
25
38
 
39
+ # Close this view and stop the update clock.
26
40
  def close
27
41
  @clock&.stop
28
42
 
29
43
  super
30
44
  end
31
45
 
46
+ # Render this view as HTML.
47
+ # @parameter builder [Live::Builder] The HTML builder for constructing the view.
32
48
  def render(builder)
33
49
  builder.tag(:h1) do
34
50
  builder.text("Hello, I'm Lively!")
@@ -5,10 +5,20 @@
5
5
 
6
6
  require "xrb/template"
7
7
 
8
+ # @namespace
8
9
  module Lively
10
+ # @namespace
9
11
  module Pages
12
+ # Represents the main index page for a Lively application.
13
+ #
14
+ # This class renders the initial HTML page that users see when they visit
15
+ # a Lively application. It uses an XRB template to generate the page structure
16
+ # and embeds the Live view component for dynamic content.
10
17
  class Index
11
- def initialize(title: "Lively", body: "Hello World")
18
+ # Initialize a new index page.
19
+ # @parameter title [String] The title of the page.
20
+ # @parameter body [Object] The body content of the page.
21
+ def initialize(title: "Lively", body: nil)
12
22
  @title = title
13
23
  @body = body
14
24
 
@@ -16,9 +26,17 @@ module Lively
16
26
  @template = XRB::Template.load_file(path)
17
27
  end
18
28
 
29
+ # @attribute [String] The title of the page.
19
30
  attr :title
31
+
32
+ # @attribute [Object] The body content of the page.
20
33
  attr :body
21
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.
22
40
  def call
23
41
  @template.to_string(self)
24
42
  end
@@ -14,15 +14,13 @@
14
14
  {
15
15
  "imports": {
16
16
  "live": "/_components/@socketry/live/Live.js",
17
+ "live-audio": "/_components/@socketry/live-audio/Live/Audio.js",
17
18
  "morphdom": "/_components/morphdom/morphdom-esm.js"
18
19
  }
19
20
  }
20
21
  </script>
21
22
 
22
- <script type="module">
23
- import {Live} from 'live';
24
- window.live = Live.start();
25
- </script>
23
+ <script type="module" src="/application.js"></script>
26
24
  </head>
27
25
 
28
26
  <body>
@@ -1,8 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2021-2024, by Samuel Williams.
4
+ # Copyright, 2021-2025, by Samuel Williams.
5
5
 
6
+ # @namespace
6
7
  module Lively
7
- VERSION = "0.12.0"
8
+ VERSION = "0.13.0"
8
9
  end
@@ -1,5 +1,33 @@
1
1
  import morphdom from 'morphdom';
2
2
 
3
+ // ViewElement - Base class for Live.js custom elements
4
+ export class ViewElement extends HTMLElement {
5
+ static observedAttributes = [];
6
+ static connectedElements = new Set();
7
+
8
+ connectedCallback() {
9
+ if (!this.id) {
10
+ this.id = crypto.randomUUID();
11
+ }
12
+
13
+ ViewElement.connectedElements.add(this);
14
+
15
+ const window = this.ownerDocument.defaultView;
16
+ if (window && window.live) {
17
+ window.live.bind(this);
18
+ }
19
+ }
20
+
21
+ disconnectedCallback() {
22
+ ViewElement.connectedElements.delete(this);
23
+
24
+ const window = this.ownerDocument.defaultView;
25
+ if (window && window.live) {
26
+ window.live.unbind(this);
27
+ }
28
+ }
29
+ }
30
+
3
31
  export class Live {
4
32
  #window;
5
33
  #document;
@@ -10,13 +38,20 @@ export class Live {
10
38
 
11
39
  static start(options = {}) {
12
40
  let window = options.window || globalThis;
41
+ if (window.live) throw new Error("Live.js is already started!");
42
+
13
43
  let path = options.path || 'live'
14
44
  let base = options.base || window.location.href;
15
45
 
16
46
  let url = new URL(path, base);
17
47
  url.protocol = url.protocol.replace('http', 'ws');
18
48
 
19
- return new this(window, url);
49
+ window.live = new this(window, url);
50
+ if (!window.customElements.get('live-view')) {
51
+ window.customElements.define('live-view', ViewElement);
52
+ }
53
+
54
+ return window.live;
20
55
  }
21
56
 
22
57
  constructor(window, url) {
@@ -34,43 +69,6 @@ export class Live {
34
69
  this.#document.addEventListener("visibilitychange", () => this.#handleVisibilityChange());
35
70
 
36
71
  this.#handleVisibilityChange();
37
-
38
- const elementNodeType = this.#window.Node.ELEMENT_NODE;
39
-
40
- // Create a MutationObserver to watch for removed nodes
41
- this.observer = new this.#window.MutationObserver((mutationsList, observer) => {
42
- for (let mutation of mutationsList) {
43
- if (mutation.type === 'childList') {
44
- for (let node of mutation.removedNodes) {
45
- if (node.nodeType !== elementNodeType) continue;
46
-
47
- if (node.classList?.contains('live')) {
48
- this.#unbind(node);
49
- }
50
-
51
- // Unbind any child nodes:
52
- for (let child of node.getElementsByClassName('live')) {
53
- this.#unbind(child);
54
- }
55
- }
56
-
57
- for (let node of mutation.addedNodes) {
58
- if (node.nodeType !== elementNodeType) continue;
59
-
60
- if (node.classList.contains('live')) {
61
- this.#bind(node);
62
- }
63
-
64
- // Bind any child nodes:
65
- for (let child of node.getElementsByClassName('live')) {
66
- this.#bind(child);
67
- }
68
- }
69
- }
70
- }
71
- });
72
-
73
- this.observer.observe(this.#document.body, {childList: true, subtree: true});
74
72
  }
75
73
 
76
74
  // -- Connection Handling --
@@ -167,23 +165,19 @@ export class Live {
167
165
  }
168
166
  }
169
167
 
170
- #bind(element) {
171
- console.log("bind", element.id, element.dataset);
172
-
168
+ bind(element) {
173
169
  this.#send(JSON.stringify(['bind', element.id, element.dataset]));
174
170
  }
175
-
176
- #unbind(element) {
177
- console.log("unbind", element.id, element.dataset);
178
-
171
+
172
+ unbind(element) {
179
173
  if (this.#server) {
180
174
  this.#send(JSON.stringify(['unbind', element.id]));
181
175
  }
182
176
  }
183
-
177
+
184
178
  #attach() {
185
- for (let node of this.#document.getElementsByClassName('live')) {
186
- this.#bind(node);
179
+ for (let element of ViewElement.connectedElements) {
180
+ this.bind(element);
187
181
  }
188
182
  }
189
183
 
@@ -1,9 +1,12 @@
1
1
  {
2
2
  "name": "@socketry/live",
3
3
  "type": "module",
4
- "version": "0.14.0",
4
+ "version": "0.16.0",
5
5
  "description": "Live HTML tags for Ruby.",
6
6
  "main": "Live.js",
7
+ "files": [
8
+ "Live.js"
9
+ ],
7
10
  "repository": {
8
11
  "type": "git",
9
12
  "url": "git+https://github.com/socketry/live-js.git"