feat(siteruntime): event-driven Attributes.WaitAsync attribute-change helper

Adds InstanceActor one-shot waiter registry (fast-path + change-match + scheduled
timeout self-eviction), threads per-script timeout token through ScriptRuntimeContext,
and exposes Attributes.WaitAsync(value|predicate, timeout). Replaces handshake busy-poll.
Implements spec docs/plans/2026-06-17-waitfor-attribute-change-helper-spec.md §3-§5;
§6 routed variant + WaitForAsync + quality-only mode deferred.
This commit is contained in:
Joseph Doherty
2026-06-17 08:25:06 -04:00
parent b89d69a008
commit 75ffa09b8f
8 changed files with 731 additions and 4 deletions
@@ -73,6 +73,35 @@ public class AttributeAccessor
/// <returns>A task that represents the asynchronous operation.</returns>
public Task SetAsync(string key, object? value)
=> _ctx.SetAttribute(Resolve(key), AttributeValueCodec.Encode(value) ?? string.Empty);
/// <summary>
/// WaitForAttribute (spec §3-§5): waits event-driven until the attribute equals
/// <paramref name="targetValue"/> (value-equality, codec-normalized), bounded by
/// <paramref name="timeout"/>. Returns <c>true</c> if matched within the timeout,
/// <c>false</c> on timeout (no throw). Honors the script's execution-timeout token.
/// Scope/composition path resolution (<see cref="Resolve"/>) is applied just like
/// <see cref="GetAsync"/> / <see cref="SetAsync"/>.
/// </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).</param>
/// <param name="timeout">How long to wait before returning false.</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);
/// <summary>
/// WaitForAttribute (spec §3-§5): predicate form — waits event-driven until
/// <paramref name="predicate"/> returns <c>true</c> for the attribute's current
/// value, bounded by <paramref name="timeout"/>. Site-local only (the predicate
/// is an in-process delegate). Returns <c>true</c> if matched within the timeout,
/// <c>false</c> on timeout (no throw). Scope/composition path resolution applies.
/// </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>
/// <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);
}
/// <summary>