namespace AcDream.App.Rendering.Wb;
///
/// Per-resource progress for a teardown which must attempt every independent
/// release even when an earlier release fails. Successful operations are never
/// replayed. A backend which throws after changing ownership reports that fact
/// with so the
/// ledger can preserve the physical outcome while still surfacing the error.
///
internal sealed class RetryableResourceReleaseLedger
{
private readonly Entry[] _entries;
private int _remaining;
private bool _advancing;
public RetryableResourceReleaseLedger(IEnumerable<(string Name, Action Release)> entries)
{
ArgumentNullException.ThrowIfNull(entries);
_entries = entries
.Select(entry => new Entry(entry.Name, entry.Release))
.ToArray();
_remaining = _entries.Length;
}
public bool IsComplete => _remaining == 0;
public int RemainingCount => _remaining;
///
/// Attempts every unfinished operation once. Ordinary exceptions retain
/// the operation for a later attempt; committed-outcome exceptions mark it
/// complete. Either kind remains in the returned diagnostics.
///
public ResourceReleaseAttempt Advance()
{
// Release callbacks may synchronously drain their owner. The active
// top-level pass already owns every unfinished entry; a nested pass
// must not give a failed later entry a second attempt in this frame.
if (_advancing)
return new ResourceReleaseAttempt(0, 0, []);
_advancing = true;
List? failures = null;
int attempted = 0;
int completed = 0;
try
{
for (int i = 0; i < _entries.Length; i++)
{
Entry entry = _entries[i];
if (entry.Completed || entry.Running)
continue;
attempted++;
entry.Running = true;
try
{
entry.Release();
entry.Completed = true;
_remaining--;
completed++;
}
catch (Exception error)
{
bool committed = error is MeshReferenceMutationException
{
MutationCommitted: true,
};
if (committed)
{
entry.Completed = true;
_remaining--;
completed++;
}
(failures ??= []).Add(
new ResourceReleaseFailure(entry.Name, error, committed));
}
finally
{
entry.Running = false;
}
}
}
finally
{
_advancing = false;
}
return new ResourceReleaseAttempt(
attempted,
completed,
failures ?? []);
}
private sealed class Entry
{
public Entry(string name, Action release)
{
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("A resource-release stage needs a name.", nameof(name));
ArgumentNullException.ThrowIfNull(release);
Name = name;
Release = release;
}
public string Name { get; }
public Action Release { get; }
public bool Completed { get; set; }
public bool Running { get; set; }
}
}
internal readonly record struct ResourceReleaseFailure(
string Stage,
Exception Error,
bool MutationCommitted);
internal readonly record struct ResourceReleaseAttempt(
int AttemptedCount,
int CompletedCount,
IReadOnlyList Failures)
{
public bool HasFailures => Failures.Count != 0;
public AggregateException ToException(string message) =>
new(
message,
Failures.Select(failure =>
new InvalidOperationException(
$"Resource-release stage '{failure.Stage}' failed "
+ (failure.MutationCommitted
? "after committing its mutation."
: "before committing its mutation."),
failure.Error)));
}