event_sourcery 1.0.1 → 1.0.2
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 +4 -4
- data/CHANGELOG.md +26 -2
- data/README.md +2 -2
- data/event_sourcery.gemspec +15 -10
- data/lib/event_sourcery/aggregate_root.rb +8 -3
- data/lib/event_sourcery/config.rb +3 -1
- data/lib/event_sourcery/event.rb +33 -17
- data/lib/event_sourcery/event_body_serializer.rb +3 -1
- data/lib/event_sourcery/event_processing/error_handlers/constant_retry.rb +3 -3
- data/lib/event_sourcery/event_processing/error_handlers/exponential_backoff_retry.rb +6 -4
- data/lib/event_sourcery/event_processing/error_handlers/no_retry.rb +2 -2
- data/lib/event_sourcery/event_processing/esp_process.rb +1 -1
- data/lib/event_sourcery/event_processing/esp_runner.rb +5 -5
- data/lib/event_sourcery/event_processing/event_stream_processor.rb +5 -6
- data/lib/event_sourcery/event_store/each_by_range.rb +1 -3
- data/lib/event_sourcery/event_store/event_type_serializers/legacy.rb +2 -2
- data/lib/event_sourcery/event_store/event_type_serializers/underscored.rb +15 -12
- data/lib/event_sourcery/event_store/poll_waiter.rb +5 -3
- data/lib/event_sourcery/event_store/signal_handling_subscription_master.rb +1 -1
- data/lib/event_sourcery/event_store/subscription.rb +5 -3
- data/lib/event_sourcery/memory/config.rb +0 -2
- data/lib/event_sourcery/memory/event_store.rb +21 -18
- data/lib/event_sourcery/memory/projector.rb +0 -1
- data/lib/event_sourcery/memory/tracker.rb +1 -1
- data/lib/event_sourcery/rspec/event_store_shared_examples.rb +19 -20
- data/lib/event_sourcery/version.rb +2 -2
- metadata +34 -17
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 819d971e975a4fb5a0db2f5b6e7f7605af8d65448125b292ec62854fa0024162
|
|
4
|
+
data.tar.gz: 363d610db1c8c11d04bf815acae6447d7367a006f76cf03388f4cde2e6d76d32
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e6c1d51e23d2d11efb4002e0350c5458fcefb3d8cc40a7e4a660d8b8064b7b887d68311615d4913f80af3a104560669f8769f4bfbca4a826e10f5b8fe1303d5d
|
|
7
|
+
data.tar.gz: c9c68fd9366316f52f4774f69aac877e0a69d92280e5f4c297eb6e31e07d5572e075624e0835bb6dedc5a89cfb08e9ac257964f30d2ab20d8627ba679815e306
|
data/CHANGELOG.md
CHANGED
|
@@ -7,11 +7,37 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
[Unreleased]: https://github.com/envato/event_sourcery/compare/v1.0.2...HEAD
|
|
11
|
+
|
|
12
|
+
## [1.0.2] - 2025-12-28
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- Minor fixups in gem metadata ([#240]).
|
|
17
|
+
- Resolve issues as identified by RuboCop ([#241], [#244]).
|
|
18
|
+
- Removed support for older Ruby versions: Ruby 2.6 or greater is now required ([#243]).
|
|
19
|
+
|
|
20
|
+
[1.0.2]: https://github.com/envato/event_sourcery/compare/v1.0.1...v1.0.2
|
|
21
|
+
[#240]: https://github.com/envato/event_sourcery/pull/240
|
|
22
|
+
[#241]: https://github.com/envato/event_sourcery/pull/241
|
|
23
|
+
[#243]: https://github.com/envato/event_sourcery/pull/243
|
|
24
|
+
[#244]: https://github.com/envato/event_sourcery/pull/244
|
|
25
|
+
|
|
26
|
+
## [1.0.1] - 2025-12-27
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- Support Ruby 4.0, make `logger` dependency explicit ([#239])
|
|
31
|
+
|
|
32
|
+
[1.0.1]: https://github.com/envato/event_sourcery/compare/v1.0.0...v1.0.1
|
|
33
|
+
[#239]: https://github.com/envato/event_sourcery/pull/239
|
|
34
|
+
|
|
10
35
|
## [1.0.0] - 2023-11-29
|
|
11
36
|
|
|
12
37
|
### Removed
|
|
13
38
|
- Removed Event.persisted? as it was potentially misleading ([#235])
|
|
14
39
|
|
|
40
|
+
[1.0.0]: https://github.com/envato/event_sourcery/compare/v0.24.0...v1.0.0
|
|
15
41
|
[#235]: https://github.com/envato/event_sourcery/pull/235
|
|
16
42
|
|
|
17
43
|
## [0.24.0] - 2021-11-18
|
|
@@ -261,8 +287,6 @@ moving all Postgres related code into a separate gem.
|
|
|
261
287
|
- EventSourcery no longer depends on Virtus.
|
|
262
288
|
- `Command` and `CommandHandler` have been removed.
|
|
263
289
|
|
|
264
|
-
[Unreleased]: https://github.com/envato/event_sourcery/compare/v0.24.0...HEAD
|
|
265
|
-
[1.0.0]: https://github.com/envato/event_sourcery/compare/v0.24.0...v1.0.0
|
|
266
290
|
[0.24.0]: https://github.com/envato/event_sourcery/compare/v0.23.1...v0.24.0
|
|
267
291
|
[0.23.1]: https://github.com/envato/event_sourcery/compare/v0.23.0...v0.23.1
|
|
268
292
|
[0.23.0]: https://github.com/envato/event_sourcery/compare/v0.22.0...v0.23.0
|
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# EventSourcery
|
|
2
2
|
|
|
3
|
-
[](https://github.com/envato/event_sourcery/actions/workflows/test.yml)
|
|
4
4
|
|
|
5
5
|
A framework for building event sourced, CQRS applications.
|
|
6
6
|
|
|
@@ -34,7 +34,7 @@ A framework for building event sourced, CQRS applications.
|
|
|
34
34
|
|
|
35
35
|
## Development Status
|
|
36
36
|
|
|
37
|
-
[](https://github.com/envato/event_sourcery/actions/workflows/test.yml)
|
|
38
38
|
|
|
39
39
|
Event Sourcery is in production use at [Envato](http://envato.com).
|
|
40
40
|
|
data/event_sourcery.gemspec
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
lib = File.expand_path('../lib', __FILE__)
|
|
1
|
+
lib = File.expand_path('lib', __dir__)
|
|
3
2
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
3
|
require 'event_sourcery/version'
|
|
5
4
|
|
|
@@ -12,24 +11,30 @@ Gem::Specification.new do |spec|
|
|
|
12
11
|
spec.summary = 'Event Sourcing Library'
|
|
13
12
|
spec.description = ''
|
|
14
13
|
spec.homepage = 'https://github.com/envato/event_sourcery'
|
|
14
|
+
spec.license = 'MIT'
|
|
15
15
|
spec.metadata = {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
'allowed_push_host' => 'https://rubygems.org',
|
|
17
|
+
'bug_tracker_uri' => "#{spec.homepage}/issues",
|
|
18
|
+
'changelog_uri' => "#{spec.homepage}/blob/HEAD/CHANGELOG.md",
|
|
19
|
+
'documentation_uri' => "https://www.rubydoc.info/gems/event_sourcery/#{spec.version}",
|
|
20
|
+
'source_code_uri' => "#{spec.homepage}/tree/v#{spec.version}"
|
|
21
|
+
}
|
|
20
22
|
|
|
21
|
-
spec.files
|
|
23
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
|
24
|
+
f.match(%r{^(\.|Gemfile|Rakefile|bin/|script/|spec/)})
|
|
25
|
+
end
|
|
22
26
|
spec.bindir = 'exe'
|
|
23
27
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
24
28
|
spec.require_paths = ['lib']
|
|
25
29
|
|
|
26
|
-
spec.required_ruby_version = '>= 2.
|
|
30
|
+
spec.required_ruby_version = '>= 2.6.0'
|
|
27
31
|
|
|
28
32
|
spec.add_runtime_dependency 'logger'
|
|
29
33
|
|
|
34
|
+
spec.add_development_dependency 'benchmark-ips'
|
|
30
35
|
spec.add_development_dependency 'bundler'
|
|
36
|
+
spec.add_development_dependency 'pry'
|
|
31
37
|
spec.add_development_dependency 'rake', '~> 13'
|
|
32
38
|
spec.add_development_dependency 'rspec'
|
|
33
|
-
spec.add_development_dependency '
|
|
34
|
-
spec.add_development_dependency 'benchmark-ips'
|
|
39
|
+
spec.add_development_dependency 'rubocop', '~> 1'
|
|
35
40
|
end
|
|
@@ -3,7 +3,9 @@ module EventSourcery
|
|
|
3
3
|
# EventSourcery::AggregateRoot provides a foundation for writing your own aggregate root classes.
|
|
4
4
|
# You can use it by including it in your classes, as show in the example code.
|
|
5
5
|
#
|
|
6
|
-
# Excerpt from {https://github.com/envato/event_sourcery/blob/HEAD/docs/core-concepts.md EventSourcery Core Concepts}
|
|
6
|
+
# Excerpt from {https://github.com/envato/event_sourcery/blob/HEAD/docs/core-concepts.md EventSourcery Core Concepts}
|
|
7
|
+
# on Aggregates follows:
|
|
8
|
+
#
|
|
7
9
|
# === Aggregates and Command Handling
|
|
8
10
|
#
|
|
9
11
|
# An aggregate is a cluster of domain objects that can be treated as a single unit.
|
|
@@ -18,7 +20,8 @@ module EventSourcery
|
|
|
18
20
|
# A typical EventSourcery application will have one or more aggregate roots with multiple commands.
|
|
19
21
|
#
|
|
20
22
|
# The following partial example is taken from the EventSourceryTodoApp.
|
|
21
|
-
# Refer a more complete example
|
|
23
|
+
# Refer a more complete example
|
|
24
|
+
# {https://github.com/envato/event_sourcery_todo_app/blob/HEAD/app/aggregates/todo.rb here}.
|
|
22
25
|
#
|
|
23
26
|
# @example
|
|
24
27
|
# module EventSourceryTodoApp
|
|
@@ -85,7 +88,9 @@ module EventSourcery
|
|
|
85
88
|
#
|
|
86
89
|
# @param id [String] ID (a UUID represented as a string) of the aggregate instance to be loaded
|
|
87
90
|
# @param events [Array] Events from which the aggregate's current state will be formed
|
|
88
|
-
# @param on_unknown_event [Proc] Optional. The proc to be run if an unknown
|
|
91
|
+
# @param on_unknown_event [Proc] Optional. The proc to be run if an unknown
|
|
92
|
+
# event type (for which no event handler is registered using
|
|
93
|
+
# {ClassMethods#apply}) is to be loaded.
|
|
89
94
|
def initialize(id, events, on_unknown_event: EventSourcery.config.on_unknown_event)
|
|
90
95
|
@id = id.to_str
|
|
91
96
|
@version = 0
|
|
@@ -76,7 +76,9 @@ module EventSourcery
|
|
|
76
76
|
# ({EventStore::EventBuilder}). By default {EventBodySerializer} will be used.
|
|
77
77
|
# Provide a custom serializer here to change how the event body is serialized.
|
|
78
78
|
def event_body_serializer
|
|
79
|
-
@event_body_serializer ||=
|
|
79
|
+
@event_body_serializer ||=
|
|
80
|
+
EventBodySerializer
|
|
81
|
+
.new
|
|
80
82
|
.add(Hash, EventBodySerializer::HashSerializer)
|
|
81
83
|
.add(Array, EventBodySerializer::ArraySerializer)
|
|
82
84
|
.add(Time, &:iso8601)
|
data/lib/event_sourcery/event.rb
CHANGED
|
@@ -7,9 +7,9 @@ module EventSourcery
|
|
|
7
7
|
#
|
|
8
8
|
# Will return `nil` if called on an instance of {EventSourcery::Event}.
|
|
9
9
|
def self.type
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
return if self == Event
|
|
11
|
+
|
|
12
|
+
EventSourcery.config.event_type_serializer.serialize(self)
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
# Use this method to add "upcasting" to your events.
|
|
@@ -61,19 +61,35 @@ module EventSourcery
|
|
|
61
61
|
# @return [Time] Created at timestamp (in UTC) for the event.
|
|
62
62
|
|
|
63
63
|
# @!attribute [r] correlation_id
|
|
64
|
-
# @return [String] UUID attached to the event that allows reference to a
|
|
64
|
+
# @return [String] UUID attached to the event that allows reference to a
|
|
65
|
+
# particular transaction or event chain. This value is often supplied as
|
|
66
|
+
# part of a command issued by clients.
|
|
65
67
|
|
|
66
68
|
# @!attribute [r] causation_id
|
|
67
69
|
# @return [String] UUID of the event that caused this event.
|
|
68
70
|
|
|
69
71
|
#
|
|
70
|
-
# @param id [Integer] Optional. Unique identifier at the persistent layer.
|
|
71
|
-
#
|
|
72
|
-
#
|
|
72
|
+
# @param id [Integer] Optional. Unique identifier at the persistent layer.
|
|
73
|
+
# By default this will be set by the underlying persistence layer when
|
|
74
|
+
# persisting the event.
|
|
75
|
+
#
|
|
76
|
+
# @param uuid [String] UUID as a string. Optional. Unique identifier for
|
|
77
|
+
# this event. A random UUID will be generated by default.
|
|
78
|
+
#
|
|
79
|
+
# @param aggregate_id [String] UUID as a string. Aggregate instance UUID to
|
|
80
|
+
# which this event belongs to.
|
|
81
|
+
#
|
|
73
82
|
# @param type [Class] Optional. Event type. {Event.type} will be used by default.
|
|
74
|
-
#
|
|
83
|
+
#
|
|
84
|
+
# @param version [String] Optional. Event's aggregate version. Used by some
|
|
85
|
+
# event stores to guard against concurrency errors.
|
|
86
|
+
#
|
|
75
87
|
# @param created_at [Time] Optional. Created at timestamp (in UTC) for the event.
|
|
76
|
-
#
|
|
88
|
+
#
|
|
89
|
+
# @param correlation_id [String] Optional. UUID attached to the event that
|
|
90
|
+
# allows reference to a particular transaction or event chain. This value
|
|
91
|
+
# is often supplied as part of a command issued by clients.
|
|
92
|
+
#
|
|
77
93
|
# @param causation_id [String] Optional. UUID of the event that caused this event.
|
|
78
94
|
def initialize(id: nil,
|
|
79
95
|
uuid: SecureRandom.uuid,
|
|
@@ -143,15 +159,15 @@ module EventSourcery
|
|
|
143
159
|
# @return Hash
|
|
144
160
|
def to_h
|
|
145
161
|
{
|
|
146
|
-
id:
|
|
147
|
-
uuid:
|
|
148
|
-
aggregate_id:
|
|
149
|
-
type:
|
|
150
|
-
body:
|
|
151
|
-
version:
|
|
152
|
-
created_at:
|
|
162
|
+
id: id,
|
|
163
|
+
uuid: uuid,
|
|
164
|
+
aggregate_id: aggregate_id,
|
|
165
|
+
type: type,
|
|
166
|
+
body: body,
|
|
167
|
+
version: version,
|
|
168
|
+
created_at: created_at,
|
|
153
169
|
correlation_id: correlation_id,
|
|
154
|
-
causation_id:
|
|
170
|
+
causation_id: causation_id
|
|
155
171
|
}
|
|
156
172
|
end
|
|
157
173
|
end
|
|
@@ -4,7 +4,9 @@ module EventSourcery
|
|
|
4
4
|
# Serialize the given event body, with the default or the provided, serializer
|
|
5
5
|
#
|
|
6
6
|
# @param event_body event body to be serialized
|
|
7
|
-
# @param serializer Optional. Serializer to be used. By default
|
|
7
|
+
# @param serializer Optional. Serializer to be used. By default
|
|
8
|
+
# {Config#event_body_serializer EventSourcery.config.event_body_serializer}
|
|
9
|
+
# will be used.
|
|
8
10
|
def self.serialize(event_body,
|
|
9
11
|
serializer: EventSourcery.config.event_body_serializer)
|
|
10
12
|
serializer.serialize(event_body)
|
|
@@ -9,7 +9,7 @@ module EventSourcery
|
|
|
9
9
|
#
|
|
10
10
|
# @api private
|
|
11
11
|
DEFAULT_RETRY_INTERVAL = 1
|
|
12
|
-
|
|
12
|
+
|
|
13
13
|
def initialize(processor_name:)
|
|
14
14
|
@processor_name = processor_name
|
|
15
15
|
@retry_interval = DEFAULT_RETRY_INTERVAL
|
|
@@ -18,8 +18,8 @@ module EventSourcery
|
|
|
18
18
|
# Will yield the block and attempt to retry after a defined retry interval {DEFAULT_RETRY_INTERVAL}.
|
|
19
19
|
def with_error_handling
|
|
20
20
|
yield
|
|
21
|
-
rescue =>
|
|
22
|
-
report_error(
|
|
21
|
+
rescue StandardError => e
|
|
22
|
+
report_error(e)
|
|
23
23
|
sleep(@retry_interval)
|
|
24
24
|
|
|
25
25
|
retry
|
|
@@ -23,11 +23,13 @@ module EventSourcery
|
|
|
23
23
|
# Will yield the block and attempt to retry in an exponential backoff.
|
|
24
24
|
def with_error_handling
|
|
25
25
|
yield
|
|
26
|
-
rescue =>
|
|
27
|
-
report_error(
|
|
28
|
-
update_retry_interval(
|
|
26
|
+
rescue StandardError => e
|
|
27
|
+
report_error(e)
|
|
28
|
+
update_retry_interval(e)
|
|
29
29
|
sleep(@retry_interval)
|
|
30
|
-
EventSourcery.logger.info
|
|
30
|
+
EventSourcery.logger.info do
|
|
31
|
+
"Retrying #{@processor_name} with error: #{e.message} at interval=#{@retry_interval}"
|
|
32
|
+
end
|
|
31
33
|
retry
|
|
32
34
|
end
|
|
33
35
|
|
|
@@ -30,9 +30,7 @@ module EventSourcery
|
|
|
30
30
|
record_terminated_processes
|
|
31
31
|
end
|
|
32
32
|
terminate_remaining_processes
|
|
33
|
-
until all_processes_terminated? || waited_long_enough?
|
|
34
|
-
record_terminated_processes
|
|
35
|
-
end
|
|
33
|
+
record_terminated_processes until all_processes_terminated? || waited_long_enough?
|
|
36
34
|
kill_remaining_processes
|
|
37
35
|
record_terminated_processes until all_processes_terminated?
|
|
38
36
|
end
|
|
@@ -43,7 +41,7 @@ module EventSourcery
|
|
|
43
41
|
process = ESPProcess.new(
|
|
44
42
|
event_processor: event_processor,
|
|
45
43
|
event_source: @event_source,
|
|
46
|
-
after_fork: @after_fork
|
|
44
|
+
after_fork: @after_fork
|
|
47
45
|
)
|
|
48
46
|
pid = Process.fork { process.start }
|
|
49
47
|
@pids[pid] = event_processor
|
|
@@ -72,7 +70,7 @@ module EventSourcery
|
|
|
72
70
|
end
|
|
73
71
|
|
|
74
72
|
def listen_for_shutdown_signals
|
|
75
|
-
%i
|
|
73
|
+
%i[TERM INT].each do |signal|
|
|
76
74
|
Signal.trap(signal) { shutdown }
|
|
77
75
|
end
|
|
78
76
|
end
|
|
@@ -81,6 +79,7 @@ module EventSourcery
|
|
|
81
79
|
loop do
|
|
82
80
|
yield
|
|
83
81
|
break if shutdown_requested?
|
|
82
|
+
|
|
84
83
|
sleep(1)
|
|
85
84
|
end
|
|
86
85
|
end
|
|
@@ -109,6 +108,7 @@ module EventSourcery
|
|
|
109
108
|
logger.info("ESPRunner: Process #{event_processor&.processor_name || pid} " \
|
|
110
109
|
"terminated with exit status: #{status.exitstatus.inspect}")
|
|
111
110
|
next unless event_processor
|
|
111
|
+
|
|
112
112
|
@exit_status &&= !!status.success?
|
|
113
113
|
@after_subprocess_termination&.call(processor: event_processor, runner: self, exit_status: status.exitstatus)
|
|
114
114
|
end
|
|
@@ -31,13 +31,12 @@ module EventSourcery
|
|
|
31
31
|
instance_exec(event, &handler)
|
|
32
32
|
end
|
|
33
33
|
@_event = nil
|
|
34
|
-
rescue
|
|
34
|
+
rescue StandardError
|
|
35
35
|
raise EventProcessingError.new(event: event, processor: self)
|
|
36
36
|
end
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
module ClassMethods
|
|
40
|
-
|
|
41
40
|
# @attr_reader processes_event_types [Array] Process Event Types
|
|
42
41
|
# @attr_reader event_handlers [Hash] Hash of handler blocks keyed by event
|
|
43
42
|
# @attr_reader all_event_handler [Proc] An event handler
|
|
@@ -75,9 +74,9 @@ module EventSourcery
|
|
|
75
74
|
if event_classes.empty?
|
|
76
75
|
if @all_event_handler
|
|
77
76
|
raise MultipleCatchAllHandlersDefined, 'Attemping to define multiple catch all event handlers.'
|
|
78
|
-
else
|
|
79
|
-
@all_event_handler = block
|
|
80
77
|
end
|
|
78
|
+
|
|
79
|
+
@all_event_handler = block
|
|
81
80
|
else
|
|
82
81
|
@processes_event_types ||= []
|
|
83
82
|
event_classes.each do |event_class|
|
|
@@ -127,8 +126,8 @@ module EventSourcery
|
|
|
127
126
|
def subscribe_to(event_source, subscription_master: EventStore::SignalHandlingSubscriptionMaster.new)
|
|
128
127
|
setup
|
|
129
128
|
event_source.subscribe(from_id: last_processed_event_id + 1,
|
|
130
|
-
|
|
131
|
-
|
|
129
|
+
event_types: processes_event_types,
|
|
130
|
+
subscription_master: subscription_master) do |events|
|
|
132
131
|
process_events(events, subscription_master)
|
|
133
132
|
end
|
|
134
133
|
end
|
|
@@ -4,11 +4,11 @@ module EventSourcery
|
|
|
4
4
|
# To support legacy implementations. Type is provided when initializing
|
|
5
5
|
# the event, not derived from the class constant
|
|
6
6
|
class Legacy
|
|
7
|
-
def serialize(
|
|
7
|
+
def serialize(_event_class)
|
|
8
8
|
nil
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
-
def deserialize(
|
|
11
|
+
def deserialize(_event_type)
|
|
12
12
|
Event
|
|
13
13
|
end
|
|
14
14
|
end
|
|
@@ -13,32 +13,35 @@ module EventSourcery
|
|
|
13
13
|
def underscore(camel_cased_word)
|
|
14
14
|
word = camel_cased_word.to_s.dup
|
|
15
15
|
word.gsub!(/::/, '/')
|
|
16
|
-
word.gsub!(/([A-Z\d]+)([A-Z][a-z])/,'\1_\2')
|
|
17
|
-
word.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
|
|
18
|
-
word.tr!(
|
|
16
|
+
word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
|
|
17
|
+
word.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
|
|
18
|
+
word.tr!('-', '_')
|
|
19
19
|
word.downcase!
|
|
20
20
|
word
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
def camelize(term, uppercase_first_letter = true)
|
|
24
24
|
string = term.to_s
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
25
|
+
string =
|
|
26
|
+
if uppercase_first_letter
|
|
27
|
+
string.sub(/^[a-z\d]*/) { capitalize(::Regexp.last_match(0)) }
|
|
28
|
+
else
|
|
29
|
+
string.sub(/^(?:(?=\b|[A-Z_])|\w)/) { ::Regexp.last_match(0).downcase }
|
|
30
|
+
end
|
|
31
|
+
string.gsub(%r{(?:_|(/))([a-z\d]*)}i) do
|
|
32
|
+
"#{::Regexp.last_match(1)}#{capitalize(::Regexp.last_match(2))}"
|
|
33
|
+
end.gsub('/', '::')
|
|
31
34
|
end
|
|
32
35
|
|
|
33
36
|
private
|
|
34
37
|
|
|
35
38
|
def capitalize(lower_case_and_underscored_word)
|
|
36
39
|
result = lower_case_and_underscored_word.to_s.dup
|
|
37
|
-
result.gsub!(/_id$/,
|
|
40
|
+
result.gsub!(/_id$/, '')
|
|
38
41
|
result.gsub!(/_/, ' ')
|
|
39
|
-
result.gsub(/([a-z\d]*)/i)
|
|
42
|
+
result.gsub(/([a-z\d]*)/i) do |match|
|
|
40
43
|
"#{match.downcase}"
|
|
41
|
-
|
|
44
|
+
end.gsub(/^\w/) { ::Regexp.last_match(0).upcase }
|
|
42
45
|
end
|
|
43
46
|
end
|
|
44
47
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
module EventSourcery
|
|
2
2
|
module EventStore
|
|
3
|
-
|
|
4
|
-
#
|
|
3
|
+
# This class provides a basic poll waiter implementation that calls the
|
|
4
|
+
# provided block and sleeps for the specified interval, to be used by a
|
|
5
|
+
# {Subscription}.
|
|
5
6
|
class PollWaiter
|
|
6
7
|
#
|
|
7
8
|
# @param interval [Float] Optional. Will default to `0.5`
|
|
@@ -9,7 +10,8 @@ module EventSourcery
|
|
|
9
10
|
@interval = interval
|
|
10
11
|
end
|
|
11
12
|
|
|
12
|
-
# Start polling. Call the provided block and sleep. Repeat until `:stop`
|
|
13
|
+
# Start polling. Call the provided block and sleep. Repeat until `:stop`
|
|
14
|
+
# is thrown (usually via a subscription master).
|
|
13
15
|
#
|
|
14
16
|
# @param block [Proc] code block to be called when polling
|
|
15
17
|
#
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
module EventSourcery
|
|
2
2
|
module EventStore
|
|
3
|
-
|
|
4
3
|
# This allows Event Stream Processors (ESPs) to subscribe to an event store, and be notified when new events are
|
|
5
4
|
# added.
|
|
6
5
|
class Subscription
|
|
@@ -10,14 +9,16 @@ module EventSourcery
|
|
|
10
9
|
# @param from_event_id [Integer] Start reading events from this event ID
|
|
11
10
|
# @param event_types [Array] Optional. If specified, only subscribe to given event types.
|
|
12
11
|
# @param on_new_events [Proc] Code block to be executed when new events are received
|
|
13
|
-
# @param subscription_master A subscription master instance (such as
|
|
12
|
+
# @param subscription_master A subscription master instance (such as
|
|
13
|
+
# {EventStore::SignalHandlingSubscriptionMaster}) which orchestrates a
|
|
14
|
+
# graceful shutdown of the subscription, if one is requested.
|
|
14
15
|
# @param events_table_name [Symbol] Optional. Defaults to `:events`
|
|
15
16
|
def initialize(event_store:,
|
|
16
17
|
poll_waiter:,
|
|
17
18
|
from_event_id:,
|
|
18
|
-
event_types: nil,
|
|
19
19
|
on_new_events:,
|
|
20
20
|
subscription_master:,
|
|
21
|
+
event_types: nil,
|
|
21
22
|
events_table_name: :events,
|
|
22
23
|
batch_size: EventSourcery.config.subscription_batch_size)
|
|
23
24
|
@event_store = event_store
|
|
@@ -51,6 +52,7 @@ module EventSourcery
|
|
|
51
52
|
@subscription_master.shutdown_if_requested
|
|
52
53
|
events = @event_store.get_next_from(@current_event_id + 1, event_types: @event_types, limit: batch_size)
|
|
53
54
|
break if events.empty?
|
|
55
|
+
|
|
54
56
|
EventSourcery.logger.debug { "New events in subscription: #{events.inspect}" }
|
|
55
57
|
@on_new_events.call(events)
|
|
56
58
|
@current_event_id = events.last.id
|
|
@@ -19,7 +19,6 @@ module EventSourcery
|
|
|
19
19
|
def event_sink
|
|
20
20
|
@event_sink ||= ::EventSourcery::EventStore::EventSink.new(event_store)
|
|
21
21
|
end
|
|
22
|
-
|
|
23
22
|
end
|
|
24
23
|
|
|
25
24
|
def self.configure
|
|
@@ -29,6 +28,5 @@ module EventSourcery
|
|
|
29
28
|
def self.config
|
|
30
29
|
@config ||= Config.new
|
|
31
30
|
end
|
|
32
|
-
|
|
33
31
|
end
|
|
34
32
|
end
|
|
@@ -18,16 +18,17 @@ module EventSourcery
|
|
|
18
18
|
# Store given events to the in-memory store
|
|
19
19
|
#
|
|
20
20
|
# @param event_or_events Event(s) to be stored
|
|
21
|
-
# @param expected_version [Optional] Expected version for the aggregate.
|
|
21
|
+
# @param expected_version [Optional] Expected version for the aggregate.
|
|
22
|
+
# This is the version the caller of this method expect the aggregate to
|
|
23
|
+
# be in. If it's different from the expected version a
|
|
24
|
+
# {EventSourcery::ConcurrencyError} will be raised. Defaults to nil.
|
|
22
25
|
# @raise EventSourcery::ConcurrencyError
|
|
23
26
|
# @return Boolean
|
|
24
27
|
def sink(event_or_events, expected_version: nil)
|
|
25
28
|
events = Array(event_or_events)
|
|
26
29
|
ensure_one_aggregate(events)
|
|
27
30
|
|
|
28
|
-
if expected_version && version_for(events.first.aggregate_id) != expected_version
|
|
29
|
-
raise ConcurrencyError
|
|
30
|
-
end
|
|
31
|
+
raise ConcurrencyError if expected_version && version_for(events.first.aggregate_id) != expected_version
|
|
31
32
|
|
|
32
33
|
events.each do |event|
|
|
33
34
|
@events << @event_builder.build(
|
|
@@ -39,7 +40,7 @@ module EventSourcery
|
|
|
39
40
|
created_at: event.created_at || Time.now.utc,
|
|
40
41
|
uuid: event.uuid,
|
|
41
42
|
correlation_id: event.correlation_id,
|
|
42
|
-
causation_id: event.causation_id
|
|
43
|
+
causation_id: event.causation_id
|
|
43
44
|
)
|
|
44
45
|
end
|
|
45
46
|
|
|
@@ -55,11 +56,12 @@ module EventSourcery
|
|
|
55
56
|
# @param limit [Integer] Optional. Number of events to retrieve (starting from the given event ID).
|
|
56
57
|
# @return Array
|
|
57
58
|
def get_next_from(id, event_types: nil, limit: 1000)
|
|
58
|
-
events =
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
59
|
+
events =
|
|
60
|
+
if event_types.nil?
|
|
61
|
+
@events
|
|
62
|
+
else
|
|
63
|
+
@events.select { |e| event_types.include?(e.type) }
|
|
64
|
+
end
|
|
63
65
|
|
|
64
66
|
events.select { |event| event.id >= id }.first(limit)
|
|
65
67
|
end
|
|
@@ -69,11 +71,12 @@ module EventSourcery
|
|
|
69
71
|
# @param event_types [Array] Optional. If supplied, only retrieve events of given type(s).
|
|
70
72
|
# @return Integer
|
|
71
73
|
def latest_event_id(event_types: nil)
|
|
72
|
-
events =
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
74
|
+
events =
|
|
75
|
+
if event_types.nil?
|
|
76
|
+
@events
|
|
77
|
+
else
|
|
78
|
+
@events.select { |e| event_types.include?(e.type) }
|
|
79
|
+
end
|
|
77
80
|
|
|
78
81
|
events.empty? ? 0 : events.last.id
|
|
79
82
|
end
|
|
@@ -108,9 +111,9 @@ module EventSourcery
|
|
|
108
111
|
# @param events [Array] Collection of events
|
|
109
112
|
# @raise AtomicWriteToMultipleAggregatesNotSupported
|
|
110
113
|
def ensure_one_aggregate(events)
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
+
return if events.map(&:aggregate_id).uniq.one?
|
|
115
|
+
|
|
116
|
+
raise AtomicWriteToMultipleAggregatesNotSupported
|
|
114
117
|
end
|
|
115
118
|
|
|
116
119
|
# Adds a listener or listeners to the memory store.
|
|
@@ -51,22 +51,22 @@ RSpec.shared_examples 'an event store' do
|
|
|
51
51
|
end
|
|
52
52
|
|
|
53
53
|
it 'writes multiple events' do
|
|
54
|
-
event_store.sink([ItemAdded.new(aggregate_id: aggregate_id, body: {e: 1}),
|
|
55
|
-
ItemAdded.new(aggregate_id: aggregate_id, body: {e: 2}),
|
|
56
|
-
ItemAdded.new(aggregate_id: aggregate_id, body: {e: 3})])
|
|
54
|
+
event_store.sink([ItemAdded.new(aggregate_id: aggregate_id, body: { e: 1 }),
|
|
55
|
+
ItemAdded.new(aggregate_id: aggregate_id, body: { e: 2 }),
|
|
56
|
+
ItemAdded.new(aggregate_id: aggregate_id, body: { e: 3 })])
|
|
57
57
|
events = event_store.get_next_from(1)
|
|
58
58
|
expect(events.count).to eq 3
|
|
59
59
|
expect(events.map(&:id)).to eq [1, 2, 3]
|
|
60
|
-
expect(events.map(&:body)).to eq [{'e' => 1}, {'e' => 2}, {'e' => 3}]
|
|
60
|
+
expect(events.map(&:body)).to eq [{ 'e' => 1 }, { 'e' => 2 }, { 'e' => 3 }]
|
|
61
61
|
expect(events.map(&:version)).to eq [1, 2, 3]
|
|
62
62
|
end
|
|
63
63
|
|
|
64
64
|
it 'sets the correct aggregates version' do
|
|
65
|
-
event_store.sink([ItemAdded.new(aggregate_id: aggregate_id, body: {e: 1}),
|
|
66
|
-
ItemAdded.new(aggregate_id: aggregate_id, body: {e: 2})])
|
|
65
|
+
event_store.sink([ItemAdded.new(aggregate_id: aggregate_id, body: { e: 1 }),
|
|
66
|
+
ItemAdded.new(aggregate_id: aggregate_id, body: { e: 2 })])
|
|
67
67
|
# this will throw a unique constrain error if the aggregate version was not set correctly ^
|
|
68
|
-
event_store.sink([ItemAdded.new(aggregate_id: aggregate_id, body: {e: 1}),
|
|
69
|
-
ItemAdded.new(aggregate_id: aggregate_id, body: {e: 2})])
|
|
68
|
+
event_store.sink([ItemAdded.new(aggregate_id: aggregate_id, body: { e: 1 }),
|
|
69
|
+
ItemAdded.new(aggregate_id: aggregate_id, body: { e: 2 })])
|
|
70
70
|
events = event_store.get_next_from(1)
|
|
71
71
|
expect(events.count).to eq 4
|
|
72
72
|
expect(events.map(&:id)).to eq [1, 2, 3, 4]
|
|
@@ -104,16 +104,17 @@ RSpec.shared_examples 'an event store' do
|
|
|
104
104
|
|
|
105
105
|
it 'correctly inserts created at times when inserting multiple events atomically' do
|
|
106
106
|
time = Time.parse('2016-10-14T00:00:00.646191Z')
|
|
107
|
-
event_store.sink([ItemAdded.new(aggregate_id: aggregate_id, created_at: nil),
|
|
107
|
+
event_store.sink([ItemAdded.new(aggregate_id: aggregate_id, created_at: nil),
|
|
108
|
+
ItemAdded.new(aggregate_id: aggregate_id, created_at: time)])
|
|
108
109
|
created_ats = event_store.get_next_from(0).map(&:created_at)
|
|
109
110
|
expect(created_ats.map(&:class)).to eq [Time, Time]
|
|
110
111
|
expect(created_ats.last).to eq time
|
|
111
112
|
end
|
|
112
113
|
|
|
113
114
|
it 'raises an error if the events given are for more than one aggregate' do
|
|
114
|
-
expect
|
|
115
|
+
expect do
|
|
115
116
|
event_store.sink([ItemAdded.new(aggregate_id: aggregate_id), ItemAdded.new(aggregate_id: SecureRandom.uuid)])
|
|
116
|
-
|
|
117
|
+
end.to raise_error(EventSourcery::AtomicWriteToMultipleAggregatesNotSupported)
|
|
117
118
|
end
|
|
118
119
|
end
|
|
119
120
|
|
|
@@ -168,7 +169,7 @@ RSpec.shared_examples 'an event store' do
|
|
|
168
169
|
|
|
169
170
|
expect(event_store.latest_event_id(event_types: ['type1'])).to eq 2
|
|
170
171
|
expect(event_store.latest_event_id(event_types: ['type2'])).to eq 3
|
|
171
|
-
expect(event_store.latest_event_id(event_types: [
|
|
172
|
+
expect(event_store.latest_event_id(event_types: %w[type1 type2])).to eq 3
|
|
172
173
|
end
|
|
173
174
|
end
|
|
174
175
|
end
|
|
@@ -207,7 +208,7 @@ RSpec.shared_examples 'an event store' do
|
|
|
207
208
|
|
|
208
209
|
describe '#each_by_range' do
|
|
209
210
|
before do
|
|
210
|
-
|
|
211
|
+
21.times do
|
|
211
212
|
event_store.sink(ItemAdded.new(aggregate_id: aggregate_id, body: {}))
|
|
212
213
|
end
|
|
213
214
|
end
|
|
@@ -255,7 +256,7 @@ RSpec.shared_examples 'an event store' do
|
|
|
255
256
|
def save_event(expected_version: nil)
|
|
256
257
|
event_store.sink(
|
|
257
258
|
BillingDetailsProvided.new(aggregate_id: aggregate_id, body: { my_event: 'data' }),
|
|
258
|
-
expected_version: expected_version
|
|
259
|
+
expected_version: expected_version
|
|
259
260
|
)
|
|
260
261
|
end
|
|
261
262
|
|
|
@@ -278,9 +279,8 @@ RSpec.shared_examples 'an event store' do
|
|
|
278
279
|
|
|
279
280
|
context 'and the expected version is incorrect - 1' do
|
|
280
281
|
it 'raises a ConcurrencyError' do
|
|
281
|
-
expect {
|
|
282
|
-
|
|
283
|
-
}.to raise_error(EventSourcery::ConcurrencyError)
|
|
282
|
+
expect { save_event(expected_version: 1) }
|
|
283
|
+
.to raise_error(EventSourcery::ConcurrencyError)
|
|
284
284
|
end
|
|
285
285
|
end
|
|
286
286
|
|
|
@@ -299,9 +299,8 @@ RSpec.shared_examples 'an event store' do
|
|
|
299
299
|
|
|
300
300
|
context 'with an incorrect expected version - 0' do
|
|
301
301
|
it 'raises a ConcurrencyError' do
|
|
302
|
-
expect {
|
|
303
|
-
|
|
304
|
-
}.to raise_error(EventSourcery::ConcurrencyError)
|
|
302
|
+
expect { save_event(expected_version: 0) }
|
|
303
|
+
.to raise_error(EventSourcery::ConcurrencyError)
|
|
305
304
|
end
|
|
306
305
|
end
|
|
307
306
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: event_sourcery
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.
|
|
4
|
+
version: 1.0.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Envato
|
|
@@ -24,7 +24,7 @@ dependencies:
|
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '0'
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
|
-
name:
|
|
27
|
+
name: benchmark-ips
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
29
29
|
requirements:
|
|
30
30
|
- - ">="
|
|
@@ -38,21 +38,21 @@ dependencies:
|
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
39
|
version: '0'
|
|
40
40
|
- !ruby/object:Gem::Dependency
|
|
41
|
-
name:
|
|
41
|
+
name: bundler
|
|
42
42
|
requirement: !ruby/object:Gem::Requirement
|
|
43
43
|
requirements:
|
|
44
|
-
- - "
|
|
44
|
+
- - ">="
|
|
45
45
|
- !ruby/object:Gem::Version
|
|
46
|
-
version: '
|
|
46
|
+
version: '0'
|
|
47
47
|
type: :development
|
|
48
48
|
prerelease: false
|
|
49
49
|
version_requirements: !ruby/object:Gem::Requirement
|
|
50
50
|
requirements:
|
|
51
|
-
- - "
|
|
51
|
+
- - ">="
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
|
-
version: '
|
|
53
|
+
version: '0'
|
|
54
54
|
- !ruby/object:Gem::Dependency
|
|
55
|
-
name:
|
|
55
|
+
name: pry
|
|
56
56
|
requirement: !ruby/object:Gem::Requirement
|
|
57
57
|
requirements:
|
|
58
58
|
- - ">="
|
|
@@ -66,21 +66,21 @@ dependencies:
|
|
|
66
66
|
- !ruby/object:Gem::Version
|
|
67
67
|
version: '0'
|
|
68
68
|
- !ruby/object:Gem::Dependency
|
|
69
|
-
name:
|
|
69
|
+
name: rake
|
|
70
70
|
requirement: !ruby/object:Gem::Requirement
|
|
71
71
|
requirements:
|
|
72
|
-
- - "
|
|
72
|
+
- - "~>"
|
|
73
73
|
- !ruby/object:Gem::Version
|
|
74
|
-
version: '
|
|
74
|
+
version: '13'
|
|
75
75
|
type: :development
|
|
76
76
|
prerelease: false
|
|
77
77
|
version_requirements: !ruby/object:Gem::Requirement
|
|
78
78
|
requirements:
|
|
79
|
-
- - "
|
|
79
|
+
- - "~>"
|
|
80
80
|
- !ruby/object:Gem::Version
|
|
81
|
-
version: '
|
|
81
|
+
version: '13'
|
|
82
82
|
- !ruby/object:Gem::Dependency
|
|
83
|
-
name:
|
|
83
|
+
name: rspec
|
|
84
84
|
requirement: !ruby/object:Gem::Requirement
|
|
85
85
|
requirements:
|
|
86
86
|
- - ">="
|
|
@@ -93,6 +93,20 @@ dependencies:
|
|
|
93
93
|
- - ">="
|
|
94
94
|
- !ruby/object:Gem::Version
|
|
95
95
|
version: '0'
|
|
96
|
+
- !ruby/object:Gem::Dependency
|
|
97
|
+
name: rubocop
|
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
|
99
|
+
requirements:
|
|
100
|
+
- - "~>"
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: '1'
|
|
103
|
+
type: :development
|
|
104
|
+
prerelease: false
|
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - "~>"
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: '1'
|
|
96
110
|
description: ''
|
|
97
111
|
email:
|
|
98
112
|
- rubygems@envato.com
|
|
@@ -137,11 +151,14 @@ files:
|
|
|
137
151
|
- lib/event_sourcery/rspec/event_store_shared_examples.rb
|
|
138
152
|
- lib/event_sourcery/version.rb
|
|
139
153
|
homepage: https://github.com/envato/event_sourcery
|
|
140
|
-
licenses:
|
|
154
|
+
licenses:
|
|
155
|
+
- MIT
|
|
141
156
|
metadata:
|
|
157
|
+
allowed_push_host: https://rubygems.org
|
|
142
158
|
bug_tracker_uri: https://github.com/envato/event_sourcery/issues
|
|
143
159
|
changelog_uri: https://github.com/envato/event_sourcery/blob/HEAD/CHANGELOG.md
|
|
144
|
-
|
|
160
|
+
documentation_uri: https://www.rubydoc.info/gems/event_sourcery/1.0.2
|
|
161
|
+
source_code_uri: https://github.com/envato/event_sourcery/tree/v1.0.2
|
|
145
162
|
rdoc_options: []
|
|
146
163
|
require_paths:
|
|
147
164
|
- lib
|
|
@@ -149,7 +166,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
149
166
|
requirements:
|
|
150
167
|
- - ">="
|
|
151
168
|
- !ruby/object:Gem::Version
|
|
152
|
-
version: 2.
|
|
169
|
+
version: 2.6.0
|
|
153
170
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
154
171
|
requirements:
|
|
155
172
|
- - ">="
|