-
Notifications
You must be signed in to change notification settings - Fork 0
Streaming Events
LichessSharp uses IAsyncEnumerable<T> for all streaming endpoints. This guide covers patterns for handling streaming events effectively.
All streaming methods return IAsyncEnumerable<T> and should be consumed with await foreach:
using var client = new LichessClient(token);
using var cts = new CancellationTokenSource();
// Graceful shutdown on Ctrl+C
Console.CancelKeyPress += (_, e) => { e.Cancel = true; cts.Cancel(); };
await foreach (var evt in client.Bot.StreamEventsAsync(cts.Token))
{
Console.WriteLine($"Event: {evt.Type}");
}Many streaming endpoints return polymorphic events with a type discriminator. Use pattern matching to handle each event type:
await foreach (var evt in client.Bot.StreamGameAsync(gameId, cancellationToken))
{
switch (evt)
{
case BotGameFullEvent full:
// First event - full game state
Console.WriteLine($"Game started: {full.Id}");
Console.WriteLine($"White: {full.White?.Name}");
Console.WriteLine($"Black: {full.Black?.Name}");
break;
case BotGameStateEvent state:
// Move update
Console.WriteLine($"Moves: {state.Moves}");
Console.WriteLine($"Status: {state.Status}");
break;
case BotChatLineEvent chat:
// Chat message
Console.WriteLine($"[{chat.Room}] {chat.Username}: {chat.Text}");
break;
case BotOpponentGoneEvent gone:
// Opponent disconnected
if (gone.ClaimWinInSeconds > 0)
{
Console.WriteLine($"Can claim win in {gone.ClaimWinInSeconds}s");
}
break;
}
}Use LINQ's OfType<T>() to filter for specific event types:
// Only process game state updates
await foreach (var state in client.Bot.StreamGameAsync(gameId)
.OfType<BotGameStateEvent>())
{
ProcessMove(state.Moves);
}Handle multiple games concurrently using background tasks:
var activeGames = new ConcurrentDictionary<string, Task>();
await foreach (var evt in client.Bot.StreamEventsAsync(cts.Token))
{
if (evt.Type == "gameStart")
{
var gameId = evt.Game!.GameId!;
// Start game handler in background
var task = Task.Run(async () =>
{
try
{
await PlayGameAsync(gameId, cts.Token);
}
finally
{
activeGames.TryRemove(gameId, out _);
}
});
activeGames[gameId] = task;
}
}
// Wait for all games to complete
await Task.WhenAll(activeGames.Values);Use Channel<T> to multiplex events from multiple streams:
var channel = Channel.CreateUnbounded<GameEvent>();
// Start multiple user streams
var streamTasks = users.Select(async user =>
{
await foreach (var game in client.Games.StreamByUsersAsync(new[] { user }, withCurrentGames: true))
{
await channel.Writer.WriteAsync(new GameEvent(user, game));
}
});
// Process all events in single consumer
var processTask = Task.Run(async () =>
{
await foreach (var evt in channel.Reader.ReadAllAsync())
{
Console.WriteLine($"{evt.User}: {evt.Game.Id}");
}
});Implement automatic reconnection for long-running streams:
async Task StreamWithReconnectAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
try
{
await foreach (var evt in client.Bot.StreamEventsAsync(ct))
{
ProcessEvent(evt);
}
}
catch (HttpRequestException ex) when (!ct.IsCancellationRequested)
{
Console.WriteLine($"Connection lost: {ex.Message}");
Console.WriteLine("Reconnecting in 5 seconds...");
await Task.Delay(TimeSpan.FromSeconds(5), ct);
}
catch (OperationCanceledException)
{
break;
}
}
}Always use CancellationToken for clean shutdown:
using var cts = new CancellationTokenSource();
// Handle shutdown signals
Console.CancelKeyPress += (_, e) =>
{
e.Cancel = true;
Console.WriteLine("Shutting down...");
cts.Cancel();
};
AppDomain.CurrentDomain.ProcessExit += (_, _) =>
{
cts.Cancel();
};
try
{
await foreach (var evt in client.Bot.StreamEventsAsync(cts.Token))
{
// Process events
}
}
catch (OperationCanceledException)
{
Console.WriteLine("Stream stopped gracefully");
}LichessSharp configures appropriate timeouts automatically:
- Regular requests: 30 seconds (configurable)
- Streaming requests: Infinite (configurable)
Override if needed:
var options = new LichessClientOptions
{
DefaultTimeout = TimeSpan.FromSeconds(60),
StreamingTimeout = Timeout.InfiniteTimeSpan
};
using var client = new LichessClient(token, options);LichessSharp handles rate limiting automatically with configurable retry:
var options = new LichessClientOptions
{
AutoRetryOnRateLimit = true,
MaxRateLimitRetries = 3,
// For long-running bots, retry indefinitely
UnlimitedRateLimitRetries = true
};The client respects Retry-After headers from Lichess.
Process events as they arrive instead of collecting them:
// ❌ Bad - collects all events in memory
var allEvents = await client.Bot.StreamEventsAsync().ToListAsync();
// ✅ Good - processes events as they arrive
await foreach (var evt in client.Bot.StreamEventsAsync())
{
ProcessEvent(evt);
}Always dispose clients to close connections:
// Using statement ensures disposal
using var client = new LichessClient(token);
// Or explicit disposal
var client = new LichessClient(token);
try
{
// Use client
}
finally
{
client.Dispose();
}Here's a complete bot that handles all event types:
using LichessSharp;
using LichessSharp.Api.Contracts;
using System.Collections.Concurrent;
var token = Environment.GetEnvironmentVariable("LICHESS_BOT_TOKEN")
?? throw new InvalidOperationException("Set LICHESS_BOT_TOKEN");
using var client = new LichessClient(token);
using var cts = new CancellationTokenSource();
Console.CancelKeyPress += (_, e) => { e.Cancel = true; cts.Cancel(); };
var activeGames = new ConcurrentDictionary<string, CancellationTokenSource>();
Console.WriteLine("Bot starting...");
try
{
await foreach (var evt in client.Bot.StreamEventsAsync(cts.Token))
{
switch (evt.Type)
{
case "challenge":
await HandleChallengeAsync(evt.Challenge!);
break;
case "gameStart":
StartGame(evt.Game!.GameId!);
break;
case "gameFinish":
StopGame(evt.Game!.GameId!);
break;
}
}
}
catch (OperationCanceledException)
{
Console.WriteLine("Bot shutting down...");
}
// Wait for active games to finish
foreach (var gameCts in activeGames.Values)
{
gameCts.Cancel();
}
async Task HandleChallengeAsync(ChallengeJson challenge)
{
// Accept blitz and rapid only
if (challenge.Speed is "blitz" or "rapid")
{
await client.Challenges.AcceptAsync(challenge.Id);
Console.WriteLine($"Accepted challenge from {challenge.Challenger?.Name}");
}
else
{
await client.Challenges.DeclineAsync(challenge.Id, ChallengeDeclineReason.TimeControl);
Console.WriteLine($"Declined challenge from {challenge.Challenger?.Name}");
}
}
void StartGame(string gameId)
{
var gameCts = CancellationTokenSource.CreateLinkedTokenSource(cts.Token);
activeGames[gameId] = gameCts;
_ = Task.Run(async () =>
{
try
{
await PlayGameAsync(gameId, gameCts.Token);
}
catch (Exception ex)
{
Console.WriteLine($"Game {gameId} error: {ex.Message}");
}
finally
{
activeGames.TryRemove(gameId, out _);
}
});
}
void StopGame(string gameId)
{
if (activeGames.TryRemove(gameId, out var gameCts))
{
gameCts.Cancel();
}
}
async Task PlayGameAsync(string gameId, CancellationToken ct)
{
Console.WriteLine($"Playing game {gameId}");
string? myColor = null;
await foreach (var evt in client.Bot.StreamGameAsync(gameId, ct))
{
switch (evt)
{
case BotGameFullEvent full:
var me = await client.Account.GetProfileAsync(ct);
myColor = full.White?.Id == me.Id?.ToLower() ? "white" : "black";
Console.WriteLine($"Game {gameId}: Playing as {myColor}");
// Say hello
await client.Bot.WriteChatAsync(gameId, ChatRoom.Player, "glhf!", ct);
// Make first move if white
if (myColor == "white" && full.State?.Moves == "")
{
await client.Bot.MakeMoveAsync(gameId, "e2e4", cancellationToken: ct);
}
break;
case BotGameStateEvent state:
if (state.Status != "started") break;
var moveCount = string.IsNullOrEmpty(state.Moves) ? 0 : state.Moves.Split(' ').Length;
var isMyTurn = (myColor == "white") == (moveCount % 2 == 0);
if (isMyTurn)
{
// Your engine logic here
var move = GetBestMove(state.Moves);
await client.Bot.MakeMoveAsync(gameId, move, cancellationToken: ct);
}
break;
case BotOpponentGoneEvent gone when gone.ClaimWinInSeconds > 0:
await Task.Delay(TimeSpan.FromSeconds(gone.ClaimWinInSeconds.Value + 1), ct);
await client.Bot.ClaimVictoryAsync(gameId, ct);
break;
}
}
Console.WriteLine($"Game {gameId} finished");
}
string GetBestMove(string? moves)
{
// Placeholder - integrate your chess engine here
return "d2d4"; // Always play d4
}