fractor 0.1.3 → 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.
- checksums.yaml +4 -4
- data/.rubocop-https---raw-githubusercontent-com-riboseinc-oss-guides-main-ci-rubocop-yml +552 -0
- data/.rubocop.yml +14 -8
- data/.rubocop_todo.yml +154 -48
- data/README.adoc +1371 -317
- data/examples/auto_detection/README.adoc +52 -0
- data/examples/auto_detection/auto_detection.rb +170 -0
- data/examples/continuous_chat_common/message_protocol.rb +53 -0
- data/examples/continuous_chat_fractor/README.adoc +217 -0
- data/examples/continuous_chat_fractor/chat_client.rb +303 -0
- data/examples/continuous_chat_fractor/chat_common.rb +83 -0
- data/examples/continuous_chat_fractor/chat_server.rb +167 -0
- data/examples/continuous_chat_fractor/simulate.rb +345 -0
- data/examples/continuous_chat_server/README.adoc +135 -0
- data/examples/continuous_chat_server/chat_client.rb +303 -0
- data/examples/continuous_chat_server/chat_server.rb +359 -0
- data/examples/continuous_chat_server/simulate.rb +343 -0
- data/examples/hierarchical_hasher/hierarchical_hasher.rb +12 -8
- data/examples/multi_work_type/multi_work_type.rb +30 -29
- data/examples/pipeline_processing/pipeline_processing.rb +15 -15
- data/examples/producer_subscriber/producer_subscriber.rb +20 -16
- data/examples/scatter_gather/scatter_gather.rb +29 -28
- data/examples/simple/sample.rb +38 -6
- data/examples/specialized_workers/specialized_workers.rb +44 -37
- data/lib/fractor/continuous_server.rb +188 -0
- data/lib/fractor/result_aggregator.rb +1 -1
- data/lib/fractor/supervisor.rb +291 -108
- data/lib/fractor/version.rb +1 -1
- data/lib/fractor/work_queue.rb +68 -0
- data/lib/fractor/work_result.rb +1 -1
- data/lib/fractor/worker.rb +2 -1
- data/lib/fractor/wrapped_ractor.rb +12 -2
- data/lib/fractor.rb +2 -0
- metadata +17 -2
|
@@ -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
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative '../continuous_chat_common/message_protocol'
|
|
5
|
+
require_relative '../../lib/fractor'
|
|
6
|
+
|
|
7
|
+
module ContinuousChatFractor
|
|
8
|
+
# ChatMessage represents a chat message as a unit of work for Fractor
|
|
9
|
+
class ChatMessage < Fractor::Work
|
|
10
|
+
def initialize(packet, client_socket = nil)
|
|
11
|
+
super({ packet: packet, client_socket: client_socket })
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def packet
|
|
15
|
+
input[:packet]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def client_socket
|
|
19
|
+
input[:client_socket]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def to_s
|
|
23
|
+
"ChatMessage: #{packet.type} from #{packet.data}"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# ChatWorker processes chat messages using Fractor
|
|
28
|
+
class ChatWorker < Fractor::Worker
|
|
29
|
+
def process(work)
|
|
30
|
+
packet = work.packet
|
|
31
|
+
work.client_socket
|
|
32
|
+
|
|
33
|
+
# Process based on message type
|
|
34
|
+
result = case packet.type
|
|
35
|
+
when :broadcast
|
|
36
|
+
# Broadcast message processing
|
|
37
|
+
{
|
|
38
|
+
action: :broadcast,
|
|
39
|
+
from: packet.data[:from],
|
|
40
|
+
content: packet.data[:content],
|
|
41
|
+
timestamp: packet.timestamp
|
|
42
|
+
}
|
|
43
|
+
when :direct_message
|
|
44
|
+
# Direct message processing
|
|
45
|
+
{
|
|
46
|
+
action: :direct_message,
|
|
47
|
+
from: packet.data[:from],
|
|
48
|
+
to: packet.data[:to],
|
|
49
|
+
content: packet.data[:content],
|
|
50
|
+
timestamp: packet.timestamp
|
|
51
|
+
}
|
|
52
|
+
when :server_message
|
|
53
|
+
# Server message processing
|
|
54
|
+
{
|
|
55
|
+
action: :server_message,
|
|
56
|
+
message: packet.data[:message],
|
|
57
|
+
timestamp: packet.timestamp
|
|
58
|
+
}
|
|
59
|
+
when :user_list
|
|
60
|
+
# User list update
|
|
61
|
+
{
|
|
62
|
+
action: :user_list,
|
|
63
|
+
users: packet.data[:users],
|
|
64
|
+
timestamp: packet.timestamp
|
|
65
|
+
}
|
|
66
|
+
else
|
|
67
|
+
# Unknown message type
|
|
68
|
+
{
|
|
69
|
+
action: :error,
|
|
70
|
+
message: "Unknown message type: #{packet.type}",
|
|
71
|
+
timestamp: packet.timestamp
|
|
72
|
+
}
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
Fractor::WorkResult.new(result: result, work: work)
|
|
76
|
+
rescue StandardError => e
|
|
77
|
+
Fractor::WorkResult.new(
|
|
78
|
+
error: "Error processing message: #{e.message}",
|
|
79
|
+
work: work
|
|
80
|
+
)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'socket'
|
|
5
|
+
require 'json'
|
|
6
|
+
require 'time'
|
|
7
|
+
require_relative 'chat_common'
|
|
8
|
+
|
|
9
|
+
# Refactored Chat Server using new Fractor primitives
|
|
10
|
+
puts 'Starting Fractor-based chat server (refactored)...'
|
|
11
|
+
|
|
12
|
+
# Parse command line args
|
|
13
|
+
port = ARGV[0]&.to_i || 3000
|
|
14
|
+
log_file_path = ARGV[1] || 'logs/server_messages.log'
|
|
15
|
+
|
|
16
|
+
# Thread-safe hash for client connections
|
|
17
|
+
clients = {}
|
|
18
|
+
clients_mutex = Mutex.new
|
|
19
|
+
|
|
20
|
+
# Create server socket
|
|
21
|
+
server = TCPServer.new('0.0.0.0', port)
|
|
22
|
+
|
|
23
|
+
# Set up work queue and Fractor server
|
|
24
|
+
work_queue = Fractor::WorkQueue.new
|
|
25
|
+
|
|
26
|
+
fractor_server = Fractor::ContinuousServer.new(
|
|
27
|
+
worker_pools: [
|
|
28
|
+
{ worker_class: ContinuousChatFractor::ChatWorker, num_workers: 2 }
|
|
29
|
+
],
|
|
30
|
+
work_queue: work_queue,
|
|
31
|
+
log_file: log_file_path
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# Handle results from Fractor workers
|
|
35
|
+
fractor_server.on_result do |result|
|
|
36
|
+
action_data = result.result
|
|
37
|
+
case action_data[:action]
|
|
38
|
+
when :broadcast
|
|
39
|
+
puts "Broadcasting: #{action_data[:content]}"
|
|
40
|
+
when :direct_message
|
|
41
|
+
puts "DM from #{action_data[:from]} to #{action_data[:to]}"
|
|
42
|
+
when :server_message
|
|
43
|
+
puts "Server: #{action_data[:message]}"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
fractor_server.on_error do |error|
|
|
48
|
+
puts "Error: #{error.error}"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Start Fractor server in background
|
|
52
|
+
Thread.new { fractor_server.run }
|
|
53
|
+
sleep(0.2) # Give it time to start
|
|
54
|
+
|
|
55
|
+
puts "Server started on port #{port}"
|
|
56
|
+
puts "Server ready to accept connections"
|
|
57
|
+
|
|
58
|
+
# Handle new client connections
|
|
59
|
+
begin
|
|
60
|
+
sockets = [server]
|
|
61
|
+
|
|
62
|
+
loop do
|
|
63
|
+
readable, = IO.select(sockets, [], [], 0.1)
|
|
64
|
+
next unless readable
|
|
65
|
+
|
|
66
|
+
readable.each do |socket|
|
|
67
|
+
if socket == server
|
|
68
|
+
# New client connection
|
|
69
|
+
client = server.accept
|
|
70
|
+
sockets << client
|
|
71
|
+
|
|
72
|
+
# Read join message
|
|
73
|
+
line = client.gets&.chomp
|
|
74
|
+
if line
|
|
75
|
+
message = JSON.parse(line)
|
|
76
|
+
if message['type'] == 'join' && message['data']['username']
|
|
77
|
+
username = message['data']['username']
|
|
78
|
+
clients_mutex.synchronize { clients[username] = client }
|
|
79
|
+
|
|
80
|
+
packet = ContinuousChat::MessagePacket.new(
|
|
81
|
+
:server_message,
|
|
82
|
+
{ message: "#{username} joined!" }
|
|
83
|
+
)
|
|
84
|
+
work_queue << ContinuousChatFractor::ChatMessage.new(packet)
|
|
85
|
+
|
|
86
|
+
client.puts(JSON.generate({
|
|
87
|
+
type: 'server_message',
|
|
88
|
+
data: { message: "Welcome #{username}!" },
|
|
89
|
+
timestamp: Time.now.to_i
|
|
90
|
+
}))
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
else
|
|
94
|
+
# Existing client sent data
|
|
95
|
+
line = socket.gets&.chomp
|
|
96
|
+
if line.nil?
|
|
97
|
+
# Client disconnected
|
|
98
|
+
username = clients_mutex.synchronize { clients.key(socket) }
|
|
99
|
+
if username
|
|
100
|
+
clients_mutex.synchronize { clients.delete(username) }
|
|
101
|
+
packet = ContinuousChat::MessagePacket.new(
|
|
102
|
+
:server_message,
|
|
103
|
+
{ message: "#{username} left" }
|
|
104
|
+
)
|
|
105
|
+
work_queue << ContinuousChatFractor::ChatMessage.new(packet)
|
|
106
|
+
end
|
|
107
|
+
sockets.delete(socket)
|
|
108
|
+
socket.close rescue nil
|
|
109
|
+
else
|
|
110
|
+
# Process message
|
|
111
|
+
message = JSON.parse(line)
|
|
112
|
+
username = clients_mutex.synchronize { clients.key(socket) }
|
|
113
|
+
|
|
114
|
+
if message['type'] == 'message'
|
|
115
|
+
content = message['data']['content']
|
|
116
|
+
recipient = message['data']['recipient'] || 'all'
|
|
117
|
+
|
|
118
|
+
if recipient == 'all'
|
|
119
|
+
packet = ContinuousChat::MessagePacket.new(
|
|
120
|
+
:broadcast,
|
|
121
|
+
{ from: username, content: content }
|
|
122
|
+
)
|
|
123
|
+
work_queue << ContinuousChatFractor::ChatMessage.new(packet)
|
|
124
|
+
|
|
125
|
+
# Broadcast to clients
|
|
126
|
+
broadcast_msg = {
|
|
127
|
+
type: 'broadcast',
|
|
128
|
+
data: { from: username, content: content },
|
|
129
|
+
timestamp: Time.now.to_i
|
|
130
|
+
}
|
|
131
|
+
clients_mutex.synchronize do
|
|
132
|
+
clients.each_value { |c| c.puts(JSON.generate(broadcast_msg)) rescue nil }
|
|
133
|
+
end
|
|
134
|
+
else
|
|
135
|
+
packet = ContinuousChat::MessagePacket.new(
|
|
136
|
+
:direct_message,
|
|
137
|
+
{ from: username, to: recipient, content: content }
|
|
138
|
+
)
|
|
139
|
+
work_queue << ContinuousChatFractor::ChatMessage.new(packet)
|
|
140
|
+
|
|
141
|
+
# Send direct message
|
|
142
|
+
dm_msg = {
|
|
143
|
+
type: 'direct_message',
|
|
144
|
+
data: { from: username, content: content },
|
|
145
|
+
timestamp: Time.now.to_i
|
|
146
|
+
}
|
|
147
|
+
clients_mutex.synchronize do
|
|
148
|
+
recipient_socket = clients[recipient]
|
|
149
|
+
if recipient_socket
|
|
150
|
+
recipient_socket.puts(JSON.generate(dm_msg)) rescue nil
|
|
151
|
+
socket.puts(JSON.generate(dm_msg)) if username != recipient
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
rescue Interrupt
|
|
161
|
+
puts "\nServer interrupted, shutting down..."
|
|
162
|
+
ensure
|
|
163
|
+
fractor_server.stop
|
|
164
|
+
clients_mutex.synchronize { clients.each_value { |c| c.close rescue nil } }
|
|
165
|
+
server&.close
|
|
166
|
+
puts 'Server stopped'
|
|
167
|
+
end
|