feat(siteruntime): event-driven Attributes.WriteBatchAndWaitAsync (batched DCL write + trigger + existing WaitForAttribute waiter) + compile mirror

This commit is contained in:
Joseph Doherty
2026-06-17 12:13:02 -04:00
parent e8db6c71a8
commit 0e989c867d
8 changed files with 363 additions and 0 deletions
@@ -74,6 +74,40 @@ public class AttributeAccessor
public Task SetAsync(string key, object? value)
=> _ctx.SetAttribute(Resolve(key), AttributeValueCodec.Encode(value) ?? string.Empty);
/// <summary>
/// Writes a SET of data-sourced attributes to the device in ONE gateway batch
/// round-trip, then writes <paramref name="flagKey"/>=<paramref name="flagValue"/>,
/// then waits EVENT-DRIVEN (reusing the existing <c>WaitForAttribute</c> waiter — not a
/// poll) for <paramref name="responseKey"/> to reach <paramref name="responseValue"/>,
/// bounded by <paramref name="timeout"/>. Replaces N sequential per-attribute writes
/// with one batched call before the wait. All keys are scope-resolved (<see cref="Resolve"/>)
/// and all values codec-encoded just like the other accessors. Returns <c>true</c> if
/// the response was observed within the timeout, <c>false</c> on timeout (no throw on
/// timeout); throws <see cref="System.InvalidOperationException"/> if the batch/trigger
/// write itself fails.
/// </summary>
/// <param name="values">Attribute key → value to batch-write (keys scope-resolved, values codec-encoded).</param>
/// <param name="flagKey">Trigger attribute key written AFTER the batch.</param>
/// <param name="flagValue">Value to write to the trigger.</param>
/// <param name="responseKey">Attribute key to wait on.</param>
/// <param name="responseValue">Target value to wait for (<c>null</c> ⇒ any change).</param>
/// <param name="timeout">How long to wait before returning false.</param>
/// <returns><c>true</c> on response match within the timeout; <c>false</c> on timeout.</returns>
public Task<bool> WriteBatchAndWaitAsync(
IReadOnlyDictionary<string, object?> values, string flagKey, object? flagValue,
string responseKey, object? responseValue, TimeSpan timeout)
{
var encoded = new Dictionary<string, string?>(values.Count);
foreach (var kv in values)
encoded[Resolve(kv.Key)] = AttributeValueCodec.Encode(kv.Value);
return _ctx.WriteBatchAndWait(
encoded,
Resolve(flagKey), AttributeValueCodec.Encode(flagValue),
Resolve(responseKey), AttributeValueCodec.Encode(responseValue),
timeout);
}
/// <summary>
/// WaitForAttribute (spec §3-§5): waits event-driven until the attribute equals
/// <paramref name="targetValue"/> (value-equality, codec-normalized), bounded by
@@ -525,6 +525,44 @@ public class ScriptRuntimeContext
}
}
/// <summary>
/// Writes a SET of data-sourced attributes to the device in ONE DCL batch round-trip,
/// then writes a trigger flag, then waits EVENT-DRIVEN (reusing the existing
/// <see cref="WaitAttribute"/> waiter — not a poll) for <paramref name="responseAttr"/>
/// to reach <paramref name="responseEncoded"/>, bounded by <paramref name="timeout"/>.
/// Replaces N sequential per-attribute writes with a single gateway call before the
/// wait. Throws <see cref="InvalidOperationException"/> if the write/trigger leg fails
/// (resolution error, multi-connection batch, device error); returns the wait result
/// otherwise (true = matched, false = timeout, never throws on timeout).
/// </summary>
/// <param name="encodedValues">Scope-resolved attribute name → codec-encoded value to batch-write.</param>
/// <param name="triggerAttr">Scope-resolved trigger attribute name written AFTER the batch.</param>
/// <param name="triggerEncoded">Codec-encoded value for the trigger.</param>
/// <param name="responseAttr">Scope-resolved attribute to wait on.</param>
/// <param name="responseEncoded">Codec-encoded target value (null ⇒ any change).</param>
/// <param name="timeout">How long to wait for the response before returning false.</param>
/// <returns><c>true</c> on response match within the timeout; <c>false</c> on timeout.</returns>
public async Task<bool> WriteBatchAndWait(
IReadOnlyDictionary<string, string?> encodedValues, string triggerAttr, string? triggerEncoded,
string responseAttr, string? responseEncoded, TimeSpan timeout)
{
var cid = Guid.NewGuid().ToString();
var batchReq = new WriteAttributeBatchRequest(
cid, _instanceName, encodedValues, triggerAttr, triggerEncoded, DateTimeOffset.UtcNow);
// 35s: the InstanceActor's DCL Ask is internally bounded at 30s, so allow a small
// margin so the DCL's own typed failure/timeout reply is the one we observe rather
// than an AskTimeoutException here. Honors the script execution-timeout token.
var batchResp = await _instanceActor.Ask<WriteAttributeBatchResponse>(
batchReq, TimeSpan.FromSeconds(35), _scriptTimeoutToken);
if (!batchResp.Success)
throw new InvalidOperationException(
$"WriteBatchAndWait write failed: {batchResp.ErrorMessage}");
return await WaitAttribute(responseAttr, responseEncoded, null, timeout);
}
/// <summary>
/// Calls a sibling script on the same instance by name (Ask pattern).
/// WP-20: Enforces recursion limit.