bolt_rb 0.2.1 → 0.3.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: 98c7fbaaad567ebdc0dee109df68cbb084cbaba4d532138b37e518e96893a23a
4
- data.tar.gz: d5cbcffc369f50cf85211ec54ed77e2b3cefababccd4d52b2da0c624527de54f
3
+ metadata.gz: 4e218118e1e764bfe65750f32f82c4671e3c1f777dfe50420889ba51758c179f
4
+ data.tar.gz: a39610870555d4bade286a77431583c5944bfbad836854368776e6f236b9ffc7
5
5
  SHA512:
6
- metadata.gz: 858c0cbf0ef13c010fa11594ffca9d5aa4949f0c4b8bc4d660b98faed05a76a0b65977f8bb7f9b6291a726d9f5d7f3527bd1876007d3e56a838f3a1109969aa1
7
- data.tar.gz: 9f7a90ce248a549729598d80af6a3495ed491d45c9a25c7d0226b19dac6e8ae8be3b3d46afa3b3bbb78cbf7ace90e9a7181fde7a985b22f62aa28c23f203a058
6
+ metadata.gz: 4cdbcce81133b4e5fb4720ce93e82ac041f1ae84db8b2e17f3bf4bab3a98dafad0a00e26c6def32f51d1709b0641c46de9acc51982acd77a0f6ddd21f123fcb8
7
+ data.tar.gz: 35b7cc610cd8ade5ce7900f6ad2d55d548a68c318487388e24f65245619e1f1dc4340b87405de6f2576bd8a9b3b51dbd3606153b873f9b8973813999566f3d1a
@@ -25,6 +25,8 @@ module BoltRb
25
25
  class Client
26
26
  SLACK_API_URL = 'https://slack.com/api/apps.connections.open'
27
27
  RECONNECT_DELAY = 5
28
+ # If no messages received in this many seconds, assume zombie socket
29
+ CONNECTION_STALE_THRESHOLD = 45
28
30
 
29
31
  # @return [String] The Slack app-level token
30
32
  attr_reader :app_token
@@ -42,6 +44,7 @@ module BoltRb
42
44
  @running = false
43
45
  @websocket = nil
44
46
  @message_handlers = []
47
+ @last_message_at = nil
45
48
  end
46
49
 
47
50
  # Registers a handler for incoming messages
@@ -104,11 +107,18 @@ module BoltRb
104
107
 
105
108
  while @running
106
109
  sleep 0.1
110
+
111
+ # Check for zombie socket - library reports open but no messages received
112
+ if connection_stale?
113
+ logger.warn "[SocketMode] Connection stale (no messages in #{CONNECTION_STALE_THRESHOLD}s), forcing reconnect"
114
+ force_reconnect
115
+ end
116
+
107
117
  reconnect_if_needed
108
118
 
109
119
  # Periodic heartbeat to confirm the loop is alive
110
120
  if Time.now - last_heartbeat >= heartbeat_interval
111
- logger.debug "[SocketMode] Heartbeat: connected=#{connected?}, websocket_open=#{@websocket&.open?}"
121
+ logger.debug "[SocketMode] Heartbeat: connected=#{connected?}, websocket_open=#{@websocket&.open?}, last_msg=#{@last_message_at&.strftime('%H:%M:%S') || 'never'}"
112
122
  last_heartbeat = Time.now
113
123
  end
114
124
  end
@@ -128,6 +138,31 @@ module BoltRb
128
138
  connect_with_retry
129
139
  end
130
140
 
141
+ # Checks if the connection appears stale (zombie socket)
142
+ #
143
+ # Returns true if we have an apparently open connection but haven't
144
+ # received any messages in CONNECTION_STALE_THRESHOLD seconds
145
+ #
146
+ # @return [Boolean]
147
+ def connection_stale?
148
+ return false unless @websocket&.open?
149
+ return false if @last_message_at.nil?
150
+
151
+ Time.now - @last_message_at > CONNECTION_STALE_THRESHOLD
152
+ end
153
+
154
+ # Forces a reconnection by closing the current socket
155
+ #
156
+ # Used when we detect a zombie socket that reports open but isn't
157
+ # actually receiving messages
158
+ #
159
+ # @return [void]
160
+ def force_reconnect
161
+ @websocket&.close
162
+ @last_message_at = nil
163
+ # reconnect_if_needed will pick this up on the next loop iteration
164
+ end
165
+
131
166
  # Attempts to connect with retry logic
132
167
  #
133
168
  # @return [void]
@@ -207,6 +242,7 @@ module BoltRb
207
242
  #
208
243
  # @return [void]
209
244
  def handle_open
245
+ @last_message_at = Time.now
210
246
  logger.info '[SocketMode] Connected to Slack'
211
247
  end
212
248
 
@@ -215,10 +251,21 @@ module BoltRb
215
251
  # @param msg [WebSocket::Client::Simple::Message] The message
216
252
  # @return [void]
217
253
  def handle_message(msg)
218
- logger.debug "[SocketMode] Raw message received: #{msg.data&.truncate(200) || '(nil)'}"
254
+ # Track message receipt for connection health monitoring
255
+ @last_message_at = Time.now
256
+
257
+ # Handle WebSocket protocol-level ping frames (Opcode 0x9)
258
+ # Must respond with pong frame echoing the same payload
259
+ if msg.type == :ping
260
+ handle_websocket_ping(msg.data)
261
+ return
262
+ end
263
+
264
+ raw_data = msg.data
265
+ logger.debug "[SocketMode] Raw message received (type=#{msg.type}): #{raw_data.nil? ? '(nil)' : raw_data[0, 200]}"
219
266
 
220
- # Skip nil, empty, or non-JSON data (like WebSocket ping/pong frames)
221
- return if msg.data.nil? || msg.data.empty? || !msg.data.start_with?('{')
267
+ # Skip nil, empty, or non-JSON data
268
+ return if raw_data.nil? || raw_data.empty? || !raw_data.start_with?('{')
222
269
 
223
270
  data = JSON.parse(msg.data)
224
271
 
@@ -264,7 +311,7 @@ module BoltRb
264
311
  logger.info "[SocketMode] WebSocket closed: #{event}"
265
312
  end
266
313
 
267
- # Handles Slack Socket Mode ping message
314
+ # Handles Slack Socket Mode JSON ping message
268
315
  #
269
316
  # Responds with a pong message echoing back the num field
270
317
  # @param data [Hash] The ping message data
@@ -275,7 +322,21 @@ module BoltRb
275
322
  pong = { 'type' => 'pong' }
276
323
  pong['num'] = data['num'] if data['num']
277
324
  @websocket.send(pong.to_json)
278
- logger.debug "[SocketMode] Responded to ping#{data['num'] ? " (num: #{data['num']})" : ''}"
325
+ logger.debug "[SocketMode] Sent pong response#{data['num'] ? " (num: #{data['num']})" : ''}"
326
+ end
327
+
328
+ # Handles WebSocket protocol-level ping frames (Opcode 0x9)
329
+ #
330
+ # Per WebSocket RFC 6455, we must respond with a pong frame (Opcode 0xA)
331
+ # that echoes back the exact payload from the ping frame.
332
+ # @param payload [String] The ping frame payload to echo back
333
+ # @return [void]
334
+ def handle_websocket_ping(payload)
335
+ return unless @websocket&.open?
336
+
337
+ logger.debug "[SocketMode] WebSocket ping received: '#{payload}'"
338
+ @websocket.send(payload, type: :pong)
339
+ logger.debug "[SocketMode] Sent WebSocket pong frame: '#{payload}'"
279
340
  end
280
341
 
281
342
  # Sends an acknowledgement for an event
@@ -1,4 +1,4 @@
1
1
  # lib/bolt_rb/version.rb
2
2
  module BoltRb
3
- VERSION = '0.2.1'
3
+ VERSION = '0.3.0'
4
4
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bolt_rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jon Whitcraft