async-tools 0.1.10 → 0.2.1

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: 7e998dd042f7fc4c496862c3ca2feec5f370ac86c803dcc61c55d970f6bae28a
4
- data.tar.gz: fcd146663d144d9ccf9738c58594faabda6b98cf754523b1145b65016715eac7
3
+ metadata.gz: 37e9af668ebad3f0c2356cc2a428b1bedf8b432e239099dd1b961f16d0b01036
4
+ data.tar.gz: 201bc5e60a2b19e5a1b1498c14c0e792e96bd21a688b3605f443e6f98a2b6f29
5
5
  SHA512:
6
- metadata.gz: 6315fe469b3e53cdfb87cd569e4833eb34669ab961d3a16fd1c435b5490dd52087ebfe55b8fea41ea997759918d695ab1f999effecd43cf73419cbd5a60d3a47
7
- data.tar.gz: 720846506b9f600528c7c1ada372741abd42bd2588ab2c4f9e0ec53ea7e19877b08b9bac37b103345092e9fcf67a493c2714149c06e5211f7257c609657385f2
6
+ metadata.gz: f92056eef4bc4dc65210c682b8637fc6e39088b1213cca0f4b9bf54a06a5b736ce841c351bbaf211bac3777ccda8a7220ee2a926d822e54758a83e87b5c856f8
7
+ data.tar.gz: e463ee25e793a281c1130ede835c427f03c47f15bd51d708f28396533652ef6be14e7ecd0b08b577ec2dadf73e8ec90522bc986a6911b64ea1f049c3c5cd264e
@@ -1,6 +1,8 @@
1
1
  name: Ruby
2
2
 
3
- on: [push]
3
+ on:
4
+ - push
5
+ - workflow_dispatch
4
6
 
5
7
  jobs:
6
8
  lint:
data/.rubocop.yml CHANGED
@@ -51,3 +51,12 @@ Metrics/MethodLength:
51
51
 
52
52
  Lint/UnusedBlockArgument:
53
53
  Enabled: false
54
+
55
+ Style/SymbolArray:
56
+ EnforcedStyle: brackets
57
+
58
+ Style/WordArray:
59
+ EnforcedStyle: brackets
60
+
61
+ Style/NumberedParametersLimit:
62
+ Max: 2
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- async-tools (0.1.10)
4
+ async-tools (0.2.1)
5
5
  async (~> 2.3)
6
6
  zeitwerk (~> 2.6)
7
7
 
data/Rakefile CHANGED
@@ -9,4 +9,4 @@ require "rubocop/rake_task"
9
9
 
10
10
  RuboCop::RakeTask.new
11
11
 
12
- task default: %i[spec rubocop]
12
+ task default: [:spec, :rubocop]
data/lib/async/app.rb ADDED
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Async::App
4
+ # rubocop:disable Style/GlobalVars
5
+
6
+ module Injector
7
+ def inject(name)
8
+ define_method(name) do
9
+ $__ASYNC_APP.container[name]
10
+ end
11
+ private name
12
+ end
13
+ end
14
+
15
+ module Component
16
+ def self.included(base)
17
+ base.extend(Injector)
18
+ base.inject(:bus)
19
+
20
+ base.include(Async::Logger)
21
+
22
+ strict = Dry.Types::Strict
23
+
24
+ string_like = (strict::String | strict::Symbol).constructor(&:to_s)
25
+ kv = strict::Hash.map(string_like, strict::String)
26
+
27
+ base.const_set(:T, Module.new do
28
+ include Dry.Types
29
+ const_set(:StringLike, string_like)
30
+ const_set(:KV, kv)
31
+ end)
32
+ end
33
+ end
34
+
35
+ extend Injector
36
+ include Async::Logger
37
+
38
+ inject :bus
39
+
40
+ def initialize
41
+ raise "only one instance of #{self.class} is allowed" if $__ASYNC_APP
42
+
43
+ $__ASYNC_APP = self
44
+
45
+ container.register(:bus, Async::Bus.new(:__async_app))
46
+
47
+ set_traps!
48
+ @task = Async::Task.current
49
+ container_config.each { container.register(_1, _2) }
50
+ run!
51
+ info { "Started" }
52
+ rescue StandardError => e
53
+ fatal { e }
54
+ stop
55
+ exit(1)
56
+ end
57
+ # rubocop:enable Style/GlobalVars
58
+
59
+ def container = @container ||= Dry::Container.new
60
+ def run! = nil
61
+ def container_config = {}
62
+
63
+ def stop
64
+ @task&.stop
65
+ info { "Stopped" }
66
+ end
67
+
68
+ private
69
+
70
+ def set_traps!
71
+ trap("INT") do
72
+ force_exit! if @stopping
73
+ @stopping = true
74
+ warn { "Interrupted, stopping. Press ^C once more to force exit." }
75
+ stop
76
+ end
77
+
78
+ trap("TERM") { stop }
79
+ end
80
+
81
+ def force_exit!
82
+ fatal { "Forced exit" }
83
+ exit(1)
84
+ end
85
+ end
data/lib/async/bus.rb CHANGED
@@ -1,12 +1,47 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Threading is not supported!
4
- module Async::Bus
5
- class << self
6
- def get(name = :default)
7
- @buses ||= {}
8
- @buses[name] ||= Bus.new(name:)
9
- @buses[name]
3
+ class Async::Bus
4
+ include Async::Logger
5
+ # dry-events is not a dependency of async-tools on purpose.
6
+ # add it to your bundle yourself
7
+
8
+ # Semantics:
9
+ # - Lazily registeres events
10
+ # - Synchronous by default
11
+ # - Catches exceptions in subscribers, logs them
12
+ def initialize(name)
13
+ @name = name
14
+ @w = Class.new.include(Dry::Events::Publisher[name]).new
15
+ end
16
+
17
+ # BLOCKING unless subscribers run in tasks
18
+ def publish(name, *args, **params)
19
+ @w.register_event(name)
20
+ @w.publish(name, payload: (args.first || params))
21
+ rescue StandardError => e
22
+ log_error(name, e)
23
+ end
24
+
25
+ # NON-BLOCKING
26
+ def subscribe(name)
27
+ @w.register_event(name)
28
+ @w.subscribe(name) { yield(_1[:payload]) }
29
+ end
30
+
31
+ # NON-BLOCKING, runs subscriber in a task
32
+ def async_subscribe(name, parent: Async::Task.current)
33
+ subscribe(name) do |event|
34
+ parent.async do
35
+ yield(event)
36
+ rescue StandardError => e
37
+ log_error(name, e)
38
+ end
10
39
  end
11
40
  end
41
+
42
+ def convert(from_event, to_event) = subscribe(from_event) { publish(to_event, **yield(_1)) }
43
+
44
+ private
45
+
46
+ def log_error(name, e) = warn("Subscriber for #{name.inspect} failed with exception.", e)
12
47
  end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Async::Logger
4
+ [:debug, :info, :warn, :error, :fatal].each do |name|
5
+ define_method(name) do |*args, &block|
6
+ info = respond_to?(:logger_info, true) ? logger_info : nil
7
+
8
+ Console.logger.public_send(name, self, info, *args, &block)
9
+ end
10
+ end
11
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Async # rubocop:disable Style/ClassAndModuleChildren
4
4
  module Tools
5
- VERSION = "0.1.10"
5
+ VERSION = "0.2.1"
6
6
  end
7
7
  end
data/lib/async/tools.rb CHANGED
@@ -12,6 +12,7 @@ loader = Zeitwerk::Loader.new
12
12
  loader.tag = File.basename(__FILE__, ".rb")
13
13
  loader.inflector = Zeitwerk::GemInflector.new(__FILE__)
14
14
  loader.push_dir(File.expand_path("..", __dir__.to_s))
15
+
15
16
  loader.setup
16
17
 
17
18
  module Async
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: async-tools
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.10
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gleb Sinyavskiy
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-03-02 00:00:00.000000000 Z
11
+ date: 2023-03-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: async
@@ -60,9 +60,10 @@ files:
60
60
  - async-tools.gemspec
61
61
  - bin/console
62
62
  - bin/setup
63
+ - lib/async/app.rb
63
64
  - lib/async/bus.rb
64
- - lib/async/bus/bus.rb
65
65
  - lib/async/channel.rb
66
+ - lib/async/logger.rb
66
67
  - lib/async/q.rb
67
68
  - lib/async/result_notification.rb
68
69
  - lib/async/timer.rb
data/lib/async/bus/bus.rb DELETED
@@ -1,95 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class Async::Bus::Bus
4
- attr_reader :name
5
-
6
- include Console
7
-
8
- def initialize(name: :default, limit: 10, parent: Async::Task.current)
9
- @name = name
10
- @limit = limit
11
- @parent = parent
12
-
13
- @closed = false
14
-
15
- @subscribers = Hash.new { |hash, key| hash[key] = [] }
16
- end
17
-
18
- # Blocks if any of output channels is full!
19
- def publish(nameable_or_event, **payload)
20
- check_if_open!
21
- name, payload = normalize(nameable_or_event, payload)
22
-
23
- subs = @subscribers[name]
24
- return if subs.empty?
25
-
26
- subs.each do |chan|
27
- publishing_blocked if chan.full?
28
- chan << [name, payload]
29
- end
30
- Async::Task.current.yield
31
- end
32
-
33
- # Blocks!
34
- def subscribe(nameable, callable = nil, &block)
35
- check_if_open!
36
- callable ||= block
37
- unless callable.respond_to?(:call)
38
- raise ArgumentError, "callable or block must be provided. callable must respond to :call"
39
- end
40
-
41
- event_name = normalize(nameable).first
42
-
43
- chan = Async::Channel.new(@limit)
44
- @subscribers[event_name] << chan
45
- serve(chan, event_name, callable)
46
- end
47
-
48
- def async_subscribe(*, **, &) = @parent.async { subscribe(*, **, &) }
49
- def on_event(&block) = @on_event_callback = block
50
-
51
- def close
52
- return if @closed
53
-
54
- @closed = true
55
-
56
- @subscribers.values.flatten.each(&:close)
57
- @subscribers.clear
58
- end
59
-
60
- private
61
-
62
- def normalize(nameable, payload = nil)
63
- return [nameable, payload] if nameable.is_a?(Symbol)
64
- return [nameable.to_sym, payload] if nameable.is_a?(String)
65
- return [nameable.event_name.to_sym, nameable] if nameable.respond_to?(:event_name)
66
-
67
- n = nameable[:event_name] || nameable["event_name"]
68
- return [n.to_sym, nameable] if n
69
-
70
- raise ArgumentError, "cannot infer event name from #{nameable.inspect}"
71
- end
72
-
73
- def serve(chan, event_name, callable)
74
- stopped = false
75
- unsub = lambda {
76
- chan.close
77
- stopped = true
78
- @subscribers[event_name].delete(chan)
79
- }
80
-
81
- chan.each do |name, payload|
82
- @on_event_callback&.call(wrapper)
83
- callable.call(payload, unsub:, meta: { bus: self })
84
- break if stopped
85
- end
86
- end
87
-
88
- def publishing_blocked
89
- logger.warn(self) { "One of the subscribers is slow, blocking publishing. Event name: #{name}" }
90
- end
91
-
92
- def check_if_open!
93
- raise "Bus is closed" if @closed
94
- end
95
- end