smart_message 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.
- checksums.yaml +7 -0
- data/.envrc +3 -0
- data/.gitignore +8 -0
- data/.travis.yml +7 -0
- data/CHANGELOG.md +100 -0
- data/COMMITS.md +196 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +71 -0
- data/README.md +303 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/docs/README.md +52 -0
- data/docs/architecture.md +370 -0
- data/docs/dispatcher.md +593 -0
- data/docs/examples.md +808 -0
- data/docs/getting-started.md +235 -0
- data/docs/ideas_to_think_about.md +329 -0
- data/docs/serializers.md +575 -0
- data/docs/transports.md +501 -0
- data/docs/troubleshooting.md +582 -0
- data/examples/01_point_to_point_orders.rb +200 -0
- data/examples/02_publish_subscribe_events.rb +364 -0
- data/examples/03_many_to_many_chat.rb +608 -0
- data/examples/README.md +335 -0
- data/examples/tmux_chat/README.md +283 -0
- data/examples/tmux_chat/bot_agent.rb +272 -0
- data/examples/tmux_chat/human_agent.rb +197 -0
- data/examples/tmux_chat/room_monitor.rb +158 -0
- data/examples/tmux_chat/shared_chat_system.rb +295 -0
- data/examples/tmux_chat/start_chat_demo.sh +190 -0
- data/examples/tmux_chat/stop_chat_demo.sh +22 -0
- data/lib/simple_stats.rb +57 -0
- data/lib/smart_message/base.rb +284 -0
- data/lib/smart_message/dispatcher/.keep +0 -0
- data/lib/smart_message/dispatcher.rb +146 -0
- data/lib/smart_message/errors.rb +29 -0
- data/lib/smart_message/header.rb +20 -0
- data/lib/smart_message/logger/base.rb +8 -0
- data/lib/smart_message/logger.rb +7 -0
- data/lib/smart_message/serializer/base.rb +23 -0
- data/lib/smart_message/serializer/json.rb +22 -0
- data/lib/smart_message/serializer.rb +10 -0
- data/lib/smart_message/transport/base.rb +85 -0
- data/lib/smart_message/transport/memory_transport.rb +69 -0
- data/lib/smart_message/transport/registry.rb +59 -0
- data/lib/smart_message/transport/stdout_transport.rb +62 -0
- data/lib/smart_message/transport.rb +41 -0
- data/lib/smart_message/version.rb +7 -0
- data/lib/smart_message/wrapper.rb +43 -0
- data/lib/smart_message.rb +54 -0
- data/smart_message.gemspec +53 -0
- metadata +252 -0
data/docs/transports.md
ADDED
@@ -0,0 +1,501 @@
|
|
1
|
+
# Transport Layer
|
2
|
+
|
3
|
+
The transport layer is responsible for moving messages between systems. SmartMessage provides a pluggable transport architecture that supports various backend systems.
|
4
|
+
|
5
|
+
## Overview
|
6
|
+
|
7
|
+
Transports handle:
|
8
|
+
- **Publishing**: Sending messages to a destination
|
9
|
+
- **Subscribing**: Registering interest in message types
|
10
|
+
- **Routing**: Directing incoming messages to the dispatcher
|
11
|
+
- **Connection Management**: Handling connections to external systems
|
12
|
+
|
13
|
+
## Built-in Transports
|
14
|
+
|
15
|
+
### STDOUT Transport
|
16
|
+
|
17
|
+
Perfect for development, debugging, and logging scenarios.
|
18
|
+
|
19
|
+
**Features:**
|
20
|
+
- Outputs messages to console or file
|
21
|
+
- Optional loopback for testing subscriptions
|
22
|
+
- Human-readable message formatting
|
23
|
+
- No external dependencies
|
24
|
+
|
25
|
+
**Usage:**
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
# Basic STDOUT output
|
29
|
+
transport = SmartMessage::Transport.create(:stdout)
|
30
|
+
|
31
|
+
# With loopback enabled (messages get processed locally)
|
32
|
+
transport = SmartMessage::Transport.create(:stdout, loopback: true)
|
33
|
+
|
34
|
+
# Output to file instead of console
|
35
|
+
transport = SmartMessage::Transport.create(:stdout, output: "messages.log")
|
36
|
+
|
37
|
+
# Configure in message class
|
38
|
+
class LogMessage < SmartMessage::Base
|
39
|
+
property :level
|
40
|
+
property :message
|
41
|
+
|
42
|
+
config do
|
43
|
+
transport SmartMessage::Transport.create(:stdout,
|
44
|
+
output: "app.log",
|
45
|
+
loopback: false
|
46
|
+
)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
```
|
50
|
+
|
51
|
+
**Options:**
|
52
|
+
- `loopback` (Boolean): Whether to process published messages locally (default: false)
|
53
|
+
- `output` (String|IO): Output destination - filename string or IO object (default: $stdout)
|
54
|
+
|
55
|
+
**Example Output:**
|
56
|
+
```
|
57
|
+
===================================================
|
58
|
+
== SmartMessage Published via STDOUT Transport
|
59
|
+
== Header: #<SmartMessage::Header:0x... @uuid="abc-123", @message_class="MyMessage", ...>
|
60
|
+
== Payload: {"user_id":123,"action":"login","timestamp":"2025-08-17T10:30:00Z"}
|
61
|
+
===================================================
|
62
|
+
```
|
63
|
+
|
64
|
+
### Memory Transport
|
65
|
+
|
66
|
+
Ideal for testing and in-memory message queuing.
|
67
|
+
|
68
|
+
**Features:**
|
69
|
+
- Stores messages in memory
|
70
|
+
- Thread-safe operations
|
71
|
+
- Optional auto-processing
|
72
|
+
- Message inspection capabilities
|
73
|
+
- Memory overflow protection
|
74
|
+
|
75
|
+
**Usage:**
|
76
|
+
|
77
|
+
```ruby
|
78
|
+
# Auto-process messages as they're published
|
79
|
+
transport = SmartMessage::Transport.create(:memory, auto_process: true)
|
80
|
+
|
81
|
+
# Store messages without processing (manual control)
|
82
|
+
transport = SmartMessage::Transport.create(:memory, auto_process: false)
|
83
|
+
|
84
|
+
# Configure maximum message storage
|
85
|
+
transport = SmartMessage::Transport.create(:memory,
|
86
|
+
auto_process: false,
|
87
|
+
max_messages: 500
|
88
|
+
)
|
89
|
+
|
90
|
+
# Use in message class
|
91
|
+
class TestMessage < SmartMessage::Base
|
92
|
+
property :data
|
93
|
+
|
94
|
+
config do
|
95
|
+
transport SmartMessage::Transport.create(:memory, auto_process: true)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
```
|
99
|
+
|
100
|
+
**Options:**
|
101
|
+
- `auto_process` (Boolean): Automatically route messages to dispatcher (default: true)
|
102
|
+
- `max_messages` (Integer): Maximum messages to store in memory (default: 1000)
|
103
|
+
|
104
|
+
**Message Management:**
|
105
|
+
|
106
|
+
```ruby
|
107
|
+
transport = SmartMessage::Transport.create(:memory)
|
108
|
+
|
109
|
+
# Check stored messages
|
110
|
+
puts transport.message_count
|
111
|
+
puts transport.all_messages
|
112
|
+
|
113
|
+
# Process all pending messages manually
|
114
|
+
transport.process_all
|
115
|
+
|
116
|
+
# Clear all stored messages
|
117
|
+
transport.clear_messages
|
118
|
+
|
119
|
+
# Access individual messages
|
120
|
+
messages = transport.all_messages
|
121
|
+
messages.each do |msg|
|
122
|
+
puts "Published at: #{msg[:published_at]}"
|
123
|
+
puts "Header: #{msg[:header]}"
|
124
|
+
puts "Payload: #{msg[:payload]}"
|
125
|
+
end
|
126
|
+
```
|
127
|
+
|
128
|
+
## Transport Interface
|
129
|
+
|
130
|
+
All transports must implement the `SmartMessage::Transport::Base` interface:
|
131
|
+
|
132
|
+
### Required Methods
|
133
|
+
|
134
|
+
```ruby
|
135
|
+
class CustomTransport < SmartMessage::Transport::Base
|
136
|
+
# Publish a message
|
137
|
+
def publish(message_header, message_payload)
|
138
|
+
# Send the message via your transport mechanism
|
139
|
+
end
|
140
|
+
|
141
|
+
# Optional: Override default options
|
142
|
+
def default_options
|
143
|
+
{
|
144
|
+
connection_timeout: 30,
|
145
|
+
retry_attempts: 3
|
146
|
+
}
|
147
|
+
end
|
148
|
+
|
149
|
+
# Optional: Custom configuration setup
|
150
|
+
def configure
|
151
|
+
@connection = establish_connection(@options)
|
152
|
+
end
|
153
|
+
|
154
|
+
# Optional: Connection status checking
|
155
|
+
def connected?
|
156
|
+
@connection&.connected?
|
157
|
+
end
|
158
|
+
|
159
|
+
# Optional: Cleanup resources
|
160
|
+
def disconnect
|
161
|
+
@connection&.close
|
162
|
+
end
|
163
|
+
end
|
164
|
+
```
|
165
|
+
|
166
|
+
### Inherited Methods
|
167
|
+
|
168
|
+
Transports automatically inherit these methods from `SmartMessage::Transport::Base`:
|
169
|
+
|
170
|
+
```ruby
|
171
|
+
# Subscription management (uses dispatcher)
|
172
|
+
transport.subscribe(message_class, process_method)
|
173
|
+
transport.unsubscribe(message_class, process_method)
|
174
|
+
transport.unsubscribe!(message_class)
|
175
|
+
transport.subscribers
|
176
|
+
|
177
|
+
# Connection management
|
178
|
+
transport.connect
|
179
|
+
transport.disconnect
|
180
|
+
transport.connected?
|
181
|
+
|
182
|
+
# Message receiving (call this from your transport)
|
183
|
+
transport.receive(message_header, message_payload) # protected method
|
184
|
+
```
|
185
|
+
|
186
|
+
## Transport Registration
|
187
|
+
|
188
|
+
Register custom transports for easy creation:
|
189
|
+
|
190
|
+
```ruby
|
191
|
+
# Register a transport class
|
192
|
+
SmartMessage::Transport.register(:redis, RedisTransport)
|
193
|
+
SmartMessage::Transport.register(:kafka, KafkaTransport)
|
194
|
+
|
195
|
+
# List all registered transports
|
196
|
+
puts SmartMessage::Transport.available
|
197
|
+
# => [:stdout, :memory, :redis, :kafka]
|
198
|
+
|
199
|
+
# Create instances
|
200
|
+
redis_transport = SmartMessage::Transport.create(:redis,
|
201
|
+
url: "redis://localhost:6379"
|
202
|
+
)
|
203
|
+
|
204
|
+
kafka_transport = SmartMessage::Transport.create(:kafka,
|
205
|
+
servers: ["localhost:9092"]
|
206
|
+
)
|
207
|
+
```
|
208
|
+
|
209
|
+
## Configuration Patterns
|
210
|
+
|
211
|
+
### Class-Level Configuration
|
212
|
+
|
213
|
+
```ruby
|
214
|
+
class OrderMessage < SmartMessage::Base
|
215
|
+
property :order_id
|
216
|
+
property :amount
|
217
|
+
|
218
|
+
# All instances use this transport by default
|
219
|
+
config do
|
220
|
+
transport SmartMessage::Transport.create(:memory, auto_process: true)
|
221
|
+
serializer SmartMessage::Serializer::JSON.new
|
222
|
+
end
|
223
|
+
end
|
224
|
+
```
|
225
|
+
|
226
|
+
### Instance-Level Override
|
227
|
+
|
228
|
+
```ruby
|
229
|
+
# Override transport for specific instances
|
230
|
+
order = OrderMessage.new(order_id: "123", amount: 99.99)
|
231
|
+
|
232
|
+
order.config do
|
233
|
+
# This instance will use STDOUT instead of memory
|
234
|
+
transport SmartMessage::Transport.create(:stdout, loopback: true)
|
235
|
+
end
|
236
|
+
|
237
|
+
order.publish # Uses STDOUT transport
|
238
|
+
```
|
239
|
+
|
240
|
+
### Runtime Transport Switching
|
241
|
+
|
242
|
+
```ruby
|
243
|
+
class NotificationMessage < SmartMessage::Base
|
244
|
+
property :recipient
|
245
|
+
property :message
|
246
|
+
|
247
|
+
def self.send_via_email
|
248
|
+
config do
|
249
|
+
transport EmailTransport.new
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
def self.send_via_sms
|
254
|
+
config do
|
255
|
+
transport SMSTransport.new
|
256
|
+
end
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
# Switch transport at runtime
|
261
|
+
NotificationMessage.send_via_email
|
262
|
+
notification = NotificationMessage.new(
|
263
|
+
recipient: "user@example.com",
|
264
|
+
message: "Hello!"
|
265
|
+
)
|
266
|
+
notification.publish # Sent via email
|
267
|
+
```
|
268
|
+
|
269
|
+
## Transport Options
|
270
|
+
|
271
|
+
### Common Options Pattern
|
272
|
+
|
273
|
+
Most transports support these common option patterns:
|
274
|
+
|
275
|
+
```ruby
|
276
|
+
transport = SmartMessage::Transport.create(:custom,
|
277
|
+
# Connection options
|
278
|
+
host: "localhost",
|
279
|
+
port: 5672,
|
280
|
+
username: "guest",
|
281
|
+
password: "guest",
|
282
|
+
|
283
|
+
# Retry options
|
284
|
+
retry_attempts: 3,
|
285
|
+
retry_delay: 1.0,
|
286
|
+
|
287
|
+
# Timeout options
|
288
|
+
connection_timeout: 30,
|
289
|
+
read_timeout: 10,
|
290
|
+
|
291
|
+
# Behavior options
|
292
|
+
auto_reconnect: true,
|
293
|
+
persistent: true
|
294
|
+
)
|
295
|
+
```
|
296
|
+
|
297
|
+
### Transport-Specific Options
|
298
|
+
|
299
|
+
Each transport may have specific options:
|
300
|
+
|
301
|
+
```ruby
|
302
|
+
# STDOUT specific
|
303
|
+
SmartMessage::Transport.create(:stdout,
|
304
|
+
loopback: true,
|
305
|
+
output: "/var/log/messages.log"
|
306
|
+
)
|
307
|
+
|
308
|
+
# Memory specific
|
309
|
+
SmartMessage::Transport.create(:memory,
|
310
|
+
auto_process: false,
|
311
|
+
max_messages: 1000
|
312
|
+
)
|
313
|
+
|
314
|
+
# Hypothetical Redis specific
|
315
|
+
SmartMessage::Transport.create(:redis,
|
316
|
+
url: "redis://localhost:6379",
|
317
|
+
db: 1,
|
318
|
+
namespace: "messages",
|
319
|
+
ttl: 3600
|
320
|
+
)
|
321
|
+
```
|
322
|
+
|
323
|
+
## Error Handling
|
324
|
+
|
325
|
+
### Transport Errors
|
326
|
+
|
327
|
+
Transports should handle their own connection and transmission errors:
|
328
|
+
|
329
|
+
```ruby
|
330
|
+
class RobustTransport < SmartMessage::Transport::Base
|
331
|
+
def publish(message_header, message_payload)
|
332
|
+
retry_count = 0
|
333
|
+
begin
|
334
|
+
send_message(message_header, message_payload)
|
335
|
+
rescue ConnectionError => e
|
336
|
+
retry_count += 1
|
337
|
+
if retry_count <= @options[:retry_attempts]
|
338
|
+
sleep(@options[:retry_delay])
|
339
|
+
retry
|
340
|
+
else
|
341
|
+
# Log error and potentially fallback
|
342
|
+
handle_publish_error(e, message_header, message_payload)
|
343
|
+
end
|
344
|
+
end
|
345
|
+
end
|
346
|
+
|
347
|
+
private
|
348
|
+
|
349
|
+
def handle_publish_error(error, header, payload)
|
350
|
+
# Log the error
|
351
|
+
puts "Failed to publish message: #{error.message}"
|
352
|
+
|
353
|
+
# Optional: Store for later retry
|
354
|
+
store_failed_message(header, payload)
|
355
|
+
|
356
|
+
# Optional: Use fallback transport
|
357
|
+
fallback_transport&.publish(header, payload)
|
358
|
+
end
|
359
|
+
end
|
360
|
+
```
|
361
|
+
|
362
|
+
### Connection Monitoring
|
363
|
+
|
364
|
+
```ruby
|
365
|
+
class MonitoredTransport < SmartMessage::Transport::Base
|
366
|
+
def connected?
|
367
|
+
@connection&.ping rescue false
|
368
|
+
end
|
369
|
+
|
370
|
+
def publish(message_header, message_payload)
|
371
|
+
unless connected?
|
372
|
+
connect
|
373
|
+
end
|
374
|
+
|
375
|
+
super
|
376
|
+
end
|
377
|
+
|
378
|
+
def connect
|
379
|
+
@connection = establish_connection(@options)
|
380
|
+
puts "Connected to #{@options[:host]}:#{@options[:port]}"
|
381
|
+
rescue => e
|
382
|
+
puts "Failed to connect: #{e.message}"
|
383
|
+
raise
|
384
|
+
end
|
385
|
+
end
|
386
|
+
```
|
387
|
+
|
388
|
+
## Performance Considerations
|
389
|
+
|
390
|
+
### Message Batching
|
391
|
+
|
392
|
+
For high-throughput scenarios, consider batching:
|
393
|
+
|
394
|
+
```ruby
|
395
|
+
class BatchingTransport < SmartMessage::Transport::Base
|
396
|
+
def initialize(options = {})
|
397
|
+
super
|
398
|
+
@batch = []
|
399
|
+
@batch_mutex = Mutex.new
|
400
|
+
setup_batch_timer
|
401
|
+
end
|
402
|
+
|
403
|
+
def publish(message_header, message_payload)
|
404
|
+
@batch_mutex.synchronize do
|
405
|
+
@batch << [message_header, message_payload]
|
406
|
+
|
407
|
+
if @batch.size >= @options[:batch_size]
|
408
|
+
flush_batch
|
409
|
+
end
|
410
|
+
end
|
411
|
+
end
|
412
|
+
|
413
|
+
private
|
414
|
+
|
415
|
+
def flush_batch
|
416
|
+
return if @batch.empty?
|
417
|
+
|
418
|
+
batch_to_send = @batch.dup
|
419
|
+
@batch.clear
|
420
|
+
|
421
|
+
send_batch(batch_to_send)
|
422
|
+
end
|
423
|
+
end
|
424
|
+
```
|
425
|
+
|
426
|
+
### Connection Pooling
|
427
|
+
|
428
|
+
For database or network transports:
|
429
|
+
|
430
|
+
```ruby
|
431
|
+
class PooledTransport < SmartMessage::Transport::Base
|
432
|
+
def initialize(options = {})
|
433
|
+
super
|
434
|
+
@connection_pool = ConnectionPool.new(
|
435
|
+
size: @options[:pool_size] || 5,
|
436
|
+
timeout: @options[:pool_timeout] || 5
|
437
|
+
) { create_connection }
|
438
|
+
end
|
439
|
+
|
440
|
+
def publish(message_header, message_payload)
|
441
|
+
@connection_pool.with do |connection|
|
442
|
+
connection.send(message_header, message_payload)
|
443
|
+
end
|
444
|
+
end
|
445
|
+
end
|
446
|
+
```
|
447
|
+
|
448
|
+
## Testing Transports
|
449
|
+
|
450
|
+
### Mock Transport for Testing
|
451
|
+
|
452
|
+
```ruby
|
453
|
+
class MockTransport < SmartMessage::Transport::Base
|
454
|
+
attr_reader :published_messages
|
455
|
+
|
456
|
+
def initialize(options = {})
|
457
|
+
super
|
458
|
+
@published_messages = []
|
459
|
+
end
|
460
|
+
|
461
|
+
def publish(message_header, message_payload)
|
462
|
+
@published_messages << {
|
463
|
+
header: message_header,
|
464
|
+
payload: message_payload,
|
465
|
+
published_at: Time.now
|
466
|
+
}
|
467
|
+
|
468
|
+
# Optionally trigger processing
|
469
|
+
receive(message_header, message_payload) if @options[:auto_process]
|
470
|
+
end
|
471
|
+
|
472
|
+
def clear
|
473
|
+
@published_messages.clear
|
474
|
+
end
|
475
|
+
end
|
476
|
+
|
477
|
+
# Use in tests
|
478
|
+
RSpec.describe "Message Publishing" do
|
479
|
+
let(:transport) { MockTransport.new(auto_process: true) }
|
480
|
+
|
481
|
+
before do
|
482
|
+
MyMessage.config do
|
483
|
+
transport transport
|
484
|
+
end
|
485
|
+
end
|
486
|
+
|
487
|
+
it "publishes messages" do
|
488
|
+
MyMessage.new(data: "test").publish
|
489
|
+
|
490
|
+
expect(transport.published_messages).to have(1).message
|
491
|
+
expect(transport.published_messages.first[:payload]).to include("test")
|
492
|
+
end
|
493
|
+
end
|
494
|
+
```
|
495
|
+
|
496
|
+
## Next Steps
|
497
|
+
|
498
|
+
- [Custom Transports](custom-transports.md) - Build your own transport
|
499
|
+
- [Serializers](serializers.md) - Understanding message serialization
|
500
|
+
- [Dispatcher](dispatcher.md) - Message routing and processing
|
501
|
+
- [Examples](examples.md) - Real-world transport usage patterns
|