SignalR Hubs Overview
Titan uses SignalR WebSockets for real-time communication between clients and the server.
Hub Architecture
Available Hubs
| Hub | Endpoint | Description |
|---|---|---|
| AuthHub | /authHub | Token refresh, logout, profile |
| AccountHub | /accountHub | Account info, characters, cosmetics |
| CharacterHub | /characterHub | Character progression, stats |
| InventoryHub | /inventoryHub | Bag, equipment, items |
| TradeHub | /tradeHub | Peer-to-peer trading |
| SeasonHub | /seasonHub | Seasons, migrations |
| BaseTypeHub | /baseTypeHub | Item type registry |
Connection Flow
Base Class: TitanHubBase
All authenticated hubs extend TitanHubBase, which provides:
User Identity
// Get authenticated user's ID from JWT
protected Guid GetUserId() => Guid.Parse(Context.UserIdentifier!);
Orleans Client Access
// Access Orleans cluster
protected IClusterClient ClusterClient => _clusterClient;
Ownership Verification
// Verify character belongs to caller
protected async Task VerifyCharacterOwnershipAsync(Guid characterId)
{
var characterIds = await GetOwnedCharacterIdsAsync();
if (!characterIds.Contains(characterId))
{
throw new HubException("Character does not belong to this account.");
}
}
Presence Tracking
On connect/disconnect, the base class:
- Registers/unregisters with
PlayerPresenceGrain - Logs sessions to
SessionLogGrain(first/last connection only)
Authentication
All hubs (except AuthHub login methods) require JWT authentication:
[Authorize]
public class AccountHub : TitanHubBase
{
// All methods require valid JWT
}
Some methods require additional roles:
[Authorize(Roles = "Admin")]
public async Task<BaseType> Create(BaseType baseType)
{
// Admin-only operation
}
Input Validation
Hubs use HubValidationService with FluentValidation:
public async Task<CharacterSummary> CreateCharacter(
string seasonId,
string name,
CharacterRestrictions restrictions)
{
await _validation.ValidateIdAsync(seasonId, nameof(seasonId));
await _validation.ValidateNameAsync(name, nameof(name), 50);
// ... create character
}
Invalid input throws HubException which clients receive as errors.
Rate Limiting
The RateLimitHubFilter applies rate limits to hub methods:
Policy mapping is based on the hub endpoint path.
Error Handling
Hubs throw HubException for client-visible errors:
throw new HubException("Character does not belong to this account.");
Clients receive this as an error response. Internal exceptions are logged but not exposed.
Real-Time Updates
Hubs use SignalR groups for broadcasting updates:
// Join a group
await Groups.AddToGroupAsync(Context.ConnectionId, $"trade-{tradeId}");
// Broadcast to group
await Clients.Group($"trade-{tradeId}").SendAsync("TradeUpdate", data);
Clients subscribe to receive updates in real-time.
Client Connection Example
const connection = new signalR.HubConnectionBuilder()
.withUrl("https://api.example.com/accountHub", {
accessTokenFactory: () => accessToken
})
.withAutomaticReconnect()
.build();
// Handle reconnection
connection.onreconnected(connectionId => {
console.log('Reconnected:', connectionId);
});
// Start connection
await connection.start();
// Call hub method
const account = await connection.invoke("GetAccount");