ruflet_server 0.0.18 → 0.0.20
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/server.rb +135 -20
- 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: c8ce7c65f8922fb96c632f08c3fbbfbe5ab4d2f51ef010aca2938dada94bbb06
|
|
4
|
+
data.tar.gz: '09a16364d80b69210248753a9eeb062a8d895be2cfa02844bd7850a96c4392d4'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0efb74cd09d74eb058e0cfbdaa3027483fb809552e15109070a80e3bad364f9d9d297173249033df16029c2f6c366ee24db3e40848d1adf7fa583927b4fe7a83
|
|
7
|
+
data.tar.gz: 908da326003ca5cfff54b1a608148afaf0f557c8d9871c602e8234abe6710b6e79c27f0f0de0b9bcda78368ec67735d62ba31b41f84af9f6b551791bde15a889
|
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
|
+
ruflet run
|
|
11
|
+
ruflet run --web
|
|
12
|
+
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/server.rb
CHANGED
|
@@ -124,21 +124,50 @@ module Ruflet
|
|
|
124
124
|
ws = @connections_mutex.synchronize { @connections[session_key] }
|
|
125
125
|
next unless ws
|
|
126
126
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
127
|
+
# Open dialogs, sheets, and snack bars are Navigator routes on the
|
|
128
|
+
# client; replacing the control tree does not pop them. Close them
|
|
129
|
+
# first, otherwise a reload while an overlay is open leaves it on
|
|
130
|
+
# screen wired to control ids the reloaded page cannot resolve.
|
|
131
|
+
begin
|
|
132
|
+
nil while current_page.respond_to?(:close_dialog) && current_page.close_dialog
|
|
133
|
+
rescue StandardError
|
|
134
|
+
nil
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
if current_page.respond_to?(:reset_for_reload!)
|
|
138
|
+
# Re-render on the live page. The client's overlay/service/dialogs
|
|
139
|
+
# containers stay mounted; a recreated Page would re-send them,
|
|
140
|
+
# replacing their client-side instances and detaching them — after
|
|
141
|
+
# which dialog and service patches are silently ignored.
|
|
142
|
+
current_page.reset_for_reload!
|
|
143
|
+
@app_block.call(current_page)
|
|
144
|
+
# Clear page-level chrome the reloaded block dropped and flush the
|
|
145
|
+
# overlay (appbar/drawer/FAB live in the view and rebuild wholesale;
|
|
146
|
+
# page props and the mounted overlay need explicit updates).
|
|
147
|
+
current_page.finalize_reload! if current_page.respond_to?(:finalize_reload!)
|
|
148
|
+
# Keeps route-driven apps on their current route: replays the
|
|
149
|
+
# route_change event when the block did not route itself.
|
|
150
|
+
current_page.replay_route_after_reload! if current_page.respond_to?(:replay_route_after_reload!)
|
|
151
|
+
current_page.update
|
|
152
|
+
else
|
|
153
|
+
# Older ruflet_core without reset_for_reload!: fall back to a fresh
|
|
154
|
+
# page (loses client-side container bindings until reconnect).
|
|
155
|
+
refreshed_page = Page.new(
|
|
156
|
+
session_id: current_page.session_id,
|
|
157
|
+
client_details: current_page.client_details,
|
|
158
|
+
sender: lambda do |action, payload|
|
|
159
|
+
send_message(ws, action, payload)
|
|
160
|
+
end
|
|
161
|
+
)
|
|
162
|
+
refreshed_page.title = "Ruflet App"
|
|
163
|
+
|
|
164
|
+
@sessions_mutex.synchronize do
|
|
165
|
+
@sessions[session_key] = refreshed_page
|
|
132
166
|
end
|
|
133
|
-
)
|
|
134
|
-
refreshed_page.title = "Ruflet App"
|
|
135
167
|
|
|
136
|
-
|
|
137
|
-
|
|
168
|
+
@app_block.call(refreshed_page)
|
|
169
|
+
refreshed_page.update
|
|
138
170
|
end
|
|
139
|
-
|
|
140
|
-
@app_block.call(refreshed_page)
|
|
141
|
-
refreshed_page.update
|
|
142
171
|
rescue StandardError => e
|
|
143
172
|
warn "reload error: #{e.class}: #{e.message}"
|
|
144
173
|
end
|
|
@@ -155,7 +184,10 @@ module Ruflet
|
|
|
155
184
|
|
|
156
185
|
def trap_signal(signal_name)
|
|
157
186
|
Signal.trap(signal_name) do
|
|
158
|
-
stop
|
|
187
|
+
# Trap context restricts Mutex use, so calling stop here raises
|
|
188
|
+
# ThreadError and the signal is silently swallowed. Only unwind the
|
|
189
|
+
# main thread; start's ensure performs the actual stop outside the
|
|
190
|
+
# trap context.
|
|
159
191
|
Thread.main.raise(Interrupt)
|
|
160
192
|
rescue StandardError
|
|
161
193
|
nil
|
|
@@ -324,14 +356,17 @@ module Ruflet
|
|
|
324
356
|
end
|
|
325
357
|
|
|
326
358
|
def handle_http_request(socket, path)
|
|
327
|
-
|
|
359
|
+
request_path = path.to_s.split("?", 2).first.to_s
|
|
360
|
+
return if serve_web_client(socket, request_path)
|
|
361
|
+
|
|
362
|
+
case request_path
|
|
328
363
|
when "/health"
|
|
329
364
|
write_http_response(socket, 200, "text/plain", "ok")
|
|
330
365
|
when "/"
|
|
331
366
|
write_http_response(socket, 200, "text/plain", "ruflet server")
|
|
332
367
|
else
|
|
333
|
-
if
|
|
334
|
-
serve_asset(socket,
|
|
368
|
+
if request_path.start_with?("/assets/")
|
|
369
|
+
serve_asset(socket, request_path)
|
|
335
370
|
else
|
|
336
371
|
write_http_response(socket, 404, "text/plain", "not found")
|
|
337
372
|
end
|
|
@@ -341,6 +376,37 @@ module Ruflet
|
|
|
341
376
|
write_http_response(socket, 500, "text/plain", "server error")
|
|
342
377
|
end
|
|
343
378
|
|
|
379
|
+
# The Flutter web client is served from this same port so that it loads and
|
|
380
|
+
# opens its websocket on one origin. Without that the client cannot reach
|
|
381
|
+
# /ws at all.
|
|
382
|
+
def web_client_root
|
|
383
|
+
return @web_client_root if defined?(@web_client_root)
|
|
384
|
+
|
|
385
|
+
configured = ENV["RUFLET_WEB_CLIENT_DIR"].to_s.strip
|
|
386
|
+
@web_client_root =
|
|
387
|
+
if configured.empty? || !File.directory?(configured)
|
|
388
|
+
nil
|
|
389
|
+
else
|
|
390
|
+
File.expand_path(configured)
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
def serve_web_client(socket, request_path)
|
|
395
|
+
root = web_client_root
|
|
396
|
+
return false unless root
|
|
397
|
+
|
|
398
|
+
relative = request_path
|
|
399
|
+
relative = "/index.html" if relative.empty? || relative == "/"
|
|
400
|
+
candidate = File.expand_path(File.join(root, relative))
|
|
401
|
+
# Never serve outside the client bundle.
|
|
402
|
+
return false unless candidate == root || candidate.start_with?("#{root}#{File::SEPARATOR}")
|
|
403
|
+
return false unless File.file?(candidate)
|
|
404
|
+
|
|
405
|
+
content = read_binary_file(candidate)
|
|
406
|
+
write_http_response(socket, 200, content_type_for(candidate), content, binary: true)
|
|
407
|
+
true
|
|
408
|
+
end
|
|
409
|
+
|
|
344
410
|
def serve_asset(socket, path)
|
|
345
411
|
asset_path = resolve_asset_path(path)
|
|
346
412
|
unless asset_path
|
|
@@ -392,6 +458,28 @@ module Ruflet
|
|
|
392
458
|
"image/webp"
|
|
393
459
|
when ".svg"
|
|
394
460
|
"image/svg+xml"
|
|
461
|
+
when ".html", ".htm"
|
|
462
|
+
"text/html; charset=utf-8"
|
|
463
|
+
when ".js", ".mjs"
|
|
464
|
+
"text/javascript; charset=utf-8"
|
|
465
|
+
when ".css"
|
|
466
|
+
"text/css; charset=utf-8"
|
|
467
|
+
when ".json", ".map"
|
|
468
|
+
"application/json; charset=utf-8"
|
|
469
|
+
when ".wasm"
|
|
470
|
+
"application/wasm"
|
|
471
|
+
when ".ttf"
|
|
472
|
+
"font/ttf"
|
|
473
|
+
when ".otf"
|
|
474
|
+
"font/otf"
|
|
475
|
+
when ".woff"
|
|
476
|
+
"font/woff"
|
|
477
|
+
when ".woff2"
|
|
478
|
+
"font/woff2"
|
|
479
|
+
when ".ico"
|
|
480
|
+
"image/x-icon"
|
|
481
|
+
when ".txt", ".symbols"
|
|
482
|
+
"text/plain; charset=utf-8"
|
|
395
483
|
else
|
|
396
484
|
"application/octet-stream"
|
|
397
485
|
end
|
|
@@ -533,11 +621,23 @@ module Ruflet
|
|
|
533
621
|
normalized = Protocol.normalize_register_payload(payload)
|
|
534
622
|
session_id = normalized["session_id"].to_s.empty? ? pseudo_uuid : normalized["session_id"]
|
|
535
623
|
|
|
624
|
+
# Run the app block BEFORE responding and ship the complete state in the
|
|
625
|
+
# register response (page_patch), like Flet. The client merges that map
|
|
626
|
+
# by control id, keeping existing instances alive — required for
|
|
627
|
+
# reconnecting clients (backend restarts) whose control store persists.
|
|
628
|
+
# Incremental op patches sent during the block are superseded by the
|
|
629
|
+
# full state and dropped; everything else is flushed afterwards.
|
|
630
|
+
registered = false
|
|
631
|
+
buffered = []
|
|
536
632
|
page = Page.new(
|
|
537
633
|
session_id: session_id,
|
|
538
634
|
client_details: normalized,
|
|
539
635
|
sender: lambda do |action, msg_payload|
|
|
540
|
-
|
|
636
|
+
if registered
|
|
637
|
+
send_message(ws, action, msg_payload)
|
|
638
|
+
else
|
|
639
|
+
buffered << [action, msg_payload]
|
|
640
|
+
end
|
|
541
641
|
end
|
|
542
642
|
)
|
|
543
643
|
|
|
@@ -547,14 +647,29 @@ module Ruflet
|
|
|
547
647
|
@sessions[ws.session_key] = page
|
|
548
648
|
end
|
|
549
649
|
|
|
650
|
+
@app_block.call(page)
|
|
651
|
+
|
|
652
|
+
page_patch = page.respond_to?(:register_page_patch) ? page.register_page_patch : {}
|
|
653
|
+
response = begin
|
|
654
|
+
Protocol.register_response(session_id: session_id, page_patch: page_patch)
|
|
655
|
+
rescue ArgumentError
|
|
656
|
+
# Older ruflet_core without the page_patch parameter.
|
|
657
|
+
page_patch = {}
|
|
658
|
+
Protocol.register_response(session_id: session_id)
|
|
659
|
+
end
|
|
550
660
|
initial_response = [
|
|
551
661
|
Protocol::ACTIONS[:register_client],
|
|
552
|
-
|
|
662
|
+
response
|
|
553
663
|
]
|
|
554
664
|
ws.send_binary(Ruflet::WireCodec.pack(initial_response))
|
|
665
|
+
registered = true
|
|
555
666
|
|
|
556
|
-
|
|
557
|
-
|
|
667
|
+
buffered.each do |action, msg_payload|
|
|
668
|
+
next if action == Protocol::ACTIONS[:patch_control]
|
|
669
|
+
|
|
670
|
+
send_message(ws, action, msg_payload)
|
|
671
|
+
end
|
|
672
|
+
page.update if page_patch.empty?
|
|
558
673
|
rescue StandardError => e
|
|
559
674
|
send_message(ws, Protocol::ACTIONS[:session_crashed], { "message" => e.message })
|
|
560
675
|
raise e
|
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.20
|
|
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.20
|
|
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.20
|
|
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
|