Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,11 @@ public async UniTask SetRealmAsync(URLDomain realm, CancellationToken ct)
{
serverAbout.Clear();

// Local scene development runs against a local realm and must not reach out to
// the Genesis manifest host.
if (!isLocalSceneDevelopment)
worldManifestProvider.PrefetchGenesisManifest(environment, ct);

GenericDownloadHandlerUtils.Adapter<GenericGetRequest, GenericGetArguments> genericGetRequest = webRequestController.GetAsync(new CommonArguments(url), ct, ReportCategory.REALM);
ServerAbout result = await genericGetRequest.OverwriteFromJsonAsync(serverAbout, WRJsonParser.Unity);
localSceneParcels = ParseLocalSceneParcels(result.configurations.localSceneParcels);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ public class WorldManifestProvider
private const string dclWorldName = "dcl.eth";

private WorldManifest? cachedMainManifest;
private UniTask<WorldManifest>? inFlightMainManifest;

// Genesis manifest URLs are compile-time constants, so this runs concurrently with
// the realm /about request instead of waiting on realmName. Idempotent; Preserve()
// lets the Genesis branch await this same task rather than issue a second request.
Comment on lines +32 to +34

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Comment narrates caller/external behavior (CLAUDE.md §11 anti-patterns). "runs concurrently with the realm /about request" and "lets the Genesis branch await this same task" describe what callers do, not what this method guarantees. The caller pattern can change without this code changing, silently making the comment wrong.

Suggested change
// Genesis manifest URLs are compile-time constants, so this runs concurrently with
// the realm /about request instead of waiting on realmName. Idempotent; Preserve()
// lets the Genesis branch await this same task rather than issue a second request.
// Idempotent while a fetch is pending; Preserve()d for multi-await.

public void PrefetchGenesisManifest(DecentralandEnvironment environment, CancellationToken ct) =>
inFlightMainManifest ??= FetchGenesisManifestAsync(environment, ct).Preserve();
Comment on lines +35 to +36

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Stale task after cancellation poisons genesis manifest for the rest of the session.

??= never retries once inFlightMainManifest is set. If the CancellationToken passed to SetRealmAsync fires before the HTTP request completes, FetchGenesisManifestAsync catches OperationCanceledException and returns WorldManifest.Empty. The UniTask completes successfully (status Succeeded) with Empty, cachedMainManifest stays null, and ??= skips every subsequent call — the genesis manifest remains empty for the rest of the session with no recovery path.

This is especially insidious because PrefetchGenesisManifest is called unconditionally in SetRealmAsync (even for world realms). If a world-realm connection is cancelled, the genesis manifest prefetch is also cancelled via the shared ct, poisoning future genesis connections the user never navigated to.

Fix: Guard on cache hit and pending status instead of ??=, so a cancelled/failed fetch is retried with a fresh token.

Suggested change
public void PrefetchGenesisManifest(DecentralandEnvironment environment, CancellationToken ct) =>
inFlightMainManifest ??= FetchGenesisManifestAsync(environment, ct).Preserve();
public void PrefetchGenesisManifest(DecentralandEnvironment environment, CancellationToken ct)
{
if (cachedMainManifest.HasValue) return;
if (inFlightMainManifest.HasValue && inFlightMainManifest.Value.Status == UniTaskStatus.Pending)
return;
inFlightMainManifest = FetchGenesisManifestAsync(environment, ct).Preserve();
}


public WorldManifestProvider(IWebRequestController webRequestController)
{
Expand All @@ -38,7 +45,10 @@ public async UniTask<WorldManifest> FetchWorldManifestAsync(URLDomain assetBundl
try
{
if(MAIN_REALM_NAMES.Contains(realmName))
return await FetchGenesisManifestAsync(environment, ct);
{
PrefetchGenesisManifest(environment, ct);
return await inFlightMainManifest!.Value;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Null-forgiving operator ! without justifying comment (CLAUDE.md nullable reference types). The ! is correct — PrefetchGenesisManifest guarantees inFlightMainManifest is non-null on return — but the rule requires a comment when using !.

Suggested change
return await inFlightMainManifest!.Value;
return await inFlightMainManifest!.Value; // set by PrefetchGenesisManifest above

}

if(realmName.EndsWith(dclWorldName))
return await FetchNonGenesisManifestAsync(assetBundleRegistry, realmName, ct);
Expand Down