cdc-core 0.1.0 → 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.
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CDC
4
+ module Core
5
+ # Canonical ordering scopes used by the CDC ecosystem.
6
+ #
7
+ # Ordering scopes describe how related events should be grouped when a
8
+ # runtime or sink needs to preserve relative order.
9
+ module OrderingScope
10
+ # Preserve exact stream order.
11
+ GLOBAL = :global
12
+ # Preserve transaction order and boundaries.
13
+ TRANSACTION = :transaction
14
+ # Preserve ordering per relation/table.
15
+ RELATION = :relation
16
+ # Preserve ordering per primary key.
17
+ PRIMARY_KEY = :primary_key
18
+ # Do not impose a strict ordering guarantee.
19
+ NONE = :none
20
+
21
+ # All supported ordering scopes.
22
+ SUPPORTED = Ractor.make_shareable([GLOBAL, TRANSACTION, RELATION, PRIMARY_KEY, NONE].freeze)
23
+
24
+ module_function
25
+
26
+ # Convert a scope-like value into a supported ordering scope symbol.
27
+ #
28
+ # @param scope [#to_sym] scope to normalize
29
+ # @return [Symbol] one of SUPPORTED
30
+ # @raise [InvalidOrderingScopeError] when the scope is not supported
31
+ def normalize(scope)
32
+ value = scope.to_sym
33
+ return value if SUPPORTED.include?(value)
34
+
35
+ raise InvalidOrderingScopeError, "unsupported CDC ordering scope: #{scope.inspect}"
36
+ rescue NoMethodError
37
+ raise InvalidOrderingScopeError, "unsupported CDC ordering scope: #{scope.inspect}"
38
+ end
39
+
40
+ # Check whether a scope-like value is supported.
41
+ #
42
+ # @param scope [#to_sym] scope to check
43
+ # @return [Boolean] true when the value normalizes to a supported scope
44
+ def supported?(scope)
45
+ SUPPORTED.include?(scope.to_sym)
46
+ rescue NoMethodError
47
+ false
48
+ end
49
+ end
50
+ end
51
+ end
@@ -10,15 +10,18 @@ module CDC
10
10
  class Pipeline
11
11
  # @return [#process] processor invoked for matching events
12
12
  # @return [Array<Filter>] filters that must all match before processing
13
- attr_reader :processor, :filters
13
+ # @return [Observer] observer notified of dispatch events
14
+ attr_reader :processor, :filters, :observer
14
15
 
15
16
  # Build a pipeline.
16
17
  #
17
18
  # @param processor [#process] processor for matching events
18
19
  # @param filters [Array<Filter>] filters applied before processing
19
- def initialize(processor:, filters: [])
20
+ # @param observer [Observer, nil] instrumentation observer
21
+ def initialize(processor:, filters: [], observer: NullObserver::INSTANCE)
20
22
  @processor = processor
21
23
  @filters = filters.freeze
24
+ @observer = observer || NullObserver::INSTANCE
22
25
  end
23
26
 
24
27
  # Process one event through the pipeline.
@@ -26,11 +29,16 @@ module CDC
26
29
  # @param event [ChangeEvent] event to process
27
30
  # @return [ProcessorResult]
28
31
  def process(event)
32
+ observer.dispatch_started(event)
29
33
  return ProcessorResult.skipped(event, metadata: { reason: 'filtered' }) unless matches?(event)
30
34
 
31
- normalize_result(processor.process(event), event)
35
+ result = normalize_result(processor.process(event), event)
36
+ observe_result(result)
37
+ result
32
38
  rescue StandardError => e
33
- ProcessorResult.failure(e, event:)
39
+ result = ProcessorResult.failure(e, event:, processor: processor.class.name)
40
+ observer.dispatch_failed(result)
41
+ result
34
42
  end
35
43
 
36
44
  # Process many events in order.
@@ -61,6 +69,17 @@ module CDC
61
69
 
62
70
  result ? ProcessorResult.success(event) : ProcessorResult.skipped(event)
63
71
  end
72
+
73
+ def observe_result(result)
74
+ case result.status
75
+ when :success
76
+ observer.dispatch_succeeded(result)
77
+ when :failure
78
+ observer.dispatch_failed(result)
79
+ when :skipped
80
+ observer.dispatch_skipped(result)
81
+ end
82
+ end
64
83
  end
65
84
  end
66
85
  end
@@ -29,6 +29,45 @@ module CDC
29
29
  self.class.ractor_safe?
30
30
  end
31
31
 
32
+ # Start the processor.
33
+ #
34
+ # Runtime layers can call this before dispatch begins. The default
35
+ # implementation is a no-op.
36
+ #
37
+ # @return [self]
38
+ def start
39
+ self
40
+ end
41
+
42
+ # Stop the processor.
43
+ #
44
+ # Runtime layers can call this during shutdown. The default implementation
45
+ # is a no-op.
46
+ #
47
+ # @return [self]
48
+ def stop
49
+ self
50
+ end
51
+
52
+ # Flush any buffered work.
53
+ #
54
+ # Runtime layers can call this before shutdown or checkpoints. The
55
+ # default implementation is a no-op.
56
+ #
57
+ # @return [self]
58
+ def flush
59
+ self
60
+ end
61
+
62
+ # Whether the processor is healthy and ready to accept work.
63
+ #
64
+ # The default implementation assumes the processor is healthy.
65
+ #
66
+ # @return [Boolean]
67
+ def healthy?
68
+ true
69
+ end
70
+
32
71
  # Process one event.
33
72
  #
34
73
  # Subclasses must override this method.
@@ -8,6 +8,9 @@ module CDC
8
8
  # successful processing, skipped events, and failures without relying on
9
9
  # processor-specific return values.
10
10
  class ProcessorResult
11
+ # Allowed result statuses.
12
+ VALID_STATUSES = Ractor.make_shareable(%i[success failure skipped].freeze)
13
+
11
14
  # @return [Symbol] result status
12
15
  # @return [ChangeEvent, nil] event associated with the result
13
16
  # @return [Exception, nil] failure error, when status is :failure
@@ -25,9 +28,24 @@ module CDC
25
28
  #
26
29
  # @param error [Exception] processor error
27
30
  # @param event [ChangeEvent, nil] event being processed
31
+ # @param reason [String, nil] human-readable failure reason
32
+ # @param retryable [Boolean, nil] whether the failure can be retried
33
+ # @param processor [String, nil] processor name associated with the failure
34
+ # @param failed_at [String, nil] timestamp for when the failure occurred
28
35
  # @param metadata [Hash, EventMetadata] result metadata
29
36
  # @return [ProcessorResult]
30
- def self.failure(error, event: nil, metadata: {}) = new(:failure, event:, error:, metadata:)
37
+ def self.failure(error, event: nil, reason: nil, retryable: nil, processor: nil, failed_at: nil,
38
+ metadata: nil)
39
+ base_metadata = metadata.nil? ? EventMetadata.new.to_h : metadata.to_h
40
+ failure_metadata = base_metadata.merge(
41
+ reason: reason || error.message,
42
+ retryable: retryable,
43
+ processor: processor || error.class.name,
44
+ failed_at: failed_at || Time.now.utc.strftime('%Y-%m-%dT%H:%M:%S.%6NZ')
45
+ ).compact
46
+
47
+ new(:failure, event:, error:, metadata: failure_metadata)
48
+ end
31
49
 
32
50
  # Build a skipped result.
33
51
  #
@@ -43,7 +61,7 @@ module CDC
43
61
  # @param error [Exception, nil] associated failure
44
62
  # @param metadata [Hash, EventMetadata] result metadata
45
63
  def initialize(status, event: nil, error: nil, metadata: {})
46
- @status = status.to_sym
64
+ @status = normalize_status(status)
47
65
  @event = event
48
66
  @error = error
49
67
  @metadata = metadata.is_a?(EventMetadata) ? metadata : EventMetadata.new(metadata)
@@ -58,6 +76,82 @@ module CDC
58
76
 
59
77
  # @return [Boolean] true when status is :skipped
60
78
  def skipped? = status == :skipped
79
+
80
+ # Human-readable failure reason, when present.
81
+ #
82
+ # @return [String, nil]
83
+ def failure_reason
84
+ metadata[:reason]
85
+ end
86
+
87
+ # Whether the failure is retryable.
88
+ #
89
+ # @return [Boolean]
90
+ def retryable?
91
+ metadata[:retryable] == true
92
+ end
93
+
94
+ # Name of the processor associated with the failure, when present.
95
+ #
96
+ # @return [String, nil]
97
+ def processor_name
98
+ metadata[:processor]
99
+ end
100
+
101
+ # Timestamp for when the failure occurred, when present.
102
+ #
103
+ # @return [String, nil]
104
+ def failed_at
105
+ metadata[:failed_at]
106
+ end
107
+
108
+ # Error class name, when present.
109
+ #
110
+ # @return [String, nil]
111
+ def error_class
112
+ error&.class&.name
113
+ end
114
+
115
+ # Error message, when present.
116
+ #
117
+ # @return [String, nil]
118
+ def error_message
119
+ error&.message
120
+ end
121
+
122
+ # Error backtrace, when present.
123
+ #
124
+ # @return [Array<String>]
125
+ def error_backtrace
126
+ Array(error&.backtrace)
127
+ end
128
+
129
+ # Convert the result into a shareable hash.
130
+ #
131
+ # @return [Hash{String=>Object,nil}]
132
+ def to_h
133
+ payload = {
134
+ 'status' => status,
135
+ 'event' => event&.to_h,
136
+ 'error_class' => error_class,
137
+ 'error_message' => error_message,
138
+ 'error_backtrace' => error_backtrace,
139
+ 'metadata' => metadata.to_h
140
+ }
141
+
142
+ Ractor.make_shareable(payload.freeze)
143
+ end
144
+
145
+ private
146
+
147
+ def normalize_status(status)
148
+ value = status.to_sym
149
+ return value if VALID_STATUSES.include?(value)
150
+
151
+ raise ArgumentError, "unsupported processor result status: #{status.inspect}"
152
+ rescue NoMethodError
153
+ raise ArgumentError, "unsupported processor result status: #{status.inspect}"
154
+ end
61
155
  end
62
156
  end
63
157
  end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CDC
4
+ module Core
5
+ # Routes CDC work items to the appropriate handler.
6
+ #
7
+ # Router keeps the dispatch vocabulary in core while leaving execution
8
+ # strategy to the caller. It can route single change events, transaction
9
+ # envelopes, and arrays of events.
10
+ class Router
11
+ # @return [#process] handler for individual change events
12
+ # @return [#process, nil] handler for transaction envelopes
13
+ # @return [Observer] observer notified of dispatch events
14
+ attr_reader :processor, :transaction_processor, :observer
15
+
16
+ # Build a router.
17
+ #
18
+ # @param processor [#process] handler for change events
19
+ # @param transaction_processor [#process, nil] handler for transaction envelopes
20
+ # @param observer [Observer, nil] instrumentation observer
21
+ def initialize(processor:, transaction_processor: nil, observer: NullObserver::INSTANCE)
22
+ @processor = processor
23
+ @transaction_processor = transaction_processor
24
+ @observer = observer || NullObserver::INSTANCE
25
+ end
26
+
27
+ # Process a CDC work item.
28
+ #
29
+ # @param item [ChangeEvent, TransactionEnvelope, Array<ChangeEvent>]
30
+ # @return [Object]
31
+ # @raise [UnsupportedWorkItemError] when the item cannot be routed
32
+ def process(item)
33
+ observer.dispatch_started(item)
34
+ case item
35
+ when ChangeEvent
36
+ result = processor.process(item)
37
+ observe_result(result)
38
+ result
39
+ when TransactionEnvelope
40
+ route_transaction(item)
41
+ when Array
42
+ route_many(item)
43
+ else
44
+ raise UnsupportedWorkItemError, "unsupported CDC work item: #{item.class}"
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ # Route a transaction envelope to the configured transaction processor.
51
+ #
52
+ # @param transaction [TransactionEnvelope]
53
+ # @return [Object]
54
+ def route_transaction(transaction)
55
+ return transaction_processor.process(transaction) if transaction_processor
56
+
57
+ raise UnsupportedWorkItemError,
58
+ "unsupported CDC work item: #{transaction.class} (transaction processor not configured)"
59
+ end
60
+
61
+ # Route many change events through the configured processor.
62
+ #
63
+ # @param items [Array]
64
+ # @return [Object]
65
+ def route_many(items)
66
+ unless items.all?(ChangeEvent)
67
+ raise UnsupportedWorkItemError, "unsupported CDC work item: Array(#{items.first.class})"
68
+ end
69
+
70
+ return processor.process_many(items) if processor.respond_to?(:process_many)
71
+
72
+ items.map { |event| processor.process(event) }.freeze
73
+ end
74
+
75
+ def observe_result(result)
76
+ return result unless result.is_a?(ProcessorResult)
77
+
78
+ case result.status
79
+ when :success
80
+ observer.dispatch_succeeded(result)
81
+ when :failure
82
+ observer.dispatch_failed(result)
83
+ when :skipped
84
+ observer.dispatch_skipped(result)
85
+ end
86
+
87
+ result
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CDC
4
+ module Core
5
+ # Abstract base class for source adapters that normalize upstream payloads
6
+ # into CDC core domain objects.
7
+ #
8
+ # SourceAdapter is intentionally narrow. It does not own transport,
9
+ # polling, connection management, worker scheduling, or protocol parsing.
10
+ # It only defines the contract for turning source-specific inputs into
11
+ # {ChangeEvent}, {TransactionEnvelope}, or arrays of those objects.
12
+ #
13
+ # The concrete PostgreSQL implementation currently lives in the pgoutput*
14
+ # family. This class only defines the shared boundary other adapters can
15
+ # implement later.
16
+ class SourceAdapter
17
+ # Normalize one source payload into CDC core objects.
18
+ #
19
+ # Subclasses must override this method.
20
+ #
21
+ # @param _input [Object] source-specific payload
22
+ # @return [ChangeEvent, TransactionEnvelope, Array<ChangeEvent>, Array<TransactionEnvelope>]
23
+ # @raise [NotImplementedError] when not implemented by a subclass
24
+ def normalize(_input)
25
+ raise NotImplementedError, "#{self.class} must implement #normalize"
26
+ end
27
+
28
+ # Normalize many source payloads into CDC core objects.
29
+ #
30
+ # The default implementation maps each input through {#normalize} and
31
+ # flattens one level so adapters can return a single object or a batch of
32
+ # objects for each payload.
33
+ #
34
+ # @param inputs [Enumerable] source-specific payloads
35
+ # @return [Array]
36
+ def normalize_many(inputs)
37
+ Array(inputs).flat_map { |input| normalize(input) }.freeze
38
+ end
39
+ end
40
+ end
41
+ end
@@ -3,6 +3,6 @@
3
3
  module CDC
4
4
  module Core
5
5
  # Current gem version.
6
- VERSION = '0.1.0'
6
+ VERSION = '0.1.1'
7
7
  end
8
8
  end
data/lib/cdc/core.rb CHANGED
@@ -5,12 +5,20 @@ require_relative 'core/errors'
5
5
  require_relative 'core/operation'
6
6
  require_relative 'core/column_change'
7
7
  require_relative 'core/event_metadata'
8
+ require_relative 'core/ordering_scope'
9
+ require_relative 'core/event_position'
10
+ require_relative 'core/ordering_key'
11
+ require_relative 'core/ordering_policy'
8
12
  require_relative 'core/change_event'
9
13
  require_relative 'core/transaction_envelope'
14
+ require_relative 'core/source_adapter'
10
15
  require_relative 'core/processor_result'
11
16
  require_relative 'core/processor'
12
17
  require_relative 'core/composite_processor'
18
+ require_relative 'core/observer'
19
+ require_relative 'core/null_observer'
13
20
  require_relative 'core/filter'
21
+ require_relative 'core/router'
14
22
  require_relative 'core/pipeline'
15
23
 
16
24
  # Top-level namespace for Change Data Capture libraries.
@@ -18,9 +26,10 @@ module CDC
18
26
  # Database-agnostic Change Data Capture domain primitives.
19
27
  #
20
28
  # CDC::Core intentionally contains only lightweight runtime abstractions:
21
- # events, metadata, processors, filters, pipelines, and processor results.
22
- # Transport, PostgreSQL protocol parsing, and value decoding live in sibling
23
- # gems so this layer can remain independently useful.
29
+ # events, metadata, source adapters, processors, filters, pipelines, and
30
+ # processor results. Transport, PostgreSQL protocol parsing, and value
31
+ # decoding live in sibling gems so this layer can remain independently
32
+ # useful.
24
33
  module Core
25
34
  end
26
35
  end
@@ -10,6 +10,8 @@ module CDC
10
10
 
11
11
  @fail_fast: untyped
12
12
 
13
+ @observer: untyped
14
+
13
15
  # @return [Array<Processor>] processors executed for each event
14
16
  # @return [Boolean] whether processing stops on the first failure
15
17
  attr_reader processors: untyped
@@ -18,11 +20,15 @@ module CDC
18
20
  # @return [Boolean] whether processing stops on the first failure
19
21
  attr_reader fail_fast: untyped
20
22
 
23
+ # @return [Observer] observer notified of dispatch events
24
+ attr_reader observer: untyped
25
+
21
26
  # Build a composite processor.
22
27
  #
23
28
  # @param processors [Array<#process>] processors to execute
24
29
  # @param fail_fast [Boolean] whether to stop after the first failure
25
- def initialize: (untyped processors, ?fail_fast: bool) -> void
30
+ # @param observer [Observer, nil] instrumentation observer
31
+ def initialize: (untyped processors, ?fail_fast: bool, ?observer: untyped?) -> void
26
32
 
27
33
  # Process an event through each configured processor.
28
34
  #
@@ -48,6 +54,11 @@ module CDC
48
54
  # @param event [ChangeEvent] processed event
49
55
  # @return [ProcessorResult]
50
56
  def normalize_result: (untyped result, untyped event) -> untyped
57
+
58
+ def collect_results: (untyped event) -> untyped
59
+ def process_with: (untyped processor, untyped event) -> untyped
60
+ def observe_results: (untyped results) -> untyped
61
+ def freeze_results: (untyped results) -> untyped
51
62
  end
52
63
  end
53
64
  end
@@ -8,6 +8,18 @@ module CDC
8
8
  class InvalidOperationError < Error
9
9
  end
10
10
 
11
+ # Raised when an ordering scope cannot be normalized to a supported value.
12
+ class InvalidOrderingScopeError < Error
13
+ end
14
+
15
+ # Raised when an ordering position cannot be normalized to a supported value.
16
+ class InvalidOrderingPositionError < Error
17
+ end
18
+
19
+ # Raised when a router receives an unsupported CDC work item.
20
+ class UnsupportedWorkItemError < Error
21
+ end
22
+
11
23
  # Raised by processors when a processor-specific failure needs wrapping.
12
24
  class ProcessorError < Error
13
25
  end
@@ -0,0 +1,21 @@
1
+ module CDC
2
+ module Core
3
+ # Immutable representation of an event's position metadata.
4
+ class EventPosition
5
+ @strategy: untyped
6
+ @value: untyped
7
+ @transaction_id: untyped
8
+ @sequence_number: untyped
9
+ @occurred_at: untyped
10
+
11
+ attr_reader strategy: untyped
12
+ attr_reader value: untyped
13
+ attr_reader transaction_id: untyped
14
+ attr_reader sequence_number: untyped
15
+ attr_reader occurred_at: untyped
16
+
17
+ def initialize: (strategy: untyped, value: untyped, ?transaction_id: untyped?, ?sequence_number: untyped?, ?occurred_at: untyped?) -> void
18
+ def to_h: () -> untyped
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,8 @@
1
+ module CDC
2
+ module Core
3
+ # No-op observer for callers that do not need instrumentation.
4
+ class NullObserver < Observer
5
+ INSTANCE: untyped
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,25 @@
1
+ module CDC
2
+ module Core
3
+ # Observer interface for CDC runtime instrumentation.
4
+ class Observer
5
+ METRIC_NAMES: untyped
6
+
7
+ def self.metric_tags: (untyped payload) -> untyped
8
+ def self.started_metric_name: () -> String
9
+ def self.succeeded_metric_name: () -> String
10
+ def self.failed_metric_name: () -> String
11
+ def self.skipped_metric_name: () -> String
12
+ private
13
+
14
+ def self.change_event_metric_tags: (untyped event) -> untyped
15
+ def self.transaction_envelope_metric_tags: (untyped transaction) -> untyped
16
+ def self.processor_result_metric_tags: (untyped result) -> untyped
17
+ def self.batch_metric_tags: (untyped batch) -> untyped
18
+
19
+ def dispatch_started: (untyped _event) -> void
20
+ def dispatch_succeeded: (untyped _result) -> void
21
+ def dispatch_failed: (untyped _result) -> void
22
+ def dispatch_skipped: (untyped _result) -> void
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,16 @@
1
+ module CDC
2
+ module Core
3
+ # Immutable grouping key for ordering-related dispatch.
4
+ class OrderingKey
5
+ @scope: untyped
6
+ @components: untyped
7
+
8
+ attr_reader scope: untyped
9
+ attr_reader components: untyped
10
+
11
+ def initialize: (scope: untyped, ?components: ::Hash[untyped, untyped]) -> void
12
+ def empty?: () -> bool
13
+ def to_h: () -> untyped
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,27 @@
1
+ module CDC
2
+ module Core
3
+ # Immutable description of how ordered CDC work should be grouped.
4
+ class OrderingPolicy
5
+ @scope: untyped
6
+ @position: untyped
7
+ @transaction_aware: untyped
8
+
9
+ SUPPORTED_POSITIONS: untyped
10
+
11
+ attr_reader scope: untyped
12
+ attr_reader position: untyped
13
+ attr_reader transaction_aware: untyped
14
+
15
+ def initialize: (scope: untyped, ?position: untyped?, ?transaction_aware: bool) -> void
16
+ def transaction_aware?: () -> bool
17
+ def key_for: (untyped event) -> untyped
18
+ def position_for: (untyped event) -> untyped
19
+ def to_h: () -> untyped
20
+
21
+ private
22
+
23
+ def normalize_position: (untyped position) -> untyped
24
+ def key_components: (untyped event) -> untyped
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,19 @@
1
+ module CDC
2
+ module Core
3
+ # Canonical ordering scopes used by the CDC ecosystem.
4
+ module OrderingScope
5
+ GLOBAL: :global
6
+ TRANSACTION: :transaction
7
+ RELATION: :relation
8
+ PRIMARY_KEY: :primary_key
9
+ NONE: :none
10
+ SUPPORTED: untyped
11
+
12
+ def normalize: (untyped scope) -> untyped
13
+ def supported?: (untyped scope) -> bool
14
+
15
+ def self.normalize: (untyped scope) -> untyped
16
+ def self.supported?: (untyped scope) -> bool
17
+ end
18
+ end
19
+ end
@@ -10,6 +10,8 @@ module CDC
10
10
 
11
11
  @filters: untyped
12
12
 
13
+ @observer: untyped
14
+
13
15
  # @return [#process] processor invoked for matching events
14
16
  # @return [Array<Filter>] filters that must all match before processing
15
17
  attr_reader processor: untyped
@@ -18,11 +20,15 @@ module CDC
18
20
  # @return [Array<Filter>] filters that must all match before processing
19
21
  attr_reader filters: untyped
20
22
 
23
+ # @return [Observer] observer notified of dispatch events
24
+ attr_reader observer: untyped
25
+
21
26
  # Build a pipeline.
22
27
  #
23
28
  # @param processor [#process] processor for matching events
24
29
  # @param filters [Array<Filter>] filters applied before processing
25
- def initialize: (processor: untyped, ?filters: untyped) -> void
30
+ # @param observer [Observer, nil] instrumentation observer
31
+ def initialize: (processor: untyped, ?filters: untyped, ?observer: untyped?) -> void
26
32
 
27
33
  # Process one event through the pipeline.
28
34
  #
@@ -50,6 +56,9 @@ module CDC
50
56
  # @param event [ChangeEvent] processed event
51
57
  # @return [ProcessorResult]
52
58
  def normalize_result: (untyped result, untyped event) -> untyped
59
+
60
+ def dispatch: (untyped event) -> untyped
61
+ def observe_result: (untyped result) -> untyped
53
62
  end
54
63
  end
55
64
  end