ruflet_server 0.0.14 → 0.0.15
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/lib/ruflet/server/connection_protocol.rb +285 -0
- data/lib/ruflet/server.rb +35 -213
- data/lib/ruflet/version.rb +1 -1
- data/lib/ruflet_server.rb +3 -1
- 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: 2681e7067d0b0bd1d3d998e352b89a926cc6c8d1c129a632bb325fb5d2dde8c6
|
|
4
|
+
data.tar.gz: d4562b20a7f17a82c29cca1020b1956cc18c22ff8aa8531a7c5ebd5230db6704
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a249874ec8efe3f1ac1c683ee69043077e73051c97eb7cfaa45b491e68df57b7cd22636f665918e33cfeac87913bca791a45ccacb36d16a815f8044ccb219717
|
|
7
|
+
data.tar.gz: 8b5aca6dde6daf9e51d8700de9609879873110b7793db145153140c813095bd294b9683a2b0a29ffdfd111f7ad43373ce830e9c0103cd3ec86b7d098e5474ff0
|
|
@@ -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/server.rb
CHANGED
|
@@ -7,9 +7,15 @@ require "thread"
|
|
|
7
7
|
require "ruflet_core"
|
|
8
8
|
require_relative "server/wire_codec"
|
|
9
9
|
require_relative "server/web_socket_connection"
|
|
10
|
+
require_relative "server/connection_protocol"
|
|
10
11
|
|
|
11
12
|
module Ruflet
|
|
13
|
+
# Standalone TCP transport for the Ruflet protocol. The protocol itself
|
|
14
|
+
# lives in Ruflet::ConnectionProtocol and is shared with host-server
|
|
15
|
+
# adapters (e.g. ruflet_rails runs it on the Rails server's own socket).
|
|
12
16
|
class Server
|
|
17
|
+
include ConnectionProtocol
|
|
18
|
+
|
|
13
19
|
attr_reader :port
|
|
14
20
|
|
|
15
21
|
WEBSOCKET_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
|
@@ -47,12 +53,6 @@ module Ruflet
|
|
|
47
53
|
restore_stop_signals(previous_signals)
|
|
48
54
|
end
|
|
49
55
|
|
|
50
|
-
# For Rack-hosted mode: caller already performed the HTTP upgrade.
|
|
51
|
-
def handle_upgraded_socket(io)
|
|
52
|
-
ws = Ruflet::WebSocketConnection.new(io)
|
|
53
|
-
run_connection(ws)
|
|
54
|
-
end
|
|
55
|
-
|
|
56
56
|
def bind_server_socket!(max_attempts: 100)
|
|
57
57
|
requested = @port.to_i
|
|
58
58
|
candidate = requested
|
|
@@ -65,6 +65,7 @@ module Ruflet
|
|
|
65
65
|
if @port != requested && ENV["RUFLET_SUPPRESS_SERVER_BANNER"] != "1"
|
|
66
66
|
warn "Requested port #{requested} is busy; bound to #{@port}"
|
|
67
67
|
end
|
|
68
|
+
publish_bound_port!
|
|
68
69
|
return
|
|
69
70
|
rescue Errno::EADDRINUSE
|
|
70
71
|
candidate += 1
|
|
@@ -80,6 +81,7 @@ module Ruflet
|
|
|
80
81
|
return unless @running || @server_socket
|
|
81
82
|
|
|
82
83
|
@running = false
|
|
84
|
+
remove_port_file!
|
|
83
85
|
|
|
84
86
|
server = @server_socket
|
|
85
87
|
@server_socket = nil
|
|
@@ -128,6 +130,30 @@ module Ruflet
|
|
|
128
130
|
|
|
129
131
|
private
|
|
130
132
|
|
|
133
|
+
# Lets embedding hosts (e.g. the ruby_runtime Flutter plugins) discover
|
|
134
|
+
# which port the server actually bound when the requested one was busy.
|
|
135
|
+
def publish_bound_port!
|
|
136
|
+
path = ENV["RUFLET_PORT_FILE"].to_s
|
|
137
|
+
return if path.empty?
|
|
138
|
+
|
|
139
|
+
begin
|
|
140
|
+
File.write(path, @port.to_s)
|
|
141
|
+
rescue StandardError
|
|
142
|
+
nil
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def remove_port_file!
|
|
147
|
+
path = ENV["RUFLET_PORT_FILE"].to_s
|
|
148
|
+
return if path.empty?
|
|
149
|
+
|
|
150
|
+
begin
|
|
151
|
+
File.delete(path)
|
|
152
|
+
rescue StandardError
|
|
153
|
+
nil
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
131
157
|
def trap_stop_signals
|
|
132
158
|
{
|
|
133
159
|
"INT" => trap_signal("INT"),
|
|
@@ -211,28 +237,6 @@ module Ruflet
|
|
|
211
237
|
end
|
|
212
238
|
end
|
|
213
239
|
|
|
214
|
-
def run_connection(ws)
|
|
215
|
-
register_connection(ws)
|
|
216
|
-
|
|
217
|
-
while (raw = ws.read_message)
|
|
218
|
-
handle_message(ws, raw)
|
|
219
|
-
end
|
|
220
|
-
rescue StandardError => e
|
|
221
|
-
return if disconnect_error?(e)
|
|
222
|
-
|
|
223
|
-
warn "server error: #{e.class}: #{e.message}"
|
|
224
|
-
warn e.backtrace.join("\n") if e.backtrace
|
|
225
|
-
send_message(ws, Protocol::ACTIONS[:session_crashed], { "message" => e.message.to_s.dup.force_encoding("UTF-8") })
|
|
226
|
-
ensure
|
|
227
|
-
close_connection(ws)
|
|
228
|
-
end
|
|
229
|
-
|
|
230
|
-
def close_connection(ws)
|
|
231
|
-
remove_session(ws)
|
|
232
|
-
unregister_connection(ws)
|
|
233
|
-
ws&.close
|
|
234
|
-
end
|
|
235
|
-
|
|
236
240
|
def read_http_upgrade_request(socket)
|
|
237
241
|
request_line = socket.gets("\r\n")
|
|
238
242
|
return [nil, {}] if request_line.nil?
|
|
@@ -360,15 +364,8 @@ module Ruflet
|
|
|
360
364
|
socket.write("\r\n")
|
|
361
365
|
end
|
|
362
366
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
@sessions_mutex.synchronize do
|
|
367
|
-
@sessions.delete(ws.session_key)
|
|
368
|
-
end
|
|
369
|
-
end
|
|
370
|
-
|
|
371
|
-
def register_connection(ws)
|
|
367
|
+
# ConnectionProtocol hooks: track live sockets so #stop can close them.
|
|
368
|
+
def connection_opened(ws)
|
|
372
369
|
return unless ws
|
|
373
370
|
|
|
374
371
|
@connections_mutex.synchronize do
|
|
@@ -376,7 +373,7 @@ module Ruflet
|
|
|
376
373
|
end
|
|
377
374
|
end
|
|
378
375
|
|
|
379
|
-
def
|
|
376
|
+
def connection_closed(ws)
|
|
380
377
|
return unless ws
|
|
381
378
|
|
|
382
379
|
@connections_mutex.synchronize do
|
|
@@ -384,180 +381,5 @@ module Ruflet
|
|
|
384
381
|
end
|
|
385
382
|
end
|
|
386
383
|
|
|
387
|
-
def handle_message(ws, raw)
|
|
388
|
-
action, payload = decode_incoming(raw)
|
|
389
|
-
payload ||= {}
|
|
390
|
-
|
|
391
|
-
warn "incoming action=#{action.inspect}" if ENV["RUFLET_DEBUG"] == "1"
|
|
392
|
-
|
|
393
|
-
case action
|
|
394
|
-
when Protocol::ACTIONS[:register_client], Protocol::ACTIONS[:register_web_client]
|
|
395
|
-
on_register_client(ws, payload)
|
|
396
|
-
when Protocol::ACTIONS[:control_event], Protocol::ACTIONS[:page_event_from_web]
|
|
397
|
-
on_control_event(ws, payload)
|
|
398
|
-
when Protocol::ACTIONS[:update_control], Protocol::ACTIONS[:update_control_props]
|
|
399
|
-
on_update_control(ws, payload)
|
|
400
|
-
when Protocol::ACTIONS[:invoke_control_method]
|
|
401
|
-
on_invoke_control_method(ws, payload)
|
|
402
|
-
else
|
|
403
|
-
raise "Unknown action: #{action.inspect}"
|
|
404
|
-
end
|
|
405
|
-
end
|
|
406
|
-
|
|
407
|
-
def decode_incoming(raw)
|
|
408
|
-
parsed = normalize_incoming(Ruflet::WireCodec.unpack(raw.to_s.b))
|
|
409
|
-
|
|
410
|
-
if parsed.is_a?(Array) && parsed.length >= 2
|
|
411
|
-
return [parsed[0], parsed[1]]
|
|
412
|
-
end
|
|
413
|
-
|
|
414
|
-
if parsed.is_a?(Hash)
|
|
415
|
-
action = parsed["action"] || parsed[:action]
|
|
416
|
-
payload = parsed["payload"] || parsed[:payload]
|
|
417
|
-
return [action, payload] unless action.nil?
|
|
418
|
-
|
|
419
|
-
if (parsed.key?("target") || parsed.key?(:target)) && (parsed.key?("name") || parsed.key?(:name))
|
|
420
|
-
return [Protocol::ACTIONS[:control_event], parsed]
|
|
421
|
-
end
|
|
422
|
-
end
|
|
423
|
-
|
|
424
|
-
raise "Unsupported payload format"
|
|
425
|
-
end
|
|
426
|
-
|
|
427
|
-
def normalize_incoming(value)
|
|
428
|
-
case value
|
|
429
|
-
when String
|
|
430
|
-
value.dup.force_encoding("UTF-8")
|
|
431
|
-
when Integer, Float, TrueClass, FalseClass, NilClass
|
|
432
|
-
value
|
|
433
|
-
when Symbol
|
|
434
|
-
value.to_s
|
|
435
|
-
when Array
|
|
436
|
-
value.map { |v| normalize_incoming(v) }
|
|
437
|
-
when Hash
|
|
438
|
-
value.each_with_object({}) do |(k, v), out|
|
|
439
|
-
out[k.to_s] = normalize_incoming(v)
|
|
440
|
-
end
|
|
441
|
-
else
|
|
442
|
-
value.to_s
|
|
443
|
-
end
|
|
444
|
-
end
|
|
445
|
-
|
|
446
|
-
def on_register_client(ws, payload)
|
|
447
|
-
normalized = Protocol.normalize_register_payload(payload)
|
|
448
|
-
session_id = normalized["session_id"].to_s.empty? ? pseudo_uuid : normalized["session_id"]
|
|
449
|
-
|
|
450
|
-
page = Page.new(
|
|
451
|
-
session_id: session_id,
|
|
452
|
-
client_details: normalized,
|
|
453
|
-
sender: lambda do |action, msg_payload|
|
|
454
|
-
send_message(ws, action, msg_payload)
|
|
455
|
-
end
|
|
456
|
-
)
|
|
457
|
-
|
|
458
|
-
page.title = "Ruflet App"
|
|
459
|
-
|
|
460
|
-
@sessions_mutex.synchronize do
|
|
461
|
-
@sessions[ws.session_key] = page
|
|
462
|
-
end
|
|
463
|
-
|
|
464
|
-
initial_response = [
|
|
465
|
-
Protocol::ACTIONS[:register_client],
|
|
466
|
-
{
|
|
467
|
-
"session_id" => session_id,
|
|
468
|
-
"page_patch" => {},
|
|
469
|
-
"error" => nil
|
|
470
|
-
}
|
|
471
|
-
]
|
|
472
|
-
ws.send_binary(Ruflet::WireCodec.pack(initial_response))
|
|
473
|
-
|
|
474
|
-
@app_block.call(page)
|
|
475
|
-
page.update
|
|
476
|
-
rescue StandardError => e
|
|
477
|
-
send_message(ws, Protocol::ACTIONS[:session_crashed], { "message" => e.message })
|
|
478
|
-
raise
|
|
479
|
-
end
|
|
480
|
-
|
|
481
|
-
def on_invoke_control_method(ws, payload)
|
|
482
|
-
page = fetch_page(ws)
|
|
483
|
-
page.handle_invoke_method_result(Protocol.normalize_invoke_method_result_payload(payload))
|
|
484
|
-
end
|
|
485
|
-
|
|
486
|
-
def on_control_event(ws, payload)
|
|
487
|
-
event = Protocol.normalize_control_event_payload(payload)
|
|
488
|
-
page = fetch_page(ws)
|
|
489
|
-
return if event["target"].nil? || event["name"].to_s.empty?
|
|
490
|
-
|
|
491
|
-
page.dispatch_event(
|
|
492
|
-
target: event["target"],
|
|
493
|
-
name: event["name"],
|
|
494
|
-
data: normalize_event_data(event["data"])
|
|
495
|
-
)
|
|
496
|
-
end
|
|
497
|
-
|
|
498
|
-
def on_update_control(ws, payload)
|
|
499
|
-
update = Protocol.normalize_update_control_payload(payload)
|
|
500
|
-
page = fetch_page(ws)
|
|
501
|
-
return if update["id"].nil?
|
|
502
|
-
|
|
503
|
-
page.apply_client_update(update["id"], update["props"] || {})
|
|
504
|
-
end
|
|
505
|
-
|
|
506
|
-
def fetch_page(ws)
|
|
507
|
-
page = @sessions_mutex.synchronize { @sessions[ws.session_key] }
|
|
508
|
-
raise "Session not found" unless page
|
|
509
|
-
|
|
510
|
-
page
|
|
511
|
-
end
|
|
512
|
-
|
|
513
|
-
def normalize_event_data(value)
|
|
514
|
-
case value
|
|
515
|
-
when Hash
|
|
516
|
-
value.each_with_object({}) { |(k, v), out| out[k.to_sym] = normalize_event_data(v) }
|
|
517
|
-
when Array
|
|
518
|
-
value.map { |entry| normalize_event_data(entry) }
|
|
519
|
-
else
|
|
520
|
-
value
|
|
521
|
-
end
|
|
522
|
-
end
|
|
523
|
-
|
|
524
|
-
def send_message(ws, action, payload)
|
|
525
|
-
return if ws.nil? || ws.closed?
|
|
526
|
-
|
|
527
|
-
message = [action, payload]
|
|
528
|
-
ws.send_binary(Ruflet::WireCodec.pack(message))
|
|
529
|
-
rescue StandardError => e
|
|
530
|
-
unless disconnect_error?(e)
|
|
531
|
-
warn "send error: #{e.class}: #{e.message}"
|
|
532
|
-
end
|
|
533
|
-
remove_session(ws)
|
|
534
|
-
unregister_connection(ws)
|
|
535
|
-
ws&.close
|
|
536
|
-
end
|
|
537
|
-
|
|
538
|
-
def disconnect_error?(error)
|
|
539
|
-
return true if error.is_a?(IOError)
|
|
540
|
-
return true if error.is_a?(Errno::EPIPE)
|
|
541
|
-
return true if error.is_a?(Errno::ECONNRESET)
|
|
542
|
-
return true if error.is_a?(Errno::ECONNABORTED)
|
|
543
|
-
return true if error.is_a?(Errno::ENOTCONN)
|
|
544
|
-
return true if error.is_a?(Errno::ESHUTDOWN)
|
|
545
|
-
return true if error.is_a?(Errno::EBADF)
|
|
546
|
-
return true if error.is_a?(Errno::EINVAL)
|
|
547
|
-
|
|
548
|
-
false
|
|
549
|
-
end
|
|
550
|
-
|
|
551
|
-
def pseudo_uuid
|
|
552
|
-
now = Process.clock_gettime(Process::CLOCK_REALTIME, :nanosecond)
|
|
553
|
-
rnd = rand(0..0xffff_ffff)
|
|
554
|
-
"%08x-%04x-%04x-%04x-%012x" % [
|
|
555
|
-
rnd,
|
|
556
|
-
now & 0xffff,
|
|
557
|
-
(now >> 16) & 0xffff,
|
|
558
|
-
(now >> 32) & 0xffff,
|
|
559
|
-
(now >> 48) & 0xffff_ffff_ffff
|
|
560
|
-
]
|
|
561
|
-
end
|
|
562
384
|
end
|
|
563
385
|
end
|
data/lib/ruflet/version.rb
CHANGED
data/lib/ruflet_server.rb
CHANGED
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.15
|
|
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.15
|
|
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.15
|
|
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
|