pg_eventstore 0.3.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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