Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions Assets/Plugins/StreamChat/Core/Configs/IStreamClientConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,21 @@ public interface IStreamClientConfig
/// </summary>
MessageCacheWindow DefaultMessageCacheWindow { get; set; }

/// <summary>
/// When the app goes to the background, temporarily drop the chat connection without logging
/// the user out. When the app returns to the foreground, reconnect and recover missed state.
/// Defaults to <c>true</c>. Set to <c>false</c> to keep the connection alive while backgrounded.
///
/// In the Unity Editor this has no effect — pausing play mode or unfocusing the Game view
/// would otherwise disconnect constantly. A warning is logged once.
///
/// Applies when you create the client with <see cref="StreamChatClient.CreateDefaultClient"/>.
/// If you drive the client yourself (you call Update each frame), pause and resume with
/// <see cref="IStreamChatClient.PauseConnectionAsync"/> /
/// <see cref="IStreamChatClient.ResumeConnectionAsync"/> instead.
/// </summary>
bool DisconnectOnApplicationPause { get; set; }

/// <summary>
/// How the client restores local state after the websocket reconnects.
/// Default is <see cref="Configs.StateRecoveryStrategy.ReplayEvents"/>.
Expand Down
2 changes: 2 additions & 0 deletions Assets/Plugins/StreamChat/Core/Configs/StreamClientConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ public class StreamClientConfig : IStreamClientConfig

public MessageCacheWindow DefaultMessageCacheWindow { get; set; } = null;

public bool DisconnectOnApplicationPause { get; set; } = true;

public StateRecoveryStrategy StateRecoveryStrategy { get; set; } = Configs.StateRecoveryStrategy.ReplayEvents;
}
}
22 changes: 22 additions & 0 deletions Assets/Plugins/StreamChat/Core/IStreamChatClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -366,8 +366,30 @@ Task<StreamDeleteChannelsResponse> DeleteMultipleChannelsAsync(IEnumerable<IStre
/// <param name="timeoutMinutes">Optional timeout. Without timeout users will stay muted indefinitely</param>
Task MuteMultipleUsersAsync(IEnumerable<IStreamUser> users, int? timeoutMinutes = default);

/// <summary>
/// Disconnect the local user and stop automatic reconnects. The next connect is a fresh login,
/// not a reconnect recovery. Use <see cref="PauseConnectionAsync"/> to drop the WebSocket
/// without ending the session.
/// </summary>
Task DisconnectUserAsync();

/// <summary>
/// Temporarily drop the chat connection without logging the user out.
/// Call <see cref="DisconnectUserAsync"/> to sign off. Resume with
/// <see cref="ResumeConnectionAsync"/>. If
/// <see cref="Configs.IStreamClientConfig.DisconnectOnApplicationPause"/> is enabled,
/// <see cref="StreamChatClient.CreateDefaultClient"/> already does this when the app
/// backgrounds and returns.
/// </summary>
Task PauseConnectionAsync();

/// <summary>
/// Reconnect after <see cref="PauseConnectionAsync"/> or after the app was backgrounded.
/// No-op if already connected or connecting. This is not login — use
/// <see cref="ConnectUserAsync"/> to sign in.
/// </summary>
Task ResumeConnectionAsync();

bool IsLocalUser(IStreamUser messageUser);

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ protected Task PostEventAsync(string channelType, string channelId, object event
});

private const int InvalidAuthTokenErrorCode = 40;
#if STREAM_TESTS_ENABLED
private const int TransientSystemErrorMaxAttempts = 5;
#endif

private readonly IHttpClient _httpClient;
private readonly ISerializer _serializer;
Expand Down Expand Up @@ -172,6 +175,16 @@ private async Task<TResponse> HttpRequest<TResponse>(HttpMethodType httpMethod,
return await HandleRateLimit<TResponse>(httpMethod, endpoint, requestBody, queryParameters, attempt,
httpResponse);
}

if (IsTransientSystemError(apiError) && attempt < TransientSystemErrorMaxAttempts)
{
var delaySeconds = GetTransientSystemErrorBackoffSeconds(attempt);
_logs.Warning(
$"API CLIENT, TESTS MODE, HTTP 500 \"{apiError.Message}\" - retry in {delaySeconds}s " +
$"(attempt {attempt + 1}/{TransientSystemErrorMaxAttempts})");
await Task.Delay(delaySeconds * 1000);
return await HttpRequest<TResponse>(httpMethod, endpoint, requestBody, queryParameters, ++attempt);
}
#endif

if (apiError.Code != InvalidAuthTokenErrorCode)
Expand Down Expand Up @@ -244,6 +257,15 @@ private static bool IsRequestBodyRequiredByHttpMethod(HttpMethodType httpMethod)
=> httpMethod == HttpMethodType.Post || httpMethod == HttpMethodType.Put ||
httpMethod == HttpMethodType.Patch;

#if STREAM_TESTS_ENABLED
private static bool IsTransientSystemError(APIErrorInternalDTO apiError)
=> apiError.Code == StreamApiException.InternalSystemErrorStreamCode &&
apiError.StatusCode == StreamApiException.InternalSystemErrorHttpStatusCode;

private static int GetTransientSystemErrorBackoffSeconds(int attempt)
=> (int)Math.Min(16, Math.Pow(2, attempt));
#endif

private void LogFutureRequestIfDebug(Uri uri, string endpoint, HttpMethodType httpMethod, string request = null)
{
#if STREAM_DEBUG_ENABLED
Expand Down
46 changes: 46 additions & 0 deletions Assets/Plugins/StreamChat/Core/LowLevelClient/DisconnectCause.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
namespace StreamChat.Core.LowLevelClient
{
/// <summary>
/// Why the WebSocket was closed. Used by
/// <see cref="IStreamChatLowLevelClient.DisconnectAsync(DisconnectCause)"/> to decide whether the
/// reconnect scheduler stays armed. Logout stops auto-reconnect; every other cause leaves it running.
///
/// Stateful clients should call <see cref="IStreamChatClient.DisconnectUserAsync"/>,
/// <see cref="IStreamChatClient.PauseConnectionAsync"/>, or
/// <see cref="IStreamChatClient.ResumeConnectionAsync"/> instead of this enum.
/// </summary>
public enum DisconnectCause
{
/// <summary>
/// No disconnect has been recorded yet, or the close was not classified.
/// </summary>
Unknown = 0,

/// <summary>
/// <see cref="IStreamChatClient.DisconnectUserAsync"/>. Session ended; the scheduler is stopped
/// until the next <see cref="IStreamChatClient.ConnectUserAsync"/>.
/// </summary>
UserLogout,

/// <summary>
/// <see cref="IStreamChatClient.PauseConnectionAsync"/>. User session is kept; reconnect with
/// <see cref="IStreamChatClient.ResumeConnectionAsync"/> (the scheduler also stays armed).
/// </summary>
ConnectionReleased,

/// <summary>
/// The app was backgrounded. Session is kept; reconnects when the app returns to the foreground.
/// </summary>
ApplicationPause,

/// <summary>
/// Network became unavailable. Scheduler reconnects when the network is back.
/// </summary>
Network,

/// <summary>
/// Server health-check timed out. Scheduler reconnects.
/// </summary>
HealthTimeout,
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System.Diagnostics;

namespace StreamChat.Core.LowLevelClient
{
/// <summary>
/// Elapsed-time source for the per-frame event drain budget. Production uses
/// <see cref="DiagnosticsElapsedStopwatch"/>; tests inject a fake so pacing is deterministic.
/// </summary>
internal interface IElapsedStopwatch
{
void Restart();

double ElapsedMilliseconds { get; }
}

internal sealed class DiagnosticsElapsedStopwatch : IElapsedStopwatch
{
public void Restart() => _stopwatch.Restart();

public double ElapsedMilliseconds => _stopwatch.Elapsed.TotalMilliseconds;

private readonly Stopwatch _stopwatch = new Stopwatch();
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,25 @@ void SetReconnectStrategySettings(ReconnectStrategy reconnectStrategy, float? ex

void ConnectUser(AuthCredentials userAuthCredentials);

Task DisconnectAsync(bool permanent = false);
/// <summary>
/// Close the WebSocket. Pass <see cref="DisconnectCause.UserLogout"/> to stop automatic reconnects;
/// every other cause leaves the scheduler armed.
/// </summary>
Task DisconnectAsync(DisconnectCause cause = DisconnectCause.ConnectionReleased);

/// <summary>
/// Close the WebSocket. <paramref name="permanent"/> <c>true</c> maps to
/// <see cref="DisconnectCause.UserLogout"/>; <c>false</c> maps to
/// <see cref="DisconnectCause.ConnectionReleased"/>.
/// </summary>
[Obsolete("Use DisconnectAsync(DisconnectCause). true maps to UserLogout, false to ConnectionReleased.")]
Task DisconnectAsync(bool permanent);

/// <summary>
/// Fetch missed events via <c>/sync</c> and apply them. The returned task completes when
/// those events have been processed, which may span several <see cref="Update"/> calls
/// after a large catch-up. Keep calling <see cref="Update"/> while awaiting.
/// </summary>
Task FetchAndProcessEventsSinceLastReceivedEvent(IEnumerable<string> channelCids);

/// <summary>
Expand Down
Loading
Loading