Skip to content

Commit 32adae6

Browse files
Explicit proxying.
1 parent 3f59358 commit 32adae6

File tree

14 files changed

+452
-190
lines changed

14 files changed

+452
-190
lines changed

lib/async/bus.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@
44
# Copyright, 2021-2025, by Samuel Williams.
55

66
require_relative "bus/version"
7+
require_relative "bus/controller"

lib/async/bus/client.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,15 @@
99
module Async
1010
module Bus
1111
class Client
12-
def initialize(endpoint = nil)
12+
def initialize(endpoint = nil, **options)
1313
@endpoint = endpoint || Protocol.local_endpoint
14+
@options = options
1415
end
1516

1617
# @parameter persist [Boolean] Whether to keep the connection open indefiniely.
1718
def connect(persist = false)
1819
@endpoint.connect do |peer|
19-
connection = Protocol::Connection.client(peer)
20+
connection = Protocol::Connection.client(peer, **@options)
2021

2122
connection_task = Async do
2223
connection.run

lib/async/bus/controller.rb

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# frozen_string_literal: true
2+
3+
# Released under the MIT License.
4+
# Copyright, 2021-2025, by Samuel Williams.
5+
6+
module Async
7+
module Bus
8+
# Base class for controller objects designed to be proxied over Async::Bus.
9+
#
10+
# Controllers provide an explicit API for remote operations, avoiding the
11+
# confusion that comes from proxying generic objects like Array or Hash.
12+
#
13+
# @example Array Controller
14+
# class ArrayController < Async::Bus::Controller
15+
# def initialize(array)
16+
# @array = array
17+
# end
18+
#
19+
# def append(*values)
20+
# @array.concat(values)
21+
# self # Return self for chaining
22+
# end
23+
#
24+
# def get(index)
25+
# @array[index] # Returns value
26+
# end
27+
#
28+
# def size
29+
# @array.size
30+
# end
31+
# end
32+
#
33+
# @example Server Setup
34+
# server.accept do |connection|
35+
# array = []
36+
# controller = ArrayController.new(array)
37+
# connection.bind(:items, controller)
38+
# end
39+
#
40+
# @example Client Usage
41+
# client.connect do |connection|
42+
# items = connection[:items] # Returns proxy to controller
43+
# items.append(1, 2, 3) # Remote call
44+
# expect(items.size).to be == 3
45+
# end
46+
#
47+
# Controllers are automatically proxied when serialized if registered
48+
# as a reference type in the Wrapper:
49+
#
50+
# Wrapper.new(connection, reference_types: [Async::Bus::Controller])
51+
#
52+
# This allows controller methods to return other controllers and have
53+
# them automatically proxied.
54+
class Controller
55+
end
56+
end
57+
end
58+

lib/async/bus/protocol/connection.rb

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,18 @@ def self.local_endpoint(path = "bus.ipc")
1818
end
1919

2020
class Connection
21-
def self.client(peer)
22-
self.new(peer, 1)
21+
def self.client(peer, **options)
22+
self.new(peer, 1, **options)
2323
end
2424

25-
def self.server(peer)
26-
self.new(peer, 2)
25+
def self.server(peer, **options)
26+
self.new(peer, 2, **options)
2727
end
2828

29-
def initialize(peer, id)
29+
def initialize(peer, id, wrapper: Wrapper)
3030
@peer = peer
3131

32-
@wrapper = Wrapper.new(self)
32+
@wrapper = wrapper.new(self)
3333
@unpacker = @wrapper.unpacker(peer)
3434
@packer = @wrapper.packer(peer)
3535

@@ -72,23 +72,35 @@ def next_id
7272

7373
# Bind a local object to a name, such that it could be accessed remotely.
7474
#
75-
# @returns [String] The (unique) name of the object.
75+
# @returns [Proxy] A proxy instance for the bound object.
76+
def bind(name, object)
77+
@objects[name] = object
78+
return self[name]
79+
end
80+
81+
# Generate a proxy name for an object and bind it.
82+
#
83+
# @returns [Proxy] A proxy instance for the bound object.
7684
def proxy(object)
7785
name = "<#{object.class}@#{next_id.to_s(16)}>".freeze
7886

87+
return bind(name, object)
88+
end
89+
90+
# Generate a proxy name for an object and bind it, returning just the name.
91+
# Used for serialization when you need the name string, not a Proxy instance.
92+
#
93+
# @returns [String] The name of the bound object.
94+
def proxy_name(object)
95+
name = "<#{object.class}@#{next_id.to_s(16)}>".freeze
7996
bind(name, object)
80-
8197
return name
8298
end
8399

84100
def object(name)
85101
@objects[name]
86102
end
87103

88-
def bind(name, object)
89-
@objects[name] = object
90-
end
91-
92104
private def finalize(name)
93105
proc{@finalized << name}
94106
end
@@ -102,7 +114,7 @@ def [](name)
102114
proxy = Proxy.new(self, name)
103115
@proxies[name] = proxy
104116

105-
ObjectSpace.define_finalizer(proxy, finalize(name))
117+
::ObjectSpace.define_finalizer(proxy, finalize(name))
106118
end
107119

108120
return proxy
@@ -150,7 +162,7 @@ def run
150162
end
151163
else
152164
transaction = @transactions[message.id]
153-
transaction.received.enqueue(message)
165+
transaction.received.push(message)
154166
end
155167
end
156168
ensure

lib/async/bus/protocol/invoke.rb

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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+
def initialize(id, name, arguments, options, block_given)
15+
@id = id
16+
@name = name
17+
@arguments = arguments
18+
@options = options
19+
@block_given = block_given
20+
end
21+
22+
attr :id
23+
attr :name
24+
attr :arguments
25+
attr :options
26+
attr :block_given
27+
28+
def pack(packer)
29+
packer.write(@id)
30+
packer.write(@name)
31+
32+
packer.write(@arguments.size)
33+
@arguments.each do |argument|
34+
packer.write(argument)
35+
end
36+
37+
packer.write(@options.size)
38+
@options.each do |key, value|
39+
packer.write(key)
40+
packer.write(value)
41+
end
42+
43+
packer.write(@block_given)
44+
end
45+
46+
def self.unpack(unpacker)
47+
id = unpacker.read
48+
name = unpacker.read
49+
arguments = Array.new(unpacker.read) {unpacker.read}
50+
options = Array.new(unpacker.read) {[unpacker.read, unpacker.read]}.to_h
51+
block_given = unpacker.read
52+
53+
return self.new(id, name, arguments, options, block_given)
54+
end
55+
end
56+
end
57+
end
58+
end

lib/async/bus/protocol/proxy.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ def respond_to_missing?(name, include_all = false)
4848
end
4949

5050
def inspect
51-
"#<proxy #{@name}>"
51+
"#<proxy #{@name}: #{@connection.invoke(@name, [:inspect])}>"
5252
end
5353
end
5454
end

lib/async/bus/protocol/release.rb

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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+
def initialize(name)
12+
@name = name
13+
end
14+
15+
attr :name
16+
17+
def pack(packer)
18+
packer.write(@name)
19+
end
20+
21+
def self.unpack(unpacker)
22+
name = unpacker.read
23+
24+
return self.new(name)
25+
end
26+
end
27+
end
28+
end
29+
end

lib/async/bus/protocol/response.rb

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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+
class Response
10+
def initialize(id, result)
11+
@id = id
12+
@result = result
13+
end
14+
15+
attr :id
16+
attr :result
17+
18+
def pack(packer)
19+
packer.write(@id)
20+
packer.write(@result)
21+
end
22+
23+
def self.unpack(unpacker)
24+
id = unpacker.read
25+
result = unpacker.read
26+
27+
return self.new(id, result)
28+
end
29+
end
30+
31+
Return = Class.new(Response)
32+
Yield = Class.new(Response)
33+
Error = Class.new(Response)
34+
Next = Class.new(Response)
35+
Throw = Class.new(Response)
36+
Close = Class.new(Response)
37+
end
38+
end
39+
end

lib/async/bus/protocol/transaction.rb

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@ module Async
99
module Bus
1010
module Protocol
1111
class Transaction
12-
def initialize(connection, id)
12+
def initialize(connection, id, timeout: nil)
1313
@connection = connection
1414
@id = id
15+
16+
@timeout = timeout
1517

16-
@received = Async::Queue.new
18+
@received = Thread::Queue.new
1719
@accept = nil
1820
end
1921

@@ -25,12 +27,10 @@ def read
2527
@connection.flush
2628
end
2729

28-
@received.dequeue
30+
@received.pop(timeout: @timeout)
2931
end
3032

3133
def write(message)
32-
# $stderr.puts "Transaction Writing: #{message.inspect}"
33-
3434
if @connection
3535
@connection.write(message)
3636
else
@@ -41,7 +41,7 @@ def write(message)
4141
def close
4242
if connection = @connection
4343
@connection = nil
44-
@received.enqueue(nil)
44+
@received.close
4545

4646
connection.transactions.delete(@id)
4747
end

0 commit comments

Comments
 (0)