Skip to content

Thinking in Messages

Read this first. The rest of the docs assume you understand this.


Traditional vs tofu

Traditional socket programming looks like this:

1. Create socket
2. Connect to server
3. Send data
4. Receive response
5. Close socket

tofu works differently:

1. Send HelloRequest (tofu connects for you)
2. Send Request (tofu sends for you)
3. Receive Response (tofu receives for you)
4. Send ByeRequest (tofu closes for you)

You don't manage sockets. You send messages. tofu does the socket work.


Messages Are Operations

NAQ: Why does this matter?

If you think "connect first, THEN send message" - you'll fight tofu. If you think "send a message that means 'connect'" - you'll work with tofu. Same result. Different approach. The second one is easier.

Traditional approach (wrong for tofu):

socket = connect(address);
send(socket, data);

tofu approach (correct):

// HelloRequest means "connect to this address"
// You don't connect first. The message IS the connection request.
const bhdr = try chnls.post(&helloRequest);

The HelloRequest doesn't just carry data. It IS the operation. tofu sees it and thinks: "User wants to connect. Let me handle that."


Intent vs Implementation

tofu separates what you want from how it happens.

You decide tofu handles
"I want to listen for connections" Socket creation, binding, accepting
"I want to connect to server X" Socket creation, DNS lookup, TCP handshake
"I want to send this data" Serialization, write operations
"I want to close this connection" Graceful shutdown, socket cleanup

You express intent through messages. tofu handles implementation.


The Four Message Operations

Every tofu operation maps to a message:

Intent Message
Start listening WelcomeRequest
Connect to peer HelloRequest
Send data Request / Response / Signal
Close connection ByeRequest or ByeSignal

There's no connect() function

tofu doesn't have a connect function. You send a HelloRequest that contains the server address. tofu sees it, connects, and sends the message. One operation, not two.


The Basic Pattern

Almost everything follows this flow:

// 1. Get a message
var msg = try ampe.get(.always);
defer ampe.put(&msg);

// 2. Fill it in (set opCode, channel, data)
msg.?.bhdr.proto.opCode = .Request;
msg.?.bhdr.channel_number = peer_channel;
try msg.?.body.append(my_data);

// 3. Submit it
const bhdr = try chnls.post(&msg);

// 4. Wait for result
var response = try chnls.waitReceive(timeout);
defer ampe.put(&response);

Four APIs. That's all you need:

  • ampe.get() — get a message to work with
  • ampe.put() — return a message when done
  • chnls.post() — submit a message for processing
  • chnls.waitReceive() — receive incoming messages

Peer Symmetry

Here's something that surprises people coming from traditional client/server.

Before connection:

Server: Waiting for clients (has WelcomeRequest)
Client: Wants to connect (sends HelloRequest)

After connection:

Peer A and Peer B
Both can send Request, Response, Signal
Both can initiate close
No more "client" or "server"

Once HelloRequest/HelloResponse completes, both sides are equal. Either can send any message type. Either can close the connection.

NAQ: But my server needs to send jobs to workers...

That's fine. Your protocol decides who sends what. tofu just gives you symmetric capabilities.

The "server" can send Requests asking the "client" to do work. The "client" can send Signals with progress updates. Roles are your design choice, not a tofu constraint.


Channels = Virtual Connections

A channel is tofu's abstraction for a connection.

Two types:

  • Listener channel — like a server socket, accepts incoming connections
  • IO channel — like a connected socket, sends and receives messages

Channel numbers:

  • 0 = not assigned yet (you use this for HelloRequest, WelcomeRequest)
  • 1-65534 = valid channels (assigned by tofu)
  • 65535 = reserved (don't use)

Only tofu assigns channel numbers

You create messages with channel 0. tofu assigns a real number during post(). Save it. You need it for all future messages to this peer.

// You send HelloRequest with channel 0
msg.?.bhdr.channel_number = 0;  // Not assigned yet

// tofu assigns a channel and returns it
const bhdr = try chnls.post(&msg);
const my_channel = bhdr.channel_number;  // Now assigned (e.g., 7)

// Use this channel for all future messages to this peer

Async by Default

post() ≠ sent

post() means "submitted for processing". The actual send happens later on tofu's internal thread.

const bhdr = try chnls.post(&msg);
// Message is queued. Not sent yet.
// tofu will send it on its internal thread.
// Success or failure comes via waitReceive.

Everything happens asynchronously:

  • You post a message
  • tofu processes it (connect, send, whatever)
  • Results come back via waitReceive()

This is why the pattern is always: post → waitReceive.


Example: Server Setup

Here's how "start listening" works with tofu:

Server becomes available
// Get message
var welcomeReq = try ampe.get(.always);
defer ampe.put(&welcomeReq);

// Set up WelcomeRequest with listen address
var addr: Address = .{ .tcp_server_addr = address.TCPServerAddress.init("0.0.0.0", 7099) };
try addr.format(welcomeReq.?);

// Submit it — tofu creates the listener
const bhdr = try chnls.post(&welcomeReq);
const listener_channel = bhdr.channel_number;

// Wait for confirmation
var welcomeResp = try chnls.waitReceive(timeout);
defer ampe.put(&welcomeResp);

// Now listening on listener_channel

You didn't call bind() or listen(). You sent a WelcomeRequest that means "please start listening". tofu did the rest.


Example: Client Connection

Here's how "connect to server" works:

Client connects
// Get message
var helloReq = try ampe.get(.always);
defer ampe.put(&helloReq);

// Set up HelloRequest with server address
var addr: Address = .{ .tcp_client_addr = address.TCPClientAddress.init("127.0.0.1", 7099) };
try addr.format(helloReq.?);

// Submit it — tofu connects and sends
const bhdr = try chnls.post(&helloReq);
const server_channel = bhdr.channel_number;  // Save this!

// Wait for server response
var helloResp = try chnls.waitReceive(timeout);
defer ampe.put(&helloResp);

// Now connected. Use server_channel for all communication.

You didn't call connect(). You sent a HelloRequest that contains the server address. tofu connected and sent it.


Summary

Traditional tofu
Connect, then send Send (it connects)
Manage sockets Manage messages
Client vs Server Peer vs Peer
Synchronous steps Async post → waitReceive
Multiple APIs for different things Four APIs for everything

What's Next

Now you're ready to learn the details:

  • Message — The structure of a tofu message
  • Address — How to specify connection addresses
  • ChannelGroup — Managing multiple channels

Messages are operations. tofu does the network work.