using Microsoft.AspNetCore.SignalR;
namespace Extrudex.API.Hubs;
///
/// SignalR hub for real-time printer status updates.
///
/// Clients connect to this hub to receive push notifications when
/// a printer's status changes (e.g., Idle → Printing, Offline → Idle)
/// or when a heartbeat confirms the printer is still reachable.
///
/// Usage flow:
///
/// - Client connects to /hubs/printer
/// - Client calls with a printer ID
/// - Server adds the connection to a SignalR group named after the printer ID
/// - When the backend detects a status change, it calls
///
/// which broadcasts to all subscribers of that printer
///
///
/// Group naming: printer:{printerId} (lowercase GUID).
///
/// Typed client: — all server-to-client
/// calls go through this interface for compile-time safety.
///
public class PrinterHub : Hub
{
///
/// Adds the calling connection to the SignalR group for a specific printer.
/// Once joined, the client will receive all status updates and heartbeats
/// for that printer until it disconnects or calls .
///
///
/// The unique identifier of the printer to subscribe to.
/// The GUID is normalized to lowercase for consistent group naming.
///
///
/// Thrown if cannot be parsed as a valid GUID.
///
public async Task JoinPrinterGroup(Guid printerId)
{
var groupName = PrinterGroupName(printerId);
await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
}
///
/// Removes the calling connection from the SignalR group for a specific printer.
/// After leaving, the client will no longer receive updates for that printer.
///
///
/// The unique identifier of the printer to unsubscribe from.
///
public async Task LeavePrinterGroup(Guid printerId)
{
var groupName = PrinterGroupName(printerId);
await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName);
}
///
/// Overrides to perform cleanup.
/// SignalR automatically removes disconnected connections from all groups,
/// so no manual cleanup is required here.
///
/// Exception that caused the disconnection, if any.
public override Task OnDisconnectedAsync(Exception? exception)
{
// SignalR automatically removes the connection from all groups on disconnect.
// No manual cleanup needed.
return base.OnDisconnectedAsync(exception);
}
///
/// Returns the SignalR group name for a given printer ID.
/// Format: printer:{printerId} (lowercase to avoid case-sensitivity issues).
///
/// The unique identifier of the printer.
/// A consistent, lowercase group name string.
internal static string PrinterGroupName(Guid printerId) =>
$"printer:{printerId.ToString().ToLowerInvariant()}";
}
///
/// Extension methods for pushing real-time printer updates through
/// the of .
///
/// These methods are intended to be called from background services
/// (e.g., MQTT message handlers, Moonraker pollers) or other
/// server-side code that detects a printer state change.
///
public static class PrinterHubExtensions
{
///
/// Pushes a printer status change to all clients subscribed to
/// the given printer's SignalR group.
///
/// Call this from any background service or controller when a printer's
/// operational status changes (e.g., a Bambu Lab MQTT message reports
/// the printer started printing, or a Moonraker poller detects an error).
///
/// The hub context injected via DI.
/// The unique identifier of the printer that changed.
/// The new status string (e.g., "Idle", "Printing", "Offline").
/// Timestamp (UTC) of when the status was observed, or null if unknown.
/// A Task that completes when the message has been sent to all group members.
public static async Task PushPrinterStatusAsync(
this IHubContext hubContext,
Guid printerId,
string status,
DateTime? lastSeenAt = null)
{
var groupName = PrinterHub.PrinterGroupName(printerId);
await hubContext.Clients.Group(groupName)
.PrinterStatusChanged(printerId, status, lastSeenAt);
}
///
/// Pushes a heartbeat signal to all clients subscribed to the given
/// printer's SignalR group. Use this for lightweight "still alive"
/// notifications that don't require a full status payload.
///
/// The hub context injected via DI.
/// The unique identifier of the printer.
/// Whether the printer is currently active and accepting jobs.
/// Timestamp (UTC) of the last telemetry from the printer, or null.
/// A Task that completes when the message has been sent to all group members.
public static async Task PushPrinterHeartbeatAsync(
this IHubContext hubContext,
Guid printerId,
bool isActive,
DateTime? lastSeenAt = null)
{
var groupName = PrinterHub.PrinterGroupName(printerId);
await hubContext.Clients.Group(groupName)
.PrinterHeartbeat(printerId, isActive, lastSeenAt);
}
}