ruflet_server 0.0.16 → 0.0.18

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: b2720e5cd2f2bbe7ea1f8c6470c420fcb2b84bb8c6c3bab83ca12b5ea64633d5
4
- data.tar.gz: 410a06961fed394deb2b0bdc194cccede9c407de205b5a8e977da1c165290e68
3
+ metadata.gz: 9009d8e648b2101fcaf583feae53f3f5f6d9d8975849960d44c87f9a37f23597
4
+ data.tar.gz: 58d04690e2b0395698a343722a5e0e7c69bb351c918a2979f11d47f7e14d95ab
5
5
  SHA512:
6
- metadata.gz: e9d1f379990a194e710220e0cc872501c3ce9da07e7e9149b5eaf4377ab669f5d74cc814bb6c8cf37dec3f647ea767074c1aa5c49daab87a584da3f9909d9846
7
- data.tar.gz: c59c37531ebbd43796a7117d843208680562f18e09d939aeb63ab05b5560faa79fe7fbc5014de7fd4dc413f2be80d949f4cb1a4d802aa421ed4e41aa1ca325fb
6
+ metadata.gz: bfff7024cc42755729901dca5fbfe79b494c9167aecd40e01a65b153614dc004394406a2b6fe56844b88fbfce84fdd66b3ee08e250b8b11edeed5d34c652adb6
7
+ data.tar.gz: f958db4dacb8d0b6a17b1523e51e72e8374cb39c9eb32e69646d822cd6c2b7fa79a347ef1bde5fca3e71abfeeecdb0228fdec7f4936916e3d389ca1db52fc557
data/README.md CHANGED
@@ -1,17 +1,3 @@
1
1
  # ruflet_server
2
2
 
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.
3
+ Part of Ruflet monorepo.
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Ruflet
4
4
  class WebSocketConnection
5
+ TASK_IO_POLL_INTERVAL = 0.01
5
6
  # Ruflet control messages are small; anything much larger is invalid or hostile.
6
7
  MAX_FRAME_PAYLOAD_BYTES = 16 * 1024 * 1024
7
8
 
@@ -45,7 +46,27 @@ module Ruflet
45
46
  when 0xA
46
47
  read_message
47
48
  when 0x1, 0x2
48
- payload
49
+ return payload if frame[:fin]
50
+
51
+ message = payload.dup
52
+ loop do
53
+ continuation = read_frame
54
+ return nil if continuation.nil?
55
+
56
+ case continuation[:opcode]
57
+ when 0x9
58
+ send_frame(0xA, continuation[:payload])
59
+ next
60
+ when 0xA
61
+ next
62
+ when 0x0
63
+ message << continuation[:payload]
64
+ return message if continuation[:fin]
65
+ return nil if message.bytesize > MAX_FRAME_PAYLOAD_BYTES
66
+ else
67
+ return nil
68
+ end
69
+ end
49
70
  else
50
71
  read_message
51
72
  end
@@ -70,6 +91,7 @@ module Ruflet
70
91
  b1 = header.getbyte(0)
71
92
  b2 = header.getbyte(1)
72
93
 
94
+ fin = (b1 & 0x80) != 0
73
95
  masked = (b2 & 0x80) != 0
74
96
  payload_len = b2 & 0x7f
75
97
 
@@ -92,7 +114,7 @@ module Ruflet
92
114
 
93
115
  payload = unmask(payload, masking_key) if masked
94
116
 
95
- { opcode: b1 & 0x0f, payload: payload }
117
+ { fin: fin, opcode: b1 & 0x0f, payload: payload }
96
118
  end
97
119
 
98
120
  def send_frame(opcode, payload)
@@ -110,8 +132,8 @@ module Ruflet
110
132
  end
111
133
 
112
134
  @write_mutex.synchronize do
113
- @socket.write(header)
114
- @socket.write(bytes) unless bytes.empty?
135
+ @socket.write(header + bytes)
136
+ @socket.flush if @socket.respond_to?(:flush)
115
137
  end
116
138
  end
117
139
 
@@ -132,15 +154,33 @@ module Ruflet
132
154
  chunk.force_encoding(Encoding::BINARY)
133
155
 
134
156
  while chunk.bytesize < length
135
- part = @socket.read(length - chunk.bytesize)
157
+ part = if task_scheduler?
158
+ @socket.recv_nonblock(length - chunk.bytesize)
159
+ else
160
+ @socket.read(length - chunk.bytesize)
161
+ end
136
162
  return nil if part.nil? || part.empty?
137
163
 
138
164
  chunk << part
139
165
  end
140
166
 
141
167
  chunk
142
- rescue IOError, SystemCallError
168
+ rescue StandardError => error
169
+ if task_scheduler? && would_block_error?(error)
170
+ sleep TASK_IO_POLL_INTERVAL
171
+ retry
172
+ end
173
+
143
174
  nil
144
175
  end
176
+
177
+ def task_scheduler?
178
+ concurrent = Object.const_defined?(:Task) || (Thread.respond_to?(:cooperative?) && Thread.cooperative?)
179
+ concurrent && @socket.respond_to?(:recv_nonblock)
180
+ end
181
+
182
+ def would_block_error?(error)
183
+ %w[Errno::EAGAIN Errno::EWOULDBLOCK IO::EAGAINWaitReadable IO::WaitReadable].include?(error.class.to_s)
184
+ end
145
185
  end
146
186
  end
@@ -2,14 +2,15 @@
2
2
 
3
3
  module Ruflet
4
4
  class WireCodec
5
- PICKER_DATE_CONTROLS = %w[CupertinoDatePicker DatePicker DateRangePicker].freeze
6
- PICKER_DATE_KEYS = %w[current_date end_value first_date last_date start_value value].freeze
7
- PICKER_TIME_CONTROLS = %w[TimePicker].freeze
8
- PICKER_TIME_KEYS = %w[value].freeze
9
-
10
5
  class << self
11
- def pack(value, context = nil)
6
+ def pack(value)
12
7
  case value
8
+ when Ruflet::Protocol::DateTimeValue
9
+ pack_extension(1, value)
10
+ when Ruflet::Protocol::TimeOfDayValue
11
+ pack_extension(2, value)
12
+ when Ruflet::Protocol::DurationValue
13
+ pack_extension(3, value)
13
14
  when NilClass
14
15
  "\xc0".b
15
16
  when TrueClass
@@ -21,14 +22,11 @@ module Ruflet
21
22
  when Float
22
23
  "\xcb".b + [value].pack("G")
23
24
  when String
24
- return pack_ext(1, normalize_date_ext(value)) if context == :date
25
- return pack_ext(2, normalize_time_ext(value)) if context == :time
26
-
27
25
  binary_string?(value) ? pack_binary(value) : pack_string(value)
28
26
  when Symbol
29
27
  pack_string(value.to_s)
30
28
  when Array
31
- pack_array(value, context)
29
+ pack_array(value)
32
30
  when Hash
33
31
  pack_map(value)
34
32
  else
@@ -43,6 +41,20 @@ module Ruflet
43
41
 
44
42
  private
45
43
 
44
+ def pack_extension(type, value)
45
+ bytes = value.to_s.b
46
+ length = bytes.bytesize
47
+ header =
48
+ if length <= 0xff
49
+ "\xc7".b + [length].pack("C")
50
+ elsif length <= 0xffff
51
+ "\xc8".b + [length].pack("n")
52
+ else
53
+ "\xc9".b + [length].pack("N")
54
+ end
55
+ header + [type].pack("c") + bytes
56
+ end
57
+
46
58
  def pack_integer(value)
47
59
  if value >= 0
48
60
  return [value].pack("C") if value <= 0x7f
@@ -94,41 +106,7 @@ module Ruflet
94
106
  value.encoding == Encoding::BINARY || !value.valid_encoding?
95
107
  end
96
108
 
97
- def pack_ext(type, payload)
98
- bytes = payload.to_s.b
99
- len = bytes.bytesize
100
-
101
- if len == 1
102
- "\xd4".b + [type].pack("c") + bytes
103
- elsif len == 2
104
- "\xd5".b + [type].pack("c") + bytes
105
- elsif len == 4
106
- "\xd6".b + [type].pack("c") + bytes
107
- elsif len == 8
108
- "\xd7".b + [type].pack("c") + bytes
109
- elsif len == 16
110
- "\xd8".b + [type].pack("c") + bytes
111
- elsif len <= 0xff
112
- "\xc7".b + [len, type].pack("Cc") + bytes
113
- elsif len <= 0xffff
114
- "\xc8".b + [len, type].pack("nc") + bytes
115
- else
116
- "\xc9".b + [len, type].pack("Nc") + bytes
117
- end
118
- end
119
-
120
- def normalize_date_ext(value)
121
- raw = value.to_s
122
- return "#{raw}T00:00:00+00:00" if raw.match?(/\A\d{4}-\d{2}-\d{2}\z/)
123
-
124
- raw
125
- end
126
-
127
- def normalize_time_ext(value)
128
- value.to_s
129
- end
130
-
131
- def pack_array(value, context = nil)
109
+ def pack_array(value)
132
110
  len = value.length
133
111
  head =
134
112
  if len <= 15
@@ -140,7 +118,7 @@ module Ruflet
140
118
  end
141
119
 
142
120
  body = +"".b
143
- value.each { |item| body << pack(item, context) }
121
+ value.each { |item| body << pack(item) }
144
122
  head + body
145
123
  end
146
124
 
@@ -157,22 +135,13 @@ module Ruflet
157
135
  end
158
136
 
159
137
  body = +"".b
160
- control_type = pairs["_c"].to_s
161
138
  pairs.each do |k, v|
162
139
  body << pack(k)
163
- body << pack(v, picker_prop_context(control_type, k))
140
+ body << pack(v)
164
141
  end
165
142
  head + body
166
143
  end
167
144
 
168
- def picker_prop_context(control_type, key)
169
- key = key.to_s
170
- return :date if PICKER_DATE_CONTROLS.include?(control_type) && PICKER_DATE_KEYS.include?(key)
171
- return :time if PICKER_TIME_CONTROLS.include?(control_type) && PICKER_TIME_KEYS.include?(key)
172
-
173
- nil
174
- end
175
-
176
145
  def read_value(reader)
177
146
  marker = reader.read_u8
178
147
 
@@ -247,15 +216,12 @@ module Ruflet
247
216
 
248
217
  def read_ext(reader, size)
249
218
  type = reader.read_i8
250
- data = reader.read_exact(size)
251
-
219
+ value = reader.read_exact(size).force_encoding("UTF-8")
252
220
  case type
253
- when 1, 2, 4
254
- data.dup.force_encoding("UTF-8")
255
- when 3
256
- data.to_i
257
- else
258
- data
221
+ when 1 then Ruflet::Protocol.date_time(value)
222
+ when 2 then Ruflet::Protocol.time_of_day(value)
223
+ when 3 then Ruflet::Protocol.duration(value)
224
+ else value
259
225
  end
260
226
  end
261
227
  end
data/lib/ruflet/server.rb CHANGED
@@ -7,18 +7,13 @@ 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"
11
10
 
12
11
  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).
16
12
  class Server
17
- include ConnectionProtocol
18
-
19
13
  attr_reader :port
20
14
 
21
15
  WEBSOCKET_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
16
+ TASK_IO_POLL_INTERVAL = 0.01
22
17
 
23
18
  def initialize(host: "0.0.0.0", port: 8550, &app_block)
24
19
  @host = host
@@ -41,6 +36,13 @@ module Ruflet
41
36
  end
42
37
 
43
38
  def start
39
+ if Object.const_defined?(:Task) && Task.current.name == "main"
40
+ server = self
41
+ task = Task.new(name: "ruflet-server") { server.start }
42
+ Task.run
43
+ return task
44
+ end
45
+
44
46
  previous_signals = trap_stop_signals
45
47
  bind_server_socket!
46
48
  @running = true
@@ -53,6 +55,12 @@ module Ruflet
53
55
  restore_stop_signals(previous_signals)
54
56
  end
55
57
 
58
+ # For Rack-hosted mode: caller already performed the HTTP upgrade.
59
+ def handle_upgraded_socket(io)
60
+ ws = Ruflet::WebSocketConnection.new(io)
61
+ run_connection(ws)
62
+ end
63
+
56
64
  def bind_server_socket!(max_attempts: 100)
57
65
  requested = @port.to_i
58
66
  candidate = requested
@@ -61,11 +69,11 @@ module Ruflet
61
69
  attempts.times do
62
70
  begin
63
71
  @server_socket = TCPServer.new(@host, candidate)
64
- @port = candidate
65
- if @port != requested && ENV["RUFLET_SUPPRESS_SERVER_BANNER"] != "1"
66
- warn "Port #{requested} is busy; using #{@port}."
72
+ @port = @server_socket.addr[1]
73
+ publish_runtime_port
74
+ if requested > 0 && @port != requested && ENV["RUFLET_SUPPRESS_SERVER_BANNER"] != "1"
75
+ warn "Requested port #{requested} is busy; bound to #{@port}"
67
76
  end
68
- publish_bound_port!
69
77
  return
70
78
  rescue Errno::EADDRINUSE
71
79
  candidate += 1
@@ -81,7 +89,6 @@ module Ruflet
81
89
  return unless @running || @server_socket
82
90
 
83
91
  @running = false
84
- remove_port_file!
85
92
 
86
93
  server = @server_socket
87
94
  @server_socket = nil
@@ -101,6 +108,15 @@ module Ruflet
101
108
  end
102
109
  end
103
110
 
111
+ def publish_runtime_port
112
+ path = ENV["RUFLET_RUNTIME_PORT_FILE"].to_s
113
+ return if path.empty?
114
+
115
+ File.open(path, "w") { |file| file.write(@port.to_s) }
116
+ rescue StandardError => error
117
+ warn "Unable to publish Ruflet runtime port: #{error.message}"
118
+ end
119
+
104
120
  def reload_app!
105
121
  snapshots = @sessions_mutex.synchronize { @sessions.to_a }
106
122
 
@@ -130,30 +146,6 @@ module Ruflet
130
146
 
131
147
  private
132
148
 
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
-
157
149
  def trap_stop_signals
158
150
  {
159
151
  "INT" => trap_signal("INT"),
@@ -194,23 +186,39 @@ module Ruflet
194
186
  end
195
187
 
196
188
  def accept_client_socket
197
- accepted = @server_socket.accept
189
+ accepted = task_scheduler? ? @server_socket.accept_nonblock : @server_socket.accept
198
190
  accepted.is_a?(Array) ? accepted.first : accepted
199
191
  rescue IOError, Errno::EBADF
200
192
  nil
201
193
  rescue StandardError => e
202
194
  return nil unless @running && @server_socket
203
195
 
196
+ if task_scheduler? && would_block_error?(e)
197
+ sleep TASK_IO_POLL_INTERVAL
198
+ Thread.pass if Thread.respond_to?(:cooperative?)
199
+ retry
200
+ end
201
+
204
202
  warn "accept error: #{e.class}: #{e.message}"
205
203
  warn e.backtrace.join("\n") if e.backtrace
206
204
  nil
207
205
  end
208
206
 
207
+ def task_scheduler?
208
+ Object.const_defined?(:Task) || (Thread.respond_to?(:cooperative?) && Thread.cooperative?)
209
+ end
210
+
211
+ def would_block_error?(error)
212
+ %w[Errno::EAGAIN Errno::EWOULDBLOCK IO::EAGAINWaitReadable IO::WaitReadable].include?(error.class.to_s)
213
+ end
214
+
209
215
  def start_client_thread(socket)
210
- Thread.new(socket) do |client|
216
+ thread = Thread.new(socket) do |client|
211
217
  Thread.current.report_on_exception = false if Thread.current.respond_to?(:report_on_exception=)
212
218
  handle_socket(client)
213
219
  end
220
+ Thread.pass if task_scheduler?
221
+ thread
214
222
  end
215
223
 
216
224
  def handle_socket(socket)
@@ -233,26 +241,34 @@ module Ruflet
233
241
  warn e.backtrace.join("\n") if e.backtrace
234
242
  send_message(ws, Protocol::ACTIONS[:session_crashed], { "message" => e.message.to_s.dup.force_encoding("UTF-8") }) if ws
235
243
  ensure
236
- if ws
237
- close_connection(ws)
238
- else
239
- # Plain HTTP request: we answer with `Connection: close`, so we must
240
- # actually close the socket. Leaving it open exhausts the browser's
241
- # per-host connection pool and the later /ws upgrade never opens —
242
- # the app then hangs on its "connecting" screen.
243
- close_http_socket(socket)
244
- end
244
+ close_connection(ws)
245
245
  end
246
246
  end
247
247
 
248
- def close_http_socket(socket)
249
- socket.close if socket && !socket.closed?
250
- rescue StandardError
251
- nil
248
+ def run_connection(ws)
249
+ register_connection(ws)
250
+
251
+ while (raw = ws.read_message)
252
+ handle_message(ws, raw)
253
+ end
254
+ rescue StandardError => e
255
+ return if disconnect_error?(e)
256
+
257
+ warn "server error: #{e.class}: #{e.message}"
258
+ warn e.backtrace.join("\n") if e.backtrace
259
+ send_message(ws, Protocol::ACTIONS[:session_crashed], { "message" => e.message.to_s.dup.force_encoding("UTF-8") })
260
+ ensure
261
+ close_connection(ws)
262
+ end
263
+
264
+ def close_connection(ws)
265
+ remove_session(ws)
266
+ unregister_connection(ws)
267
+ ws&.close
252
268
  end
253
269
 
254
270
  def read_http_upgrade_request(socket)
255
- request_line = socket.gets("\r\n")
271
+ request_line = read_http_line(socket)
256
272
  return [nil, {}] if request_line.nil?
257
273
  return [nil, {}] unless request_line.include?(" ")
258
274
 
@@ -262,7 +278,7 @@ module Ruflet
262
278
 
263
279
  headers = {}
264
280
  loop do
265
- line = socket.gets("\r\n")
281
+ line = read_http_line(socket)
266
282
  break if line.nil? || line == "\r\n"
267
283
 
268
284
  key, value = line.split(":", 2)
@@ -272,10 +288,34 @@ module Ruflet
272
288
  end
273
289
 
274
290
  [path, headers]
291
+ ensure
292
+ @http_read_buffers&.delete(socket.object_id) if task_scheduler?
293
+ end
294
+
295
+ def read_http_line(socket)
296
+ return socket.gets("\r\n") unless task_scheduler? && socket.respond_to?(:recv_nonblock)
297
+
298
+ @http_read_buffers ||= {}
299
+ buffer = (@http_read_buffers[socket.object_id] ||= +"")
300
+ loop do
301
+ newline = buffer.index("\r\n")
302
+ return buffer.slice!(0, newline + 2) if newline
303
+
304
+ begin
305
+ part = socket.recv_nonblock(4096)
306
+ return nil if part.nil? || part.empty?
307
+
308
+ buffer << part
309
+ rescue StandardError => error
310
+ raise unless would_block_error?(error)
311
+
312
+ sleep TASK_IO_POLL_INTERVAL
313
+ end
314
+ end
275
315
  end
276
316
 
277
317
  def websocket_upgrade_request?(path, headers)
278
- return false unless path.to_s.split("?", 2).first == "/ws"
318
+ return false unless path == "/ws"
279
319
  return false unless headers["upgrade"]&.downcase == "websocket"
280
320
  return false unless headers["connection"]&.downcase&.include?("upgrade")
281
321
  return false if headers["sec-websocket-key"].to_s.empty?
@@ -284,95 +324,21 @@ module Ruflet
284
324
  end
285
325
 
286
326
  def handle_http_request(socket, path)
287
- clean = path.to_s.split("?", 2).first.split("#", 2).first
288
- return write_http_response(socket, 200, "text/plain", "ok") if clean == "/health"
289
-
290
- # In web mode the standalone backend also serves the Flutter web client,
291
- # so the browser loads the app and opens its websocket on this same
292
- # origin/port — no separate static server or proxy is needed.
293
- return serve_web_client(socket, clean) if web_client_root
294
-
295
- case clean
327
+ case path
328
+ when "/health"
329
+ write_http_response(socket, 200, "text/plain", "ok")
296
330
  when "/"
297
331
  write_http_response(socket, 200, "text/plain", "ruflet server")
298
332
  else
299
- if clean.start_with?("/assets/")
300
- serve_asset(socket, clean)
333
+ if path.start_with?("/assets/")
334
+ serve_asset(socket, path)
301
335
  else
302
336
  write_http_response(socket, 404, "text/plain", "not found")
303
337
  end
304
338
  end
305
339
  rescue StandardError => e
306
- # The browser routinely cancels in-flight asset requests (preloads,
307
- # duplicate connections); writing to a reset socket raises EPIPE/ECONNRESET
308
- # and is expected, not an error.
309
- return if disconnect_error?(e)
310
-
311
340
  warn "http error: #{e.class}: #{e.message}"
312
- begin
313
- write_http_response(socket, 500, "text/plain", "server error")
314
- rescue StandardError
315
- nil
316
- end
317
- end
318
-
319
- def web_client_root
320
- dir = ENV["RUFLET_WEB_CLIENT_DIR"].to_s
321
- return nil if dir.empty?
322
-
323
- full = File.expand_path(dir)
324
- File.directory?(full) ? full : nil
325
- end
326
-
327
- def serve_web_client(socket, path)
328
- root = web_client_root
329
-
330
- # Neutralize the Flutter service worker: when this dev server hops between
331
- # localhost ports a cached worker would otherwise keep a stale client
332
- # alive that reconnects to the wrong backend. This unregisters it and
333
- # clears caches so the browser always loads the current client.
334
- if path == "/flutter_service_worker.js"
335
- return write_http_response(socket, 200, "text/javascript", service_worker_reset_js, cache: false)
336
- end
337
-
338
- relative = path == "/" ? "index.html" : path.sub(%r{\A/}, "")
339
- full = File.expand_path(File.join(root, relative))
340
- if (full == root || full.start_with?(root + File::SEPARATOR)) && File.file?(full)
341
- return write_http_response(socket, 200, content_type_for(full), File.binread(full), binary: true, cache: false)
342
- end
343
-
344
- # App runtime assets (images referenced by the app) fall back to the
345
- # configured assets directory when not part of the client bundle.
346
- if path.start_with?("/assets/") && (asset = resolve_asset_path(path))
347
- return write_http_response(socket, 200, content_type_for(asset), File.binread(asset), binary: true, cache: false)
348
- end
349
-
350
- # SPA fallback: serve index.html for extension-less route paths.
351
- index = File.join(root, "index.html")
352
- if File.extname(path).empty? && File.file?(index)
353
- return write_http_response(socket, 200, "text/html", File.binread(index), binary: true, cache: false)
354
- end
355
-
356
- write_http_response(socket, 404, "text/plain", "not found")
357
- end
358
-
359
- # A no-op service worker: it registers cleanly (so Flutter's loader, which
360
- # awaits navigator.serviceWorker.ready, never hangs), wipes any caches a
361
- # previous run left behind, claims the page, and installs NO fetch handler —
362
- # so every request (including the app shell) goes straight to the network and
363
- # the client always loads fresh and connects to the current origin/port.
364
- # (Self-unregistering here can leave serviceWorker.ready unresolved.)
365
- def service_worker_reset_js
366
- <<~JS
367
- self.addEventListener('install', function (e) { self.skipWaiting(); });
368
- self.addEventListener('activate', function (e) {
369
- e.waitUntil((async function () {
370
- var keys = await caches.keys();
371
- await Promise.all(keys.map(function (k) { return caches.delete(k); }));
372
- await self.clients.claim();
373
- })());
374
- });
375
- JS
341
+ write_http_response(socket, 500, "text/plain", "server error")
376
342
  end
377
343
 
378
344
  def serve_asset(socket, path)
@@ -382,16 +348,23 @@ module Ruflet
382
348
  return
383
349
  end
384
350
 
385
- content = File.binread(asset_path)
351
+ content = read_binary_file(asset_path)
386
352
  write_http_response(socket, 200, content_type_for(asset_path), content, binary: true)
387
353
  end
388
354
 
355
+ def read_binary_file(path)
356
+ File.open(path, "rb") { |file| file.read }
357
+ end
358
+
389
359
  def resolve_asset_path(path)
390
360
  root = assets_root
391
361
  return nil unless root
392
362
 
393
363
  root = File.expand_path(root)
394
- relative = path.sub(%r{\A/assets/}, "")
364
+ prefix = "/assets/"
365
+ return nil unless path.start_with?(prefix)
366
+
367
+ relative = path[prefix.length..-1].to_s
395
368
  full = File.expand_path(File.join(root, relative))
396
369
  return nil unless full.start_with?(root + File::SEPARATOR) || full == root
397
370
  return nil unless File.file?(full)
@@ -403,39 +376,28 @@ module Ruflet
403
376
  root = ENV["RUFLET_ASSETS_DIR"].to_s
404
377
  return root unless root.empty?
405
378
 
406
- embedded_root = defined?($__ruflet_app_root) ? $__ruflet_app_root.to_s : ""
407
- unless embedded_root.empty?
408
- embedded_assets = File.join(embedded_root, "assets")
409
- return embedded_assets if File.directory?(embedded_assets)
410
- end
411
-
412
379
  default_root = File.join(Dir.pwd, "assets")
413
380
  File.directory?(default_root) ? default_root : nil
414
381
  end
415
382
 
416
383
  def content_type_for(path)
417
384
  case File.extname(path).downcase
418
- when ".html", ".htm" then "text/html; charset=utf-8"
419
- when ".js", ".mjs" then "text/javascript; charset=utf-8"
420
- when ".json", ".map" then "application/json; charset=utf-8"
421
- when ".css" then "text/css; charset=utf-8"
422
- when ".wasm" then "application/wasm"
423
- when ".png" then "image/png"
424
- when ".jpg", ".jpeg" then "image/jpeg"
425
- when ".gif" then "image/gif"
426
- when ".webp" then "image/webp"
427
- when ".svg" then "image/svg+xml"
428
- when ".ico" then "image/x-icon"
429
- when ".ttf" then "font/ttf"
430
- when ".otf" then "font/otf"
431
- when ".woff" then "font/woff"
432
- when ".woff2" then "font/woff2"
433
- when ".txt" then "text/plain; charset=utf-8"
434
- else "application/octet-stream"
385
+ when ".png"
386
+ "image/png"
387
+ when ".jpg", ".jpeg"
388
+ "image/jpeg"
389
+ when ".gif"
390
+ "image/gif"
391
+ when ".webp"
392
+ "image/webp"
393
+ when ".svg"
394
+ "image/svg+xml"
395
+ else
396
+ "application/octet-stream"
435
397
  end
436
398
  end
437
399
 
438
- def write_http_response(socket, status, content_type, body, binary: false, cache: true)
400
+ def write_http_response(socket, status, content_type, body, binary: false)
439
401
  reason = {
440
402
  200 => "OK",
441
403
  404 => "Not Found",
@@ -448,10 +410,6 @@ module Ruflet
448
410
  socket.write("HTTP/1.1 #{status} #{reason}\r\n")
449
411
  socket.write("Content-Type: #{content_type}\r\n")
450
412
  socket.write("Content-Length: #{length}\r\n")
451
- unless cache
452
- socket.write("Cache-Control: no-store, no-cache, must-revalidate, max-age=0\r\n")
453
- socket.write("Pragma: no-cache\r\n")
454
- end
455
413
  socket.write("Connection: close\r\n")
456
414
  socket.write("\r\n")
457
415
  socket.write(body_str)
@@ -467,8 +425,15 @@ module Ruflet
467
425
  socket.write("\r\n")
468
426
  end
469
427
 
470
- # ConnectionProtocol hooks: track live sockets so #stop can close them.
471
- def connection_opened(ws)
428
+ def remove_session(ws)
429
+ return unless ws
430
+
431
+ @sessions_mutex.synchronize do
432
+ @sessions.delete(ws.session_key)
433
+ end
434
+ end
435
+
436
+ def register_connection(ws)
472
437
  return unless ws
473
438
 
474
439
  @connections_mutex.synchronize do
@@ -476,7 +441,7 @@ module Ruflet
476
441
  end
477
442
  end
478
443
 
479
- def connection_closed(ws)
444
+ def unregister_connection(ws)
480
445
  return unless ws
481
446
 
482
447
  @connections_mutex.synchronize do
@@ -484,5 +449,197 @@ module Ruflet
484
449
  end
485
450
  end
486
451
 
452
+ def handle_message(ws, raw)
453
+ action, payload = decode_incoming(raw)
454
+ payload ||= {}
455
+
456
+ warn "incoming action=#{action.inspect}" if ENV["RUFLET_DEBUG"] == "1"
457
+
458
+ case action
459
+ when Protocol::ACTIONS[:register_client], Protocol::ACTIONS[:register_web_client]
460
+ on_register_client(ws, payload)
461
+ when Protocol::ACTIONS[:control_event], Protocol::ACTIONS[:page_event_from_web]
462
+ on_control_event(ws, payload)
463
+ when Protocol::ACTIONS[:update_control], Protocol::ACTIONS[:update_control_props]
464
+ on_update_control(ws, payload)
465
+ when Protocol::ACTIONS[:invoke_control_method]
466
+ on_invoke_control_method(ws, payload)
467
+ when Protocol::ACTIONS[:python_output]
468
+ nil
469
+ else
470
+ raise "Unknown action: #{action.inspect}"
471
+ end
472
+ rescue StandardError => e
473
+ # A per-message handler error (e.g. an event callback that renders a
474
+ # control the runtime does not implement) must not tear down the whole
475
+ # WebSocket connection — that would disconnect the client on every
476
+ # navigation. Log it and keep the session alive.
477
+ warn "[embedded server] handle_message error: #{e.class}: #{e.message}"
478
+ warn e.backtrace.join("\n") if e.backtrace && ENV["RUFLET_DEBUG"] == "1"
479
+ report_runtime_error(e, "handle_message")
480
+ end
481
+
482
+ def report_runtime_error(error, context)
483
+ path = ENV["RUFLET_RUNTIME_ERROR_FILE"].to_s
484
+ return if path.empty?
485
+
486
+ lines = ["#{context}: #{error.class}: #{error.message}"]
487
+ lines.concat(error.backtrace) if error.backtrace
488
+ File.open(path, "w") { |file| file.write(lines.join("\n")) }
489
+ rescue StandardError => report_error
490
+ warn "runtime error reporting failed: #{report_error.class}: #{report_error.message}"
491
+ end
492
+
493
+ def decode_incoming(raw)
494
+ parsed = normalize_incoming(Ruflet::WireCodec.unpack(raw.to_s.b))
495
+
496
+ if parsed.is_a?(Array) && parsed.length >= 2
497
+ return [parsed[0], parsed[1]]
498
+ end
499
+
500
+ if parsed.is_a?(Hash)
501
+ action = parsed["action"] || parsed[:action]
502
+ payload = parsed["payload"] || parsed[:payload]
503
+ return [action, payload] unless action.nil?
504
+
505
+ if (parsed.key?("target") || parsed.key?(:target)) && (parsed.key?("name") || parsed.key?(:name))
506
+ return [Protocol::ACTIONS[:control_event], parsed]
507
+ end
508
+ end
509
+
510
+ raise "Unsupported payload format"
511
+ end
512
+
513
+ def normalize_incoming(value)
514
+ case value
515
+ when String
516
+ value.dup.force_encoding("UTF-8")
517
+ when Integer, Float, TrueClass, FalseClass, NilClass
518
+ value
519
+ when Symbol
520
+ value.to_s
521
+ when Array
522
+ value.map { |v| normalize_incoming(v) }
523
+ when Hash
524
+ value.each_with_object({}) do |(k, v), out|
525
+ out[k.to_s] = normalize_incoming(v)
526
+ end
527
+ else
528
+ value.to_s
529
+ end
530
+ end
531
+
532
+ def on_register_client(ws, payload)
533
+ normalized = Protocol.normalize_register_payload(payload)
534
+ session_id = normalized["session_id"].to_s.empty? ? pseudo_uuid : normalized["session_id"]
535
+
536
+ page = Page.new(
537
+ session_id: session_id,
538
+ client_details: normalized,
539
+ sender: lambda do |action, msg_payload|
540
+ send_message(ws, action, msg_payload)
541
+ end
542
+ )
543
+
544
+ page.title = "Ruflet App"
545
+
546
+ @sessions_mutex.synchronize do
547
+ @sessions[ws.session_key] = page
548
+ end
549
+
550
+ initial_response = [
551
+ Protocol::ACTIONS[:register_client],
552
+ Protocol.register_response(session_id: session_id)
553
+ ]
554
+ ws.send_binary(Ruflet::WireCodec.pack(initial_response))
555
+
556
+ @app_block.call(page)
557
+ page.update
558
+ rescue StandardError => e
559
+ send_message(ws, Protocol::ACTIONS[:session_crashed], { "message" => e.message })
560
+ raise e
561
+ end
562
+
563
+ def on_invoke_control_method(ws, payload)
564
+ page = fetch_page(ws)
565
+ page.handle_invoke_method_result(payload)
566
+ end
567
+
568
+ def on_control_event(ws, payload)
569
+ event = Protocol.normalize_control_event_payload(payload)
570
+ page = fetch_page(ws)
571
+ return if event["target"].nil? || event["name"].to_s.empty?
572
+
573
+ page.dispatch_event(
574
+ target: event["target"],
575
+ name: event["name"],
576
+ data: normalize_event_data(event["data"])
577
+ )
578
+ end
579
+
580
+ def on_update_control(ws, payload)
581
+ update = Protocol.normalize_update_control_payload(payload)
582
+ page = fetch_page(ws)
583
+ return if update["id"].nil?
584
+
585
+ page.apply_client_update(update["id"], update["props"] || {})
586
+ end
587
+
588
+ def fetch_page(ws)
589
+ page = @sessions_mutex.synchronize { @sessions[ws.session_key] }
590
+ raise "Session not found" unless page
591
+
592
+ page
593
+ end
594
+
595
+ def normalize_event_data(value)
596
+ case value
597
+ when Hash
598
+ value.each_with_object({}) { |(k, v), out| out[k.to_sym] = normalize_event_data(v) }
599
+ when Array
600
+ value.map { |entry| normalize_event_data(entry) }
601
+ else
602
+ value
603
+ end
604
+ end
605
+
606
+ def send_message(ws, action, payload)
607
+ return if ws.nil? || ws.closed?
608
+
609
+ message = [action, payload]
610
+ packed = Ruflet::WireCodec.pack(message)
611
+ ws.send_binary(packed)
612
+ rescue StandardError => e
613
+ unless disconnect_error?(e)
614
+ warn "send error: #{e.class}: #{e.message}"
615
+ end
616
+ remove_session(ws)
617
+ unregister_connection(ws)
618
+ ws&.close
619
+ end
620
+
621
+ def disconnect_error?(error)
622
+ return true if error.is_a?(IOError)
623
+ return true if error.is_a?(Errno::EPIPE)
624
+ return true if error.is_a?(Errno::ECONNRESET)
625
+ return true if error.is_a?(Errno::ECONNABORTED)
626
+ return true if error.is_a?(Errno::ENOTCONN)
627
+ return true if error.is_a?(Errno::ESHUTDOWN)
628
+ return true if error.is_a?(Errno::EBADF)
629
+ return true if error.is_a?(Errno::EINVAL)
630
+
631
+ false
632
+ end
633
+
634
+ def pseudo_uuid
635
+ rnd = (rand(0..0xffff) << 16) | rand(0..0xffff)
636
+ "%08x-%04x-%04x-%04x-%012x" % [
637
+ rnd,
638
+ rand(0..0xffff),
639
+ rand(0..0xffff),
640
+ rand(0..0xffff),
641
+ (rand(0..0xffff) << 32) | (rand(0..0xffff) << 16) | rand(0..0xffff)
642
+ ]
643
+ end
487
644
  end
488
645
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ruflet
4
- VERSION = "0.0.16" unless const_defined?(:VERSION)
4
+ VERSION = "0.0.18" unless const_defined?(:VERSION)
5
5
  end
data/lib/ruflet_server.rb CHANGED
@@ -7,34 +7,13 @@ module Ruflet
7
7
  module_function
8
8
 
9
9
  def run(entrypoint = nil, host: "0.0.0.0", port: nil, &block)
10
- port = normalize_run_port(port || ENV["RUFLET_PORT"] || 8550)
11
10
  callback = entrypoint || block
12
11
  raise ArgumentError, "Ruflet.run requires a callable entrypoint or block" unless callback.respond_to?(:call)
13
-
14
- interceptor = run_interceptor
15
- if interceptor
16
- result = interceptor.call(entrypoint: callback, host: host, port: port)
17
- return result unless result == :pass
18
- end
12
+ port = resolved_run_port(port) if respond_to?(:resolved_run_port)
13
+ port = 8550 if port.nil?
19
14
 
20
15
  Server.new(host: host, port: port) do |page|
21
16
  callback.call(page)
22
17
  end.start
23
18
  end
24
-
25
- def run_interceptor
26
- return nil unless instance_variable_defined?(:@run_interceptors_mutex)
27
- return nil unless instance_variable_defined?(:@run_interceptors)
28
-
29
- @run_interceptors_mutex.synchronize { @run_interceptors.last }
30
- end
31
-
32
- def normalize_run_port(value)
33
- Integer(value)
34
- rescue ArgumentError, TypeError
35
- 8550
36
- end
37
- class << self
38
- private :run_interceptor, :normalize_run_port
39
- end
40
19
  end
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.16
4
+ version: 0.0.18
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.16
18
+ version: 0.0.18
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.16
25
+ version: 0.0.18
26
26
  description: Ruflet WebSocket server runtime compatible with Flet protocol.
27
27
  email:
28
28
  - adammusa2222@gmail.com
@@ -32,7 +32,6 @@ extra_rdoc_files: []
32
32
  files:
33
33
  - README.md
34
34
  - lib/ruflet/server.rb
35
- - lib/ruflet/server/connection_protocol.rb
36
35
  - lib/ruflet/server/web_socket_connection.rb
37
36
  - lib/ruflet/server/wire_codec.rb
38
37
  - lib/ruflet/version.rb
@@ -55,7 +54,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
55
54
  - !ruby/object:Gem::Version
56
55
  version: '0'
57
56
  requirements: []
58
- rubygems_version: 3.7.2
57
+ rubygems_version: 4.0.11
59
58
  specification_version: 4
60
59
  summary: Ruflet server package.
61
60
  test_files: []
@@ -1,285 +0,0 @@
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