mammoth 0.6.0 → 0.7.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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +27 -1
  3. data/README.md +16 -0
  4. data/config/mammoth.example.yml +17 -0
  5. data/config/mammoth.schema.json +56 -1
  6. data/lib/mammoth/application.rb +58 -23
  7. data/lib/mammoth/capabilities.rb +59 -0
  8. data/lib/mammoth/cdc_source.rb +1 -1
  9. data/lib/mammoth/cli.rb +16 -24
  10. data/lib/mammoth/commands/bootstrap_command.rb +26 -0
  11. data/lib/mammoth/commands/dead_letters_command.rb +22 -0
  12. data/lib/mammoth/commands/deliver_sample_command.rb +35 -0
  13. data/lib/mammoth/commands/start_command.rb +28 -0
  14. data/lib/mammoth/commands/status_command.rb +28 -0
  15. data/lib/mammoth/commands/validate_command.rb +24 -0
  16. data/lib/mammoth/configuration.rb +66 -7
  17. data/lib/mammoth/dead_letter_commands.rb +17 -9
  18. data/lib/mammoth/dead_letter_store.rb +4 -0
  19. data/lib/mammoth/delivered_envelope_store.rb +3 -0
  20. data/lib/mammoth/destinations/adapter.rb +20 -0
  21. data/lib/mammoth/destinations/registry.rb +46 -0
  22. data/lib/mammoth/destinations/webhook_adapter.rb +29 -0
  23. data/lib/mammoth/lifecycle_hooks.rb +56 -0
  24. data/lib/mammoth/node_identity.rb +54 -0
  25. data/lib/mammoth/observability_server.rb +2 -0
  26. data/lib/mammoth/operational_state/adapter.rb +37 -0
  27. data/lib/mammoth/operational_state/registry.rb +46 -0
  28. data/lib/mammoth/operational_state/sqlite_adapter.rb +44 -0
  29. data/lib/mammoth/registry.rb +46 -0
  30. data/lib/mammoth/runtimes/adapter.rb +23 -0
  31. data/lib/mammoth/runtimes/concurrent_adapter.rb +31 -0
  32. data/lib/mammoth/runtimes/inline_adapter.rb +40 -0
  33. data/lib/mammoth/runtimes/registry.rb +46 -0
  34. data/lib/mammoth/status.rb +55 -13
  35. data/lib/mammoth/version.rb +1 -1
  36. data/lib/mammoth.rb +28 -3
  37. metadata +21 -1
@@ -5,7 +5,7 @@ require "json-schema"
5
5
  require "yaml"
6
6
 
7
7
  module Mammoth
8
- # Loads and validates Mammoth YAML configuration.
8
+ # Loads and validates Mammoth configuration.
9
9
  #
10
10
  # Configuration is intentionally schema-backed so the same contract can power
11
11
  # editor IntelliSense, preflight validation, and runtime startup checks.
@@ -22,15 +22,27 @@ module Mammoth
22
22
  # @return [Mammoth::Configuration] loaded configuration
23
23
  # @raise [Mammoth::ConfigurationError] when the file is missing or invalid
24
24
  def self.load(path, schema_path: DEFAULT_SCHEMA_PATH)
25
- new(path, schema_path: schema_path).load
25
+ Providers::FileProvider.new(path).load(schema_path: schema_path)
26
+ end
27
+
28
+ # Build and validate a configuration from an in-memory Hash.
29
+ #
30
+ # @param data [Hash] configuration data
31
+ # @param path [String, nil] optional display path/source
32
+ # @param schema_path [String] JSON Schema path
33
+ # @return [Mammoth::Configuration] loaded configuration
34
+ # @raise [Mammoth::ConfigurationError] when the data is invalid
35
+ def self.from_hash(data, path: nil, schema_path: DEFAULT_SCHEMA_PATH)
36
+ Providers::HashProvider.new(data, path: path).load(schema_path: schema_path)
26
37
  end
27
38
 
28
39
  # @param path [String] YAML configuration path
29
40
  # @param schema_path [String] JSON Schema path
30
- def initialize(path, schema_path: DEFAULT_SCHEMA_PATH)
41
+ # @param data [Hash, nil] already parsed configuration data
42
+ def initialize(path, schema_path: DEFAULT_SCHEMA_PATH, data: nil)
31
43
  @path = path
32
44
  @schema_path = schema_path
33
- @data = nil
45
+ @data = data
34
46
  end
35
47
 
36
48
  # Load and validate the configuration file.
@@ -38,9 +50,7 @@ module Mammoth
38
50
  # @return [Mammoth::Configuration] self
39
51
  # @raise [Mammoth::ConfigurationError] when validation fails
40
52
  def load
41
- raise ConfigurationError, "configuration file not found: #{path}" unless File.file?(path)
42
-
43
- @data = YAML.safe_load_file(path, permitted_classes: [], aliases: false)
53
+ @data ||= load_yaml_file
44
54
  raise ConfigurationError, "configuration must be a YAML mapping" unless data.is_a?(Hash)
45
55
 
46
56
  validate_schema!
@@ -60,6 +70,12 @@ module Mammoth
60
70
 
61
71
  private
62
72
 
73
+ def load_yaml_file
74
+ raise ConfigurationError, "configuration file not found: #{path}" unless path && File.file?(path)
75
+
76
+ YAML.safe_load_file(path, permitted_classes: [], aliases: false)
77
+ end
78
+
63
79
  def validate_schema!
64
80
  raise ConfigurationError, "schema file not found: #{schema_path}" unless File.file?(schema_path)
65
81
 
@@ -85,5 +101,48 @@ module Mammoth
85
101
 
86
102
  raise ConfigurationError, "destination names must be unique: #{duplicates.join(", ")}"
87
103
  end
104
+
105
+ # Configuration provider contracts shared by CLI and future control agents.
106
+ module Providers
107
+ # Loads configuration from a YAML file.
108
+ class FileProvider
109
+ attr_reader :path
110
+
111
+ # @param path [String] YAML configuration path
112
+ def initialize(path)
113
+ @path = path
114
+ end
115
+
116
+ # @param schema_path [String] JSON Schema path
117
+ # @return [Mammoth::Configuration]
118
+ def load(schema_path: Configuration::DEFAULT_SCHEMA_PATH)
119
+ Configuration.new(path, schema_path: schema_path).load
120
+ end
121
+ end
122
+
123
+ # Loads configuration from an already parsed Hash.
124
+ class HashProvider
125
+ attr_reader :data, :path
126
+
127
+ # @param data [Hash] configuration data
128
+ # @param path [String, nil] optional source name for diagnostics/status
129
+ def initialize(data, path: nil)
130
+ @data = data
131
+ @path = path || "<hash>"
132
+ end
133
+
134
+ # @param schema_path [String] JSON Schema path
135
+ # @return [Mammoth::Configuration]
136
+ def load(schema_path: Configuration::DEFAULT_SCHEMA_PATH)
137
+ Configuration.new(path, schema_path: schema_path, data: deep_copy(data)).load
138
+ end
139
+
140
+ private
141
+
142
+ def deep_copy(value)
143
+ Marshal.load(Marshal.dump(value))
144
+ end
145
+ end
146
+ end
88
147
  end
89
148
  end
@@ -17,16 +17,18 @@ module Mammoth
17
17
  :metadata
18
18
  )
19
19
 
20
- attr_reader :argv
20
+ attr_reader :argv, :lifecycle_hooks
21
21
 
22
22
  # @param argv [Array<String>] command line arguments
23
- def self.call(argv)
24
- new(argv).call
23
+ def self.call(argv, lifecycle_hooks: LifecycleHooks.new)
24
+ new(argv, lifecycle_hooks: lifecycle_hooks).call
25
25
  end
26
26
 
27
27
  # @param argv [Array<String>] command line arguments
28
- def initialize(argv)
28
+ # @param lifecycle_hooks [Mammoth::LifecycleHooks, Hash] local lifecycle callbacks
29
+ def initialize(argv, lifecycle_hooks: LifecycleHooks.new)
29
30
  @argv = argv
31
+ @lifecycle_hooks = lifecycle_hooks.is_a?(LifecycleHooks) ? lifecycle_hooks : LifecycleHooks.new(lifecycle_hooks)
30
32
  end
31
33
 
32
34
  # Dispatch the nested dead-letter subcommand.
@@ -79,11 +81,13 @@ module Mammoth
79
81
  rows = replay_rows
80
82
  raise ConfigurationError, "no dead letters found to replay" if rows.empty?
81
83
 
84
+ lifecycle_hooks.call(:before_replay, replay_context(rows: rows))
82
85
  rows.each do |row|
83
86
  result = replay_row(row)
84
87
  dead_letter_store.resolve(row.fetch("id")) if replay_resolved?(result)
85
88
  puts replay_message(row, result)
86
89
  end
90
+ lifecycle_hooks.call(:after_replay, replay_context(rows: rows))
87
91
  0
88
92
  end
89
93
 
@@ -144,8 +148,6 @@ module Mammoth
144
148
 
145
149
  raise ConfigurationError, "unexpected argument #{args[index]}\n#{CLI::USAGE}"
146
150
  end
147
- rescue ArgumentError
148
- raise ConfigurationError, "invalid dead letter list option"
149
151
  rescue IndexError
150
152
  raise ConfigurationError, "missing value for dead letter option"
151
153
  end
@@ -188,13 +190,11 @@ module Mammoth
188
190
  end
189
191
 
190
192
  ids.map do |raw_id|
191
- id = Integer(raw_id)
193
+ id = raw_id
192
194
  row = dead_letter_store.fetch(id)
193
195
  raise ConfigurationError, "dead letter not found: #{id}" unless row
194
196
 
195
197
  row
196
- rescue ArgumentError
197
- raise ConfigurationError, "dead letter id must be an integer"
198
198
  end
199
199
  end
200
200
 
@@ -311,6 +311,14 @@ module Mammoth
311
311
  def replay_message(row, result)
312
312
  "Dead letter #{row.fetch("id")}: #{result.fetch(:status)}"
313
313
  end
314
+
315
+ def replay_context(extra = {})
316
+ {
317
+ config: load_config,
318
+ dead_letter_store: dead_letter_store,
319
+ delivery_worker: worker
320
+ }.merge(extra)
321
+ end
314
322
  end
315
323
  # rubocop:enable Metrics/ClassLength
316
324
  end
@@ -100,6 +100,10 @@ module Mammoth
100
100
  database.get_first_value("SELECT COUNT(*) FROM dead_letters#{where}", values)
101
101
  end
102
102
 
103
+ # Count dead letters grouped by destination.
104
+ #
105
+ # @param status [String, nil] optional status filter
106
+ # @return [Array<Hash>] rows with destination_name and count
103
107
  def counts_by_destination(status: nil)
104
108
  where, values = row_filters(status:)
105
109
  database.execute(
@@ -93,6 +93,9 @@ module Mammoth
93
93
  end
94
94
  end
95
95
 
96
+ # Count delivered envelopes grouped by destination.
97
+ #
98
+ # @return [Array<Hash>] rows with destination_name and count
96
99
  def counts_by_destination
97
100
  database.execute(
98
101
  "SELECT destination_name, COUNT(*) AS count FROM delivered_envelopes GROUP BY destination_name"
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mammoth
4
+ # Destination adapter contracts and registry.
5
+ module Destinations
6
+ # Base contract for destination adapters.
7
+ class Adapter
8
+ # @return [Hash] JSON-friendly adapter capabilities
9
+ def self.capabilities
10
+ { type: adapter_type }
11
+ end
12
+
13
+ # @return [String] adapter type name
14
+ def self.adapter_type
15
+ type = name.split("::").last.to_s.delete_suffix("Adapter").downcase
16
+ type.empty? ? "adapter" : type
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mammoth
4
+ module Destinations
5
+ # Registry for destination adapters.
6
+ module Registry
7
+ class << self
8
+ # Register a destination adapter.
9
+ #
10
+ # @param name [String, Symbol] adapter name
11
+ # @param adapter [Object] adapter class
12
+ # @return [Object] registered adapter
13
+ def register(name, adapter)
14
+ registry.register(name, adapter)
15
+ end
16
+
17
+ # Fetch a destination adapter.
18
+ #
19
+ # @param name [String, Symbol] adapter name
20
+ # @return [Object] adapter class
21
+ def fetch(name)
22
+ registry.fetch(name)
23
+ end
24
+
25
+ # Build a destination sink from config.
26
+ #
27
+ # @param destination [Hash] destination config
28
+ # @param label [String] config label for errors
29
+ # @return [Object] delivery sink
30
+ def build(destination, label:)
31
+ fetch(destination.fetch("type", "webhook")).build(destination, label: label)
32
+ end
33
+
34
+ # @return [Array<String>] registered destination adapter names
35
+ def names
36
+ registry.names
37
+ end
38
+
39
+ # @return [Mammoth::Registry] underlying registry
40
+ def registry
41
+ @registry ||= Mammoth::Registry.new("destination")
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mammoth
4
+ module Destinations
5
+ # Built-in webhook destination adapter.
6
+ class WebhookAdapter < Adapter
7
+ class << self
8
+ # @return [String] adapter type name
9
+ def adapter_type
10
+ "webhook"
11
+ end
12
+
13
+ # Build a webhook sink from destination configuration.
14
+ #
15
+ # @param destination [Hash] destination configuration
16
+ # @param label [String] config path label for errors
17
+ # @return [Mammoth::WebhookSink]
18
+ def build(destination, label:)
19
+ WebhookSink.from_destination_config(destination, label: label)
20
+ end
21
+
22
+ # @return [Hash] JSON-friendly capabilities
23
+ def capabilities
24
+ { type: adapter_type, delivery_units: %w[event transaction], signing: true, header_env: true }
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mammoth
4
+ # Executes optional lifecycle callbacks around local Mammoth operations.
5
+ #
6
+ # Hooks are intentionally in-process and explicit. They give extensions and
7
+ # future control agents stable observation points without changing the data
8
+ # plane or introducing remote behavior into OSS.
9
+ class LifecycleHooks
10
+ EVENTS = %i[
11
+ before_start
12
+ after_start
13
+ before_shutdown
14
+ after_shutdown
15
+ before_replay
16
+ after_replay
17
+ ].freeze
18
+ EMPTY_CALLBACKS = [nil].compact.freeze
19
+
20
+ attr_reader :callbacks
21
+
22
+ # @param callbacks [Hash] lifecycle callbacks keyed by event name
23
+ def initialize(callbacks = {})
24
+ @callbacks = normalize(callbacks || {})
25
+ end
26
+
27
+ # @param event [Symbol, String] lifecycle event
28
+ # @param context [Hash] event context
29
+ # @return [void]
30
+ def call(event, context = {})
31
+ event = event.to_sym
32
+ raise ConfigurationError, "unknown lifecycle hook: #{event}" unless EVENTS.include?(event)
33
+
34
+ callback_list = callbacks.fetch(event, EMPTY_CALLBACKS)
35
+ # @type var callback_list: untyped
36
+ Array(callback_list).each do |callback|
37
+ # @type var callback: untyped
38
+ callback.call(context.merge(event: event))
39
+ end
40
+ nil
41
+ end
42
+
43
+ private
44
+
45
+ def normalize(callbacks)
46
+ normalized = {}
47
+ # @type var normalized: Hash[Symbol, untyped]
48
+ callbacks.each_with_object(normalized) do |(event, callback), memo|
49
+ key = event.to_sym
50
+ raise ConfigurationError, "unknown lifecycle hook: #{key}" unless EVENTS.include?(key)
51
+
52
+ memo[key] = Array(callback)
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "socket"
4
+
5
+ module Mammoth
6
+ # Local Mammoth node identity used by status and future control-plane agents.
7
+ class NodeIdentity
8
+ attr_reader :node_id, :node_name, :fleet_id, :environment, :labels, :metadata
9
+
10
+ # @param node_id [String] stable node identifier
11
+ # @param node_name [String] human-readable node name
12
+ # @param fleet_id [String, nil] optional fleet identifier
13
+ # @param environment [String, nil] optional environment name
14
+ # @param labels [Hash] operator-defined labels
15
+ # @param metadata [Hash] operator-defined metadata
16
+ def initialize(node_id:, node_name:, fleet_id: nil, environment: nil, labels: {}, metadata: {})
17
+ @node_id = node_id
18
+ @node_name = node_name
19
+ @fleet_id = fleet_id
20
+ @environment = environment
21
+ @labels = labels || {}
22
+ @metadata = metadata || {}
23
+ end
24
+
25
+ # Build identity from Mammoth configuration.
26
+ #
27
+ # @param config [Mammoth::Configuration] loaded configuration
28
+ # @return [Mammoth::NodeIdentity]
29
+ def self.from_config(config)
30
+ identity = config.data["node"] || {}
31
+ hostname = Socket.gethostname
32
+ new(
33
+ node_id: identity["node_id"] || hostname,
34
+ node_name: identity["node_name"] || config.dig("mammoth", "name") || hostname,
35
+ fleet_id: identity["fleet_id"],
36
+ environment: identity["environment"],
37
+ labels: identity["labels"] || {},
38
+ metadata: identity["metadata"] || {}
39
+ )
40
+ end
41
+
42
+ # @return [Hash] JSON-friendly node identity
43
+ def to_h
44
+ {
45
+ node_id: node_id,
46
+ node_name: node_name,
47
+ fleet_id: fleet_id,
48
+ environment: environment,
49
+ labels: labels,
50
+ metadata: metadata
51
+ }
52
+ end
53
+ end
54
+ end
@@ -10,7 +10,9 @@ module Mammoth
10
10
  # may run it as a sidecar-like process or in a separate process that points at
11
11
  # the same SQLite operational database.
12
12
  class ObservabilityServer
13
+ # Default bind host for the observability server.
13
14
  DEFAULT_HOST = "0.0.0.0"
15
+ # Default TCP port for the observability server.
14
16
  DEFAULT_PORT = 9393
15
17
 
16
18
  attr_reader :config, :host, :port, :sqlite_store, :logger, :server
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mammoth
4
+ # Operational state adapter contracts and registry.
5
+ module OperationalState
6
+ # Base contract for operational state adapters.
7
+ class Adapter
8
+ # @return [void]
9
+ def initialize; end
10
+
11
+ # @return [Mammoth::CheckpointStore]
12
+ def checkpoint_store
13
+ raise NotImplementedError, "#{self.class} must implement #checkpoint_store"
14
+ end
15
+
16
+ # @return [Mammoth::DeadLetterStore]
17
+ def dead_letter_store
18
+ raise NotImplementedError, "#{self.class} must implement #dead_letter_store"
19
+ end
20
+
21
+ # @return [Mammoth::DeliveredEnvelopeStore]
22
+ def delivered_envelope_store
23
+ raise NotImplementedError, "#{self.class} must implement #delivered_envelope_store"
24
+ end
25
+
26
+ # @return [Hash] JSON-friendly state summary
27
+ def summary
28
+ {
29
+ adapter: "unknown",
30
+ checkpoints: checkpoint_store.count,
31
+ dead_letters: dead_letter_store.count,
32
+ delivered_envelopes: delivered_envelope_store.count
33
+ }
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mammoth
4
+ module OperationalState
5
+ # Registry for operational state adapters.
6
+ module Registry
7
+ class << self
8
+ # Register an operational state adapter.
9
+ #
10
+ # @param name [String, Symbol] adapter name
11
+ # @param adapter [Object] adapter class
12
+ # @return [Object] registered adapter
13
+ def register(name, adapter)
14
+ registry.register(name, adapter)
15
+ end
16
+
17
+ # Fetch an operational state adapter.
18
+ #
19
+ # @param name [String, Symbol] adapter name
20
+ # @return [Object] adapter class
21
+ def fetch(name)
22
+ registry.fetch(name)
23
+ end
24
+
25
+ # Build an operational state adapter from config.
26
+ #
27
+ # @param name [String, Symbol] adapter name
28
+ # @param config [Mammoth::Configuration] loaded configuration
29
+ # @return [Mammoth::OperationalState::Adapter] adapter instance
30
+ def build(name, config)
31
+ fetch(name).from_config(config)
32
+ end
33
+
34
+ # @return [Array<String>] registered operational state adapter names
35
+ def names
36
+ registry.names
37
+ end
38
+
39
+ # @return [Mammoth::Registry] underlying registry
40
+ def registry
41
+ @registry ||= Mammoth::Registry.new("operational_state")
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mammoth
4
+ module OperationalState
5
+ # SQLite-backed operational state adapter used by Mammoth OSS.
6
+ class SQLiteAdapter < Adapter
7
+ attr_reader :sqlite_store
8
+
9
+ # @param sqlite_store [Mammoth::SQLiteStore] bootstrapped SQLite store
10
+ def initialize(sqlite_store)
11
+ super()
12
+ @sqlite_store = sqlite_store.bootstrap!
13
+ end
14
+
15
+ # Build a SQLite state adapter from Mammoth configuration.
16
+ #
17
+ # @param config [Mammoth::Configuration] loaded configuration
18
+ # @return [Mammoth::OperationalState::SQLiteAdapter]
19
+ def self.from_config(config)
20
+ new(SQLiteStore.connect(config.dig("sqlite", "path")))
21
+ end
22
+
23
+ # @return [Mammoth::CheckpointStore]
24
+ def checkpoint_store
25
+ @checkpoint_store ||= CheckpointStore.new(sqlite_store)
26
+ end
27
+
28
+ # @return [Mammoth::DeadLetterStore]
29
+ def dead_letter_store
30
+ @dead_letter_store ||= DeadLetterStore.new(sqlite_store)
31
+ end
32
+
33
+ # @return [Mammoth::DeliveredEnvelopeStore]
34
+ def delivered_envelope_store
35
+ @delivered_envelope_store ||= DeliveredEnvelopeStore.new(sqlite_store)
36
+ end
37
+
38
+ # @return [Hash] JSON-friendly SQLite state summary
39
+ def summary
40
+ super.merge(adapter: "sqlite", path: sqlite_store.path, tables: sqlite_store.tables)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mammoth
4
+ # Small explicit registry for built-in and extension-provided adapters.
5
+ class Registry
6
+ attr_reader :namespace, :entries
7
+
8
+ # @param namespace [String] human-readable registry name for errors
9
+ def initialize(namespace)
10
+ @namespace = namespace
11
+ @entries = {}
12
+ end
13
+
14
+ # Register an adapter under a stable name.
15
+ #
16
+ # @param name [String, Symbol] adapter name
17
+ # @param adapter [Object] adapter object or class
18
+ # @return [Object] registered adapter
19
+ def register(name, adapter)
20
+ key = normalize_name(name)
21
+ raise ConfigurationError, "#{namespace} adapter already registered: #{key}" if entries.key?(key)
22
+
23
+ entries[key] = adapter
24
+ end
25
+
26
+ # Fetch a registered adapter.
27
+ #
28
+ # @param name [String, Symbol] adapter name
29
+ # @return [Object] registered adapter
30
+ def fetch(name)
31
+ key = normalize_name(name)
32
+ entries.fetch(key) { raise ConfigurationError, "unknown #{namespace} adapter: #{key}" }
33
+ end
34
+
35
+ # @return [Array<String>] registered adapter names
36
+ def names
37
+ entries.keys.sort
38
+ end
39
+
40
+ private
41
+
42
+ def normalize_name(name)
43
+ name.to_s
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mammoth
4
+ # Delivery runtime adapter contracts and registry.
5
+ module Runtimes
6
+ # Base contract for delivery runtime adapters.
7
+ class Adapter
8
+ # @return [void]
9
+ def initialize; end
10
+
11
+ # @return [Hash] JSON-friendly adapter capabilities
12
+ def self.capabilities
13
+ { type: adapter_type }
14
+ end
15
+
16
+ # @return [String] adapter type name
17
+ def self.adapter_type
18
+ type = name.split("::").last.to_s.delete_suffix("Adapter").downcase
19
+ type.empty? ? "adapter" : type
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mammoth
4
+ module Runtimes
5
+ # Runtime adapter backed by Mammoth's existing cdc-concurrent wrapper.
6
+ class ConcurrentAdapter < Adapter
7
+ class << self
8
+ # @return [String] adapter type name
9
+ def adapter_type
10
+ "concurrent"
11
+ end
12
+
13
+ # Build a concurrent runtime adapter.
14
+ #
15
+ # @param processor [#process] delivery processor
16
+ # @param concurrency [Integer] worker count
17
+ # @param timeout [Numeric, nil] optional timeout
18
+ # @param preserve_order [Boolean] preserve ordering when supported
19
+ # @return [Mammoth::ConcurrentDeliveryRuntime]
20
+ def build(processor:, concurrency:, timeout:, preserve_order:)
21
+ ConcurrentDeliveryRuntime.new(
22
+ processor: processor,
23
+ concurrency: concurrency,
24
+ timeout: timeout,
25
+ preserve_order: preserve_order
26
+ )
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end