mammoth 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mammoth
4
+ # Namespace for Mammoth source adapters.
4
5
  module Sources
5
6
  # Concrete PostgreSQL CDC source for Mammoth.
6
7
  #
@@ -12,6 +13,7 @@ module Mammoth
12
13
  # This class may mention pgoutput implementation details because it is the
13
14
  # concrete PostgreSQL source adapter used by Mammoth. The rest of Mammoth
14
15
  # should remain source-agnostic and consume only the work yielded here.
16
+ # rubocop:disable Metrics/ClassLength
15
17
  class Postgres
16
18
  # @return [Mammoth::Configuration] loaded Mammoth configuration
17
19
  attr_reader :config
@@ -23,6 +25,8 @@ module Mammoth
23
25
  attr_reader :decoder
24
26
  # @return [Object, nil] injected CDC source adapter
25
27
  attr_reader :adapter
28
+ # @return [Mammoth::CheckpointStore, nil] checkpoint store used for restart resume
29
+ attr_reader :checkpoint_store
26
30
 
27
31
  # Build a PostgreSQL CDC source.
28
32
  #
@@ -31,12 +35,14 @@ module Mammoth
31
35
  # @param parser [Object, nil] injected pgoutput parser or relation tracker
32
36
  # @param decoder [Object, nil] injected pgoutput decoder
33
37
  # @param adapter [Object, nil] injected source adapter
34
- def initialize(config, runner: nil, parser: nil, decoder: nil, adapter: nil)
38
+ # @param checkpoint_store [Mammoth::CheckpointStore, nil] persisted checkpoints for restart resume
39
+ def initialize(config, runner: nil, parser: nil, decoder: nil, adapter: nil, checkpoint_store: nil)
35
40
  @config = config
36
41
  @runner = runner
37
42
  @parser = parser
38
43
  @decoder = decoder
39
44
  @adapter = adapter
45
+ @checkpoint_store = checkpoint_store
40
46
  end
41
47
 
42
48
  # Stream CDC::Core-shaped work from PostgreSQL logical replication.
@@ -67,9 +73,39 @@ module Mammoth
67
73
  def process_payload(payload, metadata, &block)
68
74
  parsed = parse_payload(payload)
69
75
  decoded = decode_message(parsed, metadata)
70
- normalize_decoded(decoded).each { |work| block.call(work) if work }
76
+ process_decoded(decoded, metadata, &block)
71
77
  end
72
78
 
79
+ # rubocop:disable Metrics/MethodLength
80
+ def process_decoded(decoded, metadata, &block)
81
+ return if decoded.nil?
82
+
83
+ if decoded.is_a?(Array)
84
+ decoded.each { |item| process_decoded(item, metadata, &block) }
85
+ return
86
+ end
87
+
88
+ if begin_message?(decoded)
89
+ start_transaction_buffer(decoded)
90
+ return
91
+ end
92
+
93
+ if commit_message?(decoded)
94
+ emit_transaction_buffer(decoded, metadata, &block)
95
+ return
96
+ end
97
+
98
+ normalize_decoded(decoded).each do |work|
99
+ work = enrich_work_position(work, metadata, decoded)
100
+ if transaction_buffer_active?
101
+ Array(@transaction_events) << work
102
+ else
103
+ block.call(work)
104
+ end
105
+ end
106
+ end
107
+ # rubocop:enable Metrics/MethodLength
108
+
73
109
  def parse_payload(payload)
74
110
  parser = effective_parser
75
111
  return parser.process(payload) if parser.respond_to?(:process)
@@ -107,20 +143,129 @@ module Mammoth
107
143
  result.is_a?(Array) ? result.compact : [result].compact
108
144
  end
109
145
 
146
+ def begin_message?(decoded)
147
+ message_kind(decoded).include?("begin")
148
+ end
149
+
150
+ def commit_message?(decoded)
151
+ message_kind(decoded).include?("commit")
152
+ end
153
+
154
+ def message_kind(decoded)
155
+ (value_from(decoded, :message_type, :type, :kind) || decoded.class.name.to_s.split("::").last).to_s.downcase
156
+ end
157
+
158
+ def start_transaction_buffer(decoded)
159
+ @transaction_events = []
160
+ @transaction_id = value_from(decoded, :transaction_id, :xid, :final_lsn)
161
+ @transaction_metadata = value_hash(decoded, :metadata) || {}
162
+ end
163
+
164
+ def emit_transaction_buffer(decoded, metadata, &block)
165
+ return unless transaction_buffer_active?
166
+
167
+ block.call(
168
+ TransactionEnvelope.new(
169
+ @transaction_events,
170
+ transaction_id_for(decoded),
171
+ commit_lsn_for(decoded, metadata),
172
+ value_from(decoded, :commit_time, :committed_at, :timestamp),
173
+ @transaction_metadata
174
+ )
175
+ )
176
+ ensure
177
+ clear_transaction_buffer
178
+ end
179
+
180
+ def transaction_id_for(decoded)
181
+ value_from(decoded, :transaction_id, :xid) || @transaction_id || first_event_value(:transaction_id, :xid)
182
+ end
183
+
184
+ def commit_lsn_for(decoded, metadata)
185
+ value_from(decoded, :commit_lsn, :source_position, :lsn, :end_lsn, :final_lsn) ||
186
+ value_from(metadata, :commit_lsn, :source_position, :lsn, :end_lsn, :final_lsn) ||
187
+ first_event_value(:commit_lsn, :source_position, :lsn)
188
+ end
189
+
190
+ def transaction_buffer_active?
191
+ !@transaction_events.nil?
192
+ end
193
+
194
+ def clear_transaction_buffer
195
+ @transaction_events = nil
196
+ @transaction_id = nil
197
+ @transaction_metadata = nil
198
+ end
199
+
200
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
201
+ def enrich_work_position(work, metadata, decoded)
202
+ position = value_from(work, :source_position, :commit_lsn) ||
203
+ value_from(metadata, :source_position, :commit_lsn, :lsn) ||
204
+ value_from(decoded, :source_position, :commit_lsn, :lsn)
205
+ return work unless position
206
+
207
+ work_hash = work.respond_to?(:to_h) ? work.to_h : work
208
+ return work unless work_hash.is_a?(Hash)
209
+
210
+ key_style = work_hash.key?("operation") ? :string : :symbol
211
+ source_position_key = key_style == :string ? "source_position" : :source_position
212
+ commit_lsn_key = key_style == :string ? "commit_lsn" : :commit_lsn
213
+ return work if work_hash[source_position_key] || work_hash[commit_lsn_key]
214
+
215
+ work_hash.merge(source_position_key => position, commit_lsn_key => position)
216
+ end
217
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
218
+
219
+ def first_event_value(*keys)
220
+ event = Array(@transaction_events).find do |event|
221
+ keys.any? { |key| value_from(event, key) }
222
+ end
223
+ value_from(event, *keys) if event
224
+ end
225
+
226
+ def value_from(object, *keys)
227
+ return nil if object.nil?
228
+
229
+ keys.each do |key|
230
+ return object.public_send(key) if object.respond_to?(key)
231
+
232
+ hash = object.respond_to?(:to_h) ? object.to_h : object
233
+ next unless hash.respond_to?(:key?)
234
+
235
+ return hash[key] if hash.key?(key)
236
+ return hash[key.to_s] if hash.key?(key.to_s)
237
+ end
238
+
239
+ nil
240
+ end
241
+
242
+ def value_hash(object, key)
243
+ value = value_from(object, key)
244
+ value.is_a?(Hash) ? value : nil
245
+ end
246
+
110
247
  def effective_runner
111
- runner || (@effective_runner ||= build_runner)
248
+ runner || @effective_runner || begin
249
+ @effective_runner = build_runner
250
+ end
112
251
  end
113
252
 
114
253
  def effective_parser
115
- parser || (@effective_parser ||= build_parser)
254
+ parser || @effective_parser || begin
255
+ @effective_parser = build_parser
256
+ end
116
257
  end
117
258
 
118
259
  def effective_decoder
119
- decoder || (@effective_decoder ||= build_decoder)
260
+ decoder || @effective_decoder || begin
261
+ @effective_decoder = build_decoder
262
+ end
120
263
  end
121
264
 
122
265
  def effective_adapter
123
- adapter || (@effective_adapter ||= build_adapter)
266
+ adapter || @effective_adapter || begin
267
+ @effective_adapter = build_adapter
268
+ end
124
269
  end
125
270
 
126
271
  def build_runner
@@ -152,15 +297,47 @@ module Mammoth
152
297
  database_url: database_url,
153
298
  slot_name: required_config("replication", "slot"),
154
299
  publication_names: required_publications,
155
- start_lsn: config.dig("replication", "start_lsn"),
156
- auto_create_slot: !config.dig("replication", "auto_create_slot").nil?,
157
- temporary_slot: !config.dig("replication", "temporary_slot").nil?
300
+ start_lsn: replication_start_lsn,
301
+ auto_create_slot: config.dig("replication", "auto_create_slot") == true,
302
+ temporary_slot: config.dig("replication", "temporary_slot") == true
158
303
  }.tap do |options|
159
304
  feedback_interval = config.dig("replication", "feedback_interval")
160
305
  options[:feedback_interval] = feedback_interval unless feedback_interval.nil?
161
306
  end
162
307
  end
163
308
 
309
+ def replication_start_lsn
310
+ configured = config.dig("replication", "start_lsn")
311
+ return configured unless blank?(configured)
312
+
313
+ checkpoint_lsn
314
+ end
315
+
316
+ def checkpoint_lsn
317
+ return nil unless checkpoint_store
318
+
319
+ row = checkpoint_store.fetch(source_name: source_name, slot_name: required_config("replication", "slot"))
320
+ normalize_lsn(row&.fetch("last_lsn", nil))
321
+ end
322
+
323
+ def source_name
324
+ required_config("mammoth", "name")
325
+ end
326
+
327
+ def normalize_lsn(value)
328
+ return nil if blank?(value)
329
+
330
+ lsn = value.to_s
331
+ return lsn if lsn.include?("/")
332
+ return "0/#{lsn.to_i.to_s(16).upcase}" if lsn.match?(/\A\d+\z/)
333
+
334
+ lsn
335
+ end
336
+
337
+ def blank?(value)
338
+ value.nil? || value == ""
339
+ end
340
+
164
341
  def required_publications
165
342
  publications = required_config("replication", "publications")
166
343
  unless publications.is_a?(Array) && publications.any? &&
@@ -198,6 +375,9 @@ module Mammoth
198
375
  rescue LoadError => e
199
376
  raise ReplicationError, "#{gem_name} is required for PostgreSQL CDC source integration: #{e.message}"
200
377
  end
378
+ TransactionEnvelope = Data.define(:events, :transaction_id, :commit_lsn, :commit_time, :metadata)
379
+ private_constant :TransactionEnvelope
201
380
  end
381
+ # rubocop:enable Metrics/ClassLength
202
382
  end
203
383
  end
@@ -53,3 +53,27 @@ ON dead_letters(namespace, entity);
53
53
 
54
54
  CREATE INDEX IF NOT EXISTS idx_dead_letters_failed_at
55
55
  ON dead_letters(failed_at);
56
+
57
+
58
+ CREATE TABLE IF NOT EXISTS delivered_envelopes (
59
+ id INTEGER PRIMARY KEY,
60
+ idempotency_key TEXT NOT NULL,
61
+ source_name TEXT NOT NULL,
62
+ slot_name TEXT NOT NULL,
63
+ destination_name TEXT NOT NULL,
64
+ delivery_unit TEXT NOT NULL,
65
+ transaction_id TEXT,
66
+ source_position TEXT,
67
+ delivered_at TEXT NOT NULL,
68
+
69
+ UNIQUE (idempotency_key)
70
+ );
71
+
72
+ CREATE INDEX IF NOT EXISTS idx_delivered_envelopes_source
73
+ ON delivered_envelopes(source_name, slot_name);
74
+
75
+ CREATE INDEX IF NOT EXISTS idx_delivered_envelopes_destination
76
+ ON delivered_envelopes(destination_name);
77
+
78
+ CREATE INDEX IF NOT EXISTS idx_delivered_envelopes_source_position
79
+ ON delivered_envelopes(source_position);
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "securerandom"
5
+ require "time"
6
+
7
+ module Mammoth
8
+ # Serializes CDC transaction envelopes into webhook payloads.
9
+ #
10
+ # Mammoth uses transaction envelopes as the safest delivery and checkpointing
11
+ # boundary for concurrent delivery. A transaction payload preserves the commit
12
+ # position and groups the row-level changes that belong to the same database
13
+ # transaction.
14
+ class TransactionEnvelopeSerializer
15
+ # Default payload type for transaction webhook delivery.
16
+ PAYLOAD_TYPE = "transaction.committed"
17
+
18
+ # Serialize a CDC::Core::TransactionEnvelope-like object into a Hash.
19
+ #
20
+ # @param envelope [#events, #transaction_id] transaction envelope
21
+ # @return [Hash] webhook-ready transaction payload
22
+ def self.call(envelope)
23
+ new(envelope).call
24
+ end
25
+
26
+ # @param envelope [#events, #transaction_id] transaction envelope
27
+ def initialize(envelope)
28
+ @envelope = envelope
29
+ end
30
+
31
+ # Return the webhook payload.
32
+ #
33
+ # @return [Hash] transaction webhook payload
34
+ def call
35
+ event_payloads = envelope.events.map { |event| EventSerializer.call(event) }
36
+ {
37
+ "event_id" => envelope_value("event_id") || SecureRandom.uuid,
38
+ "type" => PAYLOAD_TYPE,
39
+ "source" => first_event_value(event_payloads, "source") || EventSerializer::DEFAULT_SOURCE,
40
+ "transaction_id" => envelope.transaction_id,
41
+ "source_position" => source_position(event_payloads),
42
+ "commit_lsn" => source_position(event_payloads),
43
+ "committed_at" => committed_at,
44
+ "event_count" => event_payloads.length,
45
+ "events" => event_payloads,
46
+ "metadata" => envelope_value("metadata") || {}
47
+ }
48
+ end
49
+
50
+ # Return JSON representation of the transaction payload.
51
+ #
52
+ # @return [String] JSON representation
53
+ def to_json(*_args)
54
+ JSON.generate(call)
55
+ end
56
+
57
+ private
58
+
59
+ attr_reader :envelope
60
+
61
+ def envelope_hash
62
+ @envelope_hash ||= envelope.respond_to?(:to_h) ? stringify_keys(envelope.to_h) : {}
63
+ end
64
+
65
+ def stringify_keys(hash)
66
+ hash.to_h.transform_keys(&:to_s)
67
+ end
68
+
69
+ def envelope_value(key)
70
+ return envelope.public_send(key) if envelope.respond_to?(key)
71
+
72
+ envelope_hash[key]
73
+ end
74
+
75
+ def source_position(event_payloads)
76
+ envelope_value("source_position") || envelope_value("commit_lsn") ||
77
+ first_event_value(event_payloads.reverse, "source_position")
78
+ end
79
+
80
+ def committed_at
81
+ value = envelope_value("committed_at") || envelope_value("commit_time")
82
+ return value.iso8601 if value.respond_to?(:iso8601)
83
+
84
+ value || Time.now.utc.iso8601
85
+ end
86
+
87
+ def first_event_value(event_payloads, key)
88
+ event_payloads.find { |payload| payload[key] }&.fetch(key)
89
+ end
90
+ end
91
+ end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Mammoth
4
4
  # Current Mammoth gem version.
5
- VERSION = "0.1.1"
5
+ VERSION = "0.3.0"
6
6
  end
@@ -2,7 +2,9 @@
2
2
 
3
3
  require "json"
4
4
  require "net/http"
5
+ require "openssl"
5
6
  require "socket"
7
+ require "time"
6
8
  require "uri"
7
9
 
8
10
  module Mammoth
@@ -11,27 +13,74 @@ module Mammoth
11
13
  # HTTP status range treated as successful webhook delivery.
12
14
  SUCCESS_RANGE = 200..299
13
15
 
14
- attr_reader :name, :url, :timeout_seconds
16
+ # Supported webhook signing algorithm.
17
+ SIGNING_ALGORITHM = "hmac_sha256"
18
+ # Prefix added to generated webhook signatures.
19
+ SIGNATURE_PREFIX = "sha256="
20
+
21
+ attr_reader :name, :url, :timeout_seconds, :headers, :signing
15
22
 
16
23
  # @param name [String] destination name
17
24
  # @param url [String] webhook endpoint URL
18
25
  # @param timeout_seconds [Integer] HTTP open/read timeout in seconds
19
- def initialize(name:, url:, timeout_seconds: 5)
26
+ # @param headers [Hash] static HTTP headers applied to every request
27
+ # @param signing [Hash, nil] HMAC signing configuration
28
+ def initialize(name:, url:, timeout_seconds: 5, headers: {}, signing: nil)
20
29
  @name = name
21
30
  @url = URI(url)
22
31
  @timeout_seconds = timeout_seconds
32
+ @headers = headers.transform_keys(&:to_s)
33
+ @signing = signing
23
34
  end
24
35
 
25
- # Build a sink from Mammoth configuration.
26
- #
27
- # @param config [Mammoth::Configuration] loaded configuration
28
- # @return [Mammoth::WebhookSink]
29
- def self.from_config(config)
30
- new(
31
- name: config.dig("webhook", "name"),
32
- url: config.dig("webhook", "url"),
33
- timeout_seconds: config.dig("webhook", "timeout_seconds")
34
- )
36
+ class << self
37
+ # Build a sink from Mammoth configuration.
38
+ #
39
+ # @param config [Mammoth::Configuration] loaded configuration
40
+ # @return [Mammoth::WebhookSink]
41
+ def from_config(config)
42
+ new(
43
+ name: config.dig("webhook", "name"),
44
+ url: config.dig("webhook", "url"),
45
+ timeout_seconds: config.dig("webhook", "timeout_seconds"),
46
+ headers: configured_headers(config),
47
+ signing: configured_signing(config)
48
+ )
49
+ end
50
+
51
+ private
52
+
53
+ def configured_headers(config)
54
+ static_headers = config.dig("webhook", "headers") || {}
55
+ env_headers = config.dig("webhook", "header_env") || {}
56
+
57
+ static_headers.merge(resolve_env_headers(env_headers))
58
+ end
59
+
60
+ def resolve_env_headers(env_headers)
61
+ env_headers.each_with_object(Hash.new) do |(header, env_name), resolved| # rubocop:disable Style/EmptyLiteral
62
+ resolved[header] = ENV.fetch(env_name) do
63
+ raise ConfigurationError, "webhook.header_env.#{header} references missing environment variable #{env_name}"
64
+ end
65
+ end
66
+ end
67
+
68
+ def configured_signing(config)
69
+ signing = config.dig("webhook", "signing")
70
+ return unless signing
71
+
72
+ algorithm = signing.fetch("algorithm", SIGNING_ALGORITHM)
73
+ raise ConfigurationError, "webhook.signing.algorithm must be #{SIGNING_ALGORITHM}" unless algorithm == SIGNING_ALGORITHM
74
+
75
+ secret_env = signing.fetch("secret_env")
76
+ {
77
+ secret: ENV.fetch(secret_env) do
78
+ raise ConfigurationError, "webhook.signing.secret_env references missing environment variable #{secret_env}"
79
+ end,
80
+ signature_header: signing.fetch("signature_header", "X-Mammoth-Signature"),
81
+ timestamp_header: signing.fetch("timestamp_header", "X-Mammoth-Timestamp")
82
+ }
83
+ end
35
84
  end
36
85
 
37
86
  # Deliver an event to the webhook endpoint.
@@ -40,7 +89,21 @@ module Mammoth
40
89
  # @return [Hash] delivery result
41
90
  # @raise [Mammoth::DeliveryError] when delivery fails
42
91
  def deliver(event)
43
- payload = EventSerializer.call(event)
92
+ deliver_payload(EventSerializer.call(event))
93
+ end
94
+
95
+ # Deliver a transaction envelope to the webhook endpoint.
96
+ #
97
+ # @param envelope [#events, #transaction_id] CDC transaction envelope
98
+ # @return [Hash] delivery result
99
+ # @raise [Mammoth::DeliveryError] when delivery fails
100
+ def deliver_transaction(envelope)
101
+ deliver_payload(TransactionEnvelopeSerializer.call(envelope))
102
+ end
103
+
104
+ private
105
+
106
+ def deliver_payload(payload)
44
107
  response = perform_request(payload)
45
108
  return delivery_result(payload, response) if SUCCESS_RANGE.cover?(response.code.to_i)
46
109
 
@@ -49,8 +112,6 @@ module Mammoth
49
112
  raise DeliveryError, "webhook #{name} delivery failed: #{e.message}"
50
113
  end
51
114
 
52
- private
53
-
54
115
  def perform_request(payload)
55
116
  Net::HTTP.start(url.host, url.port, use_ssl: url.scheme == "https", open_timeout: timeout_seconds,
56
117
  read_timeout: timeout_seconds) do |http|
@@ -59,19 +120,36 @@ module Mammoth
59
120
  end
60
121
 
61
122
  def build_request(payload)
123
+ body = JSON.generate(payload)
62
124
  Net::HTTP::Post.new(url.request_uri).tap do |request|
63
125
  request["Content-Type"] = "application/json"
64
- request.body = JSON.generate(payload)
126
+ headers.each { |header, value| request[header] = value }
127
+ apply_signature_headers(request, body)
128
+ request.body = body
65
129
  end
66
130
  end
67
131
 
68
132
  def delivery_result(payload, response)
69
133
  {
70
134
  event_id: payload.fetch("event_id"),
135
+ payload_type: payload["type"] || "event",
71
136
  destination: name,
72
137
  status: "delivered",
73
138
  http_status: response.code.to_i
74
139
  }
75
140
  end
141
+
142
+ def apply_signature_headers(request, body)
143
+ return unless signing
144
+
145
+ timestamp = Time.now.utc.iso8601
146
+ request[signing.fetch(:timestamp_header)] = timestamp
147
+ request[signing.fetch(:signature_header)] = signed_body(timestamp, body)
148
+ end
149
+
150
+ def signed_body(timestamp, body)
151
+ digest = OpenSSL::HMAC.hexdigest("SHA256", signing.fetch(:secret), "#{timestamp}.#{body}")
152
+ "#{SIGNATURE_PREFIX}#{digest}"
153
+ end
76
154
  end
77
155
  end
data/lib/mammoth.rb CHANGED
@@ -7,13 +7,18 @@ require_relative "mammoth/status"
7
7
  require_relative "mammoth/sqlite_store"
8
8
  require_relative "mammoth/checkpoint_store"
9
9
  require_relative "mammoth/dead_letter_store"
10
+ require_relative "mammoth/delivered_envelope_store"
10
11
  require_relative "mammoth/event_serializer"
12
+ require_relative "mammoth/transaction_envelope_serializer"
11
13
  require_relative "mammoth/webhook_sink"
12
14
  require_relative "mammoth/delivery_worker"
15
+ require_relative "mammoth/delivery_processor"
16
+ require_relative "mammoth/concurrent_delivery_runtime"
13
17
  require_relative "mammoth/sources/postgres"
14
18
  require_relative "mammoth/cdc_source"
15
19
  require_relative "mammoth/replication_consumer"
16
20
  require_relative "mammoth/application"
21
+ require_relative "mammoth/dead_letter_commands"
17
22
  require_relative "mammoth/cli"
18
23
 
19
24
  # Mammoth is a self-hosted PostgreSQL event relay.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mammoth
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ken C. Demanawa
@@ -9,6 +9,20 @@ bindir: exe
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: cdc-concurrent
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.1'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '0.1'
12
26
  - !ruby/object:Gem::Dependency
13
27
  name: cdc-core
14
28
  requirement: !ruby/object:Gem::Requirement
@@ -134,8 +148,12 @@ files:
134
148
  - lib/mammoth/cdc_source.rb
135
149
  - lib/mammoth/checkpoint_store.rb
136
150
  - lib/mammoth/cli.rb
151
+ - lib/mammoth/concurrent_delivery_runtime.rb
137
152
  - lib/mammoth/configuration.rb
153
+ - lib/mammoth/dead_letter_commands.rb
138
154
  - lib/mammoth/dead_letter_store.rb
155
+ - lib/mammoth/delivered_envelope_store.rb
156
+ - lib/mammoth/delivery_processor.rb
139
157
  - lib/mammoth/delivery_worker.rb
140
158
  - lib/mammoth/errors.rb
141
159
  - lib/mammoth/event_serializer.rb
@@ -144,6 +162,7 @@ files:
144
162
  - lib/mammoth/sql/__bootstrap__.sql
145
163
  - lib/mammoth/sqlite_store.rb
146
164
  - lib/mammoth/status.rb
165
+ - lib/mammoth/transaction_envelope_serializer.rb
147
166
  - lib/mammoth/version.rb
148
167
  - lib/mammoth/webhook_sink.rb
149
168
  homepage: https://kanutocd.github.io/mammoth/