mathpix 0.1.1 → 0.1.2

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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +53 -0
  3. data/README.md +114 -1
  4. data/lib/mathpix/batch.rb +7 -8
  5. data/lib/mathpix/batched_document_conversion.rb +238 -0
  6. data/lib/mathpix/client.rb +33 -27
  7. data/lib/mathpix/configuration.rb +5 -9
  8. data/lib/mathpix/conversion.rb +2 -6
  9. data/lib/mathpix/document.rb +47 -12
  10. data/lib/mathpix/document_batcher.rb +191 -0
  11. data/lib/mathpix/mcp/auth/oauth_provider.rb +8 -9
  12. data/lib/mathpix/mcp/base_tool.rb +8 -5
  13. data/lib/mathpix/mcp/elicitations/ambiguity_elicitation.rb +8 -11
  14. data/lib/mathpix/mcp/elicitations/base_elicitation.rb +2 -0
  15. data/lib/mathpix/mcp/elicitations/confidence_elicitation.rb +2 -1
  16. data/lib/mathpix/mcp/elicitations.rb +1 -1
  17. data/lib/mathpix/mcp/middleware/cors_middleware.rb +2 -6
  18. data/lib/mathpix/mcp/middleware/oauth_middleware.rb +2 -6
  19. data/lib/mathpix/mcp/middleware/rate_limiting_middleware.rb +19 -18
  20. data/lib/mathpix/mcp/resources/formats_list_resource.rb +54 -54
  21. data/lib/mathpix/mcp/resources/hierarchical_router.rb +9 -18
  22. data/lib/mathpix/mcp/resources/latest_snip_resource.rb +22 -22
  23. data/lib/mathpix/mcp/resources/recent_snips_resource.rb +11 -10
  24. data/lib/mathpix/mcp/resources/snip_stats_resource.rb +14 -12
  25. data/lib/mathpix/mcp/server.rb +18 -18
  26. data/lib/mathpix/mcp/tools/batch_convert_tool.rb +31 -37
  27. data/lib/mathpix/mcp/tools/check_document_status_tool.rb +5 -5
  28. data/lib/mathpix/mcp/tools/convert_document_tool.rb +15 -14
  29. data/lib/mathpix/mcp/tools/convert_image_tool.rb +15 -14
  30. data/lib/mathpix/mcp/tools/convert_strokes_tool.rb +13 -13
  31. data/lib/mathpix/mcp/tools/get_account_info_tool.rb +1 -1
  32. data/lib/mathpix/mcp/tools/get_usage_tool.rb +5 -7
  33. data/lib/mathpix/mcp/tools/list_formats_tool.rb +30 -30
  34. data/lib/mathpix/mcp/tools/search_results_tool.rb +13 -14
  35. data/lib/mathpix/mcp/transports/http_streaming_transport.rb +129 -118
  36. data/lib/mathpix/mcp/transports/sse_stream_handler.rb +37 -35
  37. data/lib/mathpix/result.rb +3 -2
  38. data/lib/mathpix/version.rb +1 -1
  39. data/lib/mathpix.rb +3 -1
  40. metadata +60 -2
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'mcp' # Load full SDK including VERSION constant
4
- require 'mcp/transport' # Load Transport base class
3
+ require 'mcp' # Load full SDK including VERSION constant
4
+ require 'mcp/transport' # Load Transport base class
5
5
  require 'rack'
6
6
  require 'puma'
7
7
  require 'json'
8
- require 'ostruct' # For creating error objects
8
+ require 'ostruct' # For creating error objects
9
9
  require_relative '../middleware'
10
10
 
11
11
  module Mathpix
@@ -22,7 +22,7 @@ module Mathpix
22
22
  @host = host
23
23
  @puma_server = nil
24
24
  @connections = {}
25
- @pending_requests = {} # Store pending tool invocations by request_id
25
+ @pending_requests = {} # Store pending tool invocations by request_id
26
26
  @open = false
27
27
  end
28
28
 
@@ -37,11 +37,9 @@ module Mathpix
37
37
 
38
38
  # Run in background thread to avoid blocking
39
39
  @server_thread = Thread.new do
40
- begin
41
- @puma_server.run.join
42
- rescue StandardError => e
43
- # Server stopped
44
- end
40
+ @puma_server.run.join
41
+ rescue StandardError
42
+ # Server stopped
45
43
  end
46
44
 
47
45
  sleep 0.2 # Give server time to start
@@ -49,17 +47,21 @@ module Mathpix
49
47
 
50
48
  # Override MCP::Transport#close
51
49
  def close
52
- return unless @open # Don't close twice
50
+ return unless @open # Don't close twice
53
51
 
54
52
  @open = false
55
53
 
56
54
  # Signal all SSE handlers to stop and close streams
57
- @connections.each do |session_id, conn|
55
+ @connections.each_value do |conn|
58
56
  handler = conn[:handler]
59
57
  next unless handler
60
58
 
61
59
  handler.stop_flag = true
62
- handler.close rescue nil
60
+ begin
61
+ handler.close
62
+ rescue StandardError
63
+ nil
64
+ end
63
65
  end
64
66
 
65
67
  # Wait for handlers to finish (but not too long in tests)
@@ -72,7 +74,7 @@ module Mathpix
72
74
  # Stop Puma server aggressively
73
75
  if @puma_server
74
76
  begin
75
- @puma_server.stop(true) # Force immediate stop
77
+ @puma_server.stop(true) # Force immediate stop
76
78
  rescue StandardError
77
79
  # Ignore errors
78
80
  end
@@ -81,7 +83,11 @@ module Mathpix
81
83
  # Kill server thread immediately in tests
82
84
  if @server_thread&.alive?
83
85
  @server_thread.kill
84
- @server_thread.join(0.1) rescue nil
86
+ begin
87
+ @server_thread.join(0.1)
88
+ rescue StandardError
89
+ nil
90
+ end
85
91
  end
86
92
 
87
93
  @puma_server = nil
@@ -102,9 +108,9 @@ module Mathpix
102
108
  end
103
109
 
104
110
  def unregister_connection(session_id)
105
- $stderr.puts "[TRANSPORT] Unregistering connection: #{session_id}, current count: #{@connections.size}"
111
+ warn "[TRANSPORT] Unregistering connection: #{session_id}, current count: #{@connections.size}"
106
112
  @connections.delete(session_id)
107
- $stderr.puts "[TRANSPORT] After unregister, count: #{@connections.size}"
113
+ warn "[TRANSPORT] After unregister, count: #{@connections.size}"
108
114
  end
109
115
 
110
116
  private
@@ -116,8 +122,8 @@ module Mathpix
116
122
  # CORS middleware
117
123
  use Mathpix::MCP::Middleware::CorsMiddleware,
118
124
  allowed_origins: ['*'],
119
- allowed_methods: ['GET', 'POST'],
120
- allowed_headers: ['Authorization', 'Content-Type']
125
+ allowed_methods: %w[GET POST],
126
+ allowed_headers: %w[Authorization Content-Type]
121
127
 
122
128
  # Rate limiting middleware
123
129
  use Mathpix::MCP::Middleware::RateLimitingMiddleware,
@@ -179,35 +185,39 @@ module Mathpix
179
185
  handler.handle
180
186
  end
181
187
 
182
- def handle_health_check(env)
188
+ def handle_health_check(_env)
183
189
  [
184
190
  200,
185
191
  { 'Content-Type' => 'application/json' },
186
192
  [JSON.generate({
187
- status: 'healthy',
188
- version: ::MCP::VERSION,
189
- transport: 'http_sse',
190
- active_connections: active_connections,
191
- dependencies: {
192
- mathpix_api: 'healthy',
193
- database: 'healthy'
194
- }
195
- })]
193
+ status: 'healthy',
194
+ version: ::MCP::VERSION,
195
+ transport: 'http_sse',
196
+ active_connections: active_connections,
197
+ dependencies: {
198
+ mathpix_api: 'healthy',
199
+ database: 'healthy'
200
+ }
201
+ })]
196
202
  ]
197
203
  end
198
204
 
199
205
  def handle_close_connection(env)
200
206
  # Explicit connection close endpoint for graceful disconnect
201
207
  request_body = env['rack.input'].read
202
- params = JSON.parse(request_body) rescue {}
208
+ params = begin
209
+ JSON.parse(request_body)
210
+ rescue StandardError
211
+ {}
212
+ end
203
213
  session_id = params['session_id']
204
214
 
205
- $stderr.puts "[TRANSPORT] Close connection request for session: #{session_id}"
215
+ warn "[TRANSPORT] Close connection request for session: #{session_id}"
206
216
 
207
217
  if session_id && @connections[session_id]
208
218
  handler = @connections[session_id][:handler]
209
219
  if handler
210
- $stderr.puts "[TRANSPORT] Setting stop_flag for session: #{session_id}"
220
+ warn "[TRANSPORT] Setting stop_flag for session: #{session_id}"
211
221
  handler.stop_flag = true
212
222
  # Give handler a moment to detect the flag and cleanup
213
223
  sleep 0.05
@@ -217,30 +227,30 @@ module Mathpix
217
227
  200,
218
228
  { 'Content-Type' => 'application/json' },
219
229
  [JSON.generate({
220
- status: 'closed',
221
- session_id: session_id
222
- })]
230
+ status: 'closed',
231
+ session_id: session_id
232
+ })]
223
233
  ]
224
234
  else
225
- $stderr.puts "[TRANSPORT] Session not found: #{session_id}"
235
+ warn "[TRANSPORT] Session not found: #{session_id}"
226
236
  [
227
237
  404,
228
238
  { 'Content-Type' => 'application/json' },
229
239
  [JSON.generate({
230
- error: 'session_not_found',
231
- session_id: session_id
232
- })]
240
+ error: 'session_not_found',
241
+ session_id: session_id
242
+ })]
233
243
  ]
234
244
  end
235
245
  rescue StandardError => e
236
- $stderr.puts "[TRANSPORT] Error in handle_close_connection: #{e.message}"
246
+ warn "[TRANSPORT] Error in handle_close_connection: #{e.message}"
237
247
  [
238
248
  500,
239
249
  { 'Content-Type' => 'application/json' },
240
250
  [JSON.generate({
241
- error: 'internal_error',
242
- message: e.message
243
- })]
251
+ error: 'internal_error',
252
+ message: e.message
253
+ })]
244
254
  ]
245
255
  end
246
256
 
@@ -257,33 +267,35 @@ module Mathpix
257
267
  # New handler methods for complete MCP server
258
268
 
259
269
  def handle_tool_invocation(env, path)
260
- $stderr.puts "[TRANSPORT] Handling tool invocation: #{path}"
270
+ warn "[TRANSPORT] Handling tool invocation: #{path}"
261
271
  # Read request body
262
272
  request_body = env['rack.input'].read
263
- $stderr.puts "[TRANSPORT] Request body: #{request_body[0..200]}"
273
+ warn "[TRANSPORT] Request body: #{request_body[0..200]}"
264
274
 
265
275
  # ✅ USE OFFICIAL SDK: Delegate to MCP::Server.handle_json
266
276
  # This handles JSON-RPC 2.0 protocol, tool routing, error formatting
267
277
  response_json = @server.handle_json(request_body)
268
- $stderr.puts "[TRANSPORT] SDK response: #{response_json[0..200]}"
278
+ warn "[TRANSPORT] SDK response: #{response_json[0..200]}"
269
279
 
270
280
  # Parse response to extract request_id for SSE broadcasting
271
281
  response = JSON.parse(response_json)
272
282
  request_id = response['id']
273
- $stderr.puts "[TRANSPORT] Response ID: #{request_id}, has error: #{response.key?('error')}"
283
+ warn "[TRANSPORT] Response ID: #{request_id}, has error: #{response.key?('error')}"
274
284
 
275
285
  # Store pending request for tracking
276
- @pending_requests[request_id] = {
277
- payload: JSON.parse(request_body),
278
- received_at: Time.now
279
- } if request_id
286
+ if request_id
287
+ @pending_requests[request_id] = {
288
+ payload: JSON.parse(request_body),
289
+ received_at: Time.now
290
+ }
291
+ end
280
292
 
281
293
  # Broadcast result via SSE to connected clients
282
294
  if response['error']
283
- $stderr.puts "[TRANSPORT] Broadcasting tool_error"
295
+ warn '[TRANSPORT] Broadcasting tool_error'
284
296
  broadcast_to_connections(response_json, event_type: 'tool_error', request_id: request_id)
285
297
  else
286
- $stderr.puts "[TRANSPORT] Broadcasting tool_result"
298
+ warn '[TRANSPORT] Broadcasting tool_result'
287
299
  broadcast_to_connections(response_json, event_type: 'tool_result', request_id: request_id)
288
300
  end
289
301
 
@@ -298,7 +310,7 @@ module Mathpix
298
310
  ]
299
311
  rescue StandardError => e
300
312
  # Return error response
301
- $stderr.puts "[TRANSPORT] Error in handle_tool_invocation: #{e.message}\n#{e.backtrace.first(5).join("\n")}"
313
+ warn "[TRANSPORT] Error in handle_tool_invocation: #{e.message}\n#{e.backtrace.first(5).join("\n")}"
302
314
  [
303
315
  500,
304
316
  { 'Content-Type' => 'application/json' },
@@ -306,24 +318,24 @@ module Mathpix
306
318
  ]
307
319
  end
308
320
 
309
- def handle_server_info(env)
321
+ def handle_server_info(_env)
310
322
  [
311
323
  200,
312
324
  { 'Content-Type' => 'application/json' },
313
325
  [JSON.generate({
314
- name: 'mathpix',
315
- version: '1.0.0',
316
- transport: 'http_sse',
317
- capabilities: {
318
- tools: true,
319
- resources: true,
320
- prompts: false
321
- }
322
- })]
326
+ name: 'mathpix',
327
+ version: '1.0.0',
328
+ transport: 'http_sse',
329
+ capabilities: {
330
+ tools: true,
331
+ resources: true,
332
+ prompts: false
333
+ }
334
+ })]
323
335
  ]
324
336
  end
325
337
 
326
- def handle_tool_discovery(env)
338
+ def handle_tool_discovery(_env)
327
339
  tools = [
328
340
  {
329
341
  name: 'convert_image',
@@ -426,7 +438,7 @@ module Mathpix
426
438
  ]
427
439
  end
428
440
 
429
- def handle_resource_discovery(env)
441
+ def handle_resource_discovery(_env)
430
442
  resources = [
431
443
  {
432
444
  uri: 'mathpix://snip/recent',
@@ -457,7 +469,7 @@ module Mathpix
457
469
 
458
470
  def handle_resource_request(env, path)
459
471
  # Parse resource path
460
- parts = path.split('/').drop(3) # Drop '', 'mcp', 'resources'
472
+ parts = path.split('/').drop(3) # Drop '', 'mcp', 'resources'
461
473
 
462
474
  if parts[0] == 'mathpix' && parts[1] == 'snip'
463
475
  if parts[2] == 'recent'
@@ -467,11 +479,11 @@ module Mathpix
467
479
  200,
468
480
  { 'Content-Type' => 'application/json' },
469
481
  [JSON.generate({
470
- id: parts[3],
471
- latex: '\\int_0^\\infty e^{-x^2} dx',
472
- confidence: 0.95,
473
- created_at: Time.now.iso8601
474
- })]
482
+ id: parts[3],
483
+ latex: '\\int_0^\\infty e^{-x^2} dx',
484
+ confidence: 0.95,
485
+ created_at: Time.now.iso8601
486
+ })]
475
487
  ]
476
488
  else
477
489
  # Recent snips list
@@ -491,13 +503,13 @@ module Mathpix
491
503
  200,
492
504
  { 'Content-Type' => 'application/json' },
493
505
  [JSON.generate({
494
- data: snips,
495
- pagination: {
496
- offset: offset,
497
- limit: limit,
498
- total: 100
499
- }
500
- })]
506
+ data: snips,
507
+ pagination: {
508
+ offset: offset,
509
+ limit: limit,
510
+ total: 100
511
+ }
512
+ })]
501
513
  ]
502
514
  end
503
515
  else
@@ -508,27 +520,27 @@ module Mathpix
508
520
  end
509
521
  end
510
522
 
511
- def handle_metrics(env)
523
+ def handle_metrics(_env)
512
524
  # Mock metrics - would track real metrics in production
513
525
  [
514
526
  200,
515
527
  { 'Content-Type' => 'application/json' },
516
528
  [JSON.generate({
517
- requests_total: 1234,
518
- errors_total: 5,
519
- response_time_p50: 120.5,
520
- response_time_p95: 450.2,
521
- response_time_p99: 892.1,
522
- active_connections: active_connections,
523
- dependencies: {
524
- mathpix_api: 'healthy',
525
- database: 'healthy'
526
- }
527
- })]
529
+ requests_total: 1234,
530
+ errors_total: 5,
531
+ response_time_p50: 120.5,
532
+ response_time_p95: 450.2,
533
+ response_time_p99: 892.1,
534
+ active_connections: active_connections,
535
+ dependencies: {
536
+ mathpix_api: 'healthy',
537
+ database: 'healthy'
538
+ }
539
+ })]
528
540
  ]
529
541
  end
530
542
 
531
- def handle_options(env)
543
+ def handle_options(_env)
532
544
  [
533
545
  200,
534
546
  {
@@ -565,7 +577,7 @@ module Mathpix
565
577
  enhanced['result'] = tool_response
566
578
  rescue JSON::ParserError
567
579
  # If parsing fails, keep original structure
568
- $stderr.puts "[TRANSPORT] Warning: Could not parse tool response text"
580
+ warn '[TRANSPORT] Warning: Could not parse tool response text'
569
581
  end
570
582
  end
571
583
  end
@@ -580,41 +592,40 @@ module Mathpix
580
592
  if request_id && @pending_requests[request_id]
581
593
  return @pending_requests[request_id][:payload]['params']['name']
582
594
  end
595
+
583
596
  nil
584
597
  end
585
598
 
586
599
  def broadcast_to_connections(message, event_type: 'response', request_id: nil)
587
- $stderr.puts "[TRANSPORT] Broadcasting to #{@connections.size} connections, event_type: #{event_type}"
588
- data = JSON.parse(message) # Returns string keys
600
+ warn "[TRANSPORT] Broadcasting to #{@connections.size} connections, event_type: #{event_type}"
601
+ data = JSON.parse(message) # Returns string keys
589
602
  data['request_id'] = request_id if request_id
590
603
 
591
604
  @connections.each do |session_id, conn|
592
- begin
593
- handler = conn[:handler]
594
- $stderr.puts "[TRANSPORT] Sending to handler #{session_id}"
595
-
596
- # Use SDK handler public methods (no instance_eval needed!)
597
- case event_type
598
- when 'tool_error'
599
- # JSON.parse returns string keys, not symbols
600
- error_data = data['error'] || {}
601
- error = OpenStruct.new(message: error_data['message'] || error_data['data'] || 'Unknown error')
602
- handler.send_tool_error_event(request_id, error)
603
- when 'tool_result'
604
- $stderr.puts "[TRANSPORT] Calling send_custom_event on handler"
605
- # Extract tool result from nested structure
606
- enhanced_data = extract_tool_result_data(data)
607
- handler.send_custom_event('tool_result', enhanced_data)
608
- else
609
- handler.send_custom_event(event_type, data)
610
- end
611
- rescue StandardError => e
612
- # Connection might be closed
613
- $stderr.puts "[TRANSPORT] Error broadcasting to #{session_id}: #{e.message}\n#{e.backtrace.first(3).join("\n")}"
614
- unregister_connection(session_id)
605
+ handler = conn[:handler]
606
+ warn "[TRANSPORT] Sending to handler #{session_id}"
607
+
608
+ # Use SDK handler public methods (no instance_eval needed!)
609
+ case event_type
610
+ when 'tool_error'
611
+ # JSON.parse returns string keys, not symbols
612
+ error_data = data['error'] || {}
613
+ error = OpenStruct.new(message: error_data['message'] || error_data['data'] || 'Unknown error')
614
+ handler.send_tool_error_event(request_id, error)
615
+ when 'tool_result'
616
+ warn '[TRANSPORT] Calling send_custom_event on handler'
617
+ # Extract tool result from nested structure
618
+ enhanced_data = extract_tool_result_data(data)
619
+ handler.send_custom_event('tool_result', enhanced_data)
620
+ else
621
+ handler.send_custom_event(event_type, data)
615
622
  end
623
+ rescue StandardError => e
624
+ # Connection might be closed
625
+ warn "[TRANSPORT] Error broadcasting to #{session_id}: #{e.message}\n#{e.backtrace.first(3).join("\n")}"
626
+ unregister_connection(session_id)
616
627
  end
617
- $stderr.puts "[TRANSPORT] Broadcast complete"
628
+ warn '[TRANSPORT] Broadcast complete'
618
629
  end
619
630
  end
620
631
  end
@@ -2,7 +2,6 @@
2
2
 
3
3
  require 'securerandom'
4
4
  require 'json'
5
- require 'thread' # For Queue
6
5
 
7
6
  module Mathpix
8
7
  module MCP
@@ -21,14 +20,14 @@ module Mathpix
21
20
 
22
21
  def initialize(mcp_server, env, test_mode: false, transport: nil)
23
22
  @mcp_server = mcp_server
24
- @transport = transport # Transport for connection tracking
23
+ @transport = transport # Transport for connection tracking
25
24
  @session_id = extract_session_id(env)
26
25
  @event_id = 0
27
26
  @last_activity = Time.now
28
27
  @heartbeat_thread = nil
29
28
  @test_mode = test_mode
30
29
  @stop_flag = false
31
- @event_queue = Queue.new # Thread-safe event queue
30
+ @event_queue = Queue.new # Thread-safe event queue
32
31
  end
33
32
 
34
33
  def handle
@@ -40,7 +39,7 @@ module Mathpix
40
39
  }
41
40
 
42
41
  stream = proc do |out|
43
- @stream = out # Store stream for external access
42
+ @stream = out # Store stream for external access
44
43
  @closed = false
45
44
  register_connection
46
45
 
@@ -55,39 +54,39 @@ module Mathpix
55
54
  timeout = @test_mode ? TEST_TIMEOUT : TIMEOUT
56
55
  check_interval = @test_mode ? TEST_EVENT_LOOP_CHECK_INTERVAL : EVENT_LOOP_CHECK_INTERVAL
57
56
  start_time = Time.now
58
- $stderr.puts "[SSE] Event loop started (timeout: #{timeout}s, check_interval: #{check_interval * 1000}ms)"
57
+ warn "[SSE] Event loop started (timeout: #{timeout}s, check_interval: #{check_interval * 1000}ms)"
59
58
 
60
59
  loop do
61
60
  # Check for timeout
62
61
  if Time.now - start_time > timeout
63
- $stderr.puts "[SSE] Timeout reached, sending timeout event"
62
+ warn '[SSE] Timeout reached, sending timeout event'
64
63
  send_event(out, 'timeout', {
65
- message: 'Connection timed out due to inactivity'
66
- })
64
+ message: 'Connection timed out due to inactivity'
65
+ })
67
66
  break
68
67
  end
69
68
 
70
69
  # Check for stop signal
71
70
  if @stop_flag
72
- $stderr.puts "[SSE] Stop flag set, exiting loop"
71
+ warn '[SSE] Stop flag set, exiting loop'
73
72
  break
74
73
  end
75
74
 
76
75
  # Process queued events (non-blocking with short timeout)
77
76
  begin
78
- event = @event_queue.pop(true) # non_block = true
79
- $stderr.puts "[SSE] Sending queued event: #{event[:type]}"
77
+ event = @event_queue.pop(true) # non_block = true
78
+ warn "[SSE] Sending queued event: #{event[:type]}"
80
79
  send_event(out, event[:type], event[:data])
81
80
  rescue ThreadError
82
81
  # Queue empty - that's ok, sleep briefly and check again
83
82
  sleep check_interval
84
83
  end
85
84
  end
86
- $stderr.puts "[SSE] Event loop exited"
87
- rescue IOError, Errno::EPIPE, Errno::ECONNRESET => e
85
+ warn '[SSE] Event loop exited'
86
+ rescue IOError, Errno::EPIPE, Errno::ECONNRESET
88
87
  # Connection closed by client - exit cleanly
89
88
  @closed = true
90
- rescue StandardError => e
89
+ rescue StandardError
91
90
  # Unexpected error - log and exit
92
91
  @closed = true
93
92
  ensure
@@ -135,18 +134,22 @@ module Mathpix
135
134
  def send_custom_event(event_type, data)
136
135
  return if @closed
137
136
 
138
- $stderr.puts "[SSE] Queueing event: #{event_type}"
137
+ warn "[SSE] Queueing event: #{event_type}"
139
138
  @event_queue << {
140
139
  type: event_type,
141
140
  data: data
142
141
  }
143
- $stderr.puts "[SSE] Event queued, queue size: #{@event_queue.size}"
142
+ warn "[SSE] Event queued, queue size: #{@event_queue.size}"
144
143
  end
145
144
 
146
145
  def close
147
146
  @closed = true
148
147
  @stop_flag = true
149
- @stream&.close rescue nil
148
+ begin
149
+ @stream&.close
150
+ rescue StandardError
151
+ nil
152
+ end
150
153
  end
151
154
 
152
155
  private
@@ -156,7 +159,7 @@ module Mathpix
156
159
  end
157
160
 
158
161
  def send_event(stream, event_type, data)
159
- return if @closed # Don't send if already closed
162
+ return if @closed # Don't send if already closed
160
163
 
161
164
  @event_id += 1
162
165
 
@@ -169,23 +172,22 @@ module Mathpix
169
172
  rescue IOError, Errno::EPIPE, Errno::ECONNRESET
170
173
  # Connection closed by client
171
174
  @closed = true
172
- nil # Return nil instead of raising
175
+ nil # Return nil instead of raising
173
176
  end
174
177
 
175
178
  def send_connection_event(stream)
176
179
  send_event(stream, 'connection', {
177
- session_id: session_id,
178
- transport: 'http_sse'
179
- })
180
+ session_id: session_id,
181
+ transport: 'http_sse'
182
+ })
180
183
  end
181
184
 
182
185
  def send_heartbeat(stream)
183
186
  send_event(stream, 'heartbeat', {
184
- timestamp: Time.now.iso8601
185
- })
187
+ timestamp: Time.now.iso8601
188
+ })
186
189
  end
187
190
 
188
-
189
191
  def start_heartbeat(stream)
190
192
  interval = @test_mode ? TEST_HEARTBEAT_INTERVAL : HEARTBEAT_INTERVAL
191
193
  @heartbeat_thread = Thread.new do
@@ -195,11 +197,11 @@ module Mathpix
195
197
  end
196
198
  rescue IOError, Errno::EPIPE, Errno::ECONNRESET => e
197
199
  # Client disconnected - signal main event loop to exit
198
- $stderr.puts "[HEARTBEAT] Client disconnect detected: #{e.class}"
200
+ warn "[HEARTBEAT] Client disconnect detected: #{e.class}"
199
201
  @stop_flag = true
200
202
  rescue StandardError => e
201
203
  # Unexpected error - signal main loop to exit
202
- $stderr.puts "[HEARTBEAT] Unexpected error: #{e.class}: #{e.message}"
204
+ warn "[HEARTBEAT] Unexpected error: #{e.class}: #{e.message}"
203
205
  @stop_flag = true
204
206
  end
205
207
  end
@@ -215,20 +217,20 @@ module Mathpix
215
217
 
216
218
  def unregister_connection
217
219
  # Remove connection from transport's connection tracking
218
- $stderr.puts "[SSE] Unregistering connection: #{@session_id}, transport present: #{!@transport.nil?}"
220
+ warn "[SSE] Unregistering connection: #{@session_id}, transport present: #{!@transport.nil?}"
219
221
  if @transport
220
- $stderr.puts "[SSE] About to call transport.unregister_connection"
222
+ warn '[SSE] About to call transport.unregister_connection'
221
223
  begin
222
224
  @transport.unregister_connection(@session_id)
223
- $stderr.puts "[SSE] Called transport.unregister_connection"
224
- rescue => e
225
- $stderr.puts "[SSE] ERROR calling unregister: #{e.class}: #{e.message}"
226
- $stderr.puts "[SSE] Backtrace: #{e.backtrace.first(3).join('\n')}"
225
+ warn '[SSE] Called transport.unregister_connection'
226
+ rescue StandardError => e
227
+ warn "[SSE] ERROR calling unregister: #{e.class}: #{e.message}"
228
+ warn "[SSE] Backtrace: #{e.backtrace.first(3).join('\n')}"
227
229
  end
228
230
  else
229
- $stderr.puts "[SSE] WARNING: No transport to unregister from!"
231
+ warn '[SSE] WARNING: No transport to unregister from!'
230
232
  end
231
- $stderr.puts "[SSE] Unregistration complete"
233
+ warn '[SSE] Unregistration complete'
232
234
  end
233
235
  end
234
236
  end