ruflet_server 0.0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 9ce7a082041b04a7208cf20bb1d541f6a4902361c5a31b1c188f509ebe321369
4
+ data.tar.gz: 4af18bd3e91a82bc38ce58f626005e20fb9e480bafe8ecefa768c81b77c9bf05
5
+ SHA512:
6
+ metadata.gz: 9cdf0804b857072621d1a83156b13e7b1a9b799c3152182b50c91c1fb733e83e44965d6e197729ee1589519566c913ab2e4f0475c6faffc08e79749cbac119e8
7
+ data.tar.gz: b30664476e50b799ac372795314493f3f35c5f76643e0a8967e4ded5e7e934f49d1c808706bf7593fb15f71e2f71693fb3bad870c733470591dda3ba1e7012c5
data/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # ruflet_server
2
+
3
+ Part of Ruflet monorepo.
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruflet
4
+ class WebSocketConnection
5
+ def initialize(socket)
6
+ @socket = socket
7
+ @write_mutex = Mutex.new
8
+ end
9
+
10
+ def session_key
11
+ @socket.object_id
12
+ end
13
+
14
+ def closed?
15
+ @socket.closed?
16
+ rescue IOError
17
+ true
18
+ end
19
+
20
+ def send_binary(payload)
21
+ send_frame(0x2, payload.to_s.b)
22
+ end
23
+
24
+ def send_text(payload)
25
+ send_frame(0x1, payload.to_s.b)
26
+ end
27
+
28
+ def read_message
29
+ frame = read_frame
30
+ return nil if frame.nil?
31
+
32
+ opcode = frame[:opcode]
33
+ payload = frame[:payload]
34
+
35
+ case opcode
36
+ when 0x8
37
+ close
38
+ nil
39
+ when 0x9
40
+ send_frame(0xA, payload)
41
+ read_message
42
+ when 0xA
43
+ read_message
44
+ when 0x1, 0x2
45
+ payload
46
+ else
47
+ read_message
48
+ end
49
+ end
50
+
51
+ def close
52
+ return if closed?
53
+
54
+ begin
55
+ @socket.close
56
+ rescue IOError
57
+ nil
58
+ end
59
+ end
60
+
61
+ private
62
+
63
+ def read_frame
64
+ header = read_exact(2)
65
+ return nil if header.nil?
66
+
67
+ b1 = header.getbyte(0)
68
+ b2 = header.getbyte(1)
69
+
70
+ masked = (b2 & 0x80) != 0
71
+ payload_len = b2 & 0x7f
72
+
73
+ payload_len = read_exact(2).unpack1("n") if payload_len == 126
74
+ payload_len = read_exact(8).unpack1("Q>") if payload_len == 127
75
+
76
+ masking_key = masked ? read_exact(4) : nil
77
+ payload = payload_len.zero? ? "".b : read_exact(payload_len)
78
+ return nil if payload.nil?
79
+
80
+ payload = unmask(payload, masking_key) if masked
81
+
82
+ { opcode: b1 & 0x0f, payload: payload }
83
+ end
84
+
85
+ def send_frame(opcode, payload)
86
+ bytes = payload.to_s.b
87
+ len = bytes.bytesize
88
+ header = [0x80 | (opcode & 0x0f)].pack("C")
89
+
90
+ header <<
91
+ if len <= 125
92
+ [len].pack("C")
93
+ elsif len <= 0xffff
94
+ [126].pack("C") + [len].pack("n")
95
+ else
96
+ [127].pack("C") + [len].pack("Q>")
97
+ end
98
+
99
+ @write_mutex.synchronize do
100
+ @socket.write(header)
101
+ @socket.write(bytes) unless bytes.empty?
102
+ end
103
+ end
104
+
105
+ def unmask(payload, mask)
106
+ out = +""
107
+ out.force_encoding(Encoding::BINARY)
108
+ payload.bytes.each_with_index do |byte, idx|
109
+ out << (byte ^ mask.getbyte(idx % 4))
110
+ end
111
+ out
112
+ end
113
+
114
+ def read_exact(length)
115
+ chunk = +""
116
+ chunk.force_encoding(Encoding::BINARY)
117
+
118
+ while chunk.bytesize < length
119
+ part = @socket.read(length - chunk.bytesize)
120
+ return nil if part.nil? || part.empty?
121
+
122
+ chunk << part
123
+ end
124
+
125
+ chunk
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,225 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruflet
4
+ class WireCodec
5
+ class << self
6
+ def pack(value)
7
+ case value
8
+ when NilClass
9
+ "\xc0".b
10
+ when TrueClass
11
+ "\xc3".b
12
+ when FalseClass
13
+ "\xc2".b
14
+ when Integer
15
+ pack_integer(value)
16
+ when Float
17
+ "\xcb".b + [value].pack("G")
18
+ when String
19
+ pack_string(value)
20
+ when Symbol
21
+ pack_string(value.to_s)
22
+ when Array
23
+ pack_array(value)
24
+ when Hash
25
+ pack_map(value)
26
+ else
27
+ pack_string(value.to_s)
28
+ end
29
+ end
30
+
31
+ def unpack(bytes)
32
+ reader = ByteReader.new(bytes)
33
+ read_value(reader)
34
+ end
35
+
36
+ private
37
+
38
+ def pack_integer(value)
39
+ if value >= 0
40
+ return [value].pack("C") if value <= 0x7f
41
+ return "\xcc".b + [value].pack("C") if value <= 0xff
42
+ return "\xcd".b + [value].pack("n") if value <= 0xffff
43
+ return "\xce".b + [value].pack("N") if value <= 0xffff_ffff
44
+
45
+ "\xcf".b + [value].pack("Q>")
46
+ else
47
+ return [value & 0xff].pack("C") if value >= -32
48
+ return "\xd0".b + [value].pack("c") if value >= -128
49
+ return "\xd1".b + [value].pack("s>") if value >= -32_768
50
+ return "\xd2".b + [value].pack("l>") if value >= -2_147_483_648
51
+
52
+ "\xd3".b + [value].pack("q>")
53
+ end
54
+ end
55
+
56
+ def pack_string(value)
57
+ str = value.to_s.dup.force_encoding("UTF-8")
58
+ bytes = str.b
59
+ len = bytes.bytesize
60
+
61
+ if len <= 31
62
+ [0xA0 | len].pack("C") + bytes
63
+ elsif len <= 0xff
64
+ "\xd9".b + [len].pack("C") + bytes
65
+ elsif len <= 0xffff
66
+ "\xda".b + [len].pack("n") + bytes
67
+ else
68
+ "\xdb".b + [len].pack("N") + bytes
69
+ end
70
+ end
71
+
72
+ def pack_array(value)
73
+ len = value.length
74
+ head =
75
+ if len <= 15
76
+ [0x90 | len].pack("C")
77
+ elsif len <= 0xffff
78
+ "\xdc".b + [len].pack("n")
79
+ else
80
+ "\xdd".b + [len].pack("N")
81
+ end
82
+
83
+ body = +"".b
84
+ value.each { |item| body << pack(item) }
85
+ head + body
86
+ end
87
+
88
+ def pack_map(value)
89
+ pairs = value.each_with_object({}) { |(k, v), out| out[k.to_s] = v }
90
+ len = pairs.length
91
+ head =
92
+ if len <= 15
93
+ [0x80 | len].pack("C")
94
+ elsif len <= 0xffff
95
+ "\xde".b + [len].pack("n")
96
+ else
97
+ "\xdf".b + [len].pack("N")
98
+ end
99
+
100
+ body = +"".b
101
+ pairs.each do |k, v|
102
+ body << pack(k)
103
+ body << pack(v)
104
+ end
105
+ head + body
106
+ end
107
+
108
+ def read_value(reader)
109
+ marker = reader.read_u8
110
+
111
+ return marker if marker <= 0x7f
112
+ return marker - 256 if marker >= 0xe0
113
+
114
+ case marker
115
+ when 0xc0 then nil
116
+ when 0xc2 then false
117
+ when 0xc3 then true
118
+ when 0xcc then reader.read_u8
119
+ when 0xcd then reader.read_u16
120
+ when 0xce then reader.read_u32
121
+ when 0xcf then reader.read_u64
122
+ when 0xd0 then reader.read_i8
123
+ when 0xd1 then reader.read_i16
124
+ when 0xd2 then reader.read_i32
125
+ when 0xd3 then reader.read_i64
126
+ when 0xca then reader.read_f32
127
+ when 0xcb then reader.read_f64
128
+ when 0xd9 then reader.read_string(reader.read_u8)
129
+ when 0xda then reader.read_string(reader.read_u16)
130
+ when 0xdb then reader.read_string(reader.read_u32)
131
+ when 0xdc then read_array(reader, reader.read_u16)
132
+ when 0xdd then read_array(reader, reader.read_u32)
133
+ when 0xde then read_map(reader, reader.read_u16)
134
+ when 0xdf then read_map(reader, reader.read_u32)
135
+ else
136
+ if (marker & 0xf0) == 0x90
137
+ read_array(reader, marker & 0x0f)
138
+ elsif (marker & 0xf0) == 0x80
139
+ read_map(reader, marker & 0x0f)
140
+ elsif (marker & 0xe0) == 0xa0
141
+ reader.read_string(marker & 0x1f)
142
+ else
143
+ raise "Unsupported MessagePack marker: 0x#{marker.to_s(16)}"
144
+ end
145
+ end
146
+ end
147
+
148
+ def read_array(reader, size)
149
+ Array.new(size) { read_value(reader) }
150
+ end
151
+
152
+ def read_map(reader, size)
153
+ out = {}
154
+ size.times do
155
+ key = read_value(reader)
156
+ out[key.to_s] = read_value(reader)
157
+ end
158
+ out
159
+ end
160
+ end
161
+
162
+ class ByteReader
163
+ def initialize(bytes)
164
+ @data = bytes.to_s.b
165
+ @offset = 0
166
+ end
167
+
168
+ def read_u8
169
+ value = @data.getbyte(@offset)
170
+ raise "Unexpected EOF" if value.nil?
171
+
172
+ @offset += 1
173
+ value
174
+ end
175
+
176
+ def read_exact(size)
177
+ chunk = @data.byteslice(@offset, size)
178
+ raise "Unexpected EOF" if chunk.nil? || chunk.bytesize != size
179
+
180
+ @offset += size
181
+ chunk
182
+ end
183
+
184
+ def read_u16
185
+ read_exact(2).unpack1("n")
186
+ end
187
+
188
+ def read_u32
189
+ read_exact(4).unpack1("N")
190
+ end
191
+
192
+ def read_u64
193
+ read_exact(8).unpack1("Q>")
194
+ end
195
+
196
+ def read_i8
197
+ read_exact(1).unpack1("c")
198
+ end
199
+
200
+ def read_i16
201
+ read_exact(2).unpack1("s>")
202
+ end
203
+
204
+ def read_i32
205
+ read_exact(4).unpack1("l>")
206
+ end
207
+
208
+ def read_i64
209
+ read_exact(8).unpack1("q>")
210
+ end
211
+
212
+ def read_f32
213
+ read_exact(4).unpack1("g")
214
+ end
215
+
216
+ def read_f64
217
+ read_exact(8).unpack1("G")
218
+ end
219
+
220
+ def read_string(size)
221
+ read_exact(size).force_encoding("UTF-8")
222
+ end
223
+ end
224
+ end
225
+ end
@@ -0,0 +1,407 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest/sha1"
4
+ require "socket"
5
+ require "thread"
6
+
7
+ require "ruflet"
8
+ require_relative "server/wire_codec"
9
+ require_relative "server/web_socket_connection"
10
+
11
+ module Ruflet
12
+ class Server
13
+ attr_reader :port
14
+
15
+ WEBSOCKET_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
16
+
17
+ def initialize(host: "0.0.0.0", port: 8550, &app_block)
18
+ @host = host
19
+ @port = port
20
+ @app_block = app_block
21
+ @sessions = {}
22
+ @sessions_mutex = Mutex.new
23
+ @connections = {}
24
+ @connections_mutex = Mutex.new
25
+ @running = false
26
+ @server_socket = nil
27
+
28
+ at_exit do
29
+ begin
30
+ stop
31
+ rescue StandardError
32
+ nil
33
+ end
34
+ end
35
+ end
36
+
37
+ def start
38
+ previous_signals = trap_stop_signals
39
+ bind_server_socket!
40
+ @running = true
41
+ print_server_banner
42
+ accept_loop
43
+ rescue Interrupt
44
+ nil
45
+ ensure
46
+ stop
47
+ restore_stop_signals(previous_signals)
48
+ end
49
+
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
+ def bind_server_socket!(max_attempts: 100)
57
+ requested = @port.to_i
58
+ candidate = requested
59
+
60
+ max_attempts.times do
61
+ begin
62
+ @server_socket = TCPServer.new(@host, candidate)
63
+ @port = candidate
64
+ warn "Requested port #{requested} is busy; bound to #{@port}" if @port != requested
65
+ return
66
+ rescue Errno::EADDRINUSE
67
+ candidate += 1
68
+ end
69
+ end
70
+
71
+ raise Errno::EADDRINUSE, "Unable to bind starting at #{requested} after #{max_attempts} attempts"
72
+ end
73
+
74
+ def stop
75
+ return unless @running || @server_socket
76
+
77
+ @running = false
78
+
79
+ server = @server_socket
80
+ @server_socket = nil
81
+ begin
82
+ server&.close
83
+ rescue IOError
84
+ nil
85
+ end
86
+
87
+ live_connections = @connections_mutex.synchronize { @connections.values.dup }
88
+ live_connections.each do |conn|
89
+ begin
90
+ conn.close
91
+ rescue StandardError
92
+ nil
93
+ end
94
+ end
95
+ end
96
+
97
+ private
98
+
99
+ def trap_stop_signals
100
+ {
101
+ "INT" => trap_signal("INT"),
102
+ "TERM" => trap_signal("TERM")
103
+ }
104
+ end
105
+
106
+ def trap_signal(signal_name)
107
+ Signal.trap(signal_name) do
108
+ stop
109
+ Thread.main.raise(Interrupt)
110
+ rescue StandardError
111
+ nil
112
+ end
113
+ end
114
+
115
+ def restore_stop_signals(previous_signals)
116
+ return unless previous_signals
117
+
118
+ previous_signals.each do |signal_name, handler|
119
+ Signal.trap(signal_name, handler) if handler
120
+ end
121
+ end
122
+
123
+ def print_server_banner
124
+ return if ENV["RUFLET_SUPPRESS_SERVER_BANNER"] == "1"
125
+
126
+ warn "Ruflet server listening on ws://#{@host}:#{@port}/ws"
127
+ end
128
+
129
+ def accept_loop
130
+ while @running
131
+ socket = accept_client_socket
132
+ break unless socket
133
+
134
+ start_client_thread(socket)
135
+ end
136
+ end
137
+
138
+ def accept_client_socket
139
+ accepted = @server_socket.accept
140
+ accepted.is_a?(Array) ? accepted.first : accepted
141
+ rescue IOError, Errno::EBADF
142
+ nil
143
+ rescue StandardError => e
144
+ warn "accept error: #{e.class}: #{e.message}"
145
+ warn e.backtrace.join("\n") if e.backtrace
146
+ nil
147
+ end
148
+
149
+ def start_client_thread(socket)
150
+ Thread.new(socket) do |client|
151
+ Thread.current.report_on_exception = false if Thread.current.respond_to?(:report_on_exception=)
152
+ handle_socket(client)
153
+ end
154
+ end
155
+
156
+ def handle_socket(socket)
157
+ ws = nil
158
+ begin
159
+ path, headers = read_http_upgrade_request(socket)
160
+ return unless websocket_upgrade_request?(path, headers)
161
+
162
+ send_handshake_response(socket, headers["sec-websocket-key"])
163
+ ws = Ruflet::WebSocketConnection.new(socket)
164
+ run_connection(ws)
165
+ rescue StandardError => e
166
+ warn "server error: #{e.class}: #{e.message}"
167
+ warn e.backtrace.join("\n") if e.backtrace
168
+ send_message(ws, Protocol::ACTIONS[:session_crashed], { "message" => e.message.to_s.dup.force_encoding("UTF-8") }) if ws
169
+ ensure
170
+ close_connection(ws)
171
+ end
172
+ end
173
+
174
+ def run_connection(ws)
175
+ register_connection(ws)
176
+
177
+ while (raw = ws.read_message)
178
+ handle_message(ws, raw)
179
+ end
180
+ rescue StandardError => e
181
+ warn "server error: #{e.class}: #{e.message}"
182
+ warn e.backtrace.join("\n") if e.backtrace
183
+ send_message(ws, Protocol::ACTIONS[:session_crashed], { "message" => e.message.to_s.dup.force_encoding("UTF-8") })
184
+ ensure
185
+ close_connection(ws)
186
+ end
187
+
188
+ def close_connection(ws)
189
+ remove_session(ws)
190
+ unregister_connection(ws)
191
+ ws&.close
192
+ end
193
+
194
+ def read_http_upgrade_request(socket)
195
+ request_line = socket.gets("\r\n")
196
+ raise "Invalid HTTP request" if request_line.nil?
197
+
198
+ method, path, _version = request_line.strip.split(" ", 3)
199
+ raise "Unsupported HTTP method: #{method}" unless method == "GET"
200
+
201
+ headers = {}
202
+ loop do
203
+ line = socket.gets("\r\n")
204
+ break if line.nil? || line == "\r\n"
205
+
206
+ key, value = line.split(":", 2)
207
+ next if key.nil? || value.nil?
208
+
209
+ headers[key.strip.downcase] = value.strip
210
+ end
211
+
212
+ [path, headers]
213
+ end
214
+
215
+ def websocket_upgrade_request?(path, headers)
216
+ return false unless path == "/ws"
217
+ return false unless headers["upgrade"]&.downcase == "websocket"
218
+ return false unless headers["connection"]&.downcase&.include?("upgrade")
219
+ return false if headers["sec-websocket-key"].to_s.empty?
220
+
221
+ true
222
+ end
223
+
224
+ def send_handshake_response(socket, key)
225
+ accept = [Digest::SHA1.digest("#{key}#{WEBSOCKET_GUID}")].pack("m0")
226
+
227
+ socket.write("HTTP/1.1 101 Switching Protocols\r\n")
228
+ socket.write("Upgrade: websocket\r\n")
229
+ socket.write("Connection: Upgrade\r\n")
230
+ socket.write("Sec-WebSocket-Accept: #{accept}\r\n")
231
+ socket.write("\r\n")
232
+ end
233
+
234
+ def remove_session(ws)
235
+ return unless ws
236
+
237
+ @sessions_mutex.synchronize do
238
+ @sessions.delete(ws.session_key)
239
+ end
240
+ end
241
+
242
+ def register_connection(ws)
243
+ return unless ws
244
+
245
+ @connections_mutex.synchronize do
246
+ @connections[ws.session_key] = ws
247
+ end
248
+ end
249
+
250
+ def unregister_connection(ws)
251
+ return unless ws
252
+
253
+ @connections_mutex.synchronize do
254
+ @connections.delete(ws.session_key)
255
+ end
256
+ end
257
+
258
+ def handle_message(ws, raw)
259
+ action, payload = decode_incoming(raw)
260
+ payload ||= {}
261
+
262
+ warn "incoming action=#{action.inspect}" if ENV["FLET_DEBUG"] == "1"
263
+
264
+ case action
265
+ when Protocol::ACTIONS[:register_client], Protocol::ACTIONS[:register_web_client]
266
+ on_register_client(ws, payload)
267
+ when Protocol::ACTIONS[:control_event], Protocol::ACTIONS[:page_event_from_web]
268
+ on_control_event(ws, payload)
269
+ when Protocol::ACTIONS[:update_control], Protocol::ACTIONS[:update_control_props]
270
+ on_update_control(ws, payload)
271
+ else
272
+ raise "Unknown action: #{action.inspect}"
273
+ end
274
+ end
275
+
276
+ def decode_incoming(raw)
277
+ parsed = normalize_incoming(Ruflet::WireCodec.unpack(raw.to_s.b))
278
+
279
+ if parsed.is_a?(Array) && parsed.length >= 2
280
+ return [parsed[0], parsed[1]]
281
+ end
282
+
283
+ if parsed.is_a?(Hash)
284
+ action = parsed["action"] || parsed[:action]
285
+ payload = parsed["payload"] || parsed[:payload]
286
+ return [action, payload] unless action.nil?
287
+
288
+ if (parsed.key?("target") || parsed.key?(:target)) && (parsed.key?("name") || parsed.key?(:name))
289
+ return [Protocol::ACTIONS[:control_event], parsed]
290
+ end
291
+ end
292
+
293
+ raise "Unsupported payload format"
294
+ end
295
+
296
+ def normalize_incoming(value)
297
+ case value
298
+ when String
299
+ value.dup.force_encoding("UTF-8")
300
+ when Integer, Float, TrueClass, FalseClass, NilClass
301
+ value
302
+ when Symbol
303
+ value.to_s
304
+ when Array
305
+ value.map { |v| normalize_incoming(v) }
306
+ when Hash
307
+ value.each_with_object({}) do |(k, v), out|
308
+ out[k.to_s] = normalize_incoming(v)
309
+ end
310
+ else
311
+ value.to_s
312
+ end
313
+ end
314
+
315
+ def on_register_client(ws, payload)
316
+ normalized = Protocol.normalize_register_payload(payload)
317
+ session_id = normalized["session_id"].to_s.empty? ? pseudo_uuid : normalized["session_id"]
318
+
319
+ page = Page.new(
320
+ session_id: session_id,
321
+ client_details: normalized,
322
+ sender: lambda do |action, msg_payload|
323
+ send_message(ws, action, msg_payload)
324
+ end
325
+ )
326
+
327
+ page.title = "Ruflet App"
328
+
329
+ @sessions_mutex.synchronize do
330
+ @sessions[ws.session_key] = page
331
+ end
332
+
333
+ initial_response = [
334
+ Protocol::ACTIONS[:register_client],
335
+ {
336
+ "session_id" => session_id,
337
+ "page_patch" => {},
338
+ "error" => nil
339
+ }
340
+ ]
341
+ ws.send_binary(Ruflet::WireCodec.pack(initial_response))
342
+
343
+ @app_block.call(page)
344
+ page.update
345
+ rescue StandardError => e
346
+ send_message(ws, Protocol::ACTIONS[:session_crashed], { "message" => e.message })
347
+ raise
348
+ end
349
+
350
+ def on_control_event(ws, payload)
351
+ event = Protocol.normalize_control_event_payload(payload)
352
+ page = fetch_page(ws)
353
+ return if event["target"].nil? || event["name"].to_s.empty?
354
+
355
+ page.dispatch_event(
356
+ target: event["target"],
357
+ name: event["name"],
358
+ data: normalize_event_data(event["data"])
359
+ )
360
+ end
361
+
362
+ def on_update_control(ws, payload)
363
+ update = Protocol.normalize_update_control_payload(payload)
364
+ page = fetch_page(ws)
365
+ return if update["id"].nil?
366
+
367
+ page.apply_client_update(update["id"], update["props"] || {})
368
+ end
369
+
370
+ def fetch_page(ws)
371
+ page = @sessions_mutex.synchronize { @sessions[ws.session_key] }
372
+ raise "Session not found" unless page
373
+
374
+ page
375
+ end
376
+
377
+ def normalize_event_data(value)
378
+ case value
379
+ when Hash
380
+ value.each_with_object({}) { |(k, v), out| out[k.to_sym] = normalize_event_data(v) }
381
+ when Array
382
+ value.map { |entry| normalize_event_data(entry) }
383
+ else
384
+ value
385
+ end
386
+ end
387
+
388
+ def send_message(ws, action, payload)
389
+ message = [action, payload]
390
+ ws.send_binary(Ruflet::WireCodec.pack(message))
391
+ rescue StandardError => e
392
+ warn "send error: #{e.class}: #{e.message}"
393
+ end
394
+
395
+ def pseudo_uuid
396
+ now = Process.clock_gettime(Process::CLOCK_REALTIME, :nanosecond)
397
+ rnd = rand(0..0xffff_ffff)
398
+ "%08x-%04x-%04x-%04x-%012x" % [
399
+ rnd,
400
+ now & 0xffff,
401
+ (now >> 16) & 0xffff,
402
+ (now >> 32) & 0xffff,
403
+ (now >> 48) & 0xffff_ffff_ffff
404
+ ]
405
+ end
406
+ end
407
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruflet
4
+ VERSION = "0.0.1" unless const_defined?(:VERSION)
5
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ruflet"
4
+ require_relative "ruflet/server"
5
+
6
+ module Ruflet
7
+ module_function
8
+
9
+ def run(entrypoint = nil, host: "0.0.0.0", port: 8550, &block)
10
+ callback = entrypoint || block
11
+ raise ArgumentError, "Ruflet.run requires a callable entrypoint or block" unless callback.respond_to?(:call)
12
+
13
+ Server.new(host: host, port: port) do |page|
14
+ callback.call(page)
15
+ end.start
16
+ end
17
+ end
metadata ADDED
@@ -0,0 +1,59 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ruflet_server
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - AdamMusa
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: ruflet
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - '='
17
+ - !ruby/object:Gem::Version
18
+ version: 0.0.1
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - '='
24
+ - !ruby/object:Gem::Version
25
+ version: 0.0.1
26
+ description: Ruflet WebSocket server runtime compatible with Flet protocol.
27
+ email:
28
+ - adammusa2222@gmail.com
29
+ executables: []
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - README.md
34
+ - lib/ruflet/server.rb
35
+ - lib/ruflet/server/web_socket_connection.rb
36
+ - lib/ruflet/server/wire_codec.rb
37
+ - lib/ruflet/version.rb
38
+ - lib/ruflet_server.rb
39
+ homepage: https://github.com/AdamMusa/Ruflet
40
+ licenses: []
41
+ metadata: {}
42
+ rdoc_options: []
43
+ require_paths:
44
+ - lib
45
+ required_ruby_version: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: '3.1'
50
+ required_rubygems_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ requirements: []
56
+ rubygems_version: 3.7.2
57
+ specification_version: 4
58
+ summary: Ruflet server package.
59
+ test_files: []