fractor 0.1.4 → 0.1.6

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 (33) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop-https---raw-githubusercontent-com-riboseinc-oss-guides-main-ci-rubocop-yml +552 -0
  3. data/.rubocop.yml +14 -8
  4. data/.rubocop_todo.yml +162 -46
  5. data/README.adoc +1364 -376
  6. data/examples/auto_detection/auto_detection.rb +9 -9
  7. data/examples/continuous_chat_common/message_protocol.rb +53 -0
  8. data/examples/continuous_chat_fractor/README.adoc +217 -0
  9. data/examples/continuous_chat_fractor/chat_client.rb +303 -0
  10. data/examples/continuous_chat_fractor/chat_common.rb +83 -0
  11. data/examples/continuous_chat_fractor/chat_server.rb +167 -0
  12. data/examples/continuous_chat_fractor/simulate.rb +345 -0
  13. data/examples/continuous_chat_server/README.adoc +135 -0
  14. data/examples/continuous_chat_server/chat_client.rb +303 -0
  15. data/examples/continuous_chat_server/chat_server.rb +359 -0
  16. data/examples/continuous_chat_server/simulate.rb +343 -0
  17. data/examples/hierarchical_hasher/hierarchical_hasher.rb +12 -8
  18. data/examples/multi_work_type/multi_work_type.rb +30 -29
  19. data/examples/pipeline_processing/pipeline_processing.rb +15 -15
  20. data/examples/producer_subscriber/producer_subscriber.rb +20 -16
  21. data/examples/scatter_gather/scatter_gather.rb +29 -28
  22. data/examples/simple/sample.rb +5 -5
  23. data/examples/specialized_workers/specialized_workers.rb +44 -37
  24. data/lib/fractor/continuous_server.rb +188 -0
  25. data/lib/fractor/result_aggregator.rb +1 -1
  26. data/lib/fractor/supervisor.rb +277 -104
  27. data/lib/fractor/version.rb +1 -1
  28. data/lib/fractor/work_queue.rb +68 -0
  29. data/lib/fractor/work_result.rb +1 -1
  30. data/lib/fractor/worker.rb +2 -1
  31. data/lib/fractor/wrapped_ractor.rb +12 -2
  32. data/lib/fractor.rb +2 -0
  33. metadata +15 -2
@@ -86,8 +86,8 @@ puts
86
86
 
87
87
  supervisor1 = Fractor::Supervisor.new(
88
88
  worker_pools: [
89
- { worker_class: ComputeWorker } # No num_workers specified
90
- ]
89
+ { worker_class: ComputeWorker }, # No num_workers specified
90
+ ],
91
91
  )
92
92
 
93
93
  # Add work items
@@ -97,7 +97,7 @@ supervisor1.add_work_items(work_items)
97
97
  puts "Processing 10 work items with auto-detected workers..."
98
98
  supervisor1.run
99
99
 
100
- puts "Results: #{supervisor1.results.results.map(&:result).sort.join(", ")}"
100
+ puts "Results: #{supervisor1.results.results.map(&:result).sort.join(', ')}"
101
101
  puts "✓ Auto-detection successful!"
102
102
  puts
103
103
 
@@ -110,8 +110,8 @@ puts
110
110
 
111
111
  supervisor2 = Fractor::Supervisor.new(
112
112
  worker_pools: [
113
- { worker_class: ComputeWorker, num_workers: 4 }
114
- ]
113
+ { worker_class: ComputeWorker, num_workers: 4 },
114
+ ],
115
115
  )
116
116
 
117
117
  supervisor2.add_work_items((11..20).map { |i| ComputeWork.new(i) })
@@ -119,7 +119,7 @@ supervisor2.add_work_items((11..20).map { |i| ComputeWork.new(i) })
119
119
  puts "Processing 10 work items with 4 explicitly configured workers..."
120
120
  supervisor2.run
121
121
 
122
- puts "Results: #{supervisor2.results.results.map(&:result).sort.join(", ")}"
122
+ puts "Results: #{supervisor2.results.results.map(&:result).sort.join(', ')}"
123
123
  puts "✓ Explicit configuration successful!"
124
124
  puts
125
125
 
@@ -135,8 +135,8 @@ puts
135
135
  supervisor3 = Fractor::Supervisor.new(
136
136
  worker_pools: [
137
137
  { worker_class: ComputeWorker }, # Auto-detected
138
- { worker_class: ComputeWorker, num_workers: 2 } # Explicit
139
- ]
138
+ { worker_class: ComputeWorker, num_workers: 2 }, # Explicit
139
+ ],
140
140
  )
141
141
 
142
142
  supervisor3.add_work_items((21..30).map { |i| ComputeWork.new(i) })
@@ -144,7 +144,7 @@ supervisor3.add_work_items((21..30).map { |i| ComputeWork.new(i) })
144
144
  puts "Processing 10 work items with mixed configuration..."
145
145
  supervisor3.run
146
146
 
147
- puts "Results: #{supervisor3.results.results.map(&:result).sort.join(", ")}"
147
+ puts "Results: #{supervisor3.results.results.map(&:result).sort.join(', ')}"
148
148
  puts "✓ Mixed configuration successful!"
149
149
  puts
150
150
 
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "json"
5
+ require "time"
6
+
7
+ module ContinuousChat
8
+ # Message packet class for handling protocol messages
9
+ class MessagePacket
10
+ attr_reader :type, :data, :timestamp
11
+
12
+ def initialize(type, data, timestamp = Time.now.to_i)
13
+ @type = type.to_sym
14
+ @data = data
15
+ @timestamp = timestamp
16
+ end
17
+
18
+ # Convert to JSON string
19
+ def to_json(*_args)
20
+ {
21
+ type: @type,
22
+ data: @data,
23
+ timestamp: @timestamp,
24
+ }.to_json
25
+ end
26
+
27
+ # String representation
28
+ def to_s
29
+ to_json
30
+ end
31
+ end
32
+
33
+ # Helper module for message protocol
34
+ module MessageProtocol
35
+ # Create a packet of the given type with data
36
+ def self.create_packet(type, data)
37
+ MessagePacket.new(type, data).to_json
38
+ end
39
+
40
+ # Parse a JSON string into a message packet
41
+ def self.parse_packet(json_string)
42
+ data = JSON.parse(json_string)
43
+ type = data["type"]&.to_sym
44
+ content = data["data"]
45
+ timestamp = data["timestamp"] || Time.now.to_i
46
+
47
+ MessagePacket.new(type, content, timestamp)
48
+ rescue JSON::ParserError => e
49
+ puts "Error parsing message: #{e.message}"
50
+ nil
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,217 @@
1
+ = Continuous Chat Server Example (Fractor-based)
2
+
3
+ == Overview
4
+
5
+ This example demonstrates Fractor's continuous mode feature with a chat server implementation. Unlike the plain socket implementation in `examples/continuous_chat_server/`, this version uses Fractor's Worker and Supervisor classes to process chat messages concurrently.
6
+
7
+ The example shows how to:
8
+
9
+ * Use Fractor in continuous mode (`continuous_mode: true`)
10
+ * Register work source callbacks with `register_work_source`
11
+ * Process messages asynchronously using Ractor-based workers
12
+ * Coordinate between main thread socket handling and Fractor workers
13
+ * Implement graceful shutdown
14
+
15
+ == Key Concepts
16
+
17
+ * *Continuous Mode*: The Fractor supervisor runs indefinitely, processing work as it arrives
18
+ * *Work Sources*: Callback functions that provide new work items to the supervisor on demand
19
+ * *Asynchronous Processing*: ChatWorker processes messages concurrently in Ractors
20
+ * *Thread Coordination*: Multiple threads working together - main thread handles I/O, Fractor workers process messages
21
+ * *Message Logging*: All message processing is logged to demonstrate Fractor's work distribution
22
+
23
+ == Architecture
24
+
25
+ === Fractor Components
26
+
27
+ The server uses the following Fractor components:
28
+
29
+ 1. *ChatMessage (Fractor::Work)*: Represents a chat message as a unit of work
30
+ - Encapsulates the message packet and optional client socket reference
31
+ - Each message becomes a work item in the Fractor queue
32
+
33
+ 2. *ChatWorker (Fractor::Worker)*: Processes chat messages
34
+ - Runs in a Ractor for parallel processing
35
+ - Handles different message types (broadcast, direct_message, server_message, user_list)
36
+ - Returns WorkResult with processing outcome
37
+
38
+ 3. *Supervisor*: Orchestrates the workers
39
+ - Configured with `continuous_mode: true` to run indefinitely
40
+ - Uses 2 worker Ractors (auto-detected from system processors)
41
+ - Registered work source pulls from a thread-safe Queue
42
+
43
+ === Thread Architecture
44
+
45
+ The server uses three concurrent components:
46
+
47
+ 1. *Main Thread*: Handles socket I/O with `IO.select`
48
+ - Accepts new client connections
49
+ - Reads messages from client sockets
50
+ - Sends responses back to clients
51
+ - Puts received messages into the work queue
52
+
53
+ 2. *Supervisor Thread*: Runs the Fractor supervisor
54
+ - Continuously pulls work from the queue via the work source callback
55
+ - Distributes work to available ChatWorker Ractors
56
+ - Collects results in the ResultAggregator
57
+
58
+ 3. *Results Thread*: Processes completed work
59
+ - Monitors the ResultAggregator for new results
60
+ - Logs processing outcomes
61
+ - Handles errors from workers
62
+
63
+ == Example Components
64
+
65
+ 1. *chat_common.rb*: Shared code with Fractor classes
66
+ * `MessagePacket` class for message protocol
67
+ * `MessageProtocol` module for serialization
68
+ * `ChatMessage` class extending `Fractor::Work`
69
+ * `ChatWorker` class extending `Fractor::Worker`
70
+
71
+ 2. *chat_server.rb*: Fractor-based chat server
72
+ * Socket handling in main thread
73
+ * Fractor supervisor in continuous mode
74
+ * Work source callback pulling from Queue
75
+ * Results processing thread
76
+
77
+ 3. *chat_client.rb*: Simple chat client (reused from plain example)
78
+ * Connects to server via TCP socket
79
+ * Sends and receives JSON messages
80
+
81
+ 4. *simulate.rb*: Automated simulation
82
+ * Creates server and multiple clients as processes
83
+ * Runs predefined message schedule
84
+ * Analyzes logs after completion
85
+
86
+ == Running the Example
87
+
88
+ === Running the Simulation
89
+
90
+ To run the complete automated simulation:
91
+
92
+ [source,sh]
93
+ ----
94
+ ruby examples/continuous_chat_fractor/simulate.rb
95
+ ----
96
+
97
+ Optional parameters:
98
+ * `-p, --port PORT` - Specify server port (default: 3000)
99
+ * `-d, --duration SECONDS` - Duration of simulation in seconds (default: 10)
100
+ * `-l, --log-dir DIR` - Directory for log files (default: logs)
101
+ * `-h, --help` - Show help message
102
+
103
+ === Running Server and Clients Separately
104
+
105
+ Run the server in one terminal:
106
+
107
+ [source,sh]
108
+ ----
109
+ ruby examples/continuous_chat_fractor/chat_server.rb [PORT] [LOG_FILE]
110
+ ----
111
+
112
+ Run clients in different terminals:
113
+
114
+ [source,sh]
115
+ ----
116
+ ruby examples/continuous_chat_fractor/chat_client.rb [USERNAME] [PORT] [LOG_FILE]
117
+ ----
118
+
119
+ == Features Demonstrated
120
+
121
+ * *Continuous Mode*: Supervisor runs indefinitely without stopping
122
+ * *Work Source Callback*: Dynamically provides work from a Queue
123
+ * *Concurrent Processing*: Multiple Ractor workers process messages in parallel
124
+ * *Thread Coordination*: Main thread, supervisor thread, and results thread work together
125
+ * *Message Logging*: All operations logged to files for verification
126
+ * *Graceful Shutdown*: Proper cleanup of Fractor supervisor and sockets
127
+
128
+ == Comparison with Plain Socket Implementation
129
+
130
+ The plain socket implementation (`examples/continuous_chat_server/`) uses:
131
+ - `IO.select` for non-blocking I/O
132
+ - Sequential message processing in the main thread
133
+ - Simple, straightforward architecture
134
+
135
+ The Fractor-based implementation demonstrates:
136
+ - Parallel message processing using Ractors
137
+ - Work queue pattern with work source callbacks
138
+ - Separation of concerns (I/O vs processing)
139
+ - Continuous mode supervisor pattern
140
+
141
+ Both implementations are functional. The Fractor version shows how to structure a long-running server using Fractor's continuous mode, which is useful for:
142
+ - CPU-intensive message processing
143
+ - Scaling message handling across cores
144
+ - Separating I/O from computation
145
+ - Learning Fractor's continuous mode patterns
146
+
147
+ == Expected Output
148
+
149
+ The simulation will show:
150
+ * Fractor supervisor starting with workers
151
+ * Clients connecting to the server
152
+ * Messages being sent between clients
153
+ * Messages being added to Fractor work queue (logged as "Received from...")
154
+ * Graceful shutdown of all components
155
+
156
+ NOTE: In this implementation, Fractor workers process messages in parallel for demonstration purposes (analyzing message types, logging processing), while the main thread handles actual message delivery to ensure real-time responsiveness. The work items are successfully queued and processed by workers - you can verify this by seeing that all messages are correctly broadcast/delivered to clients.
157
+
158
+ == Log Files
159
+
160
+ After running the simulation, check the `logs/` directory:
161
+
162
+ * `server_messages.log` - Server activity and Fractor processing
163
+ * `client_<username>_messages.log` - Client activity
164
+ * `client_<username>_send_messages.json` - Messages sent by client
165
+
166
+ == Implementation Notes
167
+
168
+ === Why Thread-safe Queue?
169
+
170
+ The implementation uses Ruby's `Queue` class (thread-safe) to coordinate between:
171
+ - Main thread (producing work from socket I/O)
172
+ - Supervisor thread (consuming work via work source callback)
173
+
174
+ This is necessary because Ractors cannot directly share mutable objects with threads.
175
+
176
+ === Work Source Callback
177
+
178
+ The work source callback pulls up to 5 messages from the queue at once:
179
+
180
+ [source,ruby]
181
+ ----
182
+ supervisor.register_work_source do
183
+ messages = []
184
+ 5.times do
185
+ break if message_queue.empty?
186
+ msg = message_queue.pop(true) rescue nil
187
+ messages << msg if msg
188
+ end
189
+ messages.empty? ? nil : messages
190
+ end
191
+ ----
192
+
193
+ This batching improves efficiency by reducing callback overhead.
194
+
195
+ === Results Processing
196
+
197
+ A separate thread monitors the ResultAggregator because:
198
+ - The main thread is busy with socket I/O
199
+ - The supervisor thread is running `supervisor.run`
200
+ - We want to log results as they complete
201
+
202
+ In a production system, you might process results differently (e.g., send notifications, update databases, etc.).
203
+
204
+ == Continuous Mode Benefits
205
+
206
+ This example demonstrates key benefits of Fractor's continuous mode:
207
+
208
+ 1. *Non-stopping Execution*: Server runs indefinitely, processing messages as they arrive
209
+ 2. *Dynamic Work Addition*: Work source callback provides new work on demand
210
+ 3. *Resource Efficiency*: Workers idle when no work available
211
+ 4. *Parallel Processing*: Multiple messages processed concurrently
212
+ 5. *Graceful Shutdown*: `supervisor.stop` cleanly terminates workers
213
+
214
+ == See Also
215
+
216
+ * link:../continuous_chat_server/[Plain Socket Implementation] - Simpler approach without Fractor
217
+ * link:../../README.adoc#continuous-mode[Main README Continuous Mode Section]
@@ -0,0 +1,303 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'socket'
5
+ require 'json'
6
+ require 'fileutils'
7
+
8
+ module ContinuousChat
9
+ # Simple Chat Client using Ruby's standard socket library
10
+ class ChatClient
11
+ def initialize(username, server_host = 'localhost', server_port = 3000,
12
+ log_file_path = nil)
13
+ @username = username
14
+ @server_host = server_host
15
+ @server_port = server_port
16
+ @running = true
17
+
18
+ # Set up logging
19
+ @log_file_path = log_file_path || "logs/client_#{username}_messages.log"
20
+ FileUtils.mkdir_p(File.dirname(@log_file_path))
21
+ @log_file = File.open(@log_file_path, 'w')
22
+
23
+ log_message("Client initialized for #{username}, connecting to #{server_host}:#{server_port}")
24
+ end
25
+
26
+ def connect
27
+ puts "Connecting to server at #{@server_host}:#{@server_port}..."
28
+ log_message("Connecting to server at #{@server_host}:#{@server_port}")
29
+
30
+ begin
31
+ @socket = TCPSocket.new(@server_host, @server_port)
32
+
33
+ # Send join message
34
+ join_data = {
35
+ type: 'join',
36
+ data: {
37
+ username: @username
38
+ },
39
+ timestamp: Time.now.to_i
40
+ }
41
+ @socket.puts(JSON.generate(join_data))
42
+ log_message("Sent join message: #{join_data}")
43
+
44
+ puts 'Connected to chat server!'
45
+ log_message('Connected to chat server')
46
+
47
+ true
48
+ rescue StandardError => e
49
+ puts "Failed to connect: #{e.message}"
50
+ log_message("Failed to connect: #{e.message}")
51
+ false
52
+ end
53
+ end
54
+
55
+ def start
56
+ return false unless @socket
57
+
58
+ # Main event loop that handles both sending and receiving messages
59
+ # without creating separate threads
60
+ main_event_loop
61
+
62
+ true
63
+ end
64
+
65
+ # Main event loop that handles both user input and server messages
66
+ def main_event_loop
67
+ log_message('Starting main event loop')
68
+
69
+ # Print initial prompt
70
+ print "(#{@username})> "
71
+ $stdout.flush
72
+
73
+ while @running
74
+ # Use IO.select to wait for input from either STDIN or the socket
75
+ # This is non-blocking and allows us to handle both in a single loop
76
+ readable, = IO.select([@socket, $stdin], nil, nil, 0.1)
77
+
78
+ next unless readable # Nothing to process this iteration
79
+
80
+ readable.each do |io|
81
+ if io == $stdin
82
+ # Handle user input
83
+ handle_user_input
84
+ elsif io == @socket
85
+ # Handle server message
86
+ handle_server_message
87
+ end
88
+ end
89
+ end
90
+ rescue Interrupt
91
+ log_message('Client interrupted')
92
+ @running = false
93
+ rescue StandardError => e
94
+ log_message("Error in main event loop: #{e.message}")
95
+ @running = false
96
+ ensure
97
+ disconnect
98
+ end
99
+
100
+ # Handle user input (non-blocking)
101
+ def handle_user_input
102
+ text = $stdin.gets&.chomp
103
+ return unless text
104
+
105
+ # Check if client wants to quit
106
+ if text == '/quit' || text.nil?
107
+ @running = false
108
+ return
109
+ end
110
+
111
+ # Create message packet
112
+ message_data = {
113
+ type: 'message',
114
+ data: {
115
+ content: text,
116
+ recipient: 'all' # Default to broadcast
117
+ },
118
+ timestamp: Time.now.to_i
119
+ }
120
+
121
+ # Send to server
122
+ @socket.puts(JSON.generate(message_data))
123
+ log_message("Sent message: #{text}")
124
+
125
+ # Print prompt for next input
126
+ print "(#{@username})> "
127
+ $stdout.flush
128
+ rescue StandardError => e
129
+ log_message("Error handling user input: #{e.message}")
130
+ @running = false
131
+ end
132
+
133
+ # Handle server message (non-blocking)
134
+ def handle_server_message
135
+ line = @socket.gets&.chomp
136
+
137
+ if line.nil?
138
+ # Server closed the connection
139
+ log_message('Connection to server lost')
140
+ @running = false
141
+ return
142
+ end
143
+
144
+ # Parse and handle the message
145
+ message = JSON.parse(line)
146
+ log_message("Received: #{line}")
147
+
148
+ # Display formatted message based on type
149
+ case message['type']
150
+ when 'broadcast'
151
+ puts "\r#{message['data']['from']}: #{message['data']['content']}"
152
+ when 'direct_message'
153
+ puts "\r[DM] #{message['data']['from']}: #{message['data']['content']}"
154
+ when 'server_message'
155
+ puts "\r[Server] #{message['data']['message']}"
156
+ when 'user_list'
157
+ puts "\r[Server] Users online: #{message['data']['users'].join(', ')}"
158
+ when 'error'
159
+ puts "\r[Error] #{message['data']['message']}"
160
+ end
161
+
162
+ # Reprint the prompt
163
+ print "(#{@username})> "
164
+ $stdout.flush
165
+ rescue StandardError => e
166
+ log_message("Error handling server message: #{e.message}")
167
+ # Don't immediately break for connection errors, may be temporary
168
+ # Just log and continue, IO.select will catch closed connections
169
+ end
170
+
171
+ def run_with_messages(messages, _delay_between_messages = 1)
172
+ return false unless @socket && @running
173
+
174
+ log_message("Running with #{messages.size} predefined messages")
175
+ puts "Sending #{messages.size} predefined messages"
176
+
177
+ # Send all messages in a non-blocking way
178
+ batch_send_messages(messages)
179
+
180
+ log_message('Finished sending all predefined messages')
181
+
182
+ # Start the event loop to receive responses
183
+ main_event_loop
184
+
185
+ true
186
+ end
187
+
188
+ # Helper to send a batch of messages without blocking
189
+ def batch_send_messages(messages)
190
+ messages.each_with_index do |msg, index|
191
+ content = msg[:content]
192
+ recipient = msg[:recipient] || 'all'
193
+
194
+ log_message("Sending message #{index + 1}: '#{content}' to #{recipient}")
195
+
196
+ message_data = {
197
+ type: 'message',
198
+ data: {
199
+ content: content,
200
+ recipient: recipient
201
+ },
202
+ timestamp: Time.now.to_i
203
+ }
204
+
205
+ @socket.puts(JSON.generate(message_data))
206
+ log_message("Sent message to #{recipient}: #{content}")
207
+
208
+ # Small delay between messages for stability
209
+ sleep(0.1)
210
+ end
211
+ end
212
+
213
+ def disconnect
214
+ return unless @running
215
+
216
+ @running = false
217
+ log_message('Disconnecting from server')
218
+
219
+ if @socket && !@socket.closed?
220
+ # Send leave message
221
+ leave_data = {
222
+ type: 'leave',
223
+ data: {
224
+ username: @username
225
+ },
226
+ timestamp: Time.now.to_i
227
+ }
228
+ @socket.puts(JSON.generate(leave_data))
229
+ log_message('Sent leave message')
230
+
231
+ @socket.close
232
+ end
233
+
234
+ @log_file&.close
235
+
236
+ puts 'Disconnected from server.'
237
+ true
238
+ end
239
+
240
+ private
241
+
242
+ def log_message(message)
243
+ timestamp = Time.now.strftime('%Y-%m-%d %H:%M:%S.%L')
244
+ log_entry = "[#{timestamp}] #{message}"
245
+
246
+ @log_file.puts(log_entry)
247
+ @log_file.flush # Ensure it's written immediately
248
+ end
249
+ end
250
+ end
251
+
252
+ # When run directly, start the client
253
+ if __FILE__ == $PROGRAM_NAME
254
+ require 'fileutils'
255
+ require 'json'
256
+
257
+ puts 'Chat Client'
258
+ puts '==========='
259
+ puts 'This is the chat client that connects to the chat server.'
260
+ puts 'All messages are logged to a file for later analysis.'
261
+ puts
262
+
263
+ # Get username from command line or prompt
264
+ username = ARGV[0]
265
+
266
+ unless username
267
+ print 'Enter your username: '
268
+ username = gets.chomp
269
+ end
270
+
271
+ # Get port from command line or use default
272
+ port = ARGV[1]&.to_i || 3000
273
+ log_file = ARGV[2] || "logs/client_#{username}_messages.log"
274
+
275
+ # Check for messages file
276
+ messages_file = "logs/client_#{username}_send_messages.json"
277
+
278
+ # Create and run the client
279
+ client = ContinuousChat::ChatClient.new(username, 'localhost', port, log_file)
280
+
281
+ if client.connect
282
+ begin
283
+ # Load and send messages if the file exists
284
+ if File.exist?(messages_file)
285
+ puts "Loading messages from #{messages_file}"
286
+ messages = JSON.parse(File.read(messages_file), symbolize_names: true)
287
+ puts "Loaded #{messages.size} messages"
288
+
289
+ # Send the messages
290
+ client.run_with_messages(messages)
291
+ end
292
+
293
+ # Start the client
294
+ client.start
295
+ rescue Interrupt
296
+ puts "\nClient interrupted."
297
+ ensure
298
+ client.disconnect
299
+ end
300
+ end
301
+
302
+ puts 'Chat client exited.'
303
+ end