tasker-rb 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. checksums.yaml +7 -0
  2. data/DEVELOPMENT.md +548 -0
  3. data/README.md +87 -0
  4. data/ext/tasker_core/Cargo.lock +4720 -0
  5. data/ext/tasker_core/Cargo.toml +76 -0
  6. data/ext/tasker_core/extconf.rb +38 -0
  7. data/ext/tasker_core/src/CLAUDE.md +7 -0
  8. data/ext/tasker_core/src/bootstrap.rs +320 -0
  9. data/ext/tasker_core/src/bridge.rs +400 -0
  10. data/ext/tasker_core/src/client_ffi.rs +173 -0
  11. data/ext/tasker_core/src/conversions.rs +131 -0
  12. data/ext/tasker_core/src/diagnostics.rs +57 -0
  13. data/ext/tasker_core/src/event_handler.rs +179 -0
  14. data/ext/tasker_core/src/event_publisher_ffi.rs +239 -0
  15. data/ext/tasker_core/src/ffi_logging.rs +245 -0
  16. data/ext/tasker_core/src/global_event_system.rs +16 -0
  17. data/ext/tasker_core/src/in_process_event_ffi.rs +319 -0
  18. data/ext/tasker_core/src/lib.rs +41 -0
  19. data/ext/tasker_core/src/observability_ffi.rs +339 -0
  20. data/lib/tasker_core/batch_processing/batch_aggregation_scenario.rb +85 -0
  21. data/lib/tasker_core/batch_processing/batch_worker_context.rb +238 -0
  22. data/lib/tasker_core/bootstrap.rb +394 -0
  23. data/lib/tasker_core/domain_events/base_publisher.rb +220 -0
  24. data/lib/tasker_core/domain_events/base_subscriber.rb +178 -0
  25. data/lib/tasker_core/domain_events/publisher_registry.rb +253 -0
  26. data/lib/tasker_core/domain_events/subscriber_registry.rb +152 -0
  27. data/lib/tasker_core/domain_events.rb +43 -0
  28. data/lib/tasker_core/errors/CLAUDE.md +7 -0
  29. data/lib/tasker_core/errors/common.rb +305 -0
  30. data/lib/tasker_core/errors/error_classifier.rb +61 -0
  31. data/lib/tasker_core/errors.rb +4 -0
  32. data/lib/tasker_core/event_bridge.rb +330 -0
  33. data/lib/tasker_core/handlers.rb +159 -0
  34. data/lib/tasker_core/internal.rb +31 -0
  35. data/lib/tasker_core/logger.rb +234 -0
  36. data/lib/tasker_core/models.rb +337 -0
  37. data/lib/tasker_core/observability/types.rb +158 -0
  38. data/lib/tasker_core/observability.rb +292 -0
  39. data/lib/tasker_core/registry/handler_registry.rb +453 -0
  40. data/lib/tasker_core/registry/resolver_chain.rb +258 -0
  41. data/lib/tasker_core/registry/resolvers/base_resolver.rb +90 -0
  42. data/lib/tasker_core/registry/resolvers/class_constant_resolver.rb +156 -0
  43. data/lib/tasker_core/registry/resolvers/explicit_mapping_resolver.rb +146 -0
  44. data/lib/tasker_core/registry/resolvers/method_dispatch_wrapper.rb +144 -0
  45. data/lib/tasker_core/registry/resolvers/registry_resolver.rb +229 -0
  46. data/lib/tasker_core/registry/resolvers.rb +42 -0
  47. data/lib/tasker_core/registry.rb +12 -0
  48. data/lib/tasker_core/step_handler/api.rb +48 -0
  49. data/lib/tasker_core/step_handler/base.rb +354 -0
  50. data/lib/tasker_core/step_handler/batchable.rb +50 -0
  51. data/lib/tasker_core/step_handler/decision.rb +53 -0
  52. data/lib/tasker_core/step_handler/mixins/api.rb +452 -0
  53. data/lib/tasker_core/step_handler/mixins/batchable.rb +465 -0
  54. data/lib/tasker_core/step_handler/mixins/decision.rb +252 -0
  55. data/lib/tasker_core/step_handler/mixins.rb +66 -0
  56. data/lib/tasker_core/subscriber.rb +212 -0
  57. data/lib/tasker_core/task_handler/base.rb +254 -0
  58. data/lib/tasker_core/tasker_rb.so +0 -0
  59. data/lib/tasker_core/template_discovery.rb +181 -0
  60. data/lib/tasker_core/tracing.rb +166 -0
  61. data/lib/tasker_core/types/batch_processing_outcome.rb +301 -0
  62. data/lib/tasker_core/types/client_types.rb +145 -0
  63. data/lib/tasker_core/types/decision_point_outcome.rb +177 -0
  64. data/lib/tasker_core/types/error_types.rb +72 -0
  65. data/lib/tasker_core/types/simple_message.rb +151 -0
  66. data/lib/tasker_core/types/step_context.rb +328 -0
  67. data/lib/tasker_core/types/step_handler_call_result.rb +307 -0
  68. data/lib/tasker_core/types/step_message.rb +112 -0
  69. data/lib/tasker_core/types/step_types.rb +207 -0
  70. data/lib/tasker_core/types/task_template.rb +240 -0
  71. data/lib/tasker_core/types/task_types.rb +148 -0
  72. data/lib/tasker_core/types.rb +132 -0
  73. data/lib/tasker_core/version.rb +13 -0
  74. data/lib/tasker_core/worker/CLAUDE.md +7 -0
  75. data/lib/tasker_core/worker/event_poller.rb +224 -0
  76. data/lib/tasker_core/worker/in_process_domain_event_poller.rb +271 -0
  77. data/lib/tasker_core.rb +160 -0
  78. metadata +322 -0
@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TaskerCore
4
+ module DomainEvents
5
+ # TAS-65: Base class for domain event subscribers
6
+ #
7
+ # Domain event subscribers handle business events published by steps.
8
+ # They subscribe to event patterns and receive events via the in-process
9
+ # event bus (for fast events) or can poll PGMQ (for durable events).
10
+ #
11
+ # @example Creating a subscriber for payment events
12
+ # class PaymentEventSubscriber < TaskerCore::DomainEvents::BaseSubscriber
13
+ # # Match all payment.* events
14
+ # subscribes_to 'payment.*'
15
+ #
16
+ # def handle(event)
17
+ # case event[:event_name]
18
+ # when 'payment.processed'
19
+ # notify_accounting(event[:business_payload])
20
+ # when 'payment.failed'
21
+ # alert_support(event[:business_payload])
22
+ # end
23
+ # end
24
+ #
25
+ # private
26
+ #
27
+ # def notify_accounting(payload)
28
+ # # Send to accounting system
29
+ # end
30
+ #
31
+ # def alert_support(payload)
32
+ # # Alert support team
33
+ # end
34
+ # end
35
+ #
36
+ # @example Creating a subscriber for metrics collection
37
+ # class MetricsSubscriber < TaskerCore::DomainEvents::BaseSubscriber
38
+ # # Match all events
39
+ # subscribes_to '*'
40
+ #
41
+ # def handle(event)
42
+ # StatsD.increment("domain_events.#{event[:event_name].gsub('.', '_')}")
43
+ # end
44
+ # end
45
+ #
46
+ # @example Registering and starting subscribers
47
+ # # In bootstrap
48
+ # subscribers = [
49
+ # PaymentEventSubscriber.new,
50
+ # MetricsSubscriber.new
51
+ # ]
52
+ #
53
+ # subscribers.each(&:start!)
54
+ #
55
+ class BaseSubscriber
56
+ class << self
57
+ # DSL method to declare event pattern subscriptions
58
+ #
59
+ # @param patterns [Array<String>] Event patterns to subscribe to
60
+ def subscribes_to(*patterns)
61
+ @patterns = patterns.flatten
62
+ end
63
+
64
+ # Get declared patterns
65
+ #
66
+ # @return [Array<String>]
67
+ def patterns
68
+ @patterns || ['*']
69
+ end
70
+ end
71
+
72
+ attr_reader :logger, :active
73
+
74
+ def initialize
75
+ @logger = TaskerCore::Logger.instance
76
+ @active = false
77
+ @subscriptions = []
78
+ end
79
+
80
+ # Start listening for events
81
+ def start!
82
+ return if @active
83
+
84
+ @active = true
85
+ poller = TaskerCore::Worker::InProcessDomainEventPoller.instance
86
+
87
+ self.class.patterns.each do |pattern|
88
+ # Subscribe to the poller with this subscriber's handler
89
+ poller.subscribe(pattern) do |event|
90
+ handle_event_safely(event)
91
+ end
92
+ @subscriptions << pattern
93
+ logger.info "#{self.class.name}: Subscribed to pattern '#{pattern}'"
94
+ end
95
+
96
+ logger.info "#{self.class.name}: Started with #{@subscriptions.size} subscriptions"
97
+ end
98
+
99
+ # Stop listening for events
100
+ def stop!
101
+ return unless @active
102
+
103
+ @active = false
104
+ poller = TaskerCore::Worker::InProcessDomainEventPoller.instance
105
+
106
+ @subscriptions.each do |pattern|
107
+ poller.unsubscribe(pattern)
108
+ end
109
+ @subscriptions.clear
110
+
111
+ logger.info "#{self.class.name}: Stopped"
112
+ end
113
+
114
+ # Check if subscriber is active
115
+ def active?
116
+ @active
117
+ end
118
+
119
+ # Handle a domain event
120
+ #
121
+ # Subclasses MUST implement this method.
122
+ #
123
+ # @param event [Hash] The domain event
124
+ # - :event_name [String] The event name (e.g., "order.processed")
125
+ # - :business_payload [Hash] The business data from the step
126
+ # - :metadata [Hash] Event metadata (task_uuid, step_uuid, correlation_id, etc.)
127
+ # - :execution_result [Hash] The step execution result
128
+ def handle(event)
129
+ raise NotImplementedError, "#{self.class} must implement #handle"
130
+ end
131
+
132
+ # Hook called before handling an event
133
+ #
134
+ # Override for pre-processing, validation, or filtering.
135
+ # Return false to skip handling this event.
136
+ #
137
+ # @param event [Hash] The domain event
138
+ # @return [Boolean] true to continue handling, false to skip
139
+ def before_handle(event) # rubocop:disable Lint/UnusedMethodArgument
140
+ true
141
+ end
142
+
143
+ # Hook called after successful handling
144
+ #
145
+ # Override for post-processing, metrics, or cleanup.
146
+ #
147
+ # @param event [Hash] The domain event
148
+ def after_handle(event)
149
+ # Default: no-op
150
+ end
151
+
152
+ # Hook called if handling fails
153
+ #
154
+ # Override for custom error handling, alerts, or retry logic.
155
+ # Note: Domain event handling uses fire-and-forget semantics - errors
156
+ # are logged but not propagated.
157
+ #
158
+ # @param event [Hash] The domain event
159
+ # @param error [Exception] The error that occurred
160
+ def on_handle_error(event, error)
161
+ logger.error "#{self.class.name}: Failed to handle #{event[:event_name]}: #{error.message}"
162
+ end
163
+
164
+ private
165
+
166
+ # Safely handle an event with error capture
167
+ def handle_event_safely(event)
168
+ return unless @active
169
+ return unless before_handle(event)
170
+
171
+ handle(event)
172
+ after_handle(event)
173
+ rescue StandardError => e
174
+ on_handle_error(event, e)
175
+ end
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,253 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'singleton'
4
+
5
+ module TaskerCore
6
+ module DomainEvents
7
+ # TAS-65: Registry for custom domain event publishers
8
+ #
9
+ # Maps publisher names (from YAML configuration) to their Ruby implementations.
10
+ # Publishers are registered at bootstrap time and validated against task templates.
11
+ #
12
+ # @example Registering publishers at bootstrap
13
+ # registry = TaskerCore::DomainEvents::PublisherRegistry.instance
14
+ #
15
+ # # Register custom publishers
16
+ # registry.register(PaymentEventPublisher.new)
17
+ # registry.register(InventoryEventPublisher.new)
18
+ #
19
+ # # Validate against loaded templates
20
+ # required = ['PaymentEventPublisher', 'InventoryEventPublisher', 'MissingPublisher']
21
+ # registry.validate_required!(required)
22
+ # # => raises ValidationError: Missing publishers: MissingPublisher
23
+ #
24
+ # @example Looking up publishers
25
+ # publisher = registry.get('PaymentEventPublisher')
26
+ # publisher.transform_payload(step_result, event_declaration)
27
+ #
28
+ # @example Using default publisher
29
+ # # Returns DefaultPublisher for unregistered names
30
+ # publisher = registry.get_or_default('UnknownPublisher')
31
+ #
32
+ class PublisherRegistry
33
+ include Singleton
34
+
35
+ # Error classes nested in PublisherRegistry for cleaner namespacing
36
+ class ValidationError < StandardError
37
+ attr_reader :missing_publishers, :registered_publishers
38
+
39
+ def initialize(missing, registered)
40
+ @missing_publishers = missing
41
+ @registered_publishers = registered
42
+ super("Missing publishers: #{missing.join(', ')}. Registered: #{registered.join(', ')}")
43
+ end
44
+ end
45
+
46
+ class PublisherNotFoundError < StandardError
47
+ attr_reader :publisher_name, :registered_publishers
48
+
49
+ def initialize(name, registered)
50
+ @publisher_name = name
51
+ @registered_publishers = registered
52
+ super("Publisher '#{name}' not found. Registered: #{registered.join(', ')}")
53
+ end
54
+ end
55
+
56
+ class RegistryFrozenError < StandardError; end
57
+
58
+ class DuplicatePublisherError < StandardError
59
+ attr_reader :publisher_name
60
+
61
+ def initialize(name)
62
+ @publisher_name = name
63
+ super("Publisher '#{name}' is already registered")
64
+ end
65
+ end
66
+
67
+ # Default publisher that passes step result through unchanged
68
+ class DefaultPublisher < BasePublisher
69
+ def name
70
+ 'default'
71
+ end
72
+
73
+ def transform_payload(step_result, _event_declaration, _step_context = nil)
74
+ step_result[:result] || {}
75
+ end
76
+ end
77
+
78
+ attr_reader :logger, :publishers, :default_publisher
79
+
80
+ def initialize
81
+ @logger = TaskerCore::Logger.instance
82
+ @publishers = {}
83
+ @default_publisher = DefaultPublisher.new
84
+ @frozen = false
85
+ end
86
+
87
+ # Register a custom publisher
88
+ #
89
+ # @param publisher [BasePublisher] The publisher instance to register
90
+ # @return [BasePublisher, nil] The previous publisher with the same name, if any
91
+ # @raise [ArgumentError] If publisher does not inherit from BasePublisher
92
+ # @raise [DuplicatePublisherError] If a publisher with the same name is already registered
93
+ # @raise [RegistryFrozenError] If the registry has been frozen
94
+ def register(publisher)
95
+ # Type validation first
96
+ raise ArgumentError, "Expected BasePublisher, got #{publisher.class}" unless publisher.is_a?(BasePublisher)
97
+
98
+ raise RegistryFrozenError, 'Registry is frozen after validation' if @frozen
99
+
100
+ name = publisher.name
101
+
102
+ # Check for duplicates
103
+ raise DuplicatePublisherError, name if @publishers.key?(name)
104
+
105
+ logger.info "Registering domain event publisher: #{name}"
106
+ @publishers[name] = publisher
107
+ nil # No previous since we now disallow duplicates
108
+ end
109
+
110
+ # Get a publisher by name
111
+ #
112
+ # @param name [String] The publisher name
113
+ # @return [BasePublisher, nil] The publisher, or nil if not found
114
+ def get(name)
115
+ @publishers[name]
116
+ end
117
+
118
+ # Get a publisher by name, or return the default if not found
119
+ #
120
+ # @param name [String, nil] The publisher name
121
+ # @return [BasePublisher] The publisher or default
122
+ def get_or_default(name)
123
+ return @default_publisher if name.nil? || name == 'default'
124
+
125
+ @publishers[name] || begin
126
+ logger.warn "Publisher #{name} not found, using default"
127
+ @default_publisher
128
+ end
129
+ end
130
+
131
+ # Get a publisher by name with strict mode (no fallback)
132
+ #
133
+ # @param name [String] The publisher name
134
+ # @return [BasePublisher] The publisher
135
+ # @raise [PublisherNotFoundError] If the publisher is not registered
136
+ def get_strict(name)
137
+ return @default_publisher if name == 'default'
138
+
139
+ @publishers[name] || raise(
140
+ PublisherNotFoundError.new(name, registered_names)
141
+ )
142
+ end
143
+
144
+ # Check if a publisher is registered
145
+ #
146
+ # @param name [String] The publisher name
147
+ # @return [Boolean]
148
+ def registered?(name)
149
+ @publishers.key?(name) || name == 'default'
150
+ end
151
+
152
+ # Get all registered publisher names
153
+ #
154
+ # @return [Array<String>]
155
+ def registered_names
156
+ @publishers.keys
157
+ end
158
+
159
+ # Get count of registered publishers
160
+ #
161
+ # @return [Integer]
162
+ def count
163
+ @publishers.size
164
+ end
165
+
166
+ # Check if registry has no custom publishers
167
+ #
168
+ # @return [Boolean]
169
+ def empty?
170
+ @publishers.empty?
171
+ end
172
+
173
+ # Unregister a publisher by name
174
+ #
175
+ # @param name [String] The publisher name
176
+ # @return [BasePublisher, nil] The removed publisher, if any
177
+ # @raise [RegistryFrozenError] If the registry has been frozen
178
+ def unregister(name)
179
+ raise RegistryFrozenError, 'Registry is frozen after validation' if @frozen
180
+
181
+ logger.info "Unregistering domain event publisher: #{name}"
182
+ @publishers.delete(name)
183
+ end
184
+
185
+ # Clear all registered publishers
186
+ #
187
+ # @raise [RegistryFrozenError] If the registry has been frozen
188
+ def clear
189
+ raise RegistryFrozenError, 'Registry is frozen after validation' if @frozen
190
+
191
+ logger.info 'Clearing all domain event publishers'
192
+ @publishers.clear
193
+ end
194
+
195
+ # TAS-65: Validate that all required publishers are registered
196
+ #
197
+ # Implements "loud failure validation" - validates at init time that all
198
+ # publisher names referenced in task templates exist in the registry.
199
+ # After validation, the registry is frozen to prevent changes.
200
+ #
201
+ # @param required_publishers [Array<String>] Publisher names from YAML configs
202
+ # @return [true] If all required publishers are registered
203
+ # @raise [ValidationError] If some publishers are missing
204
+ def validate_required!(required_publishers)
205
+ missing = []
206
+
207
+ required_publishers.each do |name|
208
+ next if name == 'default'
209
+ next if registered?(name)
210
+
211
+ missing << name
212
+ end
213
+
214
+ raise ValidationError.new(missing, registered_names) if missing.any?
215
+
216
+ @frozen = true
217
+ logger.info "Publisher validation passed. Registered: #{registered_names.join(', ')}"
218
+ true
219
+ end
220
+
221
+ # Check if the registry is frozen
222
+ #
223
+ # @return [Boolean]
224
+ def frozen?
225
+ @frozen
226
+ end
227
+
228
+ # Freeze the registry to prevent further changes
229
+ #
230
+ # @return [void]
231
+ def freeze!
232
+ @frozen = true
233
+ logger.info 'Publisher registry frozen'
234
+ end
235
+
236
+ # Reset the registry (for testing)
237
+ #
238
+ # @note This unfreezes the registry - use only in tests
239
+ def reset!
240
+ @publishers.clear
241
+ @frozen = false
242
+ logger.info 'Publisher registry reset'
243
+ end
244
+ end
245
+
246
+ # Backwards-compatible aliases for classes that were moved into PublisherRegistry
247
+ # These allow existing code to use the old namespace while we transition
248
+ DefaultPublisher = PublisherRegistry::DefaultPublisher
249
+ ValidationError = PublisherRegistry::ValidationError
250
+ PublisherNotFoundError = PublisherRegistry::PublisherNotFoundError
251
+ RegistryFrozenError = PublisherRegistry::RegistryFrozenError
252
+ end
253
+ end
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'singleton'
4
+
5
+ module TaskerCore
6
+ module DomainEvents
7
+ # TAS-65: Registry for domain event subscribers
8
+ #
9
+ # Manages the lifecycle of domain event subscribers. Subscribers are registered
10
+ # at bootstrap time and started/stopped together with the worker.
11
+ #
12
+ # @example Registering subscribers at bootstrap
13
+ # registry = TaskerCore::DomainEvents::SubscriberRegistry.instance
14
+ #
15
+ # # Register subscriber classes (instantiation deferred)
16
+ # registry.register(PaymentEventSubscriber)
17
+ # registry.register(MetricsSubscriber)
18
+ #
19
+ # # Or register instances directly
20
+ # registry.register_instance(CustomSubscriber.new(some_config))
21
+ #
22
+ # # Start all subscribers
23
+ # registry.start_all!
24
+ #
25
+ # @example Stopping all subscribers
26
+ # registry.stop_all!
27
+ #
28
+ class SubscriberRegistry
29
+ include Singleton
30
+
31
+ attr_reader :logger, :subscribers
32
+
33
+ def initialize
34
+ @logger = TaskerCore::Logger.instance
35
+ @subscribers = []
36
+ @started = false
37
+ end
38
+
39
+ # Register a subscriber class
40
+ #
41
+ # The class will be instantiated when start_all! is called.
42
+ #
43
+ # @param subscriber_class [Class] A subclass of BaseSubscriber
44
+ # @return [void]
45
+ def register(subscriber_class)
46
+ unless subscriber_class < BaseSubscriber
47
+ raise ArgumentError, "#{subscriber_class} must be a subclass of BaseSubscriber"
48
+ end
49
+
50
+ logger.info "SubscriberRegistry: Registered #{subscriber_class.name}"
51
+ @subscriber_classes ||= []
52
+ @subscriber_classes << subscriber_class
53
+ end
54
+
55
+ # Register a subscriber instance directly
56
+ #
57
+ # Use this when your subscriber needs custom initialization.
58
+ #
59
+ # @param subscriber [BaseSubscriber] A subscriber instance
60
+ # @return [void]
61
+ def register_instance(subscriber)
62
+ raise ArgumentError, "Expected BaseSubscriber, got #{subscriber.class}" unless subscriber.is_a?(BaseSubscriber)
63
+
64
+ logger.info "SubscriberRegistry: Registered instance of #{subscriber.class.name}"
65
+ @subscribers << subscriber
66
+ end
67
+
68
+ # Start all registered subscribers
69
+ #
70
+ # Instantiates registered classes and starts all subscribers.
71
+ #
72
+ # @return [void]
73
+ def start_all!
74
+ return if @started
75
+
76
+ # Instantiate registered classes
77
+ (@subscriber_classes || []).each do |klass|
78
+ @subscribers << klass.new
79
+ end
80
+
81
+ # Start all subscribers
82
+ @subscribers.each do |subscriber|
83
+ subscriber.start!
84
+ rescue StandardError => e
85
+ logger.error "Failed to start #{subscriber.class.name}: #{e.message}"
86
+ end
87
+
88
+ @started = true
89
+ logger.info "SubscriberRegistry: Started #{@subscribers.size} subscribers"
90
+ end
91
+
92
+ # Stop all subscribers
93
+ #
94
+ # @return [void]
95
+ def stop_all!
96
+ return unless @started
97
+
98
+ @subscribers.each do |subscriber|
99
+ subscriber.stop!
100
+ rescue StandardError => e
101
+ logger.error "Failed to stop #{subscriber.class.name}: #{e.message}"
102
+ end
103
+
104
+ @started = false
105
+ logger.info 'SubscriberRegistry: Stopped all subscribers'
106
+ end
107
+
108
+ # Check if subscribers have been started
109
+ #
110
+ # @return [Boolean]
111
+ def started?
112
+ @started
113
+ end
114
+
115
+ # Get count of registered subscribers
116
+ #
117
+ # @return [Integer]
118
+ def count
119
+ @subscribers.size + (@subscriber_classes&.size || 0)
120
+ end
121
+
122
+ # Get subscriber statistics
123
+ #
124
+ # @return [Hash]
125
+ def stats
126
+ {
127
+ started: @started,
128
+ subscriber_count: @subscribers.size,
129
+ active_count: @subscribers.count(&:active?),
130
+ subscribers: @subscribers.map do |s|
131
+ {
132
+ class: s.class.name,
133
+ active: s.active?,
134
+ patterns: s.class.patterns
135
+ }
136
+ end
137
+ }
138
+ end
139
+
140
+ # Reset the registry (for testing)
141
+ #
142
+ # @note This stops all subscribers first
143
+ def reset!
144
+ stop_all! if @started
145
+ @subscribers.clear
146
+ @subscriber_classes&.clear
147
+ @started = false
148
+ logger.info 'SubscriberRegistry: Reset'
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ # TAS-65: Domain Events Module
4
+ #
5
+ # Provides infrastructure for custom domain event publishers.
6
+ # Domain events are business events (e.g., "order.processed", "payment.completed")
7
+ # published after step execution based on YAML declarations.
8
+ #
9
+ # @example Using the domain events infrastructure
10
+ # # Register custom publishers at bootstrap
11
+ # require 'tasker_core/domain_events'
12
+ #
13
+ # class PaymentEventPublisher < TaskerCore::DomainEvents::BasePublisher
14
+ # def name
15
+ # 'PaymentEventPublisher'
16
+ # end
17
+ #
18
+ # def transform_payload(step_result, event_declaration, step_context)
19
+ # {
20
+ # payment_id: step_result[:result][:payment_id],
21
+ # amount: step_result[:result][:amount],
22
+ # status: step_result[:success] ? 'succeeded' : 'failed'
23
+ # }
24
+ # end
25
+ # end
26
+ #
27
+ # TaskerCore::DomainEvents::PublisherRegistry.instance.register(
28
+ # PaymentEventPublisher.new
29
+ # )
30
+ #
31
+ module TaskerCore
32
+ module DomainEvents
33
+ # Load domain events components
34
+ end
35
+ end
36
+
37
+ # Publishers
38
+ require_relative 'domain_events/base_publisher'
39
+ require_relative 'domain_events/publisher_registry'
40
+
41
+ # Subscribers
42
+ require_relative 'domain_events/base_subscriber'
43
+ require_relative 'domain_events/subscriber_registry'
@@ -0,0 +1,7 @@
1
+ <claude-mem-context>
2
+ # Recent Activity
3
+
4
+ <!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
5
+
6
+ *No recent activity*
7
+ </claude-mem-context>