SignalR Real-time API
Overview
PhotoFlow uses SignalR for real-time communication between the backend and frontend clients. This enables instant notifications for sync progress, state changes, and collaborative features.
Connection
Hub URL
Environment | URL |
|---|---|
Development |
|
UAT |
|
Production |
|
Connection Setup (Angular)
import { HubConnectionBuilder, HttpTransportType } from '@microsoft/signalr';
const connection = new HubConnectionBuilder()
.withUrl(environment.signalR.hubUrl, {
accessTokenFactory: () => this.authService.accessToken,
transport: HttpTransportType.WebSockets |
HttpTransportType.ServerSentEvents |
HttpTransportType.LongPolling,
withCredentials: true,
})
.withAutomaticReconnect()
.build();
await connection.start();
Events
Photo Sync Events
PhotoSyncProgress
Fired during photo synchronization to report progress.
interface PhotoSyncProgressEvent {
albumId: string;
albumTagId: string;
processedCount: number;
totalCount: number;
albumTagType: string;
estimatedSecondsRemaining?: number;
timestamp: string;
type: 'photo_sync_progress';
}
Usage:
connection.on('PhotoSyncProgress', (event: PhotoSyncProgressEvent) => {
console.log(`Syncing: ${event.processedCount}/${event.totalCount}`);
this.updateProgressBar(event.processedCount / event.totalCount * 100);
});
PhotoSyncCompleted
Fired when photo synchronization completes.
interface PhotoSyncCompletedEvent {
albumId: string;
albumTagId: string;
processedCount: number;
totalCount: number;
albumTagType: string;
message: string;
timestamp: string;
type: 'photo_sync_completed';
}
Album State Events
AlbumStateChanged
Fired when an album's state machine transitions.
interface AlbumStateChangedEvent {
albumId: string;
oldState: string;
newState: string;
message: string;
timestamp: string;
type: 'album_state_changed';
}
Usage:
connection.on('AlbumStateChanged', (event: AlbumStateChangedEvent) => {
this.toastService.success(`Album moved to ${event.newState}`);
this.refreshTransitions();
});
Album Tag Events
AlbumTagCreated
Fired when a new album tag is created.
interface AlbumTagCreatedEvent {
albumId: string;
albumTagId: string;
message: string;
timestamp: string;
type: 'album_tag_created';
}
AlbumTagStatusChanged
Fired when an album tag's status changes.
interface AlbumTagStatusChangedEvent {
albumId: string;
albumTagId: string;
oldStatus: string;
newStatus: string;
message: string;
timestamp: string;
type: 'tag_status_changed';
}
Sync Control Events
AlbumSyncStarted
interface AlbumSyncStartedEvent {
albumId: string;
message: string;
timestamp: string;
type: 'sync_started';
}
AlbumSyncCompleted
interface AlbumSyncCompletedEvent {
albumId: string;
albumTagId?: string;
message: string;
timestamp: string;
type: 'sync_completed';
}
AlbumSyncError
interface AlbumSyncErrorEvent {
albumId: string;
message: string;
timestamp: string;
type: 'sync_error';
}
Backend Implementation
NotificationHub
[Authorize]
public class NotificationHub : Hub
{
public async Task JoinAlbumGroup(string albumId)
{
await Groups.AddToGroupAsync(Context.ConnectionId, $"album_{albumId}");
}
public async Task LeaveAlbumGroup(string albumId)
{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, $"album_{albumId}");
}
}
NotificationService
public class NotificationService : INotificationService
{
private readonly IHubContext<NotificationHub> _hubContext;
public async Task SendPhotoSyncProgressAsync(
Guid albumId,
Guid albumTagId,
int processedCount,
int totalCount,
string albumTagType)
{
var message = new PhotoSyncProgressEvent
{
AlbumId = albumId.ToString(),
AlbumTagId = albumTagId.ToString(),
ProcessedCount = processedCount,
TotalCount = totalCount,
AlbumTagType = albumTagType,
Timestamp = DateTime.UtcNow.ToString("O"),
Type = "photo_sync_progress"
};
await _hubContext.Clients
.Group($"album_{albumId}")
.SendAsync("PhotoSyncProgress", message);
}
public async Task SendAlbumStateChangedAsync(
Guid albumId,
string oldState,
string newState,
string message,
string userId)
{
var notification = new AlbumStateChangedEvent
{
AlbumId = albumId.ToString(),
OldState = oldState,
NewState = newState,
Message = message,
Timestamp = DateTime.UtcNow.ToString("O"),
Type = "album_state_changed"
};
await _hubContext.Clients
.Group($"album_{albumId}")
.SendAsync("AlbumStateChanged", notification);
}
}
Frontend Integration
SignalR Service (Angular)
@Injectable({ providedIn: 'root' })
export class SignalRService implements OnDestroy {
private hubConnection: HubConnection | null = null;
private photoSyncProgress$ = new Subject<PhotoSyncProgressEvent>();
private albumStateChanged$ = new Subject<AlbumStateChangedEvent>();
getPhotoSyncProgress$(): Observable<PhotoSyncProgressEvent> {
return this.photoSyncProgress$.asObservable();
}
getAlbumStateChanged$(): Observable<AlbumStateChangedEvent> {
return this.albumStateChanged$.asObservable();
}
async joinAlbumGroup(albumId: string): Promise<void> {
await this.hubConnection?.invoke('JoinAlbumGroup', albumId);
}
async leaveAlbumGroup(albumId: string): Promise<void> {
await this.hubConnection?.invoke('LeaveAlbumGroup', albumId);
}
}
Component Usage
@Component({
selector: 'app-album-detail',
template: `...`
})
export class AlbumDetailComponent implements OnInit, OnDestroy {
private unsubscribe$ = new Subject<void>();
constructor(private signalRService: SignalRService) {}
ngOnInit(): void {
// Join album group
this.signalRService.joinAlbumGroup(this.albumId);
// Subscribe to events
this.signalRService.getAlbumStateChanged$()
.pipe(
takeUntil(this.unsubscribe$),
filter(event => event.albumId === this.albumId)
)
.subscribe(event => {
this.handleStateChange(event);
});
}
ngOnDestroy(): void {
this.signalRService.leaveAlbumGroup(this.albumId);
this.unsubscribe$.next();
this.unsubscribe$.complete();
}
}
Connection Management
Automatic Reconnection
.withAutomaticReconnect({
nextRetryDelayInMilliseconds: (retryContext) => {
if (retryContext.previousRetryCount < 5) {
return 5000; // Retry every 5 seconds
}
return null; // Stop reconnecting
}
})
Connection States
connection.onclose((error) => {
console.log('Connection closed', error);
this.connectionState$.next(HubConnectionState.Disconnected);
});
connection.onreconnecting((error) => {
console.log('Reconnecting...', error);
this.connectionState$.next(HubConnectionState.Reconnecting);
});
connection.onreconnected((connectionId) => {
console.log('Reconnected', connectionId);
this.connectionState$.next(HubConnectionState.Connected);
// Rejoin groups after reconnection
this.rejoinGroups();
});
Related Documentation
Architecture
Frontend Integration
Last modified: 25 February 2026