async-tools 0.1.10 → 0.2.1

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: 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