duoruby 0.1.0 → 0.1.1

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: 6f3074e802c24846379df300d0012092cd00a18513fe87bf1a62d757bb0d1fe4
4
- data.tar.gz: 3603e0dbc7808566d8cec775a0513068438fe9456fd322aa6f53ca534d5c0142
3
+ metadata.gz: 340349cc6d514408915a76db23e509316ab458350cee20e45150ecf19d2e0801
4
+ data.tar.gz: a1314f3199d5a896408c522e830152565f54d2da6072ebcd8f5d3a59f1009ffb
5
5
  SHA512:
6
- metadata.gz: f0cad5cb7f45d9801398bb6d3e2f945f35e3178407ebcf6a781aab081ca0d5b73ede700ce27ab656005b935e11c8ba782b130dc08eb7578daf0278bf1b2218f9
7
- data.tar.gz: b5e8e428393c969a3aaf2e161ab0307efc965364230de19fb095c4cf93ba92adff574b2b861bdceaec1cd6d4326245b26e2d0ea83848635e7e87bd994f606638
6
+ metadata.gz: 92a4603982bff2d9ddc1c83f837dbb6a154a3adae7bb05f3a059ccb1b99f5ab97135a001f9ca9b69d6583d1c0258aacce918ea95ce8a9f3c3c29961feb2a53bf
7
+ data.tar.gz: 941b8c259f7917f95f7a24c8843f8f071e7dbb90b46527cd7411147d356cfeecfcf41e9e0329adc0fca02aaeef134c13b929ef33d71c0b333d54444acee6eeaf
data/CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
1
+ # Changelog
2
+
3
+ ## v0.1.1 - 2026-05-17
4
+
5
+ - Moved native launch ownership onto `DuoRuby::Server#launch`; `duoruby launch` now builds the application server and launches that instance.
6
+ - Removed the separate `DuoRuby::Launcher` API.
7
+ - Reversed launch process ownership so the main process runs the server and the forked child owns the native webview.
8
+ - Added block-style channel namespaces for `on` and `send`, including both `channel(:name) { ... }` and yielded `channel(:name) { |namespace| ... }` forms.
9
+ - Stopped tracking `Gemfile.lock` files in the gem and examples.
10
+
11
+ ## v0.1.0 - 2026-05-17
12
+
13
+ - Initial prerelease.
@@ -23,8 +23,8 @@ module DuoRuby
23
23
  remove_handler(handlers, event, handler || block)
24
24
  end
25
25
 
26
- def channel(name)
27
- Namespace.new(self, name)
26
+ def channel(name, &block)
27
+ Namespace.call(self, name, &block)
28
28
  end
29
29
 
30
30
  def included(receiver)
@@ -3,6 +3,14 @@
3
3
  module DuoRuby
4
4
  class Channel
5
5
  class Namespace
6
+ def self.call(target, name, &block)
7
+ namespace = new(target, name)
8
+ return namespace unless block
9
+ return block.call(namespace) if block.arity.positive?
10
+
11
+ namespace.instance_exec(&block)
12
+ end
13
+
6
14
  def initialize(target, name)
7
15
  @target = target
8
16
  @name = name.to_s
@@ -95,8 +95,8 @@ module DuoRuby
95
95
  handlers.delete(event) if handlers[event]&.empty?
96
96
  end
97
97
 
98
- def channel(name)
99
- Namespace.new(self, name)
98
+ def channel(name, &block)
99
+ Namespace.call(self, name, &block)
100
100
  end
101
101
 
102
102
  # Returns a Proc wrapping the first registered handler for +event+, bound
data/lib/duoruby/cli.rb CHANGED
@@ -78,9 +78,12 @@ module DuoRuby
78
78
  options = launch_options
79
79
  return 1 unless options
80
80
 
81
- require "duoruby/launcher"
81
+ require "duoruby/server"
82
+
83
+ server_config = server_options(options)
84
+ server_config[:port] ||= free_port(server_config.fetch(:host, "127.0.0.1"))
82
85
 
83
- DuoRuby::Launcher.new(**options).run(output: @output)
86
+ DuoRuby::Server.build(**server_config).launch(**window_options(options), output: @output)
84
87
  0
85
88
  end
86
89
 
@@ -115,6 +118,23 @@ module DuoRuby
115
118
  options
116
119
  end
117
120
 
121
+ def server_options(options)
122
+ options.slice(:root, :host, :port)
123
+ end
124
+
125
+ def window_options(options)
126
+ options.slice(:title)
127
+ end
128
+
129
+ def free_port(host)
130
+ require "socket"
131
+
132
+ server = TCPServer.new(host, 0)
133
+ server.addr[1]
134
+ ensure
135
+ server&.close
136
+ end
137
+
118
138
  # Parses the options that follow the +serve+ command.
119
139
  #
120
140
  # @return [Hash, nil] option hash on success, +nil+ on parse failure
@@ -75,8 +75,8 @@ module DuoRuby
75
75
  @writer.call(Message.coerce(message).to_h)
76
76
  end
77
77
 
78
- def channel(name)
79
- Channel::Namespace.new(self, name)
78
+ def channel(name, &block)
79
+ Channel::Namespace.call(self, name, &block)
80
80
  end
81
81
 
82
82
  def resolve_call(message)
data/lib/duoruby/group.rb CHANGED
@@ -28,8 +28,8 @@ module DuoRuby
28
28
  self
29
29
  end
30
30
 
31
- def channel(name)
32
- Channel::Namespace.new(self, name)
31
+ def channel(name, &block)
32
+ Channel::Namespace.call(self, name, &block)
33
33
  end
34
34
 
35
35
  private
@@ -96,8 +96,8 @@ module DuoRuby
96
96
  self
97
97
  end
98
98
 
99
- def channel(name)
100
- Channel::Namespace.new(self, name)
99
+ def channel(name, &block)
100
+ Channel::Namespace.call(self, name, &block)
101
101
  end
102
102
 
103
103
  # Sends +event+ with +params+ to every current member.
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "json"
4
+ require "socket"
4
5
  require "uri"
5
6
  require "async"
6
7
  require "async/http/endpoint"
@@ -26,6 +27,9 @@ module DuoRuby
26
27
  #
27
28
  # @example Starting the server from application code
28
29
  # DuoRuby::Server.build(root: __dir__, port: 3000).run
30
+ #
31
+ # @example Starting the server and opening a native window
32
+ # DuoRuby::Server.build(root: __dir__).launch
29
33
  class Server < Channel
30
34
  require "duoruby/server/frontend_compiler"
31
35
 
@@ -112,6 +116,31 @@ module DuoRuby
112
116
  end
113
117
  end
114
118
 
119
+ # Forks a native browser window into a child process, then runs this server
120
+ # in the main process. Blocks until the server exits or the window closes.
121
+ #
122
+ # @param output [IO] where to print the launch banner (default: +$stdout+)
123
+ # @param title [String, nil] native window title; +nil+ uses +DuoRuby.config.title+
124
+ # @param width [Integer] native window width in pixels
125
+ # @param height [Integer] native window height in pixels
126
+ def launch(output: $stdout, title: nil, width: 1280, height: 800)
127
+ output.puts "launching http://#{host}:#{port}"
128
+
129
+ browser_pid = fork_process { run_browser(title: title, width: width, height: height) }
130
+ browser_watchdog = start_browser_watchdog(browser_pid)
131
+
132
+ null_output = File.open(File::NULL, "w")
133
+ run(output: null_output)
134
+ rescue Interrupt
135
+ nil
136
+ ensure
137
+ null_output&.close
138
+ if browser_pid
139
+ browser_watchdog&.kill
140
+ terminate_process(browser_pid)
141
+ end
142
+ end
143
+
115
144
  # Compiles the Opal frontend to a JavaScript string.
116
145
  #
117
146
  # Resets Opal's global path state, adds configured frontend gems, then
@@ -201,6 +230,48 @@ module DuoRuby
201
230
  }
202
231
  end
203
232
 
233
+ # Runs the browser in the child process.
234
+ def run_browser(title:, width:, height:)
235
+ wait_for_server
236
+
237
+ require "webview_util"
238
+
239
+ window_title = title || DuoRuby.config.title
240
+ window = WebviewUtil::Window.new(title: window_title, width: width, height: height)
241
+ window.navigate("http://#{host}:#{port}")
242
+ window.run
243
+ end
244
+
245
+ # Polls until the server is accepting TCP connections.
246
+ def wait_for_server
247
+ loop do
248
+ TCPSocket.new(host, port).close
249
+ break
250
+ rescue Errno::ECONNREFUSED
251
+ sleep 0.05
252
+ end
253
+ end
254
+
255
+ def fork_process(&block) = fork(&block)
256
+
257
+ def start_browser_watchdog(browser_pid)
258
+ Thread.new do
259
+ wait_process(browser_pid)
260
+ interrupt_server
261
+ rescue Errno::ECHILD
262
+ nil
263
+ end
264
+ end
265
+
266
+ def terminate_process(pid)
267
+ Process.kill(:TERM, pid) rescue nil
268
+ wait_process(pid) rescue nil
269
+ end
270
+
271
+ def wait_process(pid) = Process.waitpid(pid)
272
+
273
+ def interrupt_server = Thread.main.raise(Interrupt)
274
+
204
275
  # Returns the HTML shell response.
205
276
  def html
206
277
  body = <<~HTML
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DuoRuby
4
- VERSION = "0.1.0"
4
+ VERSION = "0.1.1"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: duoruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - DuoRuby contributors
@@ -172,6 +172,7 @@ executables:
172
172
  extensions: []
173
173
  extra_rdoc_files: []
174
174
  files:
175
+ - CHANGELOG.md
175
176
  - LICENSE.txt
176
177
  - README.md
177
178
  - exe/duoruby
@@ -184,7 +185,6 @@ files:
184
185
  - lib/duoruby/client.rb
185
186
  - lib/duoruby/config.rb
186
187
  - lib/duoruby/group.rb
187
- - lib/duoruby/launcher.rb
188
188
  - lib/duoruby/message.rb
189
189
  - lib/duoruby/reply_error.rb
190
190
  - lib/duoruby/reply_promise.rb
@@ -1,92 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "socket"
4
- require "duoruby/config"
5
- require "webview_util"
6
-
7
- module DuoRuby
8
- # Starts the server in a child process and opens a native webview window
9
- # on the main process's main thread.
10
- #
11
- # GTK requires all calls on the thread that called gtk_init (the OS main
12
- # thread). The Async/Falcon server runs in a forked child process so each
13
- # has full ownership of its own event loop. When the window is closed the
14
- # child is terminated; when the child dies unexpectedly the window closes.
15
- #
16
- # The window title defaults to +DuoRuby.config.title+, which the application
17
- # can set in its +duoruby.rb+ config file:
18
- #
19
- # DuoRuby.configure { |c| c.title = "My App" }
20
- #
21
- # @example
22
- # DuoRuby::Launcher.new(root: __dir__).run
23
- class Launcher
24
- # @param root [String] application root directory
25
- # @param host [String] server host (default: +"127.0.0.1"+)
26
- # @param port [Integer, nil] server port; +nil+ picks a free port automatically
27
- # @param title [String, nil] window title; +nil+ uses +DuoRuby.config.title+
28
- # @param width [Integer] window width in pixels (default: +1280+)
29
- # @param height [Integer] window height in pixels (default: +800+)
30
- def initialize(root: Dir.pwd, host: "127.0.0.1", port: nil,
31
- title: nil, width: 1280, height: 800)
32
- @root = File.expand_path(root)
33
- @host = host
34
- @port = port || free_port
35
- @title = title
36
- @width = width
37
- @height = height
38
- end
39
-
40
- # Forks the Async server into a child process, then opens the native
41
- # window on the main thread. Blocks until the window is closed, then
42
- # terminates the server child.
43
- #
44
- # @param output [IO] where to print the launch banner (default: +$stdout+)
45
- def run(output: $stdout)
46
- output.puts "launching http://#{@host}:#{@port}"
47
-
48
- server_pid = fork { run_server }
49
-
50
- wait_for_server
51
-
52
- title = @title || DuoRuby.config.title
53
- window = WebviewUtil::Window.new(title: title, width: @width, height: @height)
54
- window.navigate("http://#{@host}:#{@port}")
55
- window.run
56
- ensure
57
- if server_pid
58
- Process.kill(:TERM, server_pid) rescue nil
59
- Process.waitpid(server_pid) rescue nil
60
- end
61
- end
62
-
63
- private
64
-
65
- # Runs the server in the child process.
66
- def run_server
67
- require "console"
68
- require "duoruby/server"
69
- Console.logger.fatal!
70
- Server.build(root: @root, host: @host, port: @port)
71
- .run(output: File.open(File::NULL, "w"))
72
- end
73
-
74
- # Allocates a free TCP port by binding to port 0 and reading the assigned port.
75
- def free_port
76
- server = TCPServer.new(@host, 0)
77
- server.addr[1]
78
- ensure
79
- server&.close
80
- end
81
-
82
- # Polls until the server is accepting TCP connections.
83
- def wait_for_server
84
- loop do
85
- TCPSocket.new(@host, @port).close
86
- break
87
- rescue Errno::ECONNREFUSED
88
- sleep 0.05
89
- end
90
- end
91
- end
92
- end