downstream 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0b7d5e8e2cd8390bbb0eef4fc8df4615c1bb2a5f5202ac0711f417b6347b7adc
4
- data.tar.gz: 2a13ccafc25da88885ce49a309c41d4fc0cda93a7ffbd37cc2454e94359c1248
3
+ metadata.gz: 3806c4f875e378357e47571ed0bd5dbac1b45a80f9a01a78187b3c971378f425
4
+ data.tar.gz: e038408a8fb1ce3e18dfd7614e9fe3931fd72c7913249457c29fa00b14a22aa4
5
5
  SHA512:
6
- metadata.gz: 5b1cc9d3110a38b6575363b929cf8d2b8a6df186e4099268a9f66f6c20f4d04d3e1c62df9b0c308ee21a5a8e1fcdf9c1f62070e39708db22be739173f8a0d788
7
- data.tar.gz: 4ff6301363da1ebff02ba743571166b89eaf35151427a3f6f8e70d54e8bd18a244ae863cdab4247afb412f97ed1a7a7b01a91a3c54d0ab35a56810584d87ef39
6
+ metadata.gz: 6f81acf6c0da05267067ac4d4c60d9edcb8aefba35d326643651afcf4ba17df09b88a791b379f10318e0c63aeaa691397d10b3a2cad10e23a38b2c29990fa9bf
7
+ data.tar.gz: 97bf35f16b9542bb032fb0ed52b6192b24ea92a3157d2f0cfc883546f50ee04e759965d6281fbb956e30029ee8bd716b129b8340b5638c3d619be78ca4494faf
data/README.md CHANGED
@@ -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,6 +113,20 @@ 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.
@@ -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
@@ -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)
@@ -4,10 +4,10 @@ require_relative "subscriber"
4
4
  module Downstream
5
5
  module Stateless
6
6
  class Pubsub < AbstractPubsub
7
- def subscribe(identifier, callable)
7
+ def subscribe(identifier, callable, async: false)
8
8
  ActiveSupport::Notifications.subscribe(
9
9
  identifier,
10
- Subscriber.new(callable)
10
+ Subscriber.new(callable, async: async)
11
11
  )
12
12
  end
13
13
 
@@ -1,19 +1,53 @@
1
+ require_relative "subscriber_job"
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, 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
+ private
42
+
43
+ def async_queue_name
44
+ return @async_queue_name if defined?(@async_queue_name)
45
+
46
+ name = async[:queue] if async.is_a?(Hash)
47
+ name ||= Downstream.config.async_queue
48
+
49
+ @async_queue_name = name
50
+ end
17
51
  end
18
52
  end
19
53
  end
@@ -0,0 +1,9 @@
1
+ module Downstream
2
+ module Stateless
3
+ class SubscriberJob < ActiveJob::Base
4
+ def perform(event, callable)
5
+ callable.constantize.call(event)
6
+ end
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.2.0"
5
5
  end
data/lib/downstream.rb CHANGED
@@ -1,7 +1,10 @@
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"
@@ -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.2.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: 2021-11-01 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: '0.5'
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: '0.5'
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
@@ -124,6 +194,7 @@ files:
124
194
  - lib/downstream/pubsub_adapters/abstract_pubsub.rb
125
195
  - lib/downstream/pubsub_adapters/stateless/pubsub.rb
126
196
  - lib/downstream/pubsub_adapters/stateless/subscriber.rb
197
+ - lib/downstream/pubsub_adapters/stateless/subscriber_job.rb
127
198
  - lib/downstream/rspec.rb
128
199
  - lib/downstream/rspec/have_published_event.rb
129
200
  - lib/downstream/version.rb