salesforce_streamer 2.0.0.rc2 → 2.3.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: 62cda30f1dc4379de105485088cce3cb77a874c6b336465b05ebdc0976c46e60
4
- data.tar.gz: 8c470f3301875a1926dfc68e8b40cc3862d8478bbd95c97fe4cdd154c0bd3c05
3
+ metadata.gz: 76475a30dc6515e8071504a9383c2811fa339f0885be703929008aaf2abbed8d
4
+ data.tar.gz: 927f5ff453fc8e51adaeae3915ee4ace4b0bcf11e355fc66c5aa32dbb5eec3aa
5
5
  SHA512:
6
- metadata.gz: c8a7aee18710329e556852c68aeb626e587108f788acf880aa0810671b9c536517ee34798eb1e9a8fd03a67ff2e8171f59ddf1ab8da9b4f836f57b5cf3746015
7
- data.tar.gz: 4c91a95c717ece055e22e05b487994293c2015f04633e0d46676f2a740986345762c8d263f40f57b981ebc18ab76553dbc7252ad2ebe95ea415ff6b533eba7f6
6
+ metadata.gz: cf695ae8cf24912b2ccb403b46b3151cc9ec988202ed9dd04857dab27cbef4d9afec3425898384649bc1c5bc84e5a68d515237b20f0b0a249ebe92251d51cebf
7
+ data.tar.gz: 0b36d256fdc82499c701aa619205c0dddf3d8837200c1e1575f144fd87caed132658e3137d3c3bc9d655157086a5e6fa2b805561729b7cef50e19a526265ef1d
@@ -1,7 +1,7 @@
1
- # automatically approve PRs submitted by Dependabot
1
+ # automatically approve PRs submitted by Dependabot or Renofidev
2
2
  # this will allow Dependabot to automatically merge dependency update PRs where CI passes
3
3
  # from: https://github.com/hmarr/auto-approve-action
4
- name: Auto approve Dependabot PRs
4
+ name: Auto approve dependency upgrades PRs
5
5
 
6
6
  on:
7
7
  pull_request
@@ -11,6 +11,6 @@ jobs:
11
11
  runs-on: ubuntu-latest
12
12
  steps:
13
13
  - uses: hmarr/auto-approve-action@v2.0.0
14
- if: github.actor == 'dependabot[bot]' || github.actor == 'dependabot-preview[bot]'
14
+ if: github.actor == 'dependabot[bot]' || github.actor == 'dependabot-preview[bot]' || github.actor == 'renofidev'
15
15
  with:
16
16
  github-token: "${{ secrets.GITHUB_TOKEN }}"
@@ -0,0 +1,19 @@
1
+ name: automerge
2
+ on:
3
+ pull_request_review:
4
+ types:
5
+ - submitted
6
+ check_suite:
7
+ types:
8
+ - completed
9
+ status: {}
10
+ jobs:
11
+ automerge:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - name: automerge
15
+ uses: "pascalgn/automerge-action@v0.11.0"
16
+ env:
17
+ GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
18
+ MERGE_METHOD: squash
19
+ MERGE_DELETE_BRANCH: true
@@ -3,6 +3,38 @@
3
3
  Sorted so that the most recent change logs are near the top. Only significant
4
4
  changes are logged in the change log.
5
5
 
6
+ ## 2020-08-17 Scott Serok [scott@renofi.com](mailto:scott@renofi.com)
7
+
8
+ v2.1 changes the expected interface of `Configuration#replay_adapter`.
9
+
10
+ Normally this breaking change would require a major version bump, but since the
11
+ functionality today is quiet broken we can call this a major bug fix.
12
+
13
+ The `config.replay_adapter` should be an object that has an interface like Hash.
14
+ It must respond to `[]` and `[]=`. By default the adapter is an empty hash. If
15
+ you want your push topic replayId to persist between restarts, then you should
16
+ implement a class with an appropriate interface.
17
+
18
+ ```ruby
19
+ class MyReplayAdapter
20
+ def [](channel)
21
+ MyPersistence.get(channel)
22
+ end
23
+
24
+ def []=(channel, replay_id)
25
+ MyPersistence.set(channel, replay_id)
26
+ end
27
+ end
28
+ ```
29
+
30
+ This change was sparked by a misunderstanding of the
31
+ `Restforce::Concerns::Streaming::ReplayExtension` replay handlers.
32
+ SalesforceStreamer can eliminate some complexity and fix a bug by delegating the
33
+ responsibility of maintaining the current replayId to that ReplayExtension. The
34
+ object will be used on each request/response cycle to record and read the latest
35
+ replayId as long as the object assigned to `config.replay_adapter` responds to
36
+ `[]` and `[]=`.
37
+
6
38
  ## 2020-08-04 Scott Serok [scott@renofi.com](mailto:scott@renofi.com)
7
39
 
8
40
  v2.0 is released as a major simplification of this library. There are 2
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- salesforce_streamer (2.0.0.rc2)
4
+ salesforce_streamer (2.3.0)
5
5
  dry-initializer (~> 3.0)
6
6
  faye (~> 1.4)
7
7
  restforce (>= 4.2, < 6.0)
@@ -13,16 +13,14 @@ GEM
13
13
  public_suffix (>= 2.0.2, < 5.0)
14
14
  ast (2.4.1)
15
15
  byebug (11.1.3)
16
- codecov (0.2.3)
17
- colorize
16
+ codecov (0.2.12)
18
17
  json
19
18
  simplecov
20
- colorize (0.8.1)
21
19
  cookiejar (0.3.3)
22
20
  diff-lcs (1.4.4)
23
21
  docile (1.3.2)
24
- dry-initializer (3.0.3)
25
- em-http-request (1.1.6)
22
+ dry-initializer (3.0.4)
23
+ em-http-request (1.1.7)
26
24
  addressable (>= 2.3.4)
27
25
  cookiejar (!= 0.3.1)
28
26
  em-socksify (>= 0.3)
@@ -31,8 +29,9 @@ GEM
31
29
  em-socksify (0.3.2)
32
30
  eventmachine (>= 1.0.0.beta.4)
33
31
  eventmachine (1.2.7)
34
- faraday (1.0.1)
32
+ faraday (1.1.0)
35
33
  multipart-post (>= 1.2, < 3)
34
+ ruby2_keywords
36
35
  faraday_middleware (1.0.0)
37
36
  faraday (~> 1.0)
38
37
  faye (1.4.0)
@@ -49,56 +48,59 @@ GEM
49
48
  hashie (4.1.0)
50
49
  http_parser.rb (0.6.0)
51
50
  json (2.3.1)
52
- jwt (2.2.1)
51
+ jwt (2.2.2)
53
52
  multi_json (1.15.0)
54
53
  multipart-post (2.1.1)
55
- parallel (1.19.2)
56
- parser (2.7.1.4)
54
+ parallel (1.20.0)
55
+ parser (2.7.2.0)
57
56
  ast (~> 2.4.1)
58
- public_suffix (4.0.5)
57
+ public_suffix (4.0.6)
59
58
  rack (2.2.3)
60
59
  rainbow (3.0.0)
61
60
  rake (13.0.1)
62
- regexp_parser (1.7.1)
63
- restforce (5.0.0)
61
+ regexp_parser (1.8.2)
62
+ restforce (5.0.3)
64
63
  faraday (>= 0.9.0, <= 2.0)
65
64
  faraday_middleware (>= 0.8.8, <= 2.0)
66
65
  hashie (>= 1.2.0, < 5.0)
67
66
  jwt (>= 1.5.6)
68
67
  rexml (3.2.4)
69
- rspec (3.9.0)
70
- rspec-core (~> 3.9.0)
71
- rspec-expectations (~> 3.9.0)
72
- rspec-mocks (~> 3.9.0)
73
- rspec-core (3.9.2)
74
- rspec-support (~> 3.9.3)
75
- rspec-expectations (3.9.2)
68
+ rspec (3.10.0)
69
+ rspec-core (~> 3.10.0)
70
+ rspec-expectations (~> 3.10.0)
71
+ rspec-mocks (~> 3.10.0)
72
+ rspec-core (3.10.0)
73
+ rspec-support (~> 3.10.0)
74
+ rspec-expectations (3.10.0)
76
75
  diff-lcs (>= 1.2.0, < 2.0)
77
- rspec-support (~> 3.9.0)
78
- rspec-mocks (3.9.1)
76
+ rspec-support (~> 3.10.0)
77
+ rspec-mocks (3.10.0)
79
78
  diff-lcs (>= 1.2.0, < 2.0)
80
- rspec-support (~> 3.9.0)
81
- rspec-support (3.9.3)
82
- rubocop (0.88.0)
79
+ rspec-support (~> 3.10.0)
80
+ rspec-support (3.10.0)
81
+ rubocop (1.2.0)
83
82
  parallel (~> 1.10)
84
- parser (>= 2.7.1.1)
83
+ parser (>= 2.7.1.5)
85
84
  rainbow (>= 2.2.2, < 4.0)
86
- regexp_parser (>= 1.7)
85
+ regexp_parser (>= 1.8)
87
86
  rexml
88
- rubocop-ast (>= 0.1.0, < 1.0)
87
+ rubocop-ast (>= 1.0.1)
89
88
  ruby-progressbar (~> 1.7)
90
89
  unicode-display_width (>= 1.4.0, < 2.0)
91
- rubocop-ast (0.3.0)
92
- parser (>= 2.7.1.4)
93
- rubocop-performance (1.7.1)
94
- rubocop (>= 0.82.0)
95
- rubocop-rspec (1.42.0)
90
+ rubocop-ast (1.1.1)
91
+ parser (>= 2.7.1.5)
92
+ rubocop-performance (1.8.1)
96
93
  rubocop (>= 0.87.0)
94
+ rubocop-ast (>= 0.4.0)
95
+ rubocop-rspec (2.0.0)
96
+ rubocop (~> 1.0)
97
+ rubocop-ast (>= 1.1.0)
97
98
  ruby-progressbar (1.10.1)
98
- simplecov (0.18.5)
99
+ ruby2_keywords (0.0.2)
100
+ simplecov (0.19.1)
99
101
  docile (~> 1.1)
100
102
  simplecov-html (~> 0.11)
101
- simplecov-html (0.12.2)
103
+ simplecov-html (0.12.3)
102
104
  unicode-display_width (1.7.0)
103
105
  websocket-driver (0.7.3)
104
106
  websocket-extensions (>= 0.1.0)
data/README.md CHANGED
@@ -20,33 +20,11 @@ And then execute:
20
20
 
21
21
  ## Usage
22
22
 
23
- ### Middleware
24
-
25
- You can initialize the streamer server with any number of middleware classes.
26
- When a message is received by a PushTopic subscription, the chain of middleware
27
- classes are executed before the message handler is called.
28
-
29
- ```ruby
30
- # config/initializers/streamer.rb
31
- class MySimpleMiddleware
32
- def initialize(handler)
33
- @handler = handler
34
- end
35
- def call(message)
36
- @handler.call(message)
37
- end
38
- end
39
-
40
- SalesforceStreamer.config.use_middleware MySimpleMiddleware
41
- ```
42
-
43
23
  ### Configure Push Topics
44
24
 
45
- Create a YAML file to configure your server subscriptions. The configuration
46
- for each subscription must have a nested `salesforce:` key. These settings will
47
- be synced to your Salesforce instance when the `-x` flag is set on the command
48
- line. For more information about the `replay:` and `notify_fields_for` options
49
- please see the Salesforce Streaming API reference documentation.
25
+ Create a YAML file to configure your PushTopic subscriptions. When streamer
26
+ starts up it will check for any differences between Salesforce PushTopics and
27
+ this yaml and update any differences when `config.manage_topics = true`.
50
28
 
51
29
  ```yaml
52
30
  # config/streamer.yml
@@ -71,14 +49,6 @@ production:
71
49
  <<: *DEFAULT
72
50
  ```
73
51
 
74
- It's important to note that the way push topics are managed is by the Salesforce
75
- name attribute. This should uniquely identify each push topic. It is not
76
- recommended to change the name of your push topic definitions; otherwise, the
77
- push topic manager will not find a push topic in Salesforce resulting in the
78
- creation of a brand new push topic. If the push topic manager identifies a
79
- difference in any of the other Salesforce attributes, then it will update the
80
- push topic in Salesforce before starting the streaming server.
81
-
82
52
  ### Define Message Handlers
83
53
 
84
54
  Define your handlers somewhere in your project. They must respond to either
@@ -88,14 +58,17 @@ Define your handlers somewhere in your project. They must respond to either
88
58
  # lib/account_change_handler.rb
89
59
  # Handle account changes inline
90
60
  class AccountChangeHandler
91
- def self.call(message)
92
- puts message
61
+ class << self
62
+ def call(message)
63
+ puts message
64
+ end
93
65
  end
94
66
  end
95
67
 
96
68
  # Handle account changes asynchronously
97
69
  class AccountChangeHandler
98
70
  include Sidekiq::Worker
71
+
99
72
  def perform(message)
100
73
  puts message
101
74
  end
@@ -116,10 +89,9 @@ Configure the `SalesforceStreamer` module.
116
89
  SalesforceStreamer.configure do |config|
117
90
  config.logger = Logger.new(STDERR, level: 'INFO')
118
91
  config.exception_adapter = proc { |e| puts e }
119
- config.replay_adapter = proc { |topic|
120
- (Store.get(topic.name) || topic.replay).to_i
121
- }
92
+ config.replay_adapter = MyReplayAdapter
122
93
  config.use_middleware AfterMessageReceived
94
+ config.use_faye_extension ErrorLoggingExtension.new
123
95
  config.manage_topics = true
124
96
  end
125
97
  ```
@@ -160,6 +132,74 @@ By default, the executable will load the YAML based on the `RACK_ENV` environmen
160
132
  variable, or default to `:development` if not set. You can override this by
161
133
  setting the `config.environment = :integration`
162
134
 
135
+ ### Message Handling Middleware
136
+
137
+ You can initialize the streamer server with any number of middleware classes.
138
+ When a message is received by a PushTopic subscription, the chain of middleware
139
+ classes are executed before the message handler is called.
140
+
141
+ ```ruby
142
+ # config/initializers/streamer.rb
143
+ class MySimpleMiddleware
144
+ def initialize(handler)
145
+ @handler = handler
146
+ end
147
+
148
+ def call(message)
149
+ @handler.call(message)
150
+ end
151
+ end
152
+
153
+ SalesforceStreamer.config.use_middleware MySimpleMiddleware
154
+ ```
155
+
156
+ ### ReplayAdapter
157
+
158
+ The `config.replay_adapter` should be an object that has an interface like Hash.
159
+ It must respond to `[]` and `[]=`. By default the adapter is an empty hash. If
160
+ you want your push topic replayId to persist between restarts, then you should
161
+ implement a class with an appropriate interface.
162
+
163
+ ```ruby
164
+ class MyReplayAdapter
165
+ def [](channel)
166
+ Persistence.get(channel)
167
+ end
168
+
169
+ def []=(channel, replay_id)
170
+ Persistence.set(channel, replay_id)
171
+ end
172
+ end
173
+ ```
174
+
175
+ This adapter will be used directly by `Restforce::ReplayExtension`.
176
+
177
+ ### Use Faye Extension
178
+
179
+ The `config.use_faye_extension` should be given an object that responds to
180
+ `.incoming(message, callback)` or `.outgoing(message, callback)` or both. Find
181
+ out more about extensions from
182
+ [Faye](https://github.com/faye/faye/blob/master/spec/ruby/server/extensions_spec.rb)
183
+ specs.
184
+
185
+ Any configured extensions are added to the Faye client used by the Restforce
186
+ client when starting up the server. If the extension responds to `.server=` then
187
+ the instance of `SalesforceStreamer::Server` is assigned. This may be convenient
188
+ to restart the server subscriptions if an error requires a reset.
189
+
190
+ ```ruby
191
+ class MyRestartFayeExtension
192
+ attr_accessor :server
193
+
194
+ def incoming(message, callback)
195
+ callback.call(message).tap |message|
196
+ server.restart if message['error'] == 'tragic'
197
+ end
198
+ end
199
+ end
200
+
201
+ SalesforceStreamer.config.use_faye_extension MyRestartFayeExtension.new
202
+ ```
163
203
  ## Development
164
204
 
165
205
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake rspec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -32,7 +32,7 @@ module SalesforceStreamer
32
32
  end
33
33
 
34
34
  o.on '-v', '--verbose LEVEL', 'Set the log level (default no logging)' do |arg|
35
- @config.logger = Logger.new(STDERR, level: arg)
35
+ @config.logger = Logger.new($stderr, level: arg)
36
36
  end
37
37
 
38
38
  o.on '-V', '--version', 'Print the version information' do
@@ -3,7 +3,7 @@ module SalesforceStreamer
3
3
  class Configuration
4
4
  attr_accessor :environment, :logger, :require_path, :config_file,
5
5
  :manage_topics, :exception_adapter, :replay_adapter
6
- attr_reader :middleware
6
+ attr_reader :middleware, :faye_extensions
7
7
 
8
8
  class << self
9
9
  attr_writer :instance
@@ -21,11 +21,12 @@ module SalesforceStreamer
21
21
  @environment = ENV['RACK_ENV'] || :development
22
22
  @logger = Logger.new(IO::NULL)
23
23
  @exception_adapter = proc { |exc| fail(exc) }
24
- @replay_adapter = proc { |topic| topic.id || topic.replay }
24
+ @replay_adapter = Hash.new { |hash, key| hash[key] = -1 }
25
25
  @manage_topics = false
26
26
  @config_file = './config/streamer.yml'
27
27
  @require_path = './config/environment'
28
28
  @middleware = []
29
+ @faye_extensions = [ReplayIdErrorExtension.new]
29
30
  end
30
31
 
31
32
  def manage_topics?
@@ -37,6 +38,11 @@ module SalesforceStreamer
37
38
  @middleware << [klass, args, block]
38
39
  end
39
40
 
41
+ # adds a Faye extension
42
+ def use_faye_extension(extension)
43
+ @faye_extensions << extension
44
+ end
45
+
40
46
  # returns a ready to use chain of middleware
41
47
  def middleware_runner(last_handler)
42
48
  @middleware.reduce(last_handler) do |next_handler, current_handler|
@@ -34,7 +34,7 @@ module SalesforceStreamer
34
34
  @handler = Object.const_get(@handler)
35
35
  true
36
36
  rescue NameError, TypeError => e
37
- message = 'handler=' + @handler.to_s + ' exception=' + e.to_s
37
+ message = "handler=#{@handler} exception=#{e}"
38
38
  raise(PushTopicHandlerMissingError, message)
39
39
  end
40
40
 
@@ -8,10 +8,8 @@ module SalesforceStreamer
8
8
  @client.authenticate!
9
9
  end
10
10
 
11
- def subscribe(*args)
12
- @client.subscribe(args) do
13
- yield
14
- end
11
+ def subscribe(*args, &block)
12
+ @client.subscribe(args, &block)
15
13
  end
16
14
 
17
15
  # Returns nil or an instance of Restforce::SObject
@@ -1,15 +1,23 @@
1
1
  module SalesforceStreamer
2
2
  class Server
3
3
  attr_writer :push_topics
4
+ attr_reader :client
4
5
 
5
6
  def initialize(push_topics: [])
6
7
  @push_topics = push_topics
7
8
  end
8
9
 
9
10
  def run
10
- Log.info 'Starting Server'
11
+ Log.info 'Starting server'
11
12
  catch_signals
12
- start_em
13
+ reset_client
14
+ EM.run { subscribe }
15
+ end
16
+
17
+ def restart
18
+ Log.info 'Restarting server'
19
+ reset_client
20
+ EM.next_tick { subscribe }
13
21
  end
14
22
 
15
23
  private
@@ -23,24 +31,21 @@ module SalesforceStreamer
23
31
  end
24
32
  end
25
33
 
26
- def client
27
- return @client if @client
34
+ def reset_client
28
35
  @client = Restforce.new
29
- @client.authenticate!
30
- @client.faye.add_extension ReplayIdErrorExtension.new
31
- @client
36
+ client.authenticate!
37
+ Configuration.instance.faye_extensions.each do |extension|
38
+ Log.debug %(adding Faye extension #{extension})
39
+ extension.server = self if extension.respond_to?(:server=)
40
+ client.faye.add_extension extension
41
+ end
32
42
  end
33
43
 
34
- def start_em
35
- EM.run do
36
- @push_topics.map do |topic|
37
- replay_id = Configuration.instance.replay_adapter.call(topic)
38
- client.subscribe topic.name, replay: replay_id.to_i do |message|
39
- replay_id = message.dig('event', 'replayId')
40
- Log.info "Message #{replay_id} received from topic #{topic.name}"
41
- topic.handle message
42
- topic.id = replay_id
43
- end
44
+ def subscribe
45
+ @push_topics.each do |topic|
46
+ client.subscribe topic.name, replay: Configuration.instance.replay_adapter do |message|
47
+ Log.info "Message #{message.dig('event', 'replayId')} received from topic #{topic.name}"
48
+ topic.handle message
44
49
  end
45
50
  end
46
51
  end
@@ -1,3 +1,3 @@
1
1
  module SalesforceStreamer
2
- VERSION = '2.0.0.rc2'.freeze
2
+ VERSION = '2.3.0'.freeze
3
3
  end
@@ -14,6 +14,7 @@ Gem::Specification.new do |spec|
14
14
 
15
15
  spec.metadata['homepage_uri'] = spec.homepage
16
16
  spec.metadata['source_code_uri'] = spec.homepage
17
+ spec.metadata['documentation_uri'] = 'https://www.rubydoc.info/gems/salesforce_streamer'
17
18
 
18
19
  spec.files = Dir.chdir(File.expand_path(__dir__)) do
19
20
  `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(bin/|spec/|\.rub)}) }
metadata CHANGED
@@ -1,15 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: salesforce_streamer
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0.rc2
4
+ version: 2.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Scott Serok
8
8
  - RenoFi Engineering Team
9
- autorequire:
9
+ autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2020-08-04 00:00:00.000000000 Z
12
+ date: 2020-11-12 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: dry-initializer
@@ -157,7 +157,7 @@ dependencies:
157
157
  - - ">="
158
158
  - !ruby/object:Gem::Version
159
159
  version: '0'
160
- description:
160
+ description:
161
161
  email:
162
162
  - scott@renofi.com
163
163
  - engineering@renofi.com
@@ -172,6 +172,7 @@ files:
172
172
  - ".github/ISSUE_TEMPLATE/story.md"
173
173
  - ".github/PULL_REQUEST_TEMPLATE.md"
174
174
  - ".github/workflows/auto-approve.yml"
175
+ - ".github/workflows/auto-merge.yml"
175
176
  - ".gitignore"
176
177
  - ".rspec"
177
178
  - ".travis.yml"
@@ -202,7 +203,8 @@ licenses:
202
203
  metadata:
203
204
  homepage_uri: https://github.com/renofi/salesforce_streamer
204
205
  source_code_uri: https://github.com/renofi/salesforce_streamer
205
- post_install_message:
206
+ documentation_uri: https://www.rubydoc.info/gems/salesforce_streamer
207
+ post_install_message:
206
208
  rdoc_options: []
207
209
  require_paths:
208
210
  - lib
@@ -213,12 +215,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
213
215
  version: '2.6'
214
216
  required_rubygems_version: !ruby/object:Gem::Requirement
215
217
  requirements:
216
- - - ">"
218
+ - - ">="
217
219
  - !ruby/object:Gem::Version
218
- version: 1.3.1
220
+ version: '0'
219
221
  requirements: []
220
- rubygems_version: 3.1.2
221
- signing_key:
222
+ rubygems_version: 3.1.4
223
+ signing_key:
222
224
  specification_version: 4
223
225
  summary: A wrapper around the Restforce Streaming API with a built-in PushTopic manager.
224
226
  test_files: []