async-bus 0.1.1 → 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.
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Samuel Williams.
5
+
6
+ require "msgpack"
7
+ require_relative "proxy"
8
+
9
+ module Async
10
+ module Bus
11
+ module Protocol
12
+ # Represents a method invocation.
13
+ class Invoke
14
+ # Initialize a new invocation message.
15
+ # @parameter id [Integer] The transaction ID.
16
+ # @parameter name [Symbol] The method name to invoke.
17
+ # @parameter arguments [Array] The positional arguments.
18
+ # @parameter options [Hash] The keyword arguments.
19
+ # @parameter block_given [Boolean] Whether a block was provided.
20
+ def initialize(id, name, arguments, options, block_given)
21
+ @id = id
22
+ @name = name
23
+ @arguments = arguments
24
+ @options = options
25
+ @block_given = block_given
26
+ end
27
+
28
+ # @attribute [Integer] The transaction ID.
29
+ attr :id
30
+
31
+ # @attribute [Symbol] The method name.
32
+ attr :name
33
+
34
+ # @attribute [Array] The positional arguments.
35
+ attr :arguments
36
+
37
+ # @attribute [Hash] The keyword arguments.
38
+ attr :options
39
+
40
+ # @attribute [Boolean] Whether a block was provided.
41
+ attr :block_given
42
+
43
+ # Pack the invocation into a MessagePack packer.
44
+ # @parameter packer [MessagePack::Packer] The packer to write to.
45
+ def pack(packer)
46
+ packer.write(@id)
47
+ packer.write(@name)
48
+
49
+ packer.write(@arguments.size)
50
+ @arguments.each do |argument|
51
+ packer.write(argument)
52
+ end
53
+
54
+ packer.write(@options.size)
55
+ @options.each do |key, value|
56
+ packer.write(key)
57
+ packer.write(value)
58
+ end
59
+
60
+ packer.write(@block_given)
61
+ end
62
+
63
+ # Unpack an invocation from a MessagePack unpacker.
64
+ # @parameter unpacker [MessagePack::Unpacker] The unpacker to read from.
65
+ # @returns [Invoke] A new invocation instance.
66
+ def self.unpack(unpacker)
67
+ id = unpacker.read
68
+ name = unpacker.read
69
+ arguments = Array.new(unpacker.read){unpacker.read}
70
+ options = Array.new(unpacker.read){[unpacker.read, unpacker.read]}.to_h
71
+ block_given = unpacker.read
72
+
73
+ return self.new(id, name, arguments, options, block_given)
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -1,74 +1,77 @@
1
- # Copyright, 2018, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
- #
3
- # Permission is hereby granted, free of charge, to any person obtaining a copy
4
- # of this software and associated documentation files (the "Software"), to deal
5
- # in the Software without restriction, including without limitation the rights
6
- # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
- # copies of the Software, and to permit persons to whom the Software is
8
- # furnished to do so, subject to the following conditions:
9
- #
10
- # The above copyright notice and this permission notice shall be included in
11
- # all copies or substantial portions of the Software.
12
- #
13
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
- # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
- # THE SOFTWARE.
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2021-2025, by Samuel Williams.
20
5
 
21
6
  module Async
22
7
  module Bus
23
8
  module Protocol
9
+ # A proxy object that forwards method calls to a remote object.
10
+ #
11
+ # We must be extremely careful not to invoke any methods on the proxy object that would recursively call the proxy object.
24
12
  class Proxy < BasicObject
13
+ # Create a new proxy object.
14
+ #
15
+ # @parameter connection [Connection] The connection to the remote object.
16
+ # @parameter name [Symbol] The name (address) of the remote object.
25
17
  def initialize(connection, name)
26
18
  @connection = connection
27
19
  @name = name
28
20
  end
29
21
 
22
+ # Get the connection to the remote object.
23
+ # @returns [Connection] The connection to the remote object.
24
+ def __connection__
25
+ @connection
26
+ end
27
+
28
+ # Get the name of the remote object.
29
+ # @returns [Symbol] The name of the remote object.
30
+ def __name__
31
+ @name
32
+ end
33
+
34
+ # Logical negation operator.
35
+ # @returns [Object] The result of the negation.
30
36
  def !
31
37
  @connection.invoke(@name, [:!])
32
38
  end
33
39
 
40
+ # Equality operator.
41
+ # @parameter object [Object] The object to compare with.
42
+ # @returns [Boolean] True if equal.
34
43
  def == object
35
44
  @connection.invoke(@name, [:==, object])
36
45
  end
37
46
 
47
+ # Inequality operator.
48
+ # @parameter object [Object] The object to compare with.
49
+ # @returns [Boolean] True if not equal.
38
50
  def != object
39
51
  @connection.invoke(@name, [:!=, object])
40
52
  end
41
53
 
42
- def eql?(other)
43
- self.equal?(other)
44
- end
45
-
46
- def methods(all = true)
47
- @connection.invoke(@name, [:methods, all]) | super
48
- end
49
-
50
- def protected_methods(all = true)
51
- @connection.invoke(@name, [:protected_methods, all]) | super
52
- end
53
-
54
- def public_methods(all = true)
55
- @connection.invoke(@name, [:public_methods, all]) | super
56
- end
57
-
58
- def inspect
59
- "[Proxy (#{@name}) #{method_missing(:inspect)}]"
60
- end
61
-
54
+ # Forward method calls to the remote object.
55
+ # @parameter arguments [Array] The method arguments.
56
+ # @parameter options [Hash] The keyword arguments.
57
+ # @yields {|*args| ...} Optional block to pass to the method.
58
+ # @returns [Object] The result of the method call.
62
59
  def method_missing(*arguments, **options, &block)
63
60
  @connection.invoke(@name, arguments, options, &block)
64
61
  end
65
62
 
63
+ # Check if the remote object responds to a method.
64
+ # @parameter name [Symbol] The method name to check.
65
+ # @parameter include_all [Boolean] Whether to include private methods.
66
+ # @returns [Boolean] True if the method exists.
66
67
  def respond_to?(name, include_all = false)
67
68
  @connection.invoke(@name, [:respond_to?, name, include_all])
68
69
  end
69
70
 
70
- def respond_to_missing?(name, include_all = false)
71
- @connection.invoke(@name, [:respond_to?, name, include_all]) || super
71
+ # Return a string representation of the proxy.
72
+ # @returns [String] A string describing the proxy.
73
+ def inspect
74
+ "#<proxy #{@name}>"
72
75
  end
73
76
  end
74
77
  end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Samuel Williams.
5
+
6
+ module Async
7
+ module Bus
8
+ module Protocol
9
+ # Represents a named object that has been released (no longer available).
10
+ class Release
11
+ # Initialize a new release message.
12
+ # @parameter name [Symbol] The name of the released object.
13
+ def initialize(name)
14
+ @name = name
15
+ end
16
+
17
+ # @attribute [Symbol] The name of the released object.
18
+ attr :name
19
+
20
+ # Pack the release into a MessagePack packer.
21
+ # @parameter packer [MessagePack::Packer] The packer to write to.
22
+ def pack(packer)
23
+ packer.write(@name)
24
+ end
25
+
26
+ # Unpack a release from a MessagePack unpacker.
27
+ # @parameter unpacker [MessagePack::Unpacker] The unpacker to read from.
28
+ # @returns [Release] A new release instance.
29
+ def self.unpack(unpacker)
30
+ name = unpacker.read
31
+
32
+ return self.new(name)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Samuel Williams.
5
+
6
+ module Async
7
+ module Bus
8
+ module Protocol
9
+ # Represents a response message from a remote procedure call.
10
+ class Response
11
+ # Initialize a new response message.
12
+ # @parameter id [Integer] The transaction ID.
13
+ # @parameter result [Object] The result value.
14
+ def initialize(id, result)
15
+ @id = id
16
+ @result = result
17
+ end
18
+
19
+ # @attribute [Integer] The transaction ID.
20
+ attr :id
21
+
22
+ # @attribute [Object] The result value.
23
+ attr :result
24
+
25
+ # Pack the response into a MessagePack packer.
26
+ # @parameter packer [MessagePack::Packer] The packer to write to.
27
+ def pack(packer)
28
+ packer.write(@id)
29
+ packer.write(@result)
30
+ end
31
+
32
+ # Unpack a response from a MessagePack unpacker.
33
+ # @parameter unpacker [MessagePack::Unpacker] The unpacker to read from.
34
+ # @returns [Response] A new response instance.
35
+ def self.unpack(unpacker)
36
+ id = unpacker.read
37
+ result = unpacker.read
38
+
39
+ return self.new(id, result)
40
+ end
41
+ end
42
+
43
+ Return = Class.new(Response)
44
+ Yield = Class.new(Response)
45
+ Error = Class.new(Response)
46
+ Next = Class.new(Response)
47
+ Throw = Class.new(Response)
48
+ Close = Class.new(Response)
49
+ end
50
+ end
51
+ end
@@ -1,121 +1,151 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Copyright, 2021, by Samuel G. D. Williams. <http://www.codeotaku.com>
4
- #
5
- # Permission is hereby granted, free of charge, to any person obtaining a copy
6
- # of this software and associated documentation files (the "Software"), to deal
7
- # in the Software without restriction, including without limitation the rights
8
- # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- # copies of the Software, and to permit persons to whom the Software is
10
- # furnished to do so, subject to the following conditions:
11
- #
12
- # The above copyright notice and this permission notice shall be included in
13
- # all copies or substantial portions of the Software.
14
- #
15
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
- # THE SOFTWARE.
3
+ # Released under the MIT License.
4
+ # Copyright, 2021-2025, by Samuel Williams.
22
5
 
23
- require 'async/queue'
6
+ require "async/queue"
24
7
 
25
8
  module Async
26
9
  module Bus
27
10
  module Protocol
11
+ # Represents a transaction for a remote procedure call.
28
12
  class Transaction
29
- def initialize(connection, id)
13
+ # Initialize a new transaction.
14
+ # @parameter connection [Connection] The connection for this transaction.
15
+ # @parameter id [Integer] The transaction ID.
16
+ # @parameter timeout [Float] The timeout for the transaction.
17
+ def initialize(connection, id, timeout: nil)
30
18
  @connection = connection
31
19
  @id = id
32
20
 
33
- @received = Async::Queue.new
21
+ @timeout = timeout
22
+
23
+ @received = Thread::Queue.new
34
24
  @accept = nil
35
25
  end
36
26
 
27
+ # @attribute [Connection] The connection for this transaction.
28
+ attr :connection
29
+
30
+ # @attribute [Integer] The transaction ID.
37
31
  attr :id
32
+
33
+ # @attribute [Float] The timeout for the transaction.
34
+ attr_accessor :timeout
35
+
36
+ # @attribute [Thread::Queue] The queue of received messages.
38
37
  attr :received
39
38
 
39
+ # @attribute [Object] The accept handler.
40
+ attr :accept
41
+
42
+ # Read a message from the transaction queue.
43
+ # @returns [Object] The next message.
40
44
  def read
41
45
  if @received.empty?
42
- @connection.packer.flush
46
+ @connection.flush
43
47
  end
44
48
 
45
- @received.dequeue
49
+ @received.pop(timeout: @timeout)
50
+ end
51
+
52
+ # Write a message to the connection.
53
+ # @parameter message [Object] The message to write.
54
+ # @raises [RuntimeError] If the transaction is closed.
55
+ def write(message)
56
+ if @connection
57
+ @connection.write(message)
58
+ else
59
+ raise RuntimeError, "Transaction is closed!"
60
+ end
46
61
  end
47
62
 
48
- def write(*arguments)
49
- @connection.packer.write([id, *arguments])
50
- @connection.packer.flush
63
+ # Push a message to the transaction's received queue.
64
+ # Silently ignores messages if the queue is already closed.
65
+ # @parameter message [Object] The message to push.
66
+ def push(message)
67
+ @received.push(message)
68
+ rescue ClosedQueueError
69
+ # Queue is closed (transaction already finished/closed) - ignore silently.
51
70
  end
52
71
 
72
+ # Close the transaction and clean up resources.
53
73
  def close
54
- if @connection
55
- connection = @connection
74
+ if connection = @connection
56
75
  @connection = nil
76
+ @received.close
57
77
 
58
78
  connection.transactions.delete(@id)
59
79
  end
60
80
  end
61
81
 
62
82
  # Invoke a remote procedure.
83
+ # @parameter name [Symbol] The name of the remote object.
84
+ # @parameter arguments [Array] The positional arguments.
85
+ # @parameter options [Hash] The keyword arguments.
86
+ # @yields {|*args| ...} Optional block for yielding operations.
87
+ # @returns [Object] The result of the invocation.
63
88
  def invoke(name, arguments, options, &block)
64
- Console.logger.debug(self) {[name, arguments, options, block]}
89
+ Console.debug(self){[name, arguments, options, block]}
65
90
 
66
- self.write(:invoke, name, arguments, options, block_given?)
91
+ self.write(Invoke.new(@id, name, arguments, options, block_given?))
67
92
 
68
93
  while response = self.read
69
- what, result = response
70
-
71
- case what
72
- when :error
73
- raise(result)
74
- when :return
75
- return(result)
76
- when :yield
94
+ case response
95
+ when Return
96
+ return response.result
97
+ when Yield
77
98
  begin
78
- result = yield(*result)
79
- self.write(:next, result)
99
+ result = yield(*response.result)
100
+ self.write(Next.new(@id, result))
80
101
  rescue => error
81
- self.write(:error, error)
102
+ self.write(Error.new(@id, error))
82
103
  end
104
+ when Error
105
+ raise(response.result)
106
+ when Throw
107
+ # Re-throw the tag and value that was thrown on the server side
108
+ # Throw.result contains [tag, value] array
109
+ tag, value = response.result
110
+ throw(tag, value)
83
111
  end
84
112
  end
85
-
86
- # ensure
87
- # self.write(:close)
88
113
  end
89
114
 
90
- # Accept a remote procedure invokation.
91
- def accept(object, arguments, options, block)
92
- if block
115
+ # Accept a remote procedure invocation.
116
+ # @parameter object [Object] The object to invoke the method on.
117
+ # @parameter arguments [Array] The positional arguments.
118
+ # @parameter options [Hash] The keyword arguments.
119
+ # @parameter block_given [Boolean] Whether a block was provided.
120
+ def accept(object, arguments, options, block_given)
121
+ if block_given
93
122
  result = object.public_send(*arguments, **options) do |*yield_arguments|
94
- self.write(:yield, yield_arguments)
95
- what, result = self.read
123
+ self.write(Yield.new(@id, yield_arguments))
124
+
125
+ response = self.read
96
126
 
97
- case what
98
- when :next
99
- result
100
- when :close
101
- return
102
- when :error
103
- raise(result)
127
+ case response
128
+ when Next
129
+ response.result
130
+ when Error
131
+ raise(response.result)
132
+ when Close
133
+ break
104
134
  end
105
135
  end
106
136
  else
107
137
  result = object.public_send(*arguments, **options)
108
138
  end
109
139
 
110
- self.write(:return, result)
140
+ self.write(Return.new(@id, result))
111
141
  rescue UncaughtThrowError => error
112
- self.write(:throw, error.tag)
142
+ # UncaughtThrowError has both tag and value attributes
143
+ # Store both in the Throw message: result is tag, we'll add value handling
144
+ self.write(Throw.new(@id, [error.tag, error.value]))
113
145
  rescue => error
114
- self.write(:error, error)
115
- # ensure
116
- # self.write(:close)
146
+ self.write(Error.new(@id, error))
117
147
  end
118
148
  end
119
149
  end
120
150
  end
121
- end
151
+ end