typed_bus 0.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.
Files changed (70) hide show
  1. checksums.yaml +7 -0
  2. data/.envrc +1 -0
  3. data/.github/workflows/deploy-github-pages.yml +52 -0
  4. data/.github/workflows/deploy-yard-docs.yml +52 -0
  5. data/.irbrc +10 -0
  6. data/CHANGELOG.md +19 -0
  7. data/COMMITS.md +196 -0
  8. data/README.md +416 -0
  9. data/Rakefile +12 -0
  10. data/docs/api/channel.md +94 -0
  11. data/docs/api/configuration.md +90 -0
  12. data/docs/api/dead-letter-queue.md +52 -0
  13. data/docs/api/delivery-tracker.md +66 -0
  14. data/docs/api/delivery.md +73 -0
  15. data/docs/api/index.md +45 -0
  16. data/docs/api/message-bus.md +101 -0
  17. data/docs/api/stats.md +50 -0
  18. data/docs/architecture/concurrency-model.md +38 -0
  19. data/docs/architecture/configuration-cascade.md +76 -0
  20. data/docs/architecture/index.md +7 -0
  21. data/docs/architecture/publish-flow.md +67 -0
  22. data/docs/assets/css/custom.css +11 -0
  23. data/docs/assets/images/typed_bus.gif +0 -0
  24. data/docs/assets/images/typed_bus.jpg +0 -0
  25. data/docs/assets/images/typed_bus.mp4 +0 -0
  26. data/docs/concepts.md +47 -0
  27. data/docs/examples/index.md +37 -0
  28. data/docs/getting-started/configuration.md +87 -0
  29. data/docs/getting-started/index.md +7 -0
  30. data/docs/getting-started/installation.md +33 -0
  31. data/docs/getting-started/quick-start.md +51 -0
  32. data/docs/guides/adaptive-throttling.md +76 -0
  33. data/docs/guides/backpressure.md +58 -0
  34. data/docs/guides/dead-letter-queues.md +78 -0
  35. data/docs/guides/delivery-and-ack.md +89 -0
  36. data/docs/guides/error-handling.md +91 -0
  37. data/docs/guides/graceful-shutdown.md +75 -0
  38. data/docs/guides/index.md +13 -0
  39. data/docs/guides/logging.md +93 -0
  40. data/docs/guides/stats-and-monitoring.md +75 -0
  41. data/docs/guides/typed-channels.md +63 -0
  42. data/docs/index.md +56 -0
  43. data/examples/00_config.rb +156 -0
  44. data/examples/01_basic_usage.rb +26 -0
  45. data/examples/02_typed_channels.rb +41 -0
  46. data/examples/03_multiple_subscribers.rb +44 -0
  47. data/examples/04_nack_and_dead_letters.rb +47 -0
  48. data/examples/05_delivery_timeout.rb +44 -0
  49. data/examples/06_backpressure.rb +32 -0
  50. data/examples/07_message_bus.rb +55 -0
  51. data/examples/08_error_handling.rb +82 -0
  52. data/examples/09_stats_and_monitoring.rb +71 -0
  53. data/examples/10_concurrent_pipeline.rb +116 -0
  54. data/examples/11_logging_basic.rb +60 -0
  55. data/examples/12_logging_failures.rb +65 -0
  56. data/examples/13_logging_backpressure.rb +56 -0
  57. data/examples/14_adaptive_throttling.rb +101 -0
  58. data/examples/README.md +27 -0
  59. data/examples/temp.md +95 -0
  60. data/lib/typed_bus/channel.rb +261 -0
  61. data/lib/typed_bus/configuration.rb +72 -0
  62. data/lib/typed_bus/dead_letter_queue.rb +65 -0
  63. data/lib/typed_bus/delivery.rb +80 -0
  64. data/lib/typed_bus/delivery_tracker.rb +107 -0
  65. data/lib/typed_bus/message_bus.rb +164 -0
  66. data/lib/typed_bus/stats.rb +38 -0
  67. data/lib/typed_bus/version.rb +5 -0
  68. data/lib/typed_bus.rb +60 -0
  69. data/mkdocs.yml +183 -0
  70. metadata +129 -0
data/README.md ADDED
@@ -0,0 +1,416 @@
1
+ # TypedBus
2
+ <em>Async pub/sub with typed channels and ACK-based delivery</em>
3
+
4
+ <table>
5
+ <tr>
6
+ <td width="50%" align="center" valign="top">
7
+ <img src="docs/assets/images/typed_bus.gif" alt="TypedBus" width="438"><br>
8
+ <a href="https://madbomber.github.io/typed_bus">Comprehensive Documentation</a>
9
+ </td>
10
+ <td width="50%" valign="top">
11
+ TypedBus provides named, optionally typed pub/sub channels with explicit ACK/NACK delivery, dead letter queues, backpressure, and adaptive throttling — all within a single Async reactor.<br><br>
12
+ <strong>Key Features</strong><br>
13
+
14
+ - <strong>Typed Channels</strong> - Restrict messages to a specific class<br>
15
+ - <strong>ACK-Based Delivery</strong> - Subscribers must explicitly ack or nack<br>
16
+ - <strong>Dead Letter Queues</strong> - Collect nacked and timed-out deliveries<br>
17
+ - <strong>Backpressure</strong> - Bound pending deliveries per channel<br>
18
+ - <strong>Adaptive Throttling</strong> - Progressive slowdown as capacity fills<br>
19
+ - <strong>Configuration Cascade</strong> - Global → Bus → Channel defaults<br>
20
+ - <strong>Stats Tracking</strong> - Per-channel publish/deliver/nack/timeout counters<br>
21
+ - <strong>Structured Logging</strong> - Optional Logger integration across all components
22
+ </td>
23
+ </tr>
24
+ </table>
25
+
26
+ ## Installation
27
+
28
+ Add to your Gemfile:
29
+
30
+ ```ruby
31
+ gem "typed_bus"
32
+ ```
33
+
34
+ Then run:
35
+
36
+ ```bash
37
+ bundle install
38
+ ```
39
+
40
+ ## Quick Start
41
+
42
+ ```ruby
43
+ require "typed_bus"
44
+
45
+ bus = TypedBus::MessageBus.new
46
+ bus.add_channel(:events, timeout: 5)
47
+
48
+ bus.subscribe(:events) do |delivery|
49
+ puts delivery.message
50
+ delivery.ack!
51
+ end
52
+
53
+ Async do
54
+ bus.publish(:events, "hello world")
55
+ end
56
+ ```
57
+
58
+ ## Channels
59
+
60
+ A channel is a named pub/sub topic. Subscribers receive `Delivery` envelopes and must call `ack!` or `nack!`.
61
+
62
+ ```ruby
63
+ channel = TypedBus::Channel.new(:orders, timeout: 10)
64
+ ```
65
+
66
+ ### Options
67
+
68
+ | Option | Type | Default | Description |
69
+ |--------|------|---------|-------------|
70
+ | `type` | `Class` | `nil` | Restricts published messages to a specific class |
71
+ | `timeout` | `Numeric` | `30` | Seconds before unresolved deliveries auto-nack |
72
+ | `max_pending` | `Integer` | `nil` | Backpressure limit; `nil` = unbounded |
73
+ | `throttle` | `Float` | `0.0` | Remaining-capacity ratio where backoff begins; `0.0` = disabled |
74
+
75
+ ### Typed Channels
76
+
77
+ Restrict a channel to a single message class:
78
+
79
+ ```ruby
80
+ channel = TypedBus::Channel.new(:orders, type: Order)
81
+
82
+ Async do
83
+ channel.publish(Order.new) # OK
84
+ channel.publish("not valid") # raises ArgumentError
85
+ end
86
+ ```
87
+
88
+ ### Subscribe and Unsubscribe
89
+
90
+ ```ruby
91
+ id = channel.subscribe do |delivery|
92
+ process(delivery.message)
93
+ delivery.ack!
94
+ end
95
+
96
+ channel.unsubscribe(id)
97
+ ```
98
+
99
+ ### Publish
100
+
101
+ ```ruby
102
+ Async do
103
+ tracker = channel.publish(my_message)
104
+ # tracker is a DeliveryTracker (nil if no subscribers)
105
+ end
106
+ ```
107
+
108
+ ## Delivery
109
+
110
+ Each subscriber receives a `Delivery` envelope wrapping the published message. The subscriber must resolve it:
111
+
112
+ ```ruby
113
+ channel.subscribe do |delivery|
114
+ delivery.message # the published object
115
+ delivery.channel_name
116
+ delivery.subscriber_id
117
+
118
+ delivery.ack! # mark as successfully processed
119
+ # or
120
+ delivery.nack! # mark as failed (routes to dead letter queue)
121
+ end
122
+ ```
123
+
124
+ If neither `ack!` nor `nack!` is called before the channel's `timeout`, the delivery auto-nacks.
125
+
126
+ ### Delivery States
127
+
128
+ | Method | Description |
129
+ |--------|-------------|
130
+ | `pending?` | Not yet resolved |
131
+ | `acked?` | Successfully acknowledged |
132
+ | `nacked?` | Explicitly rejected or timed out |
133
+ | `timed_out?` | True if the nack was caused by timeout |
134
+
135
+ ## Dead Letter Queue
136
+
137
+ Every channel has a dead letter queue that collects failed deliveries (nacked or timed out).
138
+
139
+ ```ruby
140
+ channel.dead_letter_queue.size
141
+ channel.dead_letter_queue.empty?
142
+
143
+ # Iterate without removing
144
+ channel.dead_letter_queue.each do |delivery|
145
+ puts "#{delivery.message} failed on subscriber ##{delivery.subscriber_id}"
146
+ end
147
+
148
+ # Drain and reprocess
149
+ channel.dead_letter_queue.drain do |delivery|
150
+ retry_message(delivery.message)
151
+ end
152
+ ```
153
+
154
+ Messages published with no subscribers are also routed to the DLQ.
155
+
156
+ ### DLQ Callback
157
+
158
+ ```ruby
159
+ channel.dead_letter_queue.on_dead_letter do |delivery|
160
+ alert("Dead letter: #{delivery.message}")
161
+ end
162
+ ```
163
+
164
+ ## Backpressure
165
+
166
+ Set `max_pending` to bound how many unresolved deliveries a channel allows. When the limit is reached, `publish` blocks the calling fiber until subscribers ack.
167
+
168
+ ```ruby
169
+ channel = TypedBus::Channel.new(:work,
170
+ max_pending: 100,
171
+ timeout: 10
172
+ )
173
+ ```
174
+
175
+ ## Adaptive Throttling
176
+
177
+ When a bounded channel approaches capacity, adaptive throttling progressively slows publishers before they hit the hard block.
178
+
179
+ Set `throttle` to a `Float` between `0.0` (exclusive) and `1.0` (exclusive). The value is the remaining-capacity ratio at which backoff begins. Below that threshold, each publish sleeps for `1 / (max_pending * remaining_ratio)` seconds.
180
+
181
+ ```ruby
182
+ channel = TypedBus::Channel.new(:pipeline,
183
+ max_pending: 10,
184
+ throttle: 0.5 # begin backoff at 50% remaining capacity
185
+ )
186
+ ```
187
+
188
+ ### Backoff Curve
189
+
190
+ With `max_pending: 10, throttle: 0.5`:
191
+
192
+ ```
193
+ Publish Pending Remaining Ratio Delay
194
+ ───────────────────────────────────────────
195
+ 1 0 10 1.0 —
196
+ 2 1 9 0.9 —
197
+ 3 2 8 0.8 —
198
+ 4 3 7 0.7 —
199
+ 5 4 6 0.6 —
200
+ 6 5 5 0.5 0.200s
201
+ 7 6 4 0.4 0.250s
202
+ 8 7 3 0.3 0.333s
203
+ 9 8 2 0.2 0.500s
204
+ 10 9 1 0.1 1.000s
205
+ 11 10 0 0.0 hard block
206
+ ```
207
+
208
+ The first 50% of capacity fills at full speed. The last 50% applies an asymptotic delay that approaches infinity as remaining capacity approaches zero. At zero remaining, the existing `wait_for_capacity` hard-blocks until a subscriber acks.
209
+
210
+ Higher `throttle` values begin backoff earlier:
211
+
212
+ ```ruby
213
+ TypedBus::Channel.new(:conservative, max_pending: 100, throttle: 0.8) # backoff at 80% remaining
214
+ TypedBus::Channel.new(:aggressive, max_pending: 100, throttle: 0.3) # backoff at 30% remaining
215
+ ```
216
+
217
+ ## Configuration
218
+
219
+ TypedBus resolves parameters through a three-tier cascade: **Global → Bus → Channel**. Each tier inherits from the one above unless explicitly overridden.
220
+
221
+ | Parameter | Default | Description |
222
+ |-----------|---------|-------------|
223
+ | `timeout` | `30` | Delivery ACK deadline in seconds |
224
+ | `max_pending` | `nil` | Backpressure limit; `nil` = unbounded |
225
+ | `throttle` | `0.0` | Remaining-capacity ratio where backoff begins; `0.0` = disabled |
226
+ | `logger` | `nil` | Ruby Logger instance (global-only) |
227
+ | `log_level` | `Logger::INFO` | Log level applied to the logger (global-only) |
228
+
229
+ ### Global Defaults
230
+
231
+ ```ruby
232
+ TypedBus.configure do |config|
233
+ config.timeout = 60
234
+ config.max_pending = 500
235
+ config.throttle = 0.5
236
+ config.logger = Logger.new($stdout)
237
+ config.log_level = Logger::DEBUG
238
+ end
239
+ ```
240
+
241
+ ### Cascade Example
242
+
243
+ ```ruby
244
+ # Global: timeout=60, max_pending=500
245
+ TypedBus.configure do |config|
246
+ config.timeout = 60
247
+ config.max_pending = 500
248
+ end
249
+
250
+ # Bus overrides timeout, inherits max_pending
251
+ bus = TypedBus::MessageBus.new(timeout: 30)
252
+
253
+ # Channel inherits both from bus (timeout=30, max_pending=500)
254
+ bus.add_channel(:orders)
255
+
256
+ # Channel overrides timeout, inherits max_pending from bus
257
+ bus.add_channel(:alerts, timeout: 5)
258
+ ```
259
+
260
+ ### Reset
261
+
262
+ Restore factory defaults (useful in tests):
263
+
264
+ ```ruby
265
+ TypedBus.reset_configuration!
266
+ ```
267
+
268
+ ## MessageBus
269
+
270
+ `MessageBus` is a registry facade that manages multiple named channels with shared stats.
271
+
272
+ ```ruby
273
+ bus = TypedBus::MessageBus.new
274
+ bus.add_channel(:orders, type: Order, max_pending: 100, timeout: 10)
275
+ bus.add_channel(:alerts, type: String)
276
+
277
+ bus.subscribe(:orders) { |d| process(d.message); d.ack! }
278
+ bus.subscribe(:alerts) { |d| log(d.message); d.ack! }
279
+
280
+ Async do
281
+ bus.publish(:orders, Order.new)
282
+ bus.publish(:alerts, "system ok")
283
+ end
284
+ ```
285
+
286
+ ### Bus-Level Defaults
287
+
288
+ Pass `timeout`, `max_pending`, or `throttle` to the constructor to set defaults for all channels on this bus:
289
+
290
+ ```ruby
291
+ bus = TypedBus::MessageBus.new(timeout: 10, max_pending: 200, throttle: 0.5)
292
+
293
+ # Inherits all bus defaults
294
+ bus.add_channel(:orders, type: Order)
295
+
296
+ # Overrides throttle, inherits timeout and max_pending
297
+ bus.add_channel(:alerts, throttle: 0.3)
298
+
299
+ # Disables throttle for this channel
300
+ bus.add_channel(:logs, throttle: 0.0)
301
+ ```
302
+
303
+ ### Bus Methods
304
+
305
+ | Method | Description |
306
+ |--------|-------------|
307
+ | `add_channel(name, **opts)` | Register a named channel |
308
+ | `remove_channel(name)` | Close and remove a channel |
309
+ | `publish(name, message)` | Publish to a named channel |
310
+ | `subscribe(name, &block)` | Subscribe to a named channel |
311
+ | `unsubscribe(name, id)` | Remove a subscriber |
312
+ | `pending?(name)` | Check for unresolved deliveries |
313
+ | `pending_count(name)` | Count unresolved deliveries |
314
+ | `dead_letters(name)` | Access a channel's DLQ |
315
+ | `close(name)` | Close a specific channel |
316
+ | `close_all` | Close all channels |
317
+ | `channel?(name)` | Check if a channel exists |
318
+ | `channel_names` | List registered channel names |
319
+ | `clear!` | Reset all channels and stats |
320
+ | `stats` | Access the shared Stats object |
321
+
322
+ ## Stats
323
+
324
+ `MessageBus` tracks per-channel counters automatically:
325
+
326
+ ```ruby
327
+ bus.stats[:orders_published] # messages published
328
+ bus.stats[:orders_delivered] # all subscribers acked
329
+ bus.stats[:orders_nacked] # explicit nacks
330
+ bus.stats[:orders_timed_out] # delivery timeouts
331
+ bus.stats[:orders_dead_lettered] # entries added to DLQ
332
+ bus.stats[:orders_throttled] # publishes that were rate-limited
333
+
334
+ bus.stats.to_h # snapshot of all counters
335
+ bus.stats.reset! # zero all counters
336
+ ```
337
+
338
+ When using `Channel` directly, pass a `Stats` instance:
339
+
340
+ ```ruby
341
+ stats = TypedBus::Stats.new
342
+ channel = TypedBus::Channel.new(:work, stats: stats, timeout: 5)
343
+ ```
344
+
345
+ ## Logging
346
+
347
+ Assign a standard Ruby `Logger` to get structured log output from all components:
348
+
349
+ ```ruby
350
+ TypedBus.configure do |config|
351
+ config.logger = Logger.new($stdout)
352
+ end
353
+
354
+ # or use the shortcut
355
+ TypedBus.logger = Logger.new($stdout)
356
+ ```
357
+
358
+ Set to `nil` (the default) to disable logging.
359
+
360
+ Log output covers channel lifecycle, publish/subscribe events, delivery resolution, DLQ entries, backpressure waits, and throttle delays.
361
+
362
+ ## Closing and Cleanup
363
+
364
+ ### Close a Channel
365
+
366
+ Stops accepting new publishes and subscribes. Pending deliveries are force-nacked and routed to the DLQ.
367
+
368
+ ```ruby
369
+ channel.close
370
+ channel.closed? # => true
371
+ ```
372
+
373
+ ### Clear a Channel
374
+
375
+ Hard reset: cancels all timeout tasks, discards pending state and DLQ contents.
376
+
377
+ ```ruby
378
+ channel.clear!
379
+ ```
380
+
381
+ ### Bus Shutdown
382
+
383
+ ```ruby
384
+ bus.close_all # close every channel
385
+ bus.clear! # close, clear DLQs, and reset stats
386
+ ```
387
+
388
+ ## Architecture
389
+
390
+ ```
391
+ Publisher
392
+
393
+
394
+ Channel (type check → throttle → backpressure → fan-out)
395
+
396
+ ├──▶ Subscriber 1 ──▶ Delivery ──▶ ack!/nack!
397
+ ├──▶ Subscriber 2 ──▶ Delivery ──▶ ack!/nack!
398
+ └──▶ Subscriber N ──▶ Delivery ──▶ ack!/nack!
399
+
400
+
401
+ DeliveryTracker (aggregates N responses)
402
+
403
+ ├── all acked ──▶ :delivered stat
404
+ └── any nacked ──▶ Dead Letter Queue
405
+ ```
406
+
407
+ **Concurrency model:** Fiber-only, powered by the `async` gem. No mutexes anywhere. All state is managed within a single Async reactor. `sleep` calls inside throttle and backpressure yield the fiber, not the thread.
408
+
409
+ ## Requirements
410
+
411
+ - Ruby >= 3.2.0
412
+ - [async](https://github.com/socketry/async) ~> 2.0
413
+
414
+ ## License
415
+
416
+ MIT
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create(:test) do |t|
7
+ t.libs << "test"
8
+ t.test_prelude = 'require "test_helper"'
9
+ t.test_globs = ["test/**/*_test.rb"]
10
+ end
11
+
12
+ task default: :test
@@ -0,0 +1,94 @@
1
+ # Channel
2
+
3
+ `TypedBus::Channel`
4
+
5
+ A named, typed pub/sub channel with explicit acknowledgment tracking.
6
+
7
+ ## Constructor
8
+
9
+ ### `Channel.new(name, type:, timeout:, max_pending:, stats:, throttle:)`
10
+
11
+ | Parameter | Default | Description |
12
+ |-----------|---------|-------------|
13
+ | `name` | required | `Symbol` channel name |
14
+ | `type` | `nil` | Optional class constraint for messages |
15
+ | `timeout` | `30` | Seconds before delivery auto-nacks |
16
+ | `max_pending` | `nil` | Backpressure limit; `nil` = unbounded |
17
+ | `stats` | `nil` | Optional `Stats` instance for counters |
18
+ | `throttle` | `0.0` | Capacity ratio where backoff begins |
19
+
20
+ ```ruby
21
+ channel = TypedBus::Channel.new(:orders,
22
+ type: Order,
23
+ timeout: 10,
24
+ max_pending: 100,
25
+ throttle: 0.5
26
+ )
27
+ ```
28
+
29
+ !!! note
30
+ When using channels via `MessageBus`, these values are resolved through the configuration cascade. You don't normally construct channels directly with all parameters.
31
+
32
+ ## Attributes
33
+
34
+ ### `name` → `Symbol`
35
+
36
+ Channel name.
37
+
38
+ ### `type` → `Class`, `nil`
39
+
40
+ Type constraint, or `nil` for untyped channels.
41
+
42
+ ### `dead_letter_queue` → `DeadLetterQueue`
43
+
44
+ The channel's dead letter queue.
45
+
46
+ ## Pub/Sub
47
+
48
+ ### `publish(message)` → `DeliveryTracker`, `nil`
49
+
50
+ Publish a message to all current subscribers.
51
+
52
+ - Blocks the calling fiber if bounded and at capacity
53
+ - Applies throttle delay if configured
54
+ - Returns `nil` if no subscribers (message goes to DLQ)
55
+
56
+ **Raises:** `ArgumentError` for type mismatches. `RuntimeError` if closed.
57
+
58
+ ### `subscribe(&block)` → `Integer`
59
+
60
+ Subscribe to messages. Returns the subscriber ID.
61
+
62
+ **Raises:** `RuntimeError` if closed.
63
+
64
+ ### `unsubscribe(id_or_block)`
65
+
66
+ Remove a subscriber by integer ID or block reference.
67
+
68
+ ## Query
69
+
70
+ ### `subscriber_count` → `Integer`
71
+
72
+ Number of active subscribers.
73
+
74
+ ### `pending_count` → `Integer`
75
+
76
+ Number of unresolved delivery trackers.
77
+
78
+ ### `pending?` → `Boolean`
79
+
80
+ True when there are unresolved deliveries.
81
+
82
+ ### `closed?` → `Boolean`
83
+
84
+ True if the channel has been closed.
85
+
86
+ ## Lifecycle
87
+
88
+ ### `close`
89
+
90
+ Stop accepting new publishes and subscribes. Pending deliveries are force-NACKed and routed to the DLQ.
91
+
92
+ ### `clear!`
93
+
94
+ Hard reset: cancel all timeout tasks, discard pending state, clear DLQ. Does not mark the channel as closed.
@@ -0,0 +1,90 @@
1
+ # Configuration
2
+
3
+ `TypedBus::Configuration`
4
+
5
+ Holds configurable defaults for the message bus system. Supports three-tier cascade: Global → Bus → Channel.
6
+
7
+ ## Attributes
8
+
9
+ | Attribute | Type | Default | Description |
10
+ |-----------|------|---------|-------------|
11
+ | `timeout` | `Numeric` | `30` | Delivery ACK deadline in seconds |
12
+ | `max_pending` | `Integer`, `nil` | `nil` | Backpressure limit; `nil` = unbounded |
13
+ | `throttle` | `Float` | `0.0` | Remaining-capacity ratio where backoff begins |
14
+ | `logger` | `Logger`, `nil` | `nil` | Ruby Logger instance |
15
+ | `log_level` | `Integer` | `Logger::INFO` | Log level applied to the logger |
16
+
17
+ ## Constructor
18
+
19
+ ### `Configuration.new`
20
+
21
+ Creates a new Configuration with factory defaults.
22
+
23
+ ## Methods
24
+
25
+ ### `timeout=`, `max_pending=`, `throttle=`
26
+
27
+ Standard setters for cascade parameters.
28
+
29
+ ### `logger=(log)`
30
+
31
+ Sets the logger and applies the current `log_level` to it.
32
+
33
+ ```ruby
34
+ config.logger = Logger.new($stdout)
35
+ # config.logger.level is now config.log_level
36
+ ```
37
+
38
+ ### `log_level=(level)`
39
+
40
+ Sets the log level and applies it to the current logger (if present).
41
+
42
+ ```ruby
43
+ config.log_level = Logger::DEBUG
44
+ # config.logger.level is now Logger::DEBUG (if logger is set)
45
+ ```
46
+
47
+ ### `resolve(timeout:, max_pending:, throttle:)`
48
+
49
+ Merges overrides on top of this configuration's values. Parameters set to `:use_default` inherit from this config.
50
+
51
+ **Parameters:**
52
+
53
+ | Parameter | Default | Description |
54
+ |-----------|---------|-------------|
55
+ | `timeout` | `:use_default` | Delivery ACK deadline |
56
+ | `max_pending` | `:use_default` | Backpressure limit |
57
+ | `throttle` | `:use_default` | Capacity ratio for adaptive backoff |
58
+
59
+ **Returns:** `Hash` with resolved `:timeout`, `:max_pending`, and `:throttle` values.
60
+
61
+ ```ruby
62
+ config = TypedBus::Configuration.new
63
+ config.timeout = 60
64
+
65
+ config.resolve(timeout: 10)
66
+ # => { timeout: 10, max_pending: nil, throttle: 0.0 }
67
+
68
+ config.resolve
69
+ # => { timeout: 60, max_pending: nil, throttle: 0.0 }
70
+ ```
71
+
72
+ ### `dup`
73
+
74
+ Returns an independent shallow copy. The logger is intentionally shared (same Logger instance); all other fields are independent.
75
+
76
+ ## Constants
77
+
78
+ ### `DEFAULTS`
79
+
80
+ Frozen hash of factory defaults:
81
+
82
+ ```ruby
83
+ {
84
+ timeout: 30,
85
+ max_pending: nil,
86
+ throttle: 0.0,
87
+ logger: nil,
88
+ log_level: Logger::INFO
89
+ }
90
+ ```
@@ -0,0 +1,52 @@
1
+ # DeadLetterQueue
2
+
3
+ `TypedBus::DeadLetterQueue`
4
+
5
+ Stores deliveries that failed — either explicitly NACKed or timed out. Each Channel owns a DLQ. Includes `Enumerable`.
6
+
7
+ ## Constructor
8
+
9
+ ### `DeadLetterQueue.new(&on_dead_letter)`
10
+
11
+ Optionally accepts a block that fires on each `push`.
12
+
13
+ ## Methods
14
+
15
+ ### `push(delivery)`
16
+
17
+ Add a failed delivery.
18
+
19
+ ### `size` → `Integer`
20
+
21
+ Number of entries.
22
+
23
+ ### `empty?` → `Boolean`
24
+
25
+ True if no entries.
26
+
27
+ ### `each(&block)`
28
+
29
+ Iterate over dead letters without removing them.
30
+
31
+ ### `drain { |delivery| ... }` → `Array<Delivery>`
32
+
33
+ Yield each dead letter and remove it from the queue. Returns the drained entries. Without a block, just removes and returns them.
34
+
35
+ ### `clear!`
36
+
37
+ Discard all entries.
38
+
39
+ ### `on_dead_letter { |delivery| ... }`
40
+
41
+ Register or replace the callback fired when entries arrive.
42
+
43
+ ## Enumerable
44
+
45
+ `DeadLetterQueue` includes `Enumerable`, so you can use `map`, `select`, `count`, `any?`, etc.:
46
+
47
+ ```ruby
48
+ dlq = channel.dead_letter_queue
49
+
50
+ timed_out = dlq.count(&:timed_out?)
51
+ messages = dlq.map(&:message)
52
+ ```