Protocol Operations
tofu has 10 operations. Each operation is a specific message type.
Terminology
| Term | What it is |
|---|---|
| OpCode | The 4-bit identifier for an operation |
| Message Type | The domain: welcome, hello, regular, bye |
| Message Role | The pattern: request, response, signal |
An operation = type + role. Example: HelloRequest = hello type + request role.
All Operations
pub const OpCode = enum(u4) {
Request = 0,
Response = 1,
Signal = 2,
HelloRequest = 3,
HelloResponse = 4,
ByeRequest = 5,
ByeResponse = 6,
ByeSignal = 7,
WelcomeRequest = 8,
WelcomeResponse = 9,
};
Setup Operations
These operations establish connections. They happen before regular data exchange.
WelcomeRequest (OpCode 8)
Server sends this to start listening.
| Aspect | Value |
|---|---|
| Transferred? | No. Local only (app ↔ tofu). |
| Channel | Created with 0. tofu assigns a listener channel. |
| Direction | Server app → tofu |
| Response | WelcomeResponse |
var msg = try ampe.get(.always);
defer ampe.put(&msg);
var addr: Address = .{ .tcp_server_addr = address.TCPServerAddress.init("0.0.0.0", 7099) };
try addr.format(msg.?);
const bhdr = try chnls.post(&msg);
const listener_ch = bhdr.channel_number; // Save this
NAQ: Why is WelcomeRequest not transferred?
It's a local setup command. You tell your local tofu "start listening". No network involved yet. The listener channel accepts future connections.
WelcomeResponse (OpCode 9)
tofu sends this to confirm listener is ready.
| Aspect | Value |
|---|---|
| Transferred? | No. Local only. |
| Channel | Same listener channel from WelcomeRequest. |
| Direction | tofu → server app |
| Received via | waitReceive() |
var resp = try chnls.waitReceive(timeout);
defer ampe.put(&resp);
if (resp.?.bhdr.proto.opCode == .WelcomeResponse) {
// Listener is ready
if (resp.?.bhdr.status != 0) {
// Failed to start listener
}
}
HelloRequest (OpCode 3)
Client sends this to connect to a server.
| Aspect | Value |
|---|---|
| Transferred? | Yes. Goes over network. |
| Channel | Client creates with 0. tofu assigns on both sides. |
| Direction | Client app → network → server app |
| Response | HelloResponse (or ByeSignal on reject) |
Client side:
var msg = try ampe.get(.always);
defer ampe.put(&msg);
var addr: Address = .{ .tcp_client_addr = address.TCPClientAddress.init("127.0.0.1", 7099) };
try addr.format(msg.?);
const bhdr = try chnls.post(&msg);
const peer_ch = bhdr.channel_number; // Save this for all future messages
Server side (receives HelloRequest):
var req = try chnls.waitReceive(timeout);
defer ampe.put(&req);
if (req.?.bhdr.proto.opCode == .HelloRequest) {
const client_ch = req.?.bhdr.channel_number; // Server's local channel for this client
// Decide: accept or reject?
}
NAQ: Why do client and server have different channel numbers?
Each side has its own channel table. tofu maps between them automatically. Client's channel 7 might be server's channel 12. You don't need to care.
HelloResponse (OpCode 4)
Server sends this to accept a connection.
| Aspect | Value |
|---|---|
| Transferred? | Yes. Goes over network. |
| Channel | Server uses its local channel for this client. |
| Direction | Server app → network → client app |
| Effect | Connection established. Both sides are now peers. |
After HelloResponse
Both sides become equal peers. Either can send Request, Response, or Signal. The original client/server distinction is gone.
Server sends:
var resp = try ampe.get(.always);
defer ampe.put(&resp);
resp.?.bhdr.proto.opCode = .HelloResponse;
resp.?.bhdr.channel_number = client_ch; // From HelloRequest
_ = try chnls.post(&resp);
Client receives:
var resp = try chnls.waitReceive(timeout);
defer ampe.put(&resp);
if (resp.?.bhdr.proto.opCode == .HelloResponse) {
// Connected. Use peer_ch for all communication.
}
Data Operations
After connection, peers exchange data using these operations.
Request (OpCode 0)
Ask the peer for something. Expects a Response.
| Aspect | Value |
|---|---|
| Transferred? | Yes |
| Channel | Existing peer channel |
| Direction | Either peer → other peer |
| Response | Usually Response (app decides) |
| Streaming | Supports more flag for multi-message requests |
var msg = try ampe.get(.always);
defer ampe.put(&msg);
msg.?.bhdr.proto.opCode = .Request;
msg.?.bhdr.channel_number = peer_ch;
msg.?.bhdr.message_id = job_id;
try msg.?.body.append(request_data);
_ = try chnls.post(&msg);
Response (OpCode 1)
Answer to a Request.
| Aspect | Value |
|---|---|
| Transferred? | Yes |
| Channel | Existing peer channel |
| Direction | Either peer → other peer |
| Correlation | Use same message_id as Request |
| Streaming | Supports more flag for multi-message responses |
var msg = try ampe.get(.always);
defer ampe.put(&msg);
msg.?.bhdr.proto.opCode = .Response;
msg.?.bhdr.channel_number = requester_ch;
msg.?.bhdr.message_id = request.?.bhdr.message_id; // Same ID
try msg.?.body.append(response_data);
_ = try chnls.post(&msg);
Signal (OpCode 2)
One-way notification. No response expected.
| Aspect | Value |
|---|---|
| Transferred? | Yes |
| Channel | Existing peer channel |
| Direction | Either peer → other peer |
| Response | None expected |
| Use cases | Progress updates, events, notifications |
var msg = try ampe.get(.always);
defer ampe.put(&msg);
msg.?.bhdr.proto.opCode = .Signal;
msg.?.bhdr.channel_number = peer_ch;
msg.?.bhdr.message_id = job_id; // To correlate with a job
try msg.?.body.append(progress_data);
_ = try chnls.post(&msg);
NAQ: When should I use Signal vs Request?
Use Signal when you don't need a response. Progress updates, heartbeats, events. Use Request when you expect the peer to send something back.
Close Operations
These operations end connections.
ByeRequest (OpCode 5)
Start a graceful close. Waits for pending messages to send.
| Aspect | Value |
|---|---|
| Transferred? | Yes |
| Channel | Channel to close |
| Direction | Either peer → other peer |
| Response | ByeResponse |
| Behavior | Queued after pending messages |
var msg = try ampe.get(.always);
defer ampe.put(&msg);
msg.?.bhdr.proto.opCode = .ByeRequest;
msg.?.bhdr.channel_number = peer_ch;
_ = try chnls.post(&msg);
// Wait for ByeResponse
ByeResponse (OpCode 6)
Acknowledge graceful close.
| Aspect | Value |
|---|---|
| Transferred? | Yes |
| Channel | Same channel as ByeRequest |
| Direction | Responder → initiator |
| Effect | Channel closed on both sides |
// Received ByeRequest
var resp = try ampe.get(.always);
defer ampe.put(&resp);
resp.?.bhdr.proto.opCode = .ByeResponse;
resp.?.bhdr.channel_number = requester_ch;
_ = try chnls.post(&resp);
// Channel closes after send
ByeSignal (OpCode 7)
Abruptive close
ByeSignal discards pending messages and closes the socket immediately. Use only when you need to abort, not for normal shutdown.
Close immediately. No response. Discards pending messages.
| Aspect | Value |
|---|---|
| Transferred? | No. Local only. |
| Channel | Channel to abort |
| Direction | App → local tofu |
| Response | None (channel_closed from engine) |
| Behavior | Inserted at head of queue. Aborts socket. |
var msg = try ampe.get(.always);
defer ampe.put(&msg);
msg.?.bhdr.proto.opCode = .ByeSignal;
msg.?.bhdr.channel_number = peer_ch;
_ = try chnls.post(&msg);
// Channel closes immediately
// Receive channel_closed from engine
NAQ: When should I use ByeSignal vs ByeRequest?
ByeRequest: Graceful. Finishes pending work. Use for normal shutdown. ByeSignal: Immediate. Discards pending messages. Use for errors, timeouts, rejection.
Quick Reference
| OpCode | Name | Transferred | Direction | Purpose |
|---|---|---|---|---|
| 8 | WelcomeRequest | No | App → tofu | Start listener |
| 9 | WelcomeResponse | No | tofu → App | Confirm listener |
| 3 | HelloRequest | Yes | Client → Server | Connect |
| 4 | HelloResponse | Yes | Server → Client | Accept connection |
| 0 | Request | Yes | Peer ↔ Peer | Ask for something |
| 1 | Response | Yes | Peer ↔ Peer | Answer request |
| 2 | Signal | Yes | Peer ↔ Peer | One-way notification |
| 5 | ByeRequest | Yes | Peer ↔ Peer | Graceful close |
| 6 | ByeResponse | Yes | Peer ↔ Peer | Acknowledge close |
| 7 | ByeSignal | No | App → tofu | Immediate close |