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 +4 -4
- data/CHANGELOG.md +13 -0
- data/lib/duoruby/channel/handler_methods.rb +2 -2
- data/lib/duoruby/channel/namespace.rb +8 -0
- data/lib/duoruby/channel.rb +2 -2
- data/lib/duoruby/cli.rb +22 -2
- data/lib/duoruby/client.rb +2 -2
- data/lib/duoruby/group.rb +4 -4
- data/lib/duoruby/server.rb +71 -0
- data/lib/duoruby/version.rb +1 -1
- metadata +2 -2
- data/lib/duoruby/launcher.rb +0 -92
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 340349cc6d514408915a76db23e509316ab458350cee20e45150ecf19d2e0801
|
|
4
|
+
data.tar.gz: a1314f3199d5a896408c522e830152565f54d2da6072ebcd8f5d3a59f1009ffb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
@@ -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
|
data/lib/duoruby/channel.rb
CHANGED
|
@@ -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.
|
|
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/
|
|
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::
|
|
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
|
data/lib/duoruby/client.rb
CHANGED
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.
|
|
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.
|
|
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.
|
data/lib/duoruby/server.rb
CHANGED
|
@@ -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
|
data/lib/duoruby/version.rb
CHANGED
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.
|
|
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
|
data/lib/duoruby/launcher.rb
DELETED
|
@@ -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
|