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
3 changes: 3 additions & 0 deletions Assets/Plugins/StreamChat/Changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,12 @@ Features:

* Add a public StreamApiException constructor (statusCode, code, errorMessage, moreInfo, duration, exceptionFields). StreamApiException is a public, catch-and-branch type (via the StreamApiExceptionExtensions.Is* helpers), but until now it could only be constructed inside the SDK from the internal APIErrorInternalDTO, so integrators could not build one to unit-test their own error handling (e.g. simulating a 403 / code 70 "no access to channels" response). The new constructor maps directly to the type's public properties and keeps APIErrorInternalDTO internal.
* Add IStreamClientConfig.OptimisticMessageInsert (default true). When true (the existing behavior), a message you send is inserted into the local channel state and raised via IStreamChannel.MessageReceived immediately, before the server's message.new echo arrives. Set it to false to skip the optimistic local insert and wait for the server echo instead, so every participant - including the sender - observes messages in the same server-defined order. Useful when consistent cross-client ordering matters more than instant local feedback (e.g. a shared, broadcast-ordered feed).
* Add IStreamChatClient.ChannelsRewatched, raised with the channels whose local state a reconnect re-watch just replaced. A re-watch raises no per-message events for the window it replaced, so handle this to rebuild your UI from IStreamChannel.Messages.
* Add IStreamChatClient.SetRewatchOnReconnectChannelTypes(channelTypes, maxSyncReplayGap). The listed channel types skip the event-by-event /sync replay when the outage exceeded maxSyncReplayGap (default 60s) and are restored by a bounded re-watch instead, reported via ChannelsRewatched. Unset by default, so every channel replays as before. Use it for channel types that only display a latest window, where replaying an hour of backlog event by event ends at the state one re-watch request would have produced.

Fixes:

* Fix watched channels silently staying stale after a reconnect whose /sync catch-up the server refuses as too large ("Too many events to sync", HTTP 400 / code 4 - reachable after a long disconnect on a busy channel, since the ~1000-event limit counts events across every cid in the call). The failure previously reached only the fire-and-forget logger, and the stale sync point was kept so every subsequent reconnect failed identically. The SDK now drops the stale sync point and re-watches every watched channel instead, which is the same full state fetch the initial watch performs. Each channel is restored independently so one failure (e.g. a 403 on a channel deleted while offline) does not abandon the rest.
* Fix the 30-day staleness guard on the reconnect /sync catch-up computing its age backwards (lastEventReceivedAt - now, which is negative for any past timestamp), so the guard never fired and /sync was called even with a LastSyncAt the server rejects. Also removes a dead local that was almost certainly the intended operand.
* TaskUtils.LogIfFailed now logs connectivity/transport failures as warnings instead of errors/exceptions. The SDK fire-and-forgets its connect, reconnect, and state-restore operations through LogIfFailed; when the device is offline these fail with HttpRequestException / WebException / SocketException / IOException / TimeoutException, which the reconnect flow recovers from - so surfacing them at error severity flooded crash/error reporting (Sentry, Bugsnag, etc.) with handled, non-actionable noise. Genuine (non-connectivity) failures still log as exceptions. Complements the connection-attempt-timeout fix from PR #213.

Expand Down
26 changes: 26 additions & 0 deletions Assets/Plugins/StreamChat/Core/IStreamChatClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,32 @@ public interface IStreamChatClient : IDisposable, IStreamChatClientEventsListene
/// </summary>
event ChannelMemberRemovedHandler RemovedFromChannelAsMember;

/// <summary>
/// Channels whose local state was just replaced wholesale by a reconnect re-watch, which
/// raises no per-message events for the window it replaced. A consumer rendering one of
/// these channels must rebuild from <see cref="IStreamChannel.Messages"/> rather than wait
/// for message events, otherwise it keeps showing the rows it had before the disconnect.
/// </summary>
event ChannelsRewatchedHandler ChannelsRewatched;

/// <summary>
/// Opt channel types out of the event-by-event /sync replay that follows a reconnect when
/// the outage was longer than <paramref name="maxSyncReplayGap"/> (default 60 seconds).
/// Channels of these types are restored by a bounded re-watch instead - the same latest-page
/// state fetch the initial watch performs, one request per channel - and reported via
/// <see cref="ChannelsRewatched"/> so consumers can rebuild any UI showing them.
///
/// Use this for channel types whose consumers only ever display a bounded latest window
/// (livestream-style or announcement channels): replaying an hour's backlog event by event
/// costs one handler pass per event and ends with the same visible state the single
/// re-watch request would have produced. Leave types whose consumers need every individual
/// event (e.g. anything persisting full history locally) unlisted; they keep the replay.
///
/// Replaces the previously configured set. Shorter outages always replay.
/// </summary>
void SetRewatchOnReconnectChannelTypes(IEnumerable<ChannelType> channelTypes,
TimeSpan? maxSyncReplayGap = null);

/// <summary>
/// Raised when an <see cref="IStreamThread"/> becomes available locally. Use this to bind
/// per-thread UI and to subscribe to the thread's own events such as
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
using StreamChat.Core.LowLevelClient.API.Internal;
using StreamChat.Core.LowLevelClient.Events;
using StreamChat.Core.LowLevelClient.Models;
using StreamChat.Core.LowLevelClient.Responses;
using StreamChat.Core.Web;
using StreamChat.Libs;
using StreamChat.Libs.AppInfo;
Expand Down Expand Up @@ -456,12 +457,27 @@ public async Task FetchAndProcessEventsSinceLastReceivedEvent(IEnumerable<string

//StreamTodo: according to Android SDK there's an error if there are > 1000 events

var response = await ChannelApi.SyncAsync(new SyncRequest
SyncResponse response;
try
{
ChannelCids = channelCids.ToList(),
LastSyncAt = lastEventReceivedAt,
Watch = true,
});
response = await ChannelApi.SyncAsync(new SyncRequest
{
ChannelCids = channelCids.ToList(),
LastSyncAt = lastEventReceivedAt,
Watch = true,
});
}
catch (StreamApiException e) when (e.IsInputError())
{
// The gap is too large for /sync — more than the ~1000 events the server
// will replay (the StreamTodo above), which a busy channel reaches long before the
// 30-day bound checked above. Drop the sync point so the next reconnect starts from
// a fresh one instead of failing the same way forever, and let the caller re-hydrate
// the channels: the missed events are gone either way, and only a full state fetch
// brings the watched channels back up to date.
_disconnectionLastEventReceivedAt = null;
throw;
}

if (response.Events.Count == 0)
{
Expand Down Expand Up @@ -603,6 +619,22 @@ internal async Task<OwnUserInternalDTO> ConnectUserAsync(string apiKey, string u
/// </summary>
private DateTimeOffset? _disconnectionLastEventReceivedAt;

/// <summary>
/// Age of the reconnect sync point: how long ago the connection actually went quiet, which
/// is the gap the /sync replay would have to bridge. Health check events advance
/// <see cref="_lastEventReceivedAt"/> every ~30s while connected, so this measure is immune
/// to LATE DETECTION of an outage - a mobile OS suspends the process while the app is
/// backgrounded and the dead socket is only noticed on resume, so any stamp taken at the
/// Disconnected transition would measure seconds for an hours-long background. Compares a
/// server-stamped event time against local Now, the same subtraction the 30-day guard in
/// <see cref="FetchAndProcessEventsSinceLastReceivedEvent"/> makes; clock skew only nudges
/// borderline gaps between two correct recovery paths.
/// </summary>
internal TimeSpan? TimeSinceDisconnectSyncPoint
=> _disconnectionLastEventReceivedAt.HasValue
? _timeService.Now - _disconnectionLastEventReceivedAt.Value
: (TimeSpan?)null;

private async Task RefreshAuthTokenFromProvider()
{
#if STREAM_DEBUG_ENABLED
Expand Down
Loading
Loading