Skip to content

RequestResendAsync

For streaming widgets — today, drawing_canvas — input arrives as a sequence of stroke_progress events with a per-stroke seq number. If the host detects a gap (e.g. seq 12 followed by seq 15), it can ask the phone to replay the missing events from its in-memory buffer.

RequestResendAsync does exactly that, with a positive ACK — the phone tells you whether it can replay or not, so you don’t sit indefinitely waiting for events that will never come.

Two variants

// Fire-and-forget. Result is delivered via OnResendFailed (or by the events
// themselves arriving on OnInput if the replay succeeds).
void RequestResend(string playerId, string widgetId, int strokeId, int fromSeq);
// Awaitable. Returns a ResendResult so you know up-front whether to expect events.
Task<ResendResult> RequestResendAsync(
string playerId,
string widgetId,
int strokeId,
int fromSeq,
int timeoutMs = 2000,
CancellationToken ct = default);

ResendResult

public enum ResendStatus { Accepted, Rejected, TimedOut }
public record ResendResult
{
public ResendStatus Status;
public int ReplayedCount; // Accepted: how many events the phone is replaying.
public string? RejectReason; // Rejected: BufferExpired | WidgetNotReady | ...
}

Example — gap-fill in a Pictionary host

var result = await manager.RequestResendAsync(
playerId: drawer.Id,
widgetId: "canvas",
strokeId: 7,
fromSeq: 12,
timeoutMs: 2000);
switch (result.Status)
{
case ResendStatus.Accepted:
// Phone is replaying — events arrive on OnInput like any other stroke_progress.
Debug.Log($"Replaying {result.ReplayedCount} events");
break;
case ResendStatus.Rejected:
// RejectReason will be one of: BufferExpired (phone pruned it),
// WidgetNotReady (canvas was unmounted), etc.
AbandonGapSlot(7);
break;
case ResendStatus.TimedOut:
// Neither phone nor relay replied within timeoutMs.
AbandonGapSlot(7);
break;
}

When this matters

Drawing-canvas events are emitted at ~60ms cadence; under spotty cellular, packets can drop. Without resend, the host’s reconstruction of the drawing has gaps. With resend, you get every stroke or a definitive “no, you don’t” answer in ≤ 2 seconds.

For a drawing_canvas host:

  1. Track the per-stroke seq you’ve seen on each stroke_progress.
  2. If you detect seq_n jumping, queue a RequestResendAsync(strokeId, lastSeenSeq + 1).
  3. On Accepted, expect ReplayedCount more events.
  4. On Rejected / TimedOut, give up on that range and continue rendering — typically the player’s stroke will still look fine; the buffer is small.

Pitfalls

  • Don’t await it on the input thread. The replay may take longer than your frame budget. await it inside an async void Update or queue the call as a background task.
  • The phone’s buffer is small. Roughly the last 2 seconds of stroke_progress events. If you ask for older data you’ll get Rejected with BufferExpired.
  • One outstanding resend per (playerId, widgetId, strokeId). A second concurrent call may cancel the first.

Next