PhotoFlow Documentation Help

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

wss://dev.api.photoflow.vn/hubs/notification

UAT

wss://uat.api.photoflow.vn/hubs/notification

Production

wss://api.photoflow.vn/hubs/notification

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(); });
Last modified: 25 February 2026