The SDK emits events when things happen — a player joins a lobby, a peer connects, the backend goes offline. Your game listens for these to react in real time.
Listening for events
WavedashSDK.lobby_joined.connect(_on_lobby_joined)
WavedashSDK.lobby_message.connect(_on_lobby_message)
WavedashSDK.p2p_connection_established.connect(_on_peer_connected)
WavedashSDK.backend_connected.connect(_on_backend_connected)
Wavedash.SDK.OnLobbyJoined += OnLobbyJoined;
Wavedash.SDK.OnLobbyMessage += OnLobbyMessage;
Wavedash.SDK.OnP2PConnectionEstablished += OnPeerConnected;
Wavedash.SDK.OnBackendConnected += OnBackendConnected;
// Strongly typed payload
// .on() returns an unsubscribe function.
const unsubscribeLobbyJoined = Wavedash.on(Wavedash.Events.LOBBY_JOINED, (payload) => {
console.log(`Joined lobby ${payload.lobbyId}`);
});
Wavedash.on(Wavedash.Events.LOBBY_MESSAGE, (payload) => {
console.log(`${payload.username}: ${payload.message}`);
});
Wavedash.on(Wavedash.Events.P2P_CONNECTION_ESTABLISHED, (payload) => {
console.log(`Peer connected: ${payload.username}`);
});
Wavedash.on(Wavedash.Events.BACKEND_CONNECTED, (payload) => {
console.log(`Backend connected (attempt ${payload.connectionCount})`);
});
// Later, to detach:
unsubscribeLobbyJoined(); // via the unsubscribe fn
Wavedash.off(Wavedash.Events.LOBBY_JOINED, handler); // or using .off
Lobby events
| Event | Fires when | Payload |
|---|---|---|
LOBBY_JOINED | You join or create a lobby | lobbyId, hostId, users, metadata |
LOBBY_USERS_UPDATED | A user joins or leaves | lobbyId, userId, username, userAvatarUrl, isHost, changeType ("JOINED" or "LEFT") |
LOBBY_MESSAGE | A chat message arrives | messageId, lobbyId, userId, username, message, timestamp |
LOBBY_DATA_UPDATED | Lobby metadata changes | The full metadata object |
LOBBY_KICKED | You're removed from the lobby | lobbyId, reason ("KICKED" or "ERROR") |
LOBBY_INVITE | You receive a lobby invite | notificationId, lobbyId, sender (object with _id, username, avatarUrl), _creationTime |
Payload examples
LOBBY_JOINED — fires once when you create or join. users includes you. metadata is whatever the host has set with setLobbyData(); it's an empty object on a fresh lobby.
{
"lobbyId": "j570h2k8p3m7n5q1r4s9t6v2w8x0y3z4",
"hostId": "k7m1n3p5q7r9s2t4v6w8x0y2z4a6b8c0",
"users": [
{
"userId": "k7m1n3p5q7r9s2t4v6w8x0y2z4a6b8c0",
"username": "alice",
"userAvatarUrl": "https://cdn.wavedash.gg/avatars/alice.png",
"lobbyId": "j570h2k8p3m7n5q1r4s9t6v2w8x0y3z4",
"isHost": true
},
{
"userId": "n2p4q6r8s0t2v4w6x8y0z2a4b6c8d0e2",
"username": "bob",
"lobbyId": "j570h2k8p3m7n5q1r4s9t6v2w8x0y3z4",
"isHost": false
}
],
"metadata": {
"gameMode": "deathmatch",
"mapName": "arena",
"round": "2"
}
}
LOBBY_USERS_UPDATED — one event per join/leave. Use changeType to branch.
{
"userId": "n2p4q6r8s0t2v4w6x8y0z2a4b6c8d0e2",
"username": "bob",
"userAvatarUrl": "https://cdn.wavedash.gg/avatars/bob.png",
"lobbyId": "j570h2k8p3m7n5q1r4s9t6v2w8x0y3z4",
"isHost": false,
"changeType": "JOINED"
}
LOBBY_MESSAGE — timestamp is Date.now()-style ms since epoch.
{
"messageId": "m1n3p5q7r9s1t3v5w7x9y1z3a5b7c9d1",
"lobbyId": "j570h2k8p3m7n5q1r4s9t6v2w8x0y3z4",
"userId": "k7m1n3p5q7r9s2t4v6w8x0y2z4a6b8c0",
"username": "alice",
"message": "ready when you are",
"timestamp": 1714296245000
}
LOBBY_DATA_UPDATED — the entire metadata object, not a diff. Values are strings or numbers.
{
"gameMode": "deathmatch",
"mapName": "arena",
"round": "3",
"scoreLimit": 10
}
LOBBY_KICKED:
{
"lobbyId": "j570h2k8p3m7n5q1r4s9t6v2w8x0y3z4",
"reason": "KICKED"
}
LOBBY_INVITE — note the sender object uses _id and avatarUrl (not userId/userAvatarUrl like the lobby user shape above).
{
"notificationId": "p3q5r7s9t1v3w5x7y9z1a3b5c7d9e1f3",
"lobbyId": "j570h2k8p3m7n5q1r4s9t6v2w8x0y3z4",
"sender": {
"_id": "k7m1n3p5q7r9s2t4v6w8x0y2z4a6b8c0",
"username": "alice",
"avatarUrl": "https://cdn.wavedash.gg/avatars/alice.png"
},
"_creationTime": 1714296245000
}
P2P events
| Event | Fires when | Payload |
|---|---|---|
P2P_CONNECTION_ESTABLISHED | Both data channels to a peer are open and the peer is ready to send/receive | userId, username |
P2P_PEER_RECONNECTING | A peer's ICE connection failed and the SDK is attempting an ICE restart. Fires on both sides of the link. | userId, username |
P2P_PEER_RECONNECTED | A peer that was reconnecting is back online. Fires on both sides. | userId, username |
P2P_PEER_DISCONNECTED | A peer's data channel closed — they left the lobby, their browser tab exited, or the SDK gave up after too many failed ICE restarts | userId, username |
P2P_CONNECTION_FAILED | A peer connection failed terminally (no TURN credentials, data-channel error, or ICE restart attempts exhausted) | userId, username, error |
P2P_PACKET_DROPPED | The SDK dropped an outgoing or incoming P2P packet (see Packet drop reasons) | channel, direction ("SEND" or "RECEIVE"), reason, droppedCount, droppedTotal |
Peer connection lifecycle
A healthy peer lifecycle is P2P_CONNECTION_ESTABLISHED → (traffic) → P2P_PEER_DISCONNECTED. When the network hiccups — a Wi-Fi switch, a brief NAT rebinding — the SDK does an ICE restart and you'll see P2P_PEER_RECONNECTING → P2P_PEER_RECONNECTED in between. Treat that pair as "this peer is unreachable right now" → "go ahead and resume sending". It's still the same peer, no re-handshake is needed, and you don't need to tear down any per-peer state.
If ICE restarts exceed the SDK's retry budget (3 attempts), the SDK emits P2P_CONNECTION_FAILED and then P2P_PEER_DISCONNECTED as the channel closes. For broadcast sends, this is the signal to drop the peer from your "reachable" set — see Multiplayer networking for the full pattern.
Packet drop reasons
The reason field on P2P_PACKET_DROPPED tells you what remedy, if any, is needed:
| Reason | Direction | Happens when | What to do |
|---|---|---|---|
QUEUE_FULL | RECEIVE | The receive-side ring buffer is full | Drain channels more often, throttle send rate, or raise p2p.maxIncomingMessages in init() |
PAYLOAD_TOO_LARGE | Either | Payload exceeds the configured slot size | Reduce payload or raise p2p.messageSize in init() |
INVALID_PAYLOAD_SIZE | SEND | payloadSize was ≤ 0, larger than the buffer, or payload was null | Programming error — fix the call site |
INVALID_CHANNEL | Either | Channel index was outside [0, 8) | programming error, SDK version skew or a malicious peer. |
MALFORMED | RECEIVE | Incoming wire data was too short to parse a channel header | Almost always SDK version skew; channel is -1 |
PEER_NOT_READY | SEND | Target peer's channel wasn't open, or P2P wasn't initialized. Only emitted for unicast sendP2PMessage; broadcasts silently skip unready peers. | Wait for P2P_CONNECTION_ESTABLISHED and watch the reconnect/disconnect events for reachability |
Events are rate-limited per (direction, channel, reason) tuple. The first drop on an idle tuple fires immediately; further drops within 500 ms are coalesced into a single event at the end of the window. droppedCount is the number of drops in the current event, droppedTotal is the cumulative count for that tuple since P2P init.
Payload examples
The lifecycle events all share the same shape:
{
"userId": "n2p4q6r8s0t2v4w6x8y0z2a4b6c8d0e2",
"username": "bob"
}
P2P_CONNECTION_FAILED adds an error string:
{
"userId": "n2p4q6r8s0t2v4w6x8y0z2a4b6c8d0e2",
"username": "bob",
"error": "ICE restart attempts exhausted"
}
P2P_PACKET_DROPPED:
{
"channel": 0,
"direction": "SEND",
"reason": "QUEUE_FULL",
"droppedCount": 5,
"droppedTotal": 47
}
Stats events
| Event | Fires when | Payload |
|---|---|---|
STATS_STORED | Stats are persisted to the server | success, message (optional) |
Payload examples
{ "success": true }
On failure, message describes what went wrong:
{ "success": false, "message": "Network error while persisting stats" }
Backend events
| Event | Fires when | Payload |
|---|---|---|
BACKEND_CONNECTED | Connected to Wavedash | isConnected, hasEverConnected, connectionCount, connectionRetries |
BACKEND_DISCONNECTED | Lost connection | Same as above |
BACKEND_RECONNECTING | Attempting to reconnect | Same as above |
Payload examples
All three events share this shape. connectionCount increments on each successful connect; connectionRetries counts failed attempts in the current outage.
{
"isConnected": true,
"hasEverConnected": true,
"connectionCount": 2,
"connectionRetries": 0
}
Fullscreen events
| Event | Fires when | Payload |
|---|---|---|
FULLSCREEN_CHANGED | Host page enters or exits fullscreen | isFullscreen (boolean) |
See Fullscreen for the request/toggle API and overlay-aware integration.
Per-language names
Each event is exposed as a JS constant, a Unity C# event, and a Godot signal. Connect to whichever matches your engine — the payload fields are the same across all three.
| Event | JavaScript constant | Unity event | Godot signal |
|---|---|---|---|
| Lobby joined | Wavedash.Events.LOBBY_JOINED | OnLobbyJoined | lobby_joined |
| Lobby users updated | Wavedash.Events.LOBBY_USERS_UPDATED | OnLobbyUsersUpdated | lobby_users_updated |
| Lobby message | Wavedash.Events.LOBBY_MESSAGE | OnLobbyMessage | lobby_message |
| Lobby data updated | Wavedash.Events.LOBBY_DATA_UPDATED | OnLobbyDataUpdated | lobby_data_updated |
| Lobby kicked | Wavedash.Events.LOBBY_KICKED | OnLobbyKicked | lobby_kicked |
| Lobby invite | Wavedash.Events.LOBBY_INVITE | OnLobbyInvite | lobby_invite |
| P2P connection established | Wavedash.Events.P2P_CONNECTION_ESTABLISHED | OnP2PConnectionEstablished | p2p_connection_established |
| P2P peer reconnecting | Wavedash.Events.P2P_PEER_RECONNECTING | OnP2PPeerReconnecting | p2p_peer_reconnecting |
| P2P peer reconnected | Wavedash.Events.P2P_PEER_RECONNECTED | OnP2PPeerReconnected | p2p_peer_reconnected |
| P2P peer disconnected | Wavedash.Events.P2P_PEER_DISCONNECTED | OnP2PPeerDisconnected | p2p_peer_disconnected |
| P2P connection failed | Wavedash.Events.P2P_CONNECTION_FAILED | OnP2PConnectionFailed | p2p_connection_failed |
| P2P packet dropped | Wavedash.Events.P2P_PACKET_DROPPED | OnP2PPacketDropped | p2p_packet_dropped |
| Current stats received | — | OnCurrentStatsReceived | current_stats_received |
| Stats stored | Wavedash.Events.STATS_STORED | OnStatsStored | stats_stored |
| Backend connected | Wavedash.Events.BACKEND_CONNECTED | OnBackendConnected | backend_connected |
| Backend disconnected | Wavedash.Events.BACKEND_DISCONNECTED | OnBackendDisconnected | backend_disconnected |
| Backend reconnecting | Wavedash.Events.BACKEND_RECONNECTING | OnBackendReconnecting | backend_reconnecting |
| Fullscreen changed | Wavedash.Events.FULLSCREEN_CHANGED | OnFullscreenChanged | fullscreen_changed |
Current stats received is surfaced as the awaited response of requestStats() in JavaScript (no separate event). Unity and Godot raise it as a callback instead because their bindings deliver stats asynchronously through the SDK's engine-callback bridge.
Godot additionally emits per-call signals (got_leaderboard, got_leaderboard_entries, posted_leaderboard_score, ugc_item_created, ugc_item_updated, ugc_item_downloaded, remote_file_uploaded, remote_file_downloaded, remote_directory_downloaded, got_remote_directory_listing, got_lobbies, sent_lobby_invite, got_lobby_invite_link, lobby_created, lobby_left, got_friends, got_user_jwt, user_avatar_loaded). These fire with the response of the matching async call — use them if you prefer signal-based flow over await.
Deferring events
If your game needs time to load before handling events, pass deferEvents: true to init(). The SDK queues all events until you call readyForEvents().
func _ready():
WavedashSDK.init({"deferEvents": true})
# set up listeners, load assets, etc.
WavedashSDK.ready_for_events() # flush the queue
using System.Collections.Generic;
void Awake()
{
Wavedash.SDK.Init(new Dictionary<string, object>
{
{ "deferEvents", true }
});
// set up listeners, load assets, etc.
Wavedash.SDK.ReadyForEvents(); // flush the queue
}
Wavedash.init({ deferEvents: true });
// set up listeners, load assets, etc.
Wavedash.readyForEvents(); // flush the queue