Skip to content

Commit d7d3cc3

Browse files
committed
Bump minor version.
1 parent a0a6caf commit d7d3cc3

File tree

6 files changed

+336
-6
lines changed

6 files changed

+336
-6
lines changed

context/controllers.md

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
# Controllers
2+
3+
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.
4+
5+
## Why Controllers?
6+
7+
While any object can be bound and proxied directly (like `Array` or `Hash`), controllers provide important advantages:
8+
9+
1. **Pass by Reference**: Controllers are always passed by reference when serialized as arguments or return values, enabling bidirectional communication.
10+
2. **Explicit Interface**: Controllers wrap objects with a well-defined interface, making the remote API clear and preventing confusion about what methods are available.
11+
3. **Automatic Proxying**: When controllers are registered as reference types, they are automatically proxied when serialized, enabling chaining and composition.
12+
13+
## Pass by Reference vs Pass by Value
14+
15+
The key difference between controllers and regular objects:
16+
17+
- **Controllers**: Passed by reference - when serialized as arguments or return values, both sides share the same object.
18+
- **Other objects**: Copied by value - when serialized, each side gets its own copy.
19+
20+
Note that when you bind an object directly (like `connection.bind(:items, array)`), clients can still access it via a proxy (`connection[:items]`). The difference only matters when objects are serialized as arguments or return values (or as a part thereof).
21+
22+
## Creating Controllers
23+
24+
Controllers inherit from {ruby Async::Bus::Controller} and define methods that can be called remotely:
25+
26+
```ruby
27+
class ChatRoomController < Async::Bus::Controller
28+
def initialize(name)
29+
@name = name
30+
@messages = []
31+
@subscribers = []
32+
end
33+
34+
def send_message(author, text)
35+
message = {author: author, text: text, time: Time.now}
36+
@messages << message
37+
38+
# Notify all subscribers:
39+
@subscribers.each do |subscriber|
40+
subscriber.on_message(message)
41+
end
42+
43+
message
44+
end
45+
46+
def subscribe(subscriber)
47+
@subscribers << subscriber
48+
@messages.size # Return message count
49+
end
50+
51+
def get_messages(count = 10)
52+
@messages.last(count)
53+
end
54+
end
55+
```
56+
57+
To use a controller, bind it instead of the raw object:
58+
59+
```ruby
60+
# Server:
61+
room = ChatRoomController.new("general")
62+
63+
server.accept do |connection|
64+
connection.bind(:room, room)
65+
end
66+
67+
# Client:
68+
client.connect do |connection|
69+
room = connection[:room]
70+
room.send_message("Alice", "Hello, world!")
71+
messages = room.get_messages(5)
72+
end
73+
```
74+
75+
## Returning Controllers
76+
77+
Controllers can return other controllers, and they are automatically proxied when registered as reference types. This enables sharing the same controller instance across multiple clients:
78+
79+
```ruby
80+
class ChatServerController < Async::Bus::Controller
81+
def initialize
82+
@rooms = {}
83+
end
84+
85+
def get_room(name)
86+
# Return existing room or create new one - automatically proxied:
87+
@rooms[name] ||= ChatRoomController.new(name)
88+
end
89+
90+
def list_rooms
91+
@rooms.keys
92+
end
93+
end
94+
95+
class ChatRoomController < Async::Bus::Controller
96+
def initialize(name)
97+
@name = name
98+
@messages = []
99+
end
100+
101+
def send_message(author, text)
102+
@messages << {author: author, text: text, time: Time.now}
103+
end
104+
105+
def name
106+
@name
107+
end
108+
end
109+
```
110+
111+
When a controller method returns another controller, the client receives a proxy to that controller. Multiple clients accessing the same room will share the same controller instance:
112+
113+
```ruby
114+
# Server:
115+
chat = ChatServerController.new
116+
117+
server.accept do |connection|
118+
connection.bind(:chat, chat)
119+
end
120+
121+
# Client 1:
122+
client1.connect do |connection|
123+
chat = connection[:chat]
124+
room = chat.get_room("general") # Returns controller, auto-proxied
125+
room.send_message("Alice", "Hello!")
126+
end
127+
128+
# Client 2:
129+
client2.connect do |connection|
130+
chat = connection[:chat]
131+
room = chat.get_room("general") # Returns same controller instance
132+
# Can see messages from Client 1 because they share the same room
133+
end
134+
```
135+
136+
## Passing Controllers as Arguments
137+
138+
Because controllers are passed by reference, you can pass them as arguments to enable bidirectional communication. When a client passes a proxy as an argument, the server receives a proxy that points back to the client's controller. This enables the server to call methods on the client's controller. This pattern is useful for event handlers, callbacks, or subscription systems:
139+
140+
```ruby
141+
class ChatRoomController < Async::Bus::Controller
142+
def initialize(name)
143+
@name = name
144+
@messages = []
145+
@subscribers = []
146+
end
147+
148+
def subscribe(subscriber)
149+
# subscriber is a proxy to the client's controller:
150+
@subscribers << subscriber
151+
# Send existing messages to the new subscriber:
152+
@messages.each{|msg| subscriber.on_message(msg)}
153+
true
154+
end
155+
156+
def send_message(author, text)
157+
message = {author: author, text: text, time: Time.now}
158+
@messages << message
159+
160+
# Notify all subscribers by calling back to their controllers:
161+
@subscribers.each do |subscriber|
162+
subscriber.on_message(message)
163+
end
164+
165+
message
166+
end
167+
end
168+
169+
# Client: Subscribes to room messages
170+
class MessageSubscriberController < Async::Bus::Controller
171+
def initialize
172+
@received = []
173+
end
174+
175+
def on_message(message)
176+
@received << message
177+
puts "#{message[:author]}: #{message[:text]}"
178+
end
179+
180+
attr :received
181+
end
182+
183+
# Server setup:
184+
room = ChatRoomController.new("general")
185+
186+
server.accept do |connection|
187+
connection.bind(:room, room)
188+
end
189+
190+
# Client subscription:
191+
client.connect do |connection|
192+
room = connection[:room]
193+
194+
# Create a subscriber controller:
195+
subscriber = MessageSubscriberController.new
196+
subscriber_proxy = connection.bind(:subscriber, subscriber)
197+
198+
# Pass the proxy as an argument - the server can now call back:
199+
room.subscribe(subscriber_proxy)
200+
201+
# Now when messages are sent, subscriber.on_message will be called:
202+
room.send_message("Bob", "Hello, everyone!")
203+
end
204+
```

context/getting-started.md

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# Getting Started
2+
3+
This guide explains how to get started with `async-bus` to build asynchronous message-passing systems with transparent remote procedure calls in Ruby.
4+
5+
## Installation
6+
7+
Add the gem to your project:
8+
9+
```bash
10+
$ bundle add async-bus
11+
```
12+
13+
## Core Concepts
14+
15+
`async-bus` has several core concepts:
16+
17+
- {ruby Async::Bus::Server}: Accepts incoming connections and exposes objects for remote access.
18+
- {ruby Async::Bus::Client}: Connects to a server and accesses remote objects.
19+
- {ruby Async::Bus::Controller}: Base class for objects designed to be proxied remotely.
20+
- {ruby Async::Bus::Protocol::Connection}: Low-level connection handling message serialization and routing.
21+
- {ruby Async::Bus::Protocol::Proxy}: Transparent proxy objects that forward method calls to remote objects.
22+
23+
## Usage
24+
25+
### Server Setup
26+
27+
Create a server that exposes objects for remote access. The server accepts connections and binds objects that clients can access. Any object can be bound and proxied:
28+
29+
```ruby
30+
require "async"
31+
require "async/bus"
32+
33+
Async do
34+
server = Async::Bus::Server.new
35+
36+
# Shared mutable state:
37+
items = Array.new
38+
39+
server.accept do |connection|
40+
# Bind any object - it will be proxied to clients:
41+
connection.bind(:items, items)
42+
end
43+
end
44+
```
45+
46+
### Client Connection
47+
48+
Connect to the server and use remote objects. The client gets proxies to remote objects that behave like local objects:
49+
50+
```ruby
51+
require "async"
52+
require "async/bus"
53+
54+
Async do
55+
client = Async::Bus::Client.new
56+
57+
client.connect do |connection|
58+
# Get a proxy to the remote object:
59+
items = connection[:items]
60+
61+
# Use it like a local object - method calls are transparently forwarded:
62+
items.push(1, 2, 3)
63+
puts items.size # => 3
64+
end
65+
end
66+
```
67+
68+
### Persistent Clients
69+
70+
For long-running clients that need to maintain a connection, use the `run` method which automatically reconnects on failure. Override `connected!` to perform setup when a connection is established. This is useful for worker processes or monitoring systems that need to stay connected:
71+
72+
```ruby
73+
require "async"
74+
require "async/bus"
75+
76+
class PersistentClient < Async::Bus::Client
77+
protected def connected!(connection)
78+
# Setup code runs when connection is established:
79+
items = connection[:items]
80+
items.push("Hello")
81+
82+
# You can also register controllers for bidirectional communication:
83+
worker = WorkerController.new
84+
connection.bind(:worker, worker)
85+
end
86+
end
87+
88+
client = PersistentClient.new
89+
90+
# This will automatically reconnect if the connection fails:
91+
client.run
92+
```
93+
94+
The `run` method handles connection lifecycle automatically, making it ideal for production services that need resilience. It will:
95+
- Automatically reconnect when the connection fails.
96+
- Use random backoff between reconnection attempts.
97+
- Call `connected!` each time a new connection is established.
98+
- Run indefinitely until the task is stopped.

context/index.yaml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Automatically generated context index for Utopia::Project guides.
2+
# Do not edit then files in this directory directly, instead edit the guides and then run `bake utopia:project:agent:context:update`.
3+
---
4+
description: Transparent Ruby IPC over an asynchronous message bus.
5+
metadata:
6+
documentation_uri: https://socketry.github.io/async-bus/
7+
source_code_uri: https://github.com/socketry/async-bus.git
8+
files:
9+
- path: getting-started.md
10+
title: Getting Started
11+
description: This guide explains how to get started with `async-bus` to build asynchronous
12+
message-passing systems with transparent remote procedure calls in Ruby.
13+
- path: controllers.md
14+
title: Controllers
15+
description: This guide explains how to use controllers in `async-bus` to build
16+
explicit remote interfaces with pass-by-reference semantics, enabling bidirectional
17+
communication and shared state across connections.

lib/async/bus/version.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,6 @@
77
module Async
88
# @namespace
99
module Bus
10-
VERSION = "0.2.0"
10+
VERSION = "0.3.0"
1111
end
1212
end

readme.md

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,32 @@ When building distributed systems or multi-process applications, you need a way
44

55
Use `async-bus` when you need:
66

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.
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.
1111

1212
[![Development Status](https://github.com/socketry/async-bus/workflows/Test/badge.svg)](https://github.com/socketry/async-bus/actions?workflow=Test)
1313

1414
## Usage
1515

1616
Please see the [project documentation](https://socketry.github.io/async-bus/) for more details.
1717

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+
1822
## Releases
1923

2024
Please see the [project releases](https://socketry.github.io/async-bus/releases/index) for all releases.
2125

26+
### v0.3.0
27+
28+
- Add support for multi-hop proxying.
29+
- Fix proxying of throw/catch value.
30+
- `Client#run` now takes a block.
31+
- `Server#run` delegates to `Server#connected!`.
32+
2233
### v0.2.0
2334

2435
- Fix handling of temporary objects.

releases.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Releases
22

3-
## Unreleased
3+
## v0.3.0
44

55
- Add support for multi-hop proxying.
66
- Fix proxying of throw/catch value.

0 commit comments

Comments
 (0)