Skip to content

Streaming Events

dblike edited this page Jan 20, 2026 · 1 revision

Streaming Events Guide

LichessSharp uses IAsyncEnumerable<T> for all streaming endpoints. This guide covers patterns for handling streaming events effectively.


Basic Pattern

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}");
}

Polymorphic Event Handling

Many streaming endpoints return polymorphic events with a type discriminator. Use pattern matching to handle each event type:

Bot/Board Game Events

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;
    }
}

Filtering by Type

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);
}

Concurrent Streams

Multiple Games

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);

Event Multiplexing

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}");
    }
});

Error Handling

Reconnection Pattern

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;
        }
    }
}

Graceful Shutdown

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");
}

Timeouts

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);

Rate Limiting

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.


Memory Management

Avoid Unbounded Buffering

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);
}

Dispose Clients

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();
}

Complete Bot Example

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
}

See Also

Clone this wiki locally