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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +53 -0
- data/README.md +114 -1
- data/lib/mathpix/batch.rb +7 -8
- data/lib/mathpix/batched_document_conversion.rb +238 -0
- data/lib/mathpix/client.rb +33 -27
- data/lib/mathpix/configuration.rb +5 -9
- data/lib/mathpix/conversion.rb +2 -6
- data/lib/mathpix/document.rb +47 -12
- data/lib/mathpix/document_batcher.rb +191 -0
- data/lib/mathpix/mcp/auth/oauth_provider.rb +8 -9
- data/lib/mathpix/mcp/base_tool.rb +8 -5
- data/lib/mathpix/mcp/elicitations/ambiguity_elicitation.rb +8 -11
- data/lib/mathpix/mcp/elicitations/base_elicitation.rb +2 -0
- data/lib/mathpix/mcp/elicitations/confidence_elicitation.rb +2 -1
- data/lib/mathpix/mcp/elicitations.rb +1 -1
- data/lib/mathpix/mcp/middleware/cors_middleware.rb +2 -6
- data/lib/mathpix/mcp/middleware/oauth_middleware.rb +2 -6
- data/lib/mathpix/mcp/middleware/rate_limiting_middleware.rb +19 -18
- data/lib/mathpix/mcp/resources/formats_list_resource.rb +54 -54
- data/lib/mathpix/mcp/resources/hierarchical_router.rb +9 -18
- data/lib/mathpix/mcp/resources/latest_snip_resource.rb +22 -22
- data/lib/mathpix/mcp/resources/recent_snips_resource.rb +11 -10
- data/lib/mathpix/mcp/resources/snip_stats_resource.rb +14 -12
- data/lib/mathpix/mcp/server.rb +18 -18
- data/lib/mathpix/mcp/tools/batch_convert_tool.rb +31 -37
- data/lib/mathpix/mcp/tools/check_document_status_tool.rb +5 -5
- data/lib/mathpix/mcp/tools/convert_document_tool.rb +15 -14
- data/lib/mathpix/mcp/tools/convert_image_tool.rb +15 -14
- data/lib/mathpix/mcp/tools/convert_strokes_tool.rb +13 -13
- data/lib/mathpix/mcp/tools/get_account_info_tool.rb +1 -1
- data/lib/mathpix/mcp/tools/get_usage_tool.rb +5 -7
- data/lib/mathpix/mcp/tools/list_formats_tool.rb +30 -30
- data/lib/mathpix/mcp/tools/search_results_tool.rb +13 -14
- data/lib/mathpix/mcp/transports/http_streaming_transport.rb +129 -118
- data/lib/mathpix/mcp/transports/sse_stream_handler.rb +37 -35
- data/lib/mathpix/result.rb +3 -2
- data/lib/mathpix/version.rb +1 -1
- data/lib/mathpix.rb +3 -1
- metadata +60 -2
@@ -1,11 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'mcp'
|
4
|
-
require 'mcp/transport'
|
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'
|
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 = {}
|
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
|
-
|
41
|
-
|
42
|
-
|
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
|
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.
|
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
|
-
|
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)
|
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
|
-
|
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
|
-
|
111
|
+
warn "[TRANSPORT] Unregistering connection: #{session_id}, current count: #{@connections.size}"
|
106
112
|
@connections.delete(session_id)
|
107
|
-
|
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: [
|
120
|
-
allowed_headers: [
|
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(
|
188
|
+
def handle_health_check(_env)
|
183
189
|
[
|
184
190
|
200,
|
185
191
|
{ 'Content-Type' => 'application/json' },
|
186
192
|
[JSON.generate({
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
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 =
|
208
|
+
params = begin
|
209
|
+
JSON.parse(request_body)
|
210
|
+
rescue StandardError
|
211
|
+
{}
|
212
|
+
end
|
203
213
|
session_id = params['session_id']
|
204
214
|
|
205
|
-
|
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
|
-
|
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
|
-
|
221
|
-
|
222
|
-
|
230
|
+
status: 'closed',
|
231
|
+
session_id: session_id
|
232
|
+
})]
|
223
233
|
]
|
224
234
|
else
|
225
|
-
|
235
|
+
warn "[TRANSPORT] Session not found: #{session_id}"
|
226
236
|
[
|
227
237
|
404,
|
228
238
|
{ 'Content-Type' => 'application/json' },
|
229
239
|
[JSON.generate({
|
230
|
-
|
231
|
-
|
232
|
-
|
240
|
+
error: 'session_not_found',
|
241
|
+
session_id: session_id
|
242
|
+
})]
|
233
243
|
]
|
234
244
|
end
|
235
245
|
rescue StandardError => e
|
236
|
-
|
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
|
-
|
242
|
-
|
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
|
-
|
270
|
+
warn "[TRANSPORT] Handling tool invocation: #{path}"
|
261
271
|
# Read request body
|
262
272
|
request_body = env['rack.input'].read
|
263
|
-
|
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
|
-
|
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
|
-
|
283
|
+
warn "[TRANSPORT] Response ID: #{request_id}, has error: #{response.key?('error')}"
|
274
284
|
|
275
285
|
# Store pending request for tracking
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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(
|
321
|
+
def handle_server_info(_env)
|
310
322
|
[
|
311
323
|
200,
|
312
324
|
{ 'Content-Type' => 'application/json' },
|
313
325
|
[JSON.generate({
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
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(
|
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(
|
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)
|
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
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
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
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
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(
|
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
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
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(
|
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
|
-
|
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
|
-
|
588
|
-
data = JSON.parse(message)
|
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
|
-
|
593
|
-
|
594
|
-
|
595
|
-
|
596
|
-
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
|
603
|
-
|
604
|
-
|
605
|
-
|
606
|
-
|
607
|
-
|
608
|
-
|
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
|
-
|
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
|
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
|
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
|
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
|
-
|
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
|
-
|
62
|
+
warn '[SSE] Timeout reached, sending timeout event'
|
64
63
|
send_event(out, 'timeout', {
|
65
|
-
|
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
|
-
|
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)
|
79
|
-
|
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
|
-
|
87
|
-
rescue IOError, Errno::EPIPE, Errno::ECONNRESET
|
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
|
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
|
-
|
137
|
+
warn "[SSE] Queueing event: #{event_type}"
|
139
138
|
@event_queue << {
|
140
139
|
type: event_type,
|
141
140
|
data: data
|
142
141
|
}
|
143
|
-
|
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
|
-
|
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
|
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
|
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
|
-
|
178
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
220
|
+
warn "[SSE] Unregistering connection: #{@session_id}, transport present: #{!@transport.nil?}"
|
219
221
|
if @transport
|
220
|
-
|
222
|
+
warn '[SSE] About to call transport.unregister_connection'
|
221
223
|
begin
|
222
224
|
@transport.unregister_connection(@session_id)
|
223
|
-
|
224
|
-
rescue => e
|
225
|
-
|
226
|
-
|
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
|
-
|
231
|
+
warn '[SSE] WARNING: No transport to unregister from!'
|
230
232
|
end
|
231
|
-
|
233
|
+
warn '[SSE] Unregistration complete'
|
232
234
|
end
|
233
235
|
end
|
234
236
|
end
|