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
@@ -84,8 +84,10 @@ public class AttributeAccessor
///
/// <para>
/// <b>Quality-agnostic by default (spec §4.2):</b> matching tests the VALUE, not
/// the quality — a value arriving at Bad quality still satisfies the wait. A
/// quality-gated ("Good"-only) mode is a planned enhancement, deferred per spec §4.2.
/// the quality — a value arriving at Bad quality still satisfies the wait. Pass
/// <paramref name="requireGoodQuality"/><c>:true</c> for quality-gated ("Good"-only)
/// matching: a value reaching the target at Bad/Uncertain quality is ignored and
/// the wait holds until the target is reached at "Good" quality (or times out).
/// </para>
///
/// <para>
@@ -100,9 +102,13 @@ public class AttributeAccessor
/// "match on any change" (matches immediately if the attribute already has a value).
/// </param>
/// <param name="timeout">How long to wait before returning false.</param>
/// <param name="requireGoodQuality">
/// <c>true</c> for quality-gated ("Good"-only) matching (spec §4.2); defaults to
/// <c>false</c> (quality-agnostic — Bad/Uncertain-quality transients still match).
/// </param>
/// <returns><c>true</c> on match within the timeout; <c>false</c> on timeout.</returns>
public Task<bool> WaitAsync(string key, object? targetValue, TimeSpan timeout)
=> _ctx.WaitAttribute(Resolve(key), AttributeValueCodec.Encode(targetValue), null, timeout);
public Task<bool> WaitAsync(string key, object? targetValue, TimeSpan timeout, bool requireGoodQuality = false)
=> _ctx.WaitAttribute(Resolve(key), AttributeValueCodec.Encode(targetValue), null, timeout, requireGoodQuality);
/// <summary>
/// WaitForAttribute (spec §3-§5): predicate form — waits event-driven until
@@ -114,16 +120,60 @@ public class AttributeAccessor
/// <para>
/// <b>Quality-agnostic by default (spec §4.2):</b> the predicate is tested against
/// the VALUE, regardless of quality — a value arriving at Bad quality still
/// satisfies the wait if the predicate passes. A quality-gated ("Good"-only) mode
/// is a planned enhancement, deferred per spec §4.2.
/// satisfies the wait if the predicate passes. Pass <paramref name="requireGoodQuality"/>
/// <c>:true</c> for quality-gated ("Good"-only) matching: a value satisfying the
/// predicate at Bad/Uncertain quality is ignored until it does so at "Good" quality.
/// </para>
/// </summary>
/// <param name="key">The attribute key (scope-resolved before the wait is registered).</param>
/// <param name="predicate">The site-local predicate tested against the current value.</param>
/// <param name="timeout">How long to wait before returning false.</param>
/// <param name="requireGoodQuality">
/// <c>true</c> for quality-gated ("Good"-only) matching (spec §4.2); defaults to
/// <c>false</c> (quality-agnostic).
/// </param>
/// <returns><c>true</c> on match within the timeout; <c>false</c> on timeout.</returns>
public Task<bool> WaitAsync(string key, Func<object?, bool> predicate, TimeSpan timeout)
=> _ctx.WaitAttribute(Resolve(key), null, predicate, timeout);
public Task<bool> WaitAsync(string key, Func<object?, bool> predicate, TimeSpan timeout, bool requireGoodQuality = false)
=> _ctx.WaitAttribute(Resolve(key), null, predicate, timeout, requireGoodQuality);
/// <summary>
/// WaitForAttribute (spec §3): richer value-equality form — like
/// <see cref="WaitAsync(string, object?, TimeSpan, bool)"/> but returns the full
/// <see cref="WaitResult"/> (matched flag + matched value + quality + timed-out
/// flag) instead of a bare bool. Scope/composition path resolution
/// (<see cref="Resolve"/>) is applied to <paramref name="key"/> just like the
/// other accessors. Never throws on timeout — a timeout yields
/// <c>WaitResult { Matched = false, TimedOut = true }</c>.
/// </summary>
/// <param name="key">The attribute key (scope-resolved before the wait is registered).</param>
/// <param name="targetValue">
/// The value to wait for (codec-encoded for comparison); <c>null</c> means
/// "match on any change".
/// </param>
/// <param name="timeout">How long to wait before returning a timed-out result.</param>
/// <param name="requireGoodQuality">
/// <c>true</c> for quality-gated ("Good"-only) matching (spec §4.2); defaults to <c>false</c>.
/// </param>
/// <returns>The full <see cref="WaitResult"/> for the wait.</returns>
public Task<WaitResult> WaitForAsync(string key, object? targetValue, TimeSpan timeout, bool requireGoodQuality = false)
=> _ctx.WaitAttributeFull(Resolve(key), AttributeValueCodec.Encode(targetValue), null, timeout, requireGoodQuality);
/// <summary>
/// WaitForAttribute (spec §3): richer predicate form — like
/// <see cref="WaitAsync(string, Func{object?, bool}, TimeSpan, bool)"/> but returns
/// the full <see cref="WaitResult"/>. Site-local only (the predicate is an
/// in-process delegate). Scope/composition path resolution applies. Never throws
/// on timeout (<c>WaitResult { Matched = false, TimedOut = true }</c>).
/// </summary>
/// <param name="key">The attribute key (scope-resolved before the wait is registered).</param>
/// <param name="predicate">The site-local predicate tested against the current value.</param>
/// <param name="timeout">How long to wait before returning a timed-out result.</param>
/// <param name="requireGoodQuality">
/// <c>true</c> for quality-gated ("Good"-only) matching (spec §4.2); defaults to <c>false</c>.
/// </param>
/// <returns>The full <see cref="WaitResult"/> for the wait.</returns>
public Task<WaitResult> WaitForAsync(string key, Func<object?, bool> predicate, TimeSpan timeout, bool requireGoodQuality = false)
=> _ctx.WaitAttributeFull(Resolve(key), null, predicate, timeout, requireGoodQuality);
}
/// <summary>
@@ -422,31 +422,75 @@ public class ScriptRuntimeContext
/// </param>
/// <param name="predicate">Site-local predicate; null when the encoded target is used.</param>
/// <param name="timeout">How long to wait before returning false.</param>
/// <param name="requireGoodQuality">
/// Quality-gated ("Good"-only) mode (spec §4.2): when <see langword="true"/>, a
/// value reaching the target / satisfying the predicate at Bad/Uncertain quality
/// is NOT a match — the wait holds until the value satisfies the test at Good
/// quality (or times out). Defaults to <see langword="false"/> (quality-agnostic).
/// </param>
/// <returns><c>true</c> on match within the timeout; <c>false</c> on timeout.</returns>
public async Task<bool> WaitAttribute(
string name, string? targetValueEncoded, Func<object?, bool>? predicate, TimeSpan timeout)
string name, string? targetValueEncoded, Func<object?, bool>? predicate, TimeSpan timeout,
bool requireGoodQuality = false)
=> (await WaitInternal(name, targetValueEncoded, predicate, timeout, requireGoodQuality)).Matched;
/// <summary>
/// WaitForAttribute (spec §3): the richer overload backing <c>Attributes.WaitForAsync</c>
/// — identical semantics to <see cref="WaitAttribute"/> but surfaces the full
/// <see cref="WaitResult"/> (matched flag + matched value + quality + timed-out
/// flag) instead of a bare bool. Never throws on timeout (see <see cref="WaitInternal"/>).
/// </summary>
/// <param name="name">The scope-resolved attribute name to wait on.</param>
/// <param name="targetValueEncoded">The codec-encoded target value; null (with null predicate) means "any change".</param>
/// <param name="predicate">Site-local predicate; null when the encoded target is used.</param>
/// <param name="timeout">How long to wait before returning a timed-out result.</param>
/// <param name="requireGoodQuality">Quality-gated ("Good"-only) mode (spec §4.2); defaults to <see langword="false"/>.</param>
/// <returns>The full <see cref="WaitResult"/> — on timeout: <c>Matched:false, TimedOut:true</c>.</returns>
public async Task<WaitResult> WaitAttributeFull(
string name, string? targetValueEncoded, Func<object?, bool>? predicate, TimeSpan timeout,
bool requireGoodQuality = false)
{
var r = await WaitInternal(name, targetValueEncoded, predicate, timeout, requireGoodQuality);
return new WaitResult(r.Matched, r.Value, r.Quality, r.TimedOut);
}
/// <summary>
/// Shared core for <see cref="WaitAttribute"/> / <see cref="WaitAttributeFull"/>:
/// builds the <see cref="WaitForAttributeRequest"/> (incl. the §4.2
/// <paramref name="requireGoodQuality"/> flag), Asks the InstanceActor bounded by
/// the script's execution-timeout token, and returns the full response. An
/// <see cref="AskTimeoutException"/> (the pathological case where the actor's own
/// authoritative timeout reply never arrives — actor stopped/restarted) is caught
/// and surfaced as a synthetic non-matched/timed-out response, preserving the
/// "never throw on timeout" contract. An <see cref="OperationCanceledException"/> /
/// <see cref="TaskCanceledException"/> from the script-deadline token is NOT caught
/// — it propagates to abort the script (§4.3).
/// </summary>
private async Task<WaitForAttributeResponse> WaitInternal(
string name, string? targetValueEncoded, Func<object?, bool>? predicate, TimeSpan timeout,
bool requireGoodQuality)
{
var cid = Guid.NewGuid().ToString();
var req = new WaitForAttributeRequest(
cid, _instanceName, name, targetValueEncoded, predicate, timeout, DateTimeOffset.UtcNow);
cid, _instanceName, name, targetValueEncoded, predicate, timeout, DateTimeOffset.UtcNow,
requireGoodQuality);
try
{
var resp = await _instanceActor.Ask<WaitForAttributeResponse>(
return await _instanceActor.Ask<WaitForAttributeResponse>(
req, timeout + _askTimeout, _scriptTimeoutToken);
return resp.Matched;
}
catch (AskTimeoutException)
{
// Pathological: the InstanceActor's own scheduled timeout reply never
// arrived (e.g. the actor stopped/restarted under us). The helper's
// contract is "false on timeout, never throw" — so swallow and return
// false rather than leaking the Ask exception to the script.
// contract is "false on timeout, never throw" — so synthesize a
// non-matched/timed-out response rather than leaking the Ask exception.
// OperationCanceledException / TaskCanceledException from the
// script-deadline token are deliberately NOT caught here: they must
// propagate to abort the script (§4.3).
return false;
return new WaitForAttributeResponse(
cid, Matched: false, null, null, TimedOut: true);
}
}