@@ -13,7 +13,7 @@ namespace Sendspin.SDK.Client;
1313/// <summary>
1414/// Main Sendspin client that orchestrates connection, handshake, and message handling.
1515/// </summary>
16- public sealed class SendspinClientService : ISendspinClient
16+ public sealed class SendspinClientService : ISendspinClient , IDisposable
1717{
1818 private readonly ILogger < SendspinClientService > _logger ;
1919 private readonly ISendspinConnection _connection ;
@@ -1065,12 +1065,27 @@ private void OnBinaryMessageReceived(object? sender, ReadOnlyMemory<byte> data)
10651065 }
10661066 }
10671067
1068+ /// <summary>
1069+ /// Synchronous dispose — unsubscribes connection events to break the reference
1070+ /// cycle that prevents GC when <see cref="DisposeAsync"/> is not called.
1071+ /// Prefer <see cref="DisposeAsync"/> for full cleanup including async operations.
1072+ /// </summary>
1073+ public void Dispose ( )
1074+ {
1075+ if ( _disposed ) return ;
1076+ _disposed = true ;
1077+
1078+ StopTimeSyncLoop ( ) ;
1079+ UnsubscribeConnectionEvents ( ) ;
1080+ }
1081+
10681082 public async ValueTask DisposeAsync ( )
10691083 {
10701084 if ( _disposed ) return ;
10711085 _disposed = true ;
10721086
10731087 StopTimeSyncLoop ( ) ;
1088+ UnsubscribeConnectionEvents ( ) ;
10741089
10751090 // NOTE: We do NOT dispose _audioPipeline here - it's a shared singleton
10761091 // managed by the DI container. We only stop playback if active.
@@ -1079,10 +1094,13 @@ public async ValueTask DisposeAsync()
10791094 await _audioPipeline . StopAsync ( ) ;
10801095 }
10811096
1097+ await _connection . DisposeAsync ( ) ;
1098+ }
1099+
1100+ private void UnsubscribeConnectionEvents ( )
1101+ {
10821102 _connection . StateChanged -= OnConnectionStateChanged ;
10831103 _connection . TextMessageReceived -= OnTextMessageReceived ;
10841104 _connection . BinaryMessageReceived -= OnBinaryMessageReceived ;
1085-
1086- await _connection . DisposeAsync ( ) ;
10871105 }
10881106}
0 commit comments