Callbacks and Subscriptions
Server-Initiated Callbacks
Operations you start report their outcome when they complete (see Asynchronous Operations), but plenty happens that you did not initiate: players join, properties change, events arrive. The client delivers these server-initiated signals as callbacks, one per kind of signal; the Callback Overview lists them for each language.
In C++, each callback is a public member of RealtimeClient, OnPlayerJoined, OnRoomJoined, OnDisconnected and the rest, each of which is a RealtimeCore::Common::Broadcaster.
A Broadcaster<Signature> is a multicast callback list.
Subscribe(handler) registers any callable matching the signature and returns a Subscription handle.
When the client broadcasts, every live, non-blocked subscriber is invoked in turn.
In JavaScript, each callback is a named event of RealtimeClient, such as 'playerJoined', 'roomJoined' or 'disconnected'.
on(name, handler) registers a handler and returns a function that unsubscribes it.
Every handler receives a single payload object, or nothing for events without a payload, and every handler of an event is invoked in turn.
Over the C API there is nothing to subscribe to: every callback arrives as an event on the queue, as the C tabs show.
All callbacks fire inside the service call, on the thread that calls it.
There is no cross-thread delivery, and nothing fires between two service calls; see the threading model for the wider rules.
In JavaScript, the service call is a tick of the automatic pump, or your own service() call when the client was created with autoPump: false.
Subscribing to Client Callbacks
Subscribing takes a handler that receives the callback's arguments, in C++ it is a lambda, or any callable, matching the callback's signature.
Custom events are the one exception to the one-callback-per-signal pattern: they are registered by event code and are covered on the Custom Events page.
In C++, SubscribeEvent() filters by event code and returns the same Subscription type.
In JavaScript, onEvent(), onEventRange() and onAnyEvent() register a handler for one code, a range of codes or every code, and return an unsubscribe function like on().
The following subscribes to players joining and to every custom event:
C++
RealtimeCore::Common::Subscription joinSub = client.OnPlayerJoined.Subscribe(
[](const Player& player) {
std::printf("player %d joined\n", player.Number);
});
RealtimeCore::Common::Subscription eventSub = client.SubscribeEvent(
[](uint8_t eventCode, int senderId, std::span<const uint8_t> data) {
std::printf("event %d from player %d (%zu bytes)\n", eventCode, senderId, data.size());
});
C
/* Nothing is subscribed: realtime_create attaches every callback to the queue,
and dispatch is a switch over the event type inside the polled batch. */
switch (e->type)
{
case RealtimeEventType_PlayerJoined:
/* `origin` is the player number; the blob is the full player record. */
printf("player %u joined\n", e->origin);
break;
case RealtimeEventType_Event:
/* `origin` is the sender, `counter` the event code. */
printf("event %u from player %u (%d bytes)\n",
e->counter, e->origin, e->blobLength);
break;
default:
break;
}
/* Over the C API there is no Broadcaster and nothing to subscribe to.
Every callback in the table at the bottom of this page is pre-subscribed
by realtime_create and arrives as one of the broadcaster events 100 to
120; the C API page maps each event to its callback and lists what
origin, counter and the blob carry. */
JavaScript
const unsubscribeJoined = client.on('playerJoined', ({ player }) => {
console.log(`player ${player.number} joined`);
});
const unsubscribeEvents = client.onAnyEvent(
({ code, senderNumber, data }) => {
console.log(`event ${code} from player ${senderNumber} (${data.length} bytes)`);
},
{ asBytes: true },
);
{ asBytes: true } delivers the raw payload as a Uint8Array, without it data is the decoded value.
Subscription Lifetime
A subscription stays registered until you remove it. It is not tied to the lifetime of the code that registered it.
In C++, a raw Subscription does not auto-unsubscribe when it is destroyed; the handler stays registered until you call Unsubscribe().
IsSubscribed() and operator bool report whether the handle is still attached.
In JavaScript, call the function that on() returned, or off(name, handler).
on() also takes { once: true }, which removes the handler after its first call, and { signal }, which removes it when the AbortSignal aborts.
once(name) returns a promise of the next occurrence instead of taking a handler; its timeoutMs option rejects with a DOMException named 'TimeoutError' when the event does not arrive in time.
Anything the handler refers to must stay valid for as long as the subscription is live. A handler that refers to an object which is destroyed or shut down while still subscribed is the classic dangling-callback bug. Tie the subscription's lifetime to the lifetime of the object the handler refers to, which is what the following patterns are for.
ScopedSubscription
This section describes a C++ type.
In JavaScript, the owning object keeps the function that on() returned and calls it from its own dispose() method, as the JavaScript tab shows. JavaScript has no destructors, so the code that owns the object calls dispose() explicitly.
Over the C API, there is no subscription to scope: a system is fed by your own dispatcher, as the C tab shows.
ScopedSubscription is the RAII wrapper: it unsubscribes automatically when destroyed and is the recommended default for holding a subscription.
It is move-only, constructs implicitly from a Subscription, and Release() hands the raw handle back if you need to manage it manually after all.
Held as a class member, it guarantees the handler never outlives the object it captures, as in the following score board:
C++
class ScoreBoard
{
public:
explicit ScoreBoard(RealtimeClient& client)
: _playerJoined(client.OnPlayerJoined.Subscribe(
[this](const Player& player) { _names.push_back(player.Name); }))
{
}
private:
std::vector<RealtimeCore::Common::StringType> _names;
RealtimeCore::Common::ScopedSubscription _playerJoined;
};
C
/* The rt_* blob helpers, copied from "Blob Helpers" on the C API page. */
#include "photon_blob.h"
/* There is no subscription to scope, so a system is fed by the dispatcher
instead of registering with the client. */
typedef struct
{
char names[32][64];
int32_t count;
} ScoreBoard;
static void ScoreBoardOnEvent(ScoreBoard* board, const RealtimeEvent* e, const uint8_t* blob)
{
if (e->type != RealtimeEventType_PlayerJoined || e->blobOffset < 0 || board->count >= 32)
{
return;
}
/* Player blob: [int32 number][string name][string userId][map][u8][u8] */
rt_reader r = rt_read(blob + e->blobOffset, e->blobLength);
(void)rt_get_i32(&r); /* number, also available as e->origin */
int32_t nameLen = 0;
const char* name = rt_get_str(&r, &nameLen);
if (nameLen > (int32_t)sizeof(board->names[0]) - 1)
{
nameLen = (int32_t)sizeof(board->names[0]) - 1;
}
/* The blob is only valid until the next flush, so copy the name out. */
memcpy(board->names[board->count], name, (size_t)nameLen);
board->names[board->count][nameLen] = '\0';
++board->count;
}
/* Lifetime problems move with the model: instead of a dangling handler,
the risk is a dangling pointer into the blob. Copy anything a system
retains out of the blob before the frame's realtime_event_queue_flush. */
JavaScript
class ScoreBoard {
names = new Set();
#unsubscribe = client.on('playerJoined', ({ player }) => {
this.names.add(player.name);
});
dispose() {
this.#unsubscribe();
}
}
SubscriptionBag
This section describes a C++ type.
In JavaScript, pass one AbortSignal to every on() call of a system and abort its AbortController to remove them all at once, as the JavaScript tab shows.
Over the C API, one dispatch function per system takes the place of a bag, as the C tab shows.
SubscriptionBag collects any number of subscriptions for bulk lifetime management.
Add with +=, tear everything down with UnsubscribeAll() (or by destroying the bag) and inspect with Count() and IsEmpty().
One bag, or one AbortController, typically covers one gameplay system.
The following registers the three handlers of a room system and removes them in one call:
C++
RealtimeCore::Common::SubscriptionBag subscriptions;
subscriptions += client.OnRoomJoined.Subscribe([] { std::puts("room joined"); });
subscriptions += client.OnRoomLeft.Subscribe([] { std::puts("room left"); });
subscriptions += client.OnPlayerLeft.Subscribe(
[](int playerNumber, bool isInactive) {
std::printf("player %d left (inactive: %d)\n", playerNumber, isInactive);
});
// later, when the system shuts down:
subscriptions.UnsubscribeAll();
C
/* One dispatch function per system takes the place of one bag per system. */
static void RoomSystemOnEvent(const RealtimeEvent* e)
{
switch (e->type)
{
case RealtimeEventType_RoomJoined:
puts("room joined");
break;
case RealtimeEventType_RoomLeft:
puts("room left");
break;
case RealtimeEventType_PlayerLeft:
/* `counter` is 1 when the player stays reserved as inactive. */
printf("player %u left (inactive: %u)\n", e->origin, e->counter);
break;
default:
break;
}
}
/* Shutting the system down means no longer calling it: */
if (roomSystemActive)
{
RoomSystemOnEvent(e);
}
/* There is nothing to unsubscribe, so bulk teardown becomes a flag on your
own dispatcher. Events keep arriving on the queue either way, and
unhandled ones are simply dropped when the batch is flushed. */
JavaScript
const subscriptions = new AbortController();
const { signal } = subscriptions;
client.on('roomJoined', () => console.log('room joined'), { signal });
client.on('roomLeft', () => console.log('room left'), { signal });
client.on(
'playerLeft',
({ playerNumber, isInactive }) => console.log(`player ${playerNumber} left (inactive: ${isInactive})`),
{ signal },
);
// later, when the system shuts down:
subscriptions.abort();
Blocking a Subscription
This section applies to C++. JavaScript has no counterpart: remove the handler and register it again, or let the handler return early while a flag of your own is set.
Block() temporarily silences a handler without unsubscribing it; Unblock() reactivates it and IsBlocked() queries the state.
This is handy for ignoring reactions to changes you are about to cause yourself, or for muting UI updates during a cutscene or loading screen without losing the registration.
Subscribing and Unsubscribing During Dispatch
Dispatch is re-entrancy safe: a handler may subscribe new handlers or unsubscribe any handler, including itself, while a broadcast is running. A handler subscribed during a broadcast first runs for the next one.
In C++, the change is deferred and applied once the current broadcast finishes, so the in-flight broadcast still runs against the subscriber list it started with. In JavaScript, an unsubscribe takes effect at once: a handler removed during a broadcast is not called for it if its turn has not come yet.
Accessing Realtime from within a Callback
Calling back into the client from inside a handler is safe, you can send events, change properties or start operations such as joining a room directly from a callback.
The one exception is the pump itself: never call the service function from within a callback, because it is what is currently executing your callback and must not re-enter.
In C++, that means Service(true) and the DispatchIncomingCommand() method.
In JavaScript, service() throws an Error when called from inside an event handler; dispose() is allowed there.
Callback Overview
| C++ | C | JavaScript | Fires When |
|---|---|---|---|
OnDisconnected |
RealtimeEventType_Disconnected |
'disconnected' |
The connection is lost or closed, with the DisconnectCause. |
OnError |
RealtimeEventType_Error |
'error' |
An asynchronous error occurs outside any pending operation. |
OnWarning |
RealtimeEventType_Warning |
'warning' |
The SDK reports a non-fatal warning, with a WarningCode and a message. |
OnRoomJoined |
RealtimeEventType_RoomJoined |
'roomJoined' |
The local client enters a room. |
OnRoomLeft |
RealtimeEventType_RoomLeft |
'roomLeft' |
The local client leaves a room. |
OnPlayerJoined |
RealtimeEventType_PlayerJoined |
'playerJoined' |
A remote player joins the current room. |
OnPlayerLeft |
RealtimeEventType_PlayerLeft |
'playerLeft' |
A player leaves the current room or becomes inactive. |
OnMasterClientChanged |
RealtimeEventType_MasterClientChanged |
'masterClientChanged' |
Mastership moves to another player. |
OnRoomPropertiesChanged |
RealtimeEventType_RoomPropertiesChanged |
'roomPropertiesChanged' |
The room's custom properties change. |
OnPlayerPropertiesChanged |
RealtimeEventType_PlayerPropertiesChanged |
'playerPropertiesChanged' |
A player's custom properties change. |
OnPropertiesChangeFailed |
RealtimeEventType_PropertiesChangeFailed |
'propertiesChangeFailed' |
A property update is rejected by the server. |
OnDirectMessage |
RealtimeEventType_DirectMessage |
'directMessage' |
A direct message arrives, peer-to-peer or relayed by the server. |
OnDirectConnectionEstablished |
RealtimeEventType_DirectConnectionEstablished |
'directConnectionEstablished' |
A direct connection to a remote player is established. |
OnDirectConnectionFailed |
RealtimeEventType_DirectConnectionFailed |
'directConnectionFailed' |
A direct connection attempt fails. |
OnRoomListUpdated |
RealtimeEventType_RoomListUpdated |
'roomListUpdated' |
The lobby's room list changes. |
OnLobbyStats |
RealtimeEventType_LobbyStats |
'lobbyStats' |
Lobby statistics arrive. |
OnAppStatsUpdated |
RealtimeEventType_AppStatsUpdated |
'appStatsUpdated' |
Application statistics update. |
OnCustomAuthStep |
RealtimeEventType_CustomAuthStep |
'customAuthStep' |
A custom authentication provider requests another step. |
In JavaScript, the 'error' payload is a RealtimeError whose code is a ClientErrorCode, and the 'warning' payload is { code, message } with a WarningCode string as code.
The web build has no peer-to-peer transport, so its direct messages are always relayed by the server.
Custom events are absent from this table because they are registered by event code: with SubscribeEvent() in C++, as RealtimeEventType_Event over the C API, and with onEvent(), onEventRange() or onAnyEvent() in JavaScript; see Custom Events.
The full C++ signatures are listed in the client callbacks reference; in JavaScript, the package's ClientEvents type declares the payload of every event.
Last updated on
Back to top