diff --git a/Realtime/RealtimeChannel.cs b/Realtime/RealtimeChannel.cs
index 4d12390..f02782b 100644
--- a/Realtime/RealtimeChannel.cs
+++ b/Realtime/RealtimeChannel.cs
@@ -541,21 +541,53 @@ public Push Push(string eventName, string? type = null, object? payload = null,
///
public Task Send(ChannelEventName eventName, string? type, object payload, int timeoutMs = DefaultTimeout)
{
- var tsc = new TaskCompletionSource();
+ var push = Push(Core.Helpers.GetMappedToAttr(eventName).Mapping, type, payload, timeoutMs);
+ // The server only sends a `phx_reply` for a `broadcast` push when the channel joined
+ // with `config.broadcast.ack = true`. Without that, no reply will ever arrive, so
+ // waiting for one would hang forever - resolve as soon as the push has been dispatched.
+ return eventName == ChannelEventName.Broadcast && BroadcastOptions?.BroadcastAck != true
+ ? Task.FromResult(true)
+ : PushAwaiter.Await(push);
+ }
+
+ ///
+ /// Awaits a reply for a given , resolving with whether a known reply was
+ /// received, or faulting with a if the push times out first.
+ ///
+ /// Kept as instance methods (rather than local lambdas referencing each other) so there's no
+ /// self/mutually-referencing closure over locals that get assigned after the closure is created.
+ ///
+ private sealed class PushAwaiter
+ {
+ private readonly Push push;
+ private readonly TaskCompletionSource taskCompletion = new();
+
+ private PushAwaiter(Push push)
+ {
+ this.push = push;
+ this.push.AddMessageReceivedHandler(HandleMessageReceived);
+ this.push.OnTimeout += HandleTimeout;
+ }
- var ev = Core.Helpers.GetMappedToAttr(eventName).Mapping;
- var push = Push(ev, type, payload, timeoutMs);
+ public static Task Await(Push push) => new PushAwaiter(push).taskCompletion.Task;
- IRealtimePush.MessageEventHandler? messageCallback = null;
+ private void HandleMessageReceived(IRealtimePush sender, SocketResponse message)
+ {
+ Detach();
+ taskCompletion.TrySetResult(message.Event != EventType.Unknown);
+ }
- messageCallback = (_, message) =>
+ private void HandleTimeout(object sender, EventArgs e)
{
- tsc.SetResult(message.Event != EventType.Unknown);
- push.RemoveMessageReceivedHandler(messageCallback!);
- };
+ Detach();
+ taskCompletion.TrySetException(new RealtimeException("Push Timeout") { Reason = FailureHint.Reason.PushTimeout });
+ }
- push.AddMessageReceivedHandler(messageCallback);
- return tsc.Task;
+ private void Detach()
+ {
+ push.RemoveMessageReceivedHandler(HandleMessageReceived);
+ push.OnTimeout -= HandleTimeout;
+ }
}
///
diff --git a/RealtimeTests/ChannelBroadcastTests.cs b/RealtimeTests/ChannelBroadcastTests.cs
index aa8b3b6..35cc481 100644
--- a/RealtimeTests/ChannelBroadcastTests.cs
+++ b/RealtimeTests/ChannelBroadcastTests.cs
@@ -37,7 +37,7 @@ public void CleanupTest()
_socketClient!.Disconnect();
}
- [TestMethod("Channel: Can listen for broadcast")]
+ [TestMethod(DisplayName = "Channel: Can listen for broadcast")]
public async Task ClientCanListenForBroadcast()
{
var tsc = new TaskCompletionSource();
@@ -75,7 +75,24 @@ public async Task ClientCanListenForBroadcast()
await Task.WhenAll(new[] { tsc.Task, tsc2.Task });
}
- [TestMethod("Channel: Payload returns a modeled response (if possible)")]
+ [TestMethod(DisplayName = "Channel: Send resolves when broadcast ack is not explicitly enabled")]
+ public async Task ChannelSendResolvesWithoutExplicitAck()
+ {
+ // Mirrors the most natural usage: `client.Channel(name)` -> `Subscribe()` -> `Send()`,
+ // with no call to `Register(broadcastAck: true)`. See supabase-community/realtime-csharp#38.
+ var channel = _socketClient!.Channel("no-ack-broadcast");
+ await channel.Subscribe();
+
+ var sendTask = channel.Send(Supabase.Realtime.Constants.ChannelEventName.Broadcast, "test_event",
+ new BroadcastExample { UserId = Guid.NewGuid().ToString() });
+
+ var winner = await Task.WhenAny(sendTask, Task.Delay(TimeSpan.FromSeconds(5)));
+
+ Assert.AreSame(sendTask, winner, "channel.Send() did not complete within 5s (issue #38: awaited broadcast send hangs).");
+ Assert.IsTrue(await sendTask);
+ }
+
+ [TestMethod(DisplayName = "Channel: Payload returns a modeled response (if possible)")]
public async Task ChannelPayloadReturnsModel()
{
var tsc = new TaskCompletionSource();