pg_eventstore 0.3.0 → 0.5.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.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -0
  3. data/CODE_OF_CONDUCT.md +1 -1
  4. data/README.md +2 -0
  5. data/db/migrations/10_create_subscription_commands.sql +15 -0
  6. data/db/migrations/11_create_subscriptions_set_commands.sql +15 -0
  7. data/db/migrations/12_improve_events_indexes.sql +1 -0
  8. data/db/migrations/13_remove_duplicated_index.sql +1 -0
  9. data/db/migrations/9_create_subscriptions.sql +46 -0
  10. data/docs/configuration.md +42 -21
  11. data/docs/linking_events.md +96 -0
  12. data/docs/reading_events.md +56 -0
  13. data/docs/subscriptions.md +170 -0
  14. data/lib/pg_eventstore/callbacks.rb +122 -0
  15. data/lib/pg_eventstore/client.rb +32 -2
  16. data/lib/pg_eventstore/commands/append.rb +3 -11
  17. data/lib/pg_eventstore/commands/event_modifiers/prepare_link_event.rb +22 -0
  18. data/lib/pg_eventstore/commands/event_modifiers/prepare_regular_event.rb +24 -0
  19. data/lib/pg_eventstore/commands/link_to.rb +33 -0
  20. data/lib/pg_eventstore/commands/regular_stream_read_paginated.rb +63 -0
  21. data/lib/pg_eventstore/commands/system_stream_read_paginated.rb +62 -0
  22. data/lib/pg_eventstore/commands.rb +5 -0
  23. data/lib/pg_eventstore/config.rb +35 -3
  24. data/lib/pg_eventstore/errors.rb +80 -0
  25. data/lib/pg_eventstore/{pg_result_deserializer.rb → event_deserializer.rb} +10 -22
  26. data/lib/pg_eventstore/extensions/callbacks_extension.rb +95 -0
  27. data/lib/pg_eventstore/extensions/options_extension.rb +69 -29
  28. data/lib/pg_eventstore/extensions/using_connection_extension.rb +35 -0
  29. data/lib/pg_eventstore/pg_connection.rb +20 -3
  30. data/lib/pg_eventstore/queries/event_queries.rb +18 -34
  31. data/lib/pg_eventstore/queries/event_type_queries.rb +24 -0
  32. data/lib/pg_eventstore/queries/preloader.rb +37 -0
  33. data/lib/pg_eventstore/queries/stream_queries.rb +14 -1
  34. data/lib/pg_eventstore/queries/subscription_command_queries.rb +81 -0
  35. data/lib/pg_eventstore/queries/subscription_queries.rb +166 -0
  36. data/lib/pg_eventstore/queries/subscriptions_set_command_queries.rb +76 -0
  37. data/lib/pg_eventstore/queries/subscriptions_set_queries.rb +89 -0
  38. data/lib/pg_eventstore/queries.rb +7 -0
  39. data/lib/pg_eventstore/query_builders/events_filtering_query.rb +17 -22
  40. data/lib/pg_eventstore/sql_builder.rb +54 -10
  41. data/lib/pg_eventstore/stream.rb +2 -1
  42. data/lib/pg_eventstore/subscriptions/basic_runner.rb +220 -0
  43. data/lib/pg_eventstore/subscriptions/command_handlers/subscription_feeder_commands.rb +52 -0
  44. data/lib/pg_eventstore/subscriptions/command_handlers/subscription_runners_commands.rb +68 -0
  45. data/lib/pg_eventstore/subscriptions/commands_handler.rb +62 -0
  46. data/lib/pg_eventstore/subscriptions/events_processor.rb +72 -0
  47. data/lib/pg_eventstore/subscriptions/runner_state.rb +45 -0
  48. data/lib/pg_eventstore/subscriptions/subscription.rb +141 -0
  49. data/lib/pg_eventstore/subscriptions/subscription_feeder.rb +171 -0
  50. data/lib/pg_eventstore/subscriptions/subscription_handler_performance.rb +39 -0
  51. data/lib/pg_eventstore/subscriptions/subscription_runner.rb +125 -0
  52. data/lib/pg_eventstore/subscriptions/subscription_runners_feeder.rb +38 -0
  53. data/lib/pg_eventstore/subscriptions/subscriptions_manager.rb +105 -0
  54. data/lib/pg_eventstore/subscriptions/subscriptions_set.rb +97 -0
  55. data/lib/pg_eventstore/tasks/setup.rake +5 -1
  56. data/lib/pg_eventstore/utils.rb +66 -0
  57. data/lib/pg_eventstore/version.rb +1 -1
  58. data/lib/pg_eventstore.rb +19 -1
  59. metadata +38 -4
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ # Allows you to define before, around and after callbacks withing the certain action. It is especially useful during
5
+ # asynchronous programming when you need to be able to react on asynchronous actions from the outside.
6
+ # Example:
7
+ # class MyAwesomeClass
8
+ # attr_reader :callbacks
9
+ #
10
+ # def initialize
11
+ # @callbacks = PgEventstore::Callbacks.new
12
+ # end
13
+ #
14
+ # def do_something
15
+ # Thread.new do
16
+ # @callbacks.run_callbacks(:something_happened) do
17
+ # puts "I did something useful!"
18
+ # end
19
+ # end
20
+ # end
21
+ #
22
+ # def do_something_else
23
+ # @callbacks.run_callbacks(:something_else_happened, :foo, bar: :baz) do
24
+ # puts "Something else happened!"
25
+ # end
26
+ # end
27
+ # end
28
+ #
29
+ # obj = MyAwesomeClass.new
30
+ # obj.callbacks.define_callback(:something_happened, :before, proc { puts "In before callback" })
31
+ # obj.callbacks.define_callback(:something_happened, :after, proc { puts "In after callback" })
32
+ # obj.callbacks.define_callback(
33
+ # :something_happened, :around,
34
+ # proc { |action| puts "In around before action"; action.call; puts "In around after action" }
35
+ # )
36
+ # obj.do_something
37
+ # Outputs:
38
+ # In before callback
39
+ # In around callback before action
40
+ # I did something useful!
41
+ # In around callback after action
42
+ # In after callback
43
+ # Please note, that it is important to call *action.call* in around callback. Otherwise action simply won't be called.
44
+ #
45
+ # Optionally you can provide any set of arguments to {#run_callbacks} method. They will be passed to your callbacks
46
+ # functions then. Example:
47
+ #
48
+ # obj = MyAwesomeClass.new
49
+ # obj.callbacks.define_callback(
50
+ # :something_else_happened, :before,
51
+ # proc { |*args, **kwargs| puts "In before callback. Args: #{args}, kwargs: #{kwargs}." }
52
+ # )
53
+ # obj.callbacks.define_callback(
54
+ # :something_else_happened, :after,
55
+ # proc { |*args, **kwargs| puts "In after callback. Args: #{args}, kwargs: #{kwargs}." }
56
+ # )
57
+ # obj.callbacks.define_callback(
58
+ # :something_else_happened, :around,
59
+ # proc { |action, *args, **kwargs|
60
+ # puts "In around before action. Args: #{args}, kwargs: #{kwargs}."
61
+ # action.call
62
+ # puts "In around after action. Args: #{args}, kwargs: #{kwargs}."
63
+ # }
64
+ # )
65
+ # obj.do_something_else
66
+ # Outputs:
67
+ # In before callback. Args: [:foo], kwargs: {:bar=>:baz}.
68
+ # In around before action. Args: [:foo], kwargs: {:bar=>:baz}.
69
+ # I did something useful!
70
+ # In around after action. Args: [:foo], kwargs: {:bar=>:baz}.
71
+ # In after callback. Args: [:foo], kwargs: {:bar=>:baz}.
72
+ class Callbacks
73
+ def initialize
74
+ @callbacks = {}
75
+ end
76
+
77
+ # @param action [Object] an object, that represents your action. In most cases you want to use a symbol there
78
+ # @param filter [Symbol] callback filter. Supported values are :before, :after and :around
79
+ # @param callback [#call]
80
+ # @return [void]
81
+ def define_callback(action, filter, callback)
82
+ @callbacks[action] ||= {}
83
+ @callbacks[action][filter] ||= []
84
+ @callbacks[action][filter].push(callback)
85
+ end
86
+
87
+ # @param action [Object] an action to run
88
+ # @return [Object] the result of passed block
89
+ def run_callbacks(action, *args, **kwargs, &blk)
90
+ return (yield if block_given?) unless @callbacks[action]
91
+
92
+ run_before_callbacks(action, *args, **kwargs)
93
+ result = run_around_callbacks(action, *args, **kwargs, &blk)
94
+ run_after_callbacks(action, *args, **kwargs)
95
+ result
96
+ end
97
+
98
+ private
99
+
100
+ def run_before_callbacks(action, *args, **kwargs)
101
+ @callbacks[action][:before]&.each do |callback|
102
+ callback.call(*args, **kwargs)
103
+ end
104
+ end
105
+
106
+ def run_around_callbacks(action, *args, **kwargs, &blk)
107
+ result = nil
108
+ stack = [proc { result = yield if block_given? }]
109
+ @callbacks[action][:around]&.reverse_each&.with_index do |callback, index|
110
+ stack.push(proc { callback.call(stack[index], *args, **kwargs); result })
111
+ end
112
+ stack.last.call(*args, **kwargs)
113
+ result
114
+ end
115
+
116
+ def run_after_callbacks(action, *args, **kwargs)
117
+ @callbacks[action][:after]&.each do |callback|
118
+ callback.call(*args, **kwargs)
119
+ end
120
+ end
121
+ end
122
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  require_relative 'commands'
4
4
  require_relative 'event_serializer'
5
- require_relative 'pg_result_deserializer'
5
+ require_relative 'event_deserializer'
6
6
  require_relative 'queries'
7
7
 
8
8
  module PgEventstore
@@ -109,6 +109,36 @@ module PgEventstore
109
109
  call(stream, options: { max_count: config.max_count }.merge(options))
110
110
  end
111
111
 
112
+ # @see {#read} for available params
113
+ # @return [Enumerator] enumerator will yield PgEventstore::Event
114
+ def read_paginated(stream, options: {}, middlewares: nil)
115
+ cmd_class = stream.system? ? Commands::SystemStreamReadPaginated : Commands::RegularStreamReadPaginated
116
+ cmd_class.
117
+ new(Queries.new(streams: stream_queries, events: event_queries(middlewares(middlewares)))).
118
+ call(stream, options: { max_count: config.max_count }.merge(options))
119
+ end
120
+
121
+ # Links event from one stream into another stream. You can later access it by providing :resolve_link_tos option
122
+ # when reading from a stream. Only existing events can be linked.
123
+ # @param stream [PgEventstore::Stream]
124
+ # @param events_or_event [PgEventstore::Event, Array<PgEventstore::Event>]
125
+ # @param options [Hash]
126
+ # @option options [Integer] :expected_revision provide your own revision number
127
+ # @option options [Symbol] :expected_revision provide one of next values: :any, :no_stream or :stream_exists
128
+ # @param middlewares [Array] provide a list of middleware names to use. Defaults to empty array, meaning no
129
+ # middlewares will be applied to the "link" event
130
+ # @return [PgEventstore::Event, Array<PgEventstore::Event>] persisted event(s)
131
+ # @raise [PgEventstore::WrongExpectedRevisionError]
132
+ def link_to(stream, events_or_event, options: {}, middlewares: [])
133
+ result =
134
+ Commands::LinkTo.new(
135
+ Queries.new(
136
+ streams: stream_queries, events: event_queries(middlewares(middlewares)), transactions: transaction_queries
137
+ )
138
+ ).call(stream, *events_or_event, options: options)
139
+ events_or_event.is_a?(Array) ? result : result.first
140
+ end
141
+
112
142
  private
113
143
 
114
144
  # @param middlewares [Array, nil]
@@ -140,7 +170,7 @@ module PgEventstore
140
170
  EventQueries.new(
141
171
  connection,
142
172
  EventSerializer.new(middlewares),
143
- PgResultDeserializer.new(middlewares, config.event_class_resolver)
173
+ EventDeserializer.new(middlewares, config.event_class_resolver)
144
174
  )
145
175
  end
146
176
  end
@@ -9,9 +9,10 @@ module PgEventstore
9
9
  # @param options [Hash]
10
10
  # @option options [Integer] :expected_revision provide your own revision number
11
11
  # @option options [Symbol] :expected_revision provide one of next values: :any, :no_stream or :stream_exists
12
+ # @param event_modifier [#call]
12
13
  # @return [Array<PgEventstore::Event>] persisted events
13
14
  # @raise [PgEventstore::WrongExpectedRevisionError]
14
- def call(stream, *events, options: {})
15
+ def call(stream, *events, options: {}, event_modifier: EventModifiers::PrepareRegularEvent)
15
16
  raise SystemStreamError, stream if stream.system?
16
17
 
17
18
  queries.transactions.transaction do
@@ -19,7 +20,7 @@ module PgEventstore
19
20
  revision = stream.stream_revision
20
21
  assert_expected_revision!(revision, options[:expected_revision]) if options[:expected_revision]
21
22
  events.map.with_index(1) do |event, index|
22
- queries.events.insert(stream, prepared_event(event, revision + index))
23
+ queries.events.insert(stream, event_modifier.call(event, revision + index))
23
24
  end.tap do
24
25
  queries.streams.update_stream_revision(stream, revision + events.size)
25
26
  end
@@ -28,15 +29,6 @@ module PgEventstore
28
29
 
29
30
  private
30
31
 
31
- # @param event [PgEventstore::Event]
32
- # @param revision [Integer]
33
- # @return [PgEventstore::Event]
34
- def prepared_event(event, revision)
35
- event.class.new(
36
- id: event.id, data: event.data, metadata: event.metadata, type: event.type, stream_revision: revision
37
- )
38
- end
39
-
40
32
  # @param revision [Integer]
41
33
  # @param expected_revision [Symbol, Integer]
42
34
  # @raise [PgEventstore::WrongExpectedRevisionError] in case if revision does not satisfy expected revision
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ module Commands
5
+ module EventModifiers
6
+ # Defines how to transform regular event into a link event
7
+ # @!visibility private
8
+ class PrepareLinkEvent
9
+ class << self
10
+ # @param event [PgEventstore::Event]
11
+ # @param revision [Integer]
12
+ # @return [PgEventstore::Event]
13
+ def call(event, revision)
14
+ Event.new(link_id: event.id, type: Event::LINK_TYPE, stream_revision: revision).tap do |e|
15
+ %i[link_id type stream_revision].each { |attr| e.readonly!(attr) }
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ module Commands
5
+ module EventModifiers
6
+ # Defines how to transform regular event before appending it to the stream
7
+ # @!visibility private
8
+ class PrepareRegularEvent
9
+ class << self
10
+ # @param event [PgEventstore::Event]
11
+ # @param revision [Integer]
12
+ # @return [PgEventstore::Event]
13
+ def call(event, revision)
14
+ event.class.new(
15
+ id: event.id, data: event.data, metadata: event.metadata, type: event.type, stream_revision: revision
16
+ ).tap do |e|
17
+ %i[link_id stream_revision].each { |attr| e.readonly!(attr) }
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ module Commands
5
+ # @!visibility private
6
+ class LinkTo < AbstractCommand
7
+ # @param stream [PgEventstore::Stream]
8
+ # @param events [Array<PgEventstore::Event>]
9
+ # @param options [Hash]
10
+ # @option options [Integer] :expected_revision provide your own revision number
11
+ # @option options [Symbol] :expected_revision provide one of next values: :any, :no_stream or :stream_exists
12
+ # @return [Array<PgEventstore::Event>] persisted events
13
+ # @raise [PgEventstore::WrongExpectedRevisionError]
14
+ # @raise [PgEventstore::NotPersistedEventError]
15
+ def call(stream, *events, options: {})
16
+ events.each(&method(:check_id_presence))
17
+ append_cmd = Append.new(queries)
18
+ append_cmd.call(stream, *events, options: options, event_modifier: EventModifiers::PrepareLinkEvent)
19
+ end
20
+
21
+ private
22
+
23
+ # Checks if Event#id is present. An event must have the #id value in order to be linked.
24
+ # @param event [PgEventstore::Event]
25
+ # @return [void]
26
+ def check_id_presence(event)
27
+ return unless event.id.nil?
28
+
29
+ raise NotPersistedEventError, event
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ module Commands
5
+ # @!visibility private
6
+ class RegularStreamReadPaginated < AbstractCommand
7
+ # @see PgEventstore::Commands::Read for docs
8
+ def call(stream, options: {})
9
+ revision = calc_initial_revision(stream, options)
10
+ Enumerator.new do |yielder|
11
+ loop do
12
+ events = read_cmd.call(stream, options: options.merge(from_revision: revision))
13
+ yielder << events if events.any?
14
+ raise StopIteration if end_reached?(events, options[:max_count])
15
+
16
+ revision = calc_next_revision(events, revision, options[:direction])
17
+ raise StopIteration if revision.negative?
18
+ end
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ # @param stream [PgEventstore::Stream]
25
+ # @param options [Hash]
26
+ # @return [Integer]
27
+ def calc_initial_revision(stream, options)
28
+ return options[:from_revision] if options[:from_revision]
29
+ return 0 if forwards?(options[:direction])
30
+
31
+ read_cmd.call(stream, options: options.merge(max_count: 1)).first.stream_revision
32
+ end
33
+
34
+ # @param events [Array<PgEventstore::Event>]
35
+ # @param max_count [Integer]
36
+ # @return [Boolean]
37
+ def end_reached?(events, max_count)
38
+ events.size < max_count
39
+ end
40
+
41
+ # @param events [Array<PgEventstore::Event>]
42
+ # @param revision [Integer]
43
+ # @param direction [String, Symbol, nil]
44
+ # @return [Integer]
45
+ def calc_next_revision(events, revision, direction)
46
+ return revision + events.size if forwards?(direction)
47
+
48
+ revision - events.size
49
+ end
50
+
51
+ # @param direction [String, Symbol, nil]
52
+ # @return [Boolean]
53
+ def forwards?(direction)
54
+ QueryBuilders::EventsFiltering::SQL_DIRECTIONS[direction] == QueryBuilders::EventsFiltering::SQL_DIRECTIONS[:asc]
55
+ end
56
+
57
+ # @return [PgEventstore::Commands::Read]
58
+ def read_cmd
59
+ @read_cmd ||= Read.new(queries)
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ module Commands
5
+ # @!visibility private
6
+ class SystemStreamReadPaginated < AbstractCommand
7
+ # @see PgEventstore::Commands::Read for docs
8
+ def call(stream, options: {})
9
+ position = calc_initial_position(stream, options)
10
+ Enumerator.new do |yielder|
11
+ loop do
12
+ events = read_cmd.call(stream, options: options.merge(from_position: position))
13
+ yielder << events if events.any?
14
+ raise StopIteration if end_reached?(events, options[:max_count])
15
+
16
+ position = calc_next_position(events, options[:direction])
17
+ raise StopIteration if position <= 0
18
+ end
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ # @param stream [PgEventstore::Stream]
25
+ # @param options [Hash]
26
+ # @return [Integer]
27
+ def calc_initial_position(stream, options)
28
+ return options[:from_position] if options[:from_position]
29
+ return 1 if forwards?(options[:direction])
30
+
31
+ read_cmd.call(stream, options: options.merge(max_count: 1)).first.global_position
32
+ end
33
+
34
+ # @param events [Array<PgEventstore::Event>]
35
+ # @param max_count [Integer]
36
+ # @return [Boolean]
37
+ def end_reached?(events, max_count)
38
+ events.size < max_count
39
+ end
40
+
41
+ # @param events [Array<PgEventstore::Event>]
42
+ # @param direction [String, Symbol, nil]
43
+ # @return [Integer]
44
+ def calc_next_position(events, direction)
45
+ return events.last.global_position + 1 if forwards?(direction)
46
+
47
+ events.last.global_position - 1
48
+ end
49
+
50
+ # @param direction [String, Symbol, nil]
51
+ # @return [Boolean]
52
+ def forwards?(direction)
53
+ QueryBuilders::EventsFiltering::SQL_DIRECTIONS[direction] == QueryBuilders::EventsFiltering::SQL_DIRECTIONS[:asc]
54
+ end
55
+
56
+ # @return [PgEventstore::Commands::Read]
57
+ def read_cmd
58
+ @read_cmd ||= Read.new(queries)
59
+ end
60
+ end
61
+ end
62
+ end
@@ -1,6 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'abstract_command'
4
+ require_relative 'commands/event_modifiers/prepare_link_event'
5
+ require_relative 'commands/event_modifiers/prepare_regular_event'
4
6
  require_relative 'commands/append'
5
7
  require_relative 'commands/read'
8
+ require_relative 'commands/regular_stream_read_paginated'
9
+ require_relative 'commands/system_stream_read_paginated'
6
10
  require_relative 'commands/multiple'
11
+ require_relative 'commands/link_to'
@@ -6,14 +6,46 @@ module PgEventstore
6
6
 
7
7
  attr_reader :name
8
8
 
9
- # PostgreSQL connection URI docs https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING-URIS
9
+ # @!attribute pg_uri
10
+ # @return [String] PostgreSQL connection URI docs
11
+ # https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING-URIS
10
12
  option(:pg_uri) { 'postgresql://postgres:postgres@localhost:5432/eventstore' }
13
+ # @!attribute max_count
14
+ # @return [Integer] Number of events to return in one response when reading from a stream
11
15
  option(:max_count) { 1000 }
16
+ # @!attribute middlewares
17
+ # @return [Hash{Symbol => <#serialize, #deserialize>}] A set of identified(by key) objects that respond to
18
+ # #serialize and #deserialize
12
19
  option(:middlewares) { {} }
13
- # Object that responds to #call. Should accept a string and return a class
20
+ # @!attribute event_class_resolver
21
+ # @return [#call] A callable object that must accept a string and return a class. It is used when resolving
22
+ # event's class during event's deserialization process
14
23
  option(:event_class_resolver) { EventClassResolver.new }
24
+ # @!attribute connection_pool_size
25
+ # @return [Integer] Max number of connections per ruby process
15
26
  option(:connection_pool_size) { 5 }
16
- option(:connection_pool_timeout) { 5 } # seconds
27
+ # @!attribute connection_pool_timeout
28
+ # @return [Integer] Time in seconds to wait for the connection in pool to be released
29
+ option(:connection_pool_timeout) { 5 }
30
+ # @!attribute subscription_pull_interval
31
+ # @return [Integer] How often Subscription should pull new events
32
+ option(:subscription_pull_interval) { 2 }
33
+ # @!attribute subscription_max_retries
34
+ # @return [Integer] max number of retries of failed Subscription
35
+ option(:subscription_max_retries) { 100 }
36
+ # @!attribute subscription_retries_interval
37
+ # @return [Integer] interval in seconds between retries of failed Subscription
38
+ option(:subscription_retries_interval) { 1 }
39
+ # @!attribute subscription_restart_terminator
40
+ # @return [#call, nil] provide callable object that accepts Subscription object to decide whether to prevent
41
+ # further Subscription restarts
42
+ option(:subscription_restart_terminator)
43
+ # @!attribute subscriptions_set_max_retries
44
+ # @return [Integer] max number of retries of failed SubscriptionsSet
45
+ option(:subscriptions_set_max_retries) { 10 }
46
+ # @!attribute subscriptions_set_retries_interval
47
+ # @return [Integer] interval in seconds between retries of failed SubscriptionsSet
48
+ option(:subscriptions_set_retries_interval) { 1 }
17
49
 
18
50
  # @param name [Symbol] config's name. Its value matches the appropriate key in PgEventstore.config hash
19
51
  def initialize(name:, **options)
@@ -104,4 +104,84 @@ module PgEventstore
104
104
  TEXT
105
105
  end
106
106
  end
107
+
108
+ class RecordNotFound < Error
109
+ attr_reader :table_name, :id
110
+
111
+ # @param table_name [String]
112
+ # @param id [Integer, String]
113
+ def initialize(table_name, id)
114
+ @table_name = table_name
115
+ @id = id
116
+ super(user_friendly_message)
117
+ end
118
+
119
+ # @return [String]
120
+ def user_friendly_message
121
+ "Could not find/update #{table_name.inspect} record with #{id.inspect} id."
122
+ end
123
+ end
124
+
125
+ class SubscriptionAlreadyLockedError < Error
126
+ attr_reader :set, :name, :lock_id
127
+
128
+ # @param set [String] subscriptions set name
129
+ # @param name [String] subscription's name
130
+ # @param lock_id [String] UUIDv4
131
+ def initialize(set, name, lock_id)
132
+ @set = set
133
+ @name = name
134
+ @lock_id = lock_id
135
+ super(user_friendly_message)
136
+ end
137
+
138
+ # @return [String]
139
+ def user_friendly_message
140
+ <<~TEXT.strip
141
+ Could not lock subscription from #{set.inspect} set with #{name.inspect} name. It is already locked by \
142
+ #{lock_id.inspect} set.
143
+ TEXT
144
+ end
145
+ end
146
+
147
+ class SubscriptionUnlockError < Error
148
+ attr_reader :set, :name, :expected_locked_by, :actual_locked_by
149
+
150
+ # @param set [String] subscription's set name
151
+ # @param name [String] subscription's name
152
+ # @param expected_locked_by [String] UUIDv4
153
+ # @param actual_locked_by [String, nil] UUIDv4
154
+ def initialize(set, name, expected_locked_by, actual_locked_by)
155
+ @set = set
156
+ @name = name
157
+ @expected_locked_by = expected_locked_by
158
+ @actual_locked_by = actual_locked_by
159
+ super(user_friendly_message)
160
+ end
161
+
162
+ # @return [String]
163
+ def user_friendly_message
164
+ <<~TEXT.strip
165
+ Failed to unlock subscription from #{set.inspect} set with #{name.inspect} name by \
166
+ #{expected_locked_by.inspect} lock id. It is currently locked by #{actual_locked_by.inspect} lock id.
167
+ TEXT
168
+ end
169
+ end
170
+
171
+ class NotPersistedEventError < Error
172
+ attr_reader :event
173
+
174
+ # @param event [PgEventstore::Event]
175
+ def initialize(event)
176
+ @event = event
177
+ super(user_friendly_message)
178
+ end
179
+
180
+ # @return [String]
181
+ def user_friendly_message
182
+ <<~TEXT.strip
183
+ Event#id must be present, got #{event.id.inspect} instead.
184
+ TEXT
185
+ end
186
+ end
107
187
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module PgEventstore
4
4
  # @!visibility private
5
- class PgResultDeserializer
5
+ class EventDeserializer
6
6
  attr_reader :middlewares, :event_class_resolver
7
7
 
8
8
  # @param middlewares [Array<Object<#deserialize, #serialize>>]
@@ -12,31 +12,14 @@ module PgEventstore
12
12
  @event_class_resolver = event_class_resolver
13
13
  end
14
14
 
15
- # @param pg_result [PG::Result]
16
- # @return [Array<PgEventstore::Event>]
17
- def deserialize(pg_result)
18
- pg_result.map(&method(:_deserialize))
15
+ # @param raw_events [Array<Hash>]
16
+ def deserialize_many(raw_events)
17
+ raw_events.map(&method(:deserialize))
19
18
  end
20
- alias deserialize_many deserialize
21
-
22
- # @param pg_result [PG::Result]
23
- # @return [PgEventstore::Event, nil]
24
- def deserialize_one(pg_result)
25
- return if pg_result.ntuples.zero?
26
-
27
- _deserialize(pg_result.first)
28
- end
29
-
30
- # @return [PgEventstore::PgResultDeserializer]
31
- def without_middlewares
32
- self.class.new([], event_class_resolver)
33
- end
34
-
35
- private
36
19
 
37
20
  # @param attrs [Hash]
38
21
  # @return [PgEventstore::Event]
39
- def _deserialize(attrs)
22
+ def deserialize(attrs)
40
23
  event = event_class_resolver.call(attrs['type']).new(**attrs.transform_keys(&:to_sym))
41
24
  middlewares.each do |middleware|
42
25
  middleware.deserialize(event)
@@ -44,5 +27,10 @@ module PgEventstore
44
27
  event.stream = PgEventstore::Stream.new(**attrs['stream'].transform_keys(&:to_sym)) if attrs.key?('stream')
45
28
  event
46
29
  end
30
+
31
+ # @return [PgEventstore::EventDeserializer]
32
+ def without_middlewares
33
+ self.class.new([], event_class_resolver)
34
+ end
47
35
  end
48
36
  end