{"path":"NexusReader.UI.Shared/Services/SyncService.cs","purpose":"Client-side service that establishes a SignalR hub connection to synchronize reading progress (pages) with a remote server, providing debounce logic, event exposure, and lifecycle disposal.","classification":{"role":"service","layer":"frontend","confidence":0.9,"evidence":["Service naming pattern","Application/service path heuristic","Namespace NexusReader.UI.Shared.Services and class name SyncService","Creates and manages Microsoft.AspNetCore.SignalR.Client.HubConnection to 'synchub'","Exposes progress update event and implements IAsyncDisposable"]},"className":"SyncService","methods":[{"name":"SyncService","line":20,"endLine":30,"signature":"(httpClient: HttpClient, storageService: INativeStorageService, platformService: IPlatformService, logger: ILogger) -> SyncService","purpose":"Constructs the SyncService and stores injected dependencies.","calls":[],"actions":[{"id":"assignment_26","kind":"mapping","label":"store-dependencies","line":26,"detail":"Assigns HttpClient, INativeStorageService, IPlatformService, ILogger to private fields","visibility":"detail-only","confidence":0.7}]},{"name":"InitializeAsync","line":32,"endLine":66,"signature":"() -> Task","purpose":"Initializes the SignalR HubConnection (with token from secure storage), registers a progress handler, and starts the connection with automatic reconnect.","calls":[{"targetFile":"unknown","targetMethod":"GetSecureString","callLine":36,"paramSummary":"\"nexus_auth_token\""},{"targetFile":"self","targetMethod":"OnProgressReceived","callLine":53,"paramSummary":"pageId, timestamp (invoked when hub event arrives)"}],"actions":[{"id":"guard-clause_34","kind":"guard-clause","label":"already-initialized","line":34,"detail":"If already initialized, return Result.Ok()","conditionSummary":"_isInitialized == true","outcomeLabels":["return-Ok","skip-init"],"visibility":"detail-only","confidence":0.7},{"id":"initializeasync_guard-clause_34_0","kind":"guard-clause","label":"Guards early exit or rejection path","line":34,"detail":"if (_isInitialized) return Result.Ok();","conditionSummary":"_isInitialized) return Result.Ok(","outcomeLabels":["exit","continue"],"visibility":"primary-visible","confidence":0.9},{"id":"external-read_36","kind":"mapping","label":"read-secure-token","line":36,"detail":"Retrieves auth token from INativeStorageService; branches on failure","visibility":"detail-only","confidence":0.7},{"id":"initializeasync_await_36_1","kind":"await","label":"Waits for async work","line":36,"detail":"var tokenResult = await _storageService.GetSecureString(\"nexus_auth_token\");","visibility":"secondary-visible","confidence":0.81},{"id":"initializeasync_guard-clause_37_2","kind":"guard-clause","label":"Guards early exit or rejection path","line":37,"detail":"if (tokenResult.IsFailed) return Result.Fail(\"Not authenticated\");","conditionSummary":"tokenResult.IsFailed) return Result.Fail(\"Not authenticated\"","outcomeLabels":["exit","continue"],"visibility":"primary-visible","confidence":0.9},{"id":"guard-clause_37","kind":"guard-clause","label":"not-authenticated","line":37,"detail":"Returns Result.Fail(\"Not authenticated\") when secure token not present","conditionSummary":"tokenResult.IsFailed","outcomeLabels":["return-Fail","stop-init"],"visibility":"detail-only","confidence":0.7},{"id":"compute_39","kind":"mapping","label":"build-hub-url","line":39,"detail":"Combines HttpClient.BaseAddress with 'synchub' to form hubUrl","visibility":"detail-only","confidence":0.7},{"id":"configure_42","kind":"mapping","label":"configure-hub-connection","line":42,"detail":"Creates HubConnectionBuilder, sets AccessTokenProvider to return the retrieved token and enables automatic reconnect","visibility":"detail-only","confidence":0.7},{"id":"event-subscription_50","kind":"mapping","label":"register-progress-handler","line":50,"detail":"Registers 'ProgressUpdated' handler that forwards to OnProgressReceived event if not null","visibility":"detail-only","confidence":0.7},{"id":"initializeasync_branch_53_3","kind":"branch","label":"Evaluates branch condition","line":53,"detail":"if (OnProgressReceived != null) await OnProgressReceived(pageId, timestamp);","conditionSummary":"OnProgressReceived != null) await OnProgressReceived(pageId, timestamp","outcomeLabels":["true","false"],"visibility":"secondary-visible","confidence":0.78},{"id":"initializeasync_await_53_4","kind":"await","label":"Waits for async work","line":53,"detail":"if (OnProgressReceived != null) await OnProgressReceived(pageId, timestamp);","visibility":"secondary-visible","confidence":0.81},{"id":"initializeasync_try_56_5","kind":"try","label":"Begins protected execution","line":56,"detail":"try","visibility":"primary-visible","confidence":0.84},{"id":"try-catch_56","kind":"mapping","label":"start-hub-with-fallback","line":56,"detail":"Attempts StartAsync(); on success sets _isInitialized=true and returns Ok; on exception returns Fail(ex.Message)","visibility":"detail-only","confidence":0.7},{"id":"initializeasync_await_58_6","kind":"await","label":"Waits for async work","line":58,"detail":"await _hubConnection.StartAsync();","visibility":"secondary-visible","confidence":0.81},{"id":"initializeasync_return_60_7","kind":"return","label":"Returns result","line":60,"detail":"return Result.Ok();","visibility":"detail-only","confidence":0.7},{"id":"initializeasync_catch_62_8","kind":"catch","label":"Handles exception path","line":62,"detail":"catch (Exception ex)","conditionSummary":"Exception ex","outcomeLabels":["handled exception"],"visibility":"primary-visible","confidence":0.86},{"id":"initializeasync_return_64_9","kind":"return","label":"Returns result","line":64,"detail":"return Result.Fail(ex.Message);","visibility":"detail-only","confidence":0.7}]},{"name":"UpdateProgressAsync","line":70,"endLine":101,"signature":"(pageId: string, ebookId: Guid, progress: double, chapterTitle: string?, chapterIndex: int) -> Task","purpose":"Debounces and sends reading progress updates to the server via the hub connection in a trailing-edge manner, avoiding duplicate sends for the same page.","calls":[{"targetFile":"self","targetMethod":"InitializeAsync","callLine":85,"paramSummary":"none (ensures hub is initialized before sending)"}],"actions":[{"id":"updateprogressasync_guard-clause_72_0","kind":"guard-clause","label":"Guards early exit or rejection path","line":72,"detail":"if (pageId == _lastSentPageId) return Result.Ok();","conditionSummary":"pageId == _lastSentPageId) return Result.Ok(","outcomeLabels":["exit","continue"],"visibility":"primary-visible","confidence":0.9},{"id":"guard-clause_72","kind":"guard-clause","label":"suppress-duplicate-page","line":72,"detail":"Avoids sending if the same page was recently sent","conditionSummary":"pageId == _lastSentPageId","outcomeLabels":["return-Ok","skip-send"],"visibility":"detail-only","confidence":0.7},{"id":"debounce_74","kind":"mapping","label":"trailing-edge-debounce","line":74,"detail":"Cancels prior CancellationTokenSource, creates a new one, and schedules a background task to delay 2000ms before sending","visibility":"detail-only","confidence":0.7},{"id":"background-task_79","kind":"mapping","label":"delayed-send-task","line":79,"detail":"Runs Task.Run that delays with token, initializes hub if needed, checks connection state and sends progress","visibility":"detail-only","confidence":0.7},{"id":"updateprogressasync_try_81_1","kind":"try","label":"Begins protected execution","line":81,"detail":"try","visibility":"primary-visible","confidence":0.84},{"id":"updateprogressasync_await_83_2","kind":"await","label":"Waits for async work","line":83,"detail":"await Task.Delay(2000, token);","visibility":"secondary-visible","confidence":0.81},{"id":"updateprogressasync_branch_85_3","kind":"branch","label":"Evaluates branch condition","line":85,"detail":"if (!_isInitialized) await InitializeAsync();","conditionSummary":"!_isInitialized) await InitializeAsync(","outcomeLabels":["true","false"],"visibility":"secondary-visible","confidence":0.78},{"id":"updateprogressasync_await_85_4","kind":"await","label":"Waits for async work","line":85,"detail":"if (!_isInitialized) await InitializeAsync();","visibility":"secondary-visible","confidence":0.81},{"id":"updateprogressasync_branch_87_5","kind":"branch","label":"Evaluates branch condition","line":87,"detail":"if (_hubConnection?.State == HubConnectionState.Connected)","conditionSummary":"_hubConnection?.State == HubConnectionState.Connected","outcomeLabels":["true","false"],"visibility":"secondary-visible","confidence":0.78},{"id":"updateprogressasync_external-call_89_6","kind":"external-call","label":"Calls external dependency","line":89,"detail":"await _hubConnection.SendAsync(\"UpdateProgress\", pageId, ebookId, progress, chapterTitle, token);","visibility":"secondary-visible","confidence":0.82},{"id":"external-call_89","kind":"external-call","label":"send-progress-to-hub","line":89,"detail":"Calls _hubConnection.SendAsync('UpdateProgress', pageId, ebookId, progress, chapterTitle, token) when connected (external SignalR call)","visibility":"detail-only","confidence":0.7},{"id":"updateprogressasync_await_89_7","kind":"await","label":"Waits for async work","line":89,"detail":"await _hubConnection.SendAsync(\"UpdateProgress\", pageId, ebookId, progress, chapterTitle, token);","visibility":"secondary-visible","confidence":0.81},{"id":"state-change_90","kind":"mapping","label":"update-last-sent","line":90,"detail":"Sets _lastSentPageId = pageId after successful send","visibility":"detail-only","confidence":0.7},{"id":"error-handling_93","kind":"mapping","label":"handle-cancellation","line":93,"detail":"Catches TaskCanceledException and ignores (user kept scrolling)","visibility":"detail-only","confidence":0.7},{"id":"updateprogressasync_catch_93_8","kind":"catch","label":"Handles exception path","line":93,"detail":"catch (TaskCanceledException) { /* Ignored, user kept scrolling */ }","conditionSummary":"TaskCanceledException","outcomeLabels":["handled exception"],"visibility":"primary-visible","confidence":0.86},{"id":"updateprogressasync_catch_94_9","kind":"catch","label":"Handles exception path","line":94,"detail":"catch (Exception ex)","conditionSummary":"Exception ex","outcomeLabels":["handled exception"],"visibility":"primary-visible","confidence":0.86},{"id":"error-handling_94","kind":"mapping","label":"log-send-error","line":94,"detail":"Catches other exceptions and logs via ILogger","visibility":"detail-only","confidence":0.7},{"id":"updateprogressasync_log_96_10","kind":"log","label":"Logs runtime state","line":96,"detail":"_logger.LogError(ex, \"[SyncService] Error sending reading progress for page {PageId}.\", pageId);","visibility":"secondary-visible","confidence":0.92},{"id":"updateprogressasync_return_100_11","kind":"return","label":"Returns result","line":100,"detail":"return Result.Ok();","visibility":"detail-only","confidence":0.7}]},{"name":"DisposeAsync","line":103,"endLine":110,"signature":"() -> Task","purpose":"Cancels pending debounce, disposes the hub connection if present.","calls":[],"actions":[{"id":"cancel_105","kind":"mapping","label":"cancel-debounce","line":105,"detail":"Cancels the active CancellationTokenSource to stop pending background send","visibility":"detail-only","confidence":0.7},{"id":"conditional-dispose_106","kind":"mapping","label":"dispose-hub","line":106,"detail":"If _hubConnection != null then awaits _hubConnection.DisposeAsync() (external)","visibility":"detail-only","confidence":0.7},{"id":"disposeasync_branch_106_0","kind":"branch","label":"Evaluates branch condition","line":106,"detail":"if (_hubConnection != null)","conditionSummary":"_hubConnection != null","outcomeLabels":["true","false"],"visibility":"secondary-visible","confidence":0.78},{"id":"disposeasync_await_108_1","kind":"await","label":"Waits for async work","line":108,"detail":"await _hubConnection.DisposeAsync();","visibility":"secondary-visible","confidence":0.81}]},{"name":"IAsyncDisposable.DisposeAsync","line":112,"endLine":115,"signature":"() -> ValueTask","purpose":"Explicit interface implementation that forwards to DisposeAsync to unify disposal semantics.","calls":[{"targetFile":"self","targetMethod":"DisposeAsync","callLine":114,"paramSummary":"none"}],"actions":[{"id":"forwarding_114","kind":"mapping","label":"forward-dispose","line":114,"detail":"Calls the class DisposeAsync and awaits it","visibility":"detail-only","confidence":0.7},{"id":"iasyncdisposable_disposeasync_await_114_0","kind":"await","label":"Waits for async work","line":114,"detail":"await DisposeAsync();","visibility":"secondary-visible","confidence":0.81}]}],"types":[{"name":"SyncService","kind":"model","line":8,"purpose":"Encapsulates client synchronization logic for reading progress via SignalR, providing initialization, debounced update sending, event exposure, and disposal.","fields":[{"name":"_httpClient","type":"HttpClient","required":true,"line":10,"description":"Used to derive base URL for hub connection"},{"name":"_storageService","type":"INativeStorageService","required":true,"line":11,"description":"Used to read secure auth token"},{"name":"_platformService","type":"IPlatformService","required":true,"line":12,"description":"Platform abstractions (injected but not used in this file)"},{"name":"_logger","type":"ILogger","required":true,"line":13,"description":"Logs errors and diagnostics"},{"name":"_hubConnection","type":"HubConnection?","required":false,"line":14,"description":"SignalR hub connection instance"},{"name":"_isInitialized","type":"bool","required":true,"line":15,"description":"Tracks whether the hub has been successfully started"},{"name":"_debounceCts","type":"CancellationTokenSource?","required":false,"line":16,"description":"Cancellation token source used for debouncing background sends"},{"name":"OnProgressReceived","type":"Func?","required":false,"line":18,"description":"Event exposed to consumers when the hub reports ProgressUpdated (pageId, timestamp)"},{"name":"_lastSentPageId","type":"string?","required":false,"line":68,"description":"Tracks last pageId successfully sent to avoid duplicate sends"}]}],"serviceRegistrations":[],"startupActions":[],"dependencies":["NexusReader.Application.Abstractions.Services","Microsoft.AspNetCore.SignalR.Client","Microsoft.Extensions.Logging"],"patterns":["Debounce (trailing-edge)","SignalR Hub client","Event-forwarding"],"domainConcepts":["ReadingProgress","Ebook","Page"],"keyDetails":"Uses a trailing-edge 2s debounce to batch progress updates, retrieves an auth token from secure storage for SignalR AccessTokenProvider, registers a hub handler that forwards to an event, and safely cancels/disposes background work and hub connection on dispose.","orchestrationMethods":[{"name":"UpdateProgressAsync","line":70,"confidence":0.98,"reason":"Contains 4 architectural actions relevant to business execution.","actionKinds":["guard-clause","mapping","try","await","branch","external-call","catch","log","return"],"evidencePaths":["NexusReader.UI.Shared/Services/SyncService.cs","self"]},{"name":"InitializeAsync","line":32,"confidence":0.73,"reason":"Coordinates 2 downstream calls with 1 architectural actions.","actionKinds":["guard-clause","mapping","await","branch","try","return","catch"],"evidencePaths":["NexusReader.UI.Shared/Services/SyncService.cs","unknown","self"]},{"name":"DisposeAsync","line":103,"confidence":0.57,"reason":"Contains 1 architectural actions relevant to business execution.","actionKinds":["mapping","branch","await"],"evidencePaths":["NexusReader.UI.Shared/Services/SyncService.cs"]}],"typedContracts":[{"name":"SyncService","kind":"model","line":8,"fieldCount":9,"evidencePaths":["NexusReader.UI.Shared/Services/SyncService.cs"]}],"persistenceInteractions":[],"externalInteractions":[{"methodName":"UpdateProgressAsync","line":89,"kind":"external-call","detail":"await _hubConnection.SendAsync(\"UpdateProgress\", pageId, ebookId, progress, chapterTitle, token);","evidencePaths":["NexusReader.UI.Shared/Services/SyncService.cs"]},{"methodName":"UpdateProgressAsync","line":89,"kind":"external-call","detail":"Calls _hubConnection.SendAsync('UpdateProgress', pageId, ebookId, progress, chapterTitle, token) when connected (external SignalR call)","evidencePaths":["NexusReader.UI.Shared/Services/SyncService.cs"]}],"evidenceAnchors":[{"kind":"orchestration-method","label":"UpdateProgressAsync","line":70,"summary":"Contains 4 architectural actions relevant to business execution.","confidence":0.98,"evidencePaths":["NexusReader.UI.Shared/Services/SyncService.cs","self"]},{"kind":"orchestration-method","label":"InitializeAsync","line":32,"summary":"Coordinates 2 downstream calls with 1 architectural actions.","confidence":0.73,"evidencePaths":["NexusReader.UI.Shared/Services/SyncService.cs","unknown","self"]},{"kind":"orchestration-method","label":"DisposeAsync","line":103,"summary":"Contains 1 architectural actions relevant to business execution.","confidence":0.57,"evidencePaths":["NexusReader.UI.Shared/Services/SyncService.cs"]},{"kind":"typed-contract","label":"SyncService","line":8,"summary":"model with 9 fields.","confidence":0.8,"evidencePaths":["NexusReader.UI.Shared/Services/SyncService.cs"]},{"kind":"external-call","label":"UpdateProgressAsync","line":89,"summary":"await _hubConnection.SendAsync(\"UpdateProgress\", pageId, ebookId, progress, chapterTitle, token);","confidence":0.8,"evidencePaths":["NexusReader.UI.Shared/Services/SyncService.cs"]},{"kind":"external-call","label":"UpdateProgressAsync","line":89,"summary":"Calls _hubConnection.SendAsync('UpdateProgress', pageId, ebookId, progress, chapterTitle, token) when connected (external SignalR call)","confidence":0.8,"evidencePaths":["NexusReader.UI.Shared/Services/SyncService.cs"]}],"cacheMetadata":{"schemaVersion":2,"analysisVersion":"2026-05-23.cache-v1","contentChecksum":"0cf17b27709b5036963d0cf318e15e471a1f36addfc7a36a40ad21a66cef6de8","sourceByteSize":3843,"analyzedAt":"2026-05-23T16:23:17.200Z","technology":"dotnet"}}