Skip to content
Merged
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
5 changes: 5 additions & 0 deletions packages/stream_chat/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
- Added support for predefined filters for `QueryChannels` on `StreamChatClient` (`StreamChatClient.queryChannels` and `StreamChatClient.queryChannelsWithResult`).
- Added `ChatPersistenceClient.queryChannelStates` and `ChatPersistenceClient.saveChannelQueries` as the unified read/write methods for channel-query persistence. Both accept standard and predefined-filter parameters and internally dispatch.
- Added an `upsert` flag to `ChannelClientState.updateMessage` and `ChannelClientState.updateThreadInfo` (defaults to `true`). Pass `false` to update a message only if it's already loaded in the state, skipping unknown messages instead of adding them.
- Added `StreamChatNetworkError.type` (a `StreamChatNetworkErrorType` capturing the transport failure kind — connection error, timeout, cancellation, etc.).

🔄 Changed

Expand All @@ -14,10 +15,14 @@
⚠️ Deprecated

- Deprecated `ChatPersistenceClient.getChannelStates` and `ChatPersistenceClient.updateChannelQueries` in favor of the unified `queryChannelStates` / `saveChannelQueries`. The deprecated methods stay overridable so downstream subclasses keep working unchanged.
- Deprecated `StreamChatNetworkError.isRequestCancelledError` in favor of `type == StreamChatNetworkErrorType.cancel`.

🐞 Fixed

- Fixed a deprecation warning from `equatable` causing CI analysis to fail.
- Fixed `StreamWebSocketError.toString()` using a `WebSocketError(...)` prefix instead of the class name; it now also includes `code`, and `StreamChatNetworkError.toString()` now surfaces the transport `type` when known.
- Fixed `Channel.name`/`image`/`extraData` setters throwing after a *failed* initialization; they now only throw once the channel is successfully initialized.
- Fixed `Channel.initialized` staying errored after a failed init; it now reflects a subsequent successful (re)initialization.
- `ComparableField` now folds diacritics/ligatures and ignores case when comparing strings, so `SortOption` on fields like `name` no longer pushes lowercase or non-ASCII names (`jhon`, `Łukasz`, `Øystein`) to the end of client-sorted lists ([#2601](https://github.com/GetStream/stream-chat-flutter/issues/2601)).
- Fixed `message.updated` and soft `message.deleted` events being incorrectly upserted into `ChannelState.messages` (and thread reply lists) when they targeted a message outside the currently loaded window.

Expand Down
24 changes: 18 additions & 6 deletions packages/stream_chat/lib/src/client/channel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ class Channel {
///
/// {@macro name}
set name(String? name) {
if (_initializedCompleter.isCompleted) {
if (_isInitialized) {
throw StateError(
'Once the channel is initialized you should use `channel.updateName` '
'to update the channel name',
Expand All @@ -124,7 +124,7 @@ class Channel {
///
/// {@macro image}
set image(String? image) {
if (_initializedCompleter.isCompleted) {
if (_isInitialized) {
throw StateError(
'Once the channel is initialized you should use `channel.updateImage` '
'to update the channel image',
Expand All @@ -134,7 +134,7 @@ class Channel {
}

set extraData(Map<String, Object?> extraData) {
if (_initializedCompleter.isCompleted) {
if (_isInitialized) {
throw StateError(
'Once the channel is initialized you should use `channel.update` '
'to update channel data',
Expand Down Expand Up @@ -518,14 +518,17 @@ class Channel {
StreamChatClient get client => _client;
final StreamChatClient _client;

final Completer<bool> _initializedCompleter = Completer();
Completer<bool> _initializedCompleter = Completer();

/// True if this is initialized.
///
/// Call [watch] to initialize the client or instantiate it using
/// [Channel.fromState].
Future<bool> get initialized => _initializedCompleter.future;

// Whether the channel is successfully initialized and not disposed.
bool get _isInitialized => _initializedCompleter.isCompleted && state != null;

final _cancelableAttachmentUploadRequest = <String, CancelToken>{};
final _messageAttachmentsUploadCompleter = <String, Completer<Message>>{};

Expand Down Expand Up @@ -659,7 +662,8 @@ class Channel {
);
}
}).catchError((e, stk) {
if (e is StreamChatNetworkError && e.isRequestCancelledError) {
if (e is StreamChatNetworkError &&
e.type == StreamChatNetworkErrorType.cancel) {
client.logger.info('Attachment ${it.id} upload cancelled');

// remove attachment from message if cancelled.
Expand Down Expand Up @@ -1830,6 +1834,14 @@ class Channel {
PaginationParams? watchersPagination,
bool preferOffline = false,
}) async {
// A prior failed init left the completer errored; reset it so this attempt
// owns the `initialized` result. Must stay before the first `await` so a
// caller reading `initialized` right after `query()`/`watch()` begins sees
// the fresh completer.
if (_initializedCompleter.isCompleted && this.state == null) {
_initializedCompleter = Completer<bool>();
}

ChannelState? channelState;

try {
Expand Down Expand Up @@ -2162,7 +2174,7 @@ class Channel {
}

void _checkInitialized() {
if (_initializedCompleter.isCompleted && state != null) return;
if (_isInitialized) return;

throw StateError(
"Channel $cid hasn't been initialized yet or has been disposed. "
Expand Down
133 changes: 105 additions & 28 deletions packages/stream_chat/lib/src/core/error/stream_chat_error.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,22 @@ import 'package:equatable/equatable.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:web_socket_channel/web_socket_channel.dart';

/// Base class for all errors surfaced by the Stream Chat SDK.
///
/// See also:
///
/// * [StreamChatNetworkError], raised by failed HTTP requests.
/// * [StreamWebSocketError], raised on the realtime connection.
// `EquatableMixin` is deprecated in equatable ≥2.1.0 in favour of
// `with Equatable`, but switching would change the mixin type in the class
// hierarchy (e.g. `error is EquatableMixin` → false), which is a breaking
// change under semver. Migrate this when the next major version is cut.
// ignore: deprecated_member_use
class StreamChatError with EquatableMixin implements Exception {
///
/// Creates a new [StreamChatError] with the given [message].
const StreamChatError(this.message);

/// Error message
/// A human-readable description of what went wrong.
final String message;

@override
Expand All @@ -24,43 +29,43 @@ class StreamChatError with EquatableMixin implements Exception {
String toString() => 'StreamChatError(message: $message)';
}

///
/// An error received over the realtime (WebSocket) connection.
class StreamWebSocketError extends StreamChatError {
///
/// Creates a new [StreamWebSocketError] with the given [message].
const StreamWebSocketError(
super.message, {
this.data,
});

///
/// Creates a [StreamWebSocketError] from a Stream error payload.
factory StreamWebSocketError.fromStreamError(Map<String, Object?> error) {
final data = ErrorResponse.fromJson(error);
final message = data.message ?? '';
return StreamWebSocketError(message, data: data);
}

///
/// Creates a [StreamWebSocketError] from a [WebSocketChannelException].
factory StreamWebSocketError.fromWebSocketChannelError(
WebSocketChannelException error,
) {
final message = error.message ?? '';
return StreamWebSocketError(message);
}

///
/// The structured error returned by the server, if any.
final ErrorResponse? data;

/// The Stream error code, if one was provided.
int? get code => data?.code;

///
/// The [ChatErrorCode] for this error, or null if unrecognised.
ChatErrorCode? get errorCode {
final code = this.code;
if (code == null) return null;
return chatErrorCodeFromCode(code);
}

/// Response body. please refer to [ErrorResponse].
final ErrorResponse? data;

///
/// Whether the operation can be retried.
bool get isRetriable => data == null;

@override
Expand All @@ -69,37 +74,44 @@ class StreamWebSocketError extends StreamChatError {
@override
String toString() {
var params = 'message: $message';
if (code case final code?) params = 'code: $code, $params';
if (data != null) params += ', data: $data';
return 'WebSocketError($params)';
return 'StreamWebSocketError($params)';
}
}

///
/// An error raised when a network request to Stream fails.
class StreamChatNetworkError extends StreamChatError {
///
/// Creates a [StreamChatNetworkError] for a known [errorCode].
StreamChatNetworkError(
ChatErrorCode errorCode, {
int? statusCode,
this.data,
StackTrace? stacktrace,
this.isRequestCancelledError = false,
@Deprecated('Set type to StreamChatNetworkErrorType.cancel instead')
bool? isRequestCancelledError,
this.type = StreamChatNetworkErrorType.unknown,
}) : code = errorCode.code,
statusCode = statusCode ?? data?.statusCode,
stackTrace = stacktrace ?? StackTrace.current,
_isRequestCancelledError = isRequestCancelledError,
super(errorCode.message);

///
/// Creates a [StreamChatNetworkError] from raw values.
StreamChatNetworkError.raw({
required this.code,
required String message,
this.statusCode,
this.data,
StackTrace? stacktrace,
this.isRequestCancelledError = false,
@Deprecated('Set type to StreamChatNetworkErrorType.cancel instead')
bool? isRequestCancelledError,
this.type = StreamChatNetworkErrorType.unknown,
}) : stackTrace = stacktrace ?? StackTrace.current,
_isRequestCancelledError = isRequestCancelledError,
super(message);

///
/// Creates a [StreamChatNetworkError] from a [DioException].
factory StreamChatNetworkError.fromDioException(DioException exception) {
final response = exception.response;
ErrorResponse? errorResponse;
Expand All @@ -118,37 +130,49 @@ class StreamChatNetworkError extends StreamChatError {
statusCode: errorResponse?.statusCode ?? response?.statusCode,
data: errorResponse,
stacktrace: exception.stackTrace,
isRequestCancelledError: exception.type == DioExceptionType.cancel,
type: _networkErrorTypeFromDio(exception.type),
);
}

/// Error code
/// The Stream error code. See [ChatErrorCode].
final int code;

/// HTTP status code
/// The HTTP status code of the response, if any.
final int? statusCode;

/// Response body. please refer to [ErrorResponse].
/// The structured error returned by the server, if any.
final ErrorResponse? data;

/// True, in case the error is due to a cancelled network request.
final bool isRequestCancelledError;
/// The kind of transport failure that caused this error.
///
/// Defaults to [StreamChatNetworkErrorType.unknown] when it can't be
/// determined.
final StreamChatNetworkErrorType type;

/// The optional stack trace attached to the error.
final StackTrace? stackTrace;

///
/// Whether the request was cancelled before it completed.
@Deprecated('Use type == StreamChatNetworkErrorType.cancel instead')
bool get isRequestCancelledError =>
_isRequestCancelledError ?? type == StreamChatNetworkErrorType.cancel;
final bool? _isRequestCancelledError;

/// The [ChatErrorCode] for this error, or null if unrecognised.
ChatErrorCode? get errorCode => chatErrorCodeFromCode(code);

///
/// Whether the operation can be retried.
bool get isRetriable => data == null;

@override
List<Object?> get props => [...super.props, code, statusCode];
List<Object?> get props => [...super.props, code, statusCode, type];

@override
String toString({bool printStackTrace = false}) {
var params = 'code: $code, message: $message';
if (type != StreamChatNetworkErrorType.unknown) {
params += ', type: ${type.name}';
}
if (statusCode != null) params += ', statusCode: $statusCode';
if (data != null) params += ', data: $data';
var msg = 'StreamChatNetworkError($params)';
Expand All @@ -159,3 +183,56 @@ class StreamChatNetworkError extends StreamChatError {
return msg;
}
}

/// The kind of transport failure that caused a [StreamChatNetworkError].
///
/// Lets callers tell a lost connection apart from a timeout, a cancellation,
/// or a server response — for example, to show a tailored error message.
enum StreamChatNetworkErrorType {
/// The server could not be reached (e.g. no internet connection).
connectionError,

/// Opening the connection timed out.
connectionTimeout,

/// Sending the request timed out.
sendTimeout,

/// Receiving the response timed out.
receiveTimeout,

/// Transforming the response (e.g. background JSON decoding) timed out.
transformTimeout,

/// The server responded with a non-success status.
badResponse,

/// The request was cancelled before it completed.
cancel,

/// The connection's certificate could not be validated.
badCertificate,

/// The failure could not be attributed to a specific transport cause.
unknown,
}

StreamChatNetworkErrorType _networkErrorTypeFromDio(DioExceptionType type) {
return switch (type) {
DioExceptionType.connectionError =>
StreamChatNetworkErrorType.connectionError,
DioExceptionType.connectionTimeout =>
StreamChatNetworkErrorType.connectionTimeout,
DioExceptionType.sendTimeout => StreamChatNetworkErrorType.sendTimeout,
DioExceptionType.receiveTimeout =>
StreamChatNetworkErrorType.receiveTimeout,
DioExceptionType.transformTimeout =>
StreamChatNetworkErrorType.transformTimeout,
DioExceptionType.badResponse => StreamChatNetworkErrorType.badResponse,
DioExceptionType.cancel => StreamChatNetworkErrorType.cancel,
DioExceptionType.badCertificate =>
StreamChatNetworkErrorType.badCertificate,
// DioExceptionType.unknown and any future dio types map to unknown.
_ => StreamChatNetworkErrorType.unknown,
};
}
Loading
Loading