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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9009d8e648b2101fcaf583feae53f3f5f6d9d8975849960d44c87f9a37f23597
4
- data.tar.gz: 58d04690e2b0395698a343722a5e0e7c69bb351c918a2979f11d47f7e14d95ab
3
+ metadata.gz: b5c44fadc8aa766bbfe16e6dc94afb0503a03408ee5bb8108fc9064cb10adf84
4
+ data.tar.gz: 81a8de81cf0cbbfee1deb8a3fdea5b448a9ba3a8b417ffc942d33fb5dc2d8d2f
5
5
  SHA512:
6
- metadata.gz: bfff7024cc42755729901dca5fbfe79b494c9167aecd40e01a65b153614dc004394406a2b6fe56844b88fbfce84fdd66b3ee08e250b8b11edeed5d34c652adb6
7
- data.tar.gz: f958db4dacb8d0b6a17b1523e51e72e8374cb39c9eb32e69646d822cd6c2b7fa79a347ef1bde5fca3e71abfeeecdb0228fdec7f4936916e3d389ca1db52fc557
6
+ metadata.gz: 04c82e1accf25203c93e3fc987166453cec277b5e29b380c7539d3f32dd0e777843d1f084485513437108bda6cc8047a80c141a890ec926a0c63f8ef7b8ae688
7
+ data.tar.gz: 0013bf2ab8f22d327df1f48ced0d956148c495e310bcc7ac0230b0ba2344ebb97d5f744a26cafe23ce4969fa9776da17acdc66f64aacfbb5dcb1772e1826daf6
data/README.md CHANGED
@@ -1,3 +1,17 @@
1
1
  # ruflet_server
2
2
 
3
- Part of Ruflet monorepo.
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ruflet
4
- VERSION = "0.0.18" unless const_defined?(:VERSION)
4
+ VERSION = "0.0.19" unless const_defined?(:VERSION)
5
5
  end
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.18
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
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.18
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