feat(infra): Docker-compose configuration and environment-specific security guards for Beta deployment to Test environment #56

Merged
mjasin merged 12 commits from infra/beta-deploy-test into develop 2026-06-01 17:17:46 +00:00
2 changed files with 19 additions and 7 deletions
Showing only changes of commit 5fbeaf28db - Show all commits
+1 -1
View File
3
@@ -59,7 +59,7 @@ services:
volumes: volumes:
- qdrant_test_data:/qdrant/storage - qdrant_test_data:/qdrant/storage
healthcheck: healthcheck:
test: ["CMD-SHELL", "bash -c 'exec 3<>/dev/tcp/127.0.0.1/6333'"] test: ["CMD-SHELL", "curl -sf http://localhost:6333/healthz || exit 1"]
interval: 5s interval: 5s
timeout: 5s timeout: 5s
retries: 5 retries: 5
@@ -141,6 +141,7 @@
{ {
if (firstRender) if (firstRender)
{ {
_selfReference = DotNetObjectReference.Create(this);
await SyncService.InitializeAsync(); await SyncService.InitializeAsync();
_isInteractive = true; _isInteractive = true;
if (ViewModel != null) if (ViewModel != null)
@@ -173,11 +174,13 @@
try try
{ {
var module = await EnsureViewportModuleAsync(); var module = await EnsureViewportModuleAsync();
_selfReference = DotNetObjectReference.Create(this);
var isMobileViewport = await module.InvokeAsync<bool>("isMobileViewport"); var isMobileViewport = await module.InvokeAsync<bool>("isMobileViewport");
await OnViewportChanged(isMobileViewport); await OnViewportChanged(isMobileViewport);
if (_selfReference != null)
{
await module.InvokeVoidAsync("registerViewportObserver", _selfReference); await module.InvokeVoidAsync("registerViewportObserver", _selfReference);
} }
}
catch (Exception ex) catch (Exception ex)
{ {
Logger.LogWarning(ex, "Failed to initialize viewport detection in ReaderCanvas."); Logger.LogWarning(ex, "Failed to initialize viewport detection in ReaderCanvas.");
@@ -199,7 +202,10 @@
try try
{ {
var module = await JS.InvokeAsync<IJSObjectReference>("import", "./_content/NexusReader.UI.Shared/js/selectionHandler.js"); var module = await JS.InvokeAsync<IJSObjectReference>("import", "./_content/NexusReader.UI.Shared/js/selectionHandler.js");
await module.InvokeVoidAsync("initSelectionListener", DotNetObjectReference.Create(this), _containerRef); if (_selfReference != null)
{
await module.InvokeVoidAsync("initSelectionListener", _selfReference, _containerRef);
mjasin marked this conversation as resolved Outdated
Outdated
Review

🟡 Design — DotNetObjectReference created inside InitializeObserverAsync without being stored or disposed

In InitializeObserverAsync, a new DotNetObjectReference.Create(this) is passed to initObserver. This reference is not assigned to a field, so it cannot be disposed in DisposeAsync. The unmanaged JS side will hold a reference to the GC'd .NET object indefinitely, which is a memory leak.

The same pattern was correctly fixed for _selfReference (for the viewport observer). Apply the same treatment here — create a second _observerSelfReference field, assign it, and dispose it in DisposeAsync:

// At field level:
private DotNetObjectReference<ReaderCanvas>? _observerSelfReference;

// In InitializeObserverAsync:
_observerSelfReference = DotNetObjectReference.Create(this);
await module.InvokeVoidAsync("initObserver", _observerSelfReference, ...);

// In DisposeAsync:
_observerSelfReference?.Dispose();
🟡 **Design — `DotNetObjectReference` created inside `InitializeObserverAsync` without being stored or disposed** In `InitializeObserverAsync`, a new `DotNetObjectReference.Create(this)` is passed to `initObserver`. This reference is not assigned to a field, so it cannot be disposed in `DisposeAsync`. The unmanaged JS side will hold a reference to the GC'd .NET object indefinitely, which is a memory leak. The same pattern was correctly fixed for `_selfReference` (for the viewport observer). Apply the same treatment here — create a second `_observerSelfReference` field, assign it, and dispose it in `DisposeAsync`: ```csharp // At field level: private DotNetObjectReference<ReaderCanvas>? _observerSelfReference; // In InitializeObserverAsync: _observerSelfReference = DotNetObjectReference.Create(this); await module.InvokeVoidAsync("initObserver", _observerSelfReference, ...); // In DisposeAsync: _observerSelfReference?.Dispose(); ```
}
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -214,8 +220,11 @@
try try
{ {
var module = await JS.InvokeAsync<IJSObjectReference>("import", "./_content/NexusReader.UI.Shared/js/readerObserver.js"); var module = await JS.InvokeAsync<IJSObjectReference>("import", "./_content/NexusReader.UI.Shared/js/readerObserver.js");
mjasin marked this conversation as resolved Outdated
Outdated
Review

🟡 Design — Still unresolved. Untracked DotNetObjectReference in InitializeObserverAsync.

DotNetObjectReference.Create(this) is called twice inside InitializeObserverAsync (once for initObserver and once already implicitly for initScrollListener via the _scrollListenerReference returned value). The reference passed to initObserver is not stored in a field and therefore cannot be disposed, creating a GC-root memory leak from the JS side.

Create a dedicated _observerSelfReference field:

// Field
private DotNetObjectReference<ReaderCanvas>? _observerSelfReference;

// In InitializeObserverAsync:
_observerSelfReference = DotNetObjectReference.Create(this);
await module.InvokeVoidAsync("initObserver", _observerSelfReference, ".reader-flow-container", ".block-wrapper");

// In DisposeAsync:
_observerSelfReference?.Dispose();
🟡 **Design — Still unresolved. Untracked `DotNetObjectReference` in `InitializeObserverAsync`.** `DotNetObjectReference.Create(this)` is called twice inside `InitializeObserverAsync` (once for `initObserver` and once already implicitly for `initScrollListener` via the `_scrollListenerReference` returned value). The reference passed to `initObserver` is not stored in a field and therefore cannot be disposed, creating a GC-root memory leak from the JS side. Create a dedicated `_observerSelfReference` field: ```csharp // Field private DotNetObjectReference<ReaderCanvas>? _observerSelfReference; // In InitializeObserverAsync: _observerSelfReference = DotNetObjectReference.Create(this); await module.InvokeVoidAsync("initObserver", _observerSelfReference, ".reader-flow-container", ".block-wrapper"); // In DisposeAsync: _observerSelfReference?.Dispose(); ```
await module.InvokeVoidAsync("initObserver", DotNetObjectReference.Create(this), ".reader-flow-container", ".block-wrapper"); if (_selfReference != null)
_scrollListenerReference = await module.InvokeAsync<IJSObjectReference>("initScrollListener", DotNetObjectReference.Create(this), ".reader-flow-container"); {
await module.InvokeVoidAsync("initObserver", _selfReference, ".reader-flow-container", ".block-wrapper");
_scrollListenerReference = await module.InvokeAsync<IJSObjectReference>("initScrollListener", _selfReference, ".reader-flow-container");
}
} }
catch (Exception ex) catch (Exception ex)
{ {
1
@@ -454,7 +463,10 @@
await _scrollListenerReference.DisposeAsync(); await _scrollListenerReference.DisposeAsync();
} }
} }
catch { } catch (Exception ex)
{
Logger.LogDebug(ex, "Teardown of scroll listener reference failed in ReaderCanvas disposal.");
}
_selfReference?.Dispose(); _selfReference?.Dispose();
} }