event_system 0.1.0
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/.rspec +3 -0
- data/README.md +362 -0
- data/Rakefile +4 -0
- data/docs/event_visualizations/example_timeline.html +142 -0
- data/docs/example_logs/events_20251027_151744.jsonl +3 -0
- data/docs/example_logs/events_20251027_151802.jsonl +3 -0
- data/docs/examples/basic_usage.rb +64 -0
- data/exe/event_system +139 -0
- data/lib/event_system/configuration.rb +125 -0
- data/lib/event_system/event.rb +139 -0
- data/lib/event_system/event_manager.rb +191 -0
- data/lib/event_system/event_subscriber.rb +29 -0
- data/lib/event_system/storage/base.rb +64 -0
- data/lib/event_system/storage/file_store.rb +187 -0
- data/lib/event_system/storage/memory_store.rb +134 -0
- data/lib/event_system/version.rb +5 -0
- data/lib/event_system/visualization/timeline_generator.rb +237 -0
- data/lib/event_system.rb +37 -0
- data/spec/event_system/configuration_spec.rb +197 -0
- data/spec/event_system/event_manager_spec.rb +341 -0
- data/spec/event_system/event_spec.rb +193 -0
- data/spec/event_system/event_subscriber_spec.rb +295 -0
- data/spec/event_system/storage/file_store_spec.rb +341 -0
- data/spec/event_system/storage/memory_store_spec.rb +248 -0
- data/spec/event_system/visualization/timeline_generator_spec.rb +252 -0
- data/spec/event_system_spec.rb +57 -0
- data/spec/integration/readme_examples_spec.rb +447 -0
- data/spec/spec_helper.rb +80 -0
- metadata +171 -0
data/exe/event_system
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative '../lib/event_system'
|
|
5
|
+
require 'optparse'
|
|
6
|
+
|
|
7
|
+
class EventSystemCLI
|
|
8
|
+
def initialize
|
|
9
|
+
@options = {}
|
|
10
|
+
@parser = OptionParser.new do |opts|
|
|
11
|
+
opts.banner = "Usage: event_system [options] command"
|
|
12
|
+
|
|
13
|
+
opts.on("-s", "--storage TYPE", "Storage type (memory, file)") do |type|
|
|
14
|
+
@options[:storage_type] = type.to_sym
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
opts.on("-d", "--directory DIR", "Directory for file storage") do |dir|
|
|
18
|
+
@options[:storage_options] ||= {}
|
|
19
|
+
@options[:storage_options][:directory] = dir
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
opts.on("-v", "--verbose", "Verbose output") do
|
|
23
|
+
@options[:verbose] = true
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
opts.on("-h", "--help", "Show this help message") do
|
|
27
|
+
puts opts
|
|
28
|
+
exit
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def run(args)
|
|
34
|
+
@parser.parse!(args)
|
|
35
|
+
|
|
36
|
+
command = args.shift
|
|
37
|
+
case command
|
|
38
|
+
when "visualize"
|
|
39
|
+
visualize_command(args)
|
|
40
|
+
when "stats"
|
|
41
|
+
stats_command(args)
|
|
42
|
+
when "list"
|
|
43
|
+
list_command(args)
|
|
44
|
+
when "query"
|
|
45
|
+
query_command(args)
|
|
46
|
+
else
|
|
47
|
+
puts "Unknown command: #{command}"
|
|
48
|
+
puts "Available commands: visualize, stats, list, query"
|
|
49
|
+
exit 1
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def create_manager
|
|
56
|
+
config = {
|
|
57
|
+
storage_type: @options[:storage_type] || :file,
|
|
58
|
+
storage_options: @options[:storage_options] || { directory: "event_logs" }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
EventSystem.create_manager(config)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def visualize_command(args)
|
|
65
|
+
manager = create_manager
|
|
66
|
+
session_id = args[0]
|
|
67
|
+
output_file = args[1]
|
|
68
|
+
|
|
69
|
+
generator = EventSystem::Visualization::TimelineGenerator.new(manager.storage)
|
|
70
|
+
output_path = generator.generate_timeline(session_id, output_file)
|
|
71
|
+
|
|
72
|
+
if output_path
|
|
73
|
+
puts "Timeline generated: #{output_path}"
|
|
74
|
+
else
|
|
75
|
+
puts "No events found for visualization"
|
|
76
|
+
exit 1
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def stats_command(args)
|
|
81
|
+
manager = create_manager
|
|
82
|
+
stats = manager.stats
|
|
83
|
+
|
|
84
|
+
puts "Event System Statistics:"
|
|
85
|
+
puts "========================"
|
|
86
|
+
puts "Storage: #{stats[:storage][:type]}"
|
|
87
|
+
puts "Sessions: #{stats[:storage][:sessions]}"
|
|
88
|
+
puts "Subscribers: #{stats[:subscribers].values.sum}"
|
|
89
|
+
puts
|
|
90
|
+
puts "Event types:"
|
|
91
|
+
stats[:subscribers].each do |type, count|
|
|
92
|
+
puts " #{type}: #{count} subscribers"
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def list_command(args)
|
|
97
|
+
manager = create_manager
|
|
98
|
+
sessions = manager.storage.list_sessions
|
|
99
|
+
|
|
100
|
+
puts "Available Sessions:"
|
|
101
|
+
puts "==================="
|
|
102
|
+
if sessions.empty?
|
|
103
|
+
puts "No sessions found"
|
|
104
|
+
else
|
|
105
|
+
sessions.each do |session|
|
|
106
|
+
events = manager.storage.load_session(session)
|
|
107
|
+
puts "#{session} (#{events.length} events)"
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def query_command(args)
|
|
113
|
+
manager = create_manager
|
|
114
|
+
session_id = args[0]
|
|
115
|
+
event_type = args[1]
|
|
116
|
+
|
|
117
|
+
options = {}
|
|
118
|
+
options[:session_id] = session_id if session_id
|
|
119
|
+
options[:type] = event_type if event_type
|
|
120
|
+
|
|
121
|
+
events = manager.query_events(options)
|
|
122
|
+
|
|
123
|
+
puts "Query Results (#{events.length} events):"
|
|
124
|
+
puts "======================================"
|
|
125
|
+
events.each do |event|
|
|
126
|
+
puts "[#{event.timestamp}] #{event.type} from #{event.source_to_string}"
|
|
127
|
+
if @options[:verbose]
|
|
128
|
+
puts " Data: #{event.data}"
|
|
129
|
+
puts " ID: #{event.id}"
|
|
130
|
+
puts
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
if __FILE__ == $0
|
|
137
|
+
cli = EventSystemCLI.new
|
|
138
|
+
cli.run(ARGV)
|
|
139
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EventSystem
|
|
4
|
+
# Configuration class for the EventSystem
|
|
5
|
+
# Provides a centralized way to configure storage, logging, and other options
|
|
6
|
+
class Configuration
|
|
7
|
+
attr_accessor :storage_type, :storage_options, :logger, :session_id, :auto_flush
|
|
8
|
+
|
|
9
|
+
# Initialize a new configuration
|
|
10
|
+
def initialize
|
|
11
|
+
@storage_type = :memory
|
|
12
|
+
@storage_options = {}
|
|
13
|
+
@logger = nil
|
|
14
|
+
@session_id = nil
|
|
15
|
+
@auto_flush = true
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Get the storage class based on the configured type
|
|
19
|
+
# @return [Class] The storage class to use
|
|
20
|
+
def storage_class
|
|
21
|
+
case @storage_type
|
|
22
|
+
when :memory
|
|
23
|
+
Storage::MemoryStore
|
|
24
|
+
when :file
|
|
25
|
+
Storage::FileStore
|
|
26
|
+
else
|
|
27
|
+
raise ArgumentError, "Unknown storage type: #{@storage_type}. Available types: :memory, :file"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Create a new storage instance with the current configuration
|
|
32
|
+
# @return [EventSystem::Storage::Base] A new storage instance
|
|
33
|
+
def create_storage
|
|
34
|
+
storage_class.new(*storage_constructor_args)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Get the logger instance, creating a default one if none is set
|
|
38
|
+
# @return [Logger] The logger instance
|
|
39
|
+
def logger
|
|
40
|
+
@logger ||= create_default_logger
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Set the logger
|
|
44
|
+
# @param logger [Logger] The logger to use
|
|
45
|
+
def logger=(logger)
|
|
46
|
+
@logger = logger
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Get the current session ID, creating one if none is set
|
|
50
|
+
# @return [String] The current session ID
|
|
51
|
+
def session_id
|
|
52
|
+
@session_id ||= Time.now.strftime("%Y%m%d_%H%M%S")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Set the session ID
|
|
56
|
+
# @param session_id [String] The session ID to use
|
|
57
|
+
def session_id=(session_id)
|
|
58
|
+
@session_id = session_id
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Reset the configuration to defaults
|
|
62
|
+
# @return [void]
|
|
63
|
+
def reset!
|
|
64
|
+
@storage_type = :memory
|
|
65
|
+
@storage_options = {}
|
|
66
|
+
@logger = nil
|
|
67
|
+
@session_id = nil
|
|
68
|
+
@auto_flush = true
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Validate the current configuration
|
|
72
|
+
# @return [Boolean] True if configuration is valid
|
|
73
|
+
def valid?
|
|
74
|
+
return false unless [:memory, :file].include?(@storage_type)
|
|
75
|
+
return false if @storage_options.nil?
|
|
76
|
+
|
|
77
|
+
case @storage_type
|
|
78
|
+
when :file
|
|
79
|
+
directory = @storage_options[:directory] || @storage_options['directory']
|
|
80
|
+
return false if directory.nil? || directory.empty?
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
true
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Get configuration as a hash
|
|
87
|
+
# @return [Hash] Configuration as a hash
|
|
88
|
+
def to_h
|
|
89
|
+
{
|
|
90
|
+
storage_type: @storage_type,
|
|
91
|
+
storage_options: @storage_options,
|
|
92
|
+
session_id: @session_id,
|
|
93
|
+
auto_flush: @auto_flush
|
|
94
|
+
}
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
# Get arguments for storage constructor
|
|
100
|
+
# @return [Array] Arguments for the storage constructor
|
|
101
|
+
def storage_constructor_args
|
|
102
|
+
case @storage_type
|
|
103
|
+
when :memory
|
|
104
|
+
[@session_id]
|
|
105
|
+
when :file
|
|
106
|
+
directory = @storage_options[:directory] || @storage_options['directory'] || "event_logs"
|
|
107
|
+
[directory, @session_id]
|
|
108
|
+
else
|
|
109
|
+
[]
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Create a default logger
|
|
114
|
+
# @return [Logger] A default logger instance
|
|
115
|
+
def create_default_logger
|
|
116
|
+
require 'logger'
|
|
117
|
+
logger = Logger.new($stdout)
|
|
118
|
+
logger.level = Logger::INFO
|
|
119
|
+
logger.formatter = proc do |severity, datetime, progname, msg|
|
|
120
|
+
"[#{datetime.strftime('%Y-%m-%d %H:%M:%S')}] #{severity}: #{msg}\n"
|
|
121
|
+
end
|
|
122
|
+
logger
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'time'
|
|
5
|
+
require 'securerandom'
|
|
6
|
+
|
|
7
|
+
module EventSystem
|
|
8
|
+
# Base class for all events in the system
|
|
9
|
+
# Provides unique ID generation, timestamp handling, and serialization
|
|
10
|
+
class Event
|
|
11
|
+
attr_reader :id, :timestamp, :source, :type, :data
|
|
12
|
+
|
|
13
|
+
# Initialize a new event
|
|
14
|
+
# @param type [String, Symbol] The event type identifier
|
|
15
|
+
# @param source [Object] The source/originator of the event
|
|
16
|
+
# @param data [Hash] Additional event data
|
|
17
|
+
# @param id [String, nil] Optional event ID, generated if not provided
|
|
18
|
+
# @param timestamp [Time, String, nil] Optional timestamp, current time if not provided
|
|
19
|
+
def initialize(type, source = nil, data = {}, id = nil, timestamp = nil)
|
|
20
|
+
@id = id || SecureRandom.uuid
|
|
21
|
+
@type = type.to_s
|
|
22
|
+
@source = source
|
|
23
|
+
@data = data
|
|
24
|
+
@timestamp = parse_timestamp(timestamp)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# String representation of the event
|
|
28
|
+
# @return [String] Human-readable event string
|
|
29
|
+
def to_s
|
|
30
|
+
"[#{@timestamp}] #{@type}: #{@data.inspect}"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Hash representation of the event
|
|
34
|
+
# @return [Hash] Event data as a hash
|
|
35
|
+
def to_h
|
|
36
|
+
{
|
|
37
|
+
id: @id,
|
|
38
|
+
type: @type,
|
|
39
|
+
source: source_to_string,
|
|
40
|
+
timestamp: @timestamp.iso8601(3),
|
|
41
|
+
data: safe_serialize(@data)
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# JSON representation of the event
|
|
46
|
+
# @return [String] Event data as a JSON string
|
|
47
|
+
def to_json(*_args)
|
|
48
|
+
to_h.to_json
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Create an event from its JSON representation
|
|
52
|
+
# @param json [String] JSON representation of an event
|
|
53
|
+
# @return [Event] Reconstructed event
|
|
54
|
+
def self.from_json(json)
|
|
55
|
+
data = JSON.parse(json, symbolize_names: true)
|
|
56
|
+
|
|
57
|
+
new(
|
|
58
|
+
data[:type],
|
|
59
|
+
data[:source],
|
|
60
|
+
data[:data] || {},
|
|
61
|
+
data[:id],
|
|
62
|
+
data[:timestamp]
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Check if two events are equal (by ID)
|
|
67
|
+
# @param other [Event] The other event to compare
|
|
68
|
+
# @return [Boolean] True if events have the same ID
|
|
69
|
+
def ==(other)
|
|
70
|
+
other.is_a?(Event) && @id == other.id
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Hash code for the event (based on ID)
|
|
74
|
+
# @return [Integer] Hash code
|
|
75
|
+
def hash
|
|
76
|
+
@id.hash
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Check if the event is of a specific type
|
|
80
|
+
# @param event_type [String, Symbol] The event type to check
|
|
81
|
+
# @return [Boolean] True if the event matches the type
|
|
82
|
+
def type?(event_type)
|
|
83
|
+
@type == event_type.to_s
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Convert source to string representation
|
|
87
|
+
# @return [String] String representation of the source
|
|
88
|
+
def source_to_string
|
|
89
|
+
case @source
|
|
90
|
+
when nil
|
|
91
|
+
'unknown'
|
|
92
|
+
when String
|
|
93
|
+
@source
|
|
94
|
+
else
|
|
95
|
+
@source.to_s
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
# Parse timestamp from various formats
|
|
102
|
+
# @param timestamp [Time, String, nil] The timestamp to parse
|
|
103
|
+
# @return [Time] Parsed timestamp
|
|
104
|
+
def parse_timestamp(timestamp)
|
|
105
|
+
case timestamp
|
|
106
|
+
when Time
|
|
107
|
+
timestamp.utc
|
|
108
|
+
when String
|
|
109
|
+
Time.parse(timestamp).utc
|
|
110
|
+
when nil
|
|
111
|
+
Time.now.utc
|
|
112
|
+
else
|
|
113
|
+
raise ArgumentError, "Invalid timestamp type: #{timestamp.class}"
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Safely serialize data by handling non-serializable objects
|
|
118
|
+
# @param value [Object] The value to serialize
|
|
119
|
+
# @return [Object] A serializable version of the value
|
|
120
|
+
def safe_serialize(value)
|
|
121
|
+
case value
|
|
122
|
+
when Hash
|
|
123
|
+
value.each_with_object({}) do |(k, v), h|
|
|
124
|
+
h[k] = safe_serialize(v)
|
|
125
|
+
end
|
|
126
|
+
when Array
|
|
127
|
+
value.map { |v| safe_serialize(v) }
|
|
128
|
+
when Numeric, String, true, false, nil
|
|
129
|
+
value
|
|
130
|
+
else
|
|
131
|
+
# For complex objects, convert to string representation
|
|
132
|
+
value.to_s
|
|
133
|
+
end
|
|
134
|
+
rescue
|
|
135
|
+
# If any error occurs during serialization, return a safe fallback
|
|
136
|
+
"#<#{value.class} - non-serializable>"
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'event'
|
|
4
|
+
require_relative 'event_subscriber'
|
|
5
|
+
require_relative 'configuration'
|
|
6
|
+
require_relative 'storage/base'
|
|
7
|
+
require_relative 'storage/memory_store'
|
|
8
|
+
require_relative 'storage/file_store'
|
|
9
|
+
|
|
10
|
+
module EventSystem
|
|
11
|
+
# Central event management class that handles event publication and subscription
|
|
12
|
+
# Provides a complete event-driven architecture with pluggable storage backends
|
|
13
|
+
class EventManager
|
|
14
|
+
attr_reader :config, :storage
|
|
15
|
+
|
|
16
|
+
# Initialize a new event manager
|
|
17
|
+
# @param config [EventSystem::Configuration, Hash, nil] Configuration object, hash, or nil for defaults
|
|
18
|
+
def initialize(config = nil)
|
|
19
|
+
@config = case config
|
|
20
|
+
when Configuration
|
|
21
|
+
config
|
|
22
|
+
when Hash
|
|
23
|
+
Configuration.new.tap do |c|
|
|
24
|
+
c.storage_type = config[:storage_type] || :memory
|
|
25
|
+
c.storage_options = config[:storage_options] || {}
|
|
26
|
+
c.logger = config[:logger]
|
|
27
|
+
c.session_id = config[:session_id]
|
|
28
|
+
c.auto_flush = config[:auto_flush] != false
|
|
29
|
+
end
|
|
30
|
+
else
|
|
31
|
+
Configuration.new
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
@subscribers = Hash.new { |h, k| h[k] = [] }
|
|
35
|
+
@storage = @config.create_storage
|
|
36
|
+
@logger = @config.logger
|
|
37
|
+
|
|
38
|
+
@logger.info("EventSystem initialized with #{@config.storage_type} storage")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Subscribe to events of a specific type
|
|
42
|
+
# @param event_type [String, Symbol] The event type to subscribe to
|
|
43
|
+
# @param subscriber [Object] The subscriber that will handle the events
|
|
44
|
+
# @return [void]
|
|
45
|
+
def subscribe(event_type, subscriber)
|
|
46
|
+
event_type = event_type.to_s
|
|
47
|
+
@subscribers[event_type] << subscriber
|
|
48
|
+
@logger.debug("Subscribed #{subscriber.class} to #{event_type}")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Unsubscribe from events of a specific type
|
|
52
|
+
# @param event_type [String, Symbol] The event type to unsubscribe from
|
|
53
|
+
# @param subscriber [Object] The subscriber to remove
|
|
54
|
+
# @return [void]
|
|
55
|
+
def unsubscribe(event_type, subscriber)
|
|
56
|
+
event_type = event_type.to_s
|
|
57
|
+
@subscribers[event_type].delete(subscriber)
|
|
58
|
+
@logger.debug("Unsubscribed #{subscriber.class} from #{event_type}")
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Subscribe to all event types
|
|
62
|
+
# @param subscriber [Object] The subscriber that will handle all events
|
|
63
|
+
# @return [void]
|
|
64
|
+
def subscribe_all(subscriber)
|
|
65
|
+
@subscribers[:all] << subscriber
|
|
66
|
+
@logger.debug("Subscribed #{subscriber.class} to all events")
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Unsubscribe from all event types
|
|
70
|
+
# @param subscriber [Object] The subscriber to remove
|
|
71
|
+
# @return [void]
|
|
72
|
+
def unsubscribe_all(subscriber)
|
|
73
|
+
@subscribers[:all].delete(subscriber)
|
|
74
|
+
@logger.debug("Unsubscribed #{subscriber.class} from all events")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Publish an event to all subscribers
|
|
78
|
+
# @param event [EventSystem::Event] The event to publish
|
|
79
|
+
# @return [void]
|
|
80
|
+
def publish(event)
|
|
81
|
+
@logger.debug("Publishing event: #{event}")
|
|
82
|
+
|
|
83
|
+
# Store the event if storage is configured
|
|
84
|
+
@storage&.store(event)
|
|
85
|
+
|
|
86
|
+
# Deliver to type-specific subscribers
|
|
87
|
+
deliver_to_subscribers(event.type, event)
|
|
88
|
+
|
|
89
|
+
# Deliver to universal subscribers
|
|
90
|
+
deliver_to_subscribers(:all, event)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Create and publish an event in one step
|
|
94
|
+
# @param type [String, Symbol] The event type
|
|
95
|
+
# @param source [Object] The source of the event
|
|
96
|
+
# @param data [Hash] Additional event data
|
|
97
|
+
# @return [EventSystem::Event] The published event
|
|
98
|
+
def publish_event(type, source = nil, data = {})
|
|
99
|
+
@logger.debug("Publishing event: #{type}, source: #{source}, data: #{data}")
|
|
100
|
+
event = Event.new(type, source, data)
|
|
101
|
+
publish(event)
|
|
102
|
+
event
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Query for events based on options
|
|
106
|
+
# @param options [Hash] Query options
|
|
107
|
+
# @return [Array<EventSystem::Event>] Matching events
|
|
108
|
+
def query_events(options = {})
|
|
109
|
+
@storage&.query(options) || []
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Get the current session ID
|
|
113
|
+
# @return [String, nil] The current session ID, or nil if no event store
|
|
114
|
+
def current_session
|
|
115
|
+
@storage&.current_session
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Switch to a different session
|
|
119
|
+
# @param session_id [String] The session ID to switch to
|
|
120
|
+
# @return [void]
|
|
121
|
+
def switch_session(session_id)
|
|
122
|
+
@storage&.switch_session(session_id)
|
|
123
|
+
@logger.info("Switched to session: #{session_id}")
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Create a new session
|
|
127
|
+
# @param session_id [String, nil] Optional session ID, defaults to timestamp
|
|
128
|
+
# @return [String] The new session ID
|
|
129
|
+
def create_session(session_id = nil)
|
|
130
|
+
session_id = @storage&.create_session(session_id) || session_id || Time.now.strftime("%Y%m%d_%H%M%S")
|
|
131
|
+
@logger.info("Created new session: #{session_id}")
|
|
132
|
+
session_id
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Get statistics about the event system
|
|
136
|
+
# @return [Hash] Statistics about the event system
|
|
137
|
+
def stats
|
|
138
|
+
{
|
|
139
|
+
subscribers: @subscribers.transform_values(&:length),
|
|
140
|
+
storage: @storage&.stats || {},
|
|
141
|
+
config: @config.to_h
|
|
142
|
+
}
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Close the event manager and release resources
|
|
146
|
+
# @return [void]
|
|
147
|
+
def close
|
|
148
|
+
@storage&.close
|
|
149
|
+
@logger.info("EventSystem closed")
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Check if the event manager is ready for use
|
|
153
|
+
# @return [Boolean] True if ready
|
|
154
|
+
def ready?
|
|
155
|
+
@storage&.available? || false
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
private
|
|
159
|
+
|
|
160
|
+
# Deliver an event to subscribers of a specific type
|
|
161
|
+
# @param event_type [String] The event type
|
|
162
|
+
# @param event [EventSystem::Event] The event to deliver
|
|
163
|
+
# @return [void]
|
|
164
|
+
def deliver_to_subscribers(event_type, event)
|
|
165
|
+
subscribers = @subscribers[event_type] || []
|
|
166
|
+
|
|
167
|
+
# Sort subscribers by priority if they support it
|
|
168
|
+
sorted_subscribers = subscribers.sort_by do |subscriber|
|
|
169
|
+
if subscriber.respond_to?(:event_priority)
|
|
170
|
+
subscriber.event_priority
|
|
171
|
+
else
|
|
172
|
+
0
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
sorted_subscribers.each do |subscriber|
|
|
177
|
+
begin
|
|
178
|
+
# Check if subscriber handles this event type
|
|
179
|
+
if subscriber.respond_to?(:handles_event_type?) && !subscriber.handles_event_type?(event_type)
|
|
180
|
+
next
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
subscriber.handle_event(event)
|
|
184
|
+
rescue => e
|
|
185
|
+
@logger.error("Error in subscriber #{subscriber.class} handling #{event_type}: #{e.message}")
|
|
186
|
+
@logger.error(e.backtrace.join("\n"))
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EventSystem
|
|
4
|
+
# Interface for components that respond to events
|
|
5
|
+
# Classes including this module must implement the handle_event method
|
|
6
|
+
module EventSubscriber
|
|
7
|
+
# Handle an event that this subscriber is interested in
|
|
8
|
+
# @param event [EventSystem::Event] The event to handle
|
|
9
|
+
# @return [void]
|
|
10
|
+
def handle_event(event)
|
|
11
|
+
raise NotImplementedError, "#{self.class} must implement handle_event(event)"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Check if this subscriber is interested in a specific event type
|
|
15
|
+
# Override this method to provide filtering logic
|
|
16
|
+
# @param event_type [String] The event type to check
|
|
17
|
+
# @return [Boolean] True if this subscriber handles the event type
|
|
18
|
+
def handles_event_type?(event_type)
|
|
19
|
+
true # Default: handle all event types
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Get the priority for this subscriber (lower numbers = higher priority)
|
|
23
|
+
# Override this method to control event handling order
|
|
24
|
+
# @return [Integer] Priority level (default: 0)
|
|
25
|
+
def event_priority
|
|
26
|
+
0
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EventSystem
|
|
4
|
+
module Storage
|
|
5
|
+
# Abstract base class for event storage implementations
|
|
6
|
+
# All concrete event store classes should implement these methods
|
|
7
|
+
class Base
|
|
8
|
+
# Store an event
|
|
9
|
+
# @param event [EventSystem::Event] The event to store
|
|
10
|
+
# @return [void]
|
|
11
|
+
def store(event)
|
|
12
|
+
raise NotImplementedError, "#{self.class} must implement store(event)"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Query for events based on options
|
|
16
|
+
# @param options [Hash] Query options (type, time range, limit, etc.)
|
|
17
|
+
# @return [Array<EventSystem::Event>] Matching events
|
|
18
|
+
def query(options = {})
|
|
19
|
+
raise NotImplementedError, "#{self.class} must implement query(options)"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Load all events from a session
|
|
23
|
+
# @param session_id [String, nil] Session ID to load, or current session if nil
|
|
24
|
+
# @return [Array<EventSystem::Event>] Events from the session
|
|
25
|
+
def load_session(session_id = nil)
|
|
26
|
+
raise NotImplementedError, "#{self.class} must implement load_session(session_id)"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# List available sessions
|
|
30
|
+
# @return [Array<String>] List of session IDs
|
|
31
|
+
def list_sessions
|
|
32
|
+
raise NotImplementedError, "#{self.class} must implement list_sessions"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Get the current session ID
|
|
36
|
+
# @return [String, nil] Current session ID
|
|
37
|
+
def current_session
|
|
38
|
+
raise NotImplementedError, "#{self.class} must implement current_session"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Close the event store and release resources
|
|
42
|
+
# @return [void]
|
|
43
|
+
def close
|
|
44
|
+
# Optional method, default implementation does nothing
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Check if the storage is available
|
|
48
|
+
# @return [Boolean] True if storage is ready for use
|
|
49
|
+
def available?
|
|
50
|
+
true
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Get storage statistics
|
|
54
|
+
# @return [Hash] Storage statistics
|
|
55
|
+
def stats
|
|
56
|
+
{
|
|
57
|
+
type: self.class.name,
|
|
58
|
+
available: available?,
|
|
59
|
+
sessions: list_sessions.length
|
|
60
|
+
}
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|