ruflet_rails 0.0.2 → 0.0.4

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: fd72bcdef15f680ca9f3e5ed3b724a3219852751c258210f3618c150829c31aa
4
- data.tar.gz: 2cf6ada0236c8d0d6d679d4da8b6388e9f4b6749328c7b02efc5914855c07a05
3
+ metadata.gz: 192d25ccff11cbfc7a31a638a77df68daf83d24103b9580b2586ef91a2a0f7d3
4
+ data.tar.gz: 2ea8a4bf2c663a5a50d6d5f64426aa0420c70cf6dbc556d1c836cd41e7274a90
5
5
  SHA512:
6
- metadata.gz: dc67519a6c904b0d437d0408bde5726b7c352563a2d8b77dc01125d59c4cbc5a0295130db2f0151484c3b0a2458d254785dd1442d28d54f5fa78cc14b96508a5
7
- data.tar.gz: 99ef46cba567384d33d9604471c4206e7598c142e4799f3f4e417ce53b6b44b07deebcf5768c59401833b75b4c2bdc9c578ad48949d199a658eaf530d3a86e37
6
+ metadata.gz: d3e91b9375cf0df47090c8f736e50c241df1dc60a0418bc0519dadb3994a1337aa3fcfa49fb40d4bb9e3fb6f5c39337da3c9042450995203b6011301ec54dc99
7
+ data.tar.gz: d7f7021df9efd74b0c235c2ca59682eaf5ba11894b88d4c6967338c76615c691808a0630cd7d5d674db065a16ae6b2806b50f1b7534c4ea6cd25fe3db0342b28
data/README.md CHANGED
@@ -9,9 +9,7 @@ No separate protocol gem is required.
9
9
 
10
10
  ```ruby
11
11
  # Gemfile
12
- path "/path/to/ruflet/packages" do
13
- gem "ruflet_rails"
14
- end
12
+ gem "ruflet_rails", ">= 0.0.2"
15
13
  ```
16
14
 
17
15
  ```ruby
@@ -0,0 +1,185 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruflet
4
+ module Rails
5
+ module Protocol
6
+ class LocalServer
7
+ def initialize(&app_block)
8
+ @app_block = app_block
9
+ @sessions = {}
10
+ @sessions_mutex = Mutex.new
11
+ end
12
+
13
+ def handle_upgraded_socket(io)
14
+ ws = WebSocketConnection.new(io)
15
+ run_connection(ws)
16
+ end
17
+
18
+ private
19
+
20
+ def run_connection(ws)
21
+ while (raw = ws.read_message)
22
+ handle_message(ws, raw)
23
+ end
24
+ rescue StandardError => e
25
+ send_message(ws, Ruflet::Protocol::ACTIONS[:session_crashed], { "message" => e.message.to_s.dup.force_encoding("UTF-8") })
26
+ ensure
27
+ close_connection(ws)
28
+ end
29
+
30
+ def close_connection(ws)
31
+ return unless ws
32
+
33
+ remove_session(ws)
34
+ ws.close
35
+ end
36
+
37
+ def remove_session(ws)
38
+ @sessions_mutex.synchronize { @sessions.delete(ws.session_key) }
39
+ end
40
+
41
+ def handle_message(ws, raw)
42
+ action, payload = decode_incoming(raw)
43
+ payload ||= {}
44
+
45
+ case action
46
+ when Ruflet::Protocol::ACTIONS[:register_client], Ruflet::Protocol::ACTIONS[:register_web_client]
47
+ on_register_client(ws, payload)
48
+ when Ruflet::Protocol::ACTIONS[:control_event], Ruflet::Protocol::ACTIONS[:page_event_from_web]
49
+ on_control_event(ws, payload)
50
+ when Ruflet::Protocol::ACTIONS[:update_control], Ruflet::Protocol::ACTIONS[:update_control_props]
51
+ on_update_control(ws, payload)
52
+ when Ruflet::Protocol::ACTIONS[:invoke_control_method]
53
+ nil
54
+ else
55
+ raise "Unknown action: #{action.inspect}"
56
+ end
57
+ end
58
+
59
+ def decode_incoming(raw)
60
+ parsed = normalize_incoming(WireCodec.unpack(raw.to_s.b))
61
+
62
+ if parsed.is_a?(Array) && parsed.length >= 2
63
+ return [parsed[0], parsed[1]]
64
+ end
65
+
66
+ if parsed.is_a?(Hash)
67
+ action = parsed["action"] || parsed[:action]
68
+ payload = parsed["payload"] || parsed[:payload]
69
+ return [action, payload] unless action.nil?
70
+
71
+ if (parsed.key?("target") || parsed.key?(:target)) && (parsed.key?("name") || parsed.key?(:name))
72
+ return [Ruflet::Protocol::ACTIONS[:control_event], parsed]
73
+ end
74
+ end
75
+
76
+ raise "Unsupported payload format"
77
+ end
78
+
79
+ def normalize_incoming(value)
80
+ case value
81
+ when String
82
+ value.dup.force_encoding("UTF-8")
83
+ when Integer, Float, TrueClass, FalseClass, NilClass
84
+ value
85
+ when Symbol
86
+ value.to_s
87
+ when Array
88
+ value.map { |v| normalize_incoming(v) }
89
+ when Hash
90
+ value.each_with_object({}) do |(k, v), out|
91
+ out[k.to_s] = normalize_incoming(v)
92
+ end
93
+ else
94
+ value.to_s
95
+ end
96
+ end
97
+
98
+ def on_register_client(ws, payload)
99
+ normalized = Ruflet::Protocol.normalize_register_payload(payload)
100
+ session_id = normalized["session_id"].to_s.empty? ? pseudo_uuid : normalized["session_id"]
101
+
102
+ page = Ruflet::Page.new(
103
+ session_id: session_id,
104
+ client_details: normalized,
105
+ sender: lambda do |action, msg_payload|
106
+ send_message(ws, action, msg_payload)
107
+ end
108
+ )
109
+ page.title = "Ruflet App"
110
+
111
+ @sessions_mutex.synchronize { @sessions[ws.session_key] = page }
112
+
113
+ initial_response = [
114
+ Ruflet::Protocol::ACTIONS[:register_client],
115
+ Ruflet::Protocol.register_response(session_id: session_id)
116
+ ]
117
+ ws.send_binary(WireCodec.pack(initial_response))
118
+
119
+ @app_block.call(page)
120
+ page.update
121
+ rescue StandardError => e
122
+ send_message(ws, Ruflet::Protocol::ACTIONS[:session_crashed], { "message" => e.message.to_s })
123
+ raise
124
+ end
125
+
126
+
127
+ def on_control_event(ws, payload)
128
+ event = Ruflet::Protocol.normalize_control_event_payload(payload)
129
+ page = fetch_page(ws)
130
+ return if event["target"].nil? || event["name"].to_s.empty?
131
+
132
+ page.dispatch_event(
133
+ target: event["target"],
134
+ name: event["name"],
135
+ data: normalize_event_data(event["data"])
136
+ )
137
+ end
138
+
139
+ def on_update_control(ws, payload)
140
+ update = Ruflet::Protocol.normalize_update_control_payload(payload)
141
+ page = fetch_page(ws)
142
+ return if update["id"].nil?
143
+
144
+ page.apply_client_update(update["id"], update["props"] || {})
145
+ end
146
+
147
+ def fetch_page(ws)
148
+ page = @sessions_mutex.synchronize { @sessions[ws.session_key] }
149
+ raise "Session not found" unless page
150
+
151
+ page
152
+ end
153
+
154
+ def normalize_event_data(value)
155
+ case value
156
+ when Hash
157
+ value.each_with_object({}) { |(k, v), out| out[k.to_sym] = normalize_event_data(v) }
158
+ when Array
159
+ value.map { |entry| normalize_event_data(entry) }
160
+ else
161
+ value
162
+ end
163
+ end
164
+
165
+ def send_message(ws, action, payload)
166
+ ws.send_binary(WireCodec.pack([action, payload]))
167
+ rescue StandardError
168
+ nil
169
+ end
170
+
171
+ def pseudo_uuid
172
+ now = Process.clock_gettime(Process::CLOCK_REALTIME, :nanosecond)
173
+ rnd = rand(0..0xffff_ffff)
174
+ "%08x-%04x-%04x-%04x-%012x" % [
175
+ rnd,
176
+ now & 0xffff,
177
+ (now >> 16) & 0xffff,
178
+ (now >> 32) & 0xffff,
179
+ (now >> 48) & 0xffff_ffff_ffff
180
+ ]
181
+ end
182
+ end
183
+ end
184
+ end
185
+ end
@@ -22,7 +22,7 @@ module Ruflet
22
22
  private
23
23
 
24
24
  def build_server(entrypoint)
25
- Ruflet::Server.new do |page|
25
+ LocalServer.new do |page|
26
26
  env = Context.current_env
27
27
  if entrypoint.arity == 1
28
28
  entrypoint.call(page)
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruflet
4
+ module Rails
5
+ module Protocol
6
+ class WebSocketConnection
7
+ def initialize(socket)
8
+ @socket = socket
9
+ @write_mutex = Mutex.new
10
+ end
11
+
12
+ def session_key
13
+ @socket.object_id
14
+ end
15
+
16
+ def closed?
17
+ @socket.closed?
18
+ rescue IOError
19
+ true
20
+ end
21
+
22
+ def send_binary(payload)
23
+ send_frame(0x2, payload.to_s.b)
24
+ end
25
+
26
+ def read_message
27
+ frame = read_frame
28
+ return nil if frame.nil?
29
+
30
+ opcode = frame[:opcode]
31
+ payload = frame[:payload]
32
+
33
+ case opcode
34
+ when 0x8
35
+ close
36
+ nil
37
+ when 0x9
38
+ send_frame(0xA, payload)
39
+ read_message
40
+ when 0xA
41
+ read_message
42
+ when 0x1, 0x2
43
+ payload
44
+ else
45
+ read_message
46
+ end
47
+ end
48
+
49
+ def close
50
+ return if closed?
51
+
52
+ @socket.close
53
+ rescue IOError
54
+ nil
55
+ end
56
+
57
+ private
58
+
59
+ def read_frame
60
+ header = read_exact(2)
61
+ return nil if header.nil?
62
+
63
+ b1 = header.getbyte(0)
64
+ b2 = header.getbyte(1)
65
+
66
+ masked = (b2 & 0x80) != 0
67
+ payload_len = b2 & 0x7f
68
+
69
+ payload_len = read_exact(2).unpack1("n") if payload_len == 126
70
+ payload_len = read_exact(8).unpack1("Q>") if payload_len == 127
71
+
72
+ masking_key = masked ? read_exact(4) : nil
73
+ payload = payload_len.zero? ? "".b : read_exact(payload_len)
74
+ return nil if payload.nil?
75
+
76
+ payload = unmask(payload, masking_key) if masked
77
+
78
+ { opcode: b1 & 0x0f, payload: payload }
79
+ end
80
+
81
+ def send_frame(opcode, payload)
82
+ bytes = payload.to_s.b
83
+ len = bytes.bytesize
84
+ header = [0x80 | (opcode & 0x0f)].pack("C")
85
+
86
+ header <<
87
+ if len <= 125
88
+ [len].pack("C")
89
+ elsif len <= 0xffff
90
+ [126].pack("C") + [len].pack("n")
91
+ else
92
+ [127].pack("C") + [len].pack("Q>")
93
+ end
94
+
95
+ @write_mutex.synchronize do
96
+ @socket.write(header)
97
+ @socket.write(bytes) unless bytes.empty?
98
+ end
99
+ end
100
+
101
+ def unmask(payload, mask)
102
+ out = +""
103
+ out.force_encoding(Encoding::BINARY)
104
+ payload.bytes.each_with_index do |byte, idx|
105
+ out << (byte ^ mask.getbyte(idx % 4))
106
+ end
107
+ out
108
+ end
109
+
110
+ def read_exact(length)
111
+ chunk = +""
112
+ chunk.force_encoding(Encoding::BINARY)
113
+
114
+ while chunk.bytesize < length
115
+ part = @socket.read(length - chunk.bytesize)
116
+ return nil if part.nil? || part.empty?
117
+
118
+ chunk << part
119
+ end
120
+
121
+ chunk
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,211 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruflet
4
+ module Rails
5
+ module Protocol
6
+ class WireCodec
7
+ class << self
8
+ def pack(value)
9
+ case value
10
+ when NilClass
11
+ "\xc0".b
12
+ when TrueClass
13
+ "\xc3".b
14
+ when FalseClass
15
+ "\xc2".b
16
+ when Integer
17
+ pack_integer(value)
18
+ when Float
19
+ "\xcb".b + [value].pack("G")
20
+ when String
21
+ pack_string(value)
22
+ when Symbol
23
+ pack_string(value.to_s)
24
+ when Array
25
+ pack_array(value)
26
+ when Hash
27
+ pack_map(value)
28
+ else
29
+ pack_string(value.to_s)
30
+ end
31
+ end
32
+
33
+ def unpack(bytes)
34
+ reader = ByteReader.new(bytes)
35
+ read_value(reader)
36
+ end
37
+
38
+ private
39
+
40
+ def pack_integer(value)
41
+ if value >= 0
42
+ return [value].pack("C") if value <= 0x7f
43
+ return "\xcc".b + [value].pack("C") if value <= 0xff
44
+ return "\xcd".b + [value].pack("n") if value <= 0xffff
45
+ return "\xce".b + [value].pack("N") if value <= 0xffff_ffff
46
+
47
+ "\xcf".b + [value].pack("Q>")
48
+ else
49
+ return [value & 0xff].pack("C") if value >= -32
50
+ return "\xd0".b + [value].pack("c") if value >= -128
51
+ return "\xd1".b + [value].pack("s>") if value >= -32_768
52
+ return "\xd2".b + [value].pack("l>") if value >= -2_147_483_648
53
+
54
+ "\xd3".b + [value].pack("q>")
55
+ end
56
+ end
57
+
58
+ def pack_string(value)
59
+ str = value.to_s.dup.force_encoding("UTF-8")
60
+ bytes = str.b
61
+ len = bytes.bytesize
62
+
63
+ if len <= 31
64
+ [0xA0 | len].pack("C") + bytes
65
+ elsif len <= 0xff
66
+ "\xd9".b + [len].pack("C") + bytes
67
+ elsif len <= 0xffff
68
+ "\xda".b + [len].pack("n") + bytes
69
+ else
70
+ "\xdb".b + [len].pack("N") + bytes
71
+ end
72
+ end
73
+
74
+ def pack_array(value)
75
+ len = value.length
76
+ head =
77
+ if len <= 15
78
+ [0x90 | len].pack("C")
79
+ elsif len <= 0xffff
80
+ "\xdc".b + [len].pack("n")
81
+ else
82
+ "\xdd".b + [len].pack("N")
83
+ end
84
+
85
+ body = +"".b
86
+ value.each { |item| body << pack(item) }
87
+ head + body
88
+ end
89
+
90
+ def pack_map(value)
91
+ pairs = value.each_with_object({}) { |(k, v), out| out[k.to_s] = v }
92
+ len = pairs.length
93
+ head =
94
+ if len <= 15
95
+ [0x80 | len].pack("C")
96
+ elsif len <= 0xffff
97
+ "\xde".b + [len].pack("n")
98
+ else
99
+ "\xdf".b + [len].pack("N")
100
+ end
101
+
102
+ body = +"".b
103
+ pairs.each do |k, v|
104
+ body << pack(k)
105
+ body << pack(v)
106
+ end
107
+ head + body
108
+ end
109
+
110
+ def read_value(reader)
111
+ marker = reader.read_u8
112
+
113
+ return marker if marker <= 0x7f
114
+ return marker - 256 if marker >= 0xe0
115
+
116
+ case marker
117
+ when 0xc0 then nil
118
+ when 0xc2 then false
119
+ when 0xc3 then true
120
+ when 0xcc then reader.read_u8
121
+ when 0xcd then reader.read_u16
122
+ when 0xce then reader.read_u32
123
+ when 0xcf then reader.read_u64
124
+ when 0xd0 then reader.read_i8
125
+ when 0xd1 then reader.read_i16
126
+ when 0xd2 then reader.read_i32
127
+ when 0xd3 then reader.read_i64
128
+ when 0xca then reader.read_f32
129
+ when 0xcb then reader.read_f64
130
+ when 0xd9 then reader.read_string(reader.read_u8)
131
+ when 0xda then reader.read_string(reader.read_u16)
132
+ when 0xdb then reader.read_string(reader.read_u32)
133
+ when 0xdc then read_array(reader, reader.read_u16)
134
+ when 0xdd then read_array(reader, reader.read_u32)
135
+ when 0xde then read_map(reader, reader.read_u16)
136
+ when 0xdf then read_map(reader, reader.read_u32)
137
+ when 0xc7
138
+ read_ext(reader, reader.read_u8)
139
+ when 0xc8
140
+ read_ext(reader, reader.read_u16)
141
+ when 0xc9
142
+ read_ext(reader, reader.read_u32)
143
+ else
144
+ if (marker & 0xf0) == 0x90
145
+ read_array(reader, marker & 0x0f)
146
+ elsif (marker & 0xf0) == 0x80
147
+ read_map(reader, marker & 0x0f)
148
+ elsif (marker & 0xe0) == 0xa0
149
+ reader.read_string(marker & 0x1f)
150
+ else
151
+ raise "Unsupported MessagePack marker: 0x#{marker.to_s(16)}"
152
+ end
153
+ end
154
+ end
155
+
156
+ def read_array(reader, size)
157
+ Array.new(size) { read_value(reader) }
158
+ end
159
+
160
+ def read_map(reader, size)
161
+ out = {}
162
+ size.times do
163
+ key = read_value(reader)
164
+ out[key.to_s] = read_value(reader)
165
+ end
166
+ out
167
+ end
168
+
169
+ def read_ext(reader, size)
170
+ reader.read_i8
171
+ reader.read_exact(size)
172
+ end
173
+ end
174
+
175
+ class ByteReader
176
+ def initialize(bytes)
177
+ @data = bytes.to_s.b
178
+ @offset = 0
179
+ end
180
+
181
+ def read_u8
182
+ value = @data.getbyte(@offset)
183
+ raise "Unexpected EOF" if value.nil?
184
+
185
+ @offset += 1
186
+ value
187
+ end
188
+
189
+ def read_exact(size)
190
+ chunk = @data.byteslice(@offset, size)
191
+ raise "Unexpected EOF" if chunk.nil? || chunk.bytesize != size
192
+
193
+ @offset += size
194
+ chunk
195
+ end
196
+
197
+ def read_u16 = read_exact(2).unpack1("n")
198
+ def read_u32 = read_exact(4).unpack1("N")
199
+ def read_u64 = read_exact(8).unpack1("Q>")
200
+ def read_i8 = read_exact(1).unpack1("c")
201
+ def read_i16 = read_exact(2).unpack1("s>")
202
+ def read_i32 = read_exact(4).unpack1("l>")
203
+ def read_i64 = read_exact(8).unpack1("q>")
204
+ def read_f32 = read_exact(4).unpack1("g")
205
+ def read_f64 = read_exact(8).unpack1("G")
206
+ def read_string(size) = read_exact(size).force_encoding("UTF-8")
207
+ end
208
+ end
209
+ end
210
+ end
211
+ end
@@ -2,7 +2,9 @@
2
2
 
3
3
  require_relative "protocol/context"
4
4
  require_relative "protocol/middleware"
5
+ require_relative "protocol/wire_codec"
6
+ require_relative "protocol/web_socket_connection"
7
+ require_relative "protocol/local_server"
5
8
  require_relative "protocol/endpoint"
6
9
  require_relative "protocol/mobile_loader"
7
10
  require_relative "protocol/runner"
8
- require_relative "protocol/mount"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ruflet
4
- VERSION = "0.0.2" unless const_defined?(:VERSION)
4
+ VERSION = "0.0.4" unless const_defined?(:VERSION)
5
5
  end
data/lib/ruflet_rails.rb CHANGED
@@ -1,5 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ local_ruflet_lib = File.expand_path("../../ruflet/lib", __dir__)
4
+ if File.directory?(local_ruflet_lib) && !$LOAD_PATH.include?(local_ruflet_lib)
5
+ $LOAD_PATH.unshift(local_ruflet_lib)
6
+ end
7
+
3
8
  require "ruflet"
4
9
  require_relative "ruflet/rails/protocol"
5
10
  require_relative "ruflet/rails"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruflet_rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - AdamMusa
@@ -29,14 +29,14 @@ dependencies:
29
29
  requirements:
30
30
  - - '='
31
31
  - !ruby/object:Gem::Version
32
- version: 0.0.2
32
+ version: 0.0.4
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - '='
38
38
  - !ruby/object:Gem::Version
39
- version: 0.0.2
39
+ version: 0.0.4
40
40
  description: Rails-first integration package for mounting Ruflet mobile apps in Rails
41
41
  routes.
42
42
  email:
@@ -50,14 +50,16 @@ files:
50
50
  - lib/ruflet/rails/protocol.rb
51
51
  - lib/ruflet/rails/protocol/context.rb
52
52
  - lib/ruflet/rails/protocol/endpoint.rb
53
+ - lib/ruflet/rails/protocol/local_server.rb
53
54
  - lib/ruflet/rails/protocol/middleware.rb
54
55
  - lib/ruflet/rails/protocol/mobile_loader.rb
55
- - lib/ruflet/rails/protocol/mount.rb
56
56
  - lib/ruflet/rails/protocol/runner.rb
57
+ - lib/ruflet/rails/protocol/web_socket_connection.rb
58
+ - lib/ruflet/rails/protocol/wire_codec.rb
57
59
  - lib/ruflet/rails/railtie.rb
58
60
  - lib/ruflet/version.rb
59
61
  - lib/ruflet_rails.rb
60
- homepage: https://github.com/AdamMusa/Ruflet
62
+ homepage: https://github.com/AdamMusa/ruflet/tree/main/packages/ruflet_rails
61
63
  licenses: []
62
64
  metadata: {}
63
65
  rdoc_options: []
@@ -1,23 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Ruflet
4
- module Rails
5
- module Protocol
6
- class Mount
7
- def initialize(app, file_path:, path: "/ws")
8
- @app = app
9
- @path = path
10
- @endpoint = Runner.new.build_mobile_endpoint(file_path: file_path, path: "/")
11
- end
12
-
13
- def call(env)
14
- return @app.call(env) unless env["PATH_INFO"] == @path
15
-
16
- mounted_env = env.dup
17
- mounted_env["PATH_INFO"] = "/"
18
- @endpoint.call(mounted_env)
19
- end
20
- end
21
- end
22
- end
23
- end