whoosh 1.3.2 → 1.4.0

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: '058f9e32f51902477b54086f0b13891fb7dd4b3a33fbcef71387d2556af05ebe'
4
- data.tar.gz: abdfab2e7f266bd8d32f7357d531a7847a7087c30a508d1de955edddd1564383
3
+ metadata.gz: cf87392b23cb5b6a3cdc3fc27c06df4d62d7dd3b780e8695f9479cf869a0922a
4
+ data.tar.gz: 249803c4606645c7032deae70836954c3f1255ed5c9bf44342d07cf50afb2214
5
5
  SHA512:
6
- metadata.gz: 4e2f5bc134e5e2f32eb0c21258f240646abb0d140dfc8834e113d1f52a43d9919266ede8f36ba0750791ee98641d94d6059380d82274a171a8fb610ad9b0f963
7
- data.tar.gz: 3e0ce60dd666d57c6aa84804a2f5a0cf5af34bd2a215faf3d31705d4d535489de8c01a979c03144d1784a61526af8ea3e3941cacd1fee623ef9aae0bf3e7d276
6
+ metadata.gz: c871107c868e72d7f27596a48719dd752155d91ff114937b7448a7474e1a0ac6bdfa39dec8db23fe857e9728e3e2a0db3517013a70f820590386ebc7876b2707
7
+ data.tar.gz: 6ebad6447841c6587c0b3853fdb4f2666680db1e6e1eaf01a1fb1edb854af0a13ebd5e5a205b5866686346caf747a0aa49cbba30b2587afec681dc409829de23
data/README.md CHANGED
@@ -13,7 +13,7 @@
13
13
  <img src="https://img.shields.io/badge/ruby-%3E%3D%203.4.0-red" alt="Ruby">
14
14
  <img src="https://img.shields.io/badge/rack-3.0-blue" alt="Rack">
15
15
  <img src="https://img.shields.io/badge/license-MIT-green" alt="License">
16
- <img src="https://img.shields.io/badge/tests-540%20passing-brightgreen" alt="Tests">
16
+ <img src="https://img.shields.io/badge/tests-542%20passing-brightgreen" alt="Tests">
17
17
  <img src="https://img.shields.io/badge/overhead-2.5%C2%B5s-orange" alt="Performance">
18
18
  </p>
19
19
 
@@ -1,29 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "json"
4
- require "digest"
5
- require "base64"
4
+ require "faye/websocket"
6
5
 
7
6
  module Whoosh
8
7
  module Streaming
9
8
  class WebSocket
10
- GUID = "258EAFA5-E914-47DA-95CA-5AB5DC65C3E5"
11
-
12
9
  attr_reader :env
13
10
 
14
11
  def initialize(env)
15
12
  @env = env
16
- @io = nil
13
+ @ws = nil
17
14
  @closed = false
15
+ @on_open = nil
18
16
  @on_message = nil
19
17
  @on_close = nil
20
- @on_open = nil
21
18
  end
22
19
 
23
20
  # Check if the request is a WebSocket upgrade
24
21
  def self.websocket?(env)
25
- env["HTTP_UPGRADE"]&.downcase == "websocket" &&
26
- env["HTTP_CONNECTION"]&.downcase&.include?("upgrade")
22
+ Faye::WebSocket.websocket?(env)
27
23
  end
28
24
 
29
25
  # Register callbacks
@@ -41,154 +37,58 @@ module Whoosh
41
37
 
42
38
  # Send data to the client
43
39
  def send(data)
44
- return if @closed
40
+ return if @closed || @ws.nil?
45
41
  formatted = data.is_a?(String) ? data : JSON.generate(data)
46
- write_frame(formatted)
42
+ @ws.send(formatted)
47
43
  end
48
44
 
49
- def close
45
+ def close(code = nil, reason = nil)
50
46
  return if @closed
51
47
  @closed = true
52
- write_close_frame
53
- @io&.close rescue nil
54
- @on_close&.call
48
+ @ws&.close(code || 1000, reason || "")
55
49
  end
56
50
 
57
51
  def closed?
58
52
  @closed
59
53
  end
60
54
 
61
- # Returns a Rack response that hijacks the connection
55
+ # Returns a Rack response hijacks the connection via faye-websocket
62
56
  def rack_response
63
57
  unless self.class.websocket?(@env)
64
58
  return [400, { "content-type" => "text/plain" }, ["Not a WebSocket request"]]
65
59
  end
66
60
 
67
- # WebSocket handshake
68
- key = @env["HTTP_SEC_WEBSOCKET_KEY"]
69
- accept = Base64.strict_encode64(Digest::SHA1.digest(key + GUID))
70
-
71
- headers = {
72
- "Upgrade" => "websocket",
73
- "Connection" => "Upgrade",
74
- "Sec-WebSocket-Accept" => accept
75
- }
61
+ @ws = Faye::WebSocket.new(@env)
76
62
 
77
- # Use rack.hijack to take over the connection
78
- if @env["rack.hijack"]
79
- @env["rack.hijack"].call
80
- @io = @env["rack.hijack_io"]
81
-
82
- # Send handshake response manually
83
- @io.write("HTTP/1.1 101 Switching Protocols\r\n")
84
- headers.each { |k, v| @io.write("#{k}: #{v}\r\n") }
85
- @io.write("\r\n")
86
- @io.flush
87
-
88
- # Notify open
63
+ @ws.on :open do |_event|
89
64
  @on_open&.call
90
-
91
- # Start reading frames in a thread
92
- Thread.new { read_loop }
93
-
94
- # Return a dummy response (connection is hijacked)
95
- [-1, {}, []]
96
- else
97
- # Fallback: return 101 and let the server handle hijack
98
- [101, headers, []]
99
65
  end
100
- end
101
-
102
- # For testing — simulate without real socket
103
- def trigger_message(msg)
104
- @on_message&.call(msg)
105
- end
106
-
107
- def trigger_close
108
- @on_close&.call
109
- @closed = true
110
- end
111
66
 
112
- private
113
-
114
- def read_loop
115
- while !@closed && @io && !@io.closed?
116
- frame = read_frame
117
- break unless frame
118
-
119
- case frame[:opcode]
120
- when 0x1 # Text frame
121
- @on_message&.call(frame[:data])
122
- when 0x8 # Close frame
123
- close
124
- break
125
- when 0x9 # Ping
126
- write_pong(frame[:data])
127
- end
67
+ @ws.on :message do |event|
68
+ @on_message&.call(event.data)
128
69
  end
129
- rescue IOError, Errno::ECONNRESET, Errno::EPIPE
130
- @closed = true
131
- @on_close&.call
132
- end
133
-
134
- def read_frame
135
- return nil unless @io && !@io.closed?
136
-
137
- first_byte = @io.readbyte
138
- fin = (first_byte & 0x80) != 0
139
- opcode = first_byte & 0x0F
140
-
141
- second_byte = @io.readbyte
142
- masked = (second_byte & 0x80) != 0
143
- length = second_byte & 0x7F
144
-
145
- if length == 126
146
- length = @io.read(2).unpack1("n")
147
- elsif length == 127
148
- length = @io.read(8).unpack1("Q>")
149
- end
150
-
151
- mask_key = masked ? @io.read(4).bytes : nil
152
- payload = @io.read(length)&.bytes || []
153
70
 
154
- if masked && mask_key
155
- payload = payload.each_with_index.map { |b, i| b ^ mask_key[i % 4] }
71
+ @ws.on :close do |event|
72
+ @closed = true
73
+ @on_close&.call(event.code, event.reason)
74
+ @ws = nil
156
75
  end
157
76
 
158
- { opcode: opcode, data: payload.pack("C*"), fin: fin }
159
- rescue EOFError, IOError
160
- nil
77
+ @ws.rack_response
161
78
  end
162
79
 
163
- def write_frame(data, opcode: 0x1)
164
- return unless @io && !@io.closed?
165
-
166
- bytes = data.encode("UTF-8").bytes
167
- frame = [0x80 | opcode] # FIN + opcode
168
-
169
- if bytes.length < 126
170
- frame << bytes.length
171
- elsif bytes.length < 65536
172
- frame << 126
173
- frame += [bytes.length].pack("n").bytes
174
- else
175
- frame << 127
176
- frame += [bytes.length].pack("Q>").bytes
177
- end
178
-
179
- frame += bytes
180
- @io.write(frame.pack("C*"))
181
- @io.flush
182
- rescue IOError, Errno::EPIPE
183
- @closed = true
80
+ # For testing without real socket
81
+ def trigger_message(msg)
82
+ @on_message&.call(msg)
184
83
  end
185
84
 
186
- def write_close_frame
187
- write_frame("", opcode: 0x8) rescue nil
85
+ def trigger_close(code = 1000, reason = "")
86
+ @on_close&.call(code, reason)
87
+ @closed = true
188
88
  end
189
89
 
190
- def write_pong(data)
191
- write_frame(data, opcode: 0xA) rescue nil
90
+ def trigger_open
91
+ @on_open&.call
192
92
  end
193
93
  end
194
94
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Whoosh
4
- VERSION = "1.3.2"
4
+ VERSION = "1.4.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: whoosh
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.2
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Johannes Dwi Cahyo
@@ -107,6 +107,20 @@ dependencies:
107
107
  - - "~>"
108
108
  - !ruby/object:Gem::Version
109
109
  version: '1.8'
110
+ - !ruby/object:Gem::Dependency
111
+ name: faye-websocket
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: '0.11'
117
+ type: :runtime
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: '0.11'
110
124
  - !ruby/object:Gem::Dependency
111
125
  name: rspec
112
126
  requirement: !ruby/object:Gem::Requirement