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,394 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TaskerCore
4
+ module Worker
5
+ # Bootstrap orchestrator for Ruby worker
6
+ #
7
+ # Manages the complete initialization and lifecycle of the Ruby worker system,
8
+ # coordinating both Rust foundation layer and Ruby business logic components.
9
+ # This is the primary entry point for starting a TaskerCore worker process.
10
+ #
11
+ # The bootstrap process follows this sequence:
12
+ # 1. **Initialize Ruby Components**: EventBridge, HandlerRegistry, Subscribers
13
+ # 2. **Bootstrap Rust Foundation**: Start Rust worker via FFI
14
+ # 3. **Start Event Processing**: Begin EventPoller for step execution
15
+ # 4. **Register Shutdown Handlers**: Setup graceful termination on signals
16
+ #
17
+ # The Bootstrap class is a singleton, ensuring only one worker instance runs
18
+ # per process. It handles the complex coordination between Ruby and Rust,
19
+ # managing lifecycle transitions and providing health monitoring.
20
+ #
21
+ # @example Basic worker startup
22
+ # # Start with default configuration
23
+ # bootstrap = TaskerCore::Worker::Bootstrap.start!
24
+ # # => Returns bootstrap instance in :running status
25
+ #
26
+ # # Check if worker is running
27
+ # bootstrap.running?
28
+ # # => true
29
+ #
30
+ # @example Custom configuration
31
+ # bootstrap = TaskerCore::Worker::Bootstrap.start!(
32
+ # worker_id: "custom-worker-1",
33
+ # enable_web_api: false,
34
+ # event_driven_enabled: true,
35
+ # deployment_mode: "Hybrid", # PollingOnly, EventDrivenOnly, or Hybrid
36
+ # namespaces: ["payments", "fulfillment", "notifications"]
37
+ # )
38
+ #
39
+ # @example Health checking
40
+ # status = TaskerCore::Worker::Bootstrap.instance.health_check
41
+ # # => {
42
+ # # healthy: true,
43
+ # # status: :running,
44
+ # # rust: { running: true, worker_core_status: "processing" },
45
+ # # ruby: {
46
+ # # status: :running,
47
+ # # event_bridge_active: true,
48
+ # # handlers_registered: 12,
49
+ # # subscriber_active: true,
50
+ # # event_poller_active: true
51
+ # # }
52
+ # # }
53
+ #
54
+ # @example Graceful shutdown
55
+ # # Shutdown cleanly, completing in-flight work
56
+ # TaskerCore::Worker::Bootstrap.instance.shutdown!
57
+ # # => Stops Rust worker, Ruby components, runs shutdown handlers
58
+ #
59
+ # @example Custom shutdown handlers
60
+ # bootstrap = TaskerCore::Worker::Bootstrap.instance
61
+ # bootstrap.on_shutdown do
62
+ # puts "Cleaning up resources..."
63
+ # cleanup_database_connections
64
+ # flush_metrics
65
+ # end
66
+ #
67
+ # @example Getting comprehensive status
68
+ # status = bootstrap.status
69
+ # # => {
70
+ # # rust: { running: true, ... },
71
+ # # ruby: {
72
+ # # status: :running,
73
+ # # handle_stored: true,
74
+ # # handle_id: "550e8400-e29b-41d4-a716-446655440000",
75
+ # # worker_id: "ruby-worker-123",
76
+ # # event_bridge_active: true,
77
+ # # handler_registry_size: 12,
78
+ # # subscriber_active: true,
79
+ # # event_poller_active: true
80
+ # # }
81
+ # # }
82
+ #
83
+ # Deployment Modes:
84
+ # - **PollingOnly**: Traditional polling-based coordination (highest latency, most reliable)
85
+ # - **EventDrivenOnly**: Pure event-driven using PostgreSQL LISTEN/NOTIFY (lowest latency)
86
+ # - **Hybrid**: Event-driven with polling fallback (recommended for production)
87
+ #
88
+ # Worker States:
89
+ # - **:initialized**: Created but not started
90
+ # - **:running**: Fully operational and processing events
91
+ # - **:shutting_down**: Graceful shutdown in progress
92
+ # - **:stopped**: Completely shut down
93
+ #
94
+ # Signal Handling:
95
+ # The bootstrap automatically registers signal handlers for:
96
+ # - **SIGINT** (Ctrl+C): Graceful shutdown
97
+ # - **SIGTERM**: Graceful shutdown
98
+ # - **at_exit**: Cleanup on process exit
99
+ #
100
+ # @see TaskerCore::Worker::EventPoller For event polling details
101
+ # @see TaskerCore::EventBridge For event coordination
102
+ # @see TaskerCore::Registry::HandlerRegistry For handler management
103
+ # @see TaskerCore::FFI For Rust FFI operations
104
+ class Bootstrap
105
+ include Singleton
106
+
107
+ attr_reader :logger, :config, :rust_handle
108
+
109
+ def initialize
110
+ @logger = TaskerCore::Logger.instance
111
+ @status = :initialized
112
+ @shutdown_handlers = []
113
+ @rust_handle = nil
114
+ end
115
+
116
+ # Start the worker system with optional configuration
117
+ def self.start!
118
+ instance.start!
119
+ end
120
+
121
+ # Main boots
122
+ def start!
123
+ logger.info 'Starting Ruby worker bootstrap'
124
+
125
+ # Initialize Ruby components first
126
+ initialize_ruby_components!
127
+
128
+ # Bootstrap Rust foundation via FFI
129
+ bootstrap_rust_foundation!
130
+
131
+ # Start event processing
132
+ start_event_processing!
133
+
134
+ # Register shutdown handlers
135
+ register_shutdown_handlers!
136
+
137
+ @status = :running
138
+ logger.info 'Ruby worker system started successfully'
139
+
140
+ self
141
+ rescue StandardError => e
142
+ logger.error "Failed to start worker: #{e.message}"
143
+ logger.error e.backtrace.join("\n")
144
+ shutdown!
145
+ raise
146
+ end
147
+
148
+ # Check if worker is running
149
+ def running?
150
+ @status == :running && rust_worker_running?
151
+ end
152
+
153
+ # Check if Rust handle is valid and running
154
+ def rust_handle_running?
155
+ return false unless @rust_handle.is_a?(Hash) &&
156
+ (@rust_handle[:handle_id] || @rust_handle['handle_id'])
157
+
158
+ rust_worker_running?
159
+ end
160
+
161
+ # Get comprehensive status
162
+ def status
163
+ rust_status = TaskerCore::FFI.worker_status
164
+ {
165
+ rust: rust_status,
166
+ ruby: {
167
+ status: @status,
168
+ handle_stored: !@rust_handle.nil?,
169
+ handle_id: @rust_handle&.dig('handle_id') || @rust_handle&.dig(:handle_id),
170
+ worker_id: @rust_handle&.dig('worker_id') || @rust_handle&.dig(:worker_id),
171
+ event_bridge_active: EventBridge.instance.active?,
172
+ handler_registry_size: Registry::HandlerRegistry.instance.handlers.size,
173
+ subscriber_active: @step_subscriber&.active? || false,
174
+ event_poller_active: EventPoller.instance.active?
175
+ }
176
+ }
177
+ rescue StandardError => e
178
+ logger.error "Failed to get status: #{e.message}"
179
+ { error: e.message, status: @status }
180
+ end
181
+
182
+ # Execute a block with the Rust handle, bootstrapping if necessary
183
+ def with_rust_handle(&block)
184
+ bootstrap_rust_foundation! unless rust_handle_running?
185
+ block.call
186
+ end
187
+
188
+ # Graceful shutdown
189
+ def shutdown!
190
+ return if @status == :stopped
191
+
192
+ logger.info 'Initiating graceful shutdown'
193
+ @status = :shutting_down
194
+
195
+ # Transition Rust to graceful shutdown first
196
+ if @rust_handle
197
+ begin
198
+ TaskerCore::FFI.transition_to_graceful_shutdown
199
+ rescue StandardError => e
200
+ logger.error "Failed to transition to graceful shutdown: #{e.message}"
201
+ end
202
+ end
203
+
204
+ # Stop Ruby components
205
+ @step_subscriber&.stop!
206
+ EventBridge.instance.stop!
207
+ EventPoller.instance.stop!
208
+
209
+ # TAS-65: Stop domain event components
210
+ DomainEvents::SubscriberRegistry.instance.stop_all!
211
+ InProcessDomainEventPoller.instance.stop!
212
+
213
+ # Stop Rust worker and clear handle
214
+ if @rust_handle
215
+ begin
216
+ TaskerCore::FFI.stop_worker
217
+ rescue StandardError => e
218
+ logger.error "Failed to stop Rust worker: #{e.message}"
219
+ ensure
220
+ @rust_handle = nil
221
+ end
222
+ end
223
+
224
+ # Run custom shutdown handlers
225
+ @shutdown_handlers.each do |handler|
226
+ handler.call
227
+ rescue StandardError
228
+ nil
229
+ end
230
+
231
+ @status = :stopped
232
+ logger.info 'Worker shutdown complete'
233
+ end
234
+
235
+ # Register custom shutdown handler
236
+ def on_shutdown(&block)
237
+ @shutdown_handlers << block if block_given?
238
+ end
239
+
240
+ # Perform health check on both Ruby and Rust components
241
+ def health_check
242
+ return { healthy: false, status: @status, error: 'not_running' } unless running?
243
+
244
+ begin
245
+ # Get Rust worker status
246
+ rust_status = TaskerCore::FFI.worker_status
247
+ rust_running = rust_status['running'] || rust_status[:running]
248
+
249
+ # Check Ruby components
250
+ ruby_healthy = @status == :running &&
251
+ EventBridge.instance.active? &&
252
+ EventPoller.instance.active? &&
253
+ Registry::HandlerRegistry.instance.handlers.any?
254
+
255
+ overall_healthy = rust_running && ruby_healthy
256
+
257
+ {
258
+ healthy: overall_healthy,
259
+ status: @status,
260
+ rust: {
261
+ running: rust_running,
262
+ worker_core_status: rust_status['worker_core_status'] || rust_status[:worker_core_status]
263
+ },
264
+ ruby: {
265
+ status: @status,
266
+ event_bridge_active: EventBridge.instance.active?,
267
+ handlers_registered: Registry::HandlerRegistry.instance.handlers.size,
268
+ subscriber_active: @step_subscriber&.active? || false,
269
+ event_poller_active: EventPoller.instance.active?,
270
+ # TAS-65: Domain event system status
271
+ domain_event_poller_active: InProcessDomainEventPoller.instance.active?,
272
+ domain_event_subscribers: DomainEvents::SubscriberRegistry.instance.stats
273
+ }
274
+ }
275
+ rescue StandardError => e
276
+ logger.error "Health check failed: #{e.message}"
277
+ {
278
+ healthy: false,
279
+ status: @status,
280
+ error: e.message
281
+ }
282
+ end
283
+ end
284
+
285
+ private
286
+
287
+ def detect_namespaces
288
+ # Auto-detect from registered handlers
289
+ Registry::HandlerRegistry.instance.registered_handlers.map do |handler_class|
290
+ handler_class.namespace if handler_class.respond_to?(:namespace)
291
+ end.compact.uniq
292
+ end
293
+
294
+ def initialize_ruby_components!
295
+ logger.info 'Initializing Ruby components...'
296
+
297
+ # Initialize event bridge
298
+ EventBridge.instance
299
+
300
+ # Initialize handler registry (bootstrap happens automatically)
301
+ Registry::HandlerRegistry.instance
302
+
303
+ # Initialize step execution subscriber
304
+ @step_subscriber = StepExecutionSubscriber.new
305
+
306
+ logger.info 'Ruby components initialized'
307
+ end
308
+
309
+ def bootstrap_rust_foundation!
310
+ # Check if we already have a running handle
311
+ if rust_handle_running?
312
+ logger.debug 'Rust worker foundation already running, reusing handle'
313
+ return @rust_handle
314
+ end
315
+
316
+ logger.info 'Bootstrapping Rust worker foundation...'
317
+
318
+ # Bootstrap the worker and store the handle result
319
+ result = TaskerCore::FFI.bootstrap_worker
320
+ logger.info "Rust bootstrap result: #{result.inspect}"
321
+
322
+ # Check if it was already running or newly started
323
+ # Handle both string and symbol keys from Rust FFI
324
+ status = result['status'] || result[:status]
325
+ worker_id = result['worker_id'] || result[:worker_id]
326
+
327
+ if status == 'already_running'
328
+ logger.info 'Worker system was already running, reusing existing handle'
329
+ elsif status == 'started'
330
+ logger.info "New worker system started with ID: #{worker_id}"
331
+ else
332
+ raise "Unexpected bootstrap status: #{status}"
333
+ end
334
+
335
+ # Store the handle information
336
+ @rust_handle = result
337
+
338
+ # Verify it's running
339
+ status = TaskerCore::FFI.worker_status
340
+ # Handle both string and symbol keys from Rust FFI
341
+ running = status['running'] || status[:running]
342
+ unless running
343
+ @rust_handle = nil
344
+ raise "Rust worker failed to start: #{status.inspect}"
345
+ end
346
+
347
+ handle_id = @rust_handle['handle_id'] || @rust_handle[:handle_id]
348
+ logger.info "Rust foundation bootstrapped with handle: #{handle_id}"
349
+ @rust_handle
350
+ end
351
+
352
+ def start_event_processing!
353
+ logger.info 'Starting event processing...'
354
+
355
+ # Start the EventPoller to poll for step execution events from Rust
356
+ EventPoller.instance.start!
357
+
358
+ # NOTE: StepExecutionSubscriber already subscribes to step execution events
359
+ # in its initializer, so we don't need to subscribe again here.
360
+ # Duplicate subscriptions cause the same event to be processed twice,
361
+ # leading to double state transitions.
362
+
363
+ # TAS-65: Start in-process domain event poller for fast events
364
+ InProcessDomainEventPoller.instance.start!
365
+
366
+ # TAS-65: Start domain event subscribers
367
+ DomainEvents::SubscriberRegistry.instance.start_all!
368
+
369
+ logger.info 'Event processing started (step events + domain events)'
370
+ end
371
+
372
+ def register_shutdown_handlers!
373
+ # Graceful shutdown on signals
374
+ %w[INT TERM].each do |signal|
375
+ Signal.trap(signal) do
376
+ Thread.new { shutdown! }
377
+ end
378
+ end
379
+
380
+ # Shutdown on exit
381
+ at_exit { shutdown! if running? }
382
+ end
383
+
384
+ def rust_worker_running?
385
+ status = TaskerCore::FFI.worker_status
386
+ # Handle both string and symbol keys from Rust FFI
387
+ running = status['running'] || status[:running]
388
+ running == true
389
+ rescue StandardError
390
+ false
391
+ end
392
+ end
393
+ end
394
+ end
@@ -0,0 +1,220 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TaskerCore
4
+ module DomainEvents
5
+ # TAS-65: Base class for custom domain event publishers
6
+ #
7
+ # Domain event publishers transform step execution results into business events.
8
+ # They are declared in task template YAML via the `publisher:` field and registered
9
+ # at bootstrap time.
10
+ #
11
+ # @example Creating a custom publisher
12
+ # class PaymentEventPublisher < TaskerCore::DomainEvents::BasePublisher
13
+ # def name
14
+ # 'PaymentEventPublisher'
15
+ # end
16
+ #
17
+ # def transform_payload(step_result, event_declaration)
18
+ # # Extract business-specific payload from step result
19
+ # {
20
+ # payment_id: step_result[:result][:payment_id],
21
+ # amount: step_result[:result][:amount],
22
+ # currency: step_result[:result][:currency],
23
+ # status: step_result[:success] ? 'succeeded' : 'failed'
24
+ # }
25
+ # end
26
+ #
27
+ # def should_publish?(step_result, event_declaration)
28
+ # # Only publish for successful payments
29
+ # step_result[:success] && step_result[:result][:payment_id].present?
30
+ # end
31
+ # end
32
+ #
33
+ # @example Registering a custom publisher
34
+ # TaskerCore::DomainEvents::PublisherRegistry.instance.register(
35
+ # PaymentEventPublisher.new
36
+ # )
37
+ #
38
+ # @example YAML declaration
39
+ # steps:
40
+ # - name: process_payment
41
+ # publishes_events:
42
+ # - name: payment.processed
43
+ # publisher: PaymentEventPublisher
44
+ # delivery_mode: durable
45
+ # condition: success
46
+ #
47
+ class BasePublisher
48
+ attr_reader :logger
49
+
50
+ def initialize
51
+ @logger = TaskerCore::Logger.instance
52
+ end
53
+
54
+ # The publisher name used for registry lookup
55
+ # Must match the `publisher:` field in YAML
56
+ #
57
+ # @return [String] The publisher name
58
+ def name
59
+ raise NotImplementedError, "#{self.class} must implement #name"
60
+ end
61
+
62
+ # Transform step result into business event payload
63
+ #
64
+ # Override this to customize the event payload for your domain.
65
+ # The default implementation returns the step result as-is.
66
+ #
67
+ # @param step_result [Hash] The step execution result
68
+ # - :success [Boolean] Whether the step succeeded
69
+ # - :result [Hash] The step handler's return value
70
+ # - :metadata [Hash] Execution metadata
71
+ # @param event_declaration [Hash] The event declaration from YAML
72
+ # - :name [String] The event name (e.g., "order.processed")
73
+ # - :delivery_mode [String] "durable" or "fast"
74
+ # - :condition [String] "success", "failure", or "always"
75
+ # @param step_context [Hash] The step execution context
76
+ # - :task [Hash] Task information
77
+ # - :workflow_step [Hash] Workflow step information
78
+ # - :step_definition [Hash] Step definition from YAML
79
+ #
80
+ # @return [Hash] The business event payload to publish
81
+ def transform_payload(step_result, _event_declaration, _step_context = nil)
82
+ step_result[:result] || {}
83
+ end
84
+
85
+ # Determine if this event should be published
86
+ #
87
+ # Override this to add custom publishing conditions beyond the
88
+ # YAML `condition:` field. The YAML condition is evaluated first,
89
+ # then this method is called.
90
+ #
91
+ # @param step_result [Hash] The step execution result
92
+ # @param event_declaration [Hash] The event declaration from YAML
93
+ # @param step_context [Hash] The step execution context
94
+ #
95
+ # @return [Boolean] true if the event should be published
96
+ def should_publish?(_step_result, _event_declaration, _step_context = nil)
97
+ true
98
+ end
99
+
100
+ # Add additional metadata to the event
101
+ #
102
+ # Override this to add custom metadata fields to the event.
103
+ # Default returns empty hash.
104
+ #
105
+ # @param step_result [Hash] The step execution result
106
+ # @param event_declaration [Hash] The event declaration from YAML
107
+ # @param step_context [Hash] The step execution context
108
+ #
109
+ # @return [Hash] Additional metadata to merge into event metadata
110
+ def additional_metadata(_step_result, _event_declaration, _step_context = nil)
111
+ {}
112
+ end
113
+
114
+ # Hook called before publishing
115
+ #
116
+ # Override for pre-publish validation, logging, or metrics.
117
+ # Raise an exception to prevent publishing.
118
+ #
119
+ # @param event_name [String] The event name
120
+ # @param payload [Hash] The transformed payload
121
+ # @param metadata [Hash] The event metadata
122
+ def before_publish(event_name, _payload, _metadata)
123
+ logger.debug "Publishing event: #{event_name}"
124
+ end
125
+
126
+ # Hook called after successful publishing
127
+ #
128
+ # Override for post-publish logging, metrics, or cleanup.
129
+ #
130
+ # @param event_name [String] The event name
131
+ # @param payload [Hash] The transformed payload
132
+ # @param metadata [Hash] The event metadata
133
+ def after_publish(event_name, _payload, _metadata)
134
+ logger.debug "Event published: #{event_name}"
135
+ end
136
+
137
+ # Hook called if publishing fails
138
+ #
139
+ # Override for error handling, logging, or fallback behavior.
140
+ # Default logs the error but does not re-raise.
141
+ #
142
+ # @param event_name [String] The event name
143
+ # @param error [Exception] The error that occurred
144
+ # @param payload [Hash] The transformed payload
145
+ def on_publish_error(event_name, error, _payload)
146
+ logger.error "Failed to publish event #{event_name}: #{error.message}"
147
+ end
148
+
149
+ # ========================================================================
150
+ # CROSS-LANGUAGE STANDARD API (TAS-96)
151
+ # ========================================================================
152
+
153
+ # Cross-language standard: Publish an event with unified context
154
+ #
155
+ # This method coordinates the existing hooks (should_publish?, transform_payload,
156
+ # additional_metadata, before_publish, after_publish) into a single publish call.
157
+ #
158
+ # @param ctx [Hash] Step event context containing:
159
+ # - :event_name [String] The event name (e.g., "payment.processed")
160
+ # - :step_result [Hash] The step execution result
161
+ # - :event_declaration [Hash] The event declaration from YAML
162
+ # - :step_context [Hash, TaskerCore::Types::StepContext] Step execution context
163
+ # @return [Boolean] true if event was published, false if skipped
164
+ #
165
+ # @example Publishing with context
166
+ # ctx = {
167
+ # event_name: 'payment.processed',
168
+ # step_result: { success: true, result: { payment_id: '123' } },
169
+ # event_declaration: { name: 'payment.processed', delivery_mode: 'durable' },
170
+ # step_context: step_context
171
+ # }
172
+ # publisher.publish(ctx)
173
+ def publish(ctx)
174
+ event_name = ctx[:event_name]
175
+ step_result = ctx[:step_result]
176
+ event_declaration = ctx[:event_declaration] || {}
177
+ step_context = ctx[:step_context]
178
+
179
+ # Check publishing conditions
180
+ return false unless should_publish?(step_result, event_declaration, step_context)
181
+
182
+ # Transform payload
183
+ payload = transform_payload(step_result, event_declaration, step_context)
184
+
185
+ # Build metadata
186
+ base_metadata = {
187
+ publisher: name,
188
+ published_at: Time.now.utc.iso8601
189
+ }
190
+
191
+ # Add step context info if available
192
+ if step_context.respond_to?(:task_uuid)
193
+ base_metadata[:task_uuid] = step_context.task_uuid
194
+ base_metadata[:step_uuid] = step_context.step_uuid
195
+ base_metadata[:step_name] = step_context.step_name if step_context.respond_to?(:step_name)
196
+ base_metadata[:namespace] = step_context.namespace_name if step_context.respond_to?(:namespace_name)
197
+ end
198
+
199
+ metadata = base_metadata.merge(additional_metadata(step_result, event_declaration, step_context))
200
+
201
+ begin
202
+ # Pre-publish hook
203
+ before_publish(event_name, payload, metadata)
204
+
205
+ # Actual publishing is handled by the event router/bridge
206
+ # This method just prepares and validates the event
207
+ # Subclasses can override to perform actual publishing
208
+
209
+ # Post-publish hook
210
+ after_publish(event_name, payload, metadata)
211
+
212
+ true
213
+ rescue StandardError => e
214
+ on_publish_error(event_name, e, payload)
215
+ false
216
+ end
217
+ end
218
+ end
219
+ end
220
+ end