async-bus 0.2.0 → 0.3.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: bcce5759c8ff0eab16a54c2cf8dd48c054dac88bec2705124cc70589563b9fbb
4
- data.tar.gz: 63b96c8cc50edf9f357df6ea992cb4f6ec63aa89f5349c9e0d8ff85c97dafc91
3
+ metadata.gz: df5f9eee987b7c64e94b521cfdcee2e83bba9657f5155084fb105b02adc9d84d
4
+ data.tar.gz: 76a6ac9c261fd91a3a8f9cc3f20dffe5ee7036a6b1b4a07a64d66170004e39f7
5
5
  SHA512:
6
- metadata.gz: a87a86e44c67ca9e4ca76234e0f844c2bb92d14eaffd52f16104f1b14038d266e3725abc584fe779088256c2cdf34e0bfeb4e29aecddeaa71f2d2835f2ed692a
7
- data.tar.gz: c28eb52cc46f0ae4d3dcbd1c3e823174e273dde9e9dd1678d672b2f0d3ba5704fe495c511c07355be0a1215d0ade1e0824ca36e8f27d20a11f42f00711b65290
6
+ metadata.gz: 985a0ac762db79cf5feb87fa3c50aad32bdf627b26f86e015b60d553d146c843023ea7287617834151a8074e23ca87dcc8877f3b323db89994dc04dec7b54ef0
7
+ data.tar.gz: 1f1e8ebda1dd9036a9452a95c410c1232316041600dd51e8b4f820ea99e67f8e3b7dc245f9cf3870ea62d7253cfd85ee1fb7e42ccbaf25248fce4b38086850e0
checksums.yaml.gz.sig CHANGED
Binary file
@@ -8,7 +8,11 @@ require "async/queue"
8
8
 
9
9
  module Async
10
10
  module Bus
11
+ # Represents a client that can connect to a server.
11
12
  class Client
13
+ # Initialize a new client.
14
+ # @parameter endpoint [IO::Endpoint] The endpoint to connect to.
15
+ # @parameter options [Hash] Additional options for the connection.
12
16
  def initialize(endpoint = nil, **options)
13
17
  @endpoint = endpoint || Protocol.local_endpoint
14
18
  @options = options
@@ -59,14 +63,17 @@ module Async
59
63
  # Automatically reconnects when the connection fails, with random backoff.
60
64
  # This is useful for long-running clients that need to maintain a persistent connection.
61
65
  #
62
- # @parameter parent [Async::Task] The parent task to run under.
63
- def run(parent: Task.current)
64
- parent.async(annotation: "Bus Client", transient: true) do |task|
66
+ # @yields {|connection| ...} If a block is given, it will be called with the connection.
67
+ # @returns [Async::Task] The task that runs the client.
68
+ def run(&block)
69
+ Async(transient: true) do |task|
65
70
  loop do
66
71
  connection = connect!
67
72
 
68
73
  connected_task = task.async do
69
74
  connected!(connection)
75
+
76
+ yield(connection) if block_given?
70
77
  end
71
78
 
72
79
  connection.run
@@ -13,20 +13,38 @@ require_relative "response"
13
13
 
14
14
  module Async
15
15
  module Bus
16
+ # @namespace
16
17
  module Protocol
18
+ # Create a local Unix domain socket endpoint.
19
+ # @parameter path [String] The path to the socket file.
20
+ # @returns [IO::Endpoint::Unix] The Unix endpoint.
17
21
  def self.local_endpoint(path = "bus.ipc")
18
22
  ::IO::Endpoint.unix(path)
19
23
  end
20
24
 
25
+ # Represents a connection between client and server for message passing.
21
26
  class Connection
27
+ # Create a client-side connection.
28
+ # @parameter peer [IO] The peer connection.
29
+ # @parameter options [Hash] Additional options for the connection.
30
+ # @returns [Connection] A new client connection.
22
31
  def self.client(peer, **options)
23
32
  self.new(peer, 1, **options)
24
33
  end
25
34
 
35
+ # Create a server-side connection.
36
+ # @parameter peer [IO] The peer connection.
37
+ # @parameter options [Hash] Additional options for the connection.
38
+ # @returns [Connection] A new server connection.
26
39
  def self.server(peer, **options)
27
40
  self.new(peer, 2, **options)
28
41
  end
29
42
 
43
+ # Initialize a new connection.
44
+ # @parameter peer [IO] The peer connection.
45
+ # @parameter id [Integer] The initial transaction ID.
46
+ # @parameter wrapper [Class] The wrapper class for serialization.
47
+ # @parameter timeout [Float] The timeout for transactions.
30
48
  def initialize(peer, id, wrapper: Wrapper, timeout: nil)
31
49
  @peer = peer
32
50
  @id = id
@@ -47,16 +65,20 @@ module Async
47
65
  # @attribute [Float] The timeout for transactions.
48
66
  attr_accessor :timeout
49
67
 
68
+ # Flush the packer buffer.
50
69
  def flush
51
70
  @packer.flush
52
71
  end
53
72
 
73
+ # Write a message to the connection.
74
+ # @parameter message [Object] The message to write.
54
75
  def write(message)
55
76
  # $stderr.puts "Writing: #{message.inspect}"
56
77
  @packer.write(message)
57
78
  @packer.flush
58
79
  end
59
80
 
81
+ # Close the connection and clean up resources.
60
82
  def close
61
83
  @transactions.each do |id, transaction|
62
84
  transaction.close
@@ -65,16 +87,26 @@ module Async
65
87
  @peer.close
66
88
  end
67
89
 
90
+ # Return a string representation of the connection.
91
+ # @returns [String] A string describing the connection.
68
92
  def inspect
69
93
  "#<#{self.class} #{@objects.size} objects>"
70
94
  end
71
95
 
96
+ # @attribute [Hash] The bound objects.
72
97
  attr :objects
98
+
99
+ # @attribute [ObjectSpace::WeakMap] The proxy cache.
73
100
  attr :proxies
74
101
 
102
+ # @attribute [MessagePack::Unpacker] The message unpacker.
75
103
  attr :unpacker
104
+
105
+ # @attribute [MessagePack::Packer] The message packer.
76
106
  attr :packer
77
107
 
108
+ # Get the next transaction ID.
109
+ # @returns [Integer] The next transaction ID.
78
110
  def next_id
79
111
  id = @id
80
112
  @id += 2
@@ -82,6 +114,7 @@ module Async
82
114
  return id
83
115
  end
84
116
 
117
+ # @attribute [Hash] Active transactions.
85
118
  attr :transactions
86
119
 
87
120
  Explicit = Struct.new(:object) do
@@ -96,59 +129,112 @@ module Async
96
129
  end
97
130
  end
98
131
 
99
- # Bind a local object to a name, such that it could be accessed remotely.
132
+ # Explicitly bind an object to a name, such that it could be accessed remotely.
133
+ #
134
+ # This is the same as {bind} but due to the semantics of the `[]=` operator, it does not return a proxy instance.
100
135
  #
136
+ # Explicitly bound objects are not garbage collected until the connection is closed.
137
+ #
138
+ # @parameter name [String] The name to bind the object to.
139
+ # @parameter object [Object] The object to bind to the given name.
140
+ def []=(name, object)
141
+ @objects[name] = Explicit.new(object)
142
+ end
143
+
144
+ # Generate a proxy for a remotely bound object.
145
+ #
146
+ # **This always returns a proxy, even if the object is bound locally.**
147
+ # The object bus is not shared between client and server, so `[]` always
148
+ # returns a proxy to the remote instance.
149
+ #
150
+ # @parameter name [String] The name of the bound object.
151
+ # @returns [Proxy] A proxy instance for the bound object.
152
+ def [](name)
153
+ return proxy_for(name)
154
+ end
155
+
156
+ # Explicitly bind an object to a name, such that it could be accessed remotely.
157
+ #
158
+ # This method is identical to {[]=} but also returns a {Proxy} instance for the bound object which can be passed by reference.
159
+ #
160
+ # Explicitly bound objects are not garbage collected until the connection is closed.
161
+ #
162
+ # @example Binding an object to a name and accessing it remotely.
163
+ # array_proxy = connection.bind(:items, [1, 2, 3])
164
+ # connection[:remote].register(array_proxy)
165
+ #
166
+ # @parameter name [String] The name to bind the object to.
167
+ # @parameter object [Object] The object to bind to the given name.
101
168
  # @returns [Proxy] A proxy instance for the bound object.
102
169
  def bind(name, object)
103
170
  # Bind the object into the local object store (explicitly bound, not temporary):
104
171
  @objects[name] = Explicit.new(object)
105
172
 
106
- # Return the proxy instance for the bound object:
107
- return self[name]
173
+ # Always return a proxy for passing by reference, even for locally bound objects:
174
+ return proxy_for(name)
108
175
  end
109
176
 
110
- # Generate a proxy name for an object and bind it.
177
+ # Implicitly bind an object with a temporary name, such that it could be accessed remotely.
178
+ #
179
+ # Implicitly bound objects are garbage collected when the remote end no longer references them.
111
180
  #
181
+ # This method is simliar to {bind} but is designed to be used to generate temporary proxies for objects that are not explicitly bound.
182
+ #
183
+ # @parameter object [Object] The object to bind to a temporary name.
112
184
  # @returns [Proxy] A proxy instance for the bound object.
113
185
  def proxy(object)
114
- name = "<#{object.class}@#{next_id.to_s(16)}>".freeze
186
+ name = object.__id__
115
187
 
116
188
  # Bind the object into the local object store (temporary):
117
- @objects[name] = Implicit.new(object)
189
+ @objects[name] ||= Implicit.new(object)
118
190
 
119
- # This constructs the Proxy instance:
120
- return self[name]
191
+ # Always return a proxy for passing by reference:
192
+ return proxy_for(name)
121
193
  end
122
194
 
123
- # Generate a proxy name for an object and bind it, returning just the name.
124
- # Used for serialization when you need the name string, not a Proxy instance.
195
+ # Implicitly bind an object with a temporary name, such that it could be accessed remotely.
196
+ #
197
+ # Implicitly bound objects are garbage collected when the remote end no longer references them.
125
198
  #
199
+ # This method is similar to {proxy} but is designed to be used to generate temporary names for objects that are not explicitly bound during serialization.
200
+ #
201
+ # @parameter object [Object] The object to bind to a temporary name.
126
202
  # @returns [String] The name of the bound object.
127
203
  def proxy_name(object)
128
- name = "<#{object.class}@#{next_id.to_s(16)}>".freeze
204
+ name = object.__id__
129
205
 
130
206
  # Bind the object into the local object store (temporary):
131
- @objects[name] = Implicit.new(object)
207
+ @objects[name] ||= Implicit.new(object)
132
208
 
133
209
  # Return the name:
134
210
  return name
135
211
  end
136
212
 
137
- def object(name)
138
- @objects[name]&.object
139
- end
140
-
141
- private def finalize(name)
142
- proc do
143
- @finalized.push(name) rescue nil
213
+ # Get an object or proxy for a bound object, handling reverse lookup.
214
+ #
215
+ # If the object is bound locally and the proxy is for this connection, returns the actual object.
216
+ # If the object is bound remotely, or the proxy is from a different connection, returns a proxy.
217
+ # This is used when deserializing proxies to handle round-trip scenarios and avoid name collisions.
218
+ #
219
+ # @parameter name [String] The name of the bound object.
220
+ # @parameter local [Boolean] Whether the proxy is for this connection (from serialization). Defaults to true.
221
+ # @returns [Object | Proxy] The object if bound locally and proxy is for this connection, or a proxy otherwise.
222
+ def proxy_object(name)
223
+ # If the proxy is for this connection and the object is bound locally, return the actual object:
224
+ if entry = @objects[name]
225
+ # This handles round-trip scenarios correctly.
226
+ return entry.object
144
227
  end
228
+
229
+ # Otherwise, create a proxy for the remote object:
230
+ return proxy_for(name)
145
231
  end
146
232
 
147
- def []=(name, object)
148
- @objects[name] = Explicit.new(object)
149
- end
150
-
151
- def [](name)
233
+ # Get or create a proxy for a named object.
234
+ #
235
+ # @parameter name [String] The name of the object.
236
+ # @returns [Proxy] A proxy instance for the named object.
237
+ private def proxy_for(name)
152
238
  unless proxy = @proxies[name]
153
239
  proxy = Proxy.new(self, name)
154
240
  @proxies[name] = proxy
@@ -159,6 +245,15 @@ module Async
159
245
  return proxy
160
246
  end
161
247
 
248
+ private def finalize(name)
249
+ proc do
250
+ @finalized.push(name) rescue nil
251
+ end
252
+ end
253
+
254
+ # Create a new transaction.
255
+ # @parameter id [Integer] The transaction ID.
256
+ # @returns [Transaction] A new transaction.
162
257
  def transaction!(id = self.next_id)
163
258
  transaction = Transaction.new(self, id, timeout: @timeout)
164
259
  @transactions[id] = transaction
@@ -166,6 +261,12 @@ module Async
166
261
  return transaction
167
262
  end
168
263
 
264
+ # Invoke a remote procedure.
265
+ # @parameter name [Symbol] The name of the remote object.
266
+ # @parameter arguments [Array] The arguments to pass.
267
+ # @parameter options [Hash] The keyword arguments to pass.
268
+ # @yields {|*args| ...} Optional block for yielding operations.
269
+ # @returns [Object] The result of the invocation.
169
270
  def invoke(name, arguments, options = {}, &block)
170
271
  transaction = self.transaction!
171
272
 
@@ -174,10 +275,14 @@ module Async
174
275
  transaction&.close
175
276
  end
176
277
 
278
+ # Send a release message for a named object.
279
+ # @parameter name [Symbol] The name of the object to release.
177
280
  def send_release(name)
178
281
  self.write(Release.new(name))
179
282
  end
180
283
 
284
+ # Run the connection message loop.
285
+ # @parameter parent [Async::Task] The parent task to run under.
181
286
  def run(parent: Task.current)
182
287
  finalizer_task = parent.async do
183
288
  while name = @finalized.pop
@@ -11,6 +11,12 @@ module Async
11
11
  module Protocol
12
12
  # Represents a method invocation.
13
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.
14
20
  def initialize(id, name, arguments, options, block_given)
15
21
  @id = id
16
22
  @name = name
@@ -19,12 +25,23 @@ module Async
19
25
  @block_given = block_given
20
26
  end
21
27
 
28
+ # @attribute [Integer] The transaction ID.
22
29
  attr :id
30
+
31
+ # @attribute [Symbol] The method name.
23
32
  attr :name
33
+
34
+ # @attribute [Array] The positional arguments.
24
35
  attr :arguments
36
+
37
+ # @attribute [Hash] The keyword arguments.
25
38
  attr :options
39
+
40
+ # @attribute [Boolean] Whether a block was provided.
26
41
  attr :block_given
27
42
 
43
+ # Pack the invocation into a MessagePack packer.
44
+ # @parameter packer [MessagePack::Packer] The packer to write to.
28
45
  def pack(packer)
29
46
  packer.write(@id)
30
47
  packer.write(@name)
@@ -43,6 +60,9 @@ module Async
43
60
  packer.write(@block_given)
44
61
  end
45
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.
46
66
  def self.unpack(unpacker)
47
67
  id = unpacker.read
48
68
  name = unpacker.read
@@ -19,36 +19,59 @@ module Async
19
19
  @name = name
20
20
  end
21
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.
22
30
  def __name__
23
31
  @name
24
32
  end
25
33
 
34
+ # Logical negation operator.
35
+ # @returns [Object] The result of the negation.
26
36
  def !
27
37
  @connection.invoke(@name, [:!])
28
38
  end
29
39
 
40
+ # Equality operator.
41
+ # @parameter object [Object] The object to compare with.
42
+ # @returns [Boolean] True if equal.
30
43
  def == object
31
44
  @connection.invoke(@name, [:==, object])
32
45
  end
33
46
 
47
+ # Inequality operator.
48
+ # @parameter object [Object] The object to compare with.
49
+ # @returns [Boolean] True if not equal.
34
50
  def != object
35
51
  @connection.invoke(@name, [:!=, object])
36
52
  end
37
53
 
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.
38
59
  def method_missing(*arguments, **options, &block)
39
60
  @connection.invoke(@name, arguments, options, &block)
40
61
  end
41
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.
42
67
  def respond_to?(name, include_all = false)
43
68
  @connection.invoke(@name, [:respond_to?, name, include_all])
44
69
  end
45
70
 
46
- def respond_to_missing?(name, include_all = false)
47
- @connection.invoke(@name, [:respond_to?, name, include_all])
48
- end
49
-
71
+ # Return a string representation of the proxy.
72
+ # @returns [String] A string describing the proxy.
50
73
  def inspect
51
- "#<proxy #{@name}: #{@connection.invoke(@name, [:inspect])}>"
74
+ "#<proxy #{@name}>"
52
75
  end
53
76
  end
54
77
  end
@@ -8,16 +8,24 @@ module Async
8
8
  module Protocol
9
9
  # Represents a named object that has been released (no longer available).
10
10
  class Release
11
+ # Initialize a new release message.
12
+ # @parameter name [Symbol] The name of the released object.
11
13
  def initialize(name)
12
14
  @name = name
13
15
  end
14
16
 
17
+ # @attribute [Symbol] The name of the released object.
15
18
  attr :name
16
19
 
20
+ # Pack the release into a MessagePack packer.
21
+ # @parameter packer [MessagePack::Packer] The packer to write to.
17
22
  def pack(packer)
18
23
  packer.write(@name)
19
24
  end
20
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.
21
29
  def self.unpack(unpacker)
22
30
  name = unpacker.read
23
31
 
@@ -6,20 +6,32 @@
6
6
  module Async
7
7
  module Bus
8
8
  module Protocol
9
+ # Represents a response message from a remote procedure call.
9
10
  class Response
11
+ # Initialize a new response message.
12
+ # @parameter id [Integer] The transaction ID.
13
+ # @parameter result [Object] The result value.
10
14
  def initialize(id, result)
11
15
  @id = id
12
16
  @result = result
13
17
  end
14
18
 
19
+ # @attribute [Integer] The transaction ID.
15
20
  attr :id
21
+
22
+ # @attribute [Object] The result value.
16
23
  attr :result
17
24
 
25
+ # Pack the response into a MessagePack packer.
26
+ # @parameter packer [MessagePack::Packer] The packer to write to.
18
27
  def pack(packer)
19
28
  packer.write(@id)
20
29
  packer.write(@result)
21
30
  end
22
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.
23
35
  def self.unpack(unpacker)
24
36
  id = unpacker.read
25
37
  result = unpacker.read
@@ -8,7 +8,12 @@ require "async/queue"
8
8
  module Async
9
9
  module Bus
10
10
  module Protocol
11
+ # Represents a transaction for a remote procedure call.
11
12
  class Transaction
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.
12
17
  def initialize(connection, id, timeout: nil)
13
18
  @connection = connection
14
19
  @id = id
@@ -19,14 +24,23 @@ module Async
19
24
  @accept = nil
20
25
  end
21
26
 
27
+ # @attribute [Connection] The connection for this transaction.
22
28
  attr :connection
29
+
30
+ # @attribute [Integer] The transaction ID.
23
31
  attr :id
24
32
 
33
+ # @attribute [Float] The timeout for the transaction.
25
34
  attr_accessor :timeout
26
35
 
36
+ # @attribute [Thread::Queue] The queue of received messages.
27
37
  attr :received
38
+
39
+ # @attribute [Object] The accept handler.
28
40
  attr :accept
29
41
 
42
+ # Read a message from the transaction queue.
43
+ # @returns [Object] The next message.
30
44
  def read
31
45
  if @received.empty?
32
46
  @connection.flush
@@ -35,6 +49,9 @@ module Async
35
49
  @received.pop(timeout: @timeout)
36
50
  end
37
51
 
52
+ # Write a message to the connection.
53
+ # @parameter message [Object] The message to write.
54
+ # @raises [RuntimeError] If the transaction is closed.
38
55
  def write(message)
39
56
  if @connection
40
57
  @connection.write(message)
@@ -45,12 +62,14 @@ module Async
45
62
 
46
63
  # Push a message to the transaction's received queue.
47
64
  # Silently ignores messages if the queue is already closed.
65
+ # @parameter message [Object] The message to push.
48
66
  def push(message)
49
67
  @received.push(message)
50
68
  rescue ClosedQueueError
51
69
  # Queue is closed (transaction already finished/closed) - ignore silently.
52
70
  end
53
71
 
72
+ # Close the transaction and clean up resources.
54
73
  def close
55
74
  if connection = @connection
56
75
  @connection = nil
@@ -61,6 +80,11 @@ module Async
61
80
  end
62
81
 
63
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.
64
88
  def invoke(name, arguments, options, &block)
65
89
  Console.debug(self){[name, arguments, options, block]}
66
90
 
@@ -79,11 +103,20 @@ module Async
79
103
  end
80
104
  when Error
81
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)
82
111
  end
83
112
  end
84
113
  end
85
114
 
86
- # Accept a remote procedure invokation.
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.
87
120
  def accept(object, arguments, options, block_given)
88
121
  if block_given
89
122
  result = object.public_send(*arguments, **options) do |*yield_arguments|
@@ -106,7 +139,9 @@ module Async
106
139
 
107
140
  self.write(Return.new(@id, result))
108
141
  rescue UncaughtThrowError => error
109
- self.write(Throw.new(@id, 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]))
110
145
  rescue => error
111
146
  self.write(Error.new(@id, error))
112
147
  end
@@ -15,66 +15,114 @@ require_relative "../controller"
15
15
  module Async
16
16
  module Bus
17
17
  module Protocol
18
+ # Represents a MessagePack factory wrapper for async-bus serialization.
18
19
  class Wrapper < MessagePack::Factory
19
- def initialize(bus, reference_types: [Controller])
20
+ # Initialize a new wrapper.
21
+ # @parameter connection [Connection] The connection for proxy resolution.
22
+ # @parameter reference_types [Array(Class)] Types to serialize as proxies.
23
+ def initialize(connection, reference_types: [Controller])
20
24
  super()
21
25
 
22
- @bus = bus
26
+ @connection = connection
23
27
  @reference_types = reference_types
24
28
 
29
+ # Store the peer connection for forwarding proxies:
30
+ # When a proxy is forwarded (local=false), it should point back to the sender
31
+ # (the peer connection), not the receiver (this connection).
32
+ @peer_connection = nil
33
+
25
34
  # The order here matters.
26
35
 
27
36
  self.register_type(0x00, Invoke, recursive: true,
28
- packer: ->(invoke, packer){invoke.pack(packer)},
29
- unpacker: ->(unpacker){Invoke.unpack(unpacker)},
30
- )
37
+ packer: ->(invoke, packer){invoke.pack(packer)},
38
+ unpacker: ->(unpacker){Invoke.unpack(unpacker)},
39
+ )
31
40
 
32
41
  [Return, Yield, Error, Next, Throw, Close].each_with_index do |klass, index|
33
42
  self.register_type(0x01 + index, klass, recursive: true,
34
- packer: ->(value, packer){value.pack(packer)},
35
- unpacker: ->(unpacker){klass.unpack(unpacker)},
36
- )
43
+ packer: ->(value, packer){value.pack(packer)},
44
+ unpacker: ->(unpacker){klass.unpack(unpacker)},
45
+ )
37
46
  end
38
47
 
39
48
  # Reverse serialize proxies back into proxies:
40
- # When a Proxy is received, create a proxy pointing back
41
- self.register_type(0x10, Proxy,
42
- packer: ->(proxy){proxy.__name__},
43
- unpacker: @bus.method(:[]),
44
- )
49
+ # When a Proxy is received, use proxy_object to handle reverse lookup
50
+ self.register_type(0x10, Proxy, recursive: true,
51
+ packer: self.method(:pack_proxy),
52
+ unpacker: self.method(:unpack_proxy),
53
+ )
45
54
 
46
55
  self.register_type(0x11, Release, recursive: true,
47
- packer: ->(release, packer){release.pack(packer)},
48
- unpacker: ->(unpacker){Release.unpack(unpacker)},
49
- )
56
+ packer: ->(release, packer){release.pack(packer)},
57
+ unpacker: ->(unpacker){Release.unpack(unpacker)},
58
+ )
50
59
 
51
60
  self.register_type(0x20, Symbol)
52
- self.register_type(0x21, Exception,
53
- packer: self.method(:pack_exception),
54
- unpacker: self.method(:unpack_exception),
55
- recursive: true,
56
- )
61
+ self.register_type(0x21, Exception, recursive: true,
62
+ packer: self.method(:pack_exception),
63
+ unpacker: self.method(:unpack_exception),
64
+ )
57
65
 
58
66
  self.register_type(0x22, Class,
59
- packer: ->(klass){klass.name},
60
- unpacker: ->(name){Object.const_get(name)},
61
- )
67
+ packer: ->(klass){klass.name},
68
+ unpacker: ->(name){Object.const_get(name)},
69
+ )
70
+
71
+ reference_packer = self.method(:pack_reference)
72
+ reference_unpacker = self.method(:unpack_reference)
62
73
 
63
74
  # Serialize objects into proxies:
64
75
  reference_types&.each_with_index do |klass, index|
65
- self.register_type(0x30 + index, klass,
66
- packer: @bus.method(:proxy_name),
67
- unpacker: @bus.method(:[]),
68
- )
76
+ self.register_type(0x30 + index, klass, recursive: true,
77
+ packer: reference_packer,
78
+ unpacker: reference_unpacker,
79
+ )
69
80
  end
70
81
  end
71
82
 
83
+ # Pack a proxy into a MessagePack packer.
84
+ #
85
+ # Validates that the proxy is for this connection and serializes the proxy name.
86
+ # Multi-hop proxy forwarding is not supported, so proxies can only be serialized
87
+ # from the same connection they were created for (round-trip scenarios).
88
+ #
89
+ # @parameter proxy [Proxy] The proxy to serialize.
90
+ # @parameter packer [MessagePack::Packer] The packer to write to.
91
+ # @raises [ArgumentError] If the proxy is from a different connection (multi-hop forwarding not supported).
92
+ def pack_proxy(proxy, packer)
93
+ # Check if the proxy is for this connection:
94
+ if proxy.__connection__ != @connection
95
+ proxy = @connection.proxy(proxy)
96
+ end
97
+
98
+ packer.write(proxy.__name__)
99
+ end
100
+
101
+ # Unpack a proxy from a MessagePack unpacker.
102
+ #
103
+ # When deserializing a proxy:
104
+ # - If the object is bound locally, return the actual object (round-trip scenario)
105
+ # - If the object is not found locally, create a proxy pointing to this connection
106
+ # (the proxy was forwarded from another connection and should point back to the sender)
107
+ #
108
+ # @parameter unpacker [MessagePack::Unpacker] The unpacker to read from.
109
+ # @returns [Object | Proxy] The actual object if bound locally, or a proxy pointing to this connection.
110
+ def unpack_proxy(unpacker)
111
+ @connection.proxy_object(unpacker.read)
112
+ end
113
+
114
+ # Pack an exception into a MessagePack packer.
115
+ # @parameter exception [Exception] The exception to pack.
116
+ # @parameter packer [MessagePack::Packer] The packer to write to.
72
117
  def pack_exception(exception, packer)
73
118
  packer.write(exception.class.name)
74
119
  packer.write(exception.message)
75
120
  packer.write(exception.backtrace)
76
121
  end
77
122
 
123
+ # Unpack an exception from a MessagePack unpacker.
124
+ # @parameter unpacker [MessagePack::Unpacker] The unpacker to read from.
125
+ # @returns [Exception] A reconstructed exception.
78
126
  def unpack_exception(unpacker)
79
127
  klass = unpacker.read
80
128
  message = unpacker.read
@@ -87,6 +135,28 @@ module Async
87
135
 
88
136
  return exception
89
137
  end
138
+
139
+ # Pack a reference type object (e.g., Controller) into a MessagePack packer.
140
+ #
141
+ # Serializes the object as a proxy by generating a temporary name and writing it to the packer.
142
+ # The object is implicitly bound to the connection with a temporary name.
143
+ #
144
+ # @parameter object [Object] The reference type object to serialize.
145
+ # @parameter packer [MessagePack::Packer] The packer to write to.
146
+ def pack_reference(object, packer)
147
+ packer.write(@connection.proxy_name(object))
148
+ end
149
+
150
+ # Unpack a reference type object from a MessagePack unpacker.
151
+ #
152
+ # Reads a proxy name and returns the corresponding object or proxy.
153
+ # If the object is bound locally, returns the actual object; otherwise returns a proxy.
154
+ #
155
+ # @parameter unpacker [MessagePack::Unpacker] The unpacker to read from.
156
+ # @returns [Object | Proxy] The actual object if bound locally, or a proxy otherwise.
157
+ def unpack_reference(unpacker)
158
+ @connection.proxy_object(unpacker.read)
159
+ end
90
160
  end
91
161
  end
92
162
  end
@@ -6,19 +6,37 @@
6
6
  require_relative "protocol/connection"
7
7
  require "set"
8
8
 
9
+ # @namespace
9
10
  module Async
11
+ # @namespace
10
12
  module Bus
13
+ # Represents a server that accepts async-bus connections.
11
14
  class Server
15
+ # Initialize a new server.
16
+ # @parameter endpoint [IO::Endpoint] The endpoint to listen on.
17
+ # @parameter options [Hash] Additional options for connections.
12
18
  def initialize(endpoint = nil, **options)
13
19
  @endpoint = endpoint || Protocol.local_endpoint
14
20
  @options = options
15
21
  end
16
22
 
17
- def accept
23
+ # Called when a connection is established.
24
+ # Override this method to perform setup when a connection is established.
25
+ #
26
+ # @parameter connection [Protocol::Connection] The established connection.
27
+ protected def connected!(connection)
28
+ # Do nothing by default.
29
+ end
30
+
31
+ # Accept incoming connections.
32
+ # @yields {|connection| ...} Block called with each new connection.
33
+ def accept(&block)
18
34
  @endpoint.accept do |peer|
19
35
  connection = Protocol::Connection.server(peer, **@options)
20
36
 
21
- yield connection
37
+ connected!(connection, &block)
38
+
39
+ yield connection if block_given?
22
40
 
23
41
  connection.run
24
42
  ensure
@@ -3,8 +3,10 @@
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2021-2025, by Samuel Williams.
5
5
 
6
+ # @namespace
6
7
  module Async
8
+ # @namespace
7
9
  module Bus
8
- VERSION = "0.2.0"
10
+ VERSION = "0.3.1"
9
11
  end
10
12
  end
data/readme.md CHANGED
@@ -1,23 +1,39 @@
1
1
  # Async::Bus
2
2
 
3
- Provides a client and server implementation for asynchronous message buses in Ruby.
3
+ When building distributed systems or multi-process applications, you need a way for processes to communicate and invoke methods on objects in other processes. `async-bus` provides a lightweight message-passing system for inter-process communication (IPC) using Unix domain sockets, enabling transparent remote procedure calls (RPC) where remote objects feel like local objects.
4
4
 
5
- [![Development Status](https://github.com/socketry/async-bus/workflows/Test/badge.svg)](https://github.com/socketry/async-bus/actions?workflow=Test)
5
+ Use `async-bus` when you need:
6
6
 
7
- ## Features
7
+ - **Inter-process communication**: Connect multiple Ruby processes running on the same machine.
8
+ - **Transparent RPC**: Call methods on remote objects as if they were local.
9
+ - **Type-safe serialization**: Automatically serialize and deserialize Ruby objects using MessagePack.
10
+ - **Asynchronous operations**: Non-blocking message passing built on the Async framework.
8
11
 
9
- - Serialization of (rich) Ruby objects using [MessagePack](https://msgpack.org/).
10
- - Asynchronous Remote Procedure Calls (RPC) with timeouts.
11
- - Automatic client and server reconnection handling.
12
+ [![Development Status](https://github.com/socketry/async-bus/workflows/Test/badge.svg)](https://github.com/socketry/async-bus/actions?workflow=Test)
12
13
 
13
14
  ## Usage
14
15
 
15
16
  Please see the [project documentation](https://socketry.github.io/async-bus/) for more details.
16
17
 
18
+ - [Getting Started](https://socketry.github.io/async-bus/guides/getting-started/index) - This guide explains how to get started with `async-bus` to build asynchronous message-passing systems with transparent remote procedure calls in Ruby.
19
+
20
+ - [Controllers](https://socketry.github.io/async-bus/guides/controllers/index) - This guide explains how to use controllers in `async-bus` to build explicit remote interfaces with pass-by-reference semantics, enabling bidirectional communication and shared state across connections.
21
+
17
22
  ## Releases
18
23
 
19
24
  Please see the [project releases](https://socketry.github.io/async-bus/releases/index) for all releases.
20
25
 
26
+ ### v0.3.1
27
+
28
+ - `Client#run` now returns an `Async::Task` (as it did in earlier releases).
29
+
30
+ ### v0.3.0
31
+
32
+ - Add support for multi-hop proxying.
33
+ - Fix proxying of throw/catch value.
34
+ - `Client#run` now takes a block.
35
+ - `Server#run` delegates to `Server#connected!`.
36
+
21
37
  ### v0.2.0
22
38
 
23
39
  - Fix handling of temporary objects.
data/releases.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Releases
2
2
 
3
+ ## v0.3.1
4
+
5
+ - `Client#run` now returns an `Async::Task` (as it did in earlier releases).
6
+
7
+ ## v0.3.0
8
+
9
+ - Add support for multi-hop proxying.
10
+ - Fix proxying of throw/catch value.
11
+ - `Client#run` now takes a block.
12
+ - `Server#run` delegates to `Server#connected!`.
13
+
3
14
  ## v0.2.0
4
15
 
5
16
  - Fix handling of temporary objects.
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: async-bus
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
metadata.gz.sig CHANGED
Binary file