ruflet_server 0.0.18 → 0.0.19
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/README.md +15 -1
- data/lib/ruflet/server/connection_protocol.rb +285 -0
- data/lib/ruflet/version.rb +1 -1
- data/lib/ruflet_server.rb +1 -15
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b5c44fadc8aa766bbfe16e6dc94afb0503a03408ee5bb8108fc9064cb10adf84
|
|
4
|
+
data.tar.gz: 81a8de81cf0cbbfee1deb8a3fdea5b448a9ba3a8b417ffc942d33fb5dc2d8d2f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 04c82e1accf25203c93e3fc987166453cec277b5e29b380c7539d3f32dd0e777843d1f084485513437108bda6cc8047a80c141a890ec926a0c63f8ef7b8ae688
|
|
7
|
+
data.tar.gz: 0013bf2ab8f22d327df1f48ced0d956148c495e310bcc7ac0230b0ba2344ebb97d5f744a26cafe23ce4969fa9776da17acdc66f64aacfbb5dcb1772e1826daf6
|
data/README.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
1
|
# ruflet_server
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`ruflet_server` runs server-driven Ruflet applications and connects their Ruby
|
|
4
|
+
UI code to Ruflet clients.
|
|
5
|
+
|
|
6
|
+
It is installed automatically in projects created with `ruflet new`. Start an
|
|
7
|
+
application through the Ruflet CLI:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
bundle exec ruflet run
|
|
11
|
+
bundle exec ruflet run --web
|
|
12
|
+
bundle exec ruflet run --desktop
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Application code uses the public `Ruflet.run` API supplied by `ruflet_core`.
|
|
16
|
+
Rails applications should use `ruflet_rails` instead of starting this server
|
|
17
|
+
directly.
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ruflet
|
|
4
|
+
# Transport-agnostic implementation of the Ruflet wire protocol: one
|
|
5
|
+
# connection loop shared by every server that speaks to Flutter clients.
|
|
6
|
+
#
|
|
7
|
+
# The standalone TCP server (Ruflet::Server) and host-server adapters such
|
|
8
|
+
# as ruflet_rails' Rack-hijack endpoint include this module and provide
|
|
9
|
+
# only their transport plus the integration hooks below — the protocol
|
|
10
|
+
# itself is never reimplemented.
|
|
11
|
+
#
|
|
12
|
+
# Includers must initialize:
|
|
13
|
+
# @app_block — proc invoked with the Page on first registration
|
|
14
|
+
# @sessions — Hash mapping connection key => Page
|
|
15
|
+
# @sessions_mutex — Mutex guarding @sessions
|
|
16
|
+
module ConnectionProtocol
|
|
17
|
+
# ------------------------------------------------------------------
|
|
18
|
+
# Integration hooks (override in the including server as needed).
|
|
19
|
+
# ------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
# Called when a connection enters the protocol loop.
|
|
22
|
+
def connection_opened(ws); end
|
|
23
|
+
|
|
24
|
+
# Called when a connection leaves the protocol loop.
|
|
25
|
+
def connection_closed(ws); end
|
|
26
|
+
|
|
27
|
+
# Return an existing Page to resume for this session id, or nil to
|
|
28
|
+
# create a fresh one (hosts with a session registry override this).
|
|
29
|
+
def resume_session(_session_id)
|
|
30
|
+
nil
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Called after a Page is stored for a connection.
|
|
34
|
+
def session_stored(page, ws); end
|
|
35
|
+
|
|
36
|
+
# Called after a Page is removed for a connection.
|
|
37
|
+
def session_removed(page, ws); end
|
|
38
|
+
|
|
39
|
+
# Called before a control event is dispatched to the Page.
|
|
40
|
+
def before_dispatch_event(ws, event); end
|
|
41
|
+
|
|
42
|
+
def log_connection_error(error)
|
|
43
|
+
warn "server error: #{error.class}: #{error.message}"
|
|
44
|
+
warn error.backtrace.join("\n") if error.backtrace
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# ------------------------------------------------------------------
|
|
48
|
+
# Transport entry points.
|
|
49
|
+
# ------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
# For hosts that already performed the HTTP upgrade (Rack hijack, the
|
|
52
|
+
# embedded runtime, tests with socket pairs).
|
|
53
|
+
def handle_upgraded_socket(io)
|
|
54
|
+
run_connection(Ruflet::WebSocketConnection.new(io))
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def run_connection(ws)
|
|
58
|
+
connection_opened(ws)
|
|
59
|
+
|
|
60
|
+
while (raw = ws.read_message)
|
|
61
|
+
handle_message(ws, raw)
|
|
62
|
+
end
|
|
63
|
+
rescue StandardError => e
|
|
64
|
+
return if disconnect_error?(e)
|
|
65
|
+
|
|
66
|
+
log_connection_error(e)
|
|
67
|
+
send_message(ws, Protocol::ACTIONS[:session_crashed], { "message" => e.message.to_s.dup.force_encoding("UTF-8") })
|
|
68
|
+
ensure
|
|
69
|
+
close_connection(ws)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def close_connection(ws)
|
|
73
|
+
return unless ws
|
|
74
|
+
|
|
75
|
+
remove_session(ws)
|
|
76
|
+
connection_closed(ws)
|
|
77
|
+
ws.close
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# ------------------------------------------------------------------
|
|
81
|
+
# Protocol core.
|
|
82
|
+
# ------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
def handle_message(ws, raw)
|
|
85
|
+
action, payload = decode_incoming(raw)
|
|
86
|
+
payload ||= {}
|
|
87
|
+
|
|
88
|
+
warn "incoming action=#{action.inspect}" if ENV["RUFLET_DEBUG"] == "1"
|
|
89
|
+
|
|
90
|
+
case action
|
|
91
|
+
when Protocol::ACTIONS[:register_client], Protocol::ACTIONS[:register_web_client]
|
|
92
|
+
on_register_client(ws, payload)
|
|
93
|
+
when Protocol::ACTIONS[:control_event], Protocol::ACTIONS[:page_event_from_web]
|
|
94
|
+
on_control_event(ws, payload)
|
|
95
|
+
when Protocol::ACTIONS[:update_control], Protocol::ACTIONS[:update_control_props]
|
|
96
|
+
on_update_control(ws, payload)
|
|
97
|
+
when Protocol::ACTIONS[:invoke_control_method]
|
|
98
|
+
on_invoke_control_method(ws, payload)
|
|
99
|
+
else
|
|
100
|
+
raise "Unknown action: #{action.inspect}"
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def decode_incoming(raw)
|
|
105
|
+
parsed = normalize_incoming(Ruflet::WireCodec.unpack(raw.to_s.b))
|
|
106
|
+
|
|
107
|
+
if parsed.is_a?(Array) && parsed.length >= 2
|
|
108
|
+
return [parsed[0], parsed[1]]
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
if parsed.is_a?(Hash)
|
|
112
|
+
action = parsed["action"] || parsed[:action]
|
|
113
|
+
payload = parsed["payload"] || parsed[:payload]
|
|
114
|
+
return [action, payload] unless action.nil?
|
|
115
|
+
|
|
116
|
+
if (parsed.key?("target") || parsed.key?(:target)) && (parsed.key?("name") || parsed.key?(:name))
|
|
117
|
+
return [Protocol::ACTIONS[:control_event], parsed]
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
raise "Unsupported payload format"
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def normalize_incoming(value)
|
|
125
|
+
case value
|
|
126
|
+
when String
|
|
127
|
+
value.dup.force_encoding("UTF-8")
|
|
128
|
+
when Integer, Float, TrueClass, FalseClass, NilClass
|
|
129
|
+
value
|
|
130
|
+
when Symbol
|
|
131
|
+
value.to_s
|
|
132
|
+
when Array
|
|
133
|
+
value.map { |v| normalize_incoming(v) }
|
|
134
|
+
when Hash
|
|
135
|
+
value.each_with_object({}) do |(k, v), out|
|
|
136
|
+
out[k.to_s] = normalize_incoming(v)
|
|
137
|
+
end
|
|
138
|
+
else
|
|
139
|
+
value.to_s
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def on_register_client(ws, payload)
|
|
144
|
+
normalized = Protocol.normalize_register_payload(payload)
|
|
145
|
+
session_id = normalized["session_id"].to_s.empty? ? pseudo_uuid : normalized["session_id"]
|
|
146
|
+
|
|
147
|
+
page = resume_session(session_id)
|
|
148
|
+
first_registration = page.nil?
|
|
149
|
+
|
|
150
|
+
if page
|
|
151
|
+
attach_sender(page, ws)
|
|
152
|
+
reset_mount_state(page)
|
|
153
|
+
else
|
|
154
|
+
page = Page.new(
|
|
155
|
+
session_id: session_id,
|
|
156
|
+
client_details: normalized,
|
|
157
|
+
sender: sender_for(ws)
|
|
158
|
+
)
|
|
159
|
+
page.title = "Ruflet App"
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
@sessions_mutex.synchronize { @sessions[ws.session_key] = page }
|
|
163
|
+
session_stored(page, ws)
|
|
164
|
+
|
|
165
|
+
initial_response = [
|
|
166
|
+
Protocol::ACTIONS[:register_client],
|
|
167
|
+
Protocol.register_response(session_id: session_id)
|
|
168
|
+
]
|
|
169
|
+
ws.send_binary(Ruflet::WireCodec.pack(initial_response))
|
|
170
|
+
|
|
171
|
+
@app_block.call(page) if first_registration
|
|
172
|
+
page.update
|
|
173
|
+
rescue StandardError => e
|
|
174
|
+
send_message(ws, Protocol::ACTIONS[:session_crashed], { "message" => e.message.to_s })
|
|
175
|
+
raise
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def on_control_event(ws, payload)
|
|
179
|
+
event = Protocol.normalize_control_event_payload(payload)
|
|
180
|
+
page = fetch_page(ws)
|
|
181
|
+
return if event["target"].nil? || event["name"].to_s.empty?
|
|
182
|
+
|
|
183
|
+
attach_sender(page, ws)
|
|
184
|
+
before_dispatch_event(ws, event)
|
|
185
|
+
page.dispatch_event(
|
|
186
|
+
target: event["target"],
|
|
187
|
+
name: event["name"],
|
|
188
|
+
data: normalize_event_data(event["data"])
|
|
189
|
+
)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def on_update_control(ws, payload)
|
|
193
|
+
update = Protocol.normalize_update_control_payload(payload)
|
|
194
|
+
page = fetch_page(ws)
|
|
195
|
+
return if update["id"].nil?
|
|
196
|
+
|
|
197
|
+
attach_sender(page, ws)
|
|
198
|
+
page.apply_client_update(update["id"], update["props"] || {})
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def on_invoke_control_method(ws, payload)
|
|
202
|
+
page = fetch_page(ws)
|
|
203
|
+
attach_sender(page, ws)
|
|
204
|
+
page.handle_invoke_method_result(Protocol.normalize_invoke_method_result_payload(payload))
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def fetch_page(ws)
|
|
208
|
+
page = @sessions_mutex.synchronize { @sessions[ws.session_key] }
|
|
209
|
+
raise "Session not found" unless page
|
|
210
|
+
|
|
211
|
+
page
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def remove_session(ws)
|
|
215
|
+
return unless ws
|
|
216
|
+
|
|
217
|
+
page = @sessions_mutex.synchronize { @sessions.delete(ws.session_key) }
|
|
218
|
+
session_removed(page, ws) if page
|
|
219
|
+
page
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def normalize_event_data(value)
|
|
223
|
+
case value
|
|
224
|
+
when Hash
|
|
225
|
+
value.each_with_object({}) { |(k, v), out| out[k.to_sym] = normalize_event_data(v) }
|
|
226
|
+
when Array
|
|
227
|
+
value.map { |entry| normalize_event_data(entry) }
|
|
228
|
+
else
|
|
229
|
+
value
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def send_message(ws, action, payload)
|
|
234
|
+
return if ws.nil? || ws.closed?
|
|
235
|
+
|
|
236
|
+
ws.send_binary(Ruflet::WireCodec.pack([action, payload]))
|
|
237
|
+
rescue StandardError => e
|
|
238
|
+
log_connection_error(e) unless disconnect_error?(e)
|
|
239
|
+
remove_session(ws)
|
|
240
|
+
connection_closed(ws)
|
|
241
|
+
nil
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def sender_for(ws)
|
|
245
|
+
lambda do |action, msg_payload|
|
|
246
|
+
send_message(ws, action, msg_payload)
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def attach_sender(page, ws)
|
|
251
|
+
page.instance_variable_set(:@sender, sender_for(ws))
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def reset_mount_state(page)
|
|
255
|
+
page.instance_variable_set(:@overlay_container_mounted, false)
|
|
256
|
+
page.instance_variable_set(:@dialogs_container_mounted, false)
|
|
257
|
+
page.instance_variable_set(:@services_container_mounted, false)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def disconnect_error?(error)
|
|
261
|
+
return true if error.is_a?(IOError)
|
|
262
|
+
return true if error.is_a?(Errno::EPIPE)
|
|
263
|
+
return true if error.is_a?(Errno::ECONNRESET)
|
|
264
|
+
return true if error.is_a?(Errno::ECONNABORTED)
|
|
265
|
+
return true if error.is_a?(Errno::ENOTCONN)
|
|
266
|
+
return true if error.is_a?(Errno::ESHUTDOWN)
|
|
267
|
+
return true if error.is_a?(Errno::EBADF)
|
|
268
|
+
return true if error.is_a?(Errno::EINVAL)
|
|
269
|
+
|
|
270
|
+
false
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def pseudo_uuid
|
|
274
|
+
now = Process.clock_gettime(Process::CLOCK_REALTIME, :nanosecond)
|
|
275
|
+
rnd = rand(0..0xffff_ffff)
|
|
276
|
+
"%08x-%04x-%04x-%04x-%012x" % [
|
|
277
|
+
rnd,
|
|
278
|
+
now & 0xffff,
|
|
279
|
+
(now >> 16) & 0xffff,
|
|
280
|
+
(now >> 32) & 0xffff,
|
|
281
|
+
(now >> 48) & 0xffff_ffff_ffff
|
|
282
|
+
]
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
end
|
data/lib/ruflet/version.rb
CHANGED
data/lib/ruflet_server.rb
CHANGED
|
@@ -2,18 +2,4 @@
|
|
|
2
2
|
|
|
3
3
|
require "ruflet_core"
|
|
4
4
|
require_relative "ruflet/server"
|
|
5
|
-
|
|
6
|
-
module Ruflet
|
|
7
|
-
module_function
|
|
8
|
-
|
|
9
|
-
def run(entrypoint = nil, host: "0.0.0.0", port: nil, &block)
|
|
10
|
-
callback = entrypoint || block
|
|
11
|
-
raise ArgumentError, "Ruflet.run requires a callable entrypoint or block" unless callback.respond_to?(:call)
|
|
12
|
-
port = resolved_run_port(port) if respond_to?(:resolved_run_port)
|
|
13
|
-
port = 8550 if port.nil?
|
|
14
|
-
|
|
15
|
-
Server.new(host: host, port: port) do |page|
|
|
16
|
-
callback.call(page)
|
|
17
|
-
end.start
|
|
18
|
-
end
|
|
19
|
-
end
|
|
5
|
+
require_relative "ruflet/server/connection_protocol"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ruflet_server
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.19
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- AdamMusa
|
|
@@ -15,14 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - '='
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: 0.0.
|
|
18
|
+
version: 0.0.19
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - '='
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: 0.0.
|
|
25
|
+
version: 0.0.19
|
|
26
26
|
description: Ruflet WebSocket server runtime compatible with Flet protocol.
|
|
27
27
|
email:
|
|
28
28
|
- adammusa2222@gmail.com
|
|
@@ -32,6 +32,7 @@ extra_rdoc_files: []
|
|
|
32
32
|
files:
|
|
33
33
|
- README.md
|
|
34
34
|
- lib/ruflet/server.rb
|
|
35
|
+
- lib/ruflet/server/connection_protocol.rb
|
|
35
36
|
- lib/ruflet/server/web_socket_connection.rb
|
|
36
37
|
- lib/ruflet/server/wire_codec.rb
|
|
37
38
|
- lib/ruflet/version.rb
|