conduit-sse 2.0.0 → 2.0.1
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 +25 -5
- data/README.md +114 -88
- data/lib/conduit_sse/config.rb +4 -4
- data/lib/conduit_sse/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8ec5fd661f1b02da8107f093fd0d07ac6542aacdfd1c6d99d456a611cea81b23
|
|
4
|
+
data.tar.gz: 3ef67ea5be1aa63daff67184763683453a8b68f531f8539a43ebcf8ea4bbeb53
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d466f77aaae0b06d93178cef3e593e8a66152c4bbc53afd4f0b60d62a4a40172686272286adb2f8adb8791f3951d78705a1d11e4f16a9185bc7b44c69b9b4f6e
|
|
7
|
+
data.tar.gz: 9062d3fc81730440a13ebf579e4769b5a09dccc0f62f3a8f7d1844012445af91f64c92fa286358064c27911f9718be5173341996c55b79673267b2e2cf7c7e19
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,26 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [2.0.1] - 2026-05-13
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- README polish: added a hero illustration, rewrote the OpenAI streaming
|
|
13
|
+
example (the previous version double-registered `on_parsed` and
|
|
14
|
+
`on_field` and undersold the gem's built-in event-type filtering),
|
|
15
|
+
added a Config/State architecture section, replaced the ASCII pipeline
|
|
16
|
+
diagram with a Mermaid `flowchart TD`, and renamed the block parameter
|
|
17
|
+
convention from `|c|` to `|config|` in all examples for readability.
|
|
18
|
+
- `ConduitSSE::Config` class docstring updated to match the new
|
|
19
|
+
`|config|` convention.
|
|
20
|
+
- Gemspec now excludes `docs/` from the packaged gem so the hero image
|
|
21
|
+
doesn't bloat installs.
|
|
22
|
+
- Switched the README gem-version badge from `badge.fury.io` (cache-laggy,
|
|
23
|
+
semi-abandoned) to `img.shields.io`, which queries RubyGems directly and
|
|
24
|
+
refreshes within minutes of a release.
|
|
25
|
+
|
|
26
|
+
No code changes. Drop-in replacement for 2.0.0.
|
|
27
|
+
|
|
8
28
|
## [2.0.0] - 2026-05-12
|
|
9
29
|
|
|
10
30
|
### Breaking
|
|
@@ -28,19 +48,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
28
48
|
a mutable `ConduitSSE::Config` instance:
|
|
29
49
|
|
|
30
50
|
```ruby
|
|
31
|
-
ConduitSSE.new do |
|
|
32
|
-
|
|
33
|
-
|
|
51
|
+
ConduitSSE.new do |config|
|
|
52
|
+
config.parser = ->(d) { JSON.parse(d) }
|
|
53
|
+
config.stats = true
|
|
34
54
|
end
|
|
35
55
|
```
|
|
36
56
|
|
|
37
57
|
Both forms can be mixed; kwargs seed the config and the block overrides.
|
|
38
58
|
|
|
39
|
-
- **`ConduitSSE::Config
|
|
59
|
+
- **`ConduitSSE::Config`**: new public class. Holds the seven parsing knobs,
|
|
40
60
|
loads its own defaults, validates unknown keys, and exposes a `finalize!`
|
|
41
61
|
method that runs validation, computes the derived `data_field`, and
|
|
42
62
|
freezes the instance. Accessible at runtime as `stream.config`.
|
|
43
|
-
- **`ConduitSSE::State
|
|
63
|
+
- **`ConduitSSE::State`**: new public class. Holds the per-stream mutable
|
|
44
64
|
runtime: input buffer, callbacks registry, last event id / retry / type,
|
|
45
65
|
and (when enabled) the stats counter hash. Exposes a null-object
|
|
46
66
|
`#increment_stat` / `#add_fields` so the stream has no `if @stats`
|
data/README.md
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/franbach/conduit/main/docs/hero.png" alt="ConduitSSE: turns a stream of SSE chunks into the events you need, and routes them where they matter." width="780">
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
5
|
+
<p align="center">
|
|
6
|
+
<a href="https://github.com/franbach/conduit/actions/workflows/main.yml"><img src="https://github.com/franbach/conduit/actions/workflows/main.yml/badge.svg" alt="CI"></a>
|
|
7
|
+
<a href="https://rubygems.org/gems/conduit-sse"><img src="https://img.shields.io/gem/v/conduit-sse.svg" alt="Gem Version"></a>
|
|
8
|
+
<a href="https://rubygems.org/gems/conduit-sse"><img src="https://img.shields.io/gem/dt/conduit-sse.svg" alt="Downloads"></a>
|
|
9
|
+
</p>
|
|
6
10
|
|
|
7
11
|
ConduitSSE is a lightweight, zero-dependency Ruby gem for parsing Server-Sent Events (SSE) streams. It provides a flexible callback-based architecture for processing real-time server push data with full control over every stage of the parsing pipeline.
|
|
8
12
|
|
|
@@ -59,10 +63,10 @@ The canonical way to build a stream is with a configuration block. The block
|
|
|
59
63
|
receives a mutable {ConduitSSE::Config} that exposes every knob as a setter:
|
|
60
64
|
|
|
61
65
|
```ruby
|
|
62
|
-
stream = ConduitSSE.new do |
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
+
stream = ConduitSSE.new do |config|
|
|
67
|
+
config.parser = ->(d) { JSON.parse(d) }
|
|
68
|
+
config.stats = true
|
|
69
|
+
config.frame_separator = "\r\n\r\n"
|
|
66
70
|
end
|
|
67
71
|
```
|
|
68
72
|
|
|
@@ -72,12 +76,12 @@ For one- or two-knob setups, plain keyword arguments are equally fine:
|
|
|
72
76
|
stream = ConduitSSE.new(parser: ->(d) { JSON.parse(d) }, stats: true)
|
|
73
77
|
```
|
|
74
78
|
|
|
75
|
-
The two forms can also be combined
|
|
79
|
+
The two forms can also be combined. Kwargs seed the config, and the block
|
|
76
80
|
mutates whatever it wants on top:
|
|
77
81
|
|
|
78
82
|
```ruby
|
|
79
|
-
stream = ConduitSSE.new(parser: ->(d) { d }) do |
|
|
80
|
-
|
|
83
|
+
stream = ConduitSSE.new(parser: ->(d) { d }) do |config|
|
|
84
|
+
config.stats = true
|
|
81
85
|
end
|
|
82
86
|
```
|
|
83
87
|
|
|
@@ -94,8 +98,8 @@ At its core, ConduitSSE processes SSE data chunks and emits callbacks at each st
|
|
|
94
98
|
require "conduit_sse"
|
|
95
99
|
|
|
96
100
|
# Create a stream with a parser that transforms event data
|
|
97
|
-
stream = ConduitSSE.new do |
|
|
98
|
-
|
|
101
|
+
stream = ConduitSSE.new do |config|
|
|
102
|
+
config.parser = ->(data) { JSON.parse(data) }
|
|
99
103
|
end
|
|
100
104
|
|
|
101
105
|
# Subscribe to parsed events
|
|
@@ -117,8 +121,8 @@ require "net/http"
|
|
|
117
121
|
require "uri"
|
|
118
122
|
require "json"
|
|
119
123
|
|
|
120
|
-
stream = ConduitSSE.new do |
|
|
121
|
-
|
|
124
|
+
stream = ConduitSSE.new do |config|
|
|
125
|
+
config.parser = ->(d) { JSON.parse(d) rescue d }
|
|
122
126
|
end
|
|
123
127
|
|
|
124
128
|
stream.on_parsed do |parsed|
|
|
@@ -151,37 +155,36 @@ require "json"
|
|
|
151
155
|
api_key = "your-api-key-here"
|
|
152
156
|
|
|
153
157
|
# Create the stream with a parser that extracts the delta content
|
|
154
|
-
stream = ConduitSSE.new do |
|
|
155
|
-
|
|
158
|
+
stream = ConduitSSE.new do |config|
|
|
159
|
+
config.parser = ->(data) { JSON.parse(data) }
|
|
156
160
|
end
|
|
161
|
+
```
|
|
157
162
|
|
|
158
|
-
|
|
163
|
+
**Approach 1**: Use `on_parsed` to extract delta after JSON parsing
|
|
164
|
+
Since OpenAI sends structured JSON in the data field, the parser converts it to a Hash,
|
|
165
|
+
making it easy to extract the delta content directly.
|
|
159
166
|
|
|
160
|
-
|
|
161
|
-
# Since OpenAI sends structured JSON in the data field, the parser converts it to a Hash,
|
|
162
|
-
# making it easy to extract the delta content directly.
|
|
167
|
+
```ruby
|
|
163
168
|
stream.on_parsed do |parsed_data|
|
|
164
169
|
type = parsed_data["type"]
|
|
165
170
|
|
|
166
171
|
if type == "response.output_text.delta"
|
|
167
172
|
delta = parsed_data["delta"]
|
|
168
|
-
if delta
|
|
169
|
-
puts "parsed delta: #{delta}"
|
|
170
|
-
result += delta
|
|
171
173
|
|
|
172
|
-
|
|
173
|
-
# emit_to_frontend(delta)
|
|
174
|
-
end
|
|
174
|
+
do_something_with(delta) if delta
|
|
175
175
|
end
|
|
176
176
|
|
|
177
177
|
if type == "response.completed"
|
|
178
|
-
|
|
178
|
+
do_something_with(parsed_data)
|
|
179
179
|
end
|
|
180
180
|
end
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
**Approach 2**: Use `on_field` for more granular control
|
|
184
|
+
This approach gives you access to the raw field values before JSON parsing,
|
|
185
|
+
useful if you need to inspect or modify the raw data field content.
|
|
181
186
|
|
|
182
|
-
|
|
183
|
-
# This approach gives you access to the raw field values before JSON parsing,
|
|
184
|
-
# useful if you need to inspect or modify the raw data field content.
|
|
187
|
+
```ruby
|
|
185
188
|
stream.on_field do |name, value|
|
|
186
189
|
if name == "data"
|
|
187
190
|
data = JSON.parse(value)
|
|
@@ -189,22 +192,45 @@ stream.on_field do |name, value|
|
|
|
189
192
|
|
|
190
193
|
if type == "response.output_text.delta"
|
|
191
194
|
delta = data["delta"]
|
|
192
|
-
if delta
|
|
193
|
-
puts "delta: #{delta}"
|
|
194
|
-
result += delta
|
|
195
195
|
|
|
196
|
-
|
|
197
|
-
# emit_to_frontend(delta)
|
|
198
|
-
end
|
|
196
|
+
do_something_with(delta) if delta
|
|
199
197
|
end
|
|
200
198
|
|
|
201
199
|
if type == "response.completed"
|
|
202
|
-
|
|
200
|
+
do_something_with(data)
|
|
203
201
|
end
|
|
204
202
|
end
|
|
205
203
|
end
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
**Approach 3**: Use filtered parsed callbacks for maximum flexibility
|
|
207
|
+
|
|
208
|
+
```ruby
|
|
209
|
+
stream.on_parsed(type: "response.output_text.delta") do |payload|
|
|
210
|
+
delta = payload["delta"]
|
|
211
|
+
|
|
212
|
+
do_something_with(delta) if delta
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
stream.on_parsed(type: "response.completed") do |payload|
|
|
216
|
+
do_something_with(payload)
|
|
217
|
+
end
|
|
206
218
|
|
|
207
|
-
|
|
219
|
+
stream.on_parsed(type: "response.error") do |payload|
|
|
220
|
+
# capture OpenAi error
|
|
221
|
+
do_something_with(payload)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Resilience: a single malformed frame won't tear the whole stream down.
|
|
225
|
+
# Without on_error, a JSON.parse failure would bubble up out of `stream << chunk`.
|
|
226
|
+
stream.on_error do |error|
|
|
227
|
+
Sentry.capture_exception(error)
|
|
228
|
+
end
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
**Make the streaming request**
|
|
232
|
+
|
|
233
|
+
```ruby
|
|
208
234
|
uri = URI("https://api.openai.com/v1/responses")
|
|
209
235
|
http = Net::HTTP.new(uri.host, uri.port)
|
|
210
236
|
http.use_ssl = true
|
|
@@ -237,7 +263,7 @@ end
|
|
|
237
263
|
ConduitSSE provides callbacks at every stage of processing:
|
|
238
264
|
|
|
239
265
|
```ruby
|
|
240
|
-
stream = ConduitSSE.new { |
|
|
266
|
+
stream = ConduitSSE.new { |config| config.parser = ->(data) { data } }
|
|
241
267
|
|
|
242
268
|
# Raw chunk as it arrived (after normalization)
|
|
243
269
|
stream.on_chunk do |chunk|
|
|
@@ -280,7 +306,7 @@ end
|
|
|
280
306
|
Filter events by type directly on callback registration:
|
|
281
307
|
|
|
282
308
|
```ruby
|
|
283
|
-
stream = ConduitSSE.new { |
|
|
309
|
+
stream = ConduitSSE.new { |config| config.parser = ->(data) { data } }
|
|
284
310
|
|
|
285
311
|
# Only process "message" events in this callback
|
|
286
312
|
stream.on_event(type: "message") do |event|
|
|
@@ -335,7 +361,7 @@ end
|
|
|
335
361
|
**`each` / `on_parsed`** - Receives the result of your custom parser (the `parser:` lambda):
|
|
336
362
|
|
|
337
363
|
```ruby
|
|
338
|
-
stream = ConduitSSE.new { |
|
|
364
|
+
stream = ConduitSSE.new { |config| config.parser = ->(data) { JSON.parse(data) } }
|
|
339
365
|
|
|
340
366
|
stream.each do |parsed|
|
|
341
367
|
# parsed is whatever your parser returns
|
|
@@ -419,8 +445,8 @@ end
|
|
|
419
445
|
**Data Transformation**
|
|
420
446
|
|
|
421
447
|
```ruby
|
|
422
|
-
stream = ConduitSSE.new do |
|
|
423
|
-
|
|
448
|
+
stream = ConduitSSE.new do |config|
|
|
449
|
+
config.parser = ->(data) {
|
|
424
450
|
# Transform raw data into your domain models
|
|
425
451
|
raw = JSON.parse(data)
|
|
426
452
|
MyDomainModel.new(raw)
|
|
@@ -476,38 +502,38 @@ end
|
|
|
476
502
|
Every setting lives on `ConduitSSE::Config` and can be set via the block:
|
|
477
503
|
|
|
478
504
|
```ruby
|
|
479
|
-
stream = ConduitSSE.new do |
|
|
505
|
+
stream = ConduitSSE.new do |config|
|
|
480
506
|
# Required: A callable that receives the joined data field content (string)
|
|
481
507
|
# and returns whatever shape your application needs (e.g., JSON.parse, YAML.load).
|
|
482
|
-
|
|
508
|
+
config.parser = ->(data) { JSON.parse(data) }
|
|
483
509
|
|
|
484
510
|
# Optional: Transforms incoming chunks before processing.
|
|
485
511
|
# Default: UTF-8 conversion + CRLF→LF normalization.
|
|
486
512
|
# NOTE: Replacing the default fully replaces UTF-8 handling; if you need it,
|
|
487
513
|
# implement it yourself.
|
|
488
|
-
|
|
514
|
+
config.chunk_normalizer = ->(chunk) { chunk.upcase }
|
|
489
515
|
|
|
490
516
|
# Optional: Delimiter that separates frames in the stream.
|
|
491
517
|
# Default: "\n\n"
|
|
492
|
-
|
|
518
|
+
config.frame_separator = "\r\n\r\n"
|
|
493
519
|
|
|
494
520
|
# Optional: Prefix used to identify the data field.
|
|
495
521
|
# The trailing ":" is stripped to derive the field name.
|
|
496
522
|
# Default: "data:"
|
|
497
|
-
|
|
523
|
+
config.payload_start = "data:"
|
|
498
524
|
|
|
499
525
|
# Optional: Pattern identifying ping/comment frames.
|
|
500
526
|
# Default: ":"
|
|
501
|
-
|
|
527
|
+
config.ping_pattern = ":"
|
|
502
528
|
|
|
503
529
|
# Optional: Cleans or validates frame content after splitting.
|
|
504
530
|
# Default: UTF-8 conversion + strip.
|
|
505
531
|
# NOTE: Replacing the default fully replaces UTF-8 handling.
|
|
506
|
-
|
|
532
|
+
config.sanitize_pattern = ->(frame) { frame.strip }
|
|
507
533
|
|
|
508
534
|
# Optional: Enables the per-stage counter hash exposed via #stats.
|
|
509
535
|
# Default: false (off; #stats returns nil).
|
|
510
|
-
|
|
536
|
+
config.stats = true
|
|
511
537
|
end
|
|
512
538
|
```
|
|
513
539
|
|
|
@@ -519,7 +545,7 @@ prefer a compact one-liner.
|
|
|
519
545
|
For a simpler interface, use `each` to iterate over parsed events:
|
|
520
546
|
|
|
521
547
|
```ruby
|
|
522
|
-
stream = ConduitSSE.new { |
|
|
548
|
+
stream = ConduitSSE.new { |config| config.parser = ->(data) { data } }
|
|
523
549
|
|
|
524
550
|
stream.each do |parsed|
|
|
525
551
|
puts "Received: #{parsed}"
|
|
@@ -537,7 +563,7 @@ ConduitSSE tracks SSE spec state that you can access directly on the stream
|
|
|
537
563
|
[Architecture: Config and State](#architecture-config-and-state)):
|
|
538
564
|
|
|
539
565
|
```ruby
|
|
540
|
-
stream = ConduitSSE.new { |
|
|
566
|
+
stream = ConduitSSE.new { |config| config.parser = ->(data) { data } }
|
|
541
567
|
|
|
542
568
|
stream << "id: 123\ndata: hello\n\n"
|
|
543
569
|
|
|
@@ -550,30 +576,30 @@ puts stream.retry_ms # => nil (unless server sends retry field)
|
|
|
550
576
|
ConduitSSE provides read-only methods for monitoring stream activity without the overhead of the Inspector:
|
|
551
577
|
|
|
552
578
|
```ruby
|
|
553
|
-
stream = ConduitSSE.new { |
|
|
579
|
+
stream = ConduitSSE.new { |config| config.parser = ->(data) { data } }
|
|
554
580
|
|
|
555
|
-
# Check buffer size
|
|
581
|
+
# Check buffer size. Always available, zero-cost (one bytesize lookup).
|
|
556
582
|
stream << "data: hello"
|
|
557
583
|
puts stream.buffer_size # => buffer size in bytes
|
|
558
584
|
```
|
|
559
585
|
|
|
560
586
|
#### Stats are opt-in
|
|
561
587
|
|
|
562
|
-
Per-stage counters are **disabled by default**. Set `
|
|
588
|
+
Per-stage counters are **disabled by default**. Set `config.stats = true` (or pass
|
|
563
589
|
`stats: true` as a kwarg) to enable them. When stats are off, `#stats` returns
|
|
564
|
-
`nil` and the parser performs no counter bookkeeping per event
|
|
565
|
-
hot paths at high event rates.
|
|
590
|
+
`nil` and the parser performs no counter bookkeeping per event. That matters
|
|
591
|
+
on hot paths at high event rates.
|
|
566
592
|
|
|
567
593
|
```ruby
|
|
568
594
|
# Default: stats disabled
|
|
569
|
-
stream = ConduitSSE.new { |
|
|
595
|
+
stream = ConduitSSE.new { |config| config.parser = ->(data) { data } }
|
|
570
596
|
stream << "data: hello\n\n"
|
|
571
597
|
stream.stats # => nil
|
|
572
598
|
|
|
573
599
|
# Opt in:
|
|
574
|
-
stream = ConduitSSE.new do |
|
|
575
|
-
|
|
576
|
-
|
|
600
|
+
stream = ConduitSSE.new do |config|
|
|
601
|
+
config.parser = ->(data) { data }
|
|
602
|
+
config.stats = true
|
|
577
603
|
end
|
|
578
604
|
stream << "data: hello\n\n"
|
|
579
605
|
stream.stats
|
|
@@ -596,7 +622,7 @@ stream.stats
|
|
|
596
622
|
Use `finish` (or its alias `close`) once at the end of the stream to process any remaining data in the buffer:
|
|
597
623
|
|
|
598
624
|
```ruby
|
|
599
|
-
stream = ConduitSSE.new { |
|
|
625
|
+
stream = ConduitSSE.new { |config| config.parser = ->(data) { JSON.parse(data) } }
|
|
600
626
|
|
|
601
627
|
http.request(request) do |response|
|
|
602
628
|
response.read_body do |chunk|
|
|
@@ -621,7 +647,7 @@ stream.finish
|
|
|
621
647
|
Errors in callbacks are routed to the `on_error` handler, preventing stream interruption:
|
|
622
648
|
|
|
623
649
|
```ruby
|
|
624
|
-
stream = ConduitSSE.new { |
|
|
650
|
+
stream = ConduitSSE.new { |config| config.parser = ->(data) { JSON.parse(data) } }
|
|
625
651
|
|
|
626
652
|
stream.on_error do |error|
|
|
627
653
|
puts "Caught error: #{error.message}"
|
|
@@ -645,7 +671,7 @@ require "net/http"
|
|
|
645
671
|
require "uri"
|
|
646
672
|
require "json"
|
|
647
673
|
|
|
648
|
-
stream = ConduitSSE.new { |
|
|
674
|
+
stream = ConduitSSE.new { |config| config.parser = ->(data) { JSON.parse(data) } }
|
|
649
675
|
|
|
650
676
|
# Attach inspector to log everything to stdout
|
|
651
677
|
ConduitSSE::Inspector.attach(stream)
|
|
@@ -682,7 +708,7 @@ The inspector logs:
|
|
|
682
708
|
You can register multiple callbacks for the same event type:
|
|
683
709
|
|
|
684
710
|
```ruby
|
|
685
|
-
stream = ConduitSSE.new { |
|
|
711
|
+
stream = ConduitSSE.new { |config| config.parser = ->(data) { data } }
|
|
686
712
|
|
|
687
713
|
stream.on_parsed do |parsed|
|
|
688
714
|
puts "Handler 1: #{parsed}"
|
|
@@ -701,7 +727,7 @@ stream << "data: hello\n\n"
|
|
|
701
727
|
ConduitSSE emits all SSE fields, including custom ones:
|
|
702
728
|
|
|
703
729
|
```ruby
|
|
704
|
-
stream = ConduitSSE.new { |
|
|
730
|
+
stream = ConduitSSE.new { |config| config.parser = ->(data) { data } }
|
|
705
731
|
|
|
706
732
|
stream.on_field do |name, value|
|
|
707
733
|
case name
|
|
@@ -753,15 +779,15 @@ flowchart TD
|
|
|
753
779
|
|
|
754
780
|
Stage-by-stage:
|
|
755
781
|
|
|
756
|
-
- **1. Chunk Normalization
|
|
757
|
-
- **2. Buffering
|
|
758
|
-
- **3. Frame Splitting
|
|
759
|
-
- **4. Sanitization
|
|
760
|
-
- **5. Ping Detection
|
|
761
|
-
- **6. Field Parsing
|
|
762
|
-
- **7. Event Construction
|
|
763
|
-
- **8. Parser Application
|
|
764
|
-
- **9. Callback Emission
|
|
782
|
+
- **1. Chunk Normalization**: Raw chunks are normalized (UTF-8 conversion, CRLF→LF).
|
|
783
|
+
- **2. Buffering**: Chunks are buffered until frame boundaries are found.
|
|
784
|
+
- **3. Frame Splitting**: Frames are split by the separator (default: `\n\n`).
|
|
785
|
+
- **4. Sanitization**: Frames are sanitized (default: strip whitespace).
|
|
786
|
+
- **5. Ping Detection**: Ping/comment frames are identified and short-circuited.
|
|
787
|
+
- **6. Field Parsing**: SSE fields are parsed per the HTML spec.
|
|
788
|
+
- **7. Event Construction**: Events are built from parsed fields.
|
|
789
|
+
- **8. Parser Application**: Your custom parser transforms event data.
|
|
790
|
+
- **9. Callback Emission**: Callbacks are invoked at each stage.
|
|
765
791
|
|
|
766
792
|
### Architecture: Config and State
|
|
767
793
|
|
|
@@ -769,15 +795,15 @@ Internally a `Stream` is the composition of two distinct objects, each with a
|
|
|
769
795
|
different lifecycle:
|
|
770
796
|
|
|
771
797
|
```text
|
|
772
|
-
ConduitSSE::Stream
|
|
773
|
-
├─ #config : ConduitSSE::Config
|
|
774
|
-
└─ #state : ConduitSSE::State
|
|
798
|
+
ConduitSSE::Stream ← glues the two together
|
|
799
|
+
├─ #config : ConduitSSE::Config ← what to do (frozen after construction)
|
|
800
|
+
└─ #state : ConduitSSE::State ← where we are (mutated per event)
|
|
775
801
|
```
|
|
776
802
|
|
|
777
803
|
- **`ConduitSSE::Config`** holds the seven parsing knobs (parser, normalizers,
|
|
778
804
|
separators, patterns, the `stats` flag). It's populated via kwargs and/or
|
|
779
805
|
the configuration block, validated, has its derived `data_field` computed,
|
|
780
|
-
and is then **frozen
|
|
806
|
+
and is then **frozen**. Accidental mid-stream mutation raises
|
|
781
807
|
`FrozenError`. Both forms of public access work the same way:
|
|
782
808
|
|
|
783
809
|
```ruby
|
|
@@ -792,14 +818,14 @@ ConduitSSE::Stream ← glues the two together
|
|
|
792
818
|
stats counter hash. It's accessible via `stream.state` for inspection:
|
|
793
819
|
|
|
794
820
|
```ruby
|
|
795
|
-
stream.state.buffer_size
|
|
796
|
-
stream.state.last_event_id
|
|
797
|
-
stream.state.stats_enabled?
|
|
821
|
+
stream.state.buffer_size # bytes pending in the buffer
|
|
822
|
+
stream.state.last_event_id # last `id:` seen
|
|
823
|
+
stream.state.stats_enabled? # true iff stats: true was passed
|
|
798
824
|
```
|
|
799
825
|
|
|
800
826
|
The `#last_event_id`, `#retry_ms`, `#buffer_size`, and `#stats` methods on
|
|
801
827
|
`Stream` are thin forwarders to the underlying `State`, so you rarely need to
|
|
802
|
-
reach into `stream.state` directly
|
|
828
|
+
reach into `stream.state` directly, but it's there when you want to
|
|
803
829
|
introspect a stream from the outside (tests, dashboards, custom tooling).
|
|
804
830
|
|
|
805
831
|
### Performance Notes: Stats vs. Inspector
|
|
@@ -807,7 +833,7 @@ introspect a stream from the outside (tests, dashboards, custom tooling).
|
|
|
807
833
|
The `#stats` hash and the `ConduitSSE::Inspector` are **not** the same mechanism
|
|
808
834
|
and have very different performance profiles:
|
|
809
835
|
|
|
810
|
-
- **`#stats` is opt-in** (`
|
|
836
|
+
- **`#stats` is opt-in** (`config.stats = true` in the block, or `stats: true` as a
|
|
811
837
|
kwarg). When disabled (the default), `#stats` returns `nil` and the stream
|
|
812
838
|
performs **zero** per-event counter bookkeeping. The State object exposes
|
|
813
839
|
`#increment_stat` and `#add_fields` as null-object methods that return
|
|
@@ -818,8 +844,8 @@ and have very different performance profiles:
|
|
|
818
844
|
on in production if you want the metrics; safe to leave off if you don't.
|
|
819
845
|
- **`ConduitSSE::Inspector` is strictly opt-in.** It only does work after you
|
|
820
846
|
call `ConduitSSE::Inspector.attach(stream)`. Attachment registers regular
|
|
821
|
-
user callbacks (`on_chunk`, `on_frame`,
|
|
822
|
-
interpolation, and `IO#puts`. Those are **not** zero-cost
|
|
847
|
+
user callbacks (`on_chunk`, `on_frame`, etc.) that do `inspect`, string
|
|
848
|
+
interpolation, and `IO#puts`. Those are **not** zero-cost. Keep the
|
|
823
849
|
inspector off in production and use it for local debugging only.
|
|
824
850
|
- **Unregistered callbacks are free.** `Callbacks#emit` returns immediately
|
|
825
851
|
when no handler is registered for a given stage; there is no hidden
|
data/lib/conduit_sse/config.rb
CHANGED
|
@@ -15,10 +15,10 @@ module ConduitSSE
|
|
|
15
15
|
# ConduitSSE.new(parser: ->(d) { JSON.parse(d) }, stats: true)
|
|
16
16
|
#
|
|
17
17
|
# # Block form
|
|
18
|
-
# ConduitSSE.new do |
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
18
|
+
# ConduitSSE.new do |config|
|
|
19
|
+
# config.parser = ->(d) { JSON.parse(d) }
|
|
20
|
+
# config.stats = true
|
|
21
|
+
# config.frame_separator = "\r\n\r\n"
|
|
22
22
|
# end
|
|
23
23
|
#
|
|
24
24
|
# The two can be mixed; kwargs seed the config and the block then mutates
|
data/lib/conduit_sse/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: conduit-sse
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.0.
|
|
4
|
+
version: 2.0.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- franbach
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-05-
|
|
10
|
+
date: 2026-05-14 00:00:00.000000000 Z
|
|
11
11
|
dependencies: []
|
|
12
12
|
description: ConduitSSE provides a flexible callback-based architecture for processing
|
|
13
13
|
real-time server push data with full control over every stage of the parsing pipeline.
|