local_bus 0.2.0 → 0.3.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.
@@ -5,6 +5,8 @@
5
5
  class LocalBus
6
6
  # Wraps a Callable (Proc) and Message intended for asynchronous execution.
7
7
  class Subscriber
8
+ include MonitorMixin
9
+
8
10
  # Custom error class for Subscriber errors
9
11
  class Error < StandardError
10
12
  # Constructor
@@ -24,6 +26,7 @@ class LocalBus
24
26
  # @rbs callable: #call -- the subscriber's callable object
25
27
  # @rbs message: Message -- the message to be processed
26
28
  def initialize(callable, message)
29
+ super()
27
30
  @callable = callable
28
31
  @message = message
29
32
  @id = callable.object_id
@@ -68,21 +71,29 @@ class LocalBus
68
71
  metadata.any?
69
72
  end
70
73
 
71
- # Checks if the subscriber is pending
74
+ # Indicates if the subscriber is pending or unperformed
72
75
  # @rbs return: bool
73
76
  def pending?
74
77
  metadata.empty?
75
78
  end
76
79
 
80
+ # Indicates if the subscriber has errored
81
+ # @rbs return: bool
82
+ def errored?
83
+ !!error
84
+ end
85
+
77
86
  # Performs the subscriber's callable
78
87
  # @rbs return: void
79
88
  def perform
80
- return if performed?
81
-
82
- with_metadata do
83
- @value = callable.call(message)
84
- rescue => cause
85
- @error = Error.new("Invocation failed! #{cause.message}", cause: cause)
89
+ synchronize do
90
+ return if performed?
91
+
92
+ with_metadata do
93
+ @value = callable.call(message)
94
+ rescue => cause
95
+ @error = Error.new("Invocation failed! #{cause.message}", cause: cause)
96
+ end
86
97
  end
87
98
  end
88
99
 
@@ -129,7 +140,7 @@ class LocalBus
129
140
  finished_at: Time.now,
130
141
  duration: Time.now - started_at,
131
142
  latency: Time.now - message.created_at,
132
- message: message
143
+ message: message.to_h
133
144
  }.freeze
134
145
  end
135
146
  end
@@ -3,5 +3,5 @@
3
3
  # rbs_inline: enabled
4
4
 
5
5
  class LocalBus
6
- VERSION = "0.2.0"
6
+ VERSION = "0.3.0"
7
7
  end
data/lib/local_bus.rb CHANGED
@@ -6,20 +6,71 @@ require "zeitwerk"
6
6
  loader = Zeitwerk::Loader.for_gem
7
7
  loader.setup
8
8
 
9
+ require "algorithms"
9
10
  require "async"
10
11
  require "async/barrier"
11
12
  require "async/semaphore"
12
- require "concurrent-ruby"
13
+ require "etc"
13
14
  require "monitor"
14
15
  require "securerandom"
15
16
  require "singleton"
17
+ require "timers"
16
18
 
17
19
  class LocalBus
18
20
  include Singleton
19
21
 
22
+ # Default Bus instance
23
+ # @rbs return: Bus
20
24
  attr_reader :bus
25
+
26
+ # Default Station instance
27
+ # @rbs return: Station
21
28
  attr_reader :station
22
29
 
30
+ class << self
31
+ # Publishes a message via the default Station
32
+ #
33
+ # @rbs topic: String | Symbol -- topic name
34
+ # @rbs priority: Integer -- priority of the message, higher number == higher priority (default: 1)
35
+ # @rbs timeout: Float -- seconds to wait before cancelling
36
+ # @rbs payload: Hash[Symbol, untyped] -- message payload
37
+ # @rbs return: Message
38
+ def publish(...)
39
+ instance.station.publish(...)
40
+ end
41
+
42
+ # Publishes a pre-built message via the default Station
43
+ # @rbs message: Message -- message to publish
44
+ # @rbs return: Message
45
+ def publish_message(...)
46
+ instance.station.publish_message(...)
47
+ end
48
+
49
+ # Subscribe to a topic via the default Station
50
+ # @rbs topic: String -- topic name
51
+ # @rbs callable: (Message) -> untyped -- callable that will process messages published to the topic
52
+ # @rbs &block: (Message) -> untyped -- alternative way to provide a callable
53
+ # @rbs return: Station
54
+ def subscribe(...)
55
+ instance.station.subscribe(...)
56
+ end
57
+
58
+ # Unsubscribes a callable from a topic via the default Station
59
+ # @rbs topic: String -- topic name
60
+ # @rbs callable: (Message) -> untyped -- subscriber that should no longer receive messages
61
+ # @rbs return: Station
62
+ def unsubscribe(...)
63
+ instance.station.unsubscribe(...)
64
+ end
65
+
66
+ # Unsubscribes all subscribers from a topic and removes the topic via the default Station
67
+ # @rbs topic: String -- topic name
68
+ # @rbs return: Station
69
+ def unsubscribe_all(...)
70
+ instance.station.unsubscribe_all(...)
71
+ end
72
+ end
73
+
23
74
  private
24
75
 
25
76
  def initialize
@@ -0,0 +1,83 @@
1
+ # Generated from lib/local_bus/bus.rb with RBS::Inline
2
+
3
+ class LocalBus
4
+ # The Bus acts as a direct transport mechanism for messages, akin to placing a passenger directly onto a bus.
5
+ # When a message is published to the Bus, it is immediately delivered to all subscribers, ensuring prompt execution of tasks.
6
+ # This is achieved through non-blocking I/O operations, which allow the Bus to handle multiple tasks efficiently without blocking the main thread.
7
+ #
8
+ # @note While the Bus uses asynchronous operations to optimize performance,
9
+ # the actual processing of a message may still experience slight delays due to I/O wait times from prior messages.
10
+ # This means that while the Bus aims for immediate processing, the nature of asynchronous operations can introduce some latency.
11
+ class Bus
12
+ include MonitorMixin
13
+
14
+ # Constructor
15
+ # @note Creates a new Bus instance with specified max concurrency (i.e. number of tasks that can run in parallel)
16
+ # @rbs concurrency: Integer -- maximum number of concurrent tasks (default: Etc.nprocessors)
17
+ def initialize: (?concurrency: Integer) -> untyped
18
+
19
+ # Maximum number of concurrent tasks that can run in "parallel"
20
+ # @rbs return: Integer
21
+ def concurrency: () -> Integer
22
+
23
+ # Sets the max concurrency
24
+ # @rbs value: Integer -- max number of concurrent tasks that can run in "parallel"
25
+ # @rbs return: Integer -- new concurrency value
26
+ def concurrency=: (Integer value) -> Integer
27
+
28
+ # Registered topics that have subscribers
29
+ # @rbs return: Array[String] -- list of topic names
30
+ def topics: () -> Array[String]
31
+
32
+ # Registered subscriptions
33
+ # @rbs return: Hash[String, Array[callable]] -- mapping of topics to callables
34
+ def subscriptions: () -> Hash[String, Array[callable]]
35
+
36
+ # Subscribes a callable to a topic
37
+ # @rbs topic: String -- topic name
38
+ # @rbs callable: (Message) -> untyped -- callable that will process messages published to the topic
39
+ # @rbs &block: (Message) -> untyped -- alternative way to provide a callable
40
+ # @rbs return: self
41
+ # @raise [ArgumentError] if neither callable nor block is provided
42
+ def subscribe: (String topic, ?callable: Message) { (Message) -> untyped } -> self
43
+
44
+ # Unsubscribes a callable from a topic
45
+ # @rbs topic: String -- topic name
46
+ # @rbs callable: (Message) -> untyped -- subscriber that should no longer receive messages
47
+ # @rbs return: self
48
+ def unsubscribe: (String topic, callable: Message) -> self
49
+
50
+ # Unsubscribes all subscribers from a topic and removes the topic
51
+ # @rbs topic: String -- topic name
52
+ # @rbs return: self
53
+ def unsubscribe_all: (String topic) -> self
54
+
55
+ # Executes a block and unsubscribes all subscribers from the topic afterwards
56
+ # @rbs topic: String -- topic name
57
+ # @rbs block: (String) -> void -- block to execute (yields the topic)
58
+ def with_topic: (String topic) ?{ (?) -> untyped } -> untyped
59
+
60
+ # Publishes a message
61
+ #
62
+ # @note If subscribers are rapidly created/destroyed mid-publish, there's a theoretical
63
+ # possibility of object_id reuse. However, this is extremely unlikely in practice.
64
+ #
65
+ # * If subscribers are added mid-publish, they will not receive the message
66
+ # * If subscribers are removed mid-publish, they will still receive the message
67
+ #
68
+ # @note If the timeout is exceeded, the task will be cancelled before all subscribers have completed.
69
+ #
70
+ # Check individual Subscribers for possible errors.
71
+ #
72
+ # @rbs topic: String -- topic name
73
+ # @rbs timeout: Float -- seconds to wait for subscribers to process the message before cancelling (default: 60)
74
+ # @rbs payload: Hash -- message payload
75
+ # @rbs return: Message
76
+ def publish: (String topic, ?timeout: Float, **untyped payload) -> Message
77
+
78
+ # Publishes a pre-built message
79
+ # @rbs message: Message -- message to publish
80
+ # @rbs return: Message
81
+ def publish_message: (Message message) -> Message
82
+ end
83
+ end
@@ -0,0 +1,60 @@
1
+ # Generated from lib/local_bus/message.rb with RBS::Inline
2
+
3
+ class LocalBus
4
+ # Represents a message in the LocalBus system
5
+ class Message
6
+ # Constructor
7
+ # @note Creates a new Message instance with the given topic and payload
8
+ # @rbs topic: String -- the topic of the message
9
+ # @rbs timeout: Float? -- optional timeout for message processing (in seconds)
10
+ # @rbs payload: Hash -- the message payload
11
+ def initialize: (String topic, ?timeout: Float?, **untyped payload) -> untyped
12
+
13
+ # Metadata for the message
14
+ # @rbs return: Hash[Symbol, untyped]
15
+ attr_reader metadata: untyped
16
+
17
+ # Publication representing the Async barrier and subscribers handling the message
18
+ # @note May be nil if processing hasn't happened yet (e.g. it was published via Station)
19
+ # @rbs return: Publication?
20
+ attr_accessor publication: untyped
21
+
22
+ # Unique identifier for the message
23
+ # @rbs return: String
24
+ def id: () -> String
25
+
26
+ # Message topic
27
+ # @rbs return: String
28
+ def topic: () -> String
29
+
30
+ # Message payload
31
+ # @rbs return: Hash
32
+ def payload: () -> Hash
33
+
34
+ # Time when the message was created or published
35
+ # @rbs return: Time
36
+ def created_at: () -> Time
37
+
38
+ # ID of the thread that created the message
39
+ # @rbs return: Integer
40
+ def thread_id: () -> Integer
41
+
42
+ # Timeout for message processing (in seconds)
43
+ # @rbs return: Float
44
+ def timeout: () -> Float
45
+
46
+ # Blocks and waits for the message to process
47
+ # @rbs interval: Float -- time to wait between checks (default: 0.1)
48
+ # @rbs return: void
49
+ def wait: (?interval: Float) -> void
50
+
51
+ # Blocks and waits for the message process then returns all subscribers
52
+ # @rbs return: Array[Subscriber]
53
+ def subscribers: () -> Array[Subscriber]
54
+
55
+ # Allows pattern matching on message attributes
56
+ # @rbs keys: Array[Symbol] -- keys to extract from the message
57
+ # @rbs return: Hash[Symbol, untyped]
58
+ def deconstruct_keys: (Array[Symbol] keys) -> Hash[Symbol, untyped]
59
+ end
60
+ end
@@ -0,0 +1,20 @@
1
+ # Generated from lib/local_bus/publication.rb with RBS::Inline
2
+
3
+ class LocalBus
4
+ # Wraps an Async::Barrier and a list of Subscribers that are processing a Message.
5
+ class Publication
6
+ # Constructor
7
+ # @rbs barrier: Async::Barrier -- barrier used to wait for all subscribers
8
+ # @rbs subscribers: Array[Subscriber]
9
+ def initialize: (Async::Barrier barrier, *untyped subscribers) -> untyped
10
+
11
+ # Blocks and waits for the barrier (i.e. all subscribers to complete)
12
+ # @rbs return: void
13
+ def wait: () -> void
14
+
15
+ # List of Subscribers that are processing a Message
16
+ # @note Blocks until all subscribers complete
17
+ # @rbs return: Array[Subscriber]
18
+ def subscribers: () -> Array[Subscriber]
19
+ end
20
+ end
@@ -0,0 +1,113 @@
1
+ # Generated from lib/local_bus/station.rb with RBS::Inline
2
+
3
+ class LocalBus
4
+ # The Station serves as a queuing system for messages, similar to a bus station where passengers wait for their bus.
5
+ #
6
+ # When a message is published to the Station, it is queued and processed at a later time, allowing for deferred execution.
7
+ # This is particularly useful for tasks that can be handled later.
8
+ #
9
+ # The Station employs a thread pool to manage message processing, enabling high concurrency and efficient resource utilization.
10
+ # Messages can also be prioritized, ensuring that higher-priority tasks are processed first.
11
+ #
12
+ # @note: While the Station provides a robust mechanism for background processing,
13
+ # it's important to understand that the exact timing of message processing is not controlled by the publisher,
14
+ # and messages will be processed as resources become available.
15
+ class Station
16
+ include MonitorMixin
17
+
18
+ class CapacityError < StandardError
19
+ end
20
+
21
+ # Constructor
22
+ #
23
+ # @note Delays process exit in an attempt to flush the queue to avoid dropping messages.
24
+ # Exit flushing makes a "best effort" to process all messages, but it's not guaranteed.
25
+ # Will not delay process exit when the queue is empty.
26
+ #
27
+ # @rbs bus: Bus -- local message bus (default: Bus.new)
28
+ # @rbs interval: Float -- queue polling interval in seconds (default: 0.01)
29
+ # @rbs limit: Integer -- max queue size (default: 10_000)
30
+ # @rbs threads: Integer -- number of threads to use (default: Etc.nprocessors)
31
+ # @rbs timeout: Float -- seconds to wait for subscribers to process the message before cancelling (default: 60)
32
+ # @rbs wait: Float -- seconds to wait for the queue to flush at process exit (default: 5)
33
+ # @rbs return: void
34
+ def initialize: (?bus: Bus, ?interval: Float, ?limit: Integer, ?threads: Integer, ?timeout: Float, ?wait: Float) -> void
35
+
36
+ # Bus instance
37
+ # @rbs return: Bus
38
+ attr_reader bus: untyped
39
+
40
+ # Queue polling interval in seconds
41
+ # @rbs return: Float
42
+ attr_reader interval: untyped
43
+
44
+ # Max queue size
45
+ # @rbs return: Integer
46
+ attr_reader limit: untyped
47
+
48
+ # Number of threads to use
49
+ # @rbs return: Integer
50
+ attr_reader threads: untyped
51
+
52
+ # Default timeout for message processing (in seconds)
53
+ # @rbs return: Float
54
+ attr_reader timeout: untyped
55
+
56
+ # Starts the station
57
+ # @rbs interval: Float -- queue polling interval in seconds (default: 0.01)
58
+ # @rbs threads: Integer -- number of threads to use (default: self.threads)
59
+ # @rbs return: void
60
+ def start: (?interval: Float, ?threads: Integer) -> void
61
+
62
+ # Stops the station
63
+ # @rbs timeout: Float -- seconds to wait for message processing before killing the thread pool (default: nil)
64
+ # @rbs return: void
65
+ def stop: (?timeout: Float) -> void
66
+
67
+ def stopping?: () -> untyped
68
+
69
+ # Indicates if the station is running
70
+ # @rbs return: bool
71
+ def running?: () -> bool
72
+
73
+ # Indicates if the queue is empty
74
+ # @rbs return: bool
75
+ def empty?: () -> bool
76
+
77
+ # Number of unprocessed messages in the queue
78
+ # @rbs return: Integer
79
+ def count: () -> Integer
80
+
81
+ # Subscribe to a topic
82
+ # @rbs topic: String -- topic name
83
+ # @rbs callable: (Message) -> untyped -- callable that will process messages published to the topic
84
+ # @rbs &block: (Message) -> untyped -- alternative way to provide a callable
85
+ # @rbs return: self
86
+ def subscribe: () { (Message) -> untyped } -> self
87
+
88
+ # Unsubscribes a callable from a topic
89
+ # @rbs topic: String -- topic name
90
+ # @rbs callable: (Message) -> untyped -- subscriber that should no longer receive messages
91
+ # @rbs return: self
92
+ def unsubscribe: () -> self
93
+
94
+ # Unsubscribes all subscribers from a topic and removes the topic
95
+ # @rbs topic: String -- topic name
96
+ # @rbs return: self
97
+ def unsubscribe_all: () -> self
98
+
99
+ # Publishes a message
100
+ #
101
+ # @rbs topic: String | Symbol -- topic name
102
+ # @rbs priority: Integer -- priority of the message, higher number == higher priority (default: 1)
103
+ # @rbs timeout: Float -- seconds to wait before cancelling
104
+ # @rbs payload: Hash[Symbol, untyped] -- message payload
105
+ # @rbs return: Message
106
+ def publish: (String | Symbol topic, ?priority: Integer, ?timeout: Float, **untyped payload) -> Message
107
+
108
+ # Publishes a pre-built message
109
+ # @rbs message: Message -- message to publish
110
+ # @rbs return: Message
111
+ def publish_message: (Message message, ?priority: untyped) -> Message
112
+ end
113
+ end
@@ -0,0 +1,89 @@
1
+ # Generated from lib/local_bus/subscriber.rb with RBS::Inline
2
+
3
+ class LocalBus
4
+ # Wraps a Callable (Proc) and Message intended for asynchronous execution.
5
+ class Subscriber
6
+ include MonitorMixin
7
+
8
+ # Custom error class for Subscriber errors
9
+ class Error < StandardError
10
+ # Constructor
11
+ # @rbs message: String -- error message
12
+ # @rbs cause: StandardError? -- underlying cause of the error
13
+ def initialize: (String message, cause: StandardError?) -> untyped
14
+
15
+ # Underlying cause of the error
16
+ # @rbs return: StandardError?
17
+ attr_reader cause: untyped
18
+ end
19
+
20
+ # Constructor
21
+ # @rbs callable: #call -- the subscriber's callable object
22
+ # @rbs message: Message -- the message to be processed
23
+ def initialize: (untyped callable, Message message) -> untyped
24
+
25
+ # Unique identifier for the subscriber
26
+ # @rbs return: String
27
+ attr_reader id: untyped
28
+
29
+ # Source location of the callable
30
+ # @rbs return: Array[String, Integer]?
31
+ attr_reader source_location: untyped
32
+
33
+ # Callable object -- Proc, lambda, etc. (must respond to #call)
34
+ # @rbs return: #call
35
+ attr_reader callable: untyped
36
+
37
+ # Error if the subscriber fails (available after performing)
38
+ # @rbs return: Error?
39
+ attr_reader error: untyped
40
+
41
+ # Message for the subscriber to process
42
+ # @rbs return: Message
43
+ attr_reader message: untyped
44
+
45
+ # Metadata for the subscriber (available after performing)
46
+ # @rbs return: Hash[Symbol, untyped]
47
+ attr_reader metadata: untyped
48
+
49
+ # Value returned by the callable (available after performing)
50
+ # @rbs return: untyped
51
+ attr_reader value: untyped
52
+
53
+ # Indicates if the subscriber has been performed
54
+ # @rbs return: bool
55
+ def performed?: () -> bool
56
+
57
+ # Indicates if the subscriber is pending or unperformed
58
+ # @rbs return: bool
59
+ def pending?: () -> bool
60
+
61
+ # Indicates if the subscriber has errored
62
+ # @rbs return: bool
63
+ def errored?: () -> bool
64
+
65
+ # Performs the subscriber's callable
66
+ # @rbs return: void
67
+ def perform: () -> void
68
+
69
+ # Handles timeout for the subscriber
70
+ # @rbs cause: StandardError -- the cause of the timeout
71
+ # @rbs return: void
72
+ def timeout: (StandardError cause) -> void
73
+
74
+ # Returns the subscriber's data as a hash
75
+ # @rbs return: Hash[Symbol, untyped]
76
+ def to_h: () -> Hash[Symbol, untyped]
77
+
78
+ # Allows pattern matching on subscriber attributes
79
+ # @rbs keys: Array[Symbol] -- keys to extract from the subscriber
80
+ # @rbs return: Hash[Symbol, untyped]
81
+ def deconstruct_keys: (Array[Symbol] keys) -> Hash[Symbol, untyped]
82
+
83
+ private
84
+
85
+ # Captures metadata for the subscriber's performance
86
+ # @rbs return: void
87
+ def with_metadata: () -> void
88
+ end
89
+ end
@@ -0,0 +1,5 @@
1
+ # Generated from lib/local_bus/version.rb with RBS::Inline
2
+
3
+ class LocalBus
4
+ VERSION: ::String
5
+ end
@@ -0,0 +1,49 @@
1
+ # Generated from lib/local_bus.rb with RBS::Inline
2
+
3
+ class LocalBus
4
+ include Singleton
5
+
6
+ # Default Bus instance
7
+ # @rbs return: Bus
8
+ attr_reader bus: untyped
9
+
10
+ # Default Station instance
11
+ # @rbs return: Station
12
+ attr_reader station: untyped
13
+
14
+ # Publishes a message via the default Station
15
+ #
16
+ # @rbs topic: String | Symbol -- topic name
17
+ # @rbs priority: Integer -- priority of the message, higher number == higher priority (default: 1)
18
+ # @rbs timeout: Float -- seconds to wait before cancelling
19
+ # @rbs payload: Hash[Symbol, untyped] -- message payload
20
+ # @rbs return: Message
21
+ def self.publish: () -> Message
22
+
23
+ # Publishes a pre-built message via the default Station
24
+ # @rbs message: Message -- message to publish
25
+ # @rbs return: Message
26
+ def self.publish_message: () -> Message
27
+
28
+ # Subscribe to a topic via the default Station
29
+ # @rbs topic: String -- topic name
30
+ # @rbs callable: (Message) -> untyped -- callable that will process messages published to the topic
31
+ # @rbs &block: (Message) -> untyped -- alternative way to provide a callable
32
+ # @rbs return: Station
33
+ def self.subscribe: () { (Message) -> untyped } -> Station
34
+
35
+ # Unsubscribes a callable from a topic via the default Station
36
+ # @rbs topic: String -- topic name
37
+ # @rbs callable: (Message) -> untyped -- subscriber that should no longer receive messages
38
+ # @rbs return: Station
39
+ def self.unsubscribe: () -> Station
40
+
41
+ # Unsubscribes all subscribers from a topic and removes the topic via the default Station
42
+ # @rbs topic: String -- topic name
43
+ # @rbs return: Station
44
+ def self.unsubscribe_all: () -> Station
45
+
46
+ private
47
+
48
+ def initialize: () -> untyped
49
+ end