feat(siteruntime): WaitForAsync/WaitResult + quality-gated WaitAsync (spec §3, §4.2)

This commit is contained in:
Joseph Doherty
2026-06-17 09:05:12 -04:00
parent 0f6da8a106
commit 61048a4ecf
7 changed files with 463 additions and 23 deletions
@@ -577,10 +577,22 @@ public class InstanceActor : ReceiveActor
// and return WITHOUT registering (no timeout scheduled).
if (_attributes.TryGetValue(req.AttributeName, out var current))
{
// Effective quality used for BOTH the §4.2 quality gate and the match
// reply — the same `?? "Good"` default the reply has always used.
_attributeQualities.TryGetValue(req.AttributeName, out var fastQuality);
var effectiveQuality = fastQuality ?? "Good";
bool fastMatch;
try
{
fastMatch = test(current);
// §4.2 quality gate ANDed with the value test, both INSIDE the guard:
// in quality-gated mode a value already at target but at Bad/Uncertain
// quality is NOT a fast match — it falls through to register + schedule
// the timeout like any other pending waiter (do NOT fast-reply matched).
fastMatch =
(!req.RequireGoodQuality
|| string.Equals(effectiveQuality, "Good", StringComparison.Ordinal))
&& test(current);
}
catch (Exception ex)
{
@@ -595,9 +607,8 @@ public class InstanceActor : ReceiveActor
if (fastMatch)
{
_attributeQualities.TryGetValue(req.AttributeName, out var quality);
replyer.Tell(new WaitForAttributeResponse(
req.CorrelationId, Matched: true, current, quality ?? "Good", TimedOut: false));
req.CorrelationId, Matched: true, current, effectiveQuality, TimedOut: false));
return;
}
}
@@ -617,7 +628,7 @@ public class InstanceActor : ReceiveActor
req.Timeout, Self, new WaitForAttributeTimeout(req.CorrelationId), Self);
_attributeWaiters[req.CorrelationId] =
new PendingWait(req.AttributeName, test, replyer, handle);
new PendingWait(req.AttributeName, test, replyer, handle, req.RequireGoodQuality);
}
/// <summary>
@@ -1101,7 +1112,14 @@ public class InstanceActor : ReceiveActor
bool matched;
try
{
matched = pending.Test(changed.Value);
// §4.2 quality gate ANDed with the value test, both INSIDE the guard:
// in quality-gated mode a value reaching the target at Bad/Uncertain
// quality is NOT a match — the waiter stays pending until it satisfies
// the test at Good quality (or times out).
matched =
(!pending.RequireGoodQuality
|| string.Equals(changed.Quality, "Good", StringComparison.Ordinal))
&& pending.Test(changed.Value);
}
catch (Exception ex)
{
@@ -1410,9 +1428,15 @@ public class InstanceActor : ReceiveActor
/// <param name="Test">The match test (decoded-target equality OR site-local predicate OR any-change).</param>
/// <param name="Replyer">The original sender to reply to on match / timeout.</param>
/// <param name="Timeout">The scheduled timeout handle, canceled on match.</param>
/// <param name="RequireGoodQuality">
/// Quality-gated ("Good"-only) mode (spec §4.2): when <c>true</c>, the resolve
/// loop additionally requires <c>changed.Quality == "Good"</c> before the test
/// can match.
/// </param>
private sealed record PendingWait(
string AttributeName,
Func<object?, bool> Test,
IActorRef Replyer,
ICancelable Timeout);
ICancelable Timeout,
bool RequireGoodQuality);
}