feat: Ingestion Pipeline Stabilization and WASM Service Proxies (#42)
This PR stabilizes the Nexus Ingestion Engine by implementing functional service proxies for the Blazor WASM client and refining the backend infrastructure for real-time progress tracking and database compatibility. ### Key Changes - **Infrastructure Stabilization**: - Implemented production-grade `EbookRepository` with PostgreSQL `EF.Functions.ILike` support. - Enforced `IsReadyForReading = false` state for newly added ebooks (resolves #35). - Updated `SignalRSyncBroadcaster` to support targeted user messaging and ingestion-specific progress updates (resolves #37). - **WASM Client Functional Proxies**: - Replaced "Throwing" dummy services with `WasmEbookRepository`, `WasmSyncBroadcaster`, `WasmBookStorageService`, and `WasmEmbeddingGenerator`. - These services proxy requests to the backend via a new set of Minimal API endpoints in `NexusReader.Web`. - **Domain Refinement**: - Added `IsReadyForReading` flag to the `Ebook` entity to manage background AI processing states. ### Related Issues - Fixes #35 - Fixes #36 - Fixes #37 --------- Co-authored-by: Marek Jasiński <jasins.marek@gmail.com> Reviewed-on: #42 Co-authored-by: Antigravity <antigravity@google.com> Co-committed-by: Antigravity <antigravity@google.com>
This commit was merged in pull request #42.
This commit is contained in:
@@ -7,7 +7,11 @@ using GeminiDotnet;
|
||||
using GeminiDotnet.Extensions.AI;
|
||||
using NexusReader.Data.Persistence;
|
||||
|
||||
using NexusReader.Application.Abstractions.Messaging;
|
||||
using NexusReader.Application.Abstractions.Persistence;
|
||||
using NexusReader.Application.Abstractions.Services;
|
||||
using NexusReader.Infrastructure.Persistence;
|
||||
using NexusReader.Infrastructure.RealTime;
|
||||
using NexusReader.Infrastructure.Services;
|
||||
using NexusReader.Infrastructure.Configuration;
|
||||
using Polly;
|
||||
@@ -27,12 +31,21 @@ public static class DependencyInjection
|
||||
if (!string.IsNullOrEmpty(pgConnectionString))
|
||||
{
|
||||
services.AddDbContextFactory<AppDbContext>(options =>
|
||||
options.UseNpgsql(pgConnectionString, x => x.UseVector()),
|
||||
ServiceLifetime.Scoped);
|
||||
|
||||
// Also register a scoped DbContext for repositories that need it
|
||||
services.AddDbContext<AppDbContext>(options =>
|
||||
options.UseNpgsql(pgConnectionString, x => x.UseVector()));
|
||||
}
|
||||
else
|
||||
{
|
||||
var sqliteConnectionString = configuration.GetConnectionString("SqliteConnection") ?? "Data Source=nexus.db";
|
||||
services.AddDbContextFactory<AppDbContext>(options =>
|
||||
options.UseSqlite(sqliteConnectionString),
|
||||
ServiceLifetime.Scoped);
|
||||
|
||||
services.AddDbContext<AppDbContext>(options =>
|
||||
options.UseSqlite(sqliteConnectionString));
|
||||
}
|
||||
|
||||
@@ -40,8 +53,6 @@ public static class DependencyInjection
|
||||
services.Configure<StripeSettings>(configuration.GetSection(StripeSettings.SectionName));
|
||||
var aiSettings = configuration.GetSection(AiSettings.SectionName).Get<AiSettings>() ?? new AiSettings();
|
||||
|
||||
Console.WriteLine($"[Infrastructure] AI Configured: Model={aiSettings.Model}, KeyPresent={!string.IsNullOrWhiteSpace(aiSettings.ApiKey) && aiSettings.ApiKey != "PLACEHOLDER"}");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(aiSettings.ApiKey) || aiSettings.ApiKey == "PLACEHOLDER")
|
||||
{
|
||||
Console.WriteLine("[Infrastructure] WARNING: AI API Key is missing or placeholder!");
|
||||
@@ -51,7 +62,7 @@ public static class DependencyInjection
|
||||
{
|
||||
builder.AddRetry(new RetryStrategyOptions
|
||||
{
|
||||
ShouldHandle = new PredicateBuilder().Handle<Exception>(ex =>
|
||||
ShouldHandle = new PredicateBuilder().Handle<Exception>(ex =>
|
||||
ex.Message.Contains("429") || ex.Message.Contains("Too Many Requests") || ex.Message.Contains("quota")),
|
||||
BackoffType = DelayBackoffType.Exponential,
|
||||
UseJitter = true,
|
||||
@@ -60,10 +71,10 @@ public static class DependencyInjection
|
||||
});
|
||||
});
|
||||
|
||||
services.AddChatClient(new GeminiChatClient(new GeminiClientOptions
|
||||
{
|
||||
ApiKey = aiSettings.ApiKey,
|
||||
ModelId = aiSettings.Model
|
||||
services.AddChatClient(new GeminiChatClient(new GeminiClientOptions
|
||||
{
|
||||
ApiKey = aiSettings.ApiKey,
|
||||
ModelId = aiSettings.Model
|
||||
}));
|
||||
|
||||
services.AddEmbeddingGenerator(new GeminiEmbeddingGenerator(new GeminiClientOptions
|
||||
@@ -72,10 +83,20 @@ public static class DependencyInjection
|
||||
ModelId = aiSettings.EmbeddingModel ?? "text-embedding-004"
|
||||
}));
|
||||
|
||||
// Application-layer service implementations
|
||||
services.AddScoped<IKnowledgeService, KnowledgeService>();
|
||||
services.AddTransient<IEpubReader, EpubReaderService>();
|
||||
services.AddTransient<IEpubMetadataExtractor, EpubMetadataExtractor>();
|
||||
services.AddSingleton<IBookStorageService, BookStorageService>();
|
||||
|
||||
// Fix #4: Scoped instead of Singleton — BookStorageService uses path resolution
|
||||
// that is environment-specific and incompatible with Singleton lifetime in MAUI.
|
||||
services.AddScoped<IBookStorageService, BookStorageService>();
|
||||
|
||||
// Fix #1: Ebook repository (scoped, matches AppDbContext lifetime)
|
||||
services.AddScoped<IEbookRepository, EbookRepository>();
|
||||
|
||||
// Fix #2: SignalR broadcaster (scoped, wraps IHubContext which is itself a singleton wrapper)
|
||||
services.AddScoped<ISyncBroadcaster, SignalRSyncBroadcaster>();
|
||||
|
||||
services.AddAuthorizationCore(options =>
|
||||
{
|
||||
@@ -83,7 +104,6 @@ public static class DependencyInjection
|
||||
});
|
||||
|
||||
services.AddScoped<IAuthorizationHandler, ProUserHandler>();
|
||||
|
||||
services.AddScoped<IInfrastructureMarker, InfrastructureMarker>();
|
||||
|
||||
return services;
|
||||
|
||||
Reference in New Issue
Block a user