| File: Documentation\Docs\DocsCache.cs | Web Access |
| Project: src\src\Aspire.Cli\Aspire.Cli.csproj (aspire) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; namespace Aspire.Cli.Documentation.Docs; /// <summary> /// Cache for aspire.dev documentation content with ETag support. /// Uses both in-memory cache for fast access and disk cache for persistence across CLI invocations. /// </summary> internal sealed class DocsCache : IDocsCache { private const string DocsCacheSubdirectory = "docs"; private readonly FileBackedDocumentContentCache _contentCache; private readonly string _llmsTxtUrl; private readonly string _sourceCacheKey; private readonly string _indexCacheKey; private readonly string _indexSourceFingerprintCacheKey; private readonly string _legacyIndexCacheKey; private readonly string _legacyIndexSourceFingerprintCacheKey; public DocsCache( IMemoryCache memoryCache, CliExecutionContext executionContext, IConfiguration configuration, ILogger<DocsCache> logger) { _llmsTxtUrl = DocsSourceConfiguration.GetLlmsTxtUrl(configuration); _sourceCacheKey = DocsSourceConfiguration.GetContentCacheKey(_llmsTxtUrl); _indexCacheKey = DocsSourceConfiguration.GetIndexCacheKey(_llmsTxtUrl); _indexSourceFingerprintCacheKey = $"{_indexCacheKey}:fingerprint"; _legacyIndexCacheKey = DocsSourceConfiguration.GetLegacyIndexCacheKey(_llmsTxtUrl); _legacyIndexSourceFingerprintCacheKey = $"{_legacyIndexCacheKey}:fingerprint"; _contentCache = new FileBackedDocumentContentCache(memoryCache, executionContext, DocsCacheSubdirectory, logger); } public Task<string?> GetAsync(string key, CancellationToken cancellationToken = default) => _contentCache.GetAsync(key, cancellationToken); public Task SetAsync(string key, string content, CancellationToken cancellationToken = default) => _contentCache.SetAsync(key, content, cancellationToken); public Task<string?> GetETagAsync(string url, CancellationToken cancellationToken = default) => _contentCache.GetETagAsync(url, cancellationToken); public Task SetETagAsync(string url, string? etag, CancellationToken cancellationToken = default) => _contentCache.SetETagAsync(url, etag, cancellationToken); public Task InvalidateAsync(string key, CancellationToken cancellationToken = default) => _contentCache.InvalidateAsync(key, cancellationToken); public async Task<LlmsDocument[]?> GetIndexAsync(CancellationToken cancellationToken = default) { var documents = await _contentCache.GetJsonAsync(_indexCacheKey, JsonSourceGenerationContext.Default.LlmsDocumentArray, cancellationToken: cancellationToken).ConfigureAwait(false); if (documents is not null) { await MigrateLegacyIndexFingerprintAsync(cancellationToken).ConfigureAwait(false); await ClearLegacyCacheAsync(cancellationToken).ConfigureAwait(false); return documents; } if (!HasLegacyIndexCacheKey) { return null; } var legacyDocuments = await _contentCache.GetJsonAsync(_legacyIndexCacheKey, JsonSourceGenerationContext.Default.LlmsDocumentArray, cancellationToken: cancellationToken).ConfigureAwait(false); if (legacyDocuments is null) { return null; } await _contentCache.SetJsonAsync(_indexCacheKey, legacyDocuments, JsonSourceGenerationContext.Default.LlmsDocumentArray, cancellationToken).ConfigureAwait(false); await MigrateLegacyIndexFingerprintAsync(cancellationToken).ConfigureAwait(false); await ClearLegacyCacheAsync(cancellationToken).ConfigureAwait(false); return legacyDocuments; } public async Task SetIndexAsync(LlmsDocument[] documents, CancellationToken cancellationToken = default) { await _contentCache.SetJsonAsync(_indexCacheKey, documents, JsonSourceGenerationContext.Default.LlmsDocumentArray, cancellationToken).ConfigureAwait(false); await ClearLegacyCacheAsync(cancellationToken).ConfigureAwait(false); } public async Task<string?> GetIndexSourceFingerprintAsync(CancellationToken cancellationToken = default) { var fingerprint = await _contentCache.GetAsync(_indexSourceFingerprintCacheKey, cancellationToken).ConfigureAwait(false); if (fingerprint is not null) { await ClearLegacyIndexCacheAsync(cancellationToken).ConfigureAwait(false); return fingerprint; } if (!HasLegacyIndexCacheKey) { return null; } var legacyFingerprint = await _contentCache.GetAsync(_legacyIndexSourceFingerprintCacheKey, cancellationToken).ConfigureAwait(false); if (legacyFingerprint is null) { return null; } await _contentCache.SetAsync(_indexSourceFingerprintCacheKey, legacyFingerprint, cancellationToken).ConfigureAwait(false); await ClearLegacyIndexCacheAsync(cancellationToken).ConfigureAwait(false); return legacyFingerprint; } public async Task SetIndexSourceFingerprintAsync(string fingerprint, CancellationToken cancellationToken = default) { await _contentCache.SetAsync(_indexSourceFingerprintCacheKey, fingerprint, cancellationToken).ConfigureAwait(false); await ClearLegacyIndexCacheAsync(cancellationToken).ConfigureAwait(false); } private bool HasLegacyIndexCacheKey => !string.Equals(_legacyIndexCacheKey, _indexCacheKey, StringComparison.Ordinal); private async Task MigrateLegacyIndexFingerprintAsync(CancellationToken cancellationToken) { if (!HasLegacyIndexCacheKey) { return; } var currentFingerprint = await _contentCache.GetAsync(_indexSourceFingerprintCacheKey, cancellationToken).ConfigureAwait(false); if (currentFingerprint is not null) { return; } var legacyFingerprint = await _contentCache.GetAsync(_legacyIndexSourceFingerprintCacheKey, cancellationToken).ConfigureAwait(false); if (legacyFingerprint is not null) { await _contentCache.SetAsync(_indexSourceFingerprintCacheKey, legacyFingerprint, cancellationToken).ConfigureAwait(false); } } private async Task ClearLegacyCacheAsync(CancellationToken cancellationToken) { await ClearLegacySourceCacheAsync(cancellationToken).ConfigureAwait(false); await ClearLegacyIndexCacheAsync(cancellationToken).ConfigureAwait(false); } private async Task ClearLegacySourceCacheAsync(CancellationToken cancellationToken) { if (string.Equals(_sourceCacheKey, _llmsTxtUrl, StringComparison.Ordinal)) { return; } await _contentCache.InvalidateAsync(_llmsTxtUrl, cancellationToken).ConfigureAwait(false); await _contentCache.SetETagAsync(_llmsTxtUrl, null, cancellationToken).ConfigureAwait(false); } private async Task ClearLegacyIndexCacheAsync(CancellationToken cancellationToken) { if (!HasLegacyIndexCacheKey) { return; } await _contentCache.InvalidateJsonAsync(_legacyIndexCacheKey, cancellationToken).ConfigureAwait(false); await _contentCache.InvalidateAsync(_legacyIndexSourceFingerprintCacheKey, cancellationToken).ConfigureAwait(false); } }