ruby_event_store-process_manager 0.1.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 459fcf11b24e60ad73c40d07162b3580da26c0a219330fa07a14a566274f1bb3
4
+ data.tar.gz: 8d570d1bfa61aa1f6f8d998d1003fcccf2589e46b56f18bc1f956f122915c027
5
+ SHA512:
6
+ metadata.gz: f3dd0d8b9a41458c621dc6bce7de3f01982a321f07c5979a1246d726e9c18ab34330a203566f85d8635288984fc03fb030153d495d2d0b9d38f69a15cff4f685
7
+ data.tar.gz: fbe268a31631a812682d10c26370e0a853dc83b148b2b9ca8097c2fc7b33d70cb7051f01c5f935524394b27b4df049d527fed9a981c457eccd87a2ffe49e1be4
data/README.md ADDED
@@ -0,0 +1,75 @@
1
+ # ruby_event_store-process_manager
2
+
3
+ Build stateful process managers on top of [RubyEventStore](https://railseventstore.org). The state is rebuilt from events before each reaction, so a process can coordinate events from multiple parts of an application without a separate state store.
4
+
5
+ ## Installation
6
+
7
+ Add the gem to your `Gemfile`:
8
+
9
+ ```ruby
10
+ gem "ruby_event_store-process_manager"
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ This process releases an authorized payment when its order expires:
16
+
17
+ ```ruby
18
+ class ReleasePaymentOnOrderExpiration
19
+ include RubyEventStore::ProcessManager.with_state { ProcessState }
20
+
21
+ subscribes_to(
22
+ Payments::PaymentAuthorized,
23
+ Payments::PaymentReleased,
24
+ Pricing::OfferExpired
25
+ )
26
+
27
+ private
28
+
29
+ def fetch_id(event)
30
+ event.data.fetch(:order_id)
31
+ end
32
+
33
+ def apply(event)
34
+ case event
35
+ when Payments::PaymentAuthorized
36
+ state.with(payment_authorized: true)
37
+ when Payments::PaymentReleased
38
+ state.with(payment_authorized: false)
39
+ when Pricing::OfferExpired
40
+ state.with(order_expired: true)
41
+ else
42
+ state
43
+ end
44
+ end
45
+
46
+ def act
47
+ command_bus.call(Payments::ReleasePayment.new(order_id: id)) if state.release?
48
+ end
49
+
50
+ ProcessState = Data.define(:payment_authorized, :order_expired) do
51
+ def initialize(payment_authorized: false, order_expired: false) = super
52
+
53
+ def release?
54
+ payment_authorized && order_expired
55
+ end
56
+ end
57
+ end
58
+ ```
59
+
60
+ Subscribe it to the event store:
61
+
62
+ ```ruby
63
+ process = ReleasePaymentOnOrderExpiration.new(event_store, command_bus)
64
+
65
+ event_store.subscribe(
66
+ process,
67
+ to: ReleasePaymentOnOrderExpiration.subscribed_events
68
+ )
69
+ ```
70
+
71
+ `fetch_id` identifies the process instance, `apply` evolves its state, and `act` issues commands based on the rebuilt state. The state class must support a no-argument constructor. `apply` must always return the next state, including for events that do not change it.
72
+
73
+ ## License
74
+
75
+ MIT
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyEventStore
4
+ module ProcessManager
5
+ module Retry
6
+ def with_retry
7
+ yield
8
+ rescue RubyEventStore::WrongExpectedEventVersion
9
+ yield
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyEventStore
4
+ module ProcessManager
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "process_manager/version"
4
+ require_relative "process_manager/retry"
5
+
6
+ module RubyEventStore
7
+ module ProcessManager
8
+ module ProcessMethods
9
+ def initialize(event_store, command_bus)
10
+ @event_store = event_store
11
+ @command_bus = command_bus
12
+ end
13
+
14
+ def call(event)
15
+ @state = initial_state
16
+ @id = fetch_id(event)
17
+ build_state(event)
18
+ act
19
+ end
20
+
21
+ private
22
+
23
+ attr_reader :event_store, :command_bus, :id
24
+
25
+ def build_state(event)
26
+ with_retry do
27
+ past_events = event_store.read.stream(stream_name).to_a
28
+ last_stored = past_events.size - 1
29
+ event_store.link(event.event_id, stream_name:, expected_version: last_stored)
30
+ (past_events + [event]).each { |ev| @state = apply(ev) }
31
+ end
32
+ end
33
+
34
+ def stream_name
35
+ "#{self.class.name}$#{id}"
36
+ end
37
+ end
38
+
39
+ module Subscriptions
40
+ def self.extended(host_class)
41
+ host_class.instance_variable_set(:@subscribed_events, [])
42
+ end
43
+
44
+ def subscribes_to(*events)
45
+ @subscribed_events += events
46
+ end
47
+
48
+ attr_reader :subscribed_events
49
+ end
50
+
51
+ def self.with_state(&state_class_block)
52
+ unless block_given?
53
+ raise ArgumentError, "A block returning the state class is required."
54
+ end
55
+
56
+ Module.new do
57
+ @state_definition_block = state_class_block
58
+
59
+ define_method(:initial_state) do
60
+ block = self.class.instance_variable_get(:@state_definition_block)
61
+ raise "State definition block not found on #{self.class}" unless block
62
+
63
+ state_class = block.call
64
+ raise "State definition block did not return a Class" unless state_class.is_a?(Class)
65
+
66
+ state_class.new
67
+ end
68
+
69
+ define_method(:state) do
70
+ @state
71
+ end
72
+
73
+ def self.included(host_class)
74
+ host_class.instance_variable_set(:@state_definition_block, @state_definition_block)
75
+
76
+ host_class.include(ProcessMethods)
77
+ host_class.include(Retry)
78
+ host_class.extend(Subscriptions)
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
metadata ADDED
@@ -0,0 +1,67 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ruby_event_store-process_manager
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Arkency
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 2026-07-15 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: ruby_event_store
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: 1.0.0
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: 4.0.0
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: 1.0.0
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: 4.0.0
32
+ email: dev@arkency.com
33
+ executables: []
34
+ extensions: []
35
+ extra_rdoc_files:
36
+ - README.md
37
+ files:
38
+ - README.md
39
+ - lib/ruby_event_store/process_manager.rb
40
+ - lib/ruby_event_store/process_manager/retry.rb
41
+ - lib/ruby_event_store/process_manager/version.rb
42
+ homepage: https://railseventstore.org
43
+ licenses:
44
+ - MIT
45
+ metadata:
46
+ homepage_uri: https://railseventstore.org
47
+ source_code_uri: https://github.com/RailsEventStore/rails_event_store
48
+ bug_tracker_uri: https://github.com/RailsEventStore/rails_event_store/issues
49
+ rubygems_mfa_required: 'true'
50
+ rdoc_options: []
51
+ require_paths:
52
+ - lib
53
+ required_ruby_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: '3.2'
58
+ required_rubygems_version: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ requirements: []
64
+ rubygems_version: 3.6.2
65
+ specification_version: 4
66
+ summary: Stateful process manager with event-sourced state for RubyEventStore
67
+ test_files: []