a2a-ruby 1.0.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 +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +137 -0
- data/.simplecov +46 -0
- data/.yardopts +10 -0
- data/CHANGELOG.md +33 -0
- data/CODE_OF_CONDUCT.md +128 -0
- data/CONTRIBUTING.md +165 -0
- data/Gemfile +43 -0
- data/Guardfile +34 -0
- data/LICENSE.txt +21 -0
- data/PUBLISHING_CHECKLIST.md +214 -0
- data/README.md +171 -0
- data/Rakefile +165 -0
- data/docs/agent_execution.md +309 -0
- data/docs/api_reference.md +792 -0
- data/docs/configuration.md +780 -0
- data/docs/events.md +475 -0
- data/docs/getting_started.md +668 -0
- data/docs/integration.md +262 -0
- data/docs/server_apps.md +621 -0
- data/docs/troubleshooting.md +765 -0
- data/lib/a2a/client/api_methods.rb +263 -0
- data/lib/a2a/client/auth/api_key.rb +161 -0
- data/lib/a2a/client/auth/interceptor.rb +288 -0
- data/lib/a2a/client/auth/jwt.rb +189 -0
- data/lib/a2a/client/auth/oauth2.rb +146 -0
- data/lib/a2a/client/auth.rb +137 -0
- data/lib/a2a/client/base.rb +316 -0
- data/lib/a2a/client/config.rb +210 -0
- data/lib/a2a/client/connection_pool.rb +233 -0
- data/lib/a2a/client/http_client.rb +524 -0
- data/lib/a2a/client/json_rpc_handler.rb +136 -0
- data/lib/a2a/client/middleware/circuit_breaker_interceptor.rb +245 -0
- data/lib/a2a/client/middleware/logging_interceptor.rb +371 -0
- data/lib/a2a/client/middleware/rate_limit_interceptor.rb +142 -0
- data/lib/a2a/client/middleware/retry_interceptor.rb +161 -0
- data/lib/a2a/client/middleware.rb +116 -0
- data/lib/a2a/client/performance_tracker.rb +60 -0
- data/lib/a2a/configuration/defaults.rb +34 -0
- data/lib/a2a/configuration/environment_loader.rb +76 -0
- data/lib/a2a/configuration/file_loader.rb +115 -0
- data/lib/a2a/configuration/inheritance.rb +101 -0
- data/lib/a2a/configuration/validator.rb +180 -0
- data/lib/a2a/configuration.rb +201 -0
- data/lib/a2a/errors.rb +291 -0
- data/lib/a2a/modules.rb +50 -0
- data/lib/a2a/monitoring/alerting.rb +490 -0
- data/lib/a2a/monitoring/distributed_tracing.rb +398 -0
- data/lib/a2a/monitoring/health_endpoints.rb +204 -0
- data/lib/a2a/monitoring/metrics_collector.rb +438 -0
- data/lib/a2a/monitoring.rb +463 -0
- data/lib/a2a/plugin.rb +358 -0
- data/lib/a2a/plugin_manager.rb +159 -0
- data/lib/a2a/plugins/example_auth.rb +81 -0
- data/lib/a2a/plugins/example_middleware.rb +118 -0
- data/lib/a2a/plugins/example_transport.rb +76 -0
- data/lib/a2a/protocol/agent_card.rb +8 -0
- data/lib/a2a/protocol/agent_card_server.rb +584 -0
- data/lib/a2a/protocol/capability.rb +496 -0
- data/lib/a2a/protocol/json_rpc.rb +254 -0
- data/lib/a2a/protocol/message.rb +8 -0
- data/lib/a2a/protocol/task.rb +8 -0
- data/lib/a2a/rails/a2a_controller.rb +258 -0
- data/lib/a2a/rails/controller_helpers.rb +499 -0
- data/lib/a2a/rails/engine.rb +167 -0
- data/lib/a2a/rails/generators/agent_generator.rb +311 -0
- data/lib/a2a/rails/generators/install_generator.rb +209 -0
- data/lib/a2a/rails/generators/migration_generator.rb +232 -0
- data/lib/a2a/rails/generators/templates/add_a2a_indexes.rb +57 -0
- data/lib/a2a/rails/generators/templates/agent_controller.rb +122 -0
- data/lib/a2a/rails/generators/templates/agent_controller_spec.rb +160 -0
- data/lib/a2a/rails/generators/templates/agent_readme.md +200 -0
- data/lib/a2a/rails/generators/templates/create_a2a_push_notification_configs.rb +68 -0
- data/lib/a2a/rails/generators/templates/create_a2a_tasks.rb +83 -0
- data/lib/a2a/rails/generators/templates/example_agent_controller.rb +228 -0
- data/lib/a2a/rails/generators/templates/initializer.rb +108 -0
- data/lib/a2a/rails/generators/templates/push_notification_config_model.rb +228 -0
- data/lib/a2a/rails/generators/templates/task_model.rb +200 -0
- data/lib/a2a/rails/tasks/a2a.rake +228 -0
- data/lib/a2a/server/a2a_methods.rb +520 -0
- data/lib/a2a/server/agent.rb +537 -0
- data/lib/a2a/server/agent_execution/agent_executor.rb +279 -0
- data/lib/a2a/server/agent_execution/request_context.rb +219 -0
- data/lib/a2a/server/apps/rack_app.rb +311 -0
- data/lib/a2a/server/apps/sinatra_app.rb +261 -0
- data/lib/a2a/server/default_request_handler.rb +350 -0
- data/lib/a2a/server/events/event_consumer.rb +116 -0
- data/lib/a2a/server/events/event_queue.rb +226 -0
- data/lib/a2a/server/example_agent.rb +248 -0
- data/lib/a2a/server/handler.rb +281 -0
- data/lib/a2a/server/middleware/authentication_middleware.rb +212 -0
- data/lib/a2a/server/middleware/cors_middleware.rb +171 -0
- data/lib/a2a/server/middleware/logging_middleware.rb +362 -0
- data/lib/a2a/server/middleware/rate_limit_middleware.rb +382 -0
- data/lib/a2a/server/middleware.rb +213 -0
- data/lib/a2a/server/push_notification_manager.rb +327 -0
- data/lib/a2a/server/request_handler.rb +136 -0
- data/lib/a2a/server/storage/base.rb +141 -0
- data/lib/a2a/server/storage/database.rb +266 -0
- data/lib/a2a/server/storage/memory.rb +274 -0
- data/lib/a2a/server/storage/redis.rb +320 -0
- data/lib/a2a/server/storage.rb +38 -0
- data/lib/a2a/server/task_manager.rb +534 -0
- data/lib/a2a/transport/grpc.rb +481 -0
- data/lib/a2a/transport/http.rb +415 -0
- data/lib/a2a/transport/sse.rb +499 -0
- data/lib/a2a/types/agent_card.rb +540 -0
- data/lib/a2a/types/artifact.rb +99 -0
- data/lib/a2a/types/base_model.rb +223 -0
- data/lib/a2a/types/events.rb +117 -0
- data/lib/a2a/types/message.rb +106 -0
- data/lib/a2a/types/part.rb +288 -0
- data/lib/a2a/types/push_notification.rb +139 -0
- data/lib/a2a/types/security.rb +167 -0
- data/lib/a2a/types/task.rb +154 -0
- data/lib/a2a/types.rb +88 -0
- data/lib/a2a/utils/helpers.rb +245 -0
- data/lib/a2a/utils/message_buffer.rb +278 -0
- data/lib/a2a/utils/performance.rb +247 -0
- data/lib/a2a/utils/rails_detection.rb +97 -0
- data/lib/a2a/utils/structured_logger.rb +306 -0
- data/lib/a2a/utils/time_helpers.rb +167 -0
- data/lib/a2a/utils/validation.rb +8 -0
- data/lib/a2a/version.rb +6 -0
- data/lib/a2a-rails.rb +58 -0
- data/lib/a2a.rb +198 -0
- metadata +437 -0
@@ -0,0 +1,499 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
require "concurrent"
|
5
|
+
|
6
|
+
module A2A
|
7
|
+
module Transport
|
8
|
+
##
|
9
|
+
# Server-Sent Events (SSE) transport implementation
|
10
|
+
# Provides streaming responses with event parsing, connection management, and heartbeat support
|
11
|
+
#
|
12
|
+
class SSE
|
13
|
+
# SSE event types
|
14
|
+
EVENT_TYPES = %w[
|
15
|
+
message
|
16
|
+
error
|
17
|
+
heartbeat
|
18
|
+
task_status_update
|
19
|
+
task_artifact_update
|
20
|
+
connection_established
|
21
|
+
connection_closed
|
22
|
+
].freeze
|
23
|
+
|
24
|
+
# Default configuration values
|
25
|
+
DEFAULT_HEARTBEAT_INTERVAL = 30
|
26
|
+
DEFAULT_RECONNECT_DELAY = 3000
|
27
|
+
DEFAULT_MAX_RECONNECT_ATTEMPTS = 10
|
28
|
+
DEFAULT_BUFFER_SIZE = 1024 * 8 # 8KB
|
29
|
+
DEFAULT_TIMEOUT = 60
|
30
|
+
|
31
|
+
attr_reader :url, :config, :connection_state, :event_buffer, :last_event_id
|
32
|
+
|
33
|
+
##
|
34
|
+
# Initialize SSE transport
|
35
|
+
#
|
36
|
+
# @param url [String] SSE endpoint URL
|
37
|
+
# @param config [Hash] Configuration options
|
38
|
+
# @option config [Integer] :heartbeat_interval (30) Heartbeat interval in seconds
|
39
|
+
# @option config [Integer] :reconnect_delay (3000) Reconnection delay in milliseconds
|
40
|
+
# @option config [Integer] :max_reconnect_attempts (10) Maximum reconnection attempts
|
41
|
+
# @option config [Integer] :buffer_size (8192) Event buffer size in bytes
|
42
|
+
# @option config [Integer] :timeout (60) Connection timeout in seconds
|
43
|
+
# @option config [Hash] :headers ({}) Default headers
|
44
|
+
# @option config [Boolean] :auto_reconnect (true) Enable automatic reconnection
|
45
|
+
# @option config [String] :last_event_id Last received event ID for replay
|
46
|
+
#
|
47
|
+
def initialize(url, config = {})
|
48
|
+
@url = url
|
49
|
+
@config = default_config.merge(config)
|
50
|
+
@connection_state = :disconnected
|
51
|
+
@event_buffer = Concurrent::Array.new
|
52
|
+
@last_event_id = @config[:last_event_id]
|
53
|
+
@reconnect_attempts = 0
|
54
|
+
@heartbeat_timer = nil
|
55
|
+
@event_listeners = Concurrent::Hash.new { |h, k| h[k] = [] }
|
56
|
+
@mutex = Mutex.new
|
57
|
+
end
|
58
|
+
|
59
|
+
##
|
60
|
+
# Connect to SSE endpoint and start streaming
|
61
|
+
#
|
62
|
+
# @param headers [Hash] Additional headers
|
63
|
+
# @yield [event] Block to handle incoming events
|
64
|
+
# @yieldparam event [SSEEvent] Received SSE event
|
65
|
+
# @return [Enumerator] Event stream enumerator
|
66
|
+
#
|
67
|
+
def connect(headers: {}, &block)
|
68
|
+
@mutex.synchronize do
|
69
|
+
return if @connection_state == :connected
|
70
|
+
|
71
|
+
@connection_state = :connecting
|
72
|
+
@reconnect_attempts = 0
|
73
|
+
end
|
74
|
+
|
75
|
+
if block_given?
|
76
|
+
connect_with_callback(headers, &block)
|
77
|
+
else
|
78
|
+
connect_with_enumerator(headers)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
##
|
83
|
+
# Disconnect from SSE endpoint
|
84
|
+
#
|
85
|
+
def disconnect
|
86
|
+
@mutex.synchronize do
|
87
|
+
@connection_state = :disconnected
|
88
|
+
stop_heartbeat
|
89
|
+
@event_buffer.clear
|
90
|
+
end
|
91
|
+
|
92
|
+
emit_event(SSEEvent.new(type: "connection_closed", data: { reason: "manual_disconnect" }))
|
93
|
+
end
|
94
|
+
|
95
|
+
##
|
96
|
+
# Add event listener for specific event type
|
97
|
+
#
|
98
|
+
# @param event_type [String] Event type to listen for
|
99
|
+
# @param &block [Proc] Event handler block
|
100
|
+
#
|
101
|
+
def on(event_type, &block)
|
102
|
+
@event_listeners[event_type.to_s] << block if block_given?
|
103
|
+
end
|
104
|
+
|
105
|
+
##
|
106
|
+
# Remove event listener
|
107
|
+
#
|
108
|
+
# @param event_type [String] Event type
|
109
|
+
# @param handler [Proc] Handler to remove (optional, removes all if nil)
|
110
|
+
#
|
111
|
+
def off(event_type, handler = nil)
|
112
|
+
if handler
|
113
|
+
@event_listeners[event_type.to_s].delete(handler)
|
114
|
+
else
|
115
|
+
@event_listeners[event_type.to_s].clear
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
##
|
120
|
+
# Send data to SSE endpoint (for bidirectional communication)
|
121
|
+
#
|
122
|
+
# @param data [Hash] Data to send
|
123
|
+
# @param event_type [String] Event type
|
124
|
+
# @return [Boolean] Success status
|
125
|
+
#
|
126
|
+
def send_data(data, event_type: "message")
|
127
|
+
return false unless @connection_state == :connected
|
128
|
+
|
129
|
+
# This would typically use a separate HTTP connection for sending
|
130
|
+
# as SSE is primarily unidirectional from server to client
|
131
|
+
begin
|
132
|
+
http_transport = A2A::Transport::Http.new(@url.gsub("/events", ""))
|
133
|
+
response = http_transport.post(
|
134
|
+
"/events/send",
|
135
|
+
body: {
|
136
|
+
type: event_type,
|
137
|
+
data: data,
|
138
|
+
last_event_id: @last_event_id
|
139
|
+
}
|
140
|
+
)
|
141
|
+
response.status == 200
|
142
|
+
rescue StandardError => e
|
143
|
+
emit_event(SSEEvent.new(type: "error", data: { error: e.message }))
|
144
|
+
false
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
##
|
149
|
+
# Get connection status
|
150
|
+
#
|
151
|
+
# @return [Symbol] Connection state (:disconnected, :connecting, :connected, :reconnecting)
|
152
|
+
#
|
153
|
+
def connected?
|
154
|
+
@connection_state == :connected
|
155
|
+
end
|
156
|
+
|
157
|
+
##
|
158
|
+
# Get buffered events
|
159
|
+
#
|
160
|
+
# @return [Array<SSEEvent>] Buffered events
|
161
|
+
#
|
162
|
+
def buffered_events
|
163
|
+
@event_buffer.to_a
|
164
|
+
end
|
165
|
+
|
166
|
+
##
|
167
|
+
# Clear event buffer
|
168
|
+
#
|
169
|
+
def clear_buffer!
|
170
|
+
@event_buffer.clear
|
171
|
+
end
|
172
|
+
|
173
|
+
private
|
174
|
+
|
175
|
+
##
|
176
|
+
# Connect with callback-based handling
|
177
|
+
#
|
178
|
+
# @param headers [Hash] Request headers
|
179
|
+
# @yield [event] Event handler block
|
180
|
+
#
|
181
|
+
def connect_with_callback(headers)
|
182
|
+
Thread.new do
|
183
|
+
establish_connection(headers) do |event|
|
184
|
+
yield(event) if block_given?
|
185
|
+
end
|
186
|
+
rescue StandardError => e
|
187
|
+
handle_connection_error(e)
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
##
|
192
|
+
# Connect with enumerator-based handling
|
193
|
+
#
|
194
|
+
# @param headers [Hash] Request headers
|
195
|
+
# @return [Enumerator] Event stream enumerator
|
196
|
+
#
|
197
|
+
def connect_with_enumerator(headers)
|
198
|
+
Enumerator.new do |yielder|
|
199
|
+
establish_connection(headers) do |event|
|
200
|
+
yielder << event
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
##
|
206
|
+
# Establish SSE connection
|
207
|
+
#
|
208
|
+
# @param headers [Hash] Request headers
|
209
|
+
# @yield [event] Event handler block
|
210
|
+
#
|
211
|
+
def establish_connection(headers, &block)
|
212
|
+
request_headers = build_headers(headers)
|
213
|
+
|
214
|
+
# Use HTTP transport for the underlying connection
|
215
|
+
http = A2A::Transport::Http.new(@url, timeout: @config[:timeout])
|
216
|
+
|
217
|
+
http.get(headers: request_headers) do |req|
|
218
|
+
req.options.on_data = proc do |chunk, _size|
|
219
|
+
process_chunk(chunk, &block)
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
@connection_state = :connected
|
224
|
+
start_heartbeat
|
225
|
+
emit_event(SSEEvent.new(type: "connection_established", data: { url: @url }))
|
226
|
+
rescue StandardError => e
|
227
|
+
handle_connection_error(e)
|
228
|
+
end
|
229
|
+
|
230
|
+
##
|
231
|
+
# Process incoming data chunk
|
232
|
+
#
|
233
|
+
# @param chunk [String] Data chunk
|
234
|
+
# @yield [event] Event handler block
|
235
|
+
#
|
236
|
+
def process_chunk(chunk, &block)
|
237
|
+
lines = chunk.split("\n")
|
238
|
+
|
239
|
+
lines.each do |line|
|
240
|
+
event = parse_sse_line(line.strip)
|
241
|
+
next unless event
|
242
|
+
|
243
|
+
@last_event_id = event.id if event.id
|
244
|
+
buffer_event(event)
|
245
|
+
emit_event(event, &block)
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
##
|
250
|
+
# Parse SSE line into event
|
251
|
+
#
|
252
|
+
# @param line [String] SSE line
|
253
|
+
# @return [SSEEvent, nil] Parsed event or nil
|
254
|
+
#
|
255
|
+
def parse_sse_line(line)
|
256
|
+
return nil if line.empty? || line.start_with?(":")
|
257
|
+
|
258
|
+
if line.start_with?("data: ")
|
259
|
+
data_content = line[6..]
|
260
|
+
begin
|
261
|
+
data = JSON.parse(data_content)
|
262
|
+
SSEEvent.new(
|
263
|
+
type: data["type"] || "message",
|
264
|
+
data: data["data"] || data,
|
265
|
+
id: data["id"],
|
266
|
+
retry_interval: data["retry"]
|
267
|
+
)
|
268
|
+
rescue JSON::ParserError
|
269
|
+
SSEEvent.new(type: "message", data: data_content)
|
270
|
+
end
|
271
|
+
elsif line.start_with?("event: ")
|
272
|
+
# Store event type for next data line (simplified parsing)
|
273
|
+
nil
|
274
|
+
elsif line.start_with?("id: ")
|
275
|
+
# Store event ID for next data line (simplified parsing)
|
276
|
+
nil
|
277
|
+
elsif line.start_with?("retry: ")
|
278
|
+
# Update reconnection delay
|
279
|
+
@config[:reconnect_delay] = line[7..].to_i
|
280
|
+
nil
|
281
|
+
else
|
282
|
+
nil
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
##
|
287
|
+
# Buffer event for replay
|
288
|
+
#
|
289
|
+
# @param event [SSEEvent] Event to buffer
|
290
|
+
#
|
291
|
+
def buffer_event(event)
|
292
|
+
@event_buffer << event
|
293
|
+
|
294
|
+
# Limit buffer size
|
295
|
+
@event_buffer.shift while @event_buffer.size > @config[:buffer_size]
|
296
|
+
end
|
297
|
+
|
298
|
+
##
|
299
|
+
# Emit event to listeners
|
300
|
+
#
|
301
|
+
# @param event [SSEEvent] Event to emit
|
302
|
+
# @yield [event] Event handler block
|
303
|
+
#
|
304
|
+
def emit_event(event)
|
305
|
+
# Call specific event listeners
|
306
|
+
@event_listeners[event.type].each do |listener|
|
307
|
+
listener.call(event)
|
308
|
+
rescue StandardError => e
|
309
|
+
# Log error but don't stop processing
|
310
|
+
logger = if defined?(::Rails) && ::Rails.respond_to?(:logger) && ::Rails.logger
|
311
|
+
::Rails.logger
|
312
|
+
else
|
313
|
+
require "logger"
|
314
|
+
Logger.new($stdout)
|
315
|
+
end
|
316
|
+
logger.debug { "Error in event listener: #{e.message}" }
|
317
|
+
end
|
318
|
+
|
319
|
+
# Call generic block handler
|
320
|
+
yield(event) if block_given?
|
321
|
+
end
|
322
|
+
|
323
|
+
##
|
324
|
+
# Handle connection errors
|
325
|
+
#
|
326
|
+
# @param error [Exception] Connection error
|
327
|
+
#
|
328
|
+
def handle_connection_error(error)
|
329
|
+
@connection_state = :disconnected
|
330
|
+
stop_heartbeat
|
331
|
+
|
332
|
+
error_event = SSEEvent.new(
|
333
|
+
type: "error",
|
334
|
+
data: {
|
335
|
+
error: error.message,
|
336
|
+
reconnect_attempts: @reconnect_attempts
|
337
|
+
}
|
338
|
+
)
|
339
|
+
emit_event(error_event)
|
340
|
+
|
341
|
+
# Attempt reconnection if enabled
|
342
|
+
return unless @config[:auto_reconnect] && @reconnect_attempts < @config[:max_reconnect_attempts]
|
343
|
+
|
344
|
+
schedule_reconnection
|
345
|
+
end
|
346
|
+
|
347
|
+
##
|
348
|
+
# Schedule reconnection attempt
|
349
|
+
#
|
350
|
+
def schedule_reconnection
|
351
|
+
@reconnect_attempts += 1
|
352
|
+
@connection_state = :reconnecting
|
353
|
+
|
354
|
+
Thread.new do
|
355
|
+
sleep(@config[:reconnect_delay] / 1000.0)
|
356
|
+
connect if @connection_state == :reconnecting
|
357
|
+
end
|
358
|
+
end
|
359
|
+
|
360
|
+
##
|
361
|
+
# Start heartbeat timer
|
362
|
+
#
|
363
|
+
def start_heartbeat
|
364
|
+
return unless @config[:heartbeat_interval].positive?
|
365
|
+
|
366
|
+
@heartbeat_timer = Thread.new do
|
367
|
+
loop do
|
368
|
+
sleep(@config[:heartbeat_interval])
|
369
|
+
break unless @connection_state == :connected
|
370
|
+
|
371
|
+
emit_event(SSEEvent.new(
|
372
|
+
type: "heartbeat",
|
373
|
+
data: { timestamp: Time.now.iso8601 }
|
374
|
+
))
|
375
|
+
end
|
376
|
+
end
|
377
|
+
end
|
378
|
+
|
379
|
+
##
|
380
|
+
# Stop heartbeat timer
|
381
|
+
#
|
382
|
+
def stop_heartbeat
|
383
|
+
@heartbeat_timer&.kill
|
384
|
+
@heartbeat_timer = nil
|
385
|
+
end
|
386
|
+
|
387
|
+
##
|
388
|
+
# Build request headers
|
389
|
+
#
|
390
|
+
# @param additional_headers [Hash] Additional headers
|
391
|
+
# @return [Hash] Complete headers
|
392
|
+
#
|
393
|
+
def build_headers(additional_headers = {})
|
394
|
+
headers = {
|
395
|
+
"Accept" => "text/event-stream",
|
396
|
+
"Cache-Control" => "no-cache"
|
397
|
+
}
|
398
|
+
|
399
|
+
headers["Last-Event-ID"] = @last_event_id if @last_event_id
|
400
|
+
headers.merge(@config[:headers]).merge(additional_headers)
|
401
|
+
end
|
402
|
+
|
403
|
+
##
|
404
|
+
# Build default configuration
|
405
|
+
#
|
406
|
+
# @return [Hash] Default configuration
|
407
|
+
#
|
408
|
+
def default_config
|
409
|
+
{
|
410
|
+
heartbeat_interval: DEFAULT_HEARTBEAT_INTERVAL,
|
411
|
+
reconnect_delay: DEFAULT_RECONNECT_DELAY,
|
412
|
+
max_reconnect_attempts: DEFAULT_MAX_RECONNECT_ATTEMPTS,
|
413
|
+
buffer_size: DEFAULT_BUFFER_SIZE,
|
414
|
+
timeout: DEFAULT_TIMEOUT,
|
415
|
+
headers: {},
|
416
|
+
auto_reconnect: true,
|
417
|
+
last_event_id: nil
|
418
|
+
}
|
419
|
+
end
|
420
|
+
end
|
421
|
+
|
422
|
+
##
|
423
|
+
# SSE Event representation
|
424
|
+
#
|
425
|
+
class SSEEvent
|
426
|
+
attr_reader :type, :data, :id, :retry, :timestamp
|
427
|
+
|
428
|
+
##
|
429
|
+
# Initialize SSE event
|
430
|
+
#
|
431
|
+
# @param type [String] Event type
|
432
|
+
# @param data [Object] Event data
|
433
|
+
# @param id [String, nil] Event ID
|
434
|
+
# @param retry_interval [Integer, nil] Retry interval
|
435
|
+
#
|
436
|
+
def initialize(type:, data: nil, id: nil, retry_interval: nil)
|
437
|
+
@type = type.to_s
|
438
|
+
@data = data
|
439
|
+
@id = id
|
440
|
+
@retry = retry_interval
|
441
|
+
@timestamp = Time.now
|
442
|
+
end
|
443
|
+
|
444
|
+
##
|
445
|
+
# Convert event to SSE format
|
446
|
+
#
|
447
|
+
# @return [String] SSE formatted string
|
448
|
+
#
|
449
|
+
def to_sse_format
|
450
|
+
lines = []
|
451
|
+
lines << "event: #{@type}" if @type != "message"
|
452
|
+
lines << "id: #{@id}" if @id
|
453
|
+
lines << "retry: #{@retry}" if @retry
|
454
|
+
|
455
|
+
data_json = @data.is_a?(String) ? @data : @data.to_json
|
456
|
+
data_json.split("\n").each do |line|
|
457
|
+
lines << "data: #{line}"
|
458
|
+
end
|
459
|
+
|
460
|
+
lines << ""
|
461
|
+
lines.join("\n")
|
462
|
+
end
|
463
|
+
|
464
|
+
##
|
465
|
+
# Convert to hash representation
|
466
|
+
#
|
467
|
+
# @return [Hash] Event as hash
|
468
|
+
#
|
469
|
+
def to_h
|
470
|
+
{
|
471
|
+
type: @type,
|
472
|
+
data: @data,
|
473
|
+
id: @id,
|
474
|
+
retry: @retry,
|
475
|
+
timestamp: @timestamp.iso8601
|
476
|
+
}.compact
|
477
|
+
end
|
478
|
+
|
479
|
+
##
|
480
|
+
# Check if event is of specific type
|
481
|
+
#
|
482
|
+
# @param event_type [String] Event type to check
|
483
|
+
# @return [Boolean] True if event matches type
|
484
|
+
#
|
485
|
+
def type?(event_type)
|
486
|
+
@type == event_type.to_s
|
487
|
+
end
|
488
|
+
|
489
|
+
##
|
490
|
+
# Check if event has data
|
491
|
+
#
|
492
|
+
# @return [Boolean] True if event has data
|
493
|
+
#
|
494
|
+
def has_data?
|
495
|
+
!@data.nil?
|
496
|
+
end
|
497
|
+
end
|
498
|
+
end
|
499
|
+
end
|