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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 68ae173b9c0aa89989e40d996aed245db4c434b96e084b2bc83642c89f244208
4
- data.tar.gz: 9aea810e84df29511565821252e06f7c0a73cc989b5df86df683393a496d8793
3
+ metadata.gz: 2681e7067d0b0bd1d3d998e352b89a926cc6c8d1c129a632bb325fb5d2dde8c6
4
+ data.tar.gz: d4562b20a7f17a82c29cca1020b1956cc18c22ff8aa8531a7c5ebd5230db6704
5
5
  SHA512:
6
- metadata.gz: de44b6b80acfdc2ce0bca54a3460e027b053d16244cf8d1e44d81b6446d036795008cba9510d37ccec622734205b9bb41cecb8401456bbeb0fa349d8a51ad13e
7
- data.tar.gz: 5a7e6e9e7623eebc5635ac8e200e13d8e62ba68335d5fe15533d6260b88a83fe5f5d3c279819c3f89d8f2465a5fac5014e399d1c4b9d67a52a15c0a3f3a76f91
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
- def remove_session(ws)
364
- return unless ws
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 unregister_connection(ws)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ruflet
4
- VERSION = "0.0.14" unless const_defined?(:VERSION)
4
+ VERSION = "0.0.15" unless const_defined?(:VERSION)
5
5
  end
data/lib/ruflet_server.rb CHANGED
@@ -27,5 +27,7 @@ module Ruflet
27
27
 
28
28
  @run_interceptors_mutex.synchronize { @run_interceptors.last }
29
29
  end
30
- private_class_method :run_interceptor
30
+ class << self
31
+ private :run_interceptor
32
+ end
31
33
  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.14
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.14
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.14
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