downstream 1.1.0 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0b7d5e8e2cd8390bbb0eef4fc8df4615c1bb2a5f5202ac0711f417b6347b7adc
4
- data.tar.gz: 2a13ccafc25da88885ce49a309c41d4fc0cda93a7ffbd37cc2454e94359c1248
3
+ metadata.gz: 0770f84f43315d12f61f304600f5bdbb45679de5fc32820a6d599fdab4f0028d
4
+ data.tar.gz: aa877161f770583a778578d165b36e61df405eeecc4522ffe8af212085f5f2d7
5
5
  SHA512:
6
- metadata.gz: 5b1cc9d3110a38b6575363b929cf8d2b8a6df186e4099268a9f66f6c20f4d04d3e1c62df9b0c308ee21a5a8e1fcdf9c1f62070e39708db22be739173f8a0d788
7
- data.tar.gz: 4ff6301363da1ebff02ba743571166b89eaf35151427a3f6f8e70d54e8bd18a244ae863cdab4247afb412f97ed1a7a7b01a91a3c54d0ab35a56810584d87ef39
6
+ metadata.gz: 7c245ff4501fbd5d1efd3d17958a2784d9cf2ff28037c647a7b25a89f6cafd13c716947f7a0fc94b50c64952e5a175a6117858ef7c94bc339119f01e4bda98b8
7
+ data.tar.gz: 97defc1ff2d152767d833b4be84ac9b9c1202dce8f0e0673c7f5a25af09763b1ff54ea61839e7dfe1c9d16ebb6f45a8efff5d43d87d6494f193387f341b971a1
data/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
  # Downstream
5
5
 
6
- This gem provides a straightforward way to implement communication between Rails Engines using the Publish-Subscribe pattern. The gem allows decreasing decoupling engines with events. An event is a recorded object in the system that reflects an action that the engine performs, and the params that lead to its creation.
6
+ This gem provides a straightforward way to implement communication between Rails Engines using the Publish-Subscribe pattern. The gem allows decreasing the coupling of engines with events. An event is a recorded object in the system that reflects an action that the engine performs, and the params that lead to its creation.
7
7
 
8
8
  The gem inspired by [`active_event_store`](https://github.com/palkan/active_event_store), and initially based on its codebase. Having said that, it does not store in a database all happened events which ensures simplicity and performance.
9
9
 
@@ -27,6 +27,7 @@ Downstream supports various adapters for event handling. It can be configured in
27
27
  ```ruby
28
28
  Downstream.configure do |config|
29
29
  config.pubsub = :stateless # it's a default adapter
30
+ config.async_queue = :high_priority # nil by default
30
31
  end
31
32
  ```
32
33
 
@@ -112,10 +113,30 @@ Downstream.subscribed(subscriber, to: ProfileCreated) do
112
113
  end
113
114
  ```
114
115
 
116
+ If you want to handle events in a background job, you can pass the `async: true` option:
117
+
118
+ ```ruby
119
+ store.subscribe OnProfileCreated::DoThat, async: true
120
+ ```
121
+
122
+ By default, a job will be enqueued into `async_queue` name from the Downstream config. You can define your own queue name for a specific subscriber:
123
+
124
+ ```ruby
125
+ store.subscribe OnProfileCreated::DoThat, async: {queue: :low_priority}
126
+ ```
127
+
128
+ **NOTE:** all subscribers are synchronous by default
129
+
115
130
  ## Testing
116
131
 
117
132
  You can test subscribers as normal Ruby objects.
118
133
 
134
+ First, load testing helpers in the `spec_helper.rb`:
135
+
136
+ ```ruby
137
+ require "downstream/rspec"
138
+ ```
139
+
119
140
  To test that a given subscriber exists, you can do the following:
120
141
 
121
142
  ```ruby
@@ -128,6 +149,14 @@ it "is subscribed to some event" do
128
149
 
129
150
  expect(MySubscriberService).to have_received(:call).with(event)
130
151
  end
152
+
153
+ # for asynchronous subscriptions
154
+ it "is subscribed to some event" do
155
+ event = MyEvent.new(some: "data")
156
+ expect { Downstream.publish event }.
157
+ to have_enqueued_async_subscriber_for(MySubscriberService).
158
+ with(event)
159
+ end
131
160
  ```
132
161
 
133
162
  To test publishing use `have_published_event` matcher:
@@ -4,6 +4,7 @@ require "active_support/inflections"
4
4
 
5
5
  module Downstream
6
6
  class Config
7
+ attr_accessor :async_queue
7
8
  attr_writer :namespace
8
9
 
9
10
  def namespace
@@ -16,10 +17,10 @@ module Downstream
16
17
 
17
18
  def pubsub=(value)
18
19
  @pubsub = case value
19
- when String, Symbol
20
- lookup_pubsub(value)
21
- else
22
- value
20
+ when String, Symbol
21
+ lookup_pubsub(value)
22
+ else
23
+ value
23
24
  end
24
25
  end
25
26
 
@@ -1,10 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ GlobalID::Locator.use :downstream do |gid|
4
+ params = gid.params.each_with_object({}) do |(key, value), memo|
5
+ memo[key.to_sym] = if value.is_a?(String) && value.start_with?("gid://")
6
+ GlobalID::Locator.locate(value)
7
+ else
8
+ value
9
+ end
10
+ end
11
+
12
+ gid.model_name.constantize
13
+ .new(event_id: gid.model_id, **params)
14
+ end
15
+
3
16
  module Downstream
4
17
  class Event
5
18
  extend ActiveModel::Naming
19
+ include GlobalID::Identification
6
20
 
7
- RESERVED_ATTRIBUTES = %i[event_id type].freeze
21
+ RESERVED_ATTRIBUTES = %i[id event_id type].freeze
8
22
 
9
23
  class << self
10
24
  attr_writer :identifier
@@ -57,6 +71,8 @@ module Downstream
57
71
 
58
72
  attr_reader :event_id, :data, :errors
59
73
 
74
+ alias_method :id, :event_id
75
+
60
76
  def initialize(event_id: nil, **params)
61
77
  @event_id = event_id || SecureRandom.hex(10)
62
78
  validate_attributes!(params)
@@ -77,6 +93,20 @@ module Downstream
77
93
  }
78
94
  end
79
95
 
96
+ def to_global_id
97
+ new_data = data.each_with_object({}) do |(key, value), memo|
98
+ memo[key] = if value.respond_to?(:to_global_id)
99
+ value.to_global_id
100
+ else
101
+ value
102
+ end
103
+ end
104
+
105
+ super(new_data.merge!(app: :downstream))
106
+ end
107
+
108
+ alias_method :to_gid, :to_global_id
109
+
80
110
  def inspect
81
111
  "#{self.class.name}<#{type}##{event_id}>, data: #{data}"
82
112
  end
@@ -85,6 +115,15 @@ module Downstream
85
115
  data.fetch(attr)
86
116
  end
87
117
 
118
+ def ==(other)
119
+ super ||
120
+ other.instance_of?(self.class) &&
121
+ !event_id.nil? &&
122
+ other.event_id == event_id
123
+ end
124
+
125
+ alias_method :eql?, :==
126
+
88
127
  private
89
128
 
90
129
  def validate_attributes!(params)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Downstream
2
4
  class AbstractPubsub
3
5
  def subscribe(identifier, callable)
@@ -1,14 +1,13 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "active_support/notifications"
2
4
  require_relative "subscriber"
3
5
 
4
6
  module Downstream
5
7
  module Stateless
6
8
  class Pubsub < AbstractPubsub
7
- def subscribe(identifier, callable)
8
- ActiveSupport::Notifications.subscribe(
9
- identifier,
10
- Subscriber.new(callable)
11
- )
9
+ def subscribe(identifier, callable, async: false)
10
+ Subscriber.new(callable, async: async).tap { |s| s.subscribe(identifier) }
12
11
  end
13
12
 
14
13
  def subscribed(identifier, callable, &block)
@@ -20,7 +19,7 @@ module Downstream
20
19
  end
21
20
 
22
21
  def publish(identifier, event)
23
- ActiveSupport::Notifications.publish(identifier, event)
22
+ ActiveSupport::Notifications.instrument(identifier, event)
24
23
  end
25
24
  end
26
25
  end
@@ -1,19 +1,66 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Downstream
2
4
  module Stateless
3
5
  class Subscriber
4
- attr_reader :callable
6
+ include AfterCommitEverywhere
7
+
8
+ attr_reader :callable, :async
5
9
 
6
- def initialize(callable)
10
+ def initialize(callable, async: false)
7
11
  @callable = callable
12
+ @async = async
13
+ end
14
+
15
+ def async?
16
+ !!async
8
17
  end
9
18
 
10
- def call(name, event)
11
- if (callable.respond_to?(:arity) && callable.arity == 2) || callable.method(:call).arity == 2
12
- callable.call(name, event)
19
+ def call(_name, _start, _finish, _id, event)
20
+ if async?
21
+ if callable.is_a?(Proc) || callable.name.nil?
22
+ raise ArgumentError, "Anonymous subscribers (blocks/procs/lambdas or anonymous modules) cannot be asynchronous"
23
+ end
24
+
25
+ raise ArgumentError, "Async subscriber must be a module/class, not instance" unless callable.is_a?(Module)
26
+
27
+ after_commit do
28
+ SubscriberJob.then do |job|
29
+ if (queue_name = async_queue_name)
30
+ job.set(queue: queue_name)
31
+ else
32
+ job
33
+ end
34
+ end.perform_later(event, callable.name)
35
+ end
13
36
  else
14
37
  callable.call(event)
15
38
  end
16
39
  end
40
+
41
+ def subscribe(identifier)
42
+ @notification_subscriber = ActiveSupport::Notifications.subscribe(
43
+ identifier,
44
+ self
45
+ )
46
+ end
47
+
48
+ def unsubscribe
49
+ ActiveSupport::Notifications.unsubscribe(notification_subscriber)
50
+ end
51
+
52
+ private
53
+
54
+ attr_reader :notification_subscriber
55
+
56
+ def async_queue_name
57
+ return @async_queue_name if defined?(@async_queue_name)
58
+
59
+ name = async[:queue] if async.is_a?(Hash)
60
+ name ||= Downstream.config.async_queue
61
+
62
+ @async_queue_name = name
63
+ end
17
64
  end
18
65
  end
19
66
  end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rspec/rails/matchers/active_job"
4
+
5
+ module Downstream
6
+ class HaveEnqueuedAsyncSubscriberFor < RSpec::Rails::Matchers::ActiveJob::HaveEnqueuedJob
7
+ class EventMatcher
8
+ include ::RSpec::Matchers::Composable
9
+
10
+ attr_reader :event
11
+
12
+ def initialize(event)
13
+ @event = event
14
+ end
15
+
16
+ def matches?(actual)
17
+ actual == event
18
+ end
19
+
20
+ def description
21
+ "be #{event.inspect}"
22
+ end
23
+ end
24
+
25
+ attr_reader :callable
26
+
27
+ def initialize(callable)
28
+ @callable = callable
29
+ super(SubscriberJob)
30
+ end
31
+
32
+ def with(event)
33
+ super(EventMatcher.new(event), callable.name)
34
+ end
35
+
36
+ def matches?(proc)
37
+ raise ArgumentError, "have_enqueued_async_subscriber_for only supports block expectations" unless Proc === proc
38
+ super
39
+ end
40
+ end
41
+ end
42
+
43
+ RSpec.configure do |config|
44
+ config.include(Module.new do
45
+ def have_enqueued_async_subscriber_for(*args)
46
+ Downstream::HaveEnqueuedAsyncSubscriberFor.new(*args)
47
+ end
48
+ end)
49
+ end
@@ -1,3 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "rspec/have_published_event"
4
+ require_relative "rspec/have_enqueued_async_subscriber_for"
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Downstream
4
+ class SubscriberJob < ActiveJob::Base
5
+ def perform(event, callable)
6
+ callable.constantize.call(event)
7
+ end
8
+ end
9
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Downstream
4
- VERSION = "1.1.0"
4
+ VERSION = "1.4.0"
5
5
  end
data/lib/downstream.rb CHANGED
@@ -1,12 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_support"
4
+ require "active_job"
4
5
  require "active_model"
6
+ require "globalid"
7
+ require "after_commit_everywhere"
5
8
 
6
9
  require "downstream/config"
7
10
  require "downstream/event"
8
11
  require "downstream/pubsub_adapters/abstract_pubsub"
9
- require "downstream/rspec" if defined?(RSpec)
12
+ require "downstream/subscriber_job"
10
13
 
11
14
  module Downstream
12
15
  class << self
@@ -20,13 +23,13 @@ module Downstream
20
23
  yield config
21
24
  end
22
25
 
23
- def subscribe(subscriber = nil, to: nil, &block)
26
+ def subscribe(subscriber = nil, to: nil, async: false, &block)
24
27
  subscriber ||= block if block
25
28
  raise ArgumentError, "Subsriber must be present" if subscriber.nil?
26
29
 
27
30
  identifier = construct_identifier(subscriber, to)
28
31
 
29
- pubsub.subscribe(identifier, subscriber)
32
+ pubsub.subscribe(identifier, subscriber, async: async)
30
33
  end
31
34
 
32
35
  # temporary subscriptions
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: downstream
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - merkushin.m.s@gmail.com
@@ -9,8 +9,36 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2021-10-29 00:00:00.000000000 Z
12
+ date: 2022-05-22 00:00:00.000000000 Z
13
13
  dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: after_commit_everywhere
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: '1.0'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: '1.0'
28
+ - !ruby/object:Gem::Dependency
29
+ name: globalid
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: '1.0'
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: '1.0'
14
42
  - !ruby/object:Gem::Dependency
15
43
  name: rails
16
44
  requirement: !ruby/object:Gem::Requirement
@@ -53,6 +81,20 @@ dependencies:
53
81
  - - ">="
54
82
  - !ruby/object:Gem::Version
55
83
  version: '1.16'
84
+ - !ruby/object:Gem::Dependency
85
+ name: combustion
86
+ requirement: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - "~>"
89
+ - !ruby/object:Gem::Version
90
+ version: '1.3'
91
+ type: :development
92
+ prerelease: false
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - "~>"
96
+ - !ruby/object:Gem::Version
97
+ version: '1.3'
56
98
  - !ruby/object:Gem::Dependency
57
99
  name: debug
58
100
  requirement: !ruby/object:Gem::Requirement
@@ -95,6 +137,34 @@ dependencies:
95
137
  - - "~>"
96
138
  - !ruby/object:Gem::Version
97
139
  version: '3.0'
140
+ - !ruby/object:Gem::Dependency
141
+ name: rspec-rails
142
+ requirement: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - - "~>"
145
+ - !ruby/object:Gem::Version
146
+ version: '5.0'
147
+ type: :development
148
+ prerelease: false
149
+ version_requirements: !ruby/object:Gem::Requirement
150
+ requirements:
151
+ - - "~>"
152
+ - !ruby/object:Gem::Version
153
+ version: '5.0'
154
+ - !ruby/object:Gem::Dependency
155
+ name: sqlite3
156
+ requirement: !ruby/object:Gem::Requirement
157
+ requirements:
158
+ - - "~>"
159
+ - !ruby/object:Gem::Version
160
+ version: '1.4'
161
+ type: :development
162
+ prerelease: false
163
+ version_requirements: !ruby/object:Gem::Requirement
164
+ requirements:
165
+ - - "~>"
166
+ - !ruby/object:Gem::Version
167
+ version: '1.4'
98
168
  - !ruby/object:Gem::Dependency
99
169
  name: standard
100
170
  requirement: !ruby/object:Gem::Requirement
@@ -125,7 +195,9 @@ files:
125
195
  - lib/downstream/pubsub_adapters/stateless/pubsub.rb
126
196
  - lib/downstream/pubsub_adapters/stateless/subscriber.rb
127
197
  - lib/downstream/rspec.rb
198
+ - lib/downstream/rspec/have_enqueued_async_subscriber_for.rb
128
199
  - lib/downstream/rspec/have_published_event.rb
200
+ - lib/downstream/subscriber_job.rb
129
201
  - lib/downstream/version.rb
130
202
  homepage: https://github.com/bibendi/downstream
131
203
  licenses:
@@ -147,7 +219,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
147
219
  - !ruby/object:Gem::Version
148
220
  version: '0'
149
221
  requirements: []
150
- rubygems_version: 3.1.2
222
+ rubygems_version: 3.2.32
151
223
  signing_key:
152
224
  specification_version: 4
153
225
  summary: Straightforward way to implement communication between Rails Engines using