Skip to content

Tags & groups ​

The named registry maps one name → one pid. Sometimes you need the inverse: one name → many processes. That's what tags are for.

A tag is a label a process attaches to itself. Any number of processes can share the same tag. Anyone can ask which processes currently hold a tag, and send each of them a message. When a process exits, its tags are released automatically — groups only ever contain live processes.

This is RUSM's pub/sub. There's no broker, no topic object, no subscription list to manage. The group is the set of processes that tagged themselves. Broadcasting is a loop over whereisTag + send.

Joining and leaving a group ​

A process joins a group by tagging itself. It can join multiple groups. Leaving is explicit with unregisterTag, or automatic when the process exits.

ts
import { Process } from "rusm-ts";

// Join a group — typically in your open() callback or at startup:
Process.registerTag("notifications:user-123");
Process.registerTag("broadcast:all-users");   // can hold multiple tags

// Leave explicitly if needed before process exit:
Process.unregisterTag("notifications:user-123");
rust
// Join a group:
rusm_rs::register_tag("notifications:user-123");
rusm_rs::register_tag("broadcast:all-users");

// Leave explicitly:
rusm_rs::unregister_tag("notifications:user-123");
go
// Join a group:
rusm.RegisterTag("notifications:user-123")
rusm.RegisterTag("broadcast:all-users")

// Leave explicitly:
rusm.UnregisterTag("notifications:user-123")

Broadcasting to a group ​

whereisTag(tag) returns the current list of live pids in the group. Broadcast by looping over them and sending each one a message.

ts
import { Process } from "rusm-ts";

const payload = JSON.stringify({ type: "price-update", sku: "ABC-1", price: 29.99 });

for (const pid of Process.whereisTag("broadcast:all-users")) {
  Process.send(pid, payload);
}
rust
let payload = serde_json::json!({ "type": "price-update", "sku": "ABC-1", "price": 29.99 });
let bytes = payload.to_string();

for pid in rusm_rs::whereis_tag("broadcast:all-users") {
    rusm_rs::send_bytes(pid, bytes.as_bytes());
}
go
payload, _ := json.Marshal(map[string]any{"type": "price-update", "sku": "ABC-1", "price": 29.99})

for _, pid := range rusm.WhereisTag("broadcast:all-users") {
    rusm.Send(pid, payload)
}

The loop is a snapshot — it captures who's in the group at that moment. A process that exits mid-loop is skipped silently (sending to a dead pid is always a no-op).

Use case 1: a chat room ​

Each WebSocket connection is its own process. When a connection joins a room, it tags itself. When any connection says something, it broadcasts to the room's tag:

ts
import { websocket, Process } from "rusm-ts";

let room: string | null = null;

export default websocket({
  open(socket) { /* greet */ },

  message(socket, data) {
    const msg = JSON.parse(new TextDecoder().decode(data));

    if (typeof msg.join === "string") {
      room = msg.join;
      Process.registerTag(`room:${room}`);   // join — this connection is now a group member
      return;
    }

    if (typeof msg.say === "string" && room) {
      // Broadcast to every connection in this room:
      const relay = JSON.stringify({ from: String(Process.self()), text: msg.say });
      for (const pid of Process.whereisTag(`room:${room}`)) {
        Process.send(pid, relay);
      }
    }
  },

  close() {},   // tag released automatically on disconnect
});
rust
// In the WebSocket handler's message callback:
if let Some(room_name) = frame.join {
    rusm_rs::register_tag(&format!("room:{}", room_name));
    self.room = Some(room_name);
}

if let Some(text) = frame.say {
    if let Some(room) = &self.room {
        let relay = serde_json::json!({ "from": rusm_rs::me().to_string(), "text": text });
        let bytes = relay.to_string();
        for pid in rusm_rs::whereis_tag(&format!("room:{}", room)) {
            rusm_rs::send_bytes(pid, bytes.as_bytes());
        }
    }
}
go
// In the WebSocket handler's Message callback:
if msg.Join != "" {
    rusm.RegisterTag("room:" + msg.Join)
    room = msg.Join
}

if msg.Say != "" && room != "" {
    relay, _ := json.Marshal(map[string]any{
        "from": rusm.Self(),
        "text": msg.Say,
    })
    for _, pid := range rusm.WhereisTag("room:" + room) {
        rusm.Send(pid, relay)
    }
}

No broker. No pub/sub middleware. Each connection is a process; the group is the room.

Use case 2: scoped cancellation ​

Tag every process that belongs to one unit of work — all the agents working on a plan, all the background jobs for one user request. To cancel the whole unit, call killTag. One call, instant, authoritative.

ts
import { Process } from "rusm-ts";

// Each agent tags itself with the plan it's working on:
Process.registerTag(`plan:${planId}`);

// The coordinator cancels everything for that plan in one call:
Process.killTag(`plan:${planId}`);
rust
// Each agent:
rusm_rs::register_tag(&format!("plan:{}", plan_id));

// The coordinator cancels the whole plan:
rusm_rs::kill_tag(&format!("plan:{}", plan_id));
go
// Each agent:
rusm.RegisterTag("plan:" + planID)

// The coordinator:
rusm.KillTag("plan:" + planID)

No cancel tokens, no polling, no bookkeeping of which pids to stop. The platform owns the group registry and the kill. The application writes two calls.

Zero overhead on untagged processes ​

Tags are stored in a per-process list. A process that holds no tags pays nothing — no allocation, no map lookup on the hot path. The group registry only touches a process at join, leave, and exit. Running thousands of untagged processes costs exactly as much as without tags.


Next: kill & killTag — stopping processes immediately or in groups, and when to reach for supervision instead.

MIT licensed