C++ client
Rune API
Connect, define a typed topic, produce, subscribe, run a transaction, query a view.
Connect
One session to any node; leader redirects and reconnects are handled for you.
#include <rune/client/Client.h>
using namespace rune::client;
ClientConfig cfg;
cfg.gatewayChannel = "aeron:udp?endpoint=node1:40200"; // any node
cfg.gatewayStreamId = 1;
cfg.egressChannel = "aeron:udp?endpoint=me:40400"; // where replies arrive
cfg.egressStreamId = 2;
cfg.offerCrypto = true; // TLS 1.3
cfg.requireCrypto = true;
cfg.tlsExpectHost = "node1.example";
Client client(cfg);
client.connectAndWait();
from your game loop
cfg.runnerMode = RunnerMode::Invoker; // no client threads
// ...
while (running) {
client.doWork(); // once per server tick
tickWorld();
}
Topics
Defined in Rune QL from the rune console. All three are sharded by userId, so a player's data lives on one partition.
rune> topic user { userId int64, name string(32), level int32 }
partitions 16 key userId;
rune> topic character { userId int64, characterId int64, class string(16), level int32 }
partitions 16 key (userId, characterId) partition by userId;
rune> topic position { userId int64, characterId int64, zone int32, x float64, y float64, z float64 }
partitions 16 key (userId, characterId) partition by userId;
rune> alter topic character add column guildId int64, set compatibility backward;
transient topic: replicated in memory, never written to disk. For high-rate, short-lived state.
rune> transient topic presence { userId int64, online bool, zone int32 }
partitions 16 key userId;
generate C++ types from the registry
rune schema generate character --out gen/ # gen/character.gen.h
or define a topic from code
auto d = rune::codec::DescriptorBuilder("user", 1)
.add("userId", PrimType::Int64, 1, /*isKey=*/true)
.add("level", PrimType::Int32)
.build();
client.defineTopicAndWait("user", 16, "userId", /*validate=*/true, rune::codec::serialize(d));
Produce
Acks::None · Leader · All. trySend is the non-blocking form.
auto p = client.producer({.acks = Acks::All, .lingerUs = 200});
auto positions = client.topic("position");
p.send(positions, Key(userId, characterId), bytes); // keyed, idempotent
many records: sends within lingerUs are batched automatically
for (const auto& e : events)
p.send(positions, Key(e.userId, e.characterId), e.bytes);
many records as one atomic append to one partition
auto b = p.batch(positions, Key(userId, characterId));
b.add(step1);
b.add(step2);
b.add(step3);
b.send(); // contiguous offsets, all or nothing
zero-copy: write the record straight into the send buffer
auto c = p.tryClaim(positions, Key(userId, characterId), PositionEncoder::kEncodedLength);
PositionEncoder(c.buffer()).userId(userId).characterId(characterId).zone(4).x(118.5).y(7.0).z(-42.25);
c.commit(); // or c.abort()
zero-copy batch
auto b = p.claimBatch(positions, Key(userId, characterId), /*records=*/3, PositionEncoder::kEncodedLength);
for (int i = 0; i < 3; ++i)
PositionEncoder(b.record(i)).userId(userId).characterId(characterId).zone(4).x(path[i].x).y(path[i].y).z(path[i].z);
b.commit(); // one atomic append
typed topic: generated flyweight, routed by the key
auto pos = client.openTopicAndWait<Position>("position");
auto w = pos.claim(userId, characterId);
w.zone(4).x(118.5).y(7.0).z(-42.25);
w.commit();
Subscribe
Push delivery, in order per partition. key and value are zero-copy spans, valid inside the handler.
auto sub = client.subscribe(positions, Key(userId, characterId),
{.from = From::earliest(), .isolation = Isolation::ReadCommitted},
[](const Record& r) { apply(r.offset, r.key, r.value); });
Consumer group
Register before connectAndWait(). Offsets auto-commit; gc.commit() commits on demand.
GroupConfig g;
g.autoOffsetReset = OffsetReset::Earliest;
auto gc = client.subscribeGroup("matchmaking", {"match_events"}, g);
gc.onAssigned([](std::vector<TopicPartition> parts) { /* ... */ });
gc.onRecords ([](const RecordSet& rs) { /* rs.partition, rs.recordBatch */ });
client.connectAndWait();
Transaction
Create before connectAndWait(). ReadCommitted readers see the trade only after commit.
#include <rune/client/producer/TransactionalProducer.h>
auto tp = client.transactionalProducer("trade-worker-1");
client.connectAndWait();
tp.initTransactionsAndWait();
tp.beginTransaction();
tp.send(inventory, Key(int64_t{seller}), removeSword);
tp.send(inventory, Key(int64_t{buyer}), addSword);
tp.send(wallets, Key(int64_t{seller}), addGold);
tp.commitTransactionAndWait(); // all three or none
Materialized views
Defined in Rune QL, kept current on every write, read by key. Persisted by default: checkpointed and recovered on restart, no rebuild. Add transient to keep one in memory only. No separate database.
one row per character: user, character and live position joined on userId and characterId
rune> create view character_view key (userId, characterId) as
select u.name, c.class, c.level, p.zone, p.x, p.y, p.z
from character c
join user u on u.userId = c.userId
left join position p on p.userId = c.userId and p.characterId = c.characterId;
per-user stats over all of a user's characters
rune> create view user_stats key userId as
select count(*) as characters, max(level) as top_level, avg(level) as avg_level
from character group by userId;
transient view: in memory only, rebuilt from its topics on restart
rune> create transient view user_presence key userId as
select count(*) as characters_online, last(zone) as last_zone
from position group by userId;
read
rune> get character_view (42, 7);
name=Ayla class=ranger level=31 zone=4 x=118.5 y=7.0 z=-42.25
auto row = client.getViewRowAndWait(characterView, ViewKey(42, 7), fp);
auto ch = CharacterViewDecoder(row.rowBytes); // generated from the view's schema
ch.zone(); ch.x(); ch.level();
A position update rewrites only that character's row. A user rename updates every row of that user. Deleting a character retracts its row and its contribution to user_stats.