diff --git a/Docs/ASLM/content/docs/ASLM/MauiProgram.md b/Docs/ASLM/content/docs/ASLM/MauiProgram.md index f027641..15a877c 100644 --- a/Docs/ASLM/content/docs/ASLM/MauiProgram.md +++ b/Docs/ASLM/content/docs/ASLM/MauiProgram.md @@ -10,6 +10,7 @@ draft: false --- ## Public methods + #### `public static MauiApp CreateMauiApp()` ### Builder configuration @@ -24,7 +25,7 @@ draft: false ### Singleton services -`AppDataStore`, `DockerService`, `EngineInstaller`, `ModuleEnvironmentResolver`, `ModuleTrustService`, `ModuleInstaller`, `ModuleConsoleStore`, `ProcessSnapshotReader`, `ProcessTracker`, `ModuleThemePayloadBuilder`, `ModuleLocalePayloadBuilder`, `AppLocalizationService`, `ModuleInteropHostState`, `ModuleStartThrottle`, `PortRegistry`, `ModuleRunner`, `ModuleDownloadBridge`, `DownloadStateStore`, `DownloadCatalog`, `DownloadInstaller`, `NotificationCenter`, `OllamaSettingsStore`, `GitHubUpdateClient`, `UpdateManager`, `UpdateScheduler`, `ModuleLaunchCoordinator`, `AslmModuleInteropServer`, `AslmApiServer`, `SettingsService`, `CustomThemesStore`, `ThemeService`. +`AppDataStore`, `DockerService`, `EngineInstaller`, `ModuleEnvironmentResolver`, `ModuleTrustService`, `ModuleInstaller`, `ModuleConsoleStore`, `ProcessSnapshotReader`, `ProcessTracker`, `ModuleThemePayloadBuilder`, `ModuleLocalePayloadBuilder`, `AppLocalizationService`, `ModuleInteropHostState`, `ModuleStartThrottle`, `PortRegistry`, `ModuleRunner`, `ModuleDownloadBridge`, `DownloadStateStore`, `DownloadCatalog`, `DownloadInstaller`, `NotificationCenter`, `OllamaSettingsStore`, `GitHubRateLimitStore`, `GitHubUpdateClient`, `UpdateManager`, `UpdateScheduler`, `ModuleLaunchCoordinator`, `AslmModuleInteropServer`, `AslmApiServer`, `SettingsService`, `CustomThemesStore`, `ThemeService`. ### Transient UI diff --git a/Docs/ASLM/content/docs/ASLM/Models/GitHubRateLimitModels.md b/Docs/ASLM/content/docs/ASLM/Models/GitHubRateLimitModels.md new file mode 100644 index 0000000..7ea606b --- /dev/null +++ b/Docs/ASLM/content/docs/ASLM/Models/GitHubRateLimitModels.md @@ -0,0 +1,126 @@ +--- +title: "GitHubRateLimitModels" +draft: false +--- + +## Classes in `ASLM/Models/GitHubRateLimitModels.cs` + +This file contains models used for storing and persisting GitHub API rate limit data and request records. + +--- + +## Static class `GitHubRequestSources` + +**Purpose:** Known GitHub request source values persisted with each API call record. + +| Constant | Value | +| --- | --- | +| `Auto` | `"auto"` | +| `Manual` | `"manual"` | + +--- + +## Static class `GitHubRequestTypes` + +**Purpose:** Known GitHub request type values persisted with each API call record. + +| Constant | Value | +| --- | --- | +| `Releases` | `"releases"` | +| `Branches` | `"branches"` | +| `Download` | `"download"` | +| `RateLimit` | `"rate_limit"` | + +--- + +## Class `GitHubRateLimitData` + +**Purpose:** Stores persisted GitHub API usage for rate-limit budgeting across restarts. **`public sealed`**. + +**Example Usage:** + +```csharp +var data = new GitHubRateLimitData(); +data.KnownLimit = 60; +data.KnownRemaining = 59; +data.ResetUtc = DateTime.UtcNow.AddHours(1).ToString("o"); +data.Normalize(); +``` + +### Properties + +| Name | Type | Description | +| --- | --- | --- | +| `FileVersion` | `int` | Version of the file format, defaults to `1`. | +| `KnownLimit` | `int` | The most recently known rate limit, defaults to `60`. | +| `KnownRemaining` | `int` | Remaining requests in the current window, defaults to `60`. | +| `ResetUtc` | `string?` | ISO 8601 UTC timestamp of the next window reset. | +| `Requests` | `List` | Historical request records. | + +### Methods + +#### `public void Normalize()` + +**Purpose:** Restores safe defaults and trims request history to the active rate-limit window. Clamps `KnownLimit` and `KnownRemaining`, prunes old requests, and bounds the history size. + +#### `public void PruneRequestsOutsideCurrentWindow()` + +**Purpose:** Removes request records that fall outside the current GitHub rate-limit window (resolved via `ResolveWindowStartUtc`). + +#### `public void TrimRequestHistory()` + +**Purpose:** Keeps the request history bounded to the configured GitHub rate limit (`KnownLimit`) by removing the oldest records. + +#### `public DateTimeOffset ResolveWindowStartUtc()` + +**Purpose:** Returns the UTC timestamp when the active GitHub rate-limit window started. Typically 1 hour before the `ResetUtc` timestamp. + +--- + +## Class `GitHubRequestRecord` + +**Purpose:** Describes one GitHub API request recorded for rate-limit planning. **`public sealed`**. + +**Example Usage:** + +```csharp +var record = new GitHubRequestRecord +{ + Url = "https://api.github.com/repos/test/releases", + Type = GitHubRequestTypes.Releases, + Source = GitHubRequestSources.Auto, + StatusCode = 200 +}; +record.Normalize(); +bool isAuto = record.IsAutoRequest(); +``` + +### Properties + +| Name | Type | Description | +| --- | --- | --- | +| `TimestampUtc` | `string` | ISO 8601 UTC timestamp of the request. | +| `Url` | `string` | URL requested. | +| `Type` | `string` | Request type (e.g., releases, branches, download). | +| `Source` | `string` | Source of the request (e.g., auto, manual). | +| `StatusCode` | `int?` | HTTP status code returned. | + +### Methods + +#### `public void Normalize()` + +**Purpose:** Restores safe defaults after JSON deserialization. Trims strings and ensures valid values for timestamp, type, and source. + +#### `public bool IsWithinWindow(DateTimeOffset windowStartUtc)` + +**Purpose:** Returns whether this record belongs to the active GitHub rate-limit window. + +- **Parameters:** `windowStartUtc` - the start timestamp of the window. + +- **Returns:** `true` if `TimestampUtc` is after or equal to `windowStartUtc`. + +#### `public bool IsAutoRequest()` + +**Purpose:** Returns whether this record was initiated by automatic update checks. + +- **Returns:** `true` if `Source` equals `GitHubRequestSources.Auto`. diff --git a/Docs/ASLM/content/docs/ASLM/Pages/AppShellPage.md b/Docs/ASLM/content/docs/ASLM/Pages/AppShellPage.md index 97fd8c1..66134e2 100644 --- a/Docs/ASLM/content/docs/ASLM/Pages/AppShellPage.md +++ b/Docs/ASLM/content/docs/ASLM/Pages/AppShellPage.md @@ -174,7 +174,7 @@ Root: **`Grid`** `ColumnDefinitions="Auto, *"`. | 6 | `NavigateTo(HomeButton)` | | 7 | Re-apply `ApplyAslmApiNavigationState()`, `ApplyConsoleNavigationState()` | | 8 | `ScheduleEnsureModuleBrowserLeftToRight()` | -| 9 | Fire-and-forget `StartEnabledModulesAsync()`, `CheckStartupUpdatesAsync()` | +| 9 | Fire-and-forget `StartEnabledModulesAsync()` | --- @@ -231,20 +231,6 @@ If not `_shellEventsHooked`, return. Otherwise unsubscribe the same events as ** --- -#### `private async Task CheckStartupUpdatesAsync() - -**Purpose:** Checks ASLM and modules for updates once after the main shell opens. - -| Step | Action | -| --- | --- | -| 1 | `_appData.Data.Updates.Normalize()` | -| 2 | `autoUpdate = Updates.AutoUpdateEnabled`; `publishNotifications = !autoUpdate` | -| 3 | `CheckAllUpdatesAsync` on thread pool | -| 4 | If `autoUpdate` and updates found → `ApplyDiscoveredUpdatesAsync` on thread pool | -| — | On exception: debug log only (no user toast) | - ---- - ## Module refresh #### `private async Task RefreshModulesAsync()` @@ -753,8 +739,7 @@ Resolves path via **`ResolveSidebarIconFile`**; **`PackagedIconTintCache.Get(pat | `DownloadsButton` | `IconDownload` | | `SettingsButton` | `IconSettings` | | `ClassId="PAGE"` + `ModuleConfig` | `SidebarIconFullPath` if file exists, else `IconPage` | -| Other | -ull` | +| Other | `null` | --- diff --git a/Docs/ASLM/content/docs/ASLM/Pages/LoadingPage.md b/Docs/ASLM/content/docs/ASLM/Pages/LoadingPage.md index f87b12e..ffd7350 100644 --- a/Docs/ASLM/content/docs/ASLM/Pages/LoadingPage.md +++ b/Docs/ASLM/content/docs/ASLM/Pages/LoadingPage.md @@ -33,7 +33,7 @@ Layout: centered `VerticalStackLayout` on `BackgroundPrimary`. ## Constructor -#### `LoadingPage(AppDataStore, NotificationCenter, UpdateScheduler, AslmApiServer, AslmModuleInteropServer, ThemeService, CustomThemesStore, ModuleTrustService, AppLocalizationService, IServiceProvider)` +#### `LoadingPage(AppDataStore, NotificationCenter, GitHubRateLimitStore, GitHubUpdateClient, UpdateScheduler, AslmApiServer, AslmModuleInteropServer, ThemeService, CustomThemesStore, ModuleTrustService, AppLocalizationService, IServiceProvider)` **Purpose:** Calls `InitializeComponent()`, stores services, **`LocalizableAttach.Hook(this, _localization, this)`**. @@ -60,8 +60,10 @@ Layout: centered `VerticalStackLayout` on `BackgroundPrimary`. | 5 | `_notifications.InitializeAsync()` | background | | 6 | `_apiServer.StartIfEnabledAsync()` | background | | 7 | `_moduleInteropServer.EnsureStartedAsync()` | background | -| 8 | `_updateScheduler.Start()` | background | -| 9 | `_themeService.ApplyFromSettings()` | UI | +| 8 | `_rateLimitStore.InitializeAsync()` | background | +| 9 | `_githubUpdateClient.RefreshRateLimitAsync()` | background | +| 10 | `_updateScheduler.Start()` | background | +| 11 | `_themeService.ApplyFromSettings()` | UI | **Navigation:** diff --git a/Docs/ASLM/content/docs/ASLM/Pages/SetupWizardPage.md b/Docs/ASLM/content/docs/ASLM/Pages/SetupWizardPage.md index 213b5ad..15a7dea 100644 --- a/Docs/ASLM/content/docs/ASLM/Pages/SetupWizardPage.md +++ b/Docs/ASLM/content/docs/ASLM/Pages/SetupWizardPage.md @@ -13,7 +13,7 @@ draft: false | Name | Value | | --- | --- | -| `TotalSteps` | `3` | Numbered steps (excludes welcome step `0`) | +| `TotalSteps` | `3` (Numbered steps, excludes welcome step `0`) | --- @@ -82,6 +82,7 @@ Three-row grid: **header** | **content** | **footer**. --- ## Member reference + #### `private async void OnLoaded(object? sender, EventArgs e)` **Purpose:** Once: **`PopulateModuleListAsync()`**, set **`_skipDockerStep`** from **`DockerService.IsCliInstalledAsync()`**. diff --git a/Docs/ASLM/content/docs/ASLM/Services/GitHubRateLimitStore.md b/Docs/ASLM/content/docs/ASLM/Services/GitHubRateLimitStore.md new file mode 100644 index 0000000..d403afe --- /dev/null +++ b/Docs/ASLM/content/docs/ASLM/Services/GitHubRateLimitStore.md @@ -0,0 +1,108 @@ +--- +title: "GitHubRateLimitStore" +draft: false +--- + +## Class `GitHubRateLimitStore` + +`ASLM/Services/GitHubRateLimitStore.cs` — **`public sealed`** — Loads and saves GitHub API usage in `Data/App/ASLM_GitHubRateLimit.json`. This service helps manage rate limit budgeting across application restarts. + +**Example Usage:** + +```csharp +// Store is typically injected via DI +public class UpdateService +{ + private readonly GitHubRateLimitStore _rateLimitStore; + + public UpdateService(GitHubRateLimitStore rateLimitStore) + { + _rateLimitStore = rateLimitStore; + } + + public async Task CheckUpdatesAsync() + { + if (_rateLimitStore.CanMakeAutoRequest()) + { + // Proceed with automatic update check + _rateLimitStore.RecordRequest("https://api.github.com/...", GitHubRequestTypes.Releases, GitHubRequestSources.Auto, 200); + } + else + { + var delay = _rateLimitStore.CalculateInterCheckDelay(); + // Wait before next check + } + } +} +``` + +--- + +### Constructor + +#### `public GitHubRateLimitStore(ILogger logger)` + +**Purpose:** Creates the store and resolves the persisted data file path. + +- **Parameters:** `logger` - Logger instance for dependency injection. + +--- + +### Properties + +| Name | Type | Description | +| --- | --- | --- | +| `Data` | `GitHubRateLimitData` | Gets the current persisted GitHub rate-limit state. | + +--- + +### Methods + +#### `public Task InitializeAsync()` + +**Purpose:** Initializes the store by loading persisted data once at startup. + +#### `public Task LoadAsync()` + +**Purpose:** Loads persisted GitHub usage data or recreates defaults when the file is missing or invalid. + +#### `public void UpdateFromHeaders(int limit, int remaining, long resetEpochSeconds)` + +**Purpose:** Updates the known GitHub rate-limit window from response headers and saves the data. + +- **Parameters:** + - `limit` - The maximum requests allowed in the current window. + - `remaining` - The requests remaining in the current window. + - `resetEpochSeconds` - Unix epoch timestamp when the current window resets. + +#### `public void RecordRequest(string url, string type, string source, int? statusCode)` + +**Purpose:** Records one GitHub API request and persists the updated history. + +- **Parameters:** + - `url` - The requested URL. + - `type` - The request type (e.g., releases, download). + - `source` - The request source (e.g., auto, manual). + - `statusCode` - The HTTP response status code. + +#### `public bool CanMakeAutoRequest()` + +**Purpose:** Returns whether automatic update checks still have budget in the current window. + +- **Returns:** `true` if the count of automatic requests is less than half the known limit. + +#### `public int GetAutoRequestsRemaining()` + +**Purpose:** Returns how many automatic requests remain in the current window budget (half the total limit). + +#### `public TimeSpan GetDelayUntilReset()` + +**Purpose:** Returns the remaining time until the GitHub rate-limit window resets. + +#### `public TimeSpan CalculateInterCheckDelay()` + +**Purpose:** Calculates the delay before the next automatic update check request based on the remaining budget and time until reset. Returns a timespan between 5 seconds and 30 minutes. + +#### `public Task SaveAsync()` + +**Purpose:** Saves the current GitHub usage data asynchronously to disk. diff --git a/Docs/ASLM/content/docs/ASLM/Services/GitHubUpdateClient.md b/Docs/ASLM/content/docs/ASLM/Services/GitHubUpdateClient.md index bb18fd6..62708a0 100644 --- a/Docs/ASLM/content/docs/ASLM/Services/GitHubUpdateClient.md +++ b/Docs/ASLM/content/docs/ASLM/Services/GitHubUpdateClient.md @@ -5,7 +5,7 @@ draft: false ## Class `GitHubUpdateClient` -`ASLM/Services/GitHubUpdateClient.cs` — **`public sealed`** — GitHub REST API for ASLM update checks: releases, branches, file and zipball downloads. In-memory caches (5-minute TTL per repo). +`ASLM/Services/GitHubUpdateClient.cs` — **`public sealed`** — GitHub REST API for ASLM update checks: releases, branches, file and zipball downloads. In-memory caches (5-minute TTL per repo). Manages rate limit metadata via `GitHubRateLimitStore`. Branch results use [GitHubBranchInfo](../Models/UpdateModels/) from `ASLM/Models/UpdateModels.cs`. @@ -36,22 +36,39 @@ Types in same file: **`CacheEntry`**, **`GitHubReleaseInfo`**, **`GitHubRelea ## Public methods -#### `public GitHubUpdateClient()` +#### `public GitHubUpdateClient(GitHubRateLimitStore rateLimitStore)` **Purpose:** Creates the GitHub client with headers required by the REST API. **Steps:** -1. User-Agent: `ASLM-Updater`. -2. Accept: `application/vnd.github+json`. -3. `X-GitHub-Api-Version`: `2022-11-28`. +1. Stores `_rateLimitStore` dependency. +2. User-Agent: `ASLM-Updater`. +3. Accept: `application/vnd.github+json`. +4. `X-GitHub-Api-Version`: `2022-11-28`. --- -#### `public async Task GetLatestReleaseAsync(string repo, bool includePrerelease, CancellationToken ct = default)` +#### `public async Task RefreshRateLimitAsync(string requestSource = GitHubRequestSources.Auto, CancellationToken ct = default)` + +**Purpose:** Refreshes the persisted GitHub rate-limit window from the dedicated API endpoint. + +**Steps:** + +1. Requests `https://api.github.com/rate_limit`. +2. Calls `ApplyRateLimitHeaders` and records the request. +3. Deserializes `GitHubRateLimitPayload` and updates `_rateLimitStore` from core limit metadata. + +#### `public async Task GetLatestReleaseAsync(string repo, bool includePrerelease, string requestSource = GitHubRequestSources.Auto, CancellationToken ct = default)` **Purpose:** Returns the most recent release allowed by the requested channel. +- **Parameters:** + - `repo`: Repository name. + - `includePrerelease`: Whether to include pre-releases. + - `requestSource`: Source of request, auto or manual. + - `ct`: Cancellation token. + **Steps:** 1. Await **`GetReleasesAsync`**. @@ -59,10 +76,16 @@ Types in same file: **`CacheEntry`**, **`GitHubReleaseInfo`**, **`GitHubRelea --- -#### `public async Task> GetReleasesAsync(string repo, bool includePrerelease, CancellationToken ct = default)` +#### `public async Task> GetReleasesAsync(string repo, bool includePrerelease, string requestSource = GitHubRequestSources.Auto, CancellationToken ct = default)` **Purpose:** Returns the release list allowed by the requested channel. +- **Parameters:** + - `repo`: Repository name. + - `includePrerelease`: Whether to include pre-releases. + - `requestSource`: Source of request, auto or manual. + - `ct`: Cancellation token. + **Steps:** 1. Return empty list if `repo` is null/whitespace. @@ -72,10 +95,15 @@ Types in same file: **`CacheEntry`**, **`GitHubReleaseInfo`**, **`GitHubRelea --- -#### `public Task> GetBranchesAsync(string repo, CancellationToken ct = default)` +#### `public Task> GetBranchesAsync(string repo, string requestSource = GitHubRequestSources.Auto, CancellationToken ct = default)` **Purpose:** Returns all repository branch names with their current commit SHA. +- **Parameters:** + - `repo`: Repository name. + - `requestSource`: Source of request, auto or manual. + - `ct`: Cancellation token. + **Steps:** 1. Return empty list if `repo` is null/whitespace. @@ -83,10 +111,18 @@ Types in same file: **`CacheEntry`**, **`GitHubReleaseInfo`**, **`GitHubRelea --- -#### `public async Task DownloadFileAsync(string url, string destinationPath, IProgress? log = null, IProgress? downloadProgress = null, CancellationToken ct = default)` +#### `public async Task DownloadFileAsync(string url, string destinationPath, IProgress? log = null, IProgress? downloadProgress = null, string requestSource = GitHubRequestSources.Auto, CancellationToken ct = default)` **Purpose:** Downloads one URL to the requested file path with throttled progress. +- **Parameters:** + - `url`: Target URL. + - `destinationPath`: Output path. + - `log`: Progress log. + - `downloadProgress`: Download progress sink. + - `requestSource`: Source of request, auto or manual. + - `ct`: Cancellation token. + **Steps:** 1. Create destination parent directory; log URL. @@ -96,10 +132,19 @@ Types in same file: **`CacheEntry`**, **`GitHubReleaseInfo`**, **`GitHubRelea --- -#### `public Task DownloadRepositoryZipAsync(string repo, string reference, string destinationPath, IProgress? log = null, IProgress? downloadProgress = null, CancellationToken ct = default)` +#### `public Task DownloadRepositoryZipAsync(string repo, string reference, string destinationPath, IProgress? log = null, IProgress? downloadProgress = null, string requestSource = GitHubRequestSources.Auto, CancellationToken ct = default)` **Purpose:** Downloads a repository ZIP archive for the requested ref. +- **Parameters:** + - `repo`: Repository name. + - `reference`: Branch or ref. + - `destinationPath`: Output path. + - `log`: Progress log. + - `downloadProgress`: Download progress sink. + - `requestSource`: Source of request, auto or manual. + - `ct`: Cancellation token. + **Steps:** 1. Build URL via **`BuildApiUrl(repo, $"zipball/{Uri.EscapeDataString(reference)}")`**. diff --git a/Docs/ASLM/content/docs/ASLM/Services/UpdateManager.md b/Docs/ASLM/content/docs/ASLM/Services/UpdateManager.md index d0640b9..4c838aa 100644 --- a/Docs/ASLM/content/docs/ASLM/Services/UpdateManager.md +++ b/Docs/ASLM/content/docs/ASLM/Services/UpdateManager.md @@ -34,43 +34,85 @@ draft: false --- -#### `public async Task CheckAppUpdateAsync( CancellationToken ct = default, bool publishUpdateNotification = true)` +#### `public async Task CheckAppUpdateAsync( CancellationToken ct = default, bool publishUpdateNotification = true, bool isManualRequest = false)` -**Purpose:** When false, skips publishing an update-available notification (used when auto-updates will apply immediately). +**Purpose:** Checks for ASLM updates. + +- **Parameters:** + - `ct`: Cancellation token. + - `publishUpdateNotification`: When false, skips publishing an update-available notification (used when auto-updates will apply immediately). + - `isManualRequest`: Indicates if the request was triggered manually by the user, adjusting rate limit handling. --- -#### `public async Task CheckModuleUpdateAsync( ModuleConfig module, CancellationToken ct = default, bool publishUpdateNotification = true)` +#### `public async Task CheckModuleUpdateAsync( ModuleConfig module, CancellationToken ct = default, bool publishUpdateNotification = true, bool isManualRequest = false)` + +**Purpose:** Checks for ASLM updates. -**Purpose:** When false, skips publishing an update-available notification (used when auto-updates will apply immediately). +- **Parameters:** + - `ct`: Cancellation token. + - `publishUpdateNotification`: When false, skips publishing an update-available notification (used when auto-updates will apply immediately). + - `isManualRequest`: Indicates if the request was triggered manually by the user, adjusting rate limit handling. --- -#### `public async Task ResolveModuleInstallCandidateAsync( ModuleConfig module, CancellationToken ct = default)` +#### `public async Task ResolveModuleInstallCandidateAsync( ModuleConfig module, CancellationToken ct = default, bool isManualRequest = false)` **Purpose:** Resolves the concrete install target that should be used for a module during setup. +- **Parameters:** + - `module`: Module config. + - `ct`: Cancellation token. + - `isManualRequest`: Indicates if the request was manually initiated. + --- -#### `public async Task> GetModuleReleaseCandidatesAsync( ModuleConfig module, CancellationToken ct = default)` +#### `public async Task> GetModuleReleaseCandidatesAsync( ModuleConfig module, CancellationToken ct = default, bool isManualRequest = false)` -**Purpose:** --- +**Purpose:** Gets module release candidates. -#### `public async Task> CheckAllUpdatesAsync( CancellationToken ct = default, bool publishUpdateNotifications = true)` +- **Parameters:** + - `module`: The module configuration. + - `ct`: Cancellation token. + - `isManualRequest`: Indicates if the request was manually initiated. -When false, skips publishing update-available notifications for every discovery (used when auto-updates will apply immediately). +#### `public async Task> CheckAllUpdatesAsync( CancellationToken ct = default, bool publishUpdateNotifications = true, bool isManualRequest = false)` + +**Purpose:** Checks all updates. + +- **Parameters:** + - `ct`: Cancellation token. + - `publishUpdateNotifications`: When false, skips publishing update-available notifications for every discovery (used when auto-updates will apply immediately). + - `isManualRequest`: Indicates if the request was manually initiated. --- -#### `public async Task ApplyDiscoveredUpdatesAsync( IReadOnlyList updates, IProgress? log, CancellationToken ct = default)` +#### `public async Task ApplyDiscoveredUpdatesAsync( IReadOnlyList updates, IProgress? log, CancellationToken ct = default, bool isManualRequest = false)` **Purpose:** ASLM self-update prepared for the next launcher start when not already staged. +- **Parameters:** + - `updates`: Updates to apply. + - `log`: Progress log. + - `ct`: Cancellation token. + - `isManualRequest`: Indicates if the request was manually initiated. + --- -#### `public Task> GetModuleBranchesAsync(ModuleConfig module, CancellationToken ct = default)` +#### `public Task> DiscoverInstalledModulesAsync()` -**Purpose:** --- +**Purpose:** Returns every installed module manifest for background update scheduling. + +--- + +#### `public Task> GetModuleBranchesAsync(ModuleConfig module, CancellationToken ct = default, bool isManualRequest = false)` + +**Purpose:** Gets module branches. + +- **Parameters:** + - `module`: The module configuration. + - `ct`: Cancellation token. + - `isManualRequest`: Indicates if the request was manually initiated. #### `public void SaveModuleUpdatePreferences(ModuleConfig module)` @@ -84,16 +126,30 @@ Saves one module manifest after update preferences changed in UI. --- -#### `public async Task PrepareAppUpdateAsync( UpdateCandidate candidate, IProgress? log = null, IProgress? progress = null, CancellationToken ct = default)` +#### `public async Task PrepareAppUpdateAsync( UpdateCandidate candidate, IProgress? log = null, IProgress? progress = null, bool isManualRequest = false, CancellationToken ct = default)` **Purpose:** Downloads and stages an ASLM app update for the external patcher. +- **Parameters:** + - `candidate`: Update candidate. + - `log`: Progress log. + - `progress`: Download progress. + - `isManualRequest`: Indicates if the request was manually initiated. + - `ct`: Cancellation token. + --- -#### `public async Task ApplyModuleUpdateAsync( UpdateCandidate candidate, IProgress? log = null, IProgress? progress = null, CancellationToken ct = default)` +#### `public async Task ApplyModuleUpdateAsync( UpdateCandidate candidate, IProgress? log = null, IProgress? progress = null, bool isManualRequest = false, CancellationToken ct = default)` **Purpose:** Downloads and applies one module update. +- **Parameters:** + - `candidate`: Update candidate. + - `log`: Progress log. + - `progress`: Download progress. + - `isManualRequest`: Indicates if the request was manually initiated. + - `ct`: Cancellation token. + --- ## Private methods diff --git a/Docs/ASLM/content/docs/ASLM/Services/UpdateScheduler.md b/Docs/ASLM/content/docs/ASLM/Services/UpdateScheduler.md index c7698b3..bcdf9bb 100644 --- a/Docs/ASLM/content/docs/ASLM/Services/UpdateScheduler.md +++ b/Docs/ASLM/content/docs/ASLM/Services/UpdateScheduler.md @@ -7,7 +7,7 @@ draft: false `ASLM/Services/UpdateScheduler.cs` — **`public sealed`** — background loop for periodic update checks and optional auto-apply per [AppDataStore](AppDataStore/) **`Updates`** settings. -**DI:** `AddSingleton()` — [UpdateManager](UpdateManager/). +**DI:** `AddSingleton()` — [UpdateManager](UpdateManager/), [GitHubRateLimitStore](GitHubRateLimitStore/). Implements **`IDisposable`**. @@ -19,12 +19,14 @@ Implements **`IDisposable`**. | --- | --- | | `StartupDelay` | 15 seconds | | `FailureRetryDelay` | 15 minutes | +| `IdlePollDelay` | 1 minute | +| `BudgetExhaustedPadding` | 10 seconds | --- ## Public methods -#### `public UpdateScheduler(AppDataStore appData, UpdateManager updateManager, ILogger logger)` +#### `public UpdateScheduler(AppDataStore appData, UpdateManager updateManager, GitHubRateLimitStore rateLimitStore, ILogger logger)` **Purpose:** Stores dependencies. @@ -52,13 +54,13 @@ Implements **`IDisposable`**. #### `private async Task RunAsync(CancellationToken ct)` -**Purpose:** After **`StartupDelay`**: loop **`RunDueCheckAsync`**, delay **`GetSchedulerDelay`** from settings (re-read each pass), on failure log and wait **`FailureRetryDelay`**. +**Purpose:** After **`StartupDelay`**: loop **`RunSchedulerPassAsync`**, delay using returned time (re-read each pass), on failure log and wait **`FailureRetryDelay`**. --- -#### `private async Task RunDueCheckAsync(CancellationToken ct)` +#### `private async Task RunSchedulerPassAsync(CancellationToken ct)` -**Purpose:** When **`CheckEnabled`** and **`IsDue`**: stamp **`LastAutoCheckUtc`**, **`AppDataStore.SaveAsync`**, **`UpdateManager.CheckAllUpdatesAsync`** (notifications when auto-update off). When **`AutoUpdateEnabled`** and updates found: **`ApplyDiscoveredUpdatesAsync`** with logged progress. +**Purpose:** Executes one scheduler pass and returns the delay before the next pass. Populates check queue via `UpdateManager.DiscoverInstalledModulesAsync()`, respects `GitHubRateLimitStore.CanMakeAutoRequest()`, and processes items sequentially via `ProcessNextItemAsync`. --- diff --git a/Docs/ASLM/content/docs/ASLM/Tests/Services/GitHubUpdateClientTests.md b/Docs/ASLM/content/docs/ASLM/Tests/Services/GitHubUpdateClientTests.md index 4b63419..690c755 100644 --- a/Docs/ASLM/content/docs/ASLM/Tests/Services/GitHubUpdateClientTests.md +++ b/Docs/ASLM/content/docs/ASLM/Tests/Services/GitHubUpdateClientTests.md @@ -44,6 +44,12 @@ draft: false --- +#### `private static GitHubUpdateClient CreateClient()` + +**Purpose:** Creates a `GitHubUpdateClient` with a new `GitHubRateLimitStore` using a `NullLogger`. + +--- + ## Related - [GitHubUpdateClient](../../Services/GitHubUpdateClient/)