smart_message 0.0.13 → 0.0.16

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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/CHANGELOG.md +120 -0
  4. data/Gemfile.lock +3 -3
  5. data/README.md +71 -25
  6. data/docs/index.md +2 -0
  7. data/docs/reference/transports.md +46 -21
  8. data/docs/transports/memory-transport.md +2 -1
  9. data/docs/transports/multi-transport.md +484 -0
  10. data/examples/file/00_run_all_file_demos.rb +260 -0
  11. data/examples/file/01_basic_file_transport_demo.rb +237 -0
  12. data/examples/file/02_fifo_transport_demo.rb +289 -0
  13. data/examples/file/03_file_watching_demo.rb +332 -0
  14. data/examples/file/04_multi_transport_file_demo.rb +432 -0
  15. data/examples/file/README.md +257 -0
  16. data/examples/memory/00_run_all_demos.rb +317 -0
  17. data/examples/memory/01_message_deduplication_demo.rb +18 -30
  18. data/examples/memory/02_dead_letter_queue_demo.rb +9 -9
  19. data/examples/memory/03_point_to_point_orders.rb +3 -3
  20. data/examples/memory/04_publish_subscribe_events.rb +15 -15
  21. data/examples/memory/05_many_to_many_chat.rb +19 -19
  22. data/examples/memory/06_stdout_publish_only.rb +145 -0
  23. data/examples/memory/07_proc_handlers_demo.rb +13 -13
  24. data/examples/memory/08_custom_logger_demo.rb +136 -136
  25. data/examples/memory/09_error_handling_demo.rb +7 -7
  26. data/examples/memory/10_entity_addressing_basic.rb +25 -25
  27. data/examples/memory/11_entity_addressing_with_filtering.rb +32 -32
  28. data/examples/memory/12_regex_filtering_microservices.rb +10 -10
  29. data/examples/memory/14_global_configuration_demo.rb +12 -12
  30. data/examples/memory/README.md +34 -17
  31. data/examples/memory/log/demo_app.log.1 +100 -0
  32. data/examples/memory/log/demo_app.log.2 +100 -0
  33. data/examples/multi_transport_example.rb +114 -0
  34. data/examples/redis/01_smart_home_iot_demo.rb +20 -20
  35. data/examples/utilities/box_it.rb +12 -0
  36. data/examples/utilities/doing.rb +19 -0
  37. data/examples/utilities/temp.md +28 -0
  38. data/lib/smart_message/base.rb +5 -7
  39. data/lib/smart_message/errors.rb +3 -0
  40. data/lib/smart_message/header.rb +1 -1
  41. data/lib/smart_message/logger/default.rb +1 -1
  42. data/lib/smart_message/messaging.rb +36 -6
  43. data/lib/smart_message/plugins.rb +46 -4
  44. data/lib/smart_message/serializer/base.rb +1 -1
  45. data/lib/smart_message/serializer.rb +3 -2
  46. data/lib/smart_message/subscription.rb +18 -20
  47. data/lib/smart_message/transport/async_publish_queue.rb +284 -0
  48. data/lib/smart_message/transport/fifo_operations.rb +264 -0
  49. data/lib/smart_message/transport/file_operations.rb +200 -0
  50. data/lib/smart_message/transport/file_transport.rb +149 -0
  51. data/lib/smart_message/transport/file_watching.rb +72 -0
  52. data/lib/smart_message/transport/partitioned_files.rb +46 -0
  53. data/lib/smart_message/transport/stdout_transport.rb +50 -36
  54. data/lib/smart_message/transport/stdout_transport.rb.backup +88 -0
  55. data/lib/smart_message/version.rb +1 -1
  56. metadata +24 -10
  57. data/ideas/README.md +0 -41
  58. data/ideas/agents.md +0 -1001
  59. data/ideas/database_transport.md +0 -980
  60. data/ideas/improvement.md +0 -359
  61. data/ideas/meshage.md +0 -1788
  62. data/ideas/message_discovery.md +0 -178
  63. data/ideas/message_schema.md +0 -1381
  64. data/lib/smart_message/wrapper.rb.bak +0 -132
  65. /data/examples/memory/{06_pretty_print_demo.rb → 16_pretty_print_demo.rb} +0 -0
@@ -0,0 +1,264 @@
1
+ # lib/smart_message/transport/fifo_operations.rb
2
+ # encoding: utf-8
3
+ # frozen_string_literal: true
4
+
5
+ require 'rbconfig'
6
+
7
+ module SmartMessage
8
+ module Transport
9
+ # Module for FIFO operations, with cross-platform considerations.
10
+ module FifoOperations
11
+
12
+ private
13
+
14
+ def logger
15
+ # Ensure we have a proper logger, not just an IO object
16
+ if @logger && @logger.respond_to?(:error) && @logger.respond_to?(:info) && @logger.respond_to?(:warn)
17
+ return @logger
18
+ end
19
+ @logger = SmartMessage::Logger.default
20
+ end
21
+
22
+ public
23
+ def configure_fifo
24
+ unless platform_supports_fifo?
25
+ begin
26
+ logger.warn { "[FileTransport] FIFO not supported, falling back to regular file" }
27
+ rescue
28
+ # Fallback if logger is not available
29
+ end
30
+ @options[:file_type] = :regular
31
+ return configure_file_output
32
+ end
33
+
34
+ create_named_pipe if @options[:create_fifo]
35
+ end
36
+
37
+ def create_named_pipe
38
+ case RbConfig::CONFIG['host_os']
39
+ when /mswin|mingw|cygwin/
40
+ create_windows_named_pipe
41
+ else
42
+ File.mkfifo(@options[:file_path], @options[:fifo_permissions] || 0644)
43
+ begin
44
+ logger.info { "[FileTransport] Created FIFO: #{@options[:file_path]}" }
45
+ rescue
46
+ # Fallback if logger is not available
47
+ end
48
+ end
49
+ rescue NotImplementedError
50
+ begin
51
+ logger.error { "[FileTransport] Named pipes not supported on this platform" }
52
+ rescue
53
+ # Fallback if logger is not available
54
+ end
55
+ raise
56
+ rescue => e
57
+ begin
58
+ logger.error { "[FileTransport] Failed to create FIFO: #{e.message}" }
59
+ rescue
60
+ # Fallback if logger is not available
61
+ end
62
+ raise
63
+ end
64
+
65
+ def create_windows_named_pipe
66
+ require 'win32/pipe'
67
+ pipe_name = "\\\\.\\pipe\\#{File.basename(@options[:file_path])}"
68
+ @windows_pipe_server = Win32::Pipe::Server.new(pipe_name)
69
+ begin
70
+ logger.info { "[FileTransport] Created Windows named pipe: #{pipe_name}" }
71
+ rescue
72
+ # Fallback if logger is not available
73
+ end
74
+ rescue LoadError
75
+ raise "Windows named pipes require win32-pipe gem: gem install win32-pipe"
76
+ rescue => e
77
+ begin
78
+ logger.error { "[FileTransport] Failed to create Windows named pipe: #{e.message}" }
79
+ rescue
80
+ # Fallback if logger is not available
81
+ end
82
+ raise
83
+ end
84
+
85
+ def platform_supports_fifo?
86
+ case RbConfig::CONFIG['host_os']
87
+ when /mswin|mingw|cygwin/
88
+ defined?(Win32::Pipe)
89
+ else
90
+ true
91
+ end
92
+ end
93
+
94
+ def write_to_fifo(serialized_message)
95
+ handle = open_fifo_for_writing
96
+ unless handle
97
+ handle_fifo_write_failure(serialized_message)
98
+ return false
99
+ end
100
+
101
+ content = prepare_file_content(serialized_message)
102
+ handle.write(content)
103
+ handle.flush
104
+ true
105
+ rescue Errno::EPIPE
106
+ begin
107
+ logger.warn { "[FileTransport] FIFO reader disconnected" }
108
+ rescue
109
+ # Fallback if logger is not available
110
+ end
111
+ handle_fifo_write_failure(serialized_message)
112
+ false
113
+ rescue => e
114
+ begin
115
+ logger.error { "[FileTransport] FIFO write error: #{e.message}" }
116
+ rescue
117
+ # Fallback if logger is not available
118
+ end
119
+ handle_fifo_write_failure(serialized_message)
120
+ false
121
+ ensure
122
+ handle&.close
123
+ end
124
+
125
+ def open_fifo_for_writing
126
+ mode = @options[:fifo_mode] == :non_blocking ? File::WRONLY | File::NONBLOCK : 'w'
127
+ File.open(@options[:file_path], mode)
128
+ rescue Errno::ENXIO, Errno::ENOENT
129
+ nil
130
+ end
131
+
132
+ def handle_fifo_write_failure(serialized_message)
133
+ if @options[:fallback_transport]
134
+ begin
135
+ @options[:fallback_transport].do_publish(@current_message_class, serialized_message)
136
+ begin
137
+ logger.info { "[FileTransport] Message sent to fallback transport" }
138
+ rescue
139
+ # Fallback if logger is not available
140
+ end
141
+ rescue => e
142
+ begin
143
+ logger.error { "[FileTransport] Fallback transport failed: #{e.message}" }
144
+ rescue
145
+ # Fallback if logger is not available
146
+ end
147
+ end
148
+ end
149
+ end
150
+
151
+ def start_fifo_reader(message_class, process_method, filter_options)
152
+ case @options[:subscription_mode]
153
+ when :fifo_blocking
154
+ start_blocking_fifo_reader(message_class, process_method)
155
+ when :fifo_select
156
+ start_select_fifo_reader(message_class, process_method)
157
+ when :fifo_polling
158
+ start_polling_fifo_reader(message_class, process_method)
159
+ else
160
+ begin
161
+ logger.warn { "[FileTransport] Invalid FIFO subscription mode: #{@options[:subscription_mode]}" }
162
+ rescue
163
+ # Fallback if logger is not available
164
+ end
165
+ end
166
+ end
167
+
168
+ def start_blocking_fifo_reader(message_class, process_method)
169
+ @fifo_reader_thread = Thread.new do
170
+ Thread.current.name = "FileTransport-FifoReader"
171
+ loop do
172
+ begin
173
+ File.open(@options[:file_path], 'r') do |fifo|
174
+ while line = fifo.gets
175
+ next if line.strip.empty?
176
+ receive(message_class, line.strip)
177
+ end
178
+ end
179
+ rescue => e
180
+ begin
181
+ logger.error { "[FileTransport] FIFO reader error: #{e.message}" }
182
+ rescue
183
+ # Fallback if logger is not available
184
+ end
185
+ sleep 1
186
+ end
187
+ end
188
+ end
189
+ end
190
+
191
+ def start_select_fifo_reader(message_class, process_method)
192
+ @fifo_select_thread = Thread.new do
193
+ Thread.current.name = "FileTransport-FifoSelect"
194
+ fifo = File.open(@options[:file_path], File::RDONLY | File::NONBLOCK)
195
+
196
+ loop do
197
+ ready = IO.select([fifo], nil, nil, 1.0)
198
+ if ready
199
+ begin
200
+ while line = fifo.gets
201
+ next if line.strip.empty?
202
+ receive(message_class, line.strip)
203
+ end
204
+ rescue IO::WaitReadable
205
+ next
206
+ rescue => e
207
+ begin
208
+ logger.error { "[FileTransport] FIFO select error: #{e.message}" }
209
+ rescue
210
+ # Fallback if logger is not available
211
+ end
212
+ end
213
+ end
214
+ end
215
+ rescue => e
216
+ begin
217
+ logger.error { "[FileTransport] FIFO select thread error: #{e.message}" }
218
+ rescue
219
+ # Fallback if logger is not available
220
+ end
221
+ ensure
222
+ fifo&.close
223
+ end
224
+ end
225
+
226
+ def start_polling_fifo_reader(message_class, process_method)
227
+ @fifo_reader_thread = Thread.new do
228
+ Thread.current.name = "FileTransport-FifoPoller"
229
+ loop do
230
+ begin
231
+ File.open(@options[:file_path], File::RDONLY | File::NONBLOCK) do |fifo|
232
+ while line = fifo.gets
233
+ next if line.strip.empty?
234
+ receive(message_class, line.strip)
235
+ end
236
+ end
237
+ rescue Errno::EAGAIN
238
+ sleep(@options[:poll_interval] || 1.0)
239
+ rescue => e
240
+ begin
241
+ logger.error { "[FileTransport] FIFO polling error: #{e.message}" }
242
+ rescue
243
+ # Fallback if logger is not available
244
+ end
245
+ sleep 1
246
+ end
247
+ end
248
+ end
249
+ end
250
+
251
+ def stop_fifo_operations
252
+ @fifo_reader_thread&.kill
253
+ @fifo_reader_thread&.join(2)
254
+ @fifo_select_thread&.kill
255
+ @fifo_select_thread&.join(2)
256
+ @fifo_handle&.close
257
+ end
258
+
259
+ def fifo_active?
260
+ @fifo_reader_thread&.alive? || @fifo_select_thread&.alive?
261
+ end
262
+ end
263
+ end
264
+ end
@@ -0,0 +1,200 @@
1
+ # lib/smart_message/transport/file_operations.rb
2
+ # encoding: utf-8
3
+ # frozen_string_literal: true
4
+
5
+ module SmartMessage
6
+ module Transport
7
+ # Module for shared file operations, including buffering, rotation, and basic I/O.
8
+ module FileOperations
9
+ def configure_file_output
10
+ ensure_directory_exists if @options[:create_directories]
11
+ @file_handle = open_file_handle
12
+ @write_buffer = []
13
+ @last_flush = Time.now
14
+ setup_rotation_timer if rotation_enabled?
15
+ @file_mutex = Mutex.new # For thread-safety
16
+ end
17
+
18
+ def write_to_file(serialized_message)
19
+ content = prepare_file_content(serialized_message)
20
+
21
+ @file_mutex.synchronize do
22
+ if buffered_mode?
23
+ buffer_write(content)
24
+ else
25
+ direct_write(content)
26
+ end
27
+
28
+ rotate_file_if_needed
29
+ end
30
+ end
31
+
32
+ def flush_buffer
33
+ return if @write_buffer.empty?
34
+
35
+ # Only synchronize if we're not already holding the lock
36
+ if @file_mutex.owned?
37
+ @file_handle.write(@write_buffer.join)
38
+ @file_handle.flush
39
+ @write_buffer.clear
40
+ @last_flush = Time.now
41
+ else
42
+ @file_mutex.synchronize do
43
+ @file_handle.write(@write_buffer.join)
44
+ @file_handle.flush
45
+ @write_buffer.clear
46
+ @last_flush = Time.now
47
+ end
48
+ end
49
+ end
50
+
51
+ def close_file_handle
52
+ flush_buffer if buffered_mode?
53
+ if @file_mutex
54
+ @file_mutex.synchronize do
55
+ @file_handle&.close
56
+ @file_handle = nil
57
+ end
58
+ else
59
+ @file_handle&.close
60
+ @file_handle = nil
61
+ end
62
+ end
63
+
64
+ private
65
+
66
+ def prepare_file_content(serialized_message)
67
+ case @options[:file_format]
68
+ when :lines
69
+ "#{serialized_message}\n"
70
+ when :raw
71
+ serialized_message
72
+ else
73
+ "#{serialized_message}\n" # default to lines
74
+ end
75
+ end
76
+
77
+ def open_file_handle
78
+ File.open(current_file_path, file_mode, encoding: @options[:encoding])
79
+ end
80
+
81
+ def ensure_directory_exists
82
+ return unless @options[:create_directories]
83
+
84
+ dir = File.dirname(current_file_path)
85
+ FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
86
+ end
87
+
88
+ def current_file_path
89
+ if rotation_enabled? && time_based_rotation?
90
+ timestamped_file_path
91
+ else
92
+ @options[:file_path]
93
+ end
94
+ end
95
+
96
+ def file_mode
97
+ @options[:file_mode] || 'a' # append by default
98
+ end
99
+
100
+ def buffered_mode?
101
+ @options[:buffer_size] && @options[:buffer_size] > 0
102
+ end
103
+
104
+ def buffer_write(content)
105
+ @write_buffer << content
106
+
107
+ if buffer_full? || flush_interval_exceeded?
108
+ flush_buffer
109
+ end
110
+ end
111
+
112
+ def direct_write(content)
113
+ @file_handle.write(content)
114
+ @file_handle.flush if @options[:auto_flush]
115
+ end
116
+
117
+ def buffer_full?
118
+ @write_buffer.join.bytesize >= @options[:buffer_size]
119
+ end
120
+
121
+ def flush_interval_exceeded?
122
+ @options[:flush_interval] &&
123
+ (Time.now - @last_flush) >= @options[:flush_interval]
124
+ end
125
+
126
+ def rotation_enabled?
127
+ @options[:rotate_size] || @options[:rotate_time]
128
+ end
129
+
130
+ def time_based_rotation?
131
+ @options[:rotate_time]
132
+ end
133
+
134
+ def should_rotate?
135
+ size_rotation_needed? || time_rotation_needed?
136
+ end
137
+
138
+ def size_rotation_needed?
139
+ @options[:rotate_size] &&
140
+ File.exist?(current_file_path) && File.size(current_file_path) >= @options[:rotate_size]
141
+ end
142
+
143
+ def time_rotation_needed?
144
+ return false unless @options[:rotate_time]
145
+
146
+ case @options[:rotate_time]
147
+ when :hourly
148
+ Time.now.min == 0 && Time.now.sec == 0
149
+ when :daily
150
+ Time.now.hour == 0 && Time.now.min == 0 && Time.now.sec == 0
151
+ else
152
+ false
153
+ end
154
+ end
155
+
156
+ def rotate_file_if_needed
157
+ return unless should_rotate?
158
+
159
+ close_current_file
160
+ archive_current_file
161
+ @file_handle = open_file_handle
162
+ end
163
+
164
+ def close_current_file
165
+ flush_buffer if buffered_mode?
166
+ @file_handle&.close
167
+ @file_handle = nil
168
+ end
169
+
170
+ def archive_current_file
171
+ return unless File.exist?(current_file_path)
172
+
173
+ timestamp = Time.now.strftime(@options[:timestamp_format] || '%Y%m%d_%H%M%S')
174
+ base = File.basename(@options[:file_path], '.*')
175
+ ext = File.extname(@options[:file_path])
176
+ dir = File.dirname(@options[:file_path])
177
+ archive_path = File.join(dir, "#{base}_#{timestamp}#{ext}")
178
+
179
+ FileUtils.mv(current_file_path, archive_path)
180
+
181
+ # Maintain rotation count
182
+ if @options[:rotate_count]
183
+ files = Dir.glob(File.join(dir, "#{base}_*#{ext}")).sort
184
+ while files.size > @options[:rotate_count]
185
+ File.delete(files.shift)
186
+ end
187
+ end
188
+ end
189
+
190
+ def timestamped_file_path
191
+ base = File.basename(@options[:file_path], '.*')
192
+ ext = File.extname(@options[:file_path])
193
+ dir = File.dirname(@options[:file_path])
194
+ timestamp = Time.now.strftime(@options[:timestamp_format] || '%Y%m%d_%H%M%S')
195
+
196
+ File.join(dir, "#{base}_#{timestamp}#{ext}")
197
+ end
198
+ end
199
+ end
200
+ end
@@ -0,0 +1,149 @@
1
+ # lib/smart_message/transport/file_transport.rb
2
+ # encoding: utf-8
3
+ # frozen_string_literal: true
4
+
5
+ require_relative 'file_operations'
6
+ require_relative 'file_watching'
7
+ require_relative 'partitioned_files'
8
+ require_relative 'async_publish_queue'
9
+ require_relative 'fifo_operations'
10
+
11
+ module SmartMessage
12
+ module Transport
13
+ class FileTransport < Base
14
+ include FileOperations
15
+ include FileWatching
16
+ include PartitionedFiles
17
+ include AsyncPublishQueue
18
+ include FifoOperations
19
+
20
+ # @param path [String, IO, Pathname] file path or IO-like object
21
+ # @param mode [String] file open mode ("a" for append, etc.)
22
+ # @param encoding [String, nil] file encoding
23
+ def initialize(options = {})
24
+ @current_message_class = nil
25
+ super(**options)
26
+ end
27
+
28
+ def default_options
29
+ {
30
+ file_path: 'messages.log',
31
+ file_mode: 'a',
32
+ encoding: nil,
33
+ file_format: :lines,
34
+ buffer_size: 0,
35
+ flush_interval: nil,
36
+ auto_flush: true,
37
+ rotate_size: nil,
38
+ rotate_time: nil,
39
+ rotate_count: 5,
40
+ timestamp_format: '%Y%m%d_%H%M%S',
41
+ create_directories: true,
42
+ async: false,
43
+ max_queue: nil,
44
+ drop_when_full: false,
45
+ queue_overflow_strategy: :block,
46
+ max_retries: 3,
47
+ max_retry_delay: 30,
48
+ worker_timeout: 5,
49
+ shutdown_timeout: 10,
50
+ queue_warning_threshold: 0.8,
51
+ enable_queue_monitoring: true,
52
+ drain_queue_on_shutdown: true,
53
+ send_dropped_to_dlq: false,
54
+ read_from_end: true,
55
+ poll_interval: 1.0,
56
+ file_type: :regular,
57
+ create_fifo: false,
58
+ fifo_mode: :blocking,
59
+ fifo_permissions: 0644,
60
+ fallback_transport: nil,
61
+ enable_subscriptions: false,
62
+ subscription_mode: :polling,
63
+ filename_selector: nil,
64
+ directory: nil,
65
+ subscription_file_path: nil
66
+ }
67
+ end
68
+
69
+ def configure
70
+ # Call parent configuration first
71
+ super if defined?(super)
72
+
73
+ # Then configure our file-specific features
74
+ if @options[:async]
75
+ configure_async_publishing
76
+ elsif @options[:file_type] == :fifo
77
+ configure_fifo
78
+ else
79
+ configure_file_output
80
+ end
81
+ end
82
+
83
+ def publish(payload)
84
+ do_publish(nil, payload)
85
+ end
86
+
87
+ def do_publish(message_class, serialized_message)
88
+ @current_message_class = message_class
89
+ if @options[:async]
90
+ async_publish(message_class, serialized_message)
91
+ else
92
+ if @options[:filename_selector] || @options[:directory]
93
+ header = { message_class_name: message_class.to_s }
94
+ path = determine_file_path(serialized_message, header)
95
+ @file_handle = get_or_open_partition_handle(path)
96
+ write_to_file(serialized_message)
97
+ elsif @options[:file_type] == :fifo
98
+ write_to_fifo(serialized_message)
99
+ else
100
+ write_to_file(serialized_message)
101
+ end
102
+ end
103
+ end
104
+
105
+ def subscribe(message_class, process_method, filter_options = {})
106
+ unless @options[:enable_subscriptions]
107
+ logger.warn { "[FileTransport] Subscriptions disabled - set enable_subscriptions: true" }
108
+ return
109
+ end
110
+
111
+ if @options[:file_type] == :fifo
112
+ start_fifo_reader(message_class, process_method, filter_options)
113
+ else
114
+ start_file_polling(message_class, process_method, filter_options)
115
+ end
116
+
117
+ super(message_class, process_method, filter_options)
118
+ end
119
+
120
+ def connected?
121
+ case @options[:file_type]
122
+ when :fifo
123
+ subscription_active? || fifo_active?
124
+ else
125
+ (@file_handle && !@file_handle.closed?) || subscription_active?
126
+ end
127
+ end
128
+
129
+ def disconnect
130
+ stop_file_subscriptions
131
+ stop_fifo_operations if @options[:file_type] == :fifo
132
+ stop_async_publishing if @options[:async]
133
+ close_partition_handles if @options[:filename_selector] || @options[:directory]
134
+ close_file_handle
135
+ end
136
+
137
+ private
138
+
139
+ def subscription_active?
140
+ @polling_thread&.alive? || fifo_active?
141
+ end
142
+
143
+ def stop_file_subscriptions
144
+ @polling_thread&.kill
145
+ @polling_thread&.join(5)
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,72 @@
1
+ # lib/smart_message/transport/file_watching.rb
2
+ # encoding: utf-8
3
+ # frozen_string_literal: true
4
+
5
+ module SmartMessage
6
+ module Transport
7
+ # Module for file watching and subscription support (reading/tailing).
8
+ module FileWatching
9
+
10
+ private
11
+
12
+ def logger
13
+ # Ensure we have a proper logger, not just an IO object
14
+ if @logger && @logger.respond_to?(:error) && @logger.respond_to?(:info) && @logger.respond_to?(:warn)
15
+ return @logger
16
+ end
17
+ @logger = SmartMessage::Logger.default
18
+ end
19
+
20
+ public
21
+ def start_file_polling(message_class, process_method, filter_options)
22
+ poll_interval = filter_options[:poll_interval] || @options[:poll_interval] || 1.0
23
+ file_path = subscription_file_path(message_class, filter_options)
24
+
25
+ @polling_thread = Thread.new do
26
+ Thread.current.name = "FileTransport-Poller"
27
+ last_position = @options[:read_from_end] ? (File.exist?(file_path) ? File.size(file_path) : 0) : 0
28
+
29
+ loop do
30
+ sleep poll_interval
31
+ next unless File.exist?(file_path)
32
+
33
+ current_size = File.size(file_path)
34
+ if current_size > last_position
35
+ process_new_file_content(file_path, last_position, current_size, message_class)
36
+ last_position = current_size
37
+ end
38
+ end
39
+ rescue => e
40
+ logger.error { "[FileTransport] Polling thread error: #{e.message}" }
41
+ end
42
+ end
43
+
44
+ def process_new_file_content(file_path, start_pos, end_pos, message_class)
45
+ File.open(file_path, 'r', encoding: @options[:encoding]) do |file|
46
+ file.seek(start_pos)
47
+ content = file.read(end_pos - start_pos)
48
+
49
+ content.each_line do |line|
50
+ next if line.strip.empty?
51
+
52
+ begin
53
+ receive(message_class, line.strip)
54
+ rescue => e
55
+ begin
56
+ logger.error { "[FileTransport] Error processing message: #{e.message}" }
57
+ rescue
58
+ # Fallback if logger is not available
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ def subscription_file_path(message_class, filter_options)
66
+ filter_options[:file_path] ||
67
+ @options[:subscription_file_path] ||
68
+ File.join(File.dirname(@options[:file_path]), "#{message_class.to_s.downcase}.jsonl")
69
+ end
70
+ end
71
+ end
72
+ end