using System.Diagnostics; using Iceshrimp.Shared.Helpers; using Iceshrimp.Shared.Schemas.Web; using Microsoft.JSInterop; namespace Iceshrimp.Frontend.Core.Services; internal class UpdateService { private readonly ApiService _api; private readonly ILogger _logger; private readonly Lazy> _moduleTask; private UpdateStates _updateState; public EventHandler? UpdateStatusEvent { get; set; } public UpdateStates UpdateState { get => _updateState; private set { UpdateStatusEvent?.Invoke(this, value); _logger.LogInformation($"Invoked Update Status Event: {value}"); _updateState = value; } } private VersionInfo FrontendVersion { get; } = VersionHelpers.VersionInfo.Value; public VersionResponse? BackendVersion { get; private set; } // ReSharper disable once UnusedAutoPropertyAccessor.Local private Timer CheckUpdateTimer { get; set; } private Timer? CheckWaitingTimer { get; set; } public UpdateService( ApiService api, ILogger logger, IJSRuntime js ) { _api = api; _logger = logger; _moduleTask = new Lazy>(() => js.InvokeAsync( "import", "./Core/Services/UpdateService.cs.js") .AsTask()); CheckUpdateTimer = new Timer(UpdateCheckCallback, null, TimeSpan.Zero, TimeSpan.FromSeconds(60)); _ = RegisterSwUpdateCallbackAsync(); } private async Task RegisterSwUpdateCallbackAsync() { var module = await _moduleTask.Value; var objRef = DotNetObjectReference.Create(this); await module.InvokeAsync("RegisterSWUpdateCallback", objRef); } [JSInvokable] public void NewServiceWorker() { UpdateState = UpdateStates.UpdateInstalling; CheckWaitingTimer = new Timer(CheckWaitingCallback, null, TimeSpan.Zero, TimeSpan.FromSeconds(2)); } private async Task ServiceWorkerCheckWaitingAsync() { var res = await ServiceWorkerCheckStateAsync(); if (res == ServiceWorkerState.Waiting) { CheckWaitingTimer?.Change(Timeout.Infinite, Timeout.Infinite); CheckWaitingTimer?.Dispose(); UpdateState = UpdateStates.UpdateInstalled; } } private void UpdateCheckCallback(object? caller) { _ = CheckVersionAsync(); } private void CheckWaitingCallback(object? caller) { _ = ServiceWorkerCheckWaitingAsync(); } public async Task ServiceWorkerUpdateAsync() { var module = await _moduleTask.Value; var res = await module.InvokeAsync("ServiceWorkerUpdate"); if (res is null) return; UpdateState = res switch { "installing" => UpdateStates.UpdateInstalling, "waiting" => UpdateStates.UpdateInstalled, "active" => UpdateStates.NoUpdate, null => UpdateStates.Error, _ => throw new UnreachableException() }; } private async Task ServiceWorkerCheckStateAsync() { var module = await _moduleTask.Value; var res = await module.InvokeAsync("ServiceWorkerCheckRegistration"); _logger.LogTrace($"aService worker state: {res}"); return res switch { "installing" => ServiceWorkerState.Installing, "waiting" => ServiceWorkerState.Waiting, "active" => ServiceWorkerState.Active, null => ServiceWorkerState.Error, _ => throw new UnreachableException() }; } public async Task ServiceWorkerSkipWaitingAsync() { var module = await _moduleTask.Value; var res = await module.InvokeAsync("ServiceWorkerSkipWaiting"); if (res is null) throw new Exception("Error occured while updating service worker."); return (bool)res; } private async Task GetVersionAsync() { try { var backendVersion = await _api.Version.GetVersionAsync(); _logger.LogInformation("Successfully fetched backend version."); return backendVersion; } catch (Exception e) { _logger.LogError(e, "Failed to fetch backend version."); return null; } } private async Task CheckVersionAsync() { var version = await GetVersionAsync(); if (version is null) return; BackendVersion = version; if (version.Version != FrontendVersion.Version) { UpdateState = UpdateStates.UpdateAvailable; await ServiceWorkerUpdateAsync(); } } internal enum UpdateStates { NoUpdate, UpdateAvailable, UpdateInstalling, UpdateInstalled, Error } private enum ServiceWorkerState { Installing, Waiting, Active, Error } }